Skip to content

Commit

Permalink
Merge pull request #367 from hammerlab/fast-twobit
Browse files Browse the repository at this point in the history
Improvements to reference source
  • Loading branch information
danvk committed Nov 13, 2015
2 parents 2475449 + ced6079 commit 3e771ef
Show file tree
Hide file tree
Showing 16 changed files with 340 additions and 109 deletions.
3 changes: 3 additions & 0 deletions lib/underscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module "underscore" {

declare function map<T, U>(a: T[], iteratee: (val: T, n?: number)=>U): U[];
declare function map<K, T, U>(a: {[key:K]: T}, iteratee: (val: T, k?: K)=>U): U[];
declare function map<U>(a: string, iteratee: (val: string, n?: number)=>U): U[];

declare function object<K, T>(a: Array<[K, T]>): {[key:K]: T};
declare function object<K, T>(...a: [K, T][]): {[key:K]: T};
Expand Down Expand Up @@ -51,4 +52,6 @@ declare module "underscore" {

declare function uniq<T>(array: T[], isSorted?: boolean, iteratee?: (v: T)=>any): T[];
declare function unique<T>(array: T[], isSorted?: boolean, iteratee?: (v: T)=>any): T[];

declare function isString(x: any): boolean;
}
Binary file modified screenshots/pileup_gene-utf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/pileup_wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/structuralvariants_base.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/main/ContigInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class ContigInterval<T: (number|string)> {
}

toGenomeRange(): GenomeRange {
if (!(this.contig instanceof String)) {
if (!(typeof this.contig === 'string' || this.contig instanceof String)) {
throw 'Cannot convert numeric ContigInterval to GenomeRange';
}
return {
Expand Down
122 changes: 66 additions & 56 deletions src/main/GenomeTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,75 @@

import type {VizProps} from './VisualizationWrapper';
import type {TwoBitSource} from './TwoBitDataSource';
import type {DataCanvasRenderingContext2D} from 'data-canvas';

var React = require('react'),
ReactDOM = require('react-dom');
ReactDOM = require('react-dom'),
shallowEquals = require('shallow-equals');

var shallowEquals = require('shallow-equals'),
canvasUtils = require('./canvas-utils'),
var canvasUtils = require('./canvas-utils'),
ContigInterval = require('./ContigInterval'),
dataCanvas = require('data-canvas'),
d3utils = require('./d3utils'),
DisplayMode = require('./DisplayMode'),
style = require('./style');


function renderGenome(ctx: DataCanvasRenderingContext2D,
scale: (num: number) => number,
range: ContigInterval<string>,
basePairs: string) {
var pxPerLetter = scale(1) - scale(0);
var mode = DisplayMode.getDisplayMode(pxPerLetter);
var showText = DisplayMode.isText(mode);
var height = ctx.canvas.height;

if (mode != DisplayMode.HIDDEN) {
ctx.textAlign = 'center';
if (mode == DisplayMode.LOOSE) {
ctx.font = style.LOOSE_TEXT_STYLE;
} else if (mode == DisplayMode.TIGHT) {
ctx.font = style.TIGHT_TEXT_STYLE;
}

var previousBase = null;
var start = range.start(),
stop = range.stop();
for (var pos = start; pos <= stop; pos++) {
var letter = basePairs[pos - start];
if (letter == '.') continue; // not yet known

ctx.save();
ctx.pushObject({pos, letter});
ctx.fillStyle = style.BASE_COLORS[letter];
if (showText) {
// We only push objects in the text case as it involves creating a
// new object & can become a performance issue.
// 0.5 = centered
ctx.fillText(letter, scale(1 + 0.5 + pos), height - 1);
} else {
if (pxPerLetter >= style.COVERAGE_MIN_BAR_WIDTH_FOR_GAP) {
// We want a white space between blocks at this size, so we can see
// the difference between bases.
ctx.fillRect(scale(1 + pos) + 0.5, 0, pxPerLetter - 1.5, height);
} else if (previousBase === letter) {
// Otherwise, we want runs of colors to be completely solid ...
ctx.fillRect(scale(1 + pos) - 1.5, 0, pxPerLetter + 1.5, height);
} else {
// ... and minimize the amount of smudging and whitespace between
// bases.
ctx.fillRect(scale(1 + pos) - 0.5, 0, pxPerLetter + 1.5, height);
}
}

ctx.popObject();
ctx.restore();
previousBase = letter;
}
}
}


class GenomeTrack extends React.Component {
props: VizProps & {source: TwoBitSource};
state: void; // no state
Expand Down Expand Up @@ -48,68 +105,21 @@ class GenomeTrack extends React.Component {

updateVisualization() {
var canvas = ReactDOM.findDOMNode(this),
{width, height, range} = this.props;
{width, height, range} = this.props,
// The +/-1 ensures that partially-visible bases on the edge are rendered.
interval = new ContigInterval(range.contig, range.start - 1, range.stop + 1);
console.log(interval.toString());

// Hold off until height & width are known.
if (width === 0) return;
d3utils.sizeCanvas(canvas, width, height);

var scale = this.getScale();
var pxPerLetter = scale(1) - scale(0);
var mode = DisplayMode.getDisplayMode(pxPerLetter);
var showText = DisplayMode.isText(mode);

var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas));
ctx.reset();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
renderGenome(ctx, this.getScale(), interval,
this.props.source.getRangeAsString(interval.toGenomeRange()));

if (mode != DisplayMode.HIDDEN) {
var basePairs = this.props.source.getRange({
contig: range.contig,
start: Math.max(0, range.start - 1),
stop: range.stop
});

ctx.textAlign = 'center';
if (mode == DisplayMode.LOOSE) {
ctx.font = style.LOOSE_TEXT_STYLE;
} else if (mode == DisplayMode.TIGHT) {
ctx.font = style.TIGHT_TEXT_STYLE;
}

var contigColon = this.props.range.contig + ':';
var previousBase = null;
for (var pos = range.start - 1; pos <= range.stop; pos++) {
var letter = basePairs[contigColon + pos];

ctx.save();
ctx.pushObject({pos, letter});
ctx.fillStyle = style.BASE_COLORS[letter];
if (showText) {
// We only push objects in the text case as it involves creating a
// new object & can become a performance issue.
// 0.5 = centered
ctx.fillText(letter, scale(1 + 0.5 + pos), height - 1);
} else {
if (pxPerLetter >= style.COVERAGE_MIN_BAR_WIDTH_FOR_GAP) {
// We want a white space between blocks at this size, so we can see
// the difference between bases.
ctx.fillRect(scale(1 + pos) + 0.5, 0, pxPerLetter - 1.5, height);
} else if (previousBase === letter) {
// Otherwise, we want runs of colors to be completely solid ...
ctx.fillRect(scale(1 + pos) - 1.5, 0, pxPerLetter + 1.5, height);
} else {
// ... and minimize the amount of smudging and whitespace between
// bases.
ctx.fillRect(scale(1 + pos) - 0.5, 0, pxPerLetter + 1.5, height);
}
}

ctx.popObject();
ctx.restore();
previousBase = letter;
}
}
}
}

Expand Down
140 changes: 140 additions & 0 deletions src/main/SequenceStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* A store for sequences.
*
* This is used to store and retrieve reference data.
*
* @flow
*/
'use strict';

import type Interval from './Interval';
import type ContigInterval from './ContigInterval';

import _ from 'underscore';

import {altContigName} from './utils';

// Store sequences of this many base pairs together in a single string.
const CHUNK_SIZE = 1000;

type SeqMap = {[key: number]: string};
type ChunkWithOffset = {
chunkStart: number;
offset: number;
length: number;
};

// This class stores sequences efficiently.
class SequenceStore {
// contig --> start of chunk --> sequence of chunk
contigMap: {[key: string]: SeqMap};

constructor() {
this.contigMap = {};
}

/**
* Set a range of the genome to the particular sequence.
* This overwrites any existing data.
*/
setRange(range: ContigInterval<string>, sequence: string) {
if (range.length() === 0) return;
if (range.length() != sequence.length) {
throw 'setRange length mismatch';
}

var seqs = this._getSequences(range.contig);
if (!seqs) {
seqs = this.contigMap[range.contig] = {};
}

for (var chunk of this._chunkForInterval(range.interval)) {
var pos = chunk.chunkStart + chunk.offset - range.start();
this._setChunk(seqs, chunk, sequence.slice(pos, pos + chunk.length));
}
}

/**
* Retrieve a range of sequence data.
* If any portions are unknown, they will be set to '.'.
*/
getAsString(range: ContigInterval<string>): string {
const seqs = this._getSequences(range.contig);
if (!seqs) {
return '.'.repeat(range.length());
}

var chunks = this._chunkForInterval(range.interval);
var result = '';
for (var chunk of chunks) {
var seq = seqs[chunk.chunkStart];
if (!seq) {
result += '.'.repeat(chunk.length);
} else if (chunk.offset === 0 && chunk.length == seq.length) {
result += seq;
} else {
result += seq.slice(chunk.offset, chunk.offset + chunk.length);
}
}
return result;
}

/**
* Like getAsString(), but returns a less efficient object representation:
* {'chr1:10': 'A', 'chr1:11': 'C', ...}
*/
getAsObjects(range: ContigInterval<string>): {[key:string]: ?string} {
return _.object(_.map(this.getAsString(range),
(base, i) => [range.contig + ':' + (range.start() + i),
base == '.' ? null : base]));
}

// Retrieve a chunk from the sequence map.
_getChunk(seqs: SeqMap, start: number): string {
return seqs[start] || '.'.repeat(CHUNK_SIZE);
}

// Split an interval into chunks which align with the store.
_chunkForInterval(range: Interval): ChunkWithOffset[] {
var offset = range.start % CHUNK_SIZE,
chunkStart = range.start - offset;
var chunks = [{
chunkStart,
offset,
length: Math.min(CHUNK_SIZE - offset, range.length())
}];
chunkStart += CHUNK_SIZE;
for (; chunkStart <= range.stop; chunkStart += CHUNK_SIZE) {
chunks.push({
chunkStart,
offset: 0,
length: Math.min(CHUNK_SIZE, range.stop - chunkStart + 1)
});
}
return chunks;
}

// Set a (subset of a) chunk to the given sequence.
_setChunk(seqs: SeqMap, chunk: ChunkWithOffset, sequence: string) {
// First: the easy case. Total replacement.
if (chunk.offset === 0 && sequence.length == CHUNK_SIZE) {
seqs[chunk.chunkStart] = sequence;
return;
}

// We need to merge the new sequence with the old.
var oldChunk = this._getChunk(seqs, chunk.chunkStart);
seqs[chunk.chunkStart] = oldChunk.slice(0, chunk.offset) +
sequence +
oldChunk.slice(chunk.offset + sequence.length);
}

// Get the sequences for a contig, allowing chr- mismatches.
_getSequences(contig: string): ?SeqMap {
return this.contigMap[contig] ||
this.contigMap[altContigName(contig)] ||
null;
}
}

module.exports = SequenceStore;
1 change: 1 addition & 0 deletions src/main/TwoBit.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class TwoBit {
/**
* Returns the base pairs for contig:start-stop.
* The range is inclusive and zero-based.
* Returns empty string if no data is available on this range.
*/
getFeaturesInRange(contig: string, start: number, stop: number): Q.Promise<string> {
if (start > stop) {
Expand Down
Loading

0 comments on commit 3e771ef

Please sign in to comment.