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