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