1/** Small helpers shared by the canvas plots. */
3/** Round tick positions covering [min, max] with about `count` steps. */
4export function niceTicks(min: number, max: number, count = 5): number[] {
5 if (!(max > min)) return [min]
6 const raw = (max - min) / count
7 const mag = 10 ** Math.floor(Math.log10(raw))
8 const norm = raw / mag
9 const step = (norm < 1.5 ? 1 : norm < 3.5 ? 2 : norm < 7.5 ? 5 : 10) * mag
10 const out: number[] = []
11 for (let v = Math.ceil(min / step) * step; v <= max + step * 1e-9; v += step) out.push(Number(v.toFixed(10)))
12 return out
13}
15export const formatTick = (v: number): string => {
16 if (v === 0) return '0'
17 const a = Math.abs(v)
18 if (a >= 1000) return `${Number((v / 1000).toPrecision(3))}k`
19 if (a >= 10) return String(Number(v.toPrecision(3)))
20 return String(Number(v.toPrecision(2)))
21}
23export const formatSeconds = (s: number): string => (s >= 1 ? `${Number(s.toPrecision(3))} s` : `${Number((s * 1000).toPrecision(3))} ms`)
25/** A magma-like colormap: t in [0, 1] to rgb. */
26const STOPS: [number, number, number][] = [
27 [0, 0, 4],
28 [28, 16, 68],
29 [79, 18, 123],
30 [129, 37, 129],
31 [181, 54, 122],
32 [229, 80, 100],
33 [251, 135, 97],
34 [254, 194, 135],
35 [252, 253, 191],
36]
38export function magma(t: number): [number, number, number] {
39 const u = Math.min(1, Math.max(0, t)) * (STOPS.length - 1)
40 const i = Math.min(STOPS.length - 2, Math.floor(u))
41 const a = u - i
42 const c0 = STOPS[i]
43 const c1 = STOPS[i + 1]
44 return [c0[0] + a * (c1[0] - c0[0]), c0[1] + a * (c1[1] - c0[1]), c0[2] + a * (c1[2] - c0[2])]
45}
47export const FONT = '11px system-ui, sans-serif'
48export const AXIS = '#666'
49export const GRID = '#e6e8eb'
50export const FRAME = '#cfd3d8'
52function hslToRgb(h: number, s: number, l: number): [number, number, number] {
53 const c = (1 - Math.abs(2 * l - 1)) * s
54 const hp = (((h % 360) + 360) % 360) / 60
55 const x = c * (1 - Math.abs((hp % 2) - 1))
56 let r = 0
57 let g = 0
58 let b = 0
59 if (hp < 1) [r, g, b] = [c, x, 0]
60 else if (hp < 2) [r, g, b] = [x, c, 0]
61 else if (hp < 3) [r, g, b] = [0, c, x]
62 else if (hp < 4) [r, g, b] = [0, x, c]
63 else if (hp < 5) [r, g, b] = [x, 0, c]
64 else [r, g, b] = [c, 0, x]
65 const m = l - c / 2
66 return [255 * (r + m), 255 * (g + m), 255 * (b + m)]
67}
69/** Cyclic colormap for an angle in radians: hue goes once around the circle. */
70export function phaseColor(phi: number): [number, number, number] {
71 const h = ((phi + Math.PI) / (2 * Math.PI)) * 360
72 return hslToRgb(h, 0.6, 0.55)
73}
75/** Spectrogram colormap: dB value (<= 0) to rgb, black at -range. */
76export const dbColor =
77 (range: number) =>
78 (v: number): [number, number, number] =>
79 magma(1 + v / range)