"use client"; import React from "react"; import { BRAND_600 } from "@/lib/theme"; export type TrendDatum = { label: string; value: number }; // Dependency-free single-series area/line chart, following the same // hand-rolled-SVG approach as reports/charts/HorizontalBarChart.tsx. A // single continuous series over time calls for a line/area (dataviz skill // form heuristic), so this uses one brand-consistent color rather than the // bar chart's multi-category palette. const WIDTH = 600; const HEIGHT = 220; const PADDING_LEFT = 48; // room for y-axis value labels const PADDING_RIGHT = 8; const PADDING_TOP = 12; const PADDING_BOTTOM = 24; const GRIDLINES = 4; const defaultAxisFormatter = (v: number) => new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(v); export function AreaTrendChart({ data, valueFormatter, axisFormatter, }: { data: TrendDatum[]; /** Formats the headline "Latest" value — full precision is fine here. */ valueFormatter?: (v: number) => string; /** Formats the y-axis gridline labels — should stay compact (little horizontal room). Defaults to a compact number (e.g. "1.5k"). */ axisFormatter?: (v: number) => string; }) { const fmt = valueFormatter || ((v: number) => String(v)); const axisFmt = axisFormatter || defaultAxisFormatter; if (data.length === 0) { return
No data to chart.
; } const max = Math.max(1, ...data.map(d => d.value)); const min = Math.min(0, ...data.map(d => d.value)); const range = max - min || 1; const plotWidth = WIDTH - PADDING_LEFT - PADDING_RIGHT; const plotHeight = HEIGHT - PADDING_TOP - PADDING_BOTTOM; const baseline = PADDING_TOP + plotHeight; const points = data.map((d, i) => { const x = data.length === 1 ? PADDING_LEFT + plotWidth / 2 : PADDING_LEFT + (i / (data.length - 1)) * plotWidth; const y = PADDING_TOP + plotHeight - ((d.value - min) / range) * plotHeight; return { x, y, value: d.value }; }); const linePath = points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(" "); const areaPath = `${linePath} L ${points[points.length - 1].x} ${baseline} L ${points[0].x} ${baseline} Z`; // Horizontal gridlines from 0 up to the max, evenly spaced, with a value label on each. const gridlines = Array.from({ length: GRIDLINES + 1 }, (_, i) => { const value = min + (range * i) / GRIDLINES; const y = PADDING_TOP + plotHeight - (i / GRIDLINES) * plotHeight; return { y, value }; }); // Show at most ~6 x-axis labels so long series don't crowd the axis. const labelStep = Math.max(1, Math.ceil(data.length / 6)); const visibleLabels = data.filter((_, i) => i % labelStep === 0 || i === data.length - 1); const latest = data[data.length - 1]; return (
Latest {fmt(latest.value)}
{gridlines.map((g, i) => ( {axisFmt(g.value)} ))} {points.map((p, i) => ( ))}
{visibleLabels.map((d, i) => ( {d.label} ))}
); }