"use client"; import { useState, useRef } from "react"; import { cn } from "@/lib/utils"; export type TimeSeriesPoint = { date: string; value: number }; /** "day" expects "YYYY-MM-DD", "month" expects "YYYY-MM" — kept as a plain * string enum rather than a formatter function prop, since this chart is * a "use client" component and functions passed from a server component * parent aren't serializable across that boundary (React throws: "Functions * cannot be passed directly to Client Components"). */ function formatPoint(date: string, kind: "day" | "month"): string { if (kind === "month") { const [y, m] = date.split("-"); return new Date(Number(y), Number(m) - 1, 1).toLocaleDateString(undefined, { month: "short", year: "2-digit" }); } return new Date(`${date}T00:00:00Z`).toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone: "UTC" }); } /** Single-series area/line chart with a hover crosshair+tooltip and a * table-view fallback — hand-rolled SVG, no charting lib. */ export function TimeSeriesChart({ data, formatValue = (n) => String(n), dateFormat = "day", height = 200, }: { data: TimeSeriesPoint[]; formatValue?: (n: number) => string; dateFormat?: "day" | "month"; height?: number; }) { const [hovered, setHovered] = useState(null); const [showTable, setShowTable] = useState(false); const svgRef = useRef(null); const formatDate = (d: string) => formatPoint(d, dateFormat); const width = 600; const paddingBottom = 24; const paddingTop = 12; const plotHeight = height - paddingBottom - paddingTop; const max = Math.max(1, ...data.map((d) => d.value)); const n = data.length; if (n === 0) { return

No data yet.

; } const xFor = (i: number) => (n === 1 ? width / 2 : (i / (n - 1)) * width); const yFor = (v: number) => height - paddingBottom - (v / max) * plotHeight; const linePath = data.map((d, i) => `${i === 0 ? "M" : "L"} ${xFor(i)} ${yFor(d.value)}`).join(" "); const areaPath = `${linePath} L ${xFor(n - 1)} ${height - paddingBottom} L ${xFor(0)} ${height - paddingBottom} Z`; function handleMove(e: React.MouseEvent) { const svg = svgRef.current; if (!svg) return; const rect = svg.getBoundingClientRect(); const relX = (e.clientX - rect.left) / rect.width; const i = Math.round(relX * (n - 1)); setHovered(Math.max(0, Math.min(n - 1, i))); } const labelStep = Math.max(1, Math.ceil(n / 6)); return (
{showTable ? (
{data.map((d) => ( ))}
Date Value
{formatDate(d.date)} {formatValue(d.value)}
) : (
setHovered(null)} > {data.map((d, i) => ( i % labelStep === 0 && ( {formatDate(d.date)} ) ))} {hovered !== null && ( <> )} {hovered !== null && (

{formatDate(data[hovered]!.date)}

{formatValue(data[hovered]!.value)}

)}
)}
); }