// Isomorphic numeric helpers used across data-viz. export const clamp = (v: number, lo: number, hi: number): number => v < lo ? lo : v > hi ? hi : v; export const lerp = (a: number, b: number, t: number): number => a + (b - a) * t; /** Inverse lerp: where does v sit in [a,b] as 0..1 (clamped). */ export const invLerp = (a: number, b: number, v: number): number => b === a ? 0 : clamp((v - a) / (b - a), 0, 1); /** Remap v from [inMin,inMax] to [outMin,outMax]. */ export const mapRange = ( v: number, inMin: number, inMax: number, outMin: number, outMax: number, ): number => lerp(outMin, outMax, invLerp(inMin, inMax, v)); /** Round to n decimal places. */ export const round = (v: number, n = 0): number => { const p = 10 ** n; return Math.round(v * p) / p; }; /** Perceptually softer compression for delta bars (matches the broadcast look). */ export const sqrtScale = (v: number): number => Math.sign(v) * Math.sqrt(Math.abs(v)); /** Catmull-Rom → cubic-bezier path data through points (smooth area/line charts). */ export function smoothPath(pts: { x: number; y: number }[]): string { if (pts.length < 2) return ""; let d = ""; for (let i = 0; i < pts.length - 1; i++) { const p0 = pts[i - 1] || pts[i]; const p1 = pts[i]; const p2 = pts[i + 1]; const p3 = pts[i + 2] || p2; const c1x = p1.x + (p2.x - p0.x) / 6; const c1y = p1.y + (p2.y - p0.y) / 6; const c2x = p2.x - (p3.x - p1.x) / 6; const c2y = p2.y - (p3.y - p1.y) / 6; d += `C ${c1x.toFixed(2)} ${c1y.toFixed(2)} ${c2x.toFixed(2)} ${c2y.toFixed(2)} ${p2.x.toFixed(2)} ${p2.y.toFixed(2)} `; } return d; }