UNPKG

@jbrowse/plugin-wiggle

Version:

JBrowse 2 wiggle adapters, tracks, etc.

228 lines (227 loc) 9.89 kB
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, }; }