ucsc-xena-client
Version:
UCSC Xena Client. Functional genomics visualizations.
630 lines (530 loc) • 21.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
exports.findIntervals = findIntervals;
var _PureComponent3 = require('./PureComponent');
var _PureComponent4 = _interopRequireDefault(_PureComponent3);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _ = require('./underscore_ext');
var React = require('react');
var ReactDOM = require('react-dom');
var Rx = require('./rx');
var intervalTree = require('static-interval-tree');
var vgcanvas = require('./vgcanvas');
var layoutPlot = require('./layoutPlot');
var matches = intervalTree.matches,
index = intervalTree.index;
var pxTransformEach = layoutPlot.pxTransformEach;
var _require = require('./react-utils'),
rxEvents = _require.rxEvents;
var util = require('./util');
var _require2 = require('./exonLayout'),
chromPositionFromScreen = _require2.chromPositionFromScreen;
var _require3 = require('./colorScales'),
isoluminant = _require3.isoluminant;
var styles = require('./refGeneExons.module.css');
// annotate an interval with cds status
var inCds = function inCds(_ref, intvl) {
var cdsStart = _ref.cdsStart,
cdsEnd = _ref.cdsEnd;
return _.assoc(intvl, 'inCds', intvl.start <= cdsEnd && cdsStart <= intvl.end);
};
// split an interval at pos if it overlaps
var splitOnPos = function splitOnPos(pos, i) {
return i.start < pos && pos <= i.end ? [_.assoc(i, 'end', pos - 1), _.assoc(i, 'start', pos)] : i;
};
// create interval record
var toIntvl = function toIntvl(start, end, i) {
return { start: start, end: end, i: i };
};
// Create drawing intervals, by spliting exons on cds bounds, and annotating if each
// resulting region is in the cds. Each region is also annotated by its index in the
// list of exons, so we can alternate colors when rendering.
//
// findIntervals(gene :: {cdsStart :: int, cdsEnd :: int, exonStarts :: [int, ...], exonEnds :: [int, ...]})
// :: [{start :: int, end :: int, i :: int, inCds :: boolean}, ...]
function findIntervals(gene) {
if (_.isEmpty(gene)) {
return [];
}
var cdsStart = gene.cdsStart,
cdsEnd = gene.cdsEnd,
exonStarts = gene.exonStarts,
exonEnds = gene.exonEnds;
return _.map(_.flatmap(_.flatmap(_.zip(exonStarts, exonEnds), function (_ref2, i) {
var _ref3 = _slicedToArray(_ref2, 2),
s = _ref3[0],
e = _ref3[1];
return splitOnPos(cdsStart, toIntvl(s, e, i));
}), function (i) {
return splitOnPos(cdsEnd + 1, i);
}), function (i) {
return inCds(gene, i);
});
}
var shade1 = '#cccccc',
shade2 = '#999999',
shade3 = '#000080',
shade4 = '#FF0000';
function getAnnotation(index, perLaneHeight, offset) {
return {
utr: {
y: offset + perLaneHeight * (index + 0.25),
h: perLaneHeight / 2
},
cds: {
y: offset + perLaneHeight * index,
h: perLaneHeight
}
};
}
function drawIntroArrows(ctx, xStart, xEnd, endY, segments, strand) {
if (xEnd - xStart < 10) {
return;
}
var arrowSize = 2,
//arrowSize
gapSize = 4;
ctx.strokeStyle = shade2;
for (var i = xStart; i < xEnd; i = i + 10) {
var found = segments.filter(function (seg) {
return Math.abs(seg[0] - i) < gapSize || Math.abs(seg[0] - i - arrowSize) < gapSize || Math.abs(seg[1] - i) < gapSize || Math.abs(seg[1] - i - arrowSize) < gapSize;
});
if (_.isEmpty(found)) {
if (strand === '+') {
ctx.beginPath();
ctx.moveTo(i, endY - arrowSize);
ctx.lineTo(i + arrowSize, endY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(i, endY + arrowSize);
ctx.lineTo(i + arrowSize, endY);
ctx.stroke();
} else {
// "-" strand
ctx.beginPath();
ctx.moveTo(i + arrowSize, endY - arrowSize);
ctx.lineTo(i, endY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(i + arrowSize, endY + arrowSize);
ctx.lineTo(i, endY);
ctx.stroke();
}
}
}
}
var probeLayout = function probeLayout(layout, positions) {
return layoutPlot.pxTransformFlatmap(layout, function (toPx) {
return positions.map(function (pos) {
return toPx([pos.chromstart, pos.chromend]);
});
});
};
function drawProbePositions(ctx, probePosition, height, positionHeight, width, layout) {
var count = probePosition.length,
screenProbes = probeLayout(layout, probePosition),
probeHeight = 2,
probeY = height,
geneY = height - positionHeight,
// conventional rainbow scale
// colors = _.times(count, i => `hsl(${Math.round(i * 240 / count)}, 100%, 50%)`);
colors = _.times(count, isoluminant(0, count));
pxTransformEach(layout, function (toPx, _ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
start = _ref5[0],
end = _ref5[1];
ctx.lineWidth = 0.5;
var positions = probePosition.map(function (p, i) {
return [p, i];
}).filter(function (_ref6) {
var _ref7 = _slicedToArray(_ref6, 1),
_ref7$ = _ref7[0],
chromstart = _ref7$.chromstart,
chromend = _ref7$.chromend;
return chromstart <= end && start <= chromend;
});
positions.forEach(function (_ref8) {
var _ref9 = _slicedToArray(_ref8, 2),
_ref9$ = _ref9[0],
chromstart = _ref9$.chromstart,
chromend = _ref9$.chromend,
i = _ref9[1];
var startX = width / probePosition.length * (i + 0.5),
middle = (chromstart + chromend) / 2,
_toPx = toPx([middle, middle]),
_toPx2 = _slicedToArray(_toPx, 1),
endX = _toPx2[0];
ctx.beginPath();
ctx.moveTo(startX, probeY - probeHeight);
ctx.lineTo(endX, geneY + probeHeight);
ctx.strokeStyle = colors[i];
ctx.stroke();
});
});
screenProbes.forEach(function (_ref10, i) {
var _ref11 = _slicedToArray(_ref10, 2),
pxStart = _ref11[0],
pxEnd = _ref11[1];
ctx.beginPath();
ctx.moveTo(pxStart, geneY);
ctx.lineTo(pxEnd + 1, geneY);
ctx.lineTo(pxEnd + 1, geneY + probeHeight);
ctx.lineTo(pxStart, geneY + probeHeight);
ctx.fillStyle = colors[i];
ctx.fill();
});
_.times(screenProbes.length, function (i) {
ctx.beginPath();
ctx.moveTo(width / probePosition.length * i + 1, probeY - probeHeight);
ctx.lineTo(width / probePosition.length * (i + 1) - 1, probeY - probeHeight);
ctx.lineTo(width / probePosition.length * (i + 1) - 1, probeY);
ctx.lineTo(width / probePosition.length * i + 1, probeY);
ctx.fillStyle = colors[i];
ctx.fill();
});
}
var RefGeneDrawing = function (_React$Component) {
_inherits(RefGeneDrawing, _React$Component);
function RefGeneDrawing() {
var _ref12;
var _temp, _this, _ret;
_classCallCheck(this, RefGeneDrawing);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref12 = RefGeneDrawing.__proto__ || Object.getPrototypeOf(RefGeneDrawing)).call.apply(_ref12, [this].concat(args))), _this), _this.computeAnnotationLanes = function (_ref13, single) {
var position = _ref13.position,
refGene = _ref13.refGene,
height = _ref13.height,
_ref13$positionHeight = _ref13.positionHeight,
positionHeight = _ref13$positionHeight === undefined ? 0 : _ref13$positionHeight;
var bottomPad = positionHeight ? 1 : 0,
annotationHeight = height - positionHeight - bottomPad,
newAnnotationLanes;
if (position && refGene) {
var lanes = [],
_position = _slicedToArray(position, 2),
start = _position[0],
end = _position[1];
//only keep genes with in the current view
refGene = _.values(refGene).filter(function (val) {
return val.txStart <= end && val.txEnd >= start;
});
//multip lane no-overlapping genes
refGene.forEach(function (val) {
var added = lanes.some(function (lane) {
if (lane.every(function (gene) {
return !(val.txStart <= gene.txEnd && val.txEnd >= val.txStart);
})) {
return lane.push(val);
}
});
if (!added) {
// add a new lane
if (!single || lanes.length === 0) {
lanes.push([val]);
} else {
lanes[0].push(val);
}
}
});
var perLaneHeight = _.min([annotationHeight / (lanes.length || 1), 12]),
// if no position annotation, vertically center refgene.
// Otherwise, push it against the position annotation.
centering = positionHeight ? 1 : 2,
laneOffset = (annotationHeight - perLaneHeight * lanes.length) / centering;
newAnnotationLanes = {
arrows: !(refGene.length > 1 && single),
lanes: lanes,
perLaneHeight: perLaneHeight,
laneOffset: laneOffset,
annotationHeight: annotationHeight
};
} else {
newAnnotationLanes = {
lanes: undefined,
perLaneHeight: undefined,
laneOffset: undefined,
annotationHeight: annotationHeight
};
}
// cache for tooltip
_this.annotationLanes = newAnnotationLanes;
}, _this.draw = function (props) {
var width = props.width,
layout = props.layout,
height = props.height,
positionHeight = props.positionHeight,
mode = props.mode,
probePosition = props.probePosition;
_this.computeAnnotationLanes(props, false);
var _this$annotationLanes = _this.annotationLanes,
lanes = _this$annotationLanes.lanes,
perLaneHeight = _this$annotationLanes.perLaneHeight,
arrows = _this$annotationLanes.arrows,
laneOffset = _this$annotationLanes.laneOffset;
// white background
_this.vg.box(0, 0, width, height, 'white');
if (!width || !layout) {
return;
}
var vg = _this.vg,
ctx = vg.context();
if (vg.width() !== width) {
vg.width(width);
}
if (_.isEmpty(layout.chrom)) {
return;
}
//drawing start here, one lane at a time
lanes.forEach(function (lane, k) {
var annotation = getAnnotation(k, perLaneHeight, laneOffset);
lane.forEach(function (gene) {
var intervals = findIntervals(gene),
indx = index(intervals),
lineY = laneOffset + perLaneHeight * (k + 0.5);
//find segments for one gene
pxTransformEach(layout, function (toPx, _ref14) {
var _ref15 = _slicedToArray(_ref14, 2),
start = _ref15[0],
end = _ref15[1];
var nodes = matches(indx, { start: start, end: end }),
segments = nodes.map(function (_ref16) {
var i = _ref16.i,
start = _ref16.start,
end = _ref16.end,
inCds = _ref16.inCds;
var _annotation = annotation[inCds ? 'cds' : 'utr'],
y = _annotation.y,
h = _annotation.h,
_toPx5 = toPx([start, end]),
_toPx6 = _slicedToArray(_toPx5, 2),
pstart = _toPx6[0],
pend = _toPx6[1],
shade = mode === "geneExon" ? i % 2 === 1 ? shade1 : shade2 : mode === "coordinate" ? gene.strand === '-' ? shade3 : shade4 : shade2;
return [pstart, pend, shade, y, h];
}),
_toPx3 = toPx([gene.txStart, gene.txEnd]),
_toPx4 = _slicedToArray(_toPx3, 2),
pGeneStart = _toPx4[0],
pGeneEnd = _toPx4[1];
// draw a line across the gene
ctx.fillStyle = shade2;
ctx.fillRect(pGeneStart, lineY, pGeneEnd - pGeneStart, 1);
if (arrows) {
drawIntroArrows(ctx, pGeneStart, pGeneEnd, lineY, segments, mode === 'coordinate' ? gene.strand : '+');
}
// draw each segment
_.each(segments, function (_ref17) {
var _ref18 = _slicedToArray(_ref17, 5),
pstart = _ref18[0],
pend = _ref18[1],
shade = _ref18[2],
y = _ref18[3],
h = _ref18[4];
ctx.fillStyle = shade;
ctx.fillRect(pstart, y, pend - pstart || 1, h);
});
});
});
});
// what about introns?
if (!_.isEmpty(probePosition)) {
drawProbePositions(ctx, probePosition, height, positionHeight, width, layout);
}
}, _this.tooltip = function (ev) {
var _this$props = _this.props,
layout = _this$props.layout,
assembly = _this$props.column.assembly;
if (!layout) {
// gene model not loaded
return;
}
var _util$eventOffset = util.eventOffset(ev),
x = _util$eventOffset.x,
y = _util$eventOffset.y,
_this$annotationLanes2 = _this.annotationLanes,
perLaneHeight = _this$annotationLanes2.perLaneHeight,
laneOffset = _this$annotationLanes2.laneOffset,
lanes = _this$annotationLanes2.lanes,
rows = [],
assemblyString = encodeURIComponent(assembly),
contextPadding = Math.floor((layout.zoom.end - layout.zoom.start) / 4),
posLayout = layout.chromName + ':' + util.addCommas(layout.zoom.start) + '-' + util.addCommas(layout.zoom.end),
posLayoutPadding = layout.chromName + ':' + util.addCommas(layout.zoom.start - contextPadding) + '-' + util.addCommas(layout.zoom.end + contextPadding),
posLayoutString = encodeURIComponent(posLayout),
posLayoutPaddingString = encodeURIComponent(posLayoutPadding),
GBurlZoom = 'http://genome.ucsc.edu/cgi-bin/hgTracks?db=' + assemblyString + '&highlight=' + assemblyString + '.' + posLayoutString + '&position=' + posLayoutPaddingString;
if (y > laneOffset && y < laneOffset + lanes.length * perLaneHeight) {
var posStart = chromPositionFromScreen(layout, x - 0.5),
posEnd = chromPositionFromScreen(layout, x + 0.5),
matches = [],
laneIndex = Math.floor((y - laneOffset) / perLaneHeight); //find which lane by y
lanes[laneIndex].forEach(function (gene) {
if (posEnd >= gene.txStart && posStart <= gene.txEnd) {
matches.push(gene);
}
});
if (matches.length > 0) {
matches.forEach(function (match) {
var posGene = match.chrom + ':' + util.addCommas(match.txStart) + '-' + util.addCommas(match.txEnd),
positionGeneString = encodeURIComponent(posGene),
GBurlGene = 'http://genome.ucsc.edu/cgi-bin/hgTracks?db=' + assemblyString + '&position=' + positionGeneString + '&enableHighlightingDialog=0';
rows.push([['value', 'Gene '], ['url', '' + match.name2, GBurlGene]]);
});
}
}
rows.push([['value', 'Column'], ['url', assembly + ' ' + posLayout, GBurlZoom]]);
return {
rows: rows
};
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(RefGeneDrawing, [{
key: 'componentWillMount',
value: function componentWillMount() {
var _this2 = this;
var events = rxEvents(this, 'mouseout', 'mousemove', 'mouseover');
// Compute tooltip events from mouse events.
this.ttevents = events.mouseover.filter(function (ev) {
return util.hasClass(ev.currentTarget, 'Tooltip-target');
}).flatMap(function () {
return events.mousemove.takeUntil(events.mouseout).map(function (ev) {
return {
data: _this2.tooltip(ev),
open: true
};
}) // look up current data
.concat(Rx.Observable.of({ open: false }));
}).subscribe(this.props.tooltip);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.ttevents.unsubscribe();
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
var _props = this.props,
width = _props.width,
height = _props.height;
this.vg = vgcanvas(ReactDOM.findDOMNode(this.refs.canvas), width, height);
this.draw(this.props);
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate() {
return false;
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(newProps) {
if (this.vg && !_.isEqual(newProps, this.props)) {
this.draw(newProps);
}
}
// single: force a single lane, i.e. 'dense' mode. Unused.
}, {
key: 'render',
value: function render() {
return React.createElement('canvas', {
className: 'Tooltip-target',
onMouseMove: this.on.mousemove,
onMouseOut: this.on.mouseout,
onMouseOver: this.on.mouseover,
onClick: this.props.onClick,
ref: 'canvas' });
}
}]);
return RefGeneDrawing;
}(React.Component);
var RefGeneHighlight = function (_PureComponent) {
_inherits(RefGeneHighlight, _PureComponent);
function RefGeneHighlight() {
_classCallCheck(this, RefGeneHighlight);
return _possibleConstructorReturn(this, (RefGeneHighlight.__proto__ || Object.getPrototypeOf(RefGeneHighlight)).apply(this, arguments));
}
_createClass(RefGeneHighlight, [{
key: 'render',
value: function render() {
var _props2 = this.props,
height = _props2.height,
position = _props2.position,
style = height ? { width: Math.max(position[1] - position[0], 1), height: height, left: position[0] } : { display: 'none' };
return React.createElement(
'div',
{ className: styles.highlight },
React.createElement('div', { className: styles.box, style: style })
);
}
}]);
return RefGeneHighlight;
}(_PureComponent4.default);
var RefGeneAnnotation = function (_PureComponent2) {
_inherits(RefGeneAnnotation, _PureComponent2);
function RefGeneAnnotation() {
var _ref19;
var _temp2, _this4, _ret2;
_classCallCheck(this, RefGeneAnnotation);
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _ret2 = (_temp2 = (_this4 = _possibleConstructorReturn(this, (_ref19 = RefGeneAnnotation.__proto__ || Object.getPrototypeOf(RefGeneAnnotation)).call.apply(_ref19, [this].concat(args))), _this4), _this4.state = { probe: undefined }, _temp2), _possibleConstructorReturn(_this4, _ret2);
}
_createClass(RefGeneAnnotation, [{
key: 'componentWillMount',
value: function componentWillMount() {
var _this5 = this;
this.sub = this.props.tooltip.subscribe(function (ev) {
if (_.getIn(ev, ['data', 'id']) === _this5.props.id) {
_this5.setState({
probe: _.getIn(ev, ['data', 'fieldIndex']),
x: _.getIn(ev, ['data', 'x']) });
} else if (_this5.state.probe !== null || _this5.state.x !== null) {
_this5.setState({ probe: undefined, x: undefined });
}
});
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.sub.unsubscribe();
}
}, {
key: 'render',
value: function render() {
var _props3 = this.props,
probePosition = _props3.probePosition,
height = _props3.height,
positionHeight = _props3.positionHeight,
layout = _props3.layout,
_state = this.state,
probe = _state.probe,
x = _state.x,
highlight = probe != null && _.get(probePosition, probe) ? {
position: probeLayout(layout, [probePosition[probe]])[0],
height: height - positionHeight
} : x != null ? {
position: [x, x + 1],
height: height - positionHeight
} : {};
return React.createElement(
'div',
{ className: styles.refGene },
React.createElement(RefGeneDrawing, this.props),
React.createElement(RefGeneHighlight, highlight)
);
}
}]);
return RefGeneAnnotation;
}(_PureComponent4.default);
//widgets.annotation.add('gene', props => <RefGeneAnnotation {...props}/>);
exports.default = RefGeneAnnotation;