// Pure, dependency-free chart helpers (SVG- and canvas-agnostic). Shared by every // Track-1 chart (compare panels + season standings evolution). No React, no DOM. export interface Scale { (v: number): number; invert: (px: number) => number; domain: [number, number]; range: [number, number]; } /** linear scale domain -> range, with .invert for hit-testing */ export function linScale(domain: [number, number], range: [number, number]): Scale { const [d0, d1] = domain; const [r0, r1] = range; const m = d1 === d0 ? 0 : (r1 - r0) / (d1 - d0); const f = ((v: number) => r0 + (v - d0) * m) as Scale; f.invert = (px: number) => (m === 0 ? d0 : d0 + (px - r0) / m); f.domain = domain; f.range = range; return f; } /** "nice" round tick values across [min,max] (1/2/5 * 10^n steps) */ export function niceTicks(min: number, max: number, count = 5): number[] { if (!isFinite(min) || !isFinite(max) || min === max) return [min]; const step0 = (max - min) / Math.max(1, count); const mag = Math.pow(10, Math.floor(Math.log10(step0))); const norm = step0 / mag; const step = (norm >= 5 ? 5 : norm >= 2 ? 2 : 1) * mag; const start = Math.ceil(min / step) * step; const out: number[] = []; for (let v = start; v <= max + step * 1e-6; v += step) out.push(Number(v.toPrecision(12))); return out; } /** lap time ms -> "1:18.305" (or "18.305" under a minute) */ export function fmtLapMs(ms: number): string { if (!isFinite(ms) || ms <= 0) return "—"; const m = Math.floor(ms / 60000); const s = (ms % 60000) / 1000; return m > 0 ? `${m}:${s.toFixed(3).padStart(6, "0")}` : s.toFixed(3); } /** signed gap seconds -> "+1.234" / "−0.500" (true minus glyph) */ export function fmtGap(s: number): string { if (!isFinite(s)) return "—"; const sign = s > 0 ? "+" : s < 0 ? "−" : ""; return `${sign}${Math.abs(s).toFixed(3)}`; } /** ms -> "M:SS" short clock for axis labels */ export function fmtClockShort(ms: number): string { const total = Math.floor(ms / 1000); const m = Math.floor(total / 60); return `${m}:${String(total % 60).padStart(2, "0")}`; } /** path string for a polyline from [x,y] screen points (skips null = gap break) */ export function linePath(points: ([number, number] | null)[]): string { let d = ""; let pen = false; for (const p of points) { if (!p) { pen = false; continue; } d += `${pen ? "L" : "M"}${p[0].toFixed(1)} ${p[1].toFixed(1)}`; pen = true; } return d; }