Skip to content

Commit

Permalink
Mediate between backend & view with a cache layer.
Browse files Browse the repository at this point in the history
This eliminates the flicker as you pan. The view component (GenomeTrack)
no longer has to know anything about the network or async code.
  • Loading branch information
danvk committed Mar 5, 2015
1 parent f118690 commit 50bf169
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"homepage": "https://github.com/danvk/pileup.js",
"dependencies": {
"backbone": "^1.1.2",
"d3": "^3.5.5",
"q": "^1.1.2",
"react": "^0.12.2",
Expand Down
32 changes: 21 additions & 11 deletions src/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
var React = require('react'),
Controls = require('./Controls'),
GenomeTrack = require('./GenomeTrack'),
types = require('./types'),
types = require('./types');
// TODO: make this an "import type" when react-tools 0.13.0 is out.
TwoBit = require('./TwoBit');

var Root = React.createClass({
propTypes: {
Expand All @@ -23,24 +22,35 @@ var Root = React.createClass({
},
componentDidMount: function() {
// Note: flow is unable to infer this type through `this.propTypes`.
var ref: TwoBit = this.props.referenceSource;
ref.getContigList().then(contigList => {
this.setState({contigList});
var source: TwoBitDataSource = this.props.referenceSource;
source.needContigs();

source.on('contigs', () => this.update)
.on('newdata', () => this.update);

source.on('contigs', () => {
// this is here to facilitate faster iteration
this.handleRangeChange({
contig: 'chr1',
start: 123456,
stop: 123500
});
}).done();
});

this.update();
},
update: function() {
this.setState({
contigList: this.props.referenceSource.contigList(),
basePairs: this.props.referenceSource.getRange(this.state.range)
});
},
handleRangeChange: function(newRange: GenomeRange) {
this.setState({range: newRange, basePairs: null});
this.setState({range: newRange});
this.update();

var ref = this.props.referenceSource;
ref.getFeaturesInRange(newRange.contig, newRange.start, newRange.stop)
.then(basePairs => {
this.setState({basePairs});
}).done();
ref.rangeChanged(newRange);
},
render: function(): any {
return (
Expand Down
79 changes: 79 additions & 0 deletions src/TwoBitDataSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* The "glue" between TwoBit.js and GenomeTrack.js.
*
* GenomeTrack is pure view code -- it renders data which is already in-memory
* in the browser.
*
* TwoBit is purely for data parsing and fetching. It only knows how to return
* promises for various genome features.
*
* This code acts as a bridge between the two. It maintains a local version of
* the data, fetching remote data and informing the view when it becomes
* available.
*
* @flow
*/
'use strict';

// import type * as TwoBit from './TwoBit';

var Events = require('backbone').Events,
_ = require('underscore');

// Factor by which to over-request data from the network.
// If a range of 100bp is shown and this is 2.0, then 300 base pairs will be
// requested over the network (100 * 2.0 too much)
var EXPANSION_FACTOR = 2.0;


var createTwoBitDataSource = function(remoteSource: TwoBit) {
// Local cache of genomic data.
var contigList = [];
var basePairs = {}; // contig -> locus -> letter
function getBasePair(contig: string, position: number) {
return basePairs[contig] && basePairs[contig][position];
}
function setBasePair(contig: string, position: number, letter: string) {
if (!basePairs[contig]) basePairs[contig] = {};
basePairs[contig][position] = letter;
}

function fetch(range: GenomeRange) {
return remoteSource.getFeaturesInRange(range.contig, range.start, range.stop)
.then(letters => {
for (var i = 0; i < letters.length; i++) {
setBasePair(range.contig, range.start + i, letters[i]);
}
});
}

function getRange(range: GenomeRange) {
if (!range) return null;
return _.range(range.start, range.stop)
.map(x => getBasePair(range.contig, x))
.join('');
}

var o = {
rangeChanged: function(newRange: GenomeRange) {
// Range has changed! Fetch new data.
// TODO: only fetch it if it isn't cached already.
fetch(newRange)
.then(() => o.trigger('newdata', newRange))
.done();
},
needContigs: () => {
remoteSource.getContigList().then(c => {
contigList = c;
o.trigger('contigs', contigList);
}).done();
},
getRange: getRange,
contigList: () => contigList
};
_.extend(o, Events); // Make this an event emitter

return o;
};

module.exports = createTwoBitDataSource;
6 changes: 4 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* @flow */
var React = require('react'),
TwoBit = require('./TwoBit'),
Root = require('./Root');
Root = require('./Root'),
createTwoBitDataSource = require('./TwoBitDataSource');

var startMs = Date.now();
// var genome = new TwoBit('http://www.biodalliance.org/datasets/hg19.2bit');
var genome = new TwoBit('/hg19.2bit');
var dataSource = createTwoBitDataSource(genome);

genome.getFeaturesInRange('chr22', 19178140, 19178170).then(basePairs => {
var endMs = Date.now();
Expand All @@ -19,5 +21,5 @@ genome.getFeaturesInRange('chr22', 19178140, 19178170).then(basePairs => {
// pre-load some data to allow network-free panning
genome.getFeaturesInRange('chr1', 123000, 124000).done();

var root = React.render(<Root referenceSource={genome} />,
var root = React.render(<Root referenceSource={dataSource} />,
document.getElementById('root'));
2 changes: 1 addition & 1 deletion types/types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare class GenomeRange {
contig: string;
start: number;
stop: number;
stop: number; // XXX inclusive or exclusive?
}

0 comments on commit 50bf169

Please sign in to comment.