Skip to content

Commit

Permalink
generic cache implemented for features
Browse files Browse the repository at this point in the history
  • Loading branch information
akmorrow13 committed Jul 9, 2018
1 parent 3f07ddd commit 2fe8392
Show file tree
Hide file tree
Showing 11 changed files with 455 additions and 58 deletions.
23 changes: 23 additions & 0 deletions src/main/data/genericFeature.js
@@ -0,0 +1,23 @@
/**
* Generic object that can be displayed. This can be a gene, feature or variant, etc.
* See ../viz/GenericFeatureCache.js
* @flow
*/
'use strict';

import ContigInterval from '../ContigInterval';


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

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

module.exports = GenericFeature;
3 changes: 3 additions & 0 deletions src/main/style.js
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
121 changes: 85 additions & 36 deletions src/main/viz/FeatureTrack.js
@@ -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 '../../main/viz/GenericFeatureCache';
import type {VisualGroup} from '../../main/viz/GenericFeatureCache';
import type {DataCanvasRenderingContext2D} from 'data-canvas';

import type {VizProps} from '../VisualizationWrapper';
Expand All @@ -23,16 +25,19 @@ 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;
}

Expand All @@ -42,7 +47,8 @@ class FeatureTiledCanvas extends TiledCanvas {

// 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 +58,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 +72,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.gFeatures[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 +99,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 +109,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 +143,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 +152,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 @@ -148,6 +174,7 @@ class FeatureTrack extends React.Component {
this.updateVisualization();
}

// TODO this is redundant
getScale(): Scale {
return d3utils.getTrackScale(this.props.range, this.props.width);
}
Expand All @@ -163,49 +190,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)); // TODO AM fillRect wrong?

// resize height for device TODO
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
116 changes: 116 additions & 0 deletions src/main/viz/GenericFeatureCache.js
@@ -0,0 +1,116 @@
/**
* Data management for FeatureTrack.
*
* This class groups features and piles them up.
*
* @flow
*/
'use strict';

import _ from 'underscore';
import ContigInterval from '../ContigInterval';
import GenericFeature from '../data/genericFeature.js';

import Interval from '../Interval';
import {addToPileup} from './pileuputils';
import utils from '../utils';
import type {TwoBitSource} from '../sources/TwoBitDataSource';

export type VisualGroup = {
key: string;
span: ContigInterval<string>; // tip-to-tip span
row: number; // pileup row.
gFeatures: GenericFeature[];
};


// This class provides data management for the visualization
class GenericFeatureCache {
// 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;
}

// name would make a good key, but features from different contigs
// shouldn't be grouped visually. Hence we use feature name + contig.
groupKey(f: GenericFeature): string {
return f.id || f.position.toString();
}

// Load a new feature into the visualization cache.
// Calling this multiple times with the same feature is a no-op.
addFeature(feature: GenericFeature) {
var key = this.groupKey(feature);

if (!(key in this.groups)) {
this.groups[key] = {
key: key,
span: feature.position,
row: -1, // TBD
gFeatures: []
};
}
var group = this.groups[key];

if (_.find(group.gFeatures, f => f.gFeature == feature.gFeature)) {
return; // we've already got it.
}

group.gFeatures.push(feature);

if (!(feature.position.contig in this.refToPileup)) {
this.refToPileup[feature.position.contig] = [];
}
var pileup = this.refToPileup[feature.position.contig];
group.row = addToPileup(group.span.interval, pileup);

}

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 track 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 = {
GenericFeatureCache
};

0 comments on commit 2fe8392

Please sign in to comment.