From 1dc75297cba3a9908a6a1069279b0cf23eb92e42 Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Tue, 14 Jun 2016 15:33:45 +0200 Subject: [PATCH 01/11] implementation of RemoteFile from a String added - this allows user to attach dynamically generated data (instead from files it will be taken from a string source that represents content of the file) --- src/main/AbstractFile.js | 44 ++++++++++++ src/main/LocalStringFile.js | 66 +++++++++++++++++ src/main/RemoteFile.js | 4 +- src/main/data/vcf.js | 6 +- src/main/sources/VcfDataSource.js | 14 ++-- src/test/LocalStringFile-test.js | 113 ++++++++++++++++++++++++++++++ src/test/data/vcf-test.js | 58 +++++++++++++++ 7 files changed, 296 insertions(+), 9 deletions(-) create mode 100644 src/main/AbstractFile.js create mode 100644 src/main/LocalStringFile.js create mode 100644 src/test/LocalStringFile-test.js diff --git a/src/main/AbstractFile.js b/src/main/AbstractFile.js new file mode 100644 index 00000000..7bba6ec4 --- /dev/null +++ b/src/main/AbstractFile.js @@ -0,0 +1,44 @@ +/** + * AbstractFile is an abstract representation of a file. There are two implementation: + * 1. RemoteFile - representation of a file on a remote server which can be + * fetched in chunks, e.g. using a Range request. + * 2. LoclaStringFile is a representation of a file that was created from input string. + * Used for testing and small input files. + * @flow + */ +'use strict'; + +import Q from 'q'; + + +class AbstractFile { + constructor() { + //how to prevent instantation of this class??? + //this code doesn't pass npm run flow +// if (new.target === AbstractFile) { +// throw new TypeError("Cannot construct AbstractFile instances directly"); +// } + } + + getBytes(start: number, length: number): Q.Promise { + throw new TypeError("Method getBytes is not implemented"); + } + + // Read the entire file -- not recommended for large files! + getAll(): Q.Promise { + throw new TypeError("Method getAll is not implemented"); + } + + // Reads the entire file as a string (not an ArrayBuffer). + // This does not use the cache. + getAllString(): Q.Promise { + throw new TypeError("Method getAllString is not implemented"); + } + + // Returns a promise for the number of bytes in the remote file. + getSize(): Q.Promise { + throw new TypeError("Method getSize is not implemented"); + } +} + +module.exports = AbstractFile; diff --git a/src/main/LocalStringFile.js b/src/main/LocalStringFile.js new file mode 100644 index 00000000..32cbf17e --- /dev/null +++ b/src/main/LocalStringFile.js @@ -0,0 +1,66 @@ +/** + * LocalStringFile is a representation of a file that was created from input string. Used for testing and small input files. + * @flow + */ +'use strict'; + +import Q from 'q'; +import AbstractFile from './AbstractFile'; + +class LocalStringFile extends AbstractFile { + fileLength: number; + content: string; //content of this "File" + buffer: ArrayBuffer; + + constructor(content: string) { + super(); + this.fileLength = content.length; + this.buffer = new ArrayBuffer(content.length); // 2 bytes for each char + this.content = content; + + var bufView = new Uint8Array(this.buffer); + for (var i=0; i < this.fileLength; i++) { + bufView[i] = content.charCodeAt(i); + } + } + + getBytes(start: number, length: number): Q.Promise { + if (length < 0) { + return Q.reject(`Requested <0 bytes (${length})`); + } + + // If the remote file length is known, clamp the request to fit within it. + var stop = start + length - 1; + if (this.fileLength != -1) { + stop = Math.min(this.fileLength - 1, stop); + } + + // First check the cache. + var buf = this.getFromCache(start, stop); + return Q.when(buf); + } + + // Read the entire file -- not recommended for large files! + getAll(): Q.Promise { + var buf = this.getFromCache(0, this.fileLength - 1); + return Q.when(buf); + } + + // Reads the entire file as a string (not an ArrayBuffer). + // This does not use the cache. + getAllString(): Q.Promise { + return Q.when(this.content); + } + + // Returns a promise for the number of bytes in the remote file. + getSize(): Q.Promise { + return Q.when(this.fileLength); + } + + getFromCache(start: number, stop: number): ?ArrayBuffer { + return this.buffer.slice(start, stop + 1); + } + +} + +module.exports = LocalStringFile; diff --git a/src/main/RemoteFile.js b/src/main/RemoteFile.js index bfebee1f..f96229b5 100644 --- a/src/main/RemoteFile.js +++ b/src/main/RemoteFile.js @@ -6,6 +6,7 @@ 'use strict'; import Q from 'q'; +import AbstractFile from './AbstractFile'; type Chunk = { start: number; @@ -15,13 +16,14 @@ type Chunk = { } -class RemoteFile { +class RemoteFile extends AbstractFile{ url: string; fileLength: number; chunks: Array; // regions of file that have already been loaded. numNetworkRequests: number; // track this for debugging/testing constructor(url: string) { + super(); this.url = url; this.fileLength = -1; // unknown this.chunks = []; diff --git a/src/main/data/vcf.js b/src/main/data/vcf.js index 8bc9d71f..558eb623 100644 --- a/src/main/data/vcf.js +++ b/src/main/data/vcf.js @@ -8,7 +8,7 @@ 'use strict'; import type ContigInterval from '../ContigInterval'; -import type RemoteFile from '../RemoteFile'; +import type AbstractFile from '../AbstractFile'; import type Q from 'q'; export type Variant = { @@ -146,10 +146,10 @@ class ImmediateVcfFile { class VcfFile { - remoteFile: RemoteFile; + remoteFile: AbstractFile; immediate: Q.Promise; - constructor(remoteFile: RemoteFile) { + constructor(remoteFile: AbstractFile) { this.remoteFile = remoteFile; this.immediate = this.remoteFile.getAllString().then(txt => { diff --git a/src/main/sources/VcfDataSource.js b/src/main/sources/VcfDataSource.js index 68dd1557..566c3bb2 100644 --- a/src/main/sources/VcfDataSource.js +++ b/src/main/sources/VcfDataSource.js @@ -13,6 +13,7 @@ import Q from 'q'; import ContigInterval from '../ContigInterval'; import RemoteFile from '../RemoteFile'; +import LocalStringFile from '../LocalStringFile'; import VcfFile from '../data/vcf'; export type VcfDataSource = { @@ -91,13 +92,16 @@ function createFromVcfFile(remoteSource: VcfFile): VcfDataSource { return o; } -function create(data: {url:string}): VcfDataSource { +function create(data: Object): VcfDataSource { var url = data.url; - if (!url) { - throw new Error(`Missing URL from track: ${JSON.stringify(data)}`); + var content = data.content; + if (url!=null) { + return createFromVcfFile(new VcfFile(new RemoteFile(url))); } - - return createFromVcfFile(new VcfFile(new RemoteFile(url))); + if (content!=null) { + return createFromVcfFile(new VcfFile(new LocalStringFile(content))); + } + throw new Error(`Missing URL from track: ${JSON.stringify(data)}`); } module.exports = { diff --git a/src/test/LocalStringFile-test.js b/src/test/LocalStringFile-test.js new file mode 100644 index 00000000..ca8d519f --- /dev/null +++ b/src/test/LocalStringFile-test.js @@ -0,0 +1,113 @@ +/* @flow */ +'use strict'; + +import {expect} from 'chai'; + +import LocalStringFile from '../main/LocalStringFile'; +import jBinary from 'jbinary'; + +describe('LocalStringFile', () => { + function bufferToText(buf) { + return new jBinary(buf).read('string'); + } + + it('should fetch a subset of a file', function() { + var f = new LocalStringFile('0123456789\n'); + var promisedData = f.getBytes(4, 5); + + return promisedData.then(buf => { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('45678'); + }); + }); + + it('should fetch subsets from cache', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getBytes(0, 10).then(buf => { + expect(buf.byteLength).to.equal(10); + expect(bufferToText(buf)).to.equal('0123456789'); + return f.getBytes(4, 5).then(buf => { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('45678'); + }); + }); + }); + + it('should fetch entire files', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getAll().then(buf => { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + }); + }); + + it('should determine file lengths', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getSize().then(size => { + expect(size).to.equal(11); + }); + }); + + it('should get file lengths from full requests', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getAll().then(buf => { + return f.getSize().then(size => { + expect(size).to.equal(11); + }); + }); + }); + + it('should get file lengths from range requests', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getBytes(4, 5).then(buf => { + return f.getSize().then(size => { + expect(size).to.equal(11); + }); + }); + }); + + it('should cache requests for full files', function() { + var f = new LocalStringFile('0123456789\n'); + f.getAll().then(buf => { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + return f.getAll().then(buf => { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + }); + }); + }); + + it('should serve range requests from cache after getAll', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getAll().then(buf => { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + return f.getBytes(4, 5).then(buf => { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('45678'); + }); + }); + }); + + it('should truncate requests past EOF', function() { + var f = new LocalStringFile('0123456789\n'); + var promisedData = f.getBytes(4, 100); + + return promisedData.then(buf => { + expect(buf.byteLength).to.equal(7); + expect(bufferToText(buf)).to.equal('456789\n'); + return f.getBytes(6, 90).then(buf => { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('6789\n'); + }); + }); + }); + + it('should fetch entire files as strings', function() { + var f = new LocalStringFile('0123456789\n'); + return f.getAllString().then(txt => { + expect(txt).to.equal('0123456789\n'); + }); + }); +}); diff --git a/src/test/data/vcf-test.js b/src/test/data/vcf-test.js index dc0c748d..9708ba81 100644 --- a/src/test/data/vcf-test.js +++ b/src/test/data/vcf-test.js @@ -6,6 +6,9 @@ import {expect} from 'chai'; import VcfFile from '../../main/data/vcf'; import ContigInterval from '../../main/ContigInterval'; import RemoteFile from '../../main/RemoteFile'; +import LocalStringFile from '../../main/LocalStringFile'; + +var fs = require('fs'); describe('VCF', function() { it('should respond to queries', function() { @@ -51,3 +54,58 @@ describe('VCF', function() { }); }); }); +describe('VCF from string content', function() { + var content = ''; + before(function() { + content = +"##fileformat=VCFv4.1\n"+ +"##source=VarScan2\n"+ +"##INFO=\n"+ +"##INFO=\n"+ +"##INFO=\n"+ +"##INFO=\n"+ +"##INFO=\n"+ +"##INFO=\n"+ +"##FILTER=\n"+ +"##FILTER=\n"+ +"##FORMAT=\n"+ +"##FORMAT=\n"+ +"##FORMAT=\n"+ +"##FORMAT=\n"+ +"##FORMAT=\n"+ +"##FORMAT=\n"+ +"##FORMAT=\n"+ +"#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NORMAL TUMOR\n"+ +"20 61795 . 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\n"+ +"20 62731 . 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\n"+ +"20 63799 . 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\n"+ +"20 65288 . 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\n"+ +"20 65900 . 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\n"+ +"20 66370 . G A . PASS DP=66;SS=1;SSC=0;GPV=2.6498E-39;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:32:0:32:100%:0,0,11,21 1/1:.:34:0:34:100%:0,0,15,19\n"+ +"20 68749 . T C . PASS DP=64;SS=1;SSC=0;GPV=4.1752E-38;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:23:0:23:100%:0,0,7,16 1/1:.:41:0:41:100%:0,0,21,20\n"+ +"20 69094 . G A . PASS DP=25;SS=1;SSC=8;GPV=4.2836E-5;SPV=1.5657E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:12:8:4:33.33%:5,3,4,0 0/1:.:13:5:8:61.54%:3,2,6,2\n"+ +"20 69408 . C T . PASS DP=53;SS=1;SSC=0;GPV=8.7266E-12;SPV=9.8064E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:27:9:18:66.67%:5,4,9,9 0/1:.:26:15:11:42.31%:6,9,4,7\n"+ +"20 75254 . C A . PASS DP=74;SS=1;SSC=9;GPV=7.9203E-12;SPV=1.1567E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:34:22:11:33.33%:13,9,5,6 0/1:.:40:20:20:50%:5,15,14,6\n"; + }); + + it('should respond to queries', function() { + var vcf = new VcfFile(new LocalStringFile(content)); + var range = new ContigInterval('20', 63799, 69094); + return vcf.getFeaturesInRange(range).then(features => { + expect(features).to.have.length(6); + + var v0 = features[0], + v5 = features[5]; + + expect(v0.contig).to.equal('20'); + expect(v0.position).to.equal(63799); + expect(v0.ref).to.equal('C'); + expect(v0.alt).to.equal('T'); + + expect(v5.contig).to.equal('20'); + expect(v5.position).to.equal(69094); + expect(v5.ref).to.equal('G'); + expect(v5.alt).to.equal('A'); + }); + }); +}); From f11b82a14a200f0eb3b1866cae72bd0966266eab Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Tue, 14 Jun 2016 15:44:30 +0200 Subject: [PATCH 02/11] lint issues --- src/main/AbstractFile.js | 1 - src/main/sources/VcfDataSource.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/AbstractFile.js b/src/main/AbstractFile.js index 7bba6ec4..1a824b66 100644 --- a/src/main/AbstractFile.js +++ b/src/main/AbstractFile.js @@ -10,7 +10,6 @@ import Q from 'q'; - class AbstractFile { constructor() { //how to prevent instantation of this class??? diff --git a/src/main/sources/VcfDataSource.js b/src/main/sources/VcfDataSource.js index 566c3bb2..2a677f7f 100644 --- a/src/main/sources/VcfDataSource.js +++ b/src/main/sources/VcfDataSource.js @@ -95,10 +95,10 @@ function createFromVcfFile(remoteSource: VcfFile): VcfDataSource { function create(data: Object): VcfDataSource { var url = data.url; var content = data.content; - if (url!=null) { + if (url!==null && url!== undefined) { return createFromVcfFile(new VcfFile(new RemoteFile(url))); } - if (content!=null) { + if (content!==null && content!== undefined) { return createFromVcfFile(new VcfFile(new LocalStringFile(content))); } throw new Error(`Missing URL from track: ${JSON.stringify(data)}`); From cdd8226b485ebd02a10ae6b5c5d0a6bad9ed3de4 Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Tue, 14 Jun 2016 15:51:42 +0200 Subject: [PATCH 03/11] lint fix --- src/main/AbstractFile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/AbstractFile.js b/src/main/AbstractFile.js index 1a824b66..05760ddb 100644 --- a/src/main/AbstractFile.js +++ b/src/main/AbstractFile.js @@ -8,7 +8,7 @@ */ 'use strict'; -import Q from 'q'; +//import Q from 'q'; class AbstractFile { constructor() { @@ -19,23 +19,23 @@ class AbstractFile { // } } - getBytes(start: number, length: number): Q.Promise { + getBytes(start: number, length: number):Object {//: Q.Promise { throw new TypeError("Method getBytes is not implemented"); } // Read the entire file -- not recommended for large files! - getAll(): Q.Promise { + getAll():Object {//: Q.Promise { throw new TypeError("Method getAll is not implemented"); } // Reads the entire file as a string (not an ArrayBuffer). // This does not use the cache. - getAllString(): Q.Promise { + getAllString():Object {//: Q.Promise { throw new TypeError("Method getAllString is not implemented"); } // Returns a promise for the number of bytes in the remote file. - getSize(): Q.Promise { + getSize():Object {//: Q.Promise { throw new TypeError("Method getSize is not implemented"); } } From c288d42d16892d997a066c35df6ec11f456e8e94 Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Tue, 14 Jun 2016 15:55:57 +0200 Subject: [PATCH 04/11] lint issue --- src/test/data/vcf-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/data/vcf-test.js b/src/test/data/vcf-test.js index 9708ba81..af2277f0 100644 --- a/src/test/data/vcf-test.js +++ b/src/test/data/vcf-test.js @@ -8,8 +8,6 @@ import ContigInterval from '../../main/ContigInterval'; import RemoteFile from '../../main/RemoteFile'; import LocalStringFile from '../../main/LocalStringFile'; -var fs = require('fs'); - describe('VCF', function() { it('should respond to queries', function() { var vcf = new VcfFile(new RemoteFile('/test-data/snv.vcf')); From b7e0d6ae155e09a7910541a3a9be488c0f9cc7e3 Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Wed, 15 Jun 2016 10:48:00 +0200 Subject: [PATCH 05/11] armish code review suggestions --- src/main/AbstractFile.js | 2 +- src/main/sources/VcfDataSource.js | 11 ++++ src/test/data/vcf-test.js | 101 +++++++++--------------------- 3 files changed, 43 insertions(+), 71 deletions(-) diff --git a/src/main/AbstractFile.js b/src/main/AbstractFile.js index 05760ddb..76fab004 100644 --- a/src/main/AbstractFile.js +++ b/src/main/AbstractFile.js @@ -2,7 +2,7 @@ * AbstractFile is an abstract representation of a file. There are two implementation: * 1. RemoteFile - representation of a file on a remote server which can be * fetched in chunks, e.g. using a Range request. - * 2. LoclaStringFile is a representation of a file that was created from input string. + * 2. LocalStringFile is a representation of a file that was created from input string. * Used for testing and small input files. * @flow */ diff --git a/src/main/sources/VcfDataSource.js b/src/main/sources/VcfDataSource.js index 2a677f7f..7f5a3f0b 100644 --- a/src/main/sources/VcfDataSource.js +++ b/src/main/sources/VcfDataSource.js @@ -104,6 +104,17 @@ function create(data: Object): VcfDataSource { throw new Error(`Missing URL from track: ${JSON.stringify(data)}`); } +function create(data: {url?: string, content?: string}): VcfDataSource { + var {url, content} = data; + if (url) { + return createFromVcfFile(new VcfFile(new RemoteFile(url))); + } else if (content) { + return createFromVcfFile(new VcfFile(new LocalStringFile(content))); + } + // If no URL or content is passed, fail + throw new Error(`Missing URL or content from track: ${JSON.stringify(data)}`); +} + module.exports = { create, createFromVcfFile diff --git a/src/test/data/vcf-test.js b/src/test/data/vcf-test.js index af2277f0..633044fc 100644 --- a/src/test/data/vcf-test.js +++ b/src/test/data/vcf-test.js @@ -9,24 +9,40 @@ import RemoteFile from '../../main/RemoteFile'; import LocalStringFile from '../../main/LocalStringFile'; describe('VCF', function() { - it('should respond to queries', function() { - var vcf = new VcfFile(new RemoteFile('/test-data/snv.vcf')); - var range = new ContigInterval('20', 63799, 69094); - return vcf.getFeaturesInRange(range).then(features => { - expect(features).to.have.length(6); + describe('should respond to queries', function() { + var testQueries = (vcf) => { + var range = new ContigInterval('20', 63799, 69094); + return vcf.getFeaturesInRange(range).then(features => { + expect(features).to.have.length(6); + + var v0 = features[0], + v5 = features[5]; + + expect(v0.contig).to.equal('20'); + expect(v0.position).to.equal(63799); + expect(v0.ref).to.equal('C'); + expect(v0.alt).to.equal('T'); - var v0 = features[0], - v5 = features[5]; + expect(v5.contig).to.equal('20'); + expect(v5.position).to.equal(69094); + expect(v5.ref).to.equal('G'); + expect(v5.alt).to.equal('A'); + }); + }; - expect(v0.contig).to.equal('20'); - expect(v0.position).to.equal(63799); - expect(v0.ref).to.equal('C'); - expect(v0.alt).to.equal('T'); + var remoteFile = new RemoteFile('/test-data/snv.vcf'); - expect(v5.contig).to.equal('20'); - expect(v5.position).to.equal(69094); - expect(v5.ref).to.equal('G'); - expect(v5.alt).to.equal('A'); + it('remote file', function() { + var vcf = new VcfFile(remoteFile); + testQueries(vcf); + }); + + it('local file from string', function() { + return remoteFile.getAllString().then(content => { + var localFile = new LocalStringFile(content); + var vcf = new VcfFile(localFile); + testQueries(vcf); + }); }); }); @@ -52,58 +68,3 @@ describe('VCF', function() { }); }); }); -describe('VCF from string content', function() { - var content = ''; - before(function() { - content = -"##fileformat=VCFv4.1\n"+ -"##source=VarScan2\n"+ -"##INFO=\n"+ -"##INFO=\n"+ -"##INFO=\n"+ -"##INFO=\n"+ -"##INFO=\n"+ -"##INFO=\n"+ -"##FILTER=\n"+ -"##FILTER=\n"+ -"##FORMAT=\n"+ -"##FORMAT=\n"+ -"##FORMAT=\n"+ -"##FORMAT=\n"+ -"##FORMAT=\n"+ -"##FORMAT=\n"+ -"##FORMAT=\n"+ -"#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NORMAL TUMOR\n"+ -"20 61795 . 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\n"+ -"20 62731 . 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\n"+ -"20 63799 . 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\n"+ -"20 65288 . 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\n"+ -"20 65900 . 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\n"+ -"20 66370 . G A . PASS DP=66;SS=1;SSC=0;GPV=2.6498E-39;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:32:0:32:100%:0,0,11,21 1/1:.:34:0:34:100%:0,0,15,19\n"+ -"20 68749 . T C . PASS DP=64;SS=1;SSC=0;GPV=4.1752E-38;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:23:0:23:100%:0,0,7,16 1/1:.:41:0:41:100%:0,0,21,20\n"+ -"20 69094 . G A . PASS DP=25;SS=1;SSC=8;GPV=4.2836E-5;SPV=1.5657E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:12:8:4:33.33%:5,3,4,0 0/1:.:13:5:8:61.54%:3,2,6,2\n"+ -"20 69408 . C T . PASS DP=53;SS=1;SSC=0;GPV=8.7266E-12;SPV=9.8064E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:27:9:18:66.67%:5,4,9,9 0/1:.:26:15:11:42.31%:6,9,4,7\n"+ -"20 75254 . C A . PASS DP=74;SS=1;SSC=9;GPV=7.9203E-12;SPV=1.1567E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:34:22:11:33.33%:13,9,5,6 0/1:.:40:20:20:50%:5,15,14,6\n"; - }); - - it('should respond to queries', function() { - var vcf = new VcfFile(new LocalStringFile(content)); - var range = new ContigInterval('20', 63799, 69094); - return vcf.getFeaturesInRange(range).then(features => { - expect(features).to.have.length(6); - - var v0 = features[0], - v5 = features[5]; - - expect(v0.contig).to.equal('20'); - expect(v0.position).to.equal(63799); - expect(v0.ref).to.equal('C'); - expect(v0.alt).to.equal('T'); - - expect(v5.contig).to.equal('20'); - expect(v5.position).to.equal(69094); - expect(v5.ref).to.equal('G'); - expect(v5.alt).to.equal('A'); - }); - }); -}); From 95f6810d90aa8f2aa5b2662fc66c9bdfb6098a2f Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Thu, 16 Jun 2016 10:35:01 +0200 Subject: [PATCH 06/11] merge issues :) --- src/main/sources/VcfDataSource.js | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/main/sources/VcfDataSource.js b/src/main/sources/VcfDataSource.js index 7f5a3f0b..a8329361 100644 --- a/src/main/sources/VcfDataSource.js +++ b/src/main/sources/VcfDataSource.js @@ -92,27 +92,15 @@ function createFromVcfFile(remoteSource: VcfFile): VcfDataSource { return o; } -function create(data: Object): VcfDataSource { - var url = data.url; - var content = data.content; - if (url!==null && url!== undefined) { +function create(data: {url?: string, content?: string}): VcfDataSource { + var {url, content} = data; + if (url) { return createFromVcfFile(new VcfFile(new RemoteFile(url))); - } - if (content!==null && content!== undefined) { + } else if (content) { return createFromVcfFile(new VcfFile(new LocalStringFile(content))); } - throw new Error(`Missing URL from track: ${JSON.stringify(data)}`); -} - -function create(data: {url?: string, content?: string}): VcfDataSource { - var {url, content} = data; - if (url) { - return createFromVcfFile(new VcfFile(new RemoteFile(url))); - } else if (content) { - return createFromVcfFile(new VcfFile(new LocalStringFile(content))); - } - // If no URL or content is passed, fail - throw new Error(`Missing URL or content from track: ${JSON.stringify(data)}`); + // If no URL or content is passed, fail + throw new Error(`Missing URL or content from track: ${JSON.stringify(data)}`); } module.exports = { From 637d9f9f9b1958d7d97f4b48a3be9ab4fd1d6a76 Mon Sep 17 00:00:00 2001 From: Piotr Gawron Date: Wed, 2 Nov 2016 16:35:03 +0100 Subject: [PATCH 07/11] few viarant fixes and updates * fix on popup when clicking on variant (up to now there was a console error) * popup is a bootstrap modal window (not just old alert function) * if possible (and requested by user), gene variant height depends on the allel frequency of specific variant --- examples/data.js | 1 + examples/index.html | 1 + package.json | 1 + src/main/Root.js | 1 + src/main/VisualizationWrapper.js | 4 +- src/main/data/vcf.js | 18 ++++++++ src/main/types.js | 3 +- src/main/viz/VariantTrack.js | 77 +++++++++++++++++++++++++++----- src/test/data/vcf-test.js | 20 +++++++++ src/test/viz/VariantTrack.js | 73 ++++++++++++++++++++++++++++++ test-data/allelFrequency.vcf | 21 +++++++++ test-data/snv.chr17.vcf | 2 +- test-data/test.vcf | 19 ++++++++ 13 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 src/test/viz/VariantTrack.js create mode 100644 test-data/allelFrequency.vcf create mode 100644 test-data/test.vcf diff --git a/examples/data.js b/examples/data.js index 77d16e35..14d1a9df 100644 --- a/examples/data.js +++ b/examples/data.js @@ -28,6 +28,7 @@ var sources = [ data: pileup.formats.vcf({ url: '/test-data/snv.chr17.vcf' }), + options: {variantHeightByFrequency: true}, name: 'Variants' }, { diff --git a/examples/index.html b/examples/index.html index d174f5d7..51f631b6 100644 --- a/examples/index.html +++ b/examples/index.html @@ -3,6 +3,7 @@ + diff --git a/package.json b/package.json index 53c633ba..aee99873 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "pako": "^0.2.5", "q": "^1.1.2", "react": "^0.14.0", + "react-bootstrap": "0.30.6", "react-dom": "^0.14.0", "shallow-equals": "0.0.0", "underscore": "^1.7.0", diff --git a/src/main/Root.js b/src/main/Root.js index 0e33904c..c5cf5df2 100644 --- a/src/main/Root.js +++ b/src/main/Root.js @@ -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} />); diff --git a/src/main/VisualizationWrapper.js b/src/main/VisualizationWrapper.js index cb2167cb..3b97e656 100644 --- a/src/main/VisualizationWrapper.js +++ b/src/main/VisualizationWrapper.js @@ -27,6 +27,7 @@ type Props = { onRangeChange: (newRange: GenomeRange) => void; referenceSource: TwoBitSource; source: any; + options: ?Object; }; class VisualizationWrapper extends React.Component { @@ -128,6 +129,7 @@ class VisualizationWrapper extends React.Component { if (!range) { return ; } + var options = _.extend({},this.props.visualization.options,this.props.options); var el = React.createElement(component, ({ range: range, @@ -135,7 +137,7 @@ class VisualizationWrapper extends React.Component { referenceSource: this.props.referenceSource, width: this.state.width, height: this.state.height, - options: this.props.visualization.options + options: options } : VizProps)); return
{el}
; diff --git a/src/main/data/vcf.js b/src/main/data/vcf.js index 558eb623..1b60e423 100644 --- a/src/main/data/vcf.js +++ b/src/main/data/vcf.js @@ -16,6 +16,8 @@ export type Variant = { position: number; ref: string; alt: string; + id: string; + significantFrequency: ?number; vcfLine: string; } @@ -41,12 +43,28 @@ function extractLocusLine(vcfLine: string): LocusLine { function extractVariant(vcfLine: string): Variant { var parts = vcfLine.split('\t'); + var frequency = null; + if (parts.length>=7){ + var params = parts[7].split(';'); + for (var i=0;i; + return
+ + + + + + +

+ + + + + +

; + } + + closePopup() { + this.setState({ showPopup: false }); + } + + openPopup() { + this.setState({ showPopup: true }); } componentDidMount() { @@ -53,7 +85,7 @@ class VariantTrack extends React.Component { } updateVisualization() { - var canvas = ReactDOM.findDOMNode(this), + var canvas = ReactDOM.findDOMNode(this).firstChild, {width, height} = this.props; // Hold off until height & width are known. @@ -80,11 +112,21 @@ 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) { + if (variant.significantFrequency !== null && variant.significantFrequency !== undefined) { + variantHeightRatio = variant.significantFrequency; + } + } + 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(); }); @@ -95,14 +137,27 @@ class VariantTrack extends React.Component { var ev = reactEvent.nativeEvent, x = ev.offsetX, y = ev.offsetY, - canvas = ReactDOM.findDOMNode(this), + canvas = ReactDOM.findDOMNode(this).firstChild, 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 popupContent = variant.ref+" → "+variant.alt; + var popupTitle = "Variant "+variant.id+" (contig: "+variant.contig+", position: "+variant.position+")"; + + //user provided function for displaying popup title + if (typeof this.props.options.getPopupTitleByVariantId === "function") { + popupTitle = this.props.options.getPopupTitleByVariantId(variant.id, variant.vcfLine); + } + //user provided function for displaying popup content + //user provided function for displaying popup title + if (typeof this.props.options.getPopupContentByVariantId === "function") { + popupContent = this.props.options.getPopupContentByVariantId(variant.id, variant.vcfLine); + } + this.setState({ selectedVariantPopupContent: popupContent, selectedVariantPopupTitle: popupTitle }); + this.openPopup(); } } } diff --git a/src/test/data/vcf-test.js b/src/test/data/vcf-test.js index 633044fc..dca3344c 100644 --- a/src/test/data/vcf-test.js +++ b/src/test/data/vcf-test.js @@ -46,6 +46,26 @@ 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].significantFrequency).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].significantFrequency).to.equal(0.6); + }); + }); + it('should add chr', function() { var vcf = new VcfFile(new RemoteFile('/test-data/snv.vcf')); var range = new ContigInterval('chr20', 63799, 69094); diff --git a/src/test/viz/VariantTrack.js b/src/test/viz/VariantTrack.js new file mode 100644 index 00000000..4a19922b --- /dev/null +++ b/src/test/viz/VariantTrack.js @@ -0,0 +1,73 @@ +'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, callsOf} = dataCanvas.RecordingContext; + + function ready() { + return testDiv.getElementsByTagName('canvas').length > 0 && + drawnObjects(testDiv, '.variants').length > 0; + } + + it('should render variants', function() { + var popupId = null; + var getPopupTitle = function (id) { + popupId = id; + return "hello world, "+id; + } + 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: {getPopupTitleByVariantId: getPopupTitle} + } + ] + }); + + 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(popupId).to.be.null; + + //check clicking on variant + ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: -0.5, offsetY: -15.5}}); + + expect(popupId).to.not.be.null; + p.destroy(); + }); + }); + +}); diff --git a/test-data/allelFrequency.vcf b/test-data/allelFrequency.vcf new file mode 100644 index 00000000..c63f5379 --- /dev/null +++ b/test-data/allelFrequency.vcf @@ -0,0 +1,21 @@ +##fileformat=VCFv4.1 +##source=VarScan2 +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##FILTER= +##FILTER= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +#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 diff --git a/test-data/snv.chr17.vcf b/test-data/snv.chr17.vcf index bcb95918..f391885c 100644 --- a/test-data/snv.chr17.vcf +++ b/test-data/snv.chr17.vcf @@ -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 diff --git a/test-data/test.vcf b/test-data/test.vcf new file mode 100644 index 00000000..732d6007 --- /dev/null +++ b/test-data/test.vcf @@ -0,0 +1,19 @@ +##fileformat=VCFv4.1 +##source=VarScan2 +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##FILTER= +##FILTER= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +#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 From cf381aed44c86d6cb12a34ff5d71582098cff221 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 2 Nov 2016 16:50:27 +0100 Subject: [PATCH 08/11] missing flow annotation --- src/test/viz/VariantTrack.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/viz/VariantTrack.js b/src/test/viz/VariantTrack.js index 4a19922b..83de0335 100644 --- a/src/test/viz/VariantTrack.js +++ b/src/test/viz/VariantTrack.js @@ -1,3 +1,6 @@ +/** + * @flow + */ 'use strict'; import {expect} from 'chai'; From f32698c918d04a724bc8778069a9f28fb50e39f0 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 2 Nov 2016 16:53:41 +0100 Subject: [PATCH 09/11] lint issues --- src/test/viz/VariantTrack.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/viz/VariantTrack.js b/src/test/viz/VariantTrack.js index 83de0335..24564c6d 100644 --- a/src/test/viz/VariantTrack.js +++ b/src/test/viz/VariantTrack.js @@ -24,7 +24,7 @@ describe('VariantTrack', function() { // avoid pollution between tests. testDiv.innerHTML = ''; }); - var {drawnObjects, callsOf} = dataCanvas.RecordingContext; + var {drawnObjects} = dataCanvas.RecordingContext; function ready() { return testDiv.getElementsByTagName('canvas').length > 0 && @@ -36,7 +36,7 @@ describe('VariantTrack', function() { var getPopupTitle = function (id) { popupId = id; return "hello world, "+id; - } + }; var p = pileup.create(testDiv, { range: {contig: '17', start: 9386380, stop: 9537390}, tracks: [ From 174665440d1572cbdd7d34c70b3e6c8f423e361e Mon Sep 17 00:00:00 2001 From: piotr-gawron Date: Fri, 11 Nov 2016 12:36:40 +0100 Subject: [PATCH 10/11] gene variant popup reimplemented * bootstrap library removed * when user click on gene variant instead of poup user defined function will be called (if such method is not defined log information will appear in the console) * allel frequency is parsed in a way that maximum and minimum values are stored in vcf variant entry * user can define strategy that should be used when hight of the gene variant bars should depend on the alle frequency, two defined strategies are: - take max value of allel frequency for vcf entry - take min value of allel frequency for vcf entry --- examples/data.js | 11 ++++- package.json | 1 - src/main/data/vcf.js | 20 ++++++--- src/main/pileup.js | 6 +++ src/main/types.js | 5 +++ src/main/viz/VariantTrack.js | 82 +++++++++++++++--------------------- src/test/data/vcf-test.js | 6 ++- src/test/viz/VariantTrack.js | 13 +++--- 8 files changed, 79 insertions(+), 65 deletions(-) diff --git a/examples/data.js b/examples/data.js index 14d1a9df..af1bb412 100644 --- a/examples/data.js +++ b/examples/data.js @@ -28,7 +28,16 @@ var sources = [ data: pileup.formats.vcf({ url: '/test-data/snv.chr17.vcf' }), - options: {variantHeightByFrequency: true}, + 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' }, { diff --git a/package.json b/package.json index aee99873..53c633ba 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "pako": "^0.2.5", "q": "^1.1.2", "react": "^0.14.0", - "react-bootstrap": "0.30.6", "react-dom": "^0.14.0", "shallow-equals": "0.0.0", "underscore": "^1.7.0", diff --git a/src/main/data/vcf.js b/src/main/data/vcf.js index 1b60e423..880a4f1b 100644 --- a/src/main/data/vcf.js +++ b/src/main/data/vcf.js @@ -17,7 +17,12 @@ export type Variant = { ref: string; alt: string; id: string; - significantFrequency: ?number; + //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; } @@ -43,16 +48,20 @@ function extractLocusLine(vcfLine: string): LocusLine { function extractVariant(vcfLine: string): Variant { var parts = vcfLine.split('\t'); - var frequency = null; + var maxFrequency = null; + var minFrequency = null; if (parts.length>=7){ var params = parts[7].split(';'); for (var i=0;i - - - - - - -

- - - - - - ; - } - - closePopup() { - this.setState({ showPopup: false }); - } - - openPopup() { - this.setState({ showPopup: true }); + return ; } componentDidMount() { @@ -85,7 +65,7 @@ class VariantTrack extends React.Component { } updateVisualization() { - var canvas = ReactDOM.findDOMNode(this).firstChild, + var canvas = ReactDOM.findDOMNode(this), {width, height} = this.props; // Hold off until height & width are known. @@ -114,6 +94,14 @@ class VariantTrack extends React.Component { variants.forEach(variant => { var variantHeightRatio = 1.0; if (this.props.options.variantHeightByFrequency) { + var frequency = null; + if (this.props.allelFrequencyStrategy === AllelFrequencyStrategy.Major) { + frequency = variant.majorFrequency; + } else if (this.props.allelFrequencyStrategy === AllelFrequencyStrategy.Minor) { + frequency = variant.minorFrequency; + } else { + console.log("Unknown AllelFrequencyStrategy: ",this.props.allelFrequencyStrategy); + } if (variant.significantFrequency !== null && variant.significantFrequency !== undefined) { variantHeightRatio = variant.significantFrequency; } @@ -137,27 +125,23 @@ class VariantTrack extends React.Component { var ev = reactEvent.nativeEvent, x = ev.offsetX, y = ev.offsetY, - canvas = ReactDOM.findDOMNode(this).firstChild, + canvas = ReactDOM.findDOMNode(this), ctx = canvasUtils.getContext(canvas), trackingCtx = new dataCanvas.ClickTrackingContext(ctx, x, y); this.renderScene(trackingCtx); - var variant = trackingCtx.hit && trackingCtx.hit[0]; - if (variant) { - var popupContent = variant.ref+" → "+variant.alt; - var popupTitle = "Variant "+variant.id+" (contig: "+variant.contig+", position: "+variant.position+")"; - - //user provided function for displaying popup title - if (typeof this.props.options.getPopupTitleByVariantId === "function") { - popupTitle = this.props.options.getPopupTitleByVariantId(variant.id, variant.vcfLine); + var variants = trackingCtx.hit; + if (variants && variants.length>0) { + var data = []; + for (var i=0;i { expect(features).to.have.length(1); expect(features[0].contig).to.equal('20'); - expect(features[0].significantFrequency).to.equal(0.7); + expect(features[0].majorFrequency).to.equal(0.7); + expect(features[0].minorFrequency).to.equal(0.7); }); }); @@ -62,7 +63,8 @@ describe('VCF', function() { return vcf.getFeaturesInRange(range).then(features => { expect(features).to.have.length(1); expect(features[0].contig).to.equal('20'); - expect(features[0].significantFrequency).to.equal(0.6); + expect(features[0].majorFrequency).to.equal(0.6); + expect(features[0].minorFrequency).to.equal(0.3); }); }); diff --git a/src/test/viz/VariantTrack.js b/src/test/viz/VariantTrack.js index 24564c6d..06842461 100644 --- a/src/test/viz/VariantTrack.js +++ b/src/test/viz/VariantTrack.js @@ -32,10 +32,9 @@ describe('VariantTrack', function() { } it('should render variants', function() { - var popupId = null; - var getPopupTitle = function (id) { - popupId = id; - return "hello world, "+id; + var variantClickedData = null; + var variantClicked = function (data) { + variantClickedData = data; }; var p = pileup.create(testDiv, { range: {contig: '17', start: 9386380, stop: 9537390}, @@ -52,7 +51,7 @@ describe('VariantTrack', function() { url: '/test-data/test.vcf' }), viz: pileup.viz.variants(), - options: {getPopupTitleByVariantId: getPopupTitle} + options: {onVariantClicked: variantClicked}, } ] }); @@ -63,12 +62,12 @@ describe('VariantTrack', function() { expect(variants.length).to.be.equal(1); var canvasList = testDiv.getElementsByTagName('canvas'); var canvas = canvasList[1]; - expect(popupId).to.be.null; + expect(variantClickedData).to.be.null; //check clicking on variant ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: -0.5, offsetY: -15.5}}); - expect(popupId).to.not.be.null; + expect(variantClickedData).to.not.be.null; p.destroy(); }); }); From 8f50dd49a36bf44859afbb9afb894f1f6aba1a00 Mon Sep 17 00:00:00 2001 From: piotr-gawron Date: Fri, 11 Nov 2016 13:40:30 +0100 Subject: [PATCH 11/11] fix on default values of variant track options --- src/main/viz/VariantTrack.js | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/main/viz/VariantTrack.js b/src/main/viz/VariantTrack.js index b2374ed7..53d1c771 100644 --- a/src/main/viz/VariantTrack.js +++ b/src/main/viz/VariantTrack.js @@ -22,20 +22,10 @@ import canvasUtils from './canvas-utils'; import dataCanvas from 'data-canvas'; import style from '../style'; - class VariantTrack extends React.Component { - static propTypes: VizProps & {source: VcfDataSource, - variantHeightByFrequency: boolean, - allelFrequencyStrategy: Object, - onVariantClicked: Object, - }; - state: void; + props: VizProps & {source: VcfDataSource}; - static get defaultProps() { - return { - allelFrequencyStrategy: AllelFrequencyStrategy.Major, - }; - } + state: void; constructor(props: Object) { super(props); @@ -95,15 +85,17 @@ class VariantTrack extends React.Component { var variantHeightRatio = 1.0; if (this.props.options.variantHeightByFrequency) { var frequency = null; - if (this.props.allelFrequencyStrategy === AllelFrequencyStrategy.Major) { + 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.allelFrequencyStrategy === AllelFrequencyStrategy.Minor) { + } else if (this.props.options.allelFrequencyStrategy === AllelFrequencyStrategy.Minor) { frequency = variant.minorFrequency; } else { - console.log("Unknown AllelFrequencyStrategy: ",this.props.allelFrequencyStrategy); + console.log("Unknown AllelFrequencyStrategy: ",this.props.options.allelFrequencyStrategy); } - if (variant.significantFrequency !== null && variant.significantFrequency !== undefined) { - variantHeightRatio = variant.significantFrequency; + if (frequency !== null && frequency !== undefined) { + variantHeightRatio = frequency; } } var height = style.VARIANT_HEIGHT*variantHeightRatio;