"use client"; /** * Recharts wrappers tuned to the Spinza tokens: one gold accent, muted greys, * hairline solid grid, 2px lines, thin rounded bars, tooltips in a glass card. * Text never wears the series colour — identity comes from the swatch beside it. */ import * as React from "react"; import { Bar, BarChart, CartesianGrid, Cell, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { cn } from "@/lib/utils"; export const CHART = { accent: "#c9a961", accent2: "#e8cf8f", grey: "#7b8090", grey2: "#4f5464", info: "#6ea8ff", success: "#3ddc97", danger: "#ff5c7a", grid: "rgba(255,255,255,0.07)", axis: "rgba(255,255,255,0.12)", tick: "#7b8090", } as const; export type Row = Record; export interface Series { key: string; label: string; color: string; /** Stacked bars share a stackId. */ stackId?: string; } const tickStyle = { fill: CHART.tick, fontSize: 11 } as const; export type Formatter = (v: number) => string; const defaultFmt: Formatter = (v) => (Math.abs(v) >= 1_000_000 ? `${(v / 1_000_000).toFixed(1).replace(/\.0$/, "")}M` : Math.abs(v) >= 1000 ? `${(v / 1000).toFixed(1).replace(/\.0$/, "")}K` : v.toLocaleString("en-US", { maximumFractionDigits: 2 })); /* ----------------------------------------------------------- tooltip */ interface TooltipEntry { name?: string | number; value?: number | string | ReadonlyArray; color?: string; fill?: string; dataKey?: string | number; payload?: Record; } interface AdminTooltipProps { active?: boolean; payload?: ReadonlyArray; label?: unknown; labelFormatter?: (label: unknown, row?: Record) => string; valueFormatter?: Formatter; series?: Series[]; } export function AdminTooltip({ active, payload, label, labelFormatter, valueFormatter = defaultFmt, series }: AdminTooltipProps) { if (!active || !payload || payload.length === 0) return null; const row = payload[0]?.payload; const title = labelFormatter ? labelFormatter(label, row) : label === undefined ? "" : String(label); return (
{title ?
{title}
: null}
{payload.map((p, i) => { const s = series?.find((x) => x.key === p.dataKey); const v = typeof p.value === "number" ? valueFormatter(p.value) : String(p.value ?? "—"); return (
{s?.label ?? String(p.name ?? p.dataKey ?? "")} {v}
); })}
); } /* ------------------------------------------------------------ legend */ export function ChartLegend({ series, className }: { series: Series[]; className?: string }) { if (series.length < 2) return null; return (
{series.map((s) => ( {s.label} ))}
); } /* ------------------------------------------------------------- frame */ export function ChartFrame({ title, subtitle, series, children, height = 220, right, className }: { title?: React.ReactNode; subtitle?: React.ReactNode; series?: Series[]; children: React.ReactNode; height?: number; right?: React.ReactNode; className?: string }) { return (
{title || right ? (
{title ?
{title}
: null} {subtitle ?
{subtitle}
: null}
{right}
) : null} {series ? : null}
{children}
); } export function ChartEmpty({ message = "No data for this period." }: { message?: string }) { return
{message}
; } /* ------------------------------------------------------------- bars */ export function Bars({ data, x, series, xFormat, yFormat = defaultFmt, stacked, barSize = 18, tooltipLabel, yDomain, layout = "horizontal", height, hideXTicks, referenceY }: { data: Row[]; x: string; series: Series[]; xFormat?: (v: unknown) => string; yFormat?: Formatter; stacked?: boolean; barSize?: number; tooltipLabel?: (label: unknown, row?: Record) => string; yDomain?: [number | "auto" | "dataMin" | "dataMax", number | "auto" | "dataMin" | "dataMax"]; layout?: "horizontal" | "vertical"; height?: number; hideXTicks?: boolean; referenceY?: { y: number; label?: string } }) { if (data.length === 0) return ; const vertical = layout === "vertical"; return ( {vertical ? ( <> ) : ( <> )} xFormat(l) : undefined)} />} /> {referenceY ? : null} {series.map((s, i) => { const last = i === series.length - 1; return ; })} ); } /** Single-series histogram with optional highlighted bucket. */ export function Histogram({ data, x, y, xFormat, yFormat = defaultFmt, logScale, highlight, color = CHART.accent, height }: { data: Row[]; x: string; y: string; xFormat?: (v: unknown) => string; yFormat?: Formatter; logScale?: boolean; highlight?: (row: Row) => boolean; color?: string; height?: number }) { if (data.length === 0) return ; const rows = logScale ? data.map((r) => ({ ...r, [y]: (r[y] as number) > 0 ? (r[y] as number) : null })) : data; return ( 8 ? -30 : 0} textAnchor={data.length > 8 ? "end" : "middle"} height={data.length > 8 ? 42 : 24} /> xFormat(l) : undefined} />} /> {highlight ? data.map((r, i) => ) : null} ); } /* ------------------------------------------------------------- lines */ export function Lines({ data, x, series, xFormat, yFormat = defaultFmt, yDomain, reference, tooltipLabel, height, dots }: { data: Row[]; x: string; series: Series[]; xFormat?: (v: unknown) => string; yFormat?: Formatter; yDomain?: [number | "auto" | "dataMin" | "dataMax", number | "auto" | "dataMin" | "dataMax"]; reference?: { y: number; label?: string }; tooltipLabel?: (label: unknown, row?: Record) => string; height?: number; dots?: boolean }) { if (data.length === 0) return ; return ( xFormat(l) : undefined)} />} /> {reference ? : null} {series.map((s) => ( ))} ); } /* ------------------------------------------------------- sparkline */ export function Sparkline({ values, color = CHART.accent, height = 28, width = 96 }: { values: number[]; color?: string; height?: number; width?: number }) { if (values.length < 2) return —; const min = Math.min(...values); const max = Math.max(...values); const span = max - min || 1; const pts = values.map((v, i) => `${(i / (values.length - 1)) * width},${height - ((v - min) / span) * (height - 4) - 2}`).join(" "); return ( ); }