Skip to content

Commit

Permalink
Merge pull request #441 from piotr-gawron/vcf-allel-vis
Browse files Browse the repository at this point in the history
add bar graph support for VCFs and enable subscribing to variant track clicks (thanks @piotr-gawron!)
  • Loading branch information
armish committed May 17, 2017
2 parents 189b18b + 8f50dd4 commit 6399869
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 14 deletions.
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">
</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 @@ -97,6 +97,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}
ref = {(c) => {this.trackReactElements[intKey]=c}}
/>);
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 @@ -136,14 +137,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){
var params = parts[7].split(';');
for (var i=0;i<params.length;i++) {
var param = params[i];
if (param.startsWith("AF=")) {
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 @@ -176,6 +177,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

0 comments on commit 6399869

Please sign in to comment.