Skip to content

Commit

Permalink
added save svg option (#532)
Browse files Browse the repository at this point in the history
* added save svg and zoom options to pileup
  • Loading branch information
akmorrow13 committed Oct 8, 2019
1 parent 5fba8df commit 55bff78
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 50 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ The pileup object returned by `pileup.create` has these methods:
coordinates are 1-based and the range is inclusive on both ends.
* `getRange`: Returns the currently-visible range. This is a `GenomeRange`
object (see description in `setRange`).
* `zoomIn`: Zooms current range in by a factor of 2.
* `zoomOut`: Zooms current range out by a factor of 2.
* `toSvg`: Converts pileup object to SVG data URL.
* `destroy`: Tears down the pileup and releases references to allow proper
garbage collection.

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"react-color": "^2.17.3",
"shallow-equals": "0.0.0",
"underscore": "^1.7.0",
"memory-cache": "0.1.6"
"memory-cache": "0.1.6",
"html-to-image": "0.1.1",
"file-saver": "2.0.2"
},
"devDependencies": {
"arraybuffer-slice": "^0.1.2",
Expand Down
4 changes: 2 additions & 2 deletions src/main/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,12 @@ class Controls extends React.Component<Props, State> {

zoomIn(e: any) {
e.preventDefault();
this.zoomByFactor(0.5);
this.zoomByFactor(utils.ZOOM_FACTOR.IN);
}

zoomOut(e: any) {
e.preventDefault();
this.zoomByFactor(2.0);
this.zoomByFactor(utils.ZOOM_FACTOR.OUT);
}

// Updates the range using absScaleRange and a given zoom level
Expand Down
48 changes: 47 additions & 1 deletion src/main/pileup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {VizProps} from './VisualizationWrapper';
import _ from 'underscore';
import React from 'react';
import ReactDOM from 'react-dom';
import htmlToImage from 'html-to-image';
import saveAs from 'file-saver';

// Data sources
import TwoBitDataSource from './sources/TwoBitDataSource';
Expand All @@ -20,6 +22,9 @@ import VcfDataSource from './sources/VcfDataSource';
import BamDataSource from './sources/BamDataSource';
import EmptySource from './sources/EmptySource';

import utils from './utils';
import Interval from './Interval';

// Data sources from json
import GA4GHAlignmentJson from './json/GA4GHAlignmentJson';
import GA4GHVariantJson from './json/GA4GHVariantJson';
Expand Down Expand Up @@ -52,6 +57,9 @@ type GenomeRange = {
type Pileup = {
setRange(range: GenomeRange): void;
getRange(): GenomeRange;
zoomIn(): void;
zoomOut(): void;
toSVG(): Promise<string>;
destroy(): void;
}

Expand Down Expand Up @@ -147,9 +155,47 @@ function create(elOrId: string|Element, params: PileupParams): Pileup {
if (reactElement.state.range != null) {
return _.clone(reactElement.state.range);
} else {
throw 'Cannot call setRange on non-existent range';
throw 'Cannot call getRange on non-existent range';
}
},
zoomIn() {
if (reactElement === null) {
throw 'Cannot call zoomIn on a destroyed pileup';
}
var r = this.getRange();
var iv = utils.scaleRange(new Interval(r.start, r.stop), utils.ZOOM_FACTOR.IN);
var newRange = {
contig: r.contig,
start: iv.start,
stop: iv.stop
}
this.setRange(newRange);
},
zoomOut() {
if (reactElement === null) {
throw 'Cannot call zoomOut on a destroyed pileup';
}
var r = this.getRange();
var iv = utils.scaleRange(new Interval(r.start, r.stop), utils.ZOOM_FACTOR.OUT);
var newRange = {
contig: r.contig,
start: iv.start,
stop: iv.stop
}
this.setRange(newRange);
},
toSVG(filepath: ?string): Promise<string> {
if (reactElement === null) {
throw 'Cannot call toSVG on a destroyed pileup';
}

return htmlToImage.toSvgDataURL(el)
.then(function (svg) {
if (filepath != null) {
window.saveAs(svg, filepath);
}
return svg;
});
},
destroy(): void {
if (!vizTracks) {
Expand Down
6 changes: 5 additions & 1 deletion src/main/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,5 +324,9 @@ module.exports = {
isChrMatch,
flatMap,
computePercentile,
stringToLiteral
stringToLiteral,
ZOOM_FACTOR: {
'IN': 0.5,
'OUT': 2.0,
}
};
134 changes: 89 additions & 45 deletions src/test/components-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import ContigInterval from '../main/ContigInterval';
import {waitFor} from './async';

describe('pileup', function() {

var p;
var testDiv = document.getElementById('testdiv');
if (!testDiv) throw new Error("Failed to match: testDiv");

var initialRange = {contig: 'chr17', start: 100, stop: 150}

function getTracks() {
return [
{
Expand Down Expand Up @@ -58,93 +65,100 @@ describe('pileup', function() {
]
}

var testDiv = document.getElementById('testdiv');
if (!testDiv) throw new Error("Failed to match: testdiv");
function makePileup() {
p = pileup.create(testDiv, {
range: initialRange,
tracks: getTracks()
});
}


var {drawnObjects, drawnObjectsWith, callsOf} = dataCanvas.RecordingContext;

var uniqDrawnObjectsWith = function(div: any, name: string, f: any) {
return _.uniq(
drawnObjectsWith(div, name, f),
false, // not sorted
x => x.key);
};

// TODO: consider moving this into the data-canvas library
function hasCanvasAndObjects(div, selector) {
return div.querySelector(selector + ' canvas') && drawnObjects(div, selector).length > 0;
}

var ready = ((): boolean =>
// $FlowIgnore: TODO remove flow suppression
hasCanvasAndObjects(testDiv, '.reference') &&
hasCanvasAndObjects(testDiv, '.variants') &&
hasCanvasAndObjects(testDiv, '.genes') &&
hasCanvasAndObjects(testDiv, '.pileup')
);

var rangeChanged = ((): boolean =>
// $FlowIgnore: TODO remove flow suppression
initialRange != p.getRange()
);


beforeEach(() => {
dataCanvas.RecordingContext.recordAll(); // record all data canvases
testDiv.style.width = '800px';
});

afterEach(function() {
dataCanvas.RecordingContext.reset();
testDiv.innerHTML = ''; // avoid pollution between tests.
if (p) p.destroy();
testDiv.innerHTML = '';
testDiv.style.width = '';
});

it('should render reference genome and genes', function(): any {
it('should render reference genome and genes', function(done): any {
this.timeout(5000);

var div = document.createElement('div');
div.setAttribute('style', 'width: 800px; height: 200px;');
testDiv.appendChild(div);

var p = pileup.create(div, {
range: {contig: 'chr17', start: 100, stop: 150},
tracks: getTracks()
});
makePileup();

var {drawnObjects, drawnObjectsWith, callsOf} = dataCanvas.RecordingContext;

var uniqDrawnObjectsWith = function(div: any, name: string, f: any) {
return _.uniq(
drawnObjectsWith(div, name, f),
false, // not sorted
x => x.key);
};

// TODO: consider moving this into the data-canvas library
function hasCanvasAndObjects(div, selector) {
return div.querySelector(selector + ' canvas') && drawnObjects(div, selector).length > 0;
}

var ready = ((): boolean =>
// $FlowIgnore: TODO remove flow suppression
hasCanvasAndObjects(div, '.reference') &&
hasCanvasAndObjects(div, '.variants') &&
hasCanvasAndObjects(div, '.genes') &&
hasCanvasAndObjects(div, '.pileup')
);

return waitFor(ready, 5000)
waitFor(ready, 5000)
.then(() => {
var basepairs = drawnObjectsWith(div, '.reference', x => x.letter);
var basepairs = drawnObjectsWith(testDiv, '.reference', x => x.letter);
expect(basepairs).to.have.length.at.least(10);

var variants = drawnObjectsWith(div, '.variants', x => x.alt);
var variants = drawnObjectsWith(testDiv, '.variants', x => x.alt);
expect(variants).to.have.length(1);
expect(variants[0].position).to.equal(125);
expect(variants[0].ref).to.equal('G');
expect(variants[0].alt).to.equal('T');

var geneTexts = callsOf(div, '.genes', 'fillText');
var geneTexts = callsOf(testDiv, '.genes', 'fillText');
expect(geneTexts).to.have.length(1);
expect(geneTexts[0][1]).to.equal('TP53');

// Note: there are 11 exons, but two are split into coding/non-coding
expect(callsOf(div, '.genes', 'fillRect')).to.have.length(13);
expect(callsOf(testDiv, '.genes', 'fillRect')).to.have.length(13);

// check for reference
var selectedClass = div.querySelector('div > .a');
var selectedClass = testDiv.querySelector('div > .a');
expect(selectedClass).to.not.be.null;
if (selectedClass != null) {
expect(selectedClass.className).to.equal('track reference a');
}

// check for variants
selectedClass = div.querySelector('div > .b');
selectedClass = testDiv.querySelector('div > .b');
expect(selectedClass).to.not.be.null;
if (selectedClass != null) {
expect(selectedClass.className).to.equal('track variants b');
}

// check for genes
selectedClass = div.querySelector('div > .c');
selectedClass = testDiv.querySelector('div > .c');
expect(selectedClass).to.not.be.null;
if (selectedClass != null) {
expect(selectedClass.className).to.equal('track genes c');
}

// check for pileup
selectedClass = div.querySelector('div > .d');
selectedClass = testDiv.querySelector('div > .d');
expect(selectedClass).to.not.be.null;
if (selectedClass != null) {
expect(selectedClass.className).to.equal('track pileup d');
Expand All @@ -160,11 +174,41 @@ describe('pileup', function() {
// Due to tiling, some rendered reads may be off-screen.
var range = p.getRange();
var cRange = new ContigInterval(range.contig, range.start, range.stop);
var visibleReads = uniqDrawnObjectsWith(div, '.pileup', x => x.span)
var visibleReads = uniqDrawnObjectsWith(testDiv, '.pileup', x => x.span)
.filter(x => x.span.intersects(cRange));
expect(visibleReads).to.have.length(4);
done();
});
});

it('should save SVG', function(done): any {
this.timeout(5000);

makePileup();

waitFor(ready, 5000)
.then(() => {

p.destroy();
// test zoom in, out and svg
p.zoomIn();
waitFor(rangeChanged, 5000)
.then(() => {
// not waiting
expect(p.getRange()).to.deep.equal({
contig: 'chr17',
start: 112,
stop: 138
});

// test conversion to SVG
p.toSVG().then(svg => {
expect(svg).to.contain("svg");
expect(svg).to.contain("chr17");
expect(svg).to.contain("Location");
expect(svg).to.contain("Scale");
done();
});
})
});
});
});

0 comments on commit 55bff78

Please sign in to comment.