/** * Shared utilities for chart primitives. Pure functions — no React. */ /** Linear scale: maps `[d0, d1]` → `[r0, r1]`. */ export function linearScale( d0: number, d1: number, r0: number, r1: number, ): (v: number) => number { const dr = d1 - d0; if (dr === 0) return () => (r0 + r1) / 2; const mr = r1 - r0; return (v) => r0 + ((v - d0) / dr) * mr; } /** Min/max of a numeric array, ignoring NaN/non-finite. */ export function extent(values: readonly number[]): [number, number] { let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const v of values) { if (!Number.isFinite(v)) continue; if (v < min) min = v; if (v > max) max = v; } if (min === Number.POSITIVE_INFINITY) return [0, 1]; if (min === max) return [min - 1, max + 1]; return [min, max]; } /** Format a number with `Intl.NumberFormat`, falling back to plain `.toString()`. */ export function formatNumber( value: number, opts: Intl.NumberFormatOptions = {}, ): string { try { return new Intl.NumberFormat(undefined, opts).format(value); } catch { return String(value); } } /** Compact numeric formatter — `1234` → `1.2k`, `0.5` → `0.50`. */ export function compactNumber(v: number): string { if (!Number.isFinite(v)) return ''; if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(1)}k`; return v.toFixed(Math.abs(v) < 1 ? 2 : 0); } /** * Drop non-finite (NaN / Infinity) values from a numeric series, * preserving the original indices. Returns the `[index, value]` pairs * so callers can keep their X-axis spacing intact. */ export function filterFinite(values: readonly number[]): Array<[number, number]> { const out: Array<[number, number]> = []; for (let i = 0; i < values.length; i++) { const v = values[i]!; if (Number.isFinite(v)) out.push([i, v]); } return out; } /** Build a smooth catmull-rom path through `pts`. */ export function smoothPath(pts: ReadonlyArray<[number, number]>): string { if (pts.length === 0) return ''; if (pts.length === 1) { const [x, y] = pts[0]!; return `M ${x} ${y}`; } let d = `M ${pts[0]![0]} ${pts[0]![1]}`; for (let i = 1; i < pts.length; i++) { const [x0, y0] = pts[Math.max(0, i - 2)]!; const [x1, y1] = pts[i - 1]!; const [x2, y2] = pts[i]!; const [x3, y3] = pts[Math.min(pts.length - 1, i + 1)]!; const cp1x = x1 + (x2 - x0) / 6; const cp1y = y1 + (y2 - y0) / 6; const cp2x = x2 - (x3 - x1) / 6; const cp2y = y2 - (y3 - y1) / 6; d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`; } return d; }