Created
July 16, 2015 15:08
-
-
Save armish/fc1dc24349f1270753a6 to your computer and use it in GitHub Desktop.
Pileup Visualization Boilerplate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Visualization of genes, including exons and coding regions. | |
* @flow | |
*/ | |
'use strict'; | |
var React = require('./react-shim'), | |
_ = require('underscore'), | |
d3 = require('d3'), | |
shallowEquals = require('shallow-equals'), | |
types = require('./react-types'), | |
bedtools = require('./bedtools'), | |
Interval = require('./Interval'), | |
d3utils = require('./d3utils'), | |
ContigInterval = require('./ContigInterval'); | |
var GeneTrack = React.createClass({ | |
displayName: 'genes', | |
propTypes: { | |
range: types.GenomeRange, | |
source: React.PropTypes.object.isRequired, | |
onRangeChange: React.PropTypes.func.isRequired, | |
}, | |
render: function(): any { | |
var range = this.props.range; | |
if (!range) { | |
return <EmptyTrack />; | |
} | |
return <NonEmptyGeneTrack {...this.props} />; | |
} | |
}); | |
var NonEmptyGeneTrack = React.createClass({ | |
propTypes: { | |
range: types.GenomeRange.isRequired, | |
source: React.PropTypes.object.isRequired, | |
onRangeChange: React.PropTypes.func.isRequired, | |
}, | |
getInitialState: function() { | |
return { | |
width: 0, | |
height: 0, | |
genes: [] | |
}; | |
}, | |
render: function() { | |
return <div></div>; | |
}, | |
updateSize: function() { | |
var parentDiv = this.getDOMNode().parentNode; | |
this.setState({ | |
width: parentDiv.offsetWidth, | |
height: parentDiv.offsetHeight | |
}); | |
}, | |
componentDidMount: function() { | |
var div = this.getDOMNode(), | |
svg = d3.select(div) | |
.append('svg'); | |
window.addEventListener('resize', () => this.updateSize()); | |
this.updateSize(); | |
// Visualize new reference data as it comes in from the network. | |
this.props.source.on('newdata', () => { | |
var range = this.props.range, | |
ci = new ContigInterval(range.contig, range.start, range.stop); | |
this.setState({ | |
genes: this.props.source.getGenesInRange(ci) | |
}); | |
}); | |
this.updateVisualization(); | |
}, | |
getScale: function() { | |
return d3utils.getTrackScale(this.props.range, this.state.width); | |
}, | |
componentDidUpdate: function(prevProps: any, prevState: any) { | |
if (!shallowEquals(prevProps, this.props) || | |
!shallowEquals(prevState, this.state)) { | |
this.updateVisualization(); | |
} | |
}, | |
updateVisualization: function() { | |
var div = this.getDOMNode(), | |
width = this.state.width, | |
height = this.state.height, | |
svg = d3.select(div).select('svg'); | |
// Hold off until height & width are known. | |
if (width === 0) return; | |
var scale = this.getScale(), | |
// We can't clamp scale directly because of offsetPx. | |
clampedScale = d3.scale.linear() | |
.domain([scale.invert(0), scale.invert(width)]) | |
.range([0, width]) | |
.clamp(true); | |
svg.attr('width', width) | |
.attr('height', height); | |
var genes = svg.selectAll('g.gene') | |
.data(this.state.genes, gene => gene.id); | |
// Enter | |
var geneGs = genes.enter() | |
.append('g') | |
.attr('class', 'gene'); | |
geneGs.append('text'); | |
var geneLineG = geneGs.append('g').attr('class', 'track'); | |
geneLineG.selectAll('rect.exon') | |
.data(g => bedtools.splitCodingExons(g.exons, g.codingRegion)) | |
.enter() | |
.append('rect') | |
.attr('class', 'exon'); | |
geneLineG.append('line'); | |
geneLineG.append('rect').attr('class', 'strand'); | |
// The gene name goes in the center of the gene, modulo boundary effects. | |
var textCenterX = g => { | |
var p = g.position; | |
return 0.5 * (clampedScale(p.start()) + clampedScale(p.stop())); | |
}; | |
var scaledWidth = g => scale(g.position.stop()) - scale(g.position.start()); | |
var geneLineY = height / 4; | |
// Enter & update | |
var track = genes.selectAll('g.track') | |
.attr('transform', | |
g => `translate(0 ${geneLineY})`); | |
genes.selectAll('text') | |
.attr({ | |
'x': textCenterX, | |
'y': geneLineY + 15 | |
}) | |
.text(g => g.name || g.id) | |
.call(removeOverlapping); | |
track.selectAll('line').attr({ | |
'x1': g => scale(g.position.start()), | |
'x2': g => scale(g.position.stop()), | |
'y1': 0, | |
'y2': 0 | |
}); | |
track.selectAll('rect.strand') | |
.attr({ | |
'y': -4, | |
'height': 9, | |
'x': g => scale(g.position.start()), | |
'width': scaledWidth, | |
'fill': g => `url(#${g.strand == '+' ? '' : 'anti'}sense)`, | |
'stroke': 'none' | |
}); | |
track.selectAll('rect.exon') | |
.attr({ | |
'x': exon => scale(exon.start), | |
'width': exon => scale(exon.stop) - scale(exon.start), | |
'y': exon => -3 * (exon.isCoding ? 2 : 1), | |
'height': exon => 6 * (exon.isCoding ? 2 : 1) | |
}); | |
// Exit | |
genes.exit().remove(); | |
} | |
}); | |
var EmptyTrack = React.createClass({ | |
render: function() { | |
return <div className="genes empty">Zoom in to see genes</div>; | |
} | |
}); | |
module.exports = GeneTrack; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment