ucsc-xena-client
Version:
UCSC Xena Client. Functional genomics visualizations.
488 lines (401 loc) • 16.2 kB
JavaScript
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);
;