@jbrowse/plugin-wiggle
Version:
JBrowse 2 wiggle adapters, tracks, etc.
228 lines (227 loc) • 9.89 kB
JavaScript
import { readConfObject } from '@jbrowse/core/configuration';
import { clamp } from '@jbrowse/core/util';
import { colord } from '@jbrowse/core/util/colord';
import { checkStopToken2, createStopTokenChecker, } from '@jbrowse/core/util/stopToken';
import { fillRectCtx, getOrigin, getScale } from "./util.js";
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, lastCheck = createStopTokenChecker(stopToken), } = props;
const region = regions[0];
const width = (region.end - region.start) / bpPerPx;
const regionStart = region.start;
const regionEnd = region.end;
const regionReversed = region.reversed;
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 isLog = scaleOpts.scaleType === 'log';
const log2 = Math.log(2);
const domainSpan = niceMax - niceMin;
const linearRatio = domainSpan !== 0 ? height / domainSpan : 0;
const logMin = isLog ? Math.log(niceMin) / log2 : 0;
const logMax = isLog ? Math.log(niceMax) / log2 : 0;
const logSpan = logMax - logMin;
const logRatio = logSpan !== 0 ? height / logSpan : 0;
const effectiveRange = inverted ? [height, 0] : [0, height];
const rangeFlipped = effectiveRange[0] === height;
const toY = isLog
? (n) => {
const logVal = Math.log(n) / log2;
const scaled = (logVal - logMin) * logRatio;
const result = rangeFlipped ? scaled : height - scaled;
return clamp(result, 0, height) + offset;
}
: (n) => {
const scaled = (n - niceMin) * linearRatio;
const result = rangeFlipped ? scaled : height - scaled;
return clamp(result, 0, height) + offset;
};
const toOrigin = (n) => toY(originY) - toY(n);
const getHeight = (n) => (filled ? toOrigin(n) : Math.max(minSize, 1));
const inverseBpPerPx = 1 / bpPerPx;
let hasClipping = false;
let prevLeftPx = Number.NEGATIVE_INFINITY;
const reducedFeatures = [];
const crossingOrigin = niceMin < pivotValue && niceMax > pivotValue;
if (summaryScoreMode === 'whiskers') {
let lastCol;
let lastMix;
for (const feature of features.values()) {
checkStopToken2(lastCheck);
const fStart = feature.get('start');
const fEnd = feature.get('end');
const leftPx = regionReversed
? (regionEnd - fEnd) * inverseBpPerPx
: (fStart - regionStart) * inverseBpPerPx;
const rightPx = regionReversed
? (regionEnd - fStart) * inverseBpPerPx
: (fEnd - regionStart) * inverseBpPerPx;
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;
for (const feature of features.values()) {
checkStopToken2(lastCheck);
const fStart = feature.get('start');
const fEnd = feature.get('end');
const leftPx = regionReversed
? (regionEnd - fEnd) * inverseBpPerPx
: (fStart - regionStart) * inverseBpPerPx;
const rightPx = regionReversed
? (regionEnd - fStart) * inverseBpPerPx
: (fEnd - regionStart) * inverseBpPerPx;
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;
for (const feature of features.values()) {
checkStopToken2(lastCheck);
const fStart = feature.get('start');
const fEnd = feature.get('end');
const leftPx = regionReversed
? (regionEnd - fEnd) * inverseBpPerPx
: (fStart - regionStart) * inverseBpPerPx;
const rightPx = regionReversed
? (regionEnd - fStart) * inverseBpPerPx
: (fEnd - regionStart) * inverseBpPerPx;
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 {
for (const feature of features.values()) {
checkStopToken2(lastCheck);
const fStart = feature.get('start');
const fEnd = feature.get('end');
const leftPx = regionReversed
? (regionEnd - fEnd) * inverseBpPerPx
: (fStart - regionStart) * inverseBpPerPx;
const rightPx = regionReversed
? (regionEnd - fStart) * inverseBpPerPx
: (fEnd - regionStart) * inverseBpPerPx;
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;
for (const feature of features.values()) {
checkStopToken2(lastCheck);
const fStart = feature.get('start');
const fEnd = feature.get('end');
const leftPx = regionReversed
? (regionEnd - fEnd) * inverseBpPerPx
: (fStart - regionStart) * inverseBpPerPx;
const rightPx = regionReversed
? (regionEnd - fStart) * inverseBpPerPx
: (fEnd - regionStart) * inverseBpPerPx;
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,
};
}