Skip to content

Commit

Permalink
Merge pull request #20 from hammerlab/adapter
Browse files Browse the repository at this point in the history
Mediate between backend & view with a cache layer.
  • Loading branch information
danvk committed Mar 9, 2015
2 parents f118690 + f264937 commit bddf462
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 28 deletions.
14 changes: 11 additions & 3 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = function(grunt) {
},
prod: {
files: ['<%= watch.flow.files %>'],
tasks: ['prod']
tasks: ['browserify:dist']
}
},
browserify: {
Expand Down Expand Up @@ -55,6 +55,13 @@ module.exports = function(grunt) {
}
}
},
uglify: {
dist: {
files: {
'build/all.min.js': ['build/all.js']
}
}
},
jscoverage: {
src: {
expand: true,
Expand Down Expand Up @@ -93,13 +100,14 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-mocha-phantomjs');
grunt.loadNpmTasks("grunt-jscoverage");
grunt.loadNpmTasks("grunt-exorcise");
grunt.loadNpmTasks('grunt-contrib-uglify');

grunt.registerTask('watchFlow', ['flow:app:start', 'watch:flow']);
grunt.registerTask('watchFlowProd', ['flow:app:start', 'watch:flowProd']);
grunt.registerTask('prod', ['browserify:dist']);
grunt.registerTask('prod', ['browserify:dist', 'uglify:dist']);
grunt.registerTask('browsertests', ['browserify:test']);
grunt.registerTask('test', ['browsertests', 'mocha_phantomjs:run']);
grunt.registerTask('travis', ['flow', 'test']);
grunt.registerTask('coverage',
['browsertests', 'exorcise', 'jscoverage', 'mocha_phantomjs:cov']);
['browsertests', 'exorcise:bundle', 'jscoverage', 'mocha_phantomjs:cov']);
};
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ To iterate on code while running the type checker:

To continuously regenerate the combined JS, run:

grunt watchFlowProd
grunt watch:prod

[hs]: https://github.com/nodeapps/http-server
6 changes: 6 additions & 0 deletions lib/underscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
declare module "underscore" {
declare function findWhere<T>(list: Array<T>, properties: {}): ?T;
declare function clone<T>(obj: T): T;

declare function isEqual<S, T>(a: S, b: T): boolean;
declare function range(a: number, b: number): Array<number>;
declare function extend(...o: Object): Object;

declare function chain<S>(obj: S): any;
}

2 changes: 2 additions & 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 All @@ -26,6 +27,7 @@
"flow-bin": "^0.4.0",
"grunt": "^0.4.5",
"grunt-browserify": "^3.3.0",
"grunt-contrib-uglify": "^0.8.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-env": "^0.4.2",
"grunt-exorcise": "^1.0.0",
Expand Down
8 changes: 6 additions & 2 deletions playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
.reference {
width: 600px;
height: 20px;
width: 1200px;
height: 50px;
}
.basepair.a { fill: #188712; }
.basepair.g { fill: #C45C16; }
.basepair.c { fill: #0600F9; }
.basepair.t { fill: #F70016; }
.basepair.u { fill: #F70016; }
text {
font-size: 36;
font-weight: bold;
}
</style>
</head>

Expand Down
27 changes: 19 additions & 8 deletions src/GenomeTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
* @flow
*/

var React = require('react'),
var React = require('react/addons'),
_ = require('underscore'),
d3 = require('d3'),
types = require('./types');

var GenomeTrack = React.createClass({
propTypes: {
range: types.GenomeRange,
basePairs: React.PropTypes.string,
basePairs: React.PropTypes.object,
onRangeChange: React.PropTypes.func.isRequired
},
render: function(): any {
Expand All @@ -33,9 +33,12 @@ var GenomeTrack = React.createClass({
});

var NonEmptyGenomeTrack = React.createClass({
// This prevents updates if state & props have not changed.
mixins: [React.addons.PureRenderMixin],

propTypes: {
range: types.GenomeRange.isRequired,
basePairs: React.PropTypes.string.isRequired,
basePairs: React.PropTypes.object.isRequired,
onRangeChange: React.PropTypes.func.isRequired
},
render: function(): any {
Expand Down Expand Up @@ -97,7 +100,13 @@ var NonEmptyGenomeTrack = React.createClass({
return scale;
},
componentDidUpdate: function(prevProps: any, prevState: any) {
this.updateVisualization();
// Check a whitelist of properties which could change the visualization.
// For now, just basePairs and range.
var newProps = this.props;
if (!_.isEqual(newProps.basePairs, prevProps.basePairs) ||
!_.isEqual(newProps.range, prevProps.range)) {
this.updateVisualization();
}
},
updateVisualization: function() {
var div = this.getDOMNode(),
Expand All @@ -108,10 +117,12 @@ var NonEmptyGenomeTrack = React.createClass({

var scale = this.getScale();

var absBasePairs = [].map.call(this.props.basePairs, (bp, i) => ({
position: i + range.start,
letter: bp
}));
var contigColon = this.props.range.contig + ':';
var absBasePairs = _.range(range.start - 1, range.stop + 1)
.map(locus => ({
position: locus,
letter: this.props.basePairs[contigColon + locus]
}));

svg.attr('width', width)
.attr('height', height);
Expand Down
34 changes: 23 additions & 11 deletions src/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
var React = require('react'),
Controls = require('./Controls'),
GenomeTrack = require('./GenomeTrack'),
types = require('./types'),
// TODO: make this an "import type" when react-tools 0.13.0 is out.
TwoBit = require('./TwoBit');
TwoBitDataSource = require('./TwoBitDataSource'),
types = require('./types');


var Root = React.createClass({
propTypes: {
Expand All @@ -23,24 +24,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 = 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
85 changes: 85 additions & 0 deletions src/TwoBitDataSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* 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');

// TODO: make this an "import type" when react-tools 0.13.0 is out.
var TwoBit = require('./TwoBit');

// 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;


// TODO: make the return type more precise
var createTwoBitDataSource = function(remoteSource: TwoBit): any {
// 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]);
}
});
}

// Returns a {"chr12:123" -> "[ATCG]"} mapping for the range.
function getRange(range: GenomeRange) {
if (!range) return null;
return _.chain(_.range(range.start, range.stop))
.map(x => [range.contig + ':' + x, getBasePair(range.contig, x)])
.object()
.value();
}

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 bddf462

Please sign in to comment.