UNPKG

ucsc-xena-client

Version:

UCSC Xena Client. Functional genomics visualizations.

327 lines (273 loc) 9.82 kB
'use strict'; 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 _marked = /*#__PURE__*/regeneratorRuntime.mark(segmentRegions); 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 colorScales = require('./colorScales'); var labelFont = 12; var labelMargin = 1; // left & right margin var radius = 4; var minVariantHeight = function minVariantHeight(pixPerRow) { return Math.max(pixPerRow, 2); }; // minimum draw height of 2 var toYPx = function toYPx(zoom, v) { var height = zoom.height, count = zoom.count, index = zoom.index, svHeight = height / count; return { svHeight: svHeight, y: (v.y - index) * svHeight + svHeight / 2 }; }; function push(arr, v) { arr.push(v); return arr; } // A recursive implementation might be clearer. var backgroundStripes = function backgroundStripes(hasValue) { return _.reduce(_.groupByConsec(hasValue, _.identity), function (_ref, g) { var _ref2 = _slicedToArray(_ref, 2), acc = _ref2[0], sum = _ref2[1]; return [g[0] ? acc : push(acc, [sum, g.length]), sum + g.length]; }, [[], 0])[0]; }; function drawBackground(vg, width, height) { vg.smoothing(false); vg.box(0, 0, width, height, 'grey'); // grey background } function labelNulls(vg, width, height, count, stripes) { var pixPerRow = height / count, nullLabels = stripes.filter(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), len = _ref4[1]; return len * pixPerRow > labelFont; }); nullLabels.forEach(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2), offset = _ref6[0], len = _ref6[1]; vg.textCenteredPushRight(0, pixPerRow * offset, width, pixPerRow * len, 'black', labelFont, "null"); }); } function labelValues(vg, width, _ref7, toDraw) { var index = _ref7.index, height = _ref7.height, count = _ref7.count; var rheight = height / count; if (rheight > labelFont) { var h = rheight; toDraw.forEach(function (v) { var xStart = v.xStart, xEnd = v.xEnd, value = v.value, y = (v.y - index) * rheight + rheight / 2, label = '' + value, textWidth = vg.textWidth(labelFont, label); if (xEnd - xStart >= textWidth) { vg.textCenteredPushRight(xStart + labelMargin, y - h / 2, xEnd - xStart - labelMargin, h, 'black', labelFont, label); } }); } } // Computes contiguous vertial pixel regions. // There must be a better way to compute this. function findRegions(index, height, count) { var starts = _.uniq(_.times(count, function (y) { return ~~(y * height / count); })), regions = _.partitionN(starts, 2, 1, [height]), lens = regions.map(function (_ref8) { var _ref9 = _slicedToArray(_ref8, 2), s = _ref9[0], e = _ref9[1]; return e - s; }); return _.object(starts, lens); } var byEnd = function byEnd(x, y) { return x.xEnd - y.xEnd; }; var byStart = function byStart(x, y) { return x.xStart - y.xStart; }; var noDataScale = function noDataScale() { return "gray"; }; noDataScale.domain = function () { return []; }; // We compute trend by projecting onto the upper right quadrant of // the unit circle. For each segment we compute the difference // from the midpoint (determined by min, max settings). We sum // positive differences (amplification) and use as the x coordinate. // We sum negative differences (deletions) and use as the ycoordinate. // Then we use atan2 to find the angle, and normalize by PI/2 to // give a range [0, 1]. // // We compute power as root-mean-square from the midpoint. function trendPowerNullIter(iter, zero) { var count = 0, highs = 0, lows = 0, sqsum = 0, v, diff, n; if (!iter) { return null; } n = iter.next(); while (!n.done) { if (n.value.value != null) { v = n.value.value; if (v < zero) { lows += zero - v; } else { highs += v - zero; } count += 1; diff = v - zero; sqsum += diff * diff; } n = iter.next(); } if (count > 0) { return [2 * Math.atan2(highs, lows) / Math.PI, Math.sqrt(sqsum / count)]; } return [null, null]; } // Render segments using trend and power to select color and saturation, // respectively. This avoids the problem of averaging, which draws nearby // amplification and deletion as white, since they average to zero. function segmentRegions(colorSpec, index, count, width, height, zoom, nodes) { var _colorScales$colorSca, lookup, _colorSpec, zero, regions, toPxRow, byRow, is, i, rowI, ends, starts, len, scope, pxStart, pxEnd, nextStartNode, nextEndNode, j, k, mp, lastRow, color; return regeneratorRuntime.wrap(function segmentRegions$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _colorScales$colorSca = colorScales.colorScale(colorSpec), lookup = _colorScales$colorSca.lookup, _colorSpec = _slicedToArray(colorSpec, 5), zero = _colorSpec[4], regions = findRegions(index, height, count), toPxRow = function toPxRow(v) { return ~~((v.y - index) * height / count); }, byRow = _.groupBy(nodes, toPxRow); _context.t0 = regeneratorRuntime.keys(byRow); case 2: if ((_context.t1 = _context.t0()).done) { _context.next = 25; break; } is = _context.t1.value; i = parseInt(is, 10); // Ugh. rowI = byRow[i]; if (rowI) { _context.next = 8; break; } return _context.abrupt('continue', 2); case 8: // Using sort vs. _.sortBy because it's faster. ends = rowI.slice(0).sort(byEnd), starts = rowI.slice(0).sort(byStart), len = rowI.length, scope = new Set(), pxStart = starts[0].xStart, pxEnd = void 0, nextStartNode = void 0, nextEndNode = void 0, j = 0, k = 0; case 9: if (!(j < len || k < len)) { _context.next = 23; break; } while (j < len && starts[j].xStart === pxStart) { scope.add(starts[j++]); } while (k < len && ends[k].xEnd === pxStart) { scope.delete(ends[k++]); } if (!(k >= len)) { _context.next = 14; break; } return _context.abrupt('continue', 9); case 14: nextStartNode = j < len && starts[j]; nextEndNode = ends[k]; if (j < len && nextStartNode.xStart < nextEndNode.xEnd) { pxEnd = nextStartNode.xStart; } else { pxEnd = nextEndNode.xEnd; } // generators with regenerator seem to be slow, perhaps due to try/catch. // So, _.meannull generator version, and _.i methods are limiting. // let avg = meannullIter(_.i.map(scope.values(), v => v.value)), mp = trendPowerNullIter(scope.values(), zero), lastRow = i + regions[i], color = lookup.apply(undefined, _toConsumableArray(mp)); _context.next = 20; return { pxStart: pxStart, pxEnd: pxEnd, color: color, lastRow: lastRow, i: i }; case 20: pxStart = pxEnd; _context.next = 9; break; case 23: _context.next = 2; break; case 25: case 'end': return _context.stop(); } } }, _marked, this); } function drawImgSegmentsPixel(vg, colorSpec, index, count, width, height, zoom, nodes) { var ctx = vg.context(), img = ctx.createImageData(width, height), // XXX cache & reuse? regions = segmentRegions(colorSpec, index, count, width, height, zoom, nodes); for (var r = regions.next(); !r.done; r = regions.next()) { var _r$value = r.value, pxStart = _r$value.pxStart, pxEnd = _r$value.pxEnd, color = _r$value.color, lastRow = _r$value.lastRow, i = _r$value.i, l = void 0; for (var _r = i; _r < lastRow; ++_r) { var pxRow = _r * width, buffStart = (pxRow + pxStart) * 4, buffEnd = (pxRow + pxEnd) * 4; for (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? } } } ctx.putImageData(img, 0, 0); } var drawSegmentedByMethod = function drawSegmentedByMethod(drawSegments) { return function (vg, props) { var width = props.width, zoom = props.zoom, _props$nodes = props.nodes, nodes = _props$nodes === undefined ? [] : _props$nodes, color = props.color, count = zoom.count, height = zoom.height, index = zoom.index; drawBackground(vg, width, height); var samples = props.samples, samplesInDS = props.index.bySample, last = index + count, toDraw = nodes.filter(function (v) { return v.y >= index && v.y < last; }), hasValue = samples.slice(index, index + count).map(function (s) { return samplesInDS[s]; }), stripes = backgroundStripes(hasValue); drawSegments(vg, color, index, count, width, height, zoom, toDraw); vg.labels(function () { labelNulls(vg, width, height, count, stripes); labelValues(vg, width, zoom, toDraw); }); }; }; module.exports = { findRegions: findRegions, drawSegmented: drawSegmentedByMethod(drawImgSegmentsPixel), radius: radius, minVariantHeight: minVariantHeight, toYPx: toYPx, labelFont: labelFont };