UNPKG

ucsc-xena-client

Version:

UCSC Xena Client. Functional genomics visualizations.

333 lines (291 loc) 10.5 kB
'use strict'; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 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"); } }; }(); 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 partition = require('./partition'); var colorScales = require('./colorScales'); var colorHelper = require('./color_helper'); var labelFont = 12; var labelMargin = 1; // left & right margin // Writing this optimized because it's expensive when // zoomed out on a large cohort. function findContiguous(arr, min) { var start, end = 0, length = arr.length, res = [], clen; while (end < length) { start = end; while (end < length && arr[start] === arr[end]) { ++end; } clen = end - start; if (clen > min) { res.push([start, clen]); } } return res; } function codeLabels(codes, rowData, minSpan) { var groups = findContiguous(rowData, minSpan); return groups.map(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), start = _ref2[0], len = _ref2[1]; return [_.get(codes, rowData[start], null), start, len]; }); } function floatLabels(rowData, minSpan) { var nnLabels = minSpan <= 1 ? _.filter(rowData.map(function (v, i) { return v % 1 ? [v.toPrecision([3]), i, 1] : [v, i, 1]; // display float with 3 significant digit, integer no change }), function (_ref3) { var _ref4 = _slicedToArray(_ref3, 1), v = _ref4[0]; return v !== null; }) : [], nullLabels = _.filter(findContiguous(rowData, minSpan).map(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2), start = _ref6[0], len = _ref6[1]; return [rowData[start], start, len]; }), function (_ref7) { var _ref8 = _slicedToArray(_ref7, 1), v = _ref8[0]; return v === null; }); return [].concat(_toConsumableArray(nnLabels), _toConsumableArray(nullLabels)); } // Like groupBy, but combine new elements with the group, using // the reducing function fn. // We use a Map for ordered, numeric keys. function reduceByKey(arr) { var keyFn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (x) { return x; }; var fn = arguments[2]; var ret = new Map(); arr.forEach(function (e) { var k = keyFn(e); ret.set(k, fn(e, k, ret.get(k))); }); return ret; } function findRegions(index, height, count) { // Find pixel regions having the same set of samples, e.g. // 10 samples in 1 px, or 1 sample over 10 px. Record the // range of samples in the region. var regions = reduceByKey(_.range(count), function (i) { return ~~(i * height / count); }, function (i, y, r) { return r ? _extends({}, r, { end: i }) : { y: y, start: i, end: i }; }), starts = [].concat(_toConsumableArray(regions.keys())), se = _.partitionN(starts, 2, 1, [height]); // XXX side-effecting map _.mmap(starts, se, function (start, _ref9) { var _ref10 = _slicedToArray(_ref9, 2), s = _ref10[0], e = _ref10[1]; return regions.get(start).height = e - s; }); return regions; } var gte = function gte(l) { return function (v) { return v >= l; }; }; var emptyDomain = function emptyDomain() { return { count: 0, sum: 0 }; }; function tallyDomains(d, start, end, domains) { var acc = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : _.times(domains.length + 1, emptyDomain); var i = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : start; var _repeat = true; var _d, _start, _end, _domains, _acc, _i; while (_repeat) { _repeat = false; var v = d[i]; if (i === end) { return acc; } if (v !== null) { var _i2 = _.findIndexDefault(domains, gte(v), domains.length); acc[_i2].count++; acc[_i2].sum += v; } _d = d; _start = start; _end = end; _domains = domains; _acc = acc; _i = i + 1; d = _d; start = _start; end = _end; domains = _domains; acc = _acc; i = _i; _repeat = true; continue; } } var gray = colorHelper.rgb(colorHelper.greyHEX); var regionColorMethods = { // For ordinal scales, subsample by picking a random data point. // Doing slice here to simplify the random selection. We don't have // many subcolumns with ordinal data, so this shouldn't be a performance problem. 'ordinal': function ordinal(scale, d, start, end) { return _.Let(function () { var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : d.slice(start, end).filter(function (x) { return x != null; }); return s.length ? colorHelper.rgb(scale(s[Math.floor(s.length * Math.random())])) : gray; }); }, // For float scales, compute per-domain average values, and do a weighed mix of the colors. 'default': function _default(scale, d, start, end) { var domainGroups = tallyDomains(d, start, end, scale.domain()), groupColors = domainGroups.map(function (g) { return g.count ? scale.rgb(g.sum / g.count) : null; }), groupCounts = domainGroups.map(function (g) { return g.count; }), total = _.sum(groupCounts); // blend colors via rms return _.times(3, function (ch) { return ~~Math.sqrt(_.sum(_.mmap(groupColors, groupCounts, function (rgb, n) { return rgb == null ? 0 : rgb[ch] * rgb[ch] * n / total; }))); }); } }; var regionColor = function regionColor(type, scale, d, start, end) { return (regionColorMethods[type] || regionColorMethods.default)(scale, d, start, end); }; function drawLayoutByPixel(vg, opts) { var height = opts.height, width = opts.width, index = opts.index, count = opts.count, layout = opts.layout, data = opts.data, codes = opts.codes, colors = opts.colors, minTxtWidth = vg.textWidth(labelFont, 'WWWW'), first = Math.floor(index), last = Math.ceil(index + count); // reset image vg.box(0, 0, width, height, "gray"); if (data.length === 0) { // no features to draw return; } var regions = findRegions(index, height, count), ctx = vg.context(), img = ctx.createImageData(width, height); layout.forEach(function (el, i) { var rowData = data[i], colorScale = colorScales.colorScale(colors[i]); // XXX watch for poor iterator performance in this for...of. var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = regions.keys()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var rs = _step.value; var r = regions.get(rs); if (_.anyRange(rowData, first + r.start, first + r.end + 1, function (v) { return v != null; })) { var color = regionColor(colors[i][0], colorScale, rowData, first + r.start, first + r.end + 1); for (var y = rs; y < rs + r.height; ++y) { var pxRow = y * width, buffStart = (pxRow + el.start) * 4, buffEnd = (pxRow + el.start + el.size) * 4; for (var l = buffStart; l < buffEnd; l += 4) { img.data[l] = color[0]; img.data[l + 1] = color[1]; img.data[l + 2] = color[2]; img.data[l + 3] = 255; // XXX can we set + 3 to 255 globally? } } } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } }); ctx.putImageData(img, 0, 0); layout.forEach(function (el, i) { var rowData = data[i].slice(first, last), colorScale = colorScales.colorScale(colors[i]); // Add labels var minSpan = labelFont / (height / count); if (el.size - 2 * labelMargin >= minTxtWidth) { var labels = codes ? codeLabels(codes, rowData, minSpan) : floatLabels(rowData, minSpan), h = height / count, labelColors = rowData.map(colorScale), uniqStates = _.filter(_.uniq(rowData), function (c) { return c != null; }), colorWhiteBlack = uniqStates.length === 2 && // looking for [0,1] columns color differently _.indexOf(uniqStates, 1) !== -1 && _.indexOf(uniqStates, 0) !== -1 ? true : false, codedColor = colorWhiteBlack || codes; // coloring as coded column: coded column or binary float column (0s and 1s) vg.clip(el.start + labelMargin, 0, el.size - labelMargin, height, function () { return labels.forEach(function (_ref11) { var _ref12 = _slicedToArray(_ref11, 3), l = _ref12[0], i = _ref12[1], ih = _ref12[2]; return (/* label, index, count */ vg.textCenteredPushRight(el.start + labelMargin, h * i - 1, el.size - labelMargin, h * ih, codedColor && labelColors[i] ? colorHelper.contrastColor(labelColors[i]) : 'black', labelFont, l) ); }); }); } }); } var drawHeatmapByMethod = function drawHeatmapByMethod(draw) { return function (vg, props) { var _props$heatmapData = props.heatmapData, heatmapData = _props$heatmapData === undefined ? [] : _props$heatmapData, codes = props.codes, colors = props.colors, width = props.width, _props$zoom = props.zoom, index = _props$zoom.index, count = _props$zoom.count, height = _props$zoom.height; vg.labels(function () { draw(vg, { height: height, width: width, index: index, count: count, data: heatmapData, codes: codes, layout: partition.offsets(width, 0, heatmapData.length), colors: colors }); }); }; }; module.exports = { drawHeatmap: drawHeatmapByMethod(drawLayoutByPixel), tallyDomains: tallyDomains };