/** Small helpers shared by the canvas plots. */ /** Round tick positions covering [min, max] with about `count` steps. */ export function niceTicks(min: number, max: number, count = 5): number[] { if (!(max > min)) return [min] const raw = (max - min) / count const mag = 10 ** Math.floor(Math.log10(raw)) const norm = raw / mag const step = (norm < 1.5 ? 1 : norm < 3.5 ? 2 : norm < 7.5 ? 5 : 10) * mag const out: number[] = [] for (let v = Math.ceil(min / step) * step; v <= max + step * 1e-9; v += step) out.push(Number(v.toFixed(10))) return out } export const formatTick = (v: number): string => { if (v === 0) return '0' const a = Math.abs(v) if (a >= 1000) return `${Number((v / 1000).toPrecision(3))}k` if (a >= 10) return String(Number(v.toPrecision(3))) return String(Number(v.toPrecision(2))) } export const formatSeconds = (s: number): string => (s >= 1 ? `${Number(s.toPrecision(3))} s` : `${Number((s * 1000).toPrecision(3))} ms`) /** A magma-like colormap: t in [0, 1] to rgb. */ const STOPS: [number, number, number][] = [ [0, 0, 4], [28, 16, 68], [79, 18, 123], [129, 37, 129], [181, 54, 122], [229, 80, 100], [251, 135, 97], [254, 194, 135], [252, 253, 191], ] export function magma(t: number): [number, number, number] { const u = Math.min(1, Math.max(0, t)) * (STOPS.length - 1) const i = Math.min(STOPS.length - 2, Math.floor(u)) const a = u - i const c0 = STOPS[i] const c1 = STOPS[i + 1] return [c0[0] + a * (c1[0] - c0[0]), c0[1] + a * (c1[1] - c0[1]), c0[2] + a * (c1[2] - c0[2])] } export const FONT = '11px system-ui, sans-serif' export const AXIS = '#666' export const GRID = '#e6e8eb' export const FRAME = '#cfd3d8'