@jbrowse/plugin-wiggle
Version:
JBrowse 2 wiggle adapters, tracks, etc.
189 lines (188 loc) • 7.88 kB
JavaScript
import { readConfObject } from '@jbrowse/core/configuration';
import { clamp, featureSpanPx } from '@jbrowse/core/util';
import { colord } from '@jbrowse/core/util/colord';
import { checkStopToken } from '@jbrowse/core/util/stopToken';
import { fillRectCtx, getOrigin, getScale } from './util';
function lighten(color, amount) {
const hslColor = color.toHsl();
const l = hslColor.l * (1 + amount);
return colord({
...hslColor,
l: clamp(l, 0, 100),
});
}
function darken(color, amount) {
const hslColor = color.toHsl();
const l = hslColor.l * (1 - amount);
return colord({
...hslColor,
l: clamp(l, 0, 100),
});
}
const fudgeFactor = 0.3;
const clipHeight = 2;
export function drawXY(ctx, props) {
const { features, bpPerPx, regions, scaleOpts, height: unadjustedHeight, config, ticks, displayCrossHatches, offset = 0, colorCallback, inverted, stopToken, } = props;
const region = regions[0];
const width = (region.end - region.start) / bpPerPx;
const height = unadjustedHeight - offset * 2;
const filled = readConfObject(config, 'filled');
const clipColor = readConfObject(config, 'clipColor');
const summaryScoreMode = readConfObject(config, 'summaryScoreMode');
const pivotValue = readConfObject(config, 'bicolorPivotValue');
const minSize = readConfObject(config, 'minSize');
const scale = getScale({ ...scaleOpts, range: [0, height], inverted });
const originY = getOrigin(scaleOpts.scaleType);
const domain = scale.domain();
const niceMin = domain[0];
const niceMax = domain[1];
const toY = (n) => clamp(height - (scale(n) || 0), 0, height) + offset;
const toOrigin = (n) => toY(originY) - toY(n);
const getHeight = (n) => (filled ? toOrigin(n) : Math.max(minSize, 1));
let hasClipping = false;
let prevLeftPx = Number.NEGATIVE_INFINITY;
const reducedFeatures = [];
const crossingOrigin = niceMin < pivotValue && niceMax > pivotValue;
let start = performance.now();
if (summaryScoreMode === 'whiskers') {
let lastCol;
let lastMix;
start = performance.now();
for (const feature of features.values()) {
if (performance.now() - start > 400) {
checkStopToken(stopToken);
start = performance.now();
}
const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx);
if (feature.get('summary')) {
const w = Math.max(rightPx - leftPx + fudgeFactor, minSize);
const max = feature.get('maxScore');
const c = colorCallback(feature, max);
const effectiveC = crossingOrigin
? c
: c === lastCol
? lastMix
: (lastMix = lighten(colord(c), 0.4).toHex());
fillRectCtx(leftPx, toY(max), w, getHeight(max), ctx, effectiveC);
lastCol = c;
}
}
lastMix = undefined;
lastCol = undefined;
start = performance.now();
for (const feature of features.values()) {
if (performance.now() - start > 400) {
checkStopToken(stopToken);
start = performance.now();
}
const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx);
const score = feature.get('score');
const max = feature.get('maxScore');
const min = feature.get('minScore');
const summary = feature.get('summary');
const c = colorCallback(feature, score);
const effectiveC = crossingOrigin && summary
? c === lastCol
? lastMix
: (lastMix = colord(colorCallback(feature, max))
.mix(colord(colorCallback(feature, min)))
.toString())
: c;
const w = Math.max(rightPx - leftPx + fudgeFactor, minSize);
if (Math.floor(leftPx) !== Math.floor(prevLeftPx) ||
rightPx - leftPx > 1) {
reducedFeatures.push(feature);
prevLeftPx = leftPx;
}
hasClipping = hasClipping || score < niceMin || score > niceMax;
fillRectCtx(leftPx, toY(score), w, getHeight(score), ctx, effectiveC);
lastCol = c;
}
lastMix = undefined;
lastCol = undefined;
start = performance.now();
for (const feature of features.values()) {
if (performance.now() - start > 400) {
checkStopToken(stopToken);
start = performance.now();
}
const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx);
if (feature.get('summary')) {
const min = feature.get('minScore');
const c = colorCallback(feature, min);
const w = Math.max(rightPx - leftPx + fudgeFactor, minSize);
const effectiveC = crossingOrigin
? c
: c === lastCol
? lastMix
: (lastMix = darken(colord(c), 0.4).toHex());
fillRectCtx(leftPx, toY(min), w, getHeight(min), ctx, effectiveC);
lastCol = c;
}
}
}
else {
start = performance.now();
for (const feature of features.values()) {
if (performance.now() - start > 400) {
checkStopToken(stopToken);
start = performance.now();
}
const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx);
if (Math.floor(leftPx) !== Math.floor(prevLeftPx) ||
rightPx - leftPx > 1) {
reducedFeatures.push(feature);
prevLeftPx = leftPx;
}
const score = feature.get('score');
const c = colorCallback(feature, score);
hasClipping = hasClipping || score < niceMin || score > niceMax;
const w = Math.max(rightPx - leftPx + fudgeFactor, minSize);
if (summaryScoreMode === 'max') {
const s = feature.get('summary') ? feature.get('maxScore') : score;
fillRectCtx(leftPx, toY(s), w, getHeight(s), ctx, c);
}
else if (summaryScoreMode === 'min') {
const s = feature.get('summary') ? feature.get('minScore') : score;
fillRectCtx(leftPx, toY(s), w, getHeight(s), ctx, c);
}
else {
fillRectCtx(leftPx, toY(score), w, getHeight(score), ctx, c);
}
}
}
ctx.save();
if (hasClipping) {
ctx.fillStyle = clipColor;
start = performance.now();
for (const feature of features.values()) {
if (performance.now() - start > 400) {
checkStopToken(stopToken);
start = performance.now();
}
const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx);
const w = rightPx - leftPx + fudgeFactor;
const score = feature.get('score');
if (score > niceMax) {
fillRectCtx(leftPx, offset, w, clipHeight, ctx);
}
else if (score < niceMin && scaleOpts.scaleType !== 'log') {
fillRectCtx(leftPx, unadjustedHeight, w, clipHeight, ctx);
}
}
}
ctx.restore();
if (displayCrossHatches) {
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(200,200,200,0.5)';
for (const tick of ticks.values) {
ctx.beginPath();
ctx.moveTo(0, Math.round(toY(tick)));
ctx.lineTo(width, Math.round(toY(tick)));
ctx.stroke();
}
}
return {
reducedFeatures,
};
}