UNPKG

ucsc-xena-client

Version:

UCSC Xena Client. Functional genomics visualizations.

488 lines (401 loc) 16.2 kB
'use strict'; 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"); } }; }(); 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; } function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } var _ = require('./underscore_ext'); var Rx = require('./rx'); var widgets = require('./columnWidgets'); var colorScales = require('./colorScales'); var util = require('./util'); var Legend = require('./views/Legend'); var BandLegend = require('./views/BandLegend'); var React = require('react'); var CanvasDrawing = require('./CanvasDrawing'); var _require = require('./react-utils'), rxEvents = _require.rxEvents; var _require2 = require('./drawHeatmap'), drawHeatmap = _require2.drawHeatmap; // Since we don't set module.exports, but instead register ourselves // with columWidgets, react-hot-loader can't handle the updates automatically. // Accept hot loading here. if (module.hot) { module.hot.accept(); } // Since there are multiple components in the file we have to use makeHot // explicitly. function hotOrNot(component) { return module.makeHot ? module.makeHot(component) : component; } var colorFns = function colorFns(vs) { return _.map(vs, colorScales.colorScale); }; // // Tooltip // var prec = function () { var precision = 6, factor = Math.pow(10, precision); return function (val) { return val == null ? 'NA' : Math.round(val * factor) / factor; }; }(); // We're getting events with coords < 0. Not sure if this // is a side-effect of the react event system. This will // restrict values to the given range. function bounded(min, max, x) { return x < min ? min : x > max ? max : x; } var posString = function posString(p) { return p.chrom + ':' + util.addCommas(p.chromstart) + '-' + util.addCommas(p.chromend); }; //gb position string of the segment with 15bp extra on each side, centered at segment var posRegionString = function posRegionString(p) { var pad = Math.round((p.chromend - p.chromstart) / 2); return p.chrom + ':' + util.addCommas(p.chromstart - pad) + '-' + util.addCommas(p.chromend + pad); }; var gbURL = function gbURL(assembly, pos) { var assemblyString = encodeURIComponent(assembly), positionString = encodeURIComponent(posString(pos)), regionString = encodeURIComponent(posRegionString(pos)); return 'http://genome.ucsc.edu/cgi-bin/hgTracks?db=' + assemblyString + '&highlight=' + assemblyString + '.' + positionString + '&position=' + regionString; }; function tooltip(id, heatmap, assembly, fields, sampleFormat, fieldFormat, codes, position, width, zoom, samples, ev) { var coord = util.eventOffset(ev), sampleIndex = bounded(0, samples.length, Math.floor(coord.y * zoom.count / zoom.height + zoom.index)), sampleID = samples[sampleIndex], fieldIndex = bounded(0, fields.length, Math.floor(coord.x * fields.length / width)), pos = _.get(position, fieldIndex), field = fields[fieldIndex]; var val = _.getIn(heatmap, [fieldIndex, sampleIndex]), code = _.get(codes, val), label = fieldFormat(field); val = code ? code : prec(val); var mean = heatmap && prec(_.meannull(heatmap[fieldIndex])), median = heatmap && prec(_.medianNull(heatmap[fieldIndex])); return { sampleID: sampleFormat(sampleID), id: id, fieldIndex: fieldIndex, rows: [[['labelValue', label, val]]].concat(_toConsumableArray(pos && assembly ? [[['url', assembly + ' ' + posString(pos), gbURL(assembly, pos)]]] : []), _toConsumableArray(!code && mean !== 'NA' && median !== 'NA' ? [[['labelValue', 'Mean (Median)', mean + ' (' + median + ')']]] : [])) }; } // // Legends // function categoryLegend(dataIn, colorScale, codes) { if (!colorScale) { return { colors: [], labels: [] }; } // only finds categories for the current data in the column var data = _.reject(_.uniq(dataIn), function (x) { return x == null; }).sort(function (v1, v2) { return v1 - v2; }), colors = _.map(data, colorScale), labels = _.map(data, function (d) { return codes[d]; }); return { colors: colors, labels: labels }; } // Color scale cases // Use the domain of the scale as the label. // If using thresholded scales, add '<' '>' to labels. var cases = function cases(_ref, arg, c) { var _ref2 = _slicedToArray(_ref, 1), tag = _ref2[0]; return c[tag](arg); }; function legendForColorscale(colorSpec) { var scale = colorScales.colorScale(colorSpec), values = scale.domain(), colors = _.map(values, scale); var labels = cases(colorSpec, values, { 'no-data': function noData() { return []; }, 'float': _.identity, 'float-pos': _.identity, 'float-neg': _.identity, 'float-thresh': function floatThresh(_ref3) { var _ref4 = _slicedToArray(_ref3, 4), nl = _ref4[0], nh = _ref4[1], pl = _ref4[2], ph = _ref4[3]; return [nl, nh, pl, ph]; }, 'float-thresh-pos': function floatThreshPos(_ref5) { var _ref6 = _slicedToArray(_ref5, 2), low = _ref6[0], high = _ref6[1]; return [low, high]; }, 'float-thresh-neg': function floatThreshNeg(_ref7) { var _ref8 = _slicedToArray(_ref7, 2), low = _ref8[0], high = _ref8[1]; return [low, high]; }, 'float-thresh-log-pos': function floatThreshLogPos(_ref9) { var _ref10 = _slicedToArray(_ref9, 2), low = _ref10[0], high = _ref10[1]; return [low, high]; }, 'float-thresh-log-neg': function floatThreshLogNeg(_ref11) { var _ref12 = _slicedToArray(_ref11, 2), low = _ref12[0], high = _ref12[1]; return [low, high]; }, 'float-log': function floatLog(_ref13) { var _ref14 = _slicedToArray(_ref13, 2), low = _ref14[0], high = _ref14[1]; return [low, high]; } }); return { colors: colors, labels: labels }; } // We never want to draw multiple legends. We only draw the 1st scale // passed in. The caller should provide labels/colors in the 'legend' prop // if there are multiple scales. function renderFloatLegend(props) { var units = props.units, colors = props.colors, vizSettings = props.vizSettings, data = props.data; if (_.isEmpty(data)) { return React.createElement(Legend, { colors: [], labels: '', footnotes: [] });; } var subColColor = _.max(colors, function (colorList) { return _.uniq(colorList.slice(Math.ceil(colorList.length / 2.0))).length; }), _legendForColorscale = legendForColorscale(subColColor), labels = _legendForColorscale.labels, legendColors = _legendForColorscale.colors, unitText = units[0], footnotes = [units && units[0] ? React.createElement( 'span', { title: unitText }, unitText ) : null], hasViz = function hasViz(vizSettings) { return !isNaN(_.getIn(vizSettings, ['min'])); }, multiScaled = colors && colors.length > 1 && !hasViz(vizSettings); if (multiScaled) { labels = labels.map(function (label, i) { if (i === 0) { return "lower"; } else if (i === labels.length - 1) { return "higher"; } else { return ""; } }); } return React.createElement(Legend, { colors: legendColors, labels: labels, footnotes: footnotes }); } // might want to use <wbr> here, instead, so cut & paste work better, but that // will require a recursive split/flatmap to inject the <wbr> elements. var addWordBreaks = function addWordBreaks(str) { return str.replace(/([_/])/g, '\u200B$1\u200B'); }; function renderFloatLegendNew(props) { var units = props.units, colors = props.colors, data = props.data, vizSettings = props.vizSettings; if (_.isEmpty(data)) { return null; } var colorSpec = _.max(colors, function (colorList) { return _.uniq(colorList.slice(Math.ceil(colorList.length / 2.0))).length; }); if (colorSpec[0] === 'no-data') { return null; } var scale = colorScales.colorScale(colorSpec), values = scale.domain(), footnotes = units && units[0] ? [React.createElement( 'span', { title: units[0] }, addWordBreaks(units[0]) )] : null, hasViz = !isNaN(_.getIn(vizSettings, ['min'])), multiScaled = colors && colors.length > 1 && !hasViz; return React.createElement(BandLegend, { multiScaled: multiScaled, range: { min: _.first(values), max: _.last(values) }, colorScale: scale, footnotes: footnotes, width: 50, height: 20 }); } // Might have colorScale but no data (phenotype), no data & no colorScale, // or data & colorScale, no colorScale & data? function renderCodedLegend(props) { var _props$data = props.data; _props$data = _props$data === undefined ? [] : _props$data; var _props$data2 = _slicedToArray(_props$data, 1), data = _props$data2[0], codes = props.codes, _props$colors = props.colors, colors = _props$colors === undefined ? [] : _props$colors; var legendProps; var colorfn = _.first(colorFns(colors.slice(0, 1))); // We can use domain() for categorical, but we want to filter out // values not in the plot. Also, we build the categorical from all // values in the db (even those not in the plot) so that colors will // match in other datasets. if (data && colorfn) { // category legendProps = categoryLegend(data, colorfn, codes); } else { return React.createElement('span', null); } return React.createElement(Legend, legendProps); } var HeatmapLegend = hotOrNot(function (_PureComponent) { _inherits(_class, _PureComponent); function _class() { _classCallCheck(this, _class); return _possibleConstructorReturn(this, (_class.__proto__ || Object.getPrototypeOf(_class)).apply(this, arguments)); } _createClass(_class, [{ key: 'render', value: function render() { var _props = this.props, column = _props.column, data = _props.data, newLegend = _props.newLegend, units = column.units, heatmap = column.heatmap, colors = column.colors, valueType = column.valueType, vizSettings = column.vizSettings, defaultNormalization = column.defaultNormalization, props = { units: units, colors: colors, vizSettings: vizSettings, defaultNormalization: defaultNormalization, data: heatmap, coded: valueType === 'coded', codes: _.get(data, 'codes') }; return (props.coded ? renderCodedLegend : newLegend ? renderFloatLegendNew : renderFloatLegend)(props); } }]); return _class; }(_PureComponent4.default)); // // plot rendering // var HeatmapColumn = hotOrNot( // // plot rendering // function (_PureComponent2) { _inherits(_class3, _PureComponent2); function _class3() { var _ref15; var _temp, _this2, _ret; _classCallCheck(this, _class3); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this2 = _possibleConstructorReturn(this, (_ref15 = _class3.__proto__ || Object.getPrototypeOf(_class3)).call.apply(_ref15, [this].concat(args))), _this2), _this2.tooltip = function (ev) { var _this2$props = _this2.props, samples = _this2$props.samples, data = _this2$props.data, column = _this2$props.column, zoom = _this2$props.zoom, sampleFormat = _this2$props.sampleFormat, fieldFormat = _this2$props.fieldFormat, id = _this2$props.id, codes = _.get(data, 'codes'), position = column.position || _.getIn(data, ['req', 'position']), assembly = column.assembly, fields = column.fields, heatmap = column.heatmap, width = column.width; return tooltip(id, heatmap, assembly, fields, sampleFormat, fieldFormat(id), codes, position, width, zoom, samples, ev); }, _temp), _possibleConstructorReturn(_this2, _ret); } _createClass(_class3, [{ key: 'componentWillMount', value: function componentWillMount() { var _this3 = 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: _this3.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: 'render', // To reduce this set of properties, we could // - Drop data & move codes into the 'display' obj, outside of data // Might also want to copy fields into 'display', so we can drop req probes value: function render() { var _props2 = this.props, data = _props2.data, column = _props2.column, zoom = _props2.zoom, heatmap = column.heatmap, colors = column.colors, codes = _.get(data, 'codes'); return React.createElement(CanvasDrawing, { ref: 'plot', draw: drawHeatmap, wrapperProps: { className: 'Tooltip-target', onMouseMove: this.on.mousemove, onMouseOut: this.on.mouseout, onMouseOver: this.on.mouseover, onClick: this.props.onClick }, codes: codes, width: _.get(column, 'width'), zoom: zoom, colors: colors, heatmapData: heatmap }); } }]); return _class3; }(_PureComponent4.default)); var getColumn = function getColumn(props) { return React.createElement(HeatmapColumn, props); }; widgets.column.add("probes", getColumn); widgets.column.add("geneProbes", getColumn); widgets.column.add("genes", getColumn); widgets.column.add("clinical", getColumn); var getLegend = function getLegend(props) { return React.createElement(HeatmapLegend, props); }; widgets.legend.add('probes', getLegend); widgets.legend.add('geneProbes', getLegend); widgets.legend.add('genes', getLegend); widgets.legend.add('clinical', getLegend);