From 006635ed9712a627fc96c1a0c6975b393cb9be2d Mon Sep 17 00:00:00 2001 From: akmorrow13 Date: Mon, 25 Nov 2019 23:56:05 -0800 Subject: [PATCH] Karyogram geena (#537) * Implemented Idiogram with data sources --- examples/data.js | 5 + lib/underscore.js | 1 + src/main/data/chromosome.js | 57 + src/main/data/cytoBand.js | 82 + src/main/data/genericFeature.js | 4 +- src/main/json/IdiogramJson.js | 55 + src/main/pileup.js | 7 + src/main/sources/CytoBandDataSource.js | 83 + src/main/sources/DataSource.js | 3 +- src/main/style.js | 15 + src/main/viz/GeneTrack.js | 2 +- src/main/viz/IdiogramTrack.js | 185 + src/test/data/cytoBand-test.js | 26 + src/test/json/IdiogramJson-test.js | 42 + src/test/sources/CytoBandDataSource-test.js | 52 + src/test/viz/IdiogramTrack-test.js | 105 + style/pileup.css | 11 +- test-data/cytoBand.txt.gz | Bin 0 -> 6609 bytes test-data/gstained_chromosomes_data.json | 5294 +++++++++++++++++++ 19 files changed, 6023 insertions(+), 6 deletions(-) create mode 100644 src/main/data/chromosome.js create mode 100644 src/main/data/cytoBand.js create mode 100644 src/main/json/IdiogramJson.js create mode 100644 src/main/sources/CytoBandDataSource.js create mode 100644 src/main/viz/IdiogramTrack.js create mode 100644 src/test/data/cytoBand-test.js create mode 100644 src/test/json/IdiogramJson-test.js create mode 100644 src/test/sources/CytoBandDataSource-test.js create mode 100644 src/test/viz/IdiogramTrack-test.js create mode 100644 test-data/cytoBand.txt.gz create mode 100644 test-data/gstained_chromosomes_data.json diff --git a/examples/data.js b/examples/data.js index af1bb412..50e6a49f 100644 --- a/examples/data.js +++ b/examples/data.js @@ -15,6 +15,11 @@ var sources = [ }), name: 'Reference' }, + { + viz: pileup.viz.idiogram(), + data: pileup.formats.cytoBand('/test-data/cytoBand.txt.gz'), + name: 'Idiogram' + }, { viz: pileup.viz.scale(), name: 'Scale' diff --git a/lib/underscore.js b/lib/underscore.js index 4c46165f..f0c24870 100644 --- a/lib/underscore.js +++ b/lib/underscore.js @@ -42,6 +42,7 @@ declare module "underscore" { declare function groupBy(a: Array, iteratee: (val: T, index: number)=>K): {[key:K]: T[]}; declare function min(a: Array|{[key:any]: T}, iteratee: (val: T)=>any): T; + declare function max(a: Array|{[key:any]: T}, iteratee: (val: T)=>any): T; declare function max(a: Array|{[key:any]: T}): T; declare function values(o: {[key: any]: T}): T[]; diff --git a/src/main/data/chromosome.js b/src/main/data/chromosome.js new file mode 100644 index 00000000..fbc3c5f6 --- /dev/null +++ b/src/main/data/chromosome.js @@ -0,0 +1,57 @@ +/** + * Class for parsing chromosome bands for idiograms. + * Format taken from https://github.com/hammerlab/idiogrammatik. + * @flow + */ +'use strict'; + +import ContigInterval from '../ContigInterval'; +import type {CoverageCount} from '../viz/pileuputils'; +import _ from 'underscore'; + +// chromosomal band (see https://github.com/hammerlab/idiogrammatik/blob/master/data/basic-chromosomes.json) +// for an example +export type Band = { + start: number; + end: number; + name: string; + value: string; +} + +class Chromosome { + name: string; + bands: Band[]; + position: ContigInterval; + + constructor(chromosome: Object) { + this.name = chromosome.name; + this.bands = chromosome.bands; + // create region for chromosome + var start = _.min(this.bands, band => band.start).start; + + + var stop = _.max(this.bands, band => band.end).end; + this.position = new ContigInterval(this.name, start, stop); + } + + getKey(): string { + return this.name; + } + + getInterval(): ContigInterval { + return this.position; + } + + getCoverage(referenceSource: Object): CoverageCount { + return { + range: this.getInterval(), + opInfo: null + }; + } + + intersects(range: ContigInterval): boolean { + return range.intersects(this.position); + } +} + +module.exports = Chromosome; diff --git a/src/main/data/cytoBand.js b/src/main/data/cytoBand.js new file mode 100644 index 00000000..d36f61cd --- /dev/null +++ b/src/main/data/cytoBand.js @@ -0,0 +1,82 @@ +/** + * Fetcher/parser for gzipped Cytoband files. Cytoband files can be downloaded + * or accessed from http://hgdownload.cse.ucsc.edu/goldenpath for a genome build. + * + * extracts CONTIG, START, END, NAME and VALUE + * + * @flow + */ +'use strict'; + +import type AbstractFile from '../AbstractFile'; +import type Q from 'q'; +import _ from 'underscore'; +import ContigInterval from '../ContigInterval'; +import Chromosome from './chromosome'; +import pako from 'pako/lib/inflate'; // for gzip inflation + +function extractLine(cytoBandLine: string): Object { + var split = cytoBandLine.split('\t'); + return { + contig: split[0], + band: { + start: Number(split[1]), + end: Number(split[2]), + name: split[3], + value: split[4] + } + }; +} + +function groupedBandsToChromosome(grouped: Object[]): Chromosome { + var bands = _.map(grouped, g => g.band); + return new Chromosome( + {name: grouped[0].contig, + bands: bands, + position: new ContigInterval(grouped[0].contig, bands[0].start, bands.slice(-1)[0].end)} + ); +} + +class ImmediateCytoBandFile { + chrs: {[key:string]:Chromosome}; + + constructor(chrs: {[key:string]:Chromosome}) { + this.chrs = chrs; + } + + getFeaturesInRange(range: ContigInterval): Chromosome { + return this.chrs[range.contig]; + } +} + +class CytoBandFile { + remoteFile: AbstractFile; + immediate: Q.Promise; + + constructor(remoteFile: AbstractFile) { + this.remoteFile = remoteFile; + + this.immediate = this.remoteFile.getAll().then(bytes => { + var txt = pako.inflate(bytes, {to: 'string'}); + var txtLines = _.filter(txt.split('\n'), i => i); // gets rid of empty lines + var lines = txtLines.map(extractLine); + return lines; + }).then(lines => { + // group bands by contig + var grouped = _.groupBy(lines, l => l.contig); + var chrs = _.mapObject(grouped, g => groupedBandsToChromosome(g)); + return new ImmediateCytoBandFile(chrs); + }); + this.immediate.done(); + } + + getFeaturesInRange(range: ContigInterval): Q.Promise { + return this.immediate.then(immediate => { + return immediate.getFeaturesInRange(range); + }); + } +} + +module.exports = { + CytoBandFile +}; diff --git a/src/main/data/genericFeature.js b/src/main/data/genericFeature.js index ec3e6406..31b74f81 100644 --- a/src/main/data/genericFeature.js +++ b/src/main/data/genericFeature.js @@ -15,8 +15,8 @@ class GenericFeature { gFeature: Object; constructor(id: string, position: ContigInterval, genericFeature: Object) { - this.id = genericFeature.id; - this.position = genericFeature.position; + this.id = id; + this.position = position; this.gFeature = genericFeature; } } diff --git a/src/main/json/IdiogramJson.js b/src/main/json/IdiogramJson.js new file mode 100644 index 00000000..eafb68e9 --- /dev/null +++ b/src/main/json/IdiogramJson.js @@ -0,0 +1,55 @@ +/** + * A data source which implements generic JSON protocol. + * Currently only used to load alignments. + * @flow + */ +'use strict'; + +import type {DataSource} from '../sources/DataSource'; +import Chromosome from '../data/chromosome'; + +import _ from 'underscore'; +import {Events} from 'backbone'; + +import ContigInterval from '../ContigInterval'; +import type {GenomeRange} from '../types'; + +function create(json: string): DataSource { + + // parse json + var parsedJson = JSON.parse(json); + var chromosomes: Chromosome[] = []; + + // fill chromosomes with json + if (!_.isEmpty(parsedJson)) { + chromosomes = _.values(parsedJson).map(chr => new Chromosome(chr)); + } + + function rangeChanged(newRange: GenomeRange) { + // Data is already parsed, so immediately return + var range = new ContigInterval(newRange.contig, newRange.start, newRange.stop); + o.trigger('newdata', range); + o.trigger('networkdone'); + return; + } + + function getFeaturesInRange(range: ContigInterval): Chromosome[] { + return _.filter(chromosomes, chr => range.chrOnContig(chr.name)); + } + + var o = { + rangeChanged, + getFeaturesInRange, + + on: () => {}, + once: () => {}, + off: () => {}, + trigger: (string, any) => {} + }; + _.extend(o, Events); + return o; +} + +module.exports = { + create +}; diff --git a/src/main/pileup.js b/src/main/pileup.js index 074d3d7d..4ecd60f8 100644 --- a/src/main/pileup.js +++ b/src/main/pileup.js @@ -17,6 +17,8 @@ import saveAs from 'file-saver'; // Data sources import TwoBitDataSource from './sources/TwoBitDataSource'; +import CytoBandDataSource from './sources/CytoBandDataSource'; + import BigBedDataSource from './sources/BigBedDataSource'; import VcfDataSource from './sources/VcfDataSource'; import BamDataSource from './sources/BamDataSource'; @@ -29,6 +31,7 @@ import Interval from './Interval'; import GA4GHAlignmentJson from './json/GA4GHAlignmentJson'; import GA4GHVariantJson from './json/GA4GHVariantJson'; import GA4GHFeatureJson from './json/GA4GHFeatureJson'; +import IdiogramJson from './json/IdiogramJson'; // GA4GH sources import GA4GHAlignmentSource from './sources/GA4GHAlignmentSource'; @@ -41,6 +44,7 @@ import CoverageTrack from './viz/CoverageTrack'; import GenomeTrack from './viz/GenomeTrack'; import GeneTrack from './viz/GeneTrack'; import FeatureTrack from './viz/FeatureTrack'; +import IdiogramTrack from './viz/IdiogramTrack'; import LocationTrack from './viz/LocationTrack'; import PileupTrack from './viz/PileupTrack'; import ScaleTrack from './viz/ScaleTrack'; @@ -236,6 +240,8 @@ var pileup = { alignmentJson: GA4GHAlignmentJson.create, variantJson: GA4GHVariantJson.create, featureJson: GA4GHFeatureJson.create, + idiogramJson: IdiogramJson.create, + cytoBand: CytoBandDataSource.create, vcf: VcfDataSource.create, twoBit: TwoBitDataSource.create, bigBed: BigBedDataSource.create, @@ -250,6 +256,7 @@ var pileup = { genome: makeVizObject(GenomeTrack), genes: makeVizObject(GeneTrack), features: makeVizObject(FeatureTrack), + idiogram: makeVizObject(IdiogramTrack), location: makeVizObject(LocationTrack), scale: makeVizObject(ScaleTrack), variants: makeVizObject(VariantTrack), diff --git a/src/main/sources/CytoBandDataSource.js b/src/main/sources/CytoBandDataSource.js new file mode 100644 index 00000000..10398f60 --- /dev/null +++ b/src/main/sources/CytoBandDataSource.js @@ -0,0 +1,83 @@ +/** + * The "glue" between cytoBand.js and IdiogramTrack.js. + * + * Allows loading remote gzipped cytoband files into an Idiogram visualization. + * + * @flow + */ +'use strict'; + +import _ from 'underscore'; +import {Events} from 'backbone'; + +import ContigInterval from '../ContigInterval'; +import Chromosome from '../data/chromosome'; +import type {GenomeRange} from '../types'; +import {CytoBandFile} from '../data/cytoBand'; +import type {DataSource} from '../sources/DataSource'; + +import RemoteFile from '../RemoteFile'; +import utils from '../utils'; + +var createFromCytoBandFile = function(remoteSource: CytoBandFile): DataSource { + // Local cache of genomic data. + var contigMap: {[key:string]: Chromosome} = {}; + + // This either adds or removes a 'chr' as needed. + function normalizeRange(range: ContigInterval): ContigInterval { + if (contigMap[range.contig] !== undefined) { + return range; + } + var altContig = utils.altContigName(range.contig); + if (contigMap[altContig] !== undefined) { + return new ContigInterval(altContig, range.start(), range.stop()); + } + return range; + } + + function fetch(range: ContigInterval) { + remoteSource.getFeaturesInRange(range) + .then(chr => { + contigMap[chr.name] = chr; + }).then(() => { + o.trigger('newdata', range); + }).done(); + } + + function getFeaturesInRange(range: ContigInterval): Chromosome[] { + return [contigMap[normalizeRange(range).contig]]; + } + + var o = { + // The range here is 0-based, inclusive + rangeChanged: function(newRange: GenomeRange) { + // Check if this interval is already in the cache. + if ( contigMap[newRange.contig] !== undefined ) { + return; + } + fetch(new ContigInterval(newRange.contig, newRange.start, newRange.stop)); + }, + getFeaturesInRange, + // These are here to make Flow happy. + on: () => {}, + once: () => {}, + off: () => {}, + trigger: (status: string, param: any) => {} + }; + _.extend(o, Events); // Make this an event emitter + + return o; +}; + +function create(url:string): DataSource { + if (!url) { + throw new Error(`Missing URL from track: ${url}`); + } + return createFromCytoBandFile(new CytoBandFile(new RemoteFile(url))); +} + + +module.exports = { + create, + createFromCytoBandFile, +}; diff --git a/src/main/sources/DataSource.js b/src/main/sources/DataSource.js index 72b68510..80a1c5e0 100644 --- a/src/main/sources/DataSource.js +++ b/src/main/sources/DataSource.js @@ -7,11 +7,12 @@ import type {GenomeRange} from '../types'; import type ContigInterval from '../ContigInterval'; import type {Alignment} from '../Alignment'; +import Chromosome from '../data/chromosome'; import Feature from '../data/feature'; import Gene from '../data/gene'; // Flow type for export. -export type DataSource = { +export type DataSource = { rangeChanged: (newRange: GenomeRange) => void; getFeaturesInRange: (range: ContigInterval, resolution: ?number) => T[]; on: (event: string, handler: Function) => void; diff --git a/src/main/style.js b/src/main/style.js index 4b2088f8..0e9f62c3 100644 --- a/src/main/style.js +++ b/src/main/style.js @@ -59,6 +59,21 @@ module.exports = { READ_SPACING: 2, // vertical spacing between reads READ_HEIGHT: 13, // Height of read + // Idiogram track + IDIOGRAM_LINEWIDTH: 1, + IDIOGRAM_COLORS: { + "gpos100": "#000000", + "gpos": "#000000", + "gpos75": "#828282", + "gpos66": "#a0a0a0", + "gpos50": "#c8c8c8", + "gpos33": "#d2d2d2", + "gpos25": "#c8c8c8", + "gvar": "#dcdcdc", + "gneg": "#ffffff", + "acen": "#d92f27", + "stalk": "#647fa4", + }, // Coverage track COVERAGE_FONT_STYLE: `bold 9px 'Helvetica Neue', Helvetica, Arial, sans-serif`, diff --git a/src/main/viz/GeneTrack.js b/src/main/viz/GeneTrack.js index cdb8c872..1d56c73b 100644 --- a/src/main/viz/GeneTrack.js +++ b/src/main/viz/GeneTrack.js @@ -193,7 +193,7 @@ class GeneTrack extends React.Component>, State> { ctx.popObject(); }); - } // end typecheck for canvasß + } // end typecheck for canvas } } diff --git a/src/main/viz/IdiogramTrack.js b/src/main/viz/IdiogramTrack.js new file mode 100644 index 00000000..f80ddd09 --- /dev/null +++ b/src/main/viz/IdiogramTrack.js @@ -0,0 +1,185 @@ +/** + * A track which shows an idiogram. + * + * @flow + */ +'use strict'; + +import type {VizProps} from '../VisualizationWrapper'; +import type {State} from '../types'; +import type {DataSource} from '../sources/DataSource'; +import React from 'react'; +import ContigInterval from '../ContigInterval'; +import Chromosome from '../data/chromosome'; +import canvasUtils from './canvas-utils'; +import dataCanvas from 'data-canvas'; +import style from '../style'; + +import ReactDOM from 'react-dom'; +import shallowEquals from 'shallow-equals'; +import d3utils from './d3utils'; +import _ from 'underscore'; + +import GenericFeature from '../data/genericFeature'; +import {GenericFeatureCache} from './GenericFeatureCache'; + +function gstainFiller(d): string { + var stain = style.IDIOGRAM_COLORS[d.value]; + if (stain === undefined) { + return 'white'; + } else { + return stain; + } +} + +class IdiogramTrack extends React.Component>, State> { + props: VizProps>; + state: State; + cache: GenericFeatureCache; + + constructor(props: VizProps>) { + super(props); + this.state = { + networkStatus: null + }; + } + + render(): any { + return ; + } + + componentDidMount() { + this.cache = new GenericFeatureCache(this.props.referenceSource); + // Visualize new reference data as it comes in from the network. + this.props.source.on('newdata', (range) => { + // add to generic cache + var chrs = this.props.source.getFeaturesInRange(range); + chrs.forEach(chr => this.cache.addFeature(new GenericFeature(chr.name, chr.position, chr))) ; + this.updateVisualization(); + }); + this.props.referenceSource.on('newdata', range => { + this.updateVisualization(); + }); + this.props.source.on('networkprogress', e => { + this.setState({networkStatus: e}); + }); + this.props.source.on('networkdone', e => { + this.setState({networkStatus: null}); + }); + + this.updateVisualization(); + } + + componentDidUpdate(prevProps: any, prevState: any) { + if (!shallowEquals(this.props, prevProps) || + !shallowEquals(this.state, prevState)) { + this.updateVisualization(prevProps); + } + } + + updateVisualization(prevProps: any) { + var canvas = ReactDOM.findDOMNode(this), + {width, height} = this.props; + + var range = new ContigInterval(this.props.range.contig, this.props.range.start, this.props.range.stop); + + // Hold off until height & width are known. + if (width === 0 || typeof canvas == 'undefined' || canvas == null) return; + + this.updateChromosome(canvas, height, width,range); + this.updateChromosomeLocation(canvas, height, width, range); + } + + updateChromosomeLocation(canvas: Object, height: number, width: number ,range: ContigInterval) { + var chrs = _.flatten(_.map(this.cache.getGroupsOverlapping(range), + g => g.items)); + + + // Hold off until height & width are known. + if (width === 0 || typeof canvas == 'undefined' || canvas == null) return; + + if (!chrs || chrs.length == 0) { + return; + } + var chr = chrs[0]; + var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); + + var iv = {'start':chr.position.start(), + 'stop': chr.position.stop()}; + var sc = d3utils.getTrackScale(iv, this.props.width); + + var viewWidth = Math.ceil(sc(range.stop()) - sc(range.start()) ); + ctx.strokeStyle='red'; + ctx.strokeRect(sc(range.start()), + style.IDIOGRAM_LINEWIDTH*2, + viewWidth, + style.READ_HEIGHT); + + ctx.popObject(); + } + + updateChromosome(canvas: Object, height: number, width: number, range: ContigInterval) { + + // Hold off until height & width are known. + if (width === 0 || typeof canvas == 'undefined' || canvas == null) return; + + d3utils.sizeCanvas(canvas, width, height); + var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); + + ctx.reset(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + height = d3utils.heightForCanvas(canvas, style.READ_HEIGHT); + + var chrs = _.flatten(_.map(this.cache.getGroupsOverlapping(range), + g => g.items)); + + chrs.forEach(chr => { + var iv = {'start':chr.position.start(), + 'stop': chr.position.stop()}; + // add bands + var band_sc = d3utils.getTrackScale(iv, this.props.width); + chr.gFeature.bands.forEach(band => { + ctx.pushObject(band); + var filler = gstainFiller(band); + + var width = Math.ceil(band_sc(band.end) - band_sc(band.start)); + var start = band_sc(band.start)+style.IDIOGRAM_LINEWIDTH; + + + if (band.value == "acen") { + ctx.fillStyle='red'; + ctx.beginPath(); + if (band.name.startsWith("p")) { + ctx.moveTo(start+style.IDIOGRAM_LINEWIDTH, style.IDIOGRAM_LINEWIDTH*2 ); + ctx.lineTo(start+width,style.READ_HEIGHT/2); + ctx.lineTo(start+style.IDIOGRAM_LINEWIDTH, style.READ_HEIGHT+style.IDIOGRAM_LINEWIDTH); + ctx.fill(); + } else { + ctx.moveTo(start+style.IDIOGRAM_LINEWIDTH, style.READ_HEIGHT/2); + ctx.lineTo(start+width,style.IDIOGRAM_LINEWIDTH*2); + ctx.lineTo(start+width, style.READ_HEIGHT+style.IDIOGRAM_LINEWIDTH); + ctx.fill(); + } + } else { + ctx.fillStyle = filler; + ctx.strokeStyle = 'black'; + ctx.fillRect(start, + style.IDIOGRAM_LINEWIDTH*2, + width, + style.READ_HEIGHT); + ctx.strokeRect(start, + style.IDIOGRAM_LINEWIDTH*2, + width, + style.READ_HEIGHT); + } + ctx.popObject(); + }); + }); + } + +} + +IdiogramTrack.displayName = 'idiogram'; + +module.exports = IdiogramTrack; diff --git a/src/test/data/cytoBand-test.js b/src/test/data/cytoBand-test.js new file mode 100644 index 00000000..c94aa9fb --- /dev/null +++ b/src/test/data/cytoBand-test.js @@ -0,0 +1,26 @@ +/* @flow */ +'use strict'; + +import {expect} from 'chai'; + +import {CytoBandFile} from '../../main/data/cytoBand'; +import ContigInterval from '../../main/ContigInterval'; +import RemoteFile from '../../main/RemoteFile'; + +describe('CytoBand', function() { + var remoteFile = new RemoteFile('/test-data/cytoBand.txt.gz'); + + it('should get chromosomes from cytoband file', function(): any { + var cytoBand = new CytoBandFile(remoteFile); + var range = new ContigInterval('chr20', 63799, 69094); + + return cytoBand.getFeaturesInRange(range).then(chr => { + expect(chr.name).to.equal('chr20'); + expect(chr.position.start()).to.equal(0); + expect(chr.position.stop()).to.equal(63025520); + expect(chr.bands).to.have.length(20); + expect(chr.bands[0].name).to.equal('p13'); + expect(chr.bands[0].value).to.equal('gneg'); + }); + }); +}); diff --git a/src/test/json/IdiogramJson-test.js b/src/test/json/IdiogramJson-test.js new file mode 100644 index 00000000..041a5c47 --- /dev/null +++ b/src/test/json/IdiogramJson-test.js @@ -0,0 +1,42 @@ +/** @flow */ +'use strict'; + +import {expect} from 'chai'; + +import ContigInterval from '../../main/ContigInterval'; +import IdiogramJson from '../../main/json/IdiogramJson'; +import RemoteFile from '../../main/RemoteFile'; + +describe('IdiogramJson', function() { + var json; + + before(function(): any { + return new RemoteFile('/test-data/gstained_chromosomes_data.json').getAllString().then(data => { + json = data; + }); + }); + + it('should filter features from json', function(done) { + + var source = IdiogramJson.create(json); + + var requestInterval = new ContigInterval('chr1', 130000, 135000); + + var features = source.getFeaturesInRange(requestInterval)[0]; + expect(features.bands).to.have.length(63); + done(); + + }); + + it('should not fail on empty json string', function(done) { + + var source = IdiogramJson.create("{}"); + + var requestInterval = new ContigInterval('chr17', 10, 20); + + var chromosomes = source.getFeaturesInRange(requestInterval); + expect(chromosomes).to.have.length(0); + done(); + + }); +}); diff --git a/src/test/sources/CytoBandDataSource-test.js b/src/test/sources/CytoBandDataSource-test.js new file mode 100644 index 00000000..6285d44e --- /dev/null +++ b/src/test/sources/CytoBandDataSource-test.js @@ -0,0 +1,52 @@ +/* @flow */ +'use strict'; + +import {expect} from 'chai'; + +import CytoBandDataSource from '../../main/sources/CytoBandDataSource'; +import {CytoBandFile} from '../../main/data/cytoBand'; +import RemoteFile from '../../main/RemoteFile'; +import ContigInterval from '../../main/ContigInterval'; + +describe('CytoBandDataSource', function() { + function getTestSource () { + // cytoband file downloaded from + // http://hgdownload.cse.ucsc.edu/goldenpath/hg19/database/cytoBand.txt.gz + var f = new CytoBandFile(new RemoteFile('/test-data/cytoBand.txt.gz')); + return CytoBandDataSource.createFromCytoBandFile(f); + } + + it('should fetch chromsome', function(done) { + var source = getTestSource(); + var range = new ContigInterval('chr22',0,3); + // Before data has been fetched, all base pairs are null. + expect(source.getFeaturesInRange(range)).to.deep.equal([undefined]); + + source.on('newdata', () => { + var chr = source.getFeaturesInRange(range)[0]; + expect(chr.name).to.equal('chr22'); + expect(chr.bands).to.have.length(16); + expect(chr.bands[0].name).to.equal('p13'); + expect(chr.bands[0].value).to.equal('gvar'); + done(); + }); + source.rangeChanged(range); + }); + + it('should allow a mix of chr and non-chr', function(done) { + var source = getTestSource(); + var chrRange = {contig: 'chr1', start: 0, stop: 3}; + + var range = new ContigInterval('1',0,3); + + source.on('newdata', () => { + var chr = source.getFeaturesInRange(range)[0]; + expect(chr.name).to.equal('chr1'); + expect(chr.bands).to.have.length(63); + expect(chr.bands[0].name).to.equal('p36.33'); + expect(chr.bands[0].value).to.equal('gneg'); + done(); + }); + source.rangeChanged(chrRange); + }); +}); diff --git a/src/test/viz/IdiogramTrack-test.js b/src/test/viz/IdiogramTrack-test.js new file mode 100644 index 00000000..c31b9a51 --- /dev/null +++ b/src/test/viz/IdiogramTrack-test.js @@ -0,0 +1,105 @@ +/** + * This tests whether feature information is being shown/drawn correctly + * in the track. + * + * @flow + */ +'use strict'; + +import pileup from '../../main/pileup'; +import dataCanvas from 'data-canvas'; +import {waitFor} from '../async'; +import {expect} from 'chai'; +import RemoteFile from '../../main/RemoteFile'; + +describe('IdiogramTrack', function() { + var testDiv= document.getElementById('testdiv'); + if (!testDiv) throw new Error("Failed to match: testdiv"); + + var drawnObjects = dataCanvas.RecordingContext.drawnObjects; + + function ready(): boolean { + return testDiv.querySelector('.idiogram canvas') !== null && + testDiv.querySelector('.idiogram canvas') !== undefined && + drawnObjects(testDiv, '.idiogram').length > 0; + } + + beforeEach(() => { + testDiv.style.width = '800px'; + dataCanvas.RecordingContext.recordAll(); + }); + + afterEach(() => { + dataCanvas.RecordingContext.reset(); + // avoid pollution between tests. + testDiv.innerHTML = ''; + }); + + describe('json features', function() { + var json; + + before(function(): any { + return new RemoteFile('/test-data/gstained_chromosomes_data.json').getAllString().then(data => { + json = data; + }); + }); + + it('should render idiogram with json', function(): any { + var p = pileup.create(testDiv, { + range: {contig: 'chr17', start: 7500000, stop: 7500500}, + tracks: [ + { + viz: pileup.viz.genome(), + data: pileup.formats.twoBit({ + url: '/test-data/test.2bit' + }), + isReference: true + }, + { + viz: pileup.viz.idiogram(), + data: pileup.formats.idiogramJson(json), + name: 'Idiogram' + } + ] + }); + + return waitFor(ready, 2000) + .then(() => { + var bands = drawnObjects(testDiv, '.idiogram'); + expect(bands).to.have.length(24); + p.destroy(); + }); + }); + }); + + describe('cytoband features', function() { + + it('should render idiogram with gzipped file', function(): any { + var p = pileup.create(testDiv, { + range: {contig: 'chr1', start: 7500000, stop: 7500500}, + tracks: [ + { + viz: pileup.viz.genome(), + data: pileup.formats.twoBit({ + url: '/test-data/test.2bit' + }), + isReference: true + }, + { + viz: pileup.viz.idiogram(), + data: pileup.formats.cytoBand('/test-data/cytoBand.txt.gz'), + name: 'Idiogram' + } + ] + }); + + return waitFor(ready, 2000) + .then(() => { + var bands = drawnObjects(testDiv, '.idiogram'); + expect(bands).to.have.length(63); + p.destroy(); + }); + }); + + }); +}); diff --git a/style/pileup.css b/style/pileup.css index 11044df0..4c911985 100644 --- a/style/pileup.css +++ b/style/pileup.css @@ -60,14 +60,21 @@ overflow-x: hidden; position: relative; } -.features > .track-content, .pileup > .track-content, .genes > .track-content, -.variants > .track-content, .coverage > .track-content { +.track-content { border-top-left-radius: 5px; border-bottom-left-radius: 5px; border-color: #bcbbbb; border-left: 7px solid #bcbbbb; } +.reference > .track-content, .controls > .track-content, + .scale > .track-content, .location > .track-content { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-color: None; + border-left: None; +} + .track-content > div { position: absolute; /* Gets the child of the flex-item to fill height 100% */ } diff --git a/test-data/cytoBand.txt.gz b/test-data/cytoBand.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..ee9ab76fb039fac9522bed267e9d1cbb0e5ffd2f GIT binary patch literal 6609 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