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