Skip to content

Commit

Permalink
Merge pull request #485 from akmorrow13/generic-pileup
Browse files Browse the repository at this point in the history
Generic pileup
  • Loading branch information
akmorrow13 authored Jul 12, 2018
2 parents 3f07ddd + 3db3003 commit 8934954
Show file tree
Hide file tree
Showing 14 changed files with 514 additions and 128 deletions.
24 changes: 24 additions & 0 deletions src/main/data/genericFeature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Generic object that can be displayed. This can be a gene, feature or variant, etc.
* See ../viz/GenericFeatureCache.js
* @flow
*/
'use strict';

/*jshint unused:false */
import ContigInterval from '../ContigInterval';


class GenericFeature {
id: string;
position: ContigInterval<string>;
gFeature: Object;

constructor(id: string, position: ContigInterval<string>, genericFeature: Object) {
this.id = genericFeature.id;
this.position = genericFeature.position;
this.gFeature = genericFeature;
}
}

module.exports = GenericFeature;
3 changes: 3 additions & 0 deletions src/main/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ module.exports = {
ALIGNMENT_PLUS_STRAND_COLOR: 'rgb(236, 176, 176)',
DELETE_COLOR: 'black',
INSERT_COLOR: 'rgb(97, 0, 216)',
READ_SPACING: 2, // vertical spacing between reads
READ_HEIGHT: 13, // Height of read


// Coverage track
COVERAGE_FONT_STYLE: `bold 9px 'Helvetica Neue', Helvetica, Arial, sans-serif`,
Expand Down
79 changes: 79 additions & 0 deletions src/main/viz/AbstractCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* AbstractCache
* @flow
*/
'use strict';

import _ from 'underscore';
/*jshint unused:false */
import ContigInterval from '../ContigInterval';
import Interval from '../Interval';

import utils from '../utils';

import type {TwoBitSource} from '../sources/TwoBitDataSource';

import type {VisualAlignment} from './PileupCache';
import GenericFeature from '../data/genericFeature.js';


export type VisualGroup<T: (VisualAlignment | GenericFeature)> = {
key: string;
row: number; // pileup row.
span: ContigInterval<string>; // tip-to-tip span for the read group
insert: ?Interval; // interval for the connector, if applicable.
items: T[];
};

class AbstractCache {
// maps groupKey to VisualGroup
groups: {[key: string]: VisualGroup};
refToPileup: {[key: string]: Array<Interval[]>};
referenceSource: TwoBitSource;

constructor(referenceSource: TwoBitSource) {
this.groups = {};
this.refToPileup = {};
this.referenceSource = referenceSource;
}

pileupForRef(ref: string): Array<Interval[]> {
if (ref in this.refToPileup) {
return this.refToPileup[ref];
} else {
var alt = utils.altContigName(ref);
if (alt in this.refToPileup) {
return this.refToPileup[alt];
} else {
return [];
}
}
}

// How many rows tall is the pileup for a given ref? This is related to the
// maximum read depth. This is 'chr'-agnostic.
pileupHeightForRef(ref: string): number {
var pileup = this.pileupForRef(ref);
return pileup ? pileup.length : 0;
}

// Find groups overlapping the range. This is 'chr'-agnostic.
getGroupsOverlapping(range: ContigInterval<string>): VisualGroup[] {
// TODO: speed this up using an interval tree
return _.filter(this.groups, group => group.span.intersects(range));
}

// Determine the number of groups at a locus.
// Like getGroupsOverlapping(range).length > 0, but more efficient.
anyGroupsOverlapping(range: ContigInterval<string>): boolean {
for (var k in this.groups) {
var group = this.groups[k];
if (group.span.intersects(range)) {
return true;
}
}
return false;
}
}

module.exports = AbstractCache;
121 changes: 84 additions & 37 deletions src/main/viz/FeatureTrack.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/**
* Visualization of features, including exons and coding regions.
* Visualization of features.
* @flow
*/
'use strict';

import type {FeatureDataSource} from '../sources/BigBedDataSource';
import type Feature from '../data/feature';

import GenericFeature from '../data/genericFeature';
import {GenericFeatureCache} from './GenericFeatureCache';
import type {VisualGroup} from './AbstractCache';
import type {DataCanvasRenderingContext2D} from 'data-canvas';

import type {VizProps} from '../VisualizationWrapper';
Expand All @@ -23,26 +25,29 @@ import canvasUtils from './canvas-utils';
import TiledCanvas from './TiledCanvas';
import dataCanvas from 'data-canvas';
import style from '../style';
import utils from '../utils';
import type {State, NetworkStatus} from '../types';
import {yForRow} from './pileuputils';


class FeatureTiledCanvas extends TiledCanvas {
options: Object;
source: FeatureDataSource;
cache: GenericFeatureCache;

constructor(source: FeatureDataSource, options: Object) {
constructor(source: FeatureDataSource, cache: GenericFeatureCache, options: Object) {
super();
this.source = source;
this.cache = cache;
this.options = options;
}

update(newOptions: Object) {
this.options = newOptions;
}

// TODO: can update to handle overlapping features
heightForRef(ref: string): number {
return style.VARIANT_HEIGHT;
return this.cache.pileupHeightForRef(ref) *
(style.READ_HEIGHT + style.READ_SPACING);
}

render(ctx: DataCanvasRenderingContext2D,
Expand All @@ -52,7 +57,12 @@ class FeatureTiledCanvas extends TiledCanvas {
resolution: ?number) {
var relaxedRange =
new ContigInterval(range.contig, range.start() - 1, range.stop() + 1);
var vFeatures = this.source.getFeaturesInRange(relaxedRange, resolution);
// get features and put in cache
var features = this.source.getFeaturesInRange(relaxedRange, resolution);
features.forEach(f => this.cache.addFeature(new GenericFeature(f.id, f.position, f)));

// get visual features with assigned rows
var vFeatures = this.cache.getGroupsOverlapping(relaxedRange);
renderFeatures(ctx, scale, relaxedRange, vFeatures);
}
}
Expand All @@ -61,24 +71,25 @@ class FeatureTiledCanvas extends TiledCanvas {
function renderFeatures(ctx: DataCanvasRenderingContext2D,
scale: (num: number) => number,
range: ContigInterval<string>,
features: Feature[]) {
vFeatures: VisualGroup[]) {

ctx.font = `${style.GENE_FONT_SIZE}px ${style.GENE_FONT}`;
ctx.textAlign = 'center';

features.forEach(feature => {
var position = new ContigInterval(feature.position.contig, feature.position.start(), feature.position.stop());
if (!position.intersects(range)) return;
vFeatures.forEach(vFeature => {
var feature = vFeature.items[0].gFeature;
if (!vFeature.span.intersects(range)) return;
ctx.pushObject(feature);
ctx.lineWidth = 1;

// Create transparency value based on score. Score of <= 200 is the same transparency.
var alphaScore = Math.max(feature.score / 1000.0, 0.2);
ctx.fillStyle = 'rgba(0, 0, 0, ' + alphaScore + ')';

var x = Math.round(scale(feature.position.start()));
var width = Math.ceil(scale(feature.position.stop()) - scale(feature.position.start()));
ctx.fillRect(x - 0.5, 0, width, style.VARIANT_HEIGHT);
var x = Math.round(scale(vFeature.span.start()));
var width = Math.ceil(scale(vFeature.span.stop()) - scale(vFeature.span.start()));
var y = yForRow(vFeature.row);
ctx.fillRect(x - 0.5, y, width, style.READ_HEIGHT);
ctx.popObject();
});
}
Expand All @@ -87,6 +98,7 @@ class FeatureTrack extends React.Component {
props: VizProps & { source: FeatureDataSource };
state: State;
tiles: FeatureTiledCanvas;
cache: GenericFeatureCache;

constructor(props: VizProps) {
super(props);
Expand All @@ -96,6 +108,14 @@ class FeatureTrack extends React.Component {
}

render(): any {
// These styles allow vertical scrolling to see the full pileup.
// Adding a vertical scrollbar shrinks the visible area, but we have to act
// as though it doesn't, since adjusting the scale would put it out of sync
// with other tracks.
var containerStyles = {
'height': '100%'
};

var statusEl = null,
networkStatus = this.state.networkStatus;
if (networkStatus) {
Expand All @@ -122,7 +142,7 @@ class FeatureTrack extends React.Component {
return (
<div>
{statusEl}
<div ref='container'>
<div ref='container' style={containerStyles}>
<canvas ref='canvas' onClick={this.handleClick.bind(this)} />
</div>
</div>
Expand All @@ -131,13 +151,18 @@ class FeatureTrack extends React.Component {
}

componentDidMount() {
this.tiles = new FeatureTiledCanvas(this.props.source, this.props.options);
this.cache = new GenericFeatureCache(this.props.referenceSource);
this.tiles = new FeatureTiledCanvas(this.props.source, this.cache, this.props.options);

// Visualize new reference data as it comes in from the network.
this.props.source.on('newdata', (range) => {
this.tiles.invalidateRange(range);
this.updateVisualization();
});
this.props.referenceSource.on('newdata', range => {
this.tiles.invalidateRange(range);
this.updateVisualization();
});
this.props.source.on('networkprogress', e => {
this.setState({networkStatus: e});
});
Expand All @@ -163,49 +188,71 @@ class FeatureTrack extends React.Component {

updateVisualization() {
var canvas = (this.refs.canvas : HTMLCanvasElement),
{width, height} = this.props,
width = this.props.width,
genomeRange = this.props.range;

var range = new ContigInterval(genomeRange.contig, genomeRange.start, genomeRange.stop);

// Hold off until height & width are known.
if (width === 0 || typeof canvas == 'undefined') return;
d3utils.sizeCanvas(canvas, width, height);

var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas));


ctx.reset();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

this.tiles.renderToScreen(ctx, range, this.getScale());
ctx.restore();
// get parent of canvas
// The typecasts through `any` are to fool flow.
var parent = ((d3utils.findParent(canvas, "features") : any) : HTMLCanvasElement);

// Height can only be computed after the pileup has been updated.
var height = yForRow(this.cache.pileupHeightForRef(this.props.range.contig));

// resize height for device
height = d3utils.heightForCanvas(canvas, height);

// set height for parent div to include all features
if (parent) parent.style.height = `${height}px`;

d3utils.sizeCanvas(canvas, width, height);

this.tiles.renderToScreen(ctx, range, this.getScale());
}

handleClick(reactEvent: any) {
var ratio = window.devicePixelRatio;
var ev = reactEvent.nativeEvent,
x = ev.offsetX;
x = ev.offsetX, // resize offset to canvas size
y = ev.offsetY/ratio;

var ctx = canvasUtils.getContext(this.refs.canvas);
var trackingCtx = new dataCanvas.ClickTrackingContext(ctx, x, y);

var genomeRange = this.props.range,
// allow some buffering so click isn't so sensitive
range = new ContigInterval(genomeRange.contig, genomeRange.start-1, genomeRange.stop+1),
scale = this.getScale(),
// leave padding of 2px to reduce click specificity
clickStart = Math.floor(scale.invert(x)) - 2,
clickEnd = clickStart + 2,
// If click-tracking gets slow, this range could be narrowed to one
// closer to the click coordinate, rather than the whole visible range.
vFeatures = this.props.source.getFeaturesInRange(range);
var feature = _.find(vFeatures, f => utils.tupleRangeOverlaps([[f.position.start()], [f.position.stop()]], [[clickStart], [clickEnd]]));
var alert = window.alert || console.log;
vFeatures = this.cache.getGroupsOverlapping(range);

renderFeatures(trackingCtx, scale, range, vFeatures);
var feature = _.find(trackingCtx.hits[0], hit => hit);

if (feature) {
// Construct a JSON object to show the user.
var messageObject = _.extend(
{
'id': feature.id,
'range': `${feature.position.contig}:${feature.position.start()}-${feature.position.stop()}`,
'score': feature.score
});
alert(JSON.stringify(messageObject, null, ' '));
//user provided function for displaying popup
if (typeof this.props.options.onFeatureClicked === "function") {
this.props.options.onFeatureClicked(feature);
} else {
var alert = window.alert || console.log;
// Construct a JSON object to show the user.
var messageObject = _.extend(
{
'id': feature.id,
'range': `${feature.position.contig}:${feature.position.start()}-${feature.position.stop()}`,
'score': feature.score
});
alert(JSON.stringify(messageObject, null, ' '));
}
}
}
}
Expand Down
Loading

0 comments on commit 8934954

Please sign in to comment.