/** 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' function hslToRgb(h: number, s: number, l: number): [number, number, number] { const c = (1 - Math.abs(2 * l - 1)) * s const hp = (((h % 360) + 360) % 360) / 60 const x = c * (1 - Math.abs((hp % 2) - 1)) let r = 0 let g = 0 let b = 0 if (hp < 1) [r, g, b] = [c, x, 0] else if (hp < 2) [r, g, b] = [x, c, 0] else if (hp < 3) [r, g, b] = [0, c, x] else if (hp < 4) [r, g, b] = [0, x, c] else if (hp < 5) [r, g, b] = [x, 0, c] else [r, g, b] = [c, 0, x] const m = l - c / 2 return [255 * (r + m), 255 * (g + m), 255 * (b + m)] } /** Cyclic colormap for an angle in radians: hue goes once around the circle. */ export function phaseColor(phi: number): [number, number, number] { const h = ((phi + Math.PI) / (2 * Math.PI)) * 360 return hslToRgb(h, 0.6, 0.55) } /** Spectrogram colormap: dB value (<= 0) to rgb, black at -range. */ export const dbColor = (range: number) => (v: number): [number, number, number] => magma(1 + v / range)