export type ChartSample = { x: number; y: number; dataIndex: number; breakBefore: boolean }; type Bounds = { xMin: number; xMax: number; yMin: number; yMax: number; sorted: boolean }; const seriesCache = new WeakMap>(); const boundsCache = new WeakMap, Bounds>(); const cursorCache = new WeakMap(); const selectionCache = new WeakMap>(); /** Cached only by immutable source arrays; collected together with their result snapshot. */ export function preparedChartSamples(time: number[], values: number[]) { let byTime = seriesCache.get(values); if (!byTime) { byTime = new WeakMap(); seriesCache.set(values, byTime); } let samples = byTime.get(time); if (samples) return samples; samples = []; let breakBefore = true; for (let i = 0; i < time.length; i++) { const x = Number(time[i]), y = Number(values[i]); if (!Number.isFinite(x)) { breakBefore = true; continue; } // Preserve the existing isolated-force-event interpolation policy. if (!Number.isFinite(y)) continue; samples.push({ x, y, dataIndex: i, breakBefore }); breakBefore = false; } byTime.set(time, samples); return samples; } export function chartCursorSamples(time: number[]) { let samples = cursorCache.get(time); if (!samples) { samples = []; for (let i = 0; i < time.length; i++) { if (Number.isFinite(time[i])) samples.push({ x: time[i], y: i, dataIndex: i, breakBefore: false }); } cursorCache.set(time, samples); } return samples; } export function chartSampleBounds(samples: Array<{ x: number; y: number }>): Bounds { const cached = boundsCache.get(samples); if (cached) return cached; const bounds = { xMin: Infinity, xMax: -Infinity, yMin: Infinity, yMax: -Infinity, sorted: true }; let previousX = -Infinity; for (const sample of samples) { bounds.xMin = Math.min(bounds.xMin, sample.x); bounds.xMax = Math.max(bounds.xMax, sample.x); bounds.yMin = Math.min(bounds.yMin, sample.y); bounds.yMax = Math.max(bounds.yMax, sample.y); if (sample.x < previousX) bounds.sorted = false; previousX = sample.x; } boundsCache.set(samples, bounds); return bounds; } export function combinedChartBounds(curves: Array<{ samples: Array<{ x: number; y: number }> }>) { const result = { xMin: Infinity, xMax: -Infinity, yMin: Infinity, yMax: -Infinity }; for (const curve of curves) { const bounds = chartSampleBounds(curve.samples); result.xMin = Math.min(result.xMin, bounds.xMin); result.xMax = Math.max(result.xMax, bounds.xMax); result.yMin = Math.min(result.yMin, bounds.yMin); result.yMax = Math.max(result.yMax, bounds.yMax); } return result; } function lowerBound(samples: Array<{ x: number }>, x: number, upper = false) { let low = 0, high = samples.length; while (low < high) { const middle = (low + high) >>> 1; if (samples[middle].x < x || (upper && samples[middle].x === x)) low = middle + 1; else high = middle; } return low; } export function visibleSampleRange(samples: Array<{ x: number; y: number }>, xMin: number, xMax: number) { if (!chartSampleBounds(samples).sorted) return [0, samples.length] as const; // Include both outside neighbours, preserving segments crossing viewport boundaries. return [Math.max(0, lowerBound(samples, xMin) - 1), Math.min(samples.length, lowerBound(samples, xMax, true) + 1)] as const; } /** Keep first/min/max/last per pixel column in original order; no averaging of peaks. */ export function chartDisplaySamples(samples: ChartSample[], xMin: number, xMax: number, pixelWidth: number) { const width = Math.max(1, Math.ceil(pixelWidth)); const key = `${xMin}:${xMax}:${width}`; let cache = selectionCache.get(samples); if (!cache) { cache = new Map(); selectionCache.set(samples, cache); } const cached = cache.get(key); if (cached) return cached; const [start, end] = visibleSampleRange(samples, xMin, xMax); const result: ChartSample[] = []; if (end - start <= width * 4 || !chartSampleBounds(samples).sorted) { for (let i = start; i < end; i++) result.push(samples[i]); } else { let bucket = -Infinity; let first = -1, last = -1, minimum = -1, maximum = -1; const flush = () => { if (first < 0) return; const indices = [first, minimum, maximum, last].sort((a, b) => a - b); let previous = -1; for (const index of indices) { if (index !== previous) result.push(samples[index]); previous = index; } }; for (let i = start; i < end; i++) { const sample = samples[i]; const nextBucket = Math.max(-1, Math.min(width, Math.floor((sample.x - xMin) / Math.max(xMax - xMin, 1e-12) * width))); if (nextBucket !== bucket || sample.breakBefore || first < 0) { flush(); first = last = minimum = maximum = i; bucket = nextBucket; } else { last = i; if (sample.y < samples[minimum].y) minimum = i; if (sample.y > samples[maximum].y) maximum = i; } } flush(); } // A few active windows/viewports may share a curve without retaining unbounded history. if (cache.size >= 4) cache.delete(cache.keys().next().value!); cache.set(key, result); return result; } export function sampledChartPath( time: number[], values: number[], xPosition: (value: number) => number, yPosition: (value: number) => number, xMin: number, xMax: number, pixelWidth: number, ) { const samples = chartDisplaySamples(preparedChartSamples(time, values), xMin, xMax, pixelWidth); return samples.map((sample, index) => `${index === 0 || sample.breakBefore ? "M" : "L"} ${xPosition(sample.x).toFixed(2)} ${yPosition(sample.y).toFixed(2)}`, ).join(" "); }