Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualization of genome track #441

Merged
merged 13 commits into from
May 17, 2017
10 changes: 10 additions & 0 deletions examples/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ var sources = [
data: pileup.formats.vcf({
url: '/test-data/snv.chr17.vcf'
}),
options: {
variantHeightByFrequency: true,
onVariantClicked: function(data) {
var content = "Variants:\n";
for (var i =0;i< data.length;i++) {
content +=data[i].id+" - "+data[i].vcfLine+"\n";
}
alert(content);
},
},
name: 'Variants'
},
{
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="../style/pileup.css" />
<link rel="stylesheet" href="demo.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remove this stylesheet since we are not making use of it anymore?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind - let me do that to save some time.

</head>

<body>
Expand Down
1 change: 1 addition & 0 deletions src/main/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class Root extends React.Component {
range={this.state.range}
onRangeChange={this.handleRangeChange.bind(this)}
source={track.source}
options={track.track.options}
referenceSource={this.props.referenceSource}
/>);

Expand Down
4 changes: 3 additions & 1 deletion src/main/VisualizationWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Props = {
onRangeChange: (newRange: GenomeRange) => void;
referenceSource: TwoBitSource;
source: any;
options: ?Object;
};

class VisualizationWrapper extends React.Component {
Expand Down Expand Up @@ -128,14 +129,15 @@ class VisualizationWrapper extends React.Component {
if (!range) {
return <EmptyTrack className={component.displayName} />;
}
var options = _.extend({},this.props.visualization.options,this.props.options);

var el = React.createElement(component, ({
range: range,
source: this.props.source,
referenceSource: this.props.referenceSource,
width: this.state.width,
height: this.state.height,
options: this.props.visualization.options
options: options
} : VizProps));

return <div className='drag-wrapper'>{el}</div>;
Expand Down
28 changes: 28 additions & 0 deletions src/main/data/vcf.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export type Variant = {
position: number;
ref: string;
alt: string;
id: string;
//this is the bigest allel frequency for single vcf entry
//single vcf entry might contain more than one variant like the example below
//20 1110696 rs6040355 A G,T 67 PASS NS=2;DP=10;AF=0.333,0.667;AA=T;DB
majorFrequency: ?number;
//this is the smallest allel frequency for single vcf entry
minorFrequency: ?number;
vcfLine: string;
}

Expand All @@ -41,12 +48,33 @@ function extractLocusLine(vcfLine: string): LocusLine {

function extractVariant(vcfLine: string): Variant {
var parts = vcfLine.split('\t');
var maxFrequency = null;
var minFrequency = null;
if (parts.length>=7){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this is a multi-sample VCF and the first sample is not relevant for the user? I am asking this; because I know that mutect can sometimes order the samples in a way that the normal sample comes before the tumor one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know vcf can contain multiple variants regarding single nucleotide. It can be achieved in two ways:

  1. one entry in vcf can correspond to more than one variant, take a look at this entry:
    20 1234567 microsat1 GTC G,GTCT 50 PASS NS=3;DP=9;AA=G GT:GQ:DP 0/1:35:4 0/2:17:2 1/1:40:3
  2. two entries can refer to the same region in the genome (I think this is what you mentioned in the comment)

Regarding the first issue - there is nothing you can do in term of visualization apart from providing information in the popup. We could think about coloring or some fancy way of highlighting the situation, but in my opinion it's not intuitive.

When you have two (or more) overlaying variants I would also suggest to put it in the popup. I will fix my PR to handle this situation (right now you have only first element that match click in the popup).

If the behaviour is not the one user expected I think the best way to go is suggest user to filter data from vcf file and provide filtered results.

Anyway, this reminds me about one other issue: when you present gene variant it should span through the whole modified region. Right now every variant is visualized by rectangle on the first nucleotide only.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha! I think your solution regarding multiple variants makes sense for the first pass as long as we provide the additional information back to the callback function so users will have a way to know if such is the case.

Another option is to parse the variant header and provide the developer a way to only use information from a particular column. For example the NORMAL TUMOR column names in the example VCF files. AFAIK, those column names are arbitrary, so we can let the developer determine which one we pick for visualization. Do you think this makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't go that far. In most cases users know what data they visualize and when they know that there are two overlapping data sets they should separate them in different tracks (at least this is what I would do).

var params = parts[7].split(';');
for (var i=0;i<params.length;i++) {
var param = params[i];
if (param.startsWith("AF=")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think AF is an almost standard annotation for a majority of the VCF variants out there, but do you know if there are other abbreviations that people use as a synonym? If so, it might worth looking for those.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took it from standard definition: http://samtools.github.io/hts-specs/VCFv4.3.pdf
I'm not aware of any other abbreviation that might refer to the frequency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool - thanks for checking that! Was just curious whether we should be more inclusive, but looks like not 👍

maxFrequency = 0.0;
minFrequency = 1.0;
var frequenciesStrings = param.substr(3).split(",");
for (var j=0;j<frequenciesStrings.length;j++) {
var currentFrequency = parseFloat(frequenciesStrings[j]);
maxFrequency = Math.max(maxFrequency, currentFrequency);
minFrequency = Math.min(minFrequency, currentFrequency);
}
}
}
}

return {
contig: parts[0],
position: Number(parts[1]),
id: parts[2],
ref: parts[3],
alt: parts[4],
majorFrequency: maxFrequency,
minorFrequency: minFrequency,
vcfLine
};
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/pileup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
'use strict';

import type {Track, VisualizedTrack, VizWithOptions} from './types';
import {AllelFrequencyStrategy} from './types';

import _ from 'underscore';
import React from 'react';
Expand Down Expand Up @@ -136,6 +137,11 @@ var pileup = {
variants: makeVizObject(VariantTrack),
pileup: makeVizObject(PileupTrack)
},
enum: {
variants: {
allelFrequencyStrategy: AllelFrequencyStrategy,
},
},
version: '0.6.8'
};

Expand Down
8 changes: 7 additions & 1 deletion src/main/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@

import type React from 'react';

export const AllelFrequencyStrategy = {
Minor : {name: "Minor"},
Major : {name: "Major"},
};

export type VizWithOptions = {
component: ReactClass;
options: ?Object;
Expand All @@ -23,7 +28,8 @@ export type Track = {
data: Object; // This is a DataSource object
name?: string;
cssClass?: string;
isReference?: boolean
isReference?: boolean;
options?: Object
}

export type VisualizedTrack = {
Expand Down
53 changes: 42 additions & 11 deletions src/main/viz/VariantTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* @flow
*/
'use strict';
import {AllelFrequencyStrategy} from '../types';


import type {VcfDataSource} from '../sources/VcfDataSource';
import type {Variant} from '../data/vcf';
Expand All @@ -20,17 +22,17 @@ import canvasUtils from './canvas-utils';
import dataCanvas from 'data-canvas';
import style from '../style';


class VariantTrack extends React.Component {
props: VizProps & {source: VcfDataSource};
state: void; // no state

state: void;

constructor(props: Object) {
super(props);
}

render(): any {
return <canvas onClick={this.handleClick} />;
return <canvas onClick={this.handleClick.bind(this)} />;
}

componentDidMount() {
Expand Down Expand Up @@ -80,11 +82,31 @@ class VariantTrack extends React.Component {
ctx.fillStyle = style.VARIANT_FILL;
ctx.strokeStyle = style.VARIANT_STROKE;
variants.forEach(variant => {
var variantHeightRatio = 1.0;
if (this.props.options.variantHeightByFrequency) {
var frequency = null;
if (this.props.options.allelFrequencyStrategy === undefined) { //default startegy
frequency = variant.majorFrequency;
} else if (this.props.options.allelFrequencyStrategy === AllelFrequencyStrategy.Major) {
frequency = variant.majorFrequency;
} else if (this.props.options.allelFrequencyStrategy === AllelFrequencyStrategy.Minor) {
frequency = variant.minorFrequency;
} else {
console.log("Unknown AllelFrequencyStrategy: ",this.props.options.allelFrequencyStrategy);
}
if (frequency !== null && frequency !== undefined) {
variantHeightRatio = frequency;
}
}
var height = style.VARIANT_HEIGHT*variantHeightRatio;
var variantY = y - 0.5 + style.VARIANT_HEIGHT - height;
var variantX = Math.round(scale(variant.position)) - 0.5;
var width = Math.round(scale(variant.position + 1)) - 0.5 - variantX;

ctx.pushObject(variant);
var x = Math.round(scale(variant.position));
var width = Math.round(scale(variant.position + 1)) - 1 - x;
ctx.fillRect(x - 0.5, y - 0.5, width, style.VARIANT_HEIGHT);
ctx.strokeRect(x - 0.5, y - 0.5, width, style.VARIANT_HEIGHT);

ctx.fillRect(variantX, variantY, width, height);
ctx.strokeRect(variantX, variantY, width, height);
ctx.popObject();
});

Expand All @@ -99,10 +121,19 @@ class VariantTrack extends React.Component {
ctx = canvasUtils.getContext(canvas),
trackingCtx = new dataCanvas.ClickTrackingContext(ctx, x, y);
this.renderScene(trackingCtx);
var variant = trackingCtx.hit && trackingCtx.hit[0];
var alert = window.alert || console.log;
if (variant) {
alert(JSON.stringify(variant));

var variants = trackingCtx.hit;
if (variants && variants.length>0) {
var data = [];
for (var i=0;i<variants.length;i++) {
data.push({id: variants[i].id,vcfLine:variants[i].vcfLine});
}
//user provided function for displaying popup
if (typeof this.props.options.onVariantClicked === "function") {
this.props.options.onVariantClicked(data);
} else {
console.log("Variants clicked: ", data);
}
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/test/data/vcf-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ describe('VCF', function() {
});
});

it('should have frequency', function() {
var vcf = new VcfFile(new RemoteFile('/test-data/allelFrequency.vcf'));
var range = new ContigInterval('chr20', 61790, 61800);
return vcf.getFeaturesInRange(range).then(features => {
expect(features).to.have.length(1);
expect(features[0].contig).to.equal('20');
expect(features[0].majorFrequency).to.equal(0.7);
expect(features[0].minorFrequency).to.equal(0.7);
});
});

it('should have highest frequency', function() {
var vcf = new VcfFile(new RemoteFile('/test-data/allelFrequency.vcf'));
var range = new ContigInterval('chr20', 61730, 61740);
return vcf.getFeaturesInRange(range).then(features => {
expect(features).to.have.length(1);
expect(features[0].contig).to.equal('20');
expect(features[0].majorFrequency).to.equal(0.6);
expect(features[0].minorFrequency).to.equal(0.3);
});
});

it('should add chr', function() {
var vcf = new VcfFile(new RemoteFile('/test-data/snv.vcf'));
var range = new ContigInterval('chr20', 63799, 69094);
Expand Down
75 changes: 75 additions & 0 deletions src/test/viz/VariantTrack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @flow
*/
'use strict';

import {expect} from 'chai';

import pileup from '../../main/pileup';
import dataCanvas from 'data-canvas';
import {waitFor} from '../async';

import ReactTestUtils from 'react-addons-test-utils';

describe('VariantTrack', function() {
var testDiv = document.getElementById('testdiv');

beforeEach(() => {
testDiv.style.width = '700px';
dataCanvas.RecordingContext.recordAll();
});

afterEach(() => {
dataCanvas.RecordingContext.reset();
// avoid pollution between tests.
testDiv.innerHTML = '';
});
var {drawnObjects} = dataCanvas.RecordingContext;

function ready() {
return testDiv.getElementsByTagName('canvas').length > 0 &&
drawnObjects(testDiv, '.variants').length > 0;
}

it('should render variants', function() {
var variantClickedData = null;
var variantClicked = function (data) {
variantClickedData = data;
};
var p = pileup.create(testDiv, {
range: {contig: '17', start: 9386380, stop: 9537390},
tracks: [
{
viz: pileup.viz.genome(),
data: pileup.formats.twoBit({
url: '/test-data/test.2bit'
}),
isReference: true
},
{
data: pileup.formats.vcf({
url: '/test-data/test.vcf'
}),
viz: pileup.viz.variants(),
options: {onVariantClicked: variantClicked},
}
]
});

return waitFor(ready, 2000)
.then(() => {
var variants = drawnObjects(testDiv, '.variants');
expect(variants.length).to.be.equal(1);
var canvasList = testDiv.getElementsByTagName('canvas');
var canvas = canvasList[1];
expect(variantClickedData).to.be.null;

//check clicking on variant
ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: -0.5, offsetY: -15.5}});

expect(variantClickedData).to.not.be.null;
p.destroy();
});
});

});
21 changes: 21 additions & 0 deletions test-data/allelFrequency.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
##fileformat=VCFv4.1
##source=VarScan2
##INFO=<ID=DP,Number=1,Type=Integer,Description="Total depth of quality bases">
##INFO=<ID=SOMATIC,Number=0,Type=Flag,Description="Indicates if record is a somatic mutation">
##INFO=<ID=SS,Number=1,Type=String,Description="Somatic status of variant (0=Reference,1=Germline,2=Somatic,3=LOH, or 5=Unknown)">
##INFO=<ID=SSC,Number=1,Type=String,Description="Somatic score in Phred scale (0-255) derived from somatic p-value">
##INFO=<ID=GPV,Number=1,Type=Float,Description="Fisher's Exact Test P-value of tumor+normal versus no variant for Germline calls">
##INFO=<ID=SPV,Number=1,Type=Float,Description="Fisher's Exact Test P-value of tumor versus normal for Somatic/LOH calls">
##FILTER=<ID=str10,Description="Less than 10% or more than 90% of variant supporting reads on one strand">
##FILTER=<ID=indelError,Description="Likely artifact due to indel reads at this position">
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
##FORMAT=<ID=GQ,Number=1,Type=Integer,Description="Genotype Quality">
##FORMAT=<ID=DP,Number=1,Type=Integer,Description="Read Depth">
##FORMAT=<ID=RD,Number=1,Type=Integer,Description="Depth of reference-supporting bases (reads1)">
##FORMAT=<ID=AD,Number=1,Type=Integer,Description="Depth of variant-supporting bases (reads2)">
##FORMAT=<ID=FREQ,Number=1,Type=String,Description="Variant allele frequency">
##FORMAT=<ID=DP4,Number=4,Type=Integer,Description="Strand read counts: ref/fwd, ref/rev, var/fwd, var/rev">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NORMAL TUMOR
20 61795 . G T . PASS DP=81;SS=1;SSC=2;GPV=4.6768E-16;SPV=5.4057E-1;AF=0.7 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:44:22:22:50%:16,6,9,13 0/1:.:37:18:19:51.35%:10,8,10,9
20 62731 . C A,G . PASS DP=68;SS=1;SSC=1;GPV=1.4855E-11;SPV=7.5053E-1;AF=0.4,0.5 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:32:17:15:46.88%:9,8,9,6 0/1:.:36:21:15:41.67%:8,13,8,7
20 61731 . C A,G,T . PASS DP=68;SS=1;SSC=1;GPV=1.4855E-11;SPV=7.5053E-1;AF=0.4,0.6,0.3 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:32:17:15:46.88%:9,8,9,6 0/1:.:36:21:15:41.67%:8,13,8,7
2 changes: 1 addition & 1 deletion test-data/snv.chr17.vcf
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NORMAL TUMOR
17 125 . G T . PASS DP=81;SS=1;SSC=2;GPV=4.6768E-16;SPV=5.4057E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:44:22:22:50%:16,6,9,13 0/1:.:37:18:19:51.35%:10,8,10,9
17 7512444 . G T . PASS DP=81;SS=1;SSC=2;GPV=4.6768E-16;SPV=5.4057E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:44:22:22:50%:16,6,9,13 0/1:.:37:18:19:51.35%:10,8,10,9
17 7512454 . C A . PASS DP=68;SS=1;SSC=1;GPV=1.4855E-11;SPV=7.5053E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:32:17:15:46.88%:9,8,9,6 0/1:.:36:21:15:41.67%:8,13,8,7
17 7512454 . C A . PASS DP=68;SS=1;SSC=1;GPV=1.4855E-11;SPV=7.5053E-1;AF=0.73 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:32:17:15:46.88%:9,8,9,6 0/1:.:36:21:15:41.67%:8,13,8,7
17 7512544 . C T . PASS DP=72;SS=1;SSC=7;GPV=3.6893E-16;SPV=1.8005E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:39:19:19:50%:8,11,11,8 0/1:.:33:12:21:63.64%:5,7,8,13
17 7512644 . G T . PASS DP=35;SS=1;SSC=0;GPV=7.8434E-5;SPV=8.2705E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:21:13:8:38.1%:4,9,0,8 0/1:.:14:10:4:28.57%:2,8,0,4
17 7512244 . G A . PASS DP=53;SS=1;SSC=0;GPV=1.5943E-31;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:26:0:26:100%:0,0,12,14 1/1:.:27:0:27:100%:0,0,15,12
Expand Down
19 changes: 19 additions & 0 deletions test-data/test.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
##fileformat=VCFv4.1
##source=VarScan2
##INFO=<ID=DP,Number=1,Type=Integer,Description="Total depth of quality bases">
##INFO=<ID=SOMATIC,Number=0,Type=Flag,Description="Indicates if record is a somatic mutation">
##INFO=<ID=SS,Number=1,Type=String,Description="Somatic status of variant (0=Reference,1=Germline,2=Somatic,3=LOH, or 5=Unknown)">
##INFO=<ID=SSC,Number=1,Type=String,Description="Somatic score in Phred scale (0-255) derived from somatic p-value">
##INFO=<ID=GPV,Number=1,Type=Float,Description="Fisher's Exact Test P-value of tumor+normal versus no variant for Germline calls">
##INFO=<ID=SPV,Number=1,Type=Float,Description="Fisher's Exact Test P-value of tumor versus normal for Somatic/LOH calls">
##FILTER=<ID=str10,Description="Less than 10% or more than 90% of variant supporting reads on one strand">
##FILTER=<ID=indelError,Description="Likely artifact due to indel reads at this position">
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
##FORMAT=<ID=GQ,Number=1,Type=Integer,Description="Genotype Quality">
##FORMAT=<ID=DP,Number=1,Type=Integer,Description="Read Depth">
##FORMAT=<ID=RD,Number=1,Type=Integer,Description="Depth of reference-supporting bases (reads1)">
##FORMAT=<ID=AD,Number=1,Type=Integer,Description="Depth of variant-supporting bases (reads2)">
##FORMAT=<ID=FREQ,Number=1,Type=String,Description="Variant allele frequency">
##FORMAT=<ID=DP4,Number=4,Type=Integer,Description="Strand read counts: ref/fwd, ref/rev, var/fwd, var/rev">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NORMAL TUMOR
17 9386385 . G T . PASS DP=81;SS=1;SSC=2;GPV=4.6768E-16;SPV=5.4057E-1;AF=0.7 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:44:22:22:50%:16,6,9,13 0/1:.:37:18:19:51.35%:10,8,10,9