UNPKG

ucsc-xena-client

Version:

UCSC Xena Client. Functional genomics visualizations.

322 lines (279 loc) 10.1 kB
'use strict'; // Screen layout of exons for a single gene. 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 _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } var spLen = 3; // size of intronic region to draw between exons var _ = require('./underscore_ext'); // reverse layout if on negative strand. var reverseIf = function reverseIf(strand, arr) { return strand === '-' ? arr.slice(0).reverse() : arr; }; var min = function min(x, y) { return x < y ? x : y; }; var max = function max(x, y) { return x > y ? x : y; }; // Apply start and end padding. var applyPad = function applyPad(arr, _ref) { var start = _ref.start, end = _ref.end; return _.updateIn(arr, [0, 0], function (x) { return start < x ? start : x; }, [arr.length - 1, 1], function (x) { return end > x ? end : x; }); }; var applyClip = function applyClip(arr, _ref2) { var start = _ref2.start, end = _ref2.end; return arr.map(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), s = _ref4[0], e = _ref4[1]; return s > end || e < start ? null : [max(s, start), min(e, end)]; }).filter(function (x) { return x; }); }; function pad1(p, intervals, acc) { var _repeat = true; var _p, _intervals, _acc; while (_repeat) { _repeat = false; if (intervals.length === 1) { return acc.concat(intervals); } var _intervals2 = intervals, _intervals3 = _toArray(_intervals2), _intervals3$ = _slicedToArray(_intervals3[0], 2), ls = _intervals3$[0], le = _intervals3$[1], _intervals3$2 = _slicedToArray(_intervals3[1], 2), rs = _intervals3$2[0], re = _intervals3$2[1], rest = _intervals3.slice(2); _p = p; _intervals = [[rs - p, re]].concat(rest); _acc = acc.concat([[ls, le + p]]); p = _p; intervals = _intervals; acc = _acc; _repeat = true; continue; } } // Extend exon intervals, to show splice sites. // can drop this wrapper with babel, using param default acc=[], above. var pad = function pad(p, intervals) { return pad1(p, intervals, []); }; function toScreen(bpp, chrIntvls) { var lens = chrIntvls.map(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2), s = _ref6[0], e = _ref6[1]; return e - s + 1; }), starts = _.scan(lens, function (acc, x) { return acc + x; }, 0), pxStarts = starts.map(function (x) { return Math.round(x / bpp); }); return _.partitionN(pxStarts, 2, 1).slice(0, chrIntvls.length); } function baseLen(chrlo) { return _.reduce(chrlo, function (acc, _ref7) { var _ref8 = _slicedToArray(_ref7, 2), s = _ref8[0], e = _ref8[1]; return acc + e - s + 1; }, 0); } function pxLen(chrlo) { return _.reduce(chrlo, function (acc, _ref9) { var _ref10 = _slicedToArray(_ref9, 2), s = _ref10[0], e = _ref10[1]; return acc + e - s; }, 0); } // Layout exons on screen pixels. // layout(genepred :: {exonStarts : [<int>, ...], exonEnds: [<int>, ...], strand: <string>) // :: {chrom: [[<int>, <int>], ...], screen: [[<int>, <int>], ...], reversed: <boolean>} // If zoom.start or zoom.end are outside the gene, the first or last exon will be extended to cover // the zoom region. // layout chrom pos is closed coords. // layout screen pos is half open coords. function layout(_ref11, pxWidth, zoom) { var chrom = _ref11.chrom, exonStarts = _ref11.exonStarts, exonEnds = _ref11.exonEnds, strand = _ref11.strand; var addedSpliceIntvls = pad(spLen, _.zip(exonStarts, exonEnds)), paddedIntvals = applyPad(addedSpliceIntvls, zoom), clippedIntvals = applyClip(paddedIntvals, zoom), chrIntvls = reverseIf(strand, clippedIntvals), count = baseLen(chrIntvls), bpp = count / pxWidth, pixIntvls = toScreen(bpp, chrIntvls); return { chrom: chrIntvls, screen: pixIntvls, reversed: strand === '-', baseLen: count, pxLen: pxWidth, chromName: chrom, zoom: zoom }; } // layout chrom pos is closed coords. // layout screen pos is half open coords. function intronLayout(_ref12, pxWidth, zoom) { var chrom = _ref12.chrom, txStart = _ref12.txStart, txEnd = _ref12.txEnd, strand = _ref12.strand; var paddedIntvals = applyPad([[txStart, txEnd]], zoom), clippedIntvals = applyClip(paddedIntvals, zoom), chrIntvls = reverseIf(strand, clippedIntvals), count = baseLen(chrIntvls), bpp = count / pxWidth, pixIntvls = toScreen(bpp, chrIntvls); return { chrom: chrIntvls, screen: pixIntvls, reversed: strand === '-', baseLen: count, pxLen: pxWidth, chromName: chrom, zoom: zoom }; } function chromLayout(__, pxWidth, zoom, _ref13) { var chrom = _ref13.chrom, baseStart = _ref13.baseStart, baseEnd = _ref13.baseEnd; var intvals = [[baseStart, baseEnd]], clippedIntvals = applyClip(intvals, zoom), count = baseLen(clippedIntvals), bpp = count / pxWidth, pixIntvls = toScreen(bpp, clippedIntvals); return { chrom: clippedIntvals, screen: pixIntvls, reversed: false, baseLen: count, pxLen: pxWidth, chromName: chrom, zoom: zoom }; } // Finding chrom position from screen coords is more subtle that one might // hope. Perhaps there's an easier way to think about this. // // Consider a layout with 5 base pairs and three pixels: // // c | 1| 2| 3| 4| 5| // p | 0 | 1 | 2 | // // For UIs such as drag-zoom, we want to return the largest chrom range // that overlaps the pixel range. For example, if the user zooms pixel positions // [1, 2], we want to zoom to chrom coordinates [2, 5]. For the start position // we take the lowest overlapping coordinate. For the end position we take the // highest overlapping coordinate. If instead we were to project start and end // the same way, it becomes impossible to zoom on the edges of the layout. E.g. // if we take the lowest overlapping coordinate for both start and end, we // zoom to [2, 4], and it becomes impossible to ever zoom on chrom position 5. // // For start position 1 we project and floor(), to get coordinate 2, like // Math.floor(project(x)). For end position 2 it's more complex. It's not just // project and ceil(), as that would still give us 4, when the correct answer // is 5. We want the last coordinate that is strictly less than the pixel // *after* 5. I.e. we project pixel 3, which gives us 6, then take 5 as the // largest integer strictly less than 6. We can express this as // Math.ceil(project(x + 1) - 1). // // Alternatively, we can transform the 'end' case to the 'start' case by // flopping both the chrom and pixel coordinates. If we compute the coordinates // from the right edge by passing the pixels from the right edge, then // it still looks like Math.floor(project(x)). Doing it this way has // the advantage that it works nicely with our additional complication: // our intervals are sometimes reversed, for gene views of genes on the // reverse strand. To handle the reversed views, we flop either the // pixel coordinate or the chrom coordinate, depending on whether we // want the highest or lowest overlapping coordinate. // // The linear projection works as usual: we translate the input domain // to the origin, project to the new size using the slope, then translate to // the output range. We parameterize both translations so we can flop // the pixel domain, the chrom range, or both. var toPosition = function toPosition(layout) { return function (px0, chrN, x) { var chrom = layout.chrom, screen = layout.screen, i = _.findIndex(screen, function (_ref14) { var _ref15 = _slicedToArray(_ref14, 2), x0 = _ref15[0], x1 = _ref15[1]; return x0 <= x && x < x1; }); if (i !== -1) { var _screen$i = _slicedToArray(screen[i], 2), x0 = _screen$i[0], x1 = _screen$i[1], _chrom$i = _slicedToArray(chrom[i], 2), c0 = _chrom$i[0], c1 = _chrom$i[1]; return chrN(c0, c1, Math.floor(px0(x0, x1, x) * (c1 - c0 + 1) / (x1 - x0))); } return null; }; }; // pixel translated to origin var px0 = function px0(start, end, x) { return x - start; }; // pixel translated to origin, reversed domain var px0r = function px0r(start, end, x) { return end - 1 - x; }; // chrom translated from origin var chrN = function chrN(start, end, x) { return start + x; }; // chrom translated from origin, reversed range var chrNr = function chrNr(start, end, x) { return end - x; }; function chromRangeFromScreen(layout, start, end) { var toPos = toPosition(layout); return layout.reversed ? [toPos(px0r, chrN, end), toPos(px0, chrNr, start)] : [toPos(px0, chrN, start), toPos(px0r, chrNr, end)]; }; // This isn't precisely correct, but should be good enough. Gives us roughly // the coordinate in the middle of the pixel. var chromPositionFromScreen = function chromPositionFromScreen(layout, x) { return Math.round(_.meannull(chromRangeFromScreen(layout, x, x))); }; // closed coord len var chrlen = function chrlen(_ref16) { var _ref17 = _slicedToArray(_ref16, 2), s = _ref17[0], e = _ref17[1]; return e - s + 1; }; module.exports = { chromLayout: chromLayout, intronLayout: intronLayout, screenLayout: toScreen, baseLen: baseLen, pxLen: pxLen, layout: layout, pad: pad, zoomCount: function zoomCount(layout, start, end) { return _.sum(applyClip(layout.chrom, { start: start, end: end }).map(chrlen)); }, chromPositionFromScreen: chromPositionFromScreen, chromRangeFromScreen: chromRangeFromScreen };