"use client"; import { useMemo } from "react"; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Pie, PieChart, XAxis, YAxis, } from "recharts"; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig as ShadcnChartConfig, } from "../ui/chart"; import { useMediaQuery } from "../../lib/useMediaQuery"; import type { ChartData } from "../../lib/chartAggregate"; import { xAxisLabel, yAxisLabel, type ChartConfig } from "../../lib/chartConfig"; import { seriesColor } from "../../lib/homepageChart"; import { BAR_GAP } from "./surfaceGap"; const AXIS_LABEL_STYLE = { fill: "var(--muted-foreground)", fontSize: 11 }; // Safe CSS-var ids per series (channel names contain spaces/punctuation that // can't appear in `--color-`), while keeping the human label for legends. // An ungrouped chart has a single "all" series — label it by its Y metric // (e.g. "Total views") rather than the meaningless internal key. // // A series' colour (and a pie slice's) is seriesColor(i), the one the hub's // cross-site chart uses: the six validated slots --chart-1..6, then hues // spread by the golden angle. This chart used to cycle its own `i % 5`, so // the sixth series repeated the first and --chart-6 was never reached. function seriesMeta(data: ChartData, config: ChartConfig) { const single = config.groupBy === "none"; return data.series.map((s, i) => ({ id: `s${i}`, label: single ? yAxisLabel(config) : s.key, color: seriesColor(i), })); } export function ChartView({ data, config, }: { data: ChartData; config: ChartConfig; }) { const meta = useMemo(() => seriesMeta(data, config), [data, config]); // A 52px Y gutter and a 16px minimum tick gap are a desktop chart's // proportions; on a 390px canvas they eat the plot and crowd the axis into // overlapping labels. const narrow = !useMediaQuery("(min-width: 640px)"); const shadcnConfig = useMemo(() => { const c: ShadcnChartConfig = {}; for (const m of meta) c[m.id] = { label: m.label, color: m.color }; return c; }, [meta]); const rows = useMemo(() => { return data.categories.map((label, i) => { const row: Record = { x: label }; data.series.forEach((s, si) => { row[meta[si].id] = s.points[i]?.y ?? 0; }); return row; }); }, [data, meta]); if (data.categories.length === 0) { return (
No data for this configuration.
); } const showLegend = meta.length > 1; if (config.type === "pie") { const first = data.series[0]; const pieData = data.categories.map((label, i) => ({ name: label, value: first?.points[i]?.y ?? 0, })); const pieConfig: ShadcnChartConfig = {}; data.categories.forEach((label, i) => { pieConfig[`p${i}`] = { label, color: seriesColor(i) }; }); return ( } /> {pieData.map((_, i) => ( ))} ); } // Recharts discovers axis/grid/tooltip/legend children by type but does NOT // recurse into React fragments — so these must be passed as a flat array // (which React.Children flattens), never wrapped in <>.... const common = [ , , , } />, showLegend ? ( } /> ) : null, ]; const margin = { top: 8, right: 12, bottom: 20, left: 8 }; return ( {config.type === "line" ? ( {common} {meta.map((m) => ( ))} ) : config.type === "area" ? ( {common} {meta.map((m) => ( 1 ? "a" : undefined} /> ))} ) : ( {common} {meta.map((m) => ( 1 ? BAR_GAP : {})} stackId={config.type === "stackedBar" ? "a" : undefined} /> ))} )} ); }