'use client'; import { useMemo, useRef, useState } from 'react'; import { cn } from '@/lib/cn'; import { fmtCompact } from '@/lib/format'; import { LineChart, type LineChartProps, lineLayout } from './charts'; /** * Progressive enhancement of `LineChart`: the same SSR SVG, plus a crosshair and a nearest-point tooltip on hover / touch. * Pointer position is mapped from CSS pixels to viewBox units, so it works at any rendered width. */ export function InteractiveLineChart({ xFormat, ...props }: LineChartProps & { xFormat?: (x: number) => string }) { const L = useMemo(() => lineLayout(props), [props]); const wrap = useRef(null); const [hover, setHover] = useState<{ x: number; idx: number[] } | null>(null); const yFormat = props.yFormat ?? fmtCompact; if (!L) return ; const fmtXFull = xFormat ?? ((v: number) => (L.xTime ? new Date(v).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }) : String(v))); const onMove = (clientX: number) => { const el = wrap.current; if (!el) return; const rect = el.getBoundingClientRect(); const vx = ((clientX - rect.left) / rect.width) * L.w; if (vx < L.pad.l || vx > L.w - L.pad.r) { setHover(null); return; } const tx = L.xInvert(vx); // nearest point index per series const idx = L.series.map((s) => { let best = -1; let bd = Infinity; s.pts.forEach((p, i) => { const d = Math.abs(p.x - tx); if (d < bd) { bd = d; best = i; } }); return best; }); const anchor = L.series.map((s, i) => (idx[i] as number) >= 0 ? (s.pts[idx[i] as number] as { x: number }).x : NaN).filter(Number.isFinite); if (!anchor.length) return; // snap the crosshair to the point nearest the pointer among all series const snap = anchor.reduce((a, b) => (Math.abs(b - tx) < Math.abs(a - tx) ? b : a)); setHover({ x: snap, idx }); }; const rows = hover ? L.series .map((s, i) => { const p = s.pts[hover.idx[i] as number]; return p ? { name: s.name, color: s.color, p } : null; }) .filter((r): r is { name: string; color: string; p: { x: number; y: number } } => !!r) : []; const hx = hover ? L.x(hover.x) : 0; const leftPct = (hx / L.w) * 100; const flip = leftPct > 60; return (
onMove(e.clientX)} onPointerDown={(e) => onMove(e.clientX)} onPointerLeave={() => setHover(null)} onTouchMove={(e) => e.touches[0] && onMove(e.touches[0].clientX)} onTouchEnd={() => setTimeout(() => setHover(null), 1500)} data-interactive-chart > {hover && ( {rows.map((r) => ( ))} )} {hover && rows.length > 0 && (

{fmtXFull(hover.x)}

{rows.map((r) => (

{r.name} {yFormat(r.p.y)}

))}
)}
); }