From fdeab18bdb363af9c8a49ab7644129181ae01e4c Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Thu, 11 Feb 2021 10:56:48 -0600 Subject: [PATCH 1/6] do not allow users to view past contig ends --- src/main/Controls.js | 21 ++++-- src/main/Root.js | 83 ++++++++++++++++++++--- src/main/data/TwoBit.js | 50 ++++++++++---- src/main/sources/TwoBitDataSource.js | 26 +++++-- src/test/components-test.js | 37 +++++++++- src/test/data/TwoBit-test.js | 3 +- src/test/sources/TwoBitDataSource-test.js | 3 +- src/test/viz/FeatureTrack-test.js | 35 ++++++++-- src/test/viz/PileupTrack-test.js | 20 ++++-- style/pileup.css | 6 ++ 10 files changed, 233 insertions(+), 51 deletions(-) diff --git a/src/main/Controls.js b/src/main/Controls.js index a4014b33..4613a3cb 100644 --- a/src/main/Controls.js +++ b/src/main/Controls.js @@ -5,6 +5,7 @@ 'use strict'; import type {GenomeRange, PartialGenomeRange} from './types'; +import type ContigInterval from './ContigInterval'; import React from 'react'; import _ from 'underscore'; @@ -14,7 +15,7 @@ import Interval from './Interval'; type Props = { range: ?GenomeRange; - contigList: string[]; + contigList: ContigInterval[]; onChange: (newRange: GenomeRange)=>void; }; @@ -54,7 +55,7 @@ class Controls extends React.Component { // There are major performance issues with having a 'chr' mismatch in the // global location object. const contig = range.contig; - var altContig = _.find(this.props.contigList, ref => utils.isChrMatch(contig, ref)); + var altContig = _.find(this.props.contigList, ref => utils.isChrMatch(contig, ref.contig)).contig; if (altContig) range.contig = altContig; } @@ -86,7 +87,8 @@ class Controls extends React.Component { this.refs.position.value = utils.formatInterval(new Interval(r.start, r.stop)); if (this.props.contigList) { - var contigIdx = this.props.contigList.indexOf(r.contig); + var contigIdx = _.findIndex(this.props.contigList, ref => utils.isChrMatch(r.contig, ref.contig)); + this.refs.contig.selectedIndex = contigIdx; } } @@ -131,13 +133,12 @@ class Controls extends React.Component { // To be used if the range changes through a control besides the slider // Slider value is changed to roughly reflect the new range updateSlider(newInterval: Interval) { - var newSpan = (newInterval.stop - newInterval.start); - this.refs.slider.valueAsNumber = Math.ceil(-Math.log2(newSpan) + 1); + this.refs.slider.valueAsNumber = -1 * newInterval.stop; } render(): any { var contigOptions = this.props.contigList - ? this.props.contigList.map((contig, i) => ) + ? this.props.contigList.map((contig, i) => ) : null; // Note: input values are set in componentDidUpdate. @@ -161,6 +162,14 @@ class Controls extends React.Component { if (!_.isEqual(prevProps.range, this.props.range)) { this.updateRangeUI(); } + // Update slider if we have collected new information about a contig. + if (!_.isEqual(prevProps.contigList, this.props.contigList)) { + if (this.props.range != undefined) { + var newInterval = _.find(this.props.contigList, ref => utils.isChrMatch(this.props.range.contig, ref.contig)); + this.refs.slider.min = -1 * newInterval.stop(); + this.updateSlider(this.props.range); + } + } } componentDidMount() { diff --git a/src/main/Root.js b/src/main/Root.js index 87952b65..cf10e7ef 100644 --- a/src/main/Root.js +++ b/src/main/Root.js @@ -7,7 +7,8 @@ import type {GenomeRange} from './types'; import type {TwoBitSource} from './sources/TwoBitDataSource'; import type {VisualizedTrack, VizWithOptions} from './types'; - +import type ContigInterval from './ContigInterval'; +import utils from './utils'; import _ from 'underscore'; import React from 'react'; @@ -22,7 +23,7 @@ type Props = { }; type State = { - contigList: string[]; + contigList: ContigInterval[]; range: ?GenomeRange; settingsMenuKey: ?string; updateSize: boolean; @@ -50,9 +51,7 @@ class Root extends React.Component { componentDidMount() { this.props.referenceSource.on('contigs', () => { - this.setState({ - contigList: this.props.referenceSource.contigList(), - }); + this.updateOutOfBoundsChromosome(); }); if (!this.state.range) { @@ -63,13 +62,33 @@ class Root extends React.Component { } handleRangeChange(newRange: GenomeRange) { + + // copy over range so you don't modify + // this.state.range, which is bound to handleRangeChange + var modifiedRange = { + contig: newRange.contig, + start: newRange.start, + stop: newRange.stop, + }; + // Do not propagate negative ranges - if (newRange.start < 0) { - newRange.start = 0; + if (modifiedRange.start < 0) { + modifiedRange.start = 0; + } + // Do not propogate ranges exceeding contig limit + var contigInfo = _.find(this.state.contigList, ref => utils.isChrMatch(modifiedRange.contig, ref.contig)); + + if (contigInfo != undefined) { + if (modifiedRange.stop > contigInfo.stop()) { + modifiedRange.stop = contigInfo.stop(); + if (modifiedRange.start > modifiedRange.stop) { + modifiedRange.start = 0; + } + } } - this.props.referenceSource.normalizeRange(newRange).then(range => { - this.setState({range: range}); + this.props.referenceSource.normalizeRange(modifiedRange).then(range => { + this.setState({range: range}); // Inform all the sources of the range change (including referenceSource). this.props.tracks.forEach(track => { track.source.rangeChanged(range); @@ -160,7 +179,7 @@ class Root extends React.Component { ); } - + var className = ['track', track.visualization.component.displayName || '', track.track.cssClass || ''].join(' '); return ( @@ -198,6 +217,46 @@ class Root extends React.Component { ); } + updateOutOfBoundsChromosome(): any { + // We don't want to allow users to go to regions that extend past the end of + // a contig. This function truncates queries past the ends of a contig + // and updates the required states. + + var current_contig = this.props.initialRange; + if (this.state.range) { + current_contig = this.state.range.contig; + } + + var oldContig = _.find(this.state.contigList, ref => + utils.isChrMatch(current_contig, + ref.contig)); + + var contigList = this.props.referenceSource.contigList(); + + var newContig = _.find(contigList, ref => utils.isChrMatch(current_contig, ref.contig)); + + // only update if the current contig has new information regarding + // the end of the chromosome AND the current range is out of bounds + // with respect to chromosome length + if (newContig == undefined) { + this.setState({ + contigList: contigList + }); + } + + if (newContig && oldContig) { + if (!_.isEqual(oldContig, newContig)) { + // only trigger state if current contig changed + this.setState({ + contigList: contigList + }); + if (this.state.range.stop > newContig.stop()) { + this.handleRangeChange(this.state.range); + } + } + } + } + componentDidUpdate(prevProps: Props, prevState: Object) { if (this.state.updateSize) { for (var i=0;i { } this.state.updateSize=false; } + + this.props.referenceSource.on('contigs', () => { + this.updateOutOfBoundsChromosome(); + }); } } diff --git a/src/main/data/TwoBit.js b/src/main/data/TwoBit.js index d96b141e..17d1eb41 100644 --- a/src/main/data/TwoBit.js +++ b/src/main/data/TwoBit.js @@ -6,6 +6,7 @@ 'use strict'; import type RemoteFile from '../RemoteFile'; +import ContigInterval from '../ContigInterval'; import Q from 'q'; import _ from 'underscore'; @@ -206,9 +207,14 @@ function retryRemoteGet(remoteFile: RemoteFile, start: number, size: number, unt class TwoBit { remoteFile: RemoteFile; header: Q.Promise; + // Stores sequence records already seen. + // Used to keep track of lengths of each contig. + traversedSequenceRecords: {[key:string]: SequenceRecord}; + constructor(remoteFile: RemoteFile) { this.remoteFile = remoteFile; + this.traversedSequenceRecords = {}; var deferredHeader = Q.defer(); this.header = deferredHeader.promise; retryRemoteGet( @@ -250,8 +256,17 @@ class TwoBit { } // Returns a list of contig names. - getContigList(): Q.Promise { - return this.header.then(header => header.sequences.map(seq => seq.name)); + getContigList(): Q.Promise { + return this.header.then(header => { + return header.sequences.map(seq => { + // fill in end if collected + var numBases = Number.MAX_VALUE; + if (this.traversedSequenceRecords[seq.name]) { + numBases = this.traversedSequenceRecords[seq.name].numBases; + } + return new ContigInterval(seq.name, 0, numBases); + }); + }); } _getSequenceHeader(contig: string): Q.Promise { @@ -263,17 +278,26 @@ class TwoBit { } var seq = maybeSeq; // for flow, see facebook/flow#266 - return retryRemoteGet( - this.remoteFile, - seq.offset, - FIRST_SEQUENCE_CHUNKSIZE, - MAX_CHUNKSIZE, - buffer => { - return parseWithException(() => { - return parseSequenceRecord(buffer, seq.offset); - }); - } - ); + if (contig in this.traversedSequenceRecords) { + return Q.when(this.traversedSequenceRecords[contig]); + } else { + return retryRemoteGet( + this.remoteFile, + seq.offset, + FIRST_SEQUENCE_CHUNKSIZE, + MAX_CHUNKSIZE, + buffer => { + return parseWithException(() => { + + if (!this.traversedSequenceRecords[seq.name]) { + this.traversedSequenceRecords[contig] = + parseSequenceRecord(buffer, seq.offset); + } + return this.traversedSequenceRecords[contig]; + }); + } + ); + } }).then(p => { return p; }); diff --git a/src/main/sources/TwoBitDataSource.js b/src/main/sources/TwoBitDataSource.js index c4d993da..3eefdecc 100644 --- a/src/main/sources/TwoBitDataSource.js +++ b/src/main/sources/TwoBitDataSource.js @@ -42,7 +42,7 @@ export type TwoBitSource = { rangeChanged: (newRange: GenomeRange) => void; getRange: (range: GenomeRange) => {[key:string]: ?string}; getRangeAsString: (range: GenomeRange) => string; - contigList: () => string[]; + contigList: () => ContigInterval[]; normalizeRange: (range: GenomeRange) => Q.Promise; on: (event: string, handler: Function) => void; once: (event: string, handler: Function) => void; @@ -62,9 +62,13 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource { function fetch(range: ContigInterval) { var span = range.length(); if (span > MAX_BASE_PAIRS_TO_FETCH) { - //inform that we won't fetch the data - o.trigger('newdatarefused', range); - return Q.when(); // empty promise + // trigger header collection to get information about this chromosome, + // but don't fetch the actual data. + return remoteSource._getSequenceHeader(range.contig).then(header => remoteSource.getContigList()).then(c => { + contigList = c; + o.trigger('contigs', contigList); + o.trigger('newdatarefused', range); + }).done(); } //now we can add region to covered regions //doing it earlier would provide inconsistency @@ -81,18 +85,26 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource { range.start() + letters.length - 1); } store.setRange(range, letters); - }).then(() => { + }).then(() => remoteSource.getContigList()).then(c => { + contigList = c; + o.trigger('contigs', contigList); o.trigger('newdata', range); }).done(); } // This either adds or removes a 'chr' as needed. function normalizeRangeSync(range: GenomeRange): GenomeRange { - if (contigList.indexOf(range.contig) >= 0) { + + var contigIdx = _.findIndex(contigList, ref => utils.isChrMatch(range.contig, ref.contig)); + + if (contigIdx >= 0) { return range; } var altContig = utils.altContigName(range.contig); - if (contigList.indexOf(altContig) >= 0) { + + var contigIdx = _.findIndex(contigList, ref => utils.isChrMatch(altContig, ref.contig)); + + if (contigIdx >= 0) { return { contig: altContig, start: range.start, diff --git a/src/test/components-test.js b/src/test/components-test.js index d81c6529..1e5801c9 100644 --- a/src/test/components-test.js +++ b/src/test/components-test.js @@ -95,7 +95,7 @@ describe('pileup', function() { hasCanvasAndObjects(testDiv, '.pileup') ); - var rangeChanged = ((): boolean => + var rangeChanged = ((range): boolean => // $FlowIgnore: TODO remove flow suppression initialRange != p.getRange() ); @@ -181,6 +181,41 @@ describe('pileup', function() { }); }); + it('should restrict viewing region outside of chromosome', function(done): any { + this.timeout(5000); + + makePileup(); + + waitFor(ready, 5000) + .then(() => { + + expect(p.getRange()).to.deep.equal({ + contig: 'chr17', + start: 100, + stop: 150 + }); + + // out of bounds for chromosome + var range = { + contig: 'chr17', + start: 100000000, + stop: 100000200 + }; + + p.setRange(range); + + waitFor(rangeChanged, 5000) + .then(() => { + expect(p.getRange()).to.deep.equal({ + contig: 'chr17', + start: 0, + stop: 19198 + }); + done(); + }); + }); + }); + it('should save SVG', function(done): any { this.timeout(5000); diff --git a/src/test/data/TwoBit-test.js b/src/test/data/TwoBit-test.js index 8f789396..5f38ca59 100644 --- a/src/test/data/TwoBit-test.js +++ b/src/test/data/TwoBit-test.js @@ -15,7 +15,8 @@ describe('TwoBit', function() { it('should have the right contigs', function(): any { var twoBit = getTestTwoBit(); return twoBit.getContigList() - .then(contigs => { + .then(contigList => { + var contigs = contigList.map(c => c.contig); expect(contigs).to.deep.equal(['chr1', 'chr17', 'chr22']); }); }); diff --git a/src/test/sources/TwoBitDataSource-test.js b/src/test/sources/TwoBitDataSource-test.js index fefaa7f4..9f627c2a 100644 --- a/src/test/sources/TwoBitDataSource-test.js +++ b/src/test/sources/TwoBitDataSource-test.js @@ -25,7 +25,8 @@ describe('TwoBitDataSource', function() { it('should fetch contigs', function(done) { var source = getTestSource(); - source.on('contigs', contigs => { + source.on('contigs', contigList => { + var contigs = contigList.map(c => c.contig); expect(contigs).to.deep.equal(['chr1', 'chr17', 'chr22']); done(); }); diff --git a/src/test/viz/FeatureTrack-test.js b/src/test/viz/FeatureTrack-test.js index a134b86b..8d9591f0 100644 --- a/src/test/viz/FeatureTrack-test.js +++ b/src/test/viz/FeatureTrack-test.js @@ -18,6 +18,27 @@ import {yForRow} from '../../main/viz/pileuputils'; import ReactTestUtils from 'react-dom/test-utils'; +// We need a fake TwoBit file to query regions that extend the bounds in test.2bit +class FakeTwoBit extends TwoBit { + deferred: Object; + + constructor(remoteFile: RemoteFile) { + super(remoteFile); + this.deferred = Q.defer(); + } + + getFeaturesInRange(contig: string, start: number, stop: number): Q.Promise { + expect(contig).to.equal('chr17'); + expect(start).to.equal(7500000); + expect(stop).to.equal(7510000); + return this.deferred.promise; + } + + release(sequence: string) { + this.deferred.resolve(sequence); + } +} + describe('FeatureTrack', function() { var testDiv= document.getElementById('testdiv'); if (!testDiv) throw new Error("Failed to match: testdiv"); @@ -56,14 +77,16 @@ describe('FeatureTrack', function() { featureClickedData = data; }; + var fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + + var p = pileup.create(testDiv, { range: {contig: 'chr1', start: 130000, stop: 135000}, tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { @@ -120,15 +143,15 @@ describe('FeatureTrack', function() { }); it('should render features with bigBed file', function(): any { + var fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); var p = pileup.create(testDiv, { range: {contig: 'chr17', start: 10000, stop: 16500}, tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { diff --git a/src/test/viz/PileupTrack-test.js b/src/test/viz/PileupTrack-test.js index 6e1bbf60..9415ff11 100644 --- a/src/test/viz/PileupTrack-test.js +++ b/src/test/viz/PileupTrack-test.js @@ -208,13 +208,17 @@ describe('PileupTrack', function() { }); it('should hide alignments', function(): any { + + // The fake sources allow precise control over when they give up their data. + var fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + + var p = pileup.create(testDiv, { range: {contig: 'chr17', start: 7500734, stop: 7500796}, tracks: [ { - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, viz: pileup.viz.genome(), isReference: true }, @@ -238,13 +242,17 @@ describe('PileupTrack', function() { }); it('should sort reads', function(): any { + + // The fake sources allow precise control over when they give up their data. + var fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + + var p = pileup.create(testDiv, { range: {contig: 'chr17', start: 7500734, stop: 7500796}, tracks: [ { - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, viz: pileup.viz.genome(), isReference: true }, diff --git a/style/pileup.css b/style/pileup.css index 4c911985..612be33a 100644 --- a/style/pileup.css +++ b/style/pileup.css @@ -110,6 +110,12 @@ border: 1px solid #bcbbbb; vertical-align: middle; } +.controls button:active { + background-color: #bcbbbb; /* make button interactive */ +} +.controls button:focus { + outline: none !important; /* remove blue line from buttons */ +} .zoom-slider { margin-left: 10px; /* leave space between slider and zoom buttons */ display: inline-block !important; /* imporant overrides input[type="range"] from forms.less */ From 0e0fe9b13fdf77d11133470825ef912ff0877b1f Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Thu, 11 Feb 2021 17:32:59 -0600 Subject: [PATCH 2/6] rebased tests to use fakeTwoBit to find regions in range --- lib/underscore.js | 1 + src/main/Controls.js | 18 ++++++---- src/main/Root.js | 13 ++++--- src/main/data/TwoBit.js | 2 +- src/main/sources/TwoBitDataSource.js | 7 ++-- src/main/viz/GeneTrack.js | 1 - src/test/FakeAlignment.js | 2 +- src/test/viz/FeatureTrack-test.js | 54 ++++++++++++++++------------ src/test/viz/GeneTrack-test.js | 33 +++++++++-------- src/test/viz/GenotypeTrack-test.js | 42 ++++++++++++++-------- src/test/viz/VariantTrack-test.js | 28 +++++++++++++-- 11 files changed, 131 insertions(+), 70 deletions(-) diff --git a/lib/underscore.js b/lib/underscore.js index f0c24870..0fb81af1 100644 --- a/lib/underscore.js +++ b/lib/underscore.js @@ -2,6 +2,7 @@ declare module "underscore" { declare function find(list: T[], predicate: (val: T)=>boolean): ?T; + declare function findIndex(list: T[], predicate: (val: T)=>boolean): number; declare function findWhere(list: Array, properties: {[key:string]: any}): ?T; declare function clone(obj: T): T; diff --git a/src/main/Controls.js b/src/main/Controls.js index 4613a3cb..da8064f3 100644 --- a/src/main/Controls.js +++ b/src/main/Controls.js @@ -15,7 +15,7 @@ import Interval from './Interval'; type Props = { range: ?GenomeRange; - contigList: ContigInterval[]; + contigList: ContigInterval[]; onChange: (newRange: GenomeRange)=>void; }; @@ -55,8 +55,8 @@ class Controls extends React.Component { // There are major performance issues with having a 'chr' mismatch in the // global location object. const contig = range.contig; - var altContig = _.find(this.props.contigList, ref => utils.isChrMatch(contig, ref.contig)).contig; - if (altContig) range.contig = altContig; + var altContig = _.find(this.props.contigList, ref => utils.isChrMatch(contig, ref.contig)); + if (altContig) range.contig = altContig.contig; } return (_.extend(_.clone(this.props.range), range) : any); @@ -165,9 +165,15 @@ class Controls extends React.Component { // Update slider if we have collected new information about a contig. if (!_.isEqual(prevProps.contigList, this.props.contigList)) { if (this.props.range != undefined) { - var newInterval = _.find(this.props.contigList, ref => utils.isChrMatch(this.props.range.contig, ref.contig)); - this.refs.slider.min = -1 * newInterval.stop(); - this.updateSlider(this.props.range); + var range = this.props.range; // flow + if (range.contig != undefined) { + var newInterval = _.find(this.props.contigList, ref => utils.isChrMatch(range.contig, ref.contig)); + + if (newInterval != undefined) { + this.refs.slider.min = -1 * newInterval.stop(); + this.updateSlider(new Interval(range.start, range.stop)); + } + } } } } diff --git a/src/main/Root.js b/src/main/Root.js index cf10e7ef..6fc17e2e 100644 --- a/src/main/Root.js +++ b/src/main/Root.js @@ -23,7 +23,7 @@ type Props = { }; type State = { - contigList: ContigInterval[]; + contigList: ContigInterval[]; range: ?GenomeRange; settingsMenuKey: ?string; updateSize: boolean; @@ -222,7 +222,7 @@ class Root extends React.Component { // a contig. This function truncates queries past the ends of a contig // and updates the required states. - var current_contig = this.props.initialRange; + var current_contig = this.props.initialRange.contig; if (this.state.range) { current_contig = this.state.range.contig; } @@ -238,7 +238,7 @@ class Root extends React.Component { // only update if the current contig has new information regarding // the end of the chromosome AND the current range is out of bounds // with respect to chromosome length - if (newContig == undefined) { + if (this.state.contigList.length == 0) { this.setState({ contigList: contigList }); @@ -250,8 +250,11 @@ class Root extends React.Component { this.setState({ contigList: contigList }); - if (this.state.range.stop > newContig.stop()) { - this.handleRangeChange(this.state.range); + if (this.state.range !== null && this.state.range !== undefined) { + if (this.state.range.stop > newContig.stop()) { + // $FlowIgnore: TODO remove flow suppression + this.handleRangeChange(this.state.range); + } } } } diff --git a/src/main/data/TwoBit.js b/src/main/data/TwoBit.js index 17d1eb41..cc1c4f9c 100644 --- a/src/main/data/TwoBit.js +++ b/src/main/data/TwoBit.js @@ -256,7 +256,7 @@ class TwoBit { } // Returns a list of contig names. - getContigList(): Q.Promise { + getContigList(): Q.Promise[]> { return this.header.then(header => { return header.sequences.map(seq => { // fill in end if collected diff --git a/src/main/sources/TwoBitDataSource.js b/src/main/sources/TwoBitDataSource.js index 3eefdecc..6ef6965a 100644 --- a/src/main/sources/TwoBitDataSource.js +++ b/src/main/sources/TwoBitDataSource.js @@ -42,7 +42,7 @@ export type TwoBitSource = { rangeChanged: (newRange: GenomeRange) => void; getRange: (range: GenomeRange) => {[key:string]: ?string}; getRangeAsString: (range: GenomeRange) => string; - contigList: () => ContigInterval[]; + contigList: () => ContigInterval[]; normalizeRange: (range: GenomeRange) => Q.Promise; on: (event: string, handler: Function) => void; once: (event: string, handler: Function) => void; @@ -95,14 +95,15 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource { // This either adds or removes a 'chr' as needed. function normalizeRangeSync(range: GenomeRange): GenomeRange { - var contigIdx = _.findIndex(contigList, ref => utils.isChrMatch(range.contig, ref.contig)); + // check for direct match + var contigIdx = _.findIndex(contigList, ref => range.contig == ref.contig); if (contigIdx >= 0) { return range; } var altContig = utils.altContigName(range.contig); - var contigIdx = _.findIndex(contigList, ref => utils.isChrMatch(altContig, ref.contig)); + contigIdx = _.findIndex(contigList, ref => altContig == ref.contig); if (contigIdx >= 0) { return { diff --git a/src/main/viz/GeneTrack.js b/src/main/viz/GeneTrack.js index a213d1fb..509524ac 100644 --- a/src/main/viz/GeneTrack.js +++ b/src/main/viz/GeneTrack.js @@ -28,7 +28,6 @@ import utils from '../utils'; import dataCanvas from 'data-canvas'; import style from '../style'; - // Draw an arrow in the middle of the visible portion of range. function drawArrow(ctx: CanvasRenderingContext2D, clampedScale: (x: number)=>number, diff --git a/src/test/FakeAlignment.js b/src/test/FakeAlignment.js index b06bde6b..82a0dd79 100644 --- a/src/test/FakeAlignment.js +++ b/src/test/FakeAlignment.js @@ -76,7 +76,7 @@ var fakeSource = { rangeChanged: dieFn, getRange: function(): any { return {}; }, getRangeAsString: function(): string { return ''; }, - contigList: function(): string[] { return []; }, + contigList: function(): ContigInterval[] { return []; }, normalizeRange: function(range: GenomeRange): Q.Promise { return Q.when(range); }, on: dieFn, off: dieFn, diff --git a/src/test/viz/FeatureTrack-test.js b/src/test/viz/FeatureTrack-test.js index 8d9591f0..917cd3f1 100644 --- a/src/test/viz/FeatureTrack-test.js +++ b/src/test/viz/FeatureTrack-test.js @@ -10,6 +10,10 @@ import {expect} from 'chai'; import _ from 'underscore'; import RemoteFile from '../../main/RemoteFile'; +import TwoBit from '../../main/data/TwoBit'; +import TwoBitDataSource from '../../main/sources/TwoBitDataSource'; +import MappedRemoteFile from '../MappedRemoteFile'; +import {FakeTwoBit} from '../FakeTwoBit'; import pileup from '../../main/pileup'; import dataCanvas from 'data-canvas'; import {waitFor} from '../async'; @@ -18,26 +22,6 @@ import {yForRow} from '../../main/viz/pileuputils'; import ReactTestUtils from 'react-dom/test-utils'; -// We need a fake TwoBit file to query regions that extend the bounds in test.2bit -class FakeTwoBit extends TwoBit { - deferred: Object; - - constructor(remoteFile: RemoteFile) { - super(remoteFile); - this.deferred = Q.defer(); - } - - getFeaturesInRange(contig: string, start: number, stop: number): Q.Promise { - expect(contig).to.equal('chr17'); - expect(start).to.equal(7500000); - expect(stop).to.equal(7510000); - return this.deferred.promise; - } - - release(sequence: string) { - this.deferred.resolve(sequence); - } -} describe('FeatureTrack', function() { var testDiv= document.getElementById('testdiv'); @@ -51,8 +35,12 @@ describe('FeatureTrack', function() { drawnObjects(testDiv, '.features').length > 0; } + // Test data files + var twoBitFile = new MappedRemoteFile( + '/test-data/hg19.2bit.mapped', + [[0, 16383], [691179834, 691183928], [694008946, 694011447]]); + describe('jsonFeatures', function() { - var json; beforeEach(() => { testDiv.style.width = '800px'; @@ -65,8 +53,16 @@ describe('FeatureTrack', function() { testDiv.innerHTML = ''; }); + var reference: string = ''; + var json; + before(function(): any { - return new RemoteFile('/test-data/features.ga4gh.chr1.120000-125000.chr17.7500000-7515100.json').getAllString().then(data => { + var twoBit = new TwoBit(twoBitFile); + return twoBit.getFeaturesInRange('chr17', 7500000, 7510000).then(seq => { + reference = seq; + return new RemoteFile('/test-data/features.ga4gh.chr1.120000-125000.chr17.7500000-7515100.json').getAllString(); + }).then(data => { + expect(data).to.have.length.above(0); json = data; }); }); @@ -80,6 +76,8 @@ describe('FeatureTrack', function() { var fakeTwoBit = new FakeTwoBit(twoBitFile), referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + // Release the reference first. + fakeTwoBit.release(reference); var p = pileup.create(testDiv, { range: {contig: 'chr1', start: 130000, stop: 135000}, @@ -142,10 +140,22 @@ describe('FeatureTrack', function() { testDiv.innerHTML = ''; }); + var reference: string = ''; + + before(function(): any { + var twoBit = new TwoBit(twoBitFile); + return twoBit.getFeaturesInRange('chr17', 7500000, 7510000).then(seq => { + reference = seq; + }); + }); + it('should render features with bigBed file', function(): any { var fakeTwoBit = new FakeTwoBit(twoBitFile), referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + // Release the reference first. + fakeTwoBit.release(reference); + var p = pileup.create(testDiv, { range: {contig: 'chr17', start: 10000, stop: 16500}, tracks: [ diff --git a/src/test/viz/GeneTrack-test.js b/src/test/viz/GeneTrack-test.js index a89d30d8..765c4204 100644 --- a/src/test/viz/GeneTrack-test.js +++ b/src/test/viz/GeneTrack-test.js @@ -10,12 +10,16 @@ import sinon from 'sinon'; import {expect} from 'chai'; +import Q from 'q'; import pileup from '../../main/pileup'; import dataCanvas from 'data-canvas'; import {waitFor} from '../async'; import RemoteFile from '../../main/RemoteFile'; - +import TwoBit from '../../main/data/TwoBit'; +import TwoBitDataSource from '../../main/sources/TwoBitDataSource'; +import MappedRemoteFile from '../MappedRemoteFile'; +import {FakeTwoBit} from '../FakeTwoBit'; describe('GeneTrack', function() { var testDiv = document.getElementById('testdiv'); @@ -23,6 +27,16 @@ describe('GeneTrack', function() { var server: any = null, response; + // Test data files + var twoBitFile = new MappedRemoteFile( + '/test-data/hg19.2bit.mapped', + [[0, 16383], [691179834, 691183928], [694008946, 694011447]]); + + + var fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + + before((): any => { // server for genes return new RemoteFile('/test-data/refSeqGenes.chr17.75000000-75100000.json').getAllString().then(data => { @@ -31,11 +45,8 @@ describe('GeneTrack', function() { server.autoRespond = true; - // Sinon should ignore 2bit request. RemoteFile handles this request. + // // Sinon should ignore 2bit request. RemoteFile handles this request. sinon.fakeServer.xhr.useFilters = true; - sinon.fakeServer.xhr.addFilter(function (method, url) { - return url === '/test-data/test.2bit'; - }); sinon.fakeServer.xhr.addFilter(function (method, url) { return url === '/test-data/hg19.2bit.mapped'; }); @@ -72,9 +83,7 @@ describe('GeneTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { @@ -110,9 +119,7 @@ describe('GeneTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { @@ -142,9 +149,7 @@ describe('GeneTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { diff --git a/src/test/viz/GenotypeTrack-test.js b/src/test/viz/GenotypeTrack-test.js index 60d7f6b6..b8f60a75 100644 --- a/src/test/viz/GenotypeTrack-test.js +++ b/src/test/viz/GenotypeTrack-test.js @@ -15,16 +15,31 @@ import pileup from '../../main/pileup'; import dataCanvas from 'data-canvas'; import {waitFor} from '../async'; import RemoteFile from '../../main/RemoteFile'; +import TwoBit from '../../main/data/TwoBit'; +import TwoBitDataSource from '../../main/sources/TwoBitDataSource'; +import MappedRemoteFile from '../MappedRemoteFile'; +import {FakeTwoBit} from '../FakeTwoBit'; describe('GenotypeTrack', function() { var server: any = null, response; + var reference: string = ''; + var fakeTwoBit, referenceSource; var testDiv = document.getElementById('testdiv'); if (!testDiv) throw new Error("Failed to match: testdiv"); - before(function(): any { + // Test data files + var twoBitFile = new MappedRemoteFile( + '/test-data/hg19.2bit.mapped', + [[0, 16383], [691179834, 691183928], [694008946, 694011447]]); + - return new RemoteFile('/test-data/variants.ga4gh.chr1.10000-11000.json').getAllString().then(data => { + before(function(): any { + var twoBit = new TwoBit(twoBitFile); + return twoBit.getFeaturesInRange('17', 7500000, 7510000).then(seq => { + reference = seq; + return new RemoteFile('/test-data/variants.ga4gh.chr1.10000-11000.json').getAllString(); + }).then(data => { response = data; server = sinon.createFakeServer(); @@ -39,6 +54,13 @@ describe('GenotypeTrack', function() { sinon.fakeServer.xhr.addFilter(function (method, url) { return url === '/test-data/test.vcf'; }); + + fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + + // Release the reference first. + fakeTwoBit.release(reference); + }); }); @@ -72,9 +94,7 @@ describe('GenotypeTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { @@ -121,9 +141,7 @@ describe('GenotypeTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { @@ -171,9 +189,7 @@ describe('GenotypeTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { @@ -214,9 +230,7 @@ describe('GenotypeTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { diff --git a/src/test/viz/VariantTrack-test.js b/src/test/viz/VariantTrack-test.js index bf64be2b..c8d7e593 100644 --- a/src/test/viz/VariantTrack-test.js +++ b/src/test/viz/VariantTrack-test.js @@ -8,12 +8,29 @@ import {expect} from 'chai'; import pileup from '../../main/pileup'; import dataCanvas from 'data-canvas'; import {waitFor} from '../async'; +import TwoBit from '../../main/data/TwoBit'; +import TwoBitDataSource from '../../main/sources/TwoBitDataSource'; +import MappedRemoteFile from '../MappedRemoteFile'; +import {FakeTwoBit} from '../FakeTwoBit'; import ReactTestUtils from 'react-dom/test-utils'; describe('VariantTrack', function() { var testDiv = document.getElementById('testdiv'); if (!testDiv) throw new Error("Failed to match: testdiv"); + var reference: string = ''; + + // Test data files + var twoBitFile = new MappedRemoteFile( + '/test-data/hg19.2bit.mapped', + [[0, 16383], [691179834, 691183928], [694008946, 694011447]]); + + before(function(): any { + var twoBit = new TwoBit(twoBitFile); + return twoBit.getFeaturesInRange('17', 7500000, 7510000).then(seq => { + reference = seq; + }); + }); beforeEach(() => { testDiv.style.width = '700px'; @@ -33,6 +50,13 @@ describe('VariantTrack', function() { } it('should render variants', function(): any { + + var fakeTwoBit = new FakeTwoBit(twoBitFile), + referenceSource = TwoBitDataSource.createFromTwoBitFile(fakeTwoBit); + + // Release the reference first. + fakeTwoBit.release(reference); + var variantClickedData = null; var variantClicked = function(data) { variantClickedData = data; @@ -42,9 +66,7 @@ describe('VariantTrack', function() { tracks: [ { viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), + data: referenceSource, isReference: true }, { From 23b8cbdd570605985b4e698c226d43a973ce134a Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Thu, 11 Feb 2021 17:36:16 -0600 Subject: [PATCH 3/6] linter --- src/test/viz/GeneTrack-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/viz/GeneTrack-test.js b/src/test/viz/GeneTrack-test.js index 765c4204..c794cdae 100644 --- a/src/test/viz/GeneTrack-test.js +++ b/src/test/viz/GeneTrack-test.js @@ -10,13 +10,11 @@ import sinon from 'sinon'; import {expect} from 'chai'; -import Q from 'q'; import pileup from '../../main/pileup'; import dataCanvas from 'data-canvas'; import {waitFor} from '../async'; import RemoteFile from '../../main/RemoteFile'; -import TwoBit from '../../main/data/TwoBit'; import TwoBitDataSource from '../../main/sources/TwoBitDataSource'; import MappedRemoteFile from '../MappedRemoteFile'; import {FakeTwoBit} from '../FakeTwoBit'; From db2b5d89187631b896507100c2070e89d5105ed7 Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Thu, 11 Feb 2021 17:47:36 -0600 Subject: [PATCH 4/6] fix slider --- src/main/Controls.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/Controls.js b/src/main/Controls.js index da8064f3..4a5948f6 100644 --- a/src/main/Controls.js +++ b/src/main/Controls.js @@ -133,7 +133,8 @@ class Controls extends React.Component { // To be used if the range changes through a control besides the slider // Slider value is changed to roughly reflect the new range updateSlider(newInterval: Interval) { - this.refs.slider.valueAsNumber = -1 * newInterval.stop; + var newSpan = (newInterval.stop - newInterval.start); + this.refs.slider.valueAsNumber = Math.ceil(-Math.log2(newSpan) + 1); } render(): any { @@ -170,7 +171,7 @@ class Controls extends React.Component { var newInterval = _.find(this.props.contigList, ref => utils.isChrMatch(range.contig, ref.contig)); if (newInterval != undefined) { - this.refs.slider.min = -1 * newInterval.stop(); + this.refs.slider.min = Math.ceil(-Math.log2(newInterval.stop()) + 1); this.updateSlider(new Interval(range.start, range.stop)); } } From ff7c915de1cff8c528c20792e6fc020dcfae4e1c Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Fri, 12 Feb 2021 09:25:57 -0600 Subject: [PATCH 5/6] added FakeTwoBit --- src/test/FakeTwoBit.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/test/FakeTwoBit.js diff --git a/src/test/FakeTwoBit.js b/src/test/FakeTwoBit.js new file mode 100644 index 00000000..64070d7c --- /dev/null +++ b/src/test/FakeTwoBit.js @@ -0,0 +1,32 @@ +/** + * fake TwoBit file. + * Used to query regions that extend the bounds in + * the test twobit files. + * + * @flow + */ +import Q from 'q'; +import TwoBit from '../main/data/TwoBit'; +import RemoteFile from '../main/RemoteFile'; + + +class FakeTwoBit extends TwoBit { + deferred: Object; + + constructor(remoteFile: RemoteFile) { + super(remoteFile); + this.deferred = Q.defer(); + } + + getFeaturesInRange(contig: string, start: number, stop: number): Q.Promise { + return this.deferred.promise; + } + + release(sequence: string) { + this.deferred.resolve(sequence); + } +} + +module.exports = { + FakeTwoBit +}; From cabe15c545ed865d89e17d7ce89e0856541b2d66 Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Tue, 16 Feb 2021 16:12:45 -0600 Subject: [PATCH 6/6] clean up --- src/main/data/TwoBit.js | 2 +- src/main/viz/GeneTrack.js | 1 + src/test/viz/GeneTrack-test.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/data/TwoBit.js b/src/main/data/TwoBit.js index cc1c4f9c..c7282fc3 100644 --- a/src/main/data/TwoBit.js +++ b/src/main/data/TwoBit.js @@ -259,7 +259,7 @@ class TwoBit { getContigList(): Q.Promise[]> { return this.header.then(header => { return header.sequences.map(seq => { - // fill in end if collected + // fill in numBases if collected var numBases = Number.MAX_VALUE; if (this.traversedSequenceRecords[seq.name]) { numBases = this.traversedSequenceRecords[seq.name].numBases; diff --git a/src/main/viz/GeneTrack.js b/src/main/viz/GeneTrack.js index 509524ac..a213d1fb 100644 --- a/src/main/viz/GeneTrack.js +++ b/src/main/viz/GeneTrack.js @@ -28,6 +28,7 @@ import utils from '../utils'; import dataCanvas from 'data-canvas'; import style from '../style'; + // Draw an arrow in the middle of the visible portion of range. function drawArrow(ctx: CanvasRenderingContext2D, clampedScale: (x: number)=>number, diff --git a/src/test/viz/GeneTrack-test.js b/src/test/viz/GeneTrack-test.js index c794cdae..d9803b2e 100644 --- a/src/test/viz/GeneTrack-test.js +++ b/src/test/viz/GeneTrack-test.js @@ -43,7 +43,7 @@ describe('GeneTrack', function() { server.autoRespond = true; - // // Sinon should ignore 2bit request. RemoteFile handles this request. + // Sinon should ignore 2bit request. RemoteFile handles this request. sinon.fakeServer.xhr.useFilters = true; sinon.fakeServer.xhr.addFilter(function (method, url) { return url === '/test-data/hg19.2bit.mapped';