UNPKG

ucsc-xena-client

Version:

UCSC Xena Client. Functional genomics visualizations.

630 lines (530 loc) 21.2 kB
'use strict'; 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;