Legend and series toggle The legend does something here. Once a chart has more than three series the reader needs to isolate one, and making that work inside the chart is cheaper than a filter that reloads the page. npm i recharts Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --chart-1, --chart-2, --chart-3, --chart-4, --chart-5, --chart-6, --chart-7, --chart-8 ──────────────────────────────────────────────────────────────────────── // LegendChart.tsx ──────────────────────────────────────────────────────────────────────── import { useState, type MouseEvent } from "react"; import { Line, LineChart, XAxis, YAxis } from "recharts"; import { seriesColors } from "../categorical-series-palette/seriesColor"; export type Series = { key: string; values: number[] }; /** * The legend is the control. One click on a label isolates that series, * because someone looking at fourteen almost always wants one; a modifier * click adds to the set; clicking the last selected label restores all. * Table mode carries the last value, which is usually the number the viewer * came for and saves a panel beside the chart. */ export function LegendChart({ labels, series, rows = 5, defaultSelected = [], onSelectionChange, width = 420, height = 170 }: { labels: string[]; series: Series[]; /** Legend rows shown before "n more". The rest are one click away, not gone. */ rows?: number; /** Isolated keys. Empty means every series is drawn. Put this in the URL. */ defaultSelected?: string[]; onSelectionChange?: (keys: string[]) => void; width?: number; height?: number; }) { const [selected, setSelected] = useState(defaultSelected); const [expanded, setExpanded] = useState(false); const select = (keys: string[]) => { setSelected(keys); onSelectionChange?.(keys); }; const toggle = (key: string, e: MouseEvent) => { const additive = e.metaKey || e.ctrlKey; if (additive) select(selected.includes(key) ? selected.filter((k) => k !== key) : [...selected, key]); else if (selected.length === 1 && selected[0] === key) select([]); else select([key]); }; // Sorted by last value, so the legend is the summary table it looks like. const sorted = [...series].sort((a, b) => b.values.at(-1)! - a.values.at(-1)!); const { assigned, other } = seriesColors(sorted.map((s) => s.key)); const colorOf = (key: string) => assigned.find((a) => a.key === key)?.color ?? other!.color; const isolated = selected.length > 0; const shown = isolated ? sorted.filter((s) => selected.includes(s.key)) : expanded ? sorted : sorted.slice(0, rows); const hidden = isolated ? series.length - selected.length : series.length - shown.length; const data = labels.map((label, i) => Object.fromEntries([["label", label], ...series.map((s) => [s.key, s.values[i]])])); return (
{sorted.map((s) => ( ))}
SeriesLast
    {shown.map((s) => (
  • {s.values.at(-1)}
  • ))}
{hidden > 0 && ( )}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { LegendChart } from "./LegendChart"; /** * Fourteen services, requests per second over seven hours. The legend shows * the top five and folds the rest; click "checkout" to isolate it and the * footer reads "13 hidden". The eight busiest get a colour by key, the rest * share the muted "other". */ const LABELS = ["09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00"]; const SERIES = [ { key: "checkout", values: [220, 236, 226, 268, 258, 292, 312] }, { key: "search", values: [150, 158, 154, 176, 168, 194, 208] }, { key: "cart", values: [128, 134, 130, 152, 148, 170, 184] }, { key: "user", values: [96, 100, 98, 114, 110, 130, 141] }, { key: "feed", values: [68, 70, 66, 80, 78, 90, 96] }, { key: "auth", values: [60, 58, 64, 70, 72, 80, 84] }, { key: "payments", values: [48, 52, 50, 58, 60, 66, 71] }, { key: "inventory", values: [42, 44, 46, 50, 54, 58, 63] }, { key: "shipping", values: [36, 38, 36, 42, 44, 48, 52] }, { key: "notifications", values: [30, 32, 30, 36, 38, 40, 44] }, { key: "catalog", values: [26, 28, 26, 30, 32, 34, 37] }, { key: "reviews", values: [20, 22, 20, 24, 26, 28, 29] }, { key: "recommendations", values: [16, 16, 18, 18, 20, 22, 22] }, { key: "media", values: [12, 12, 14, 12, 14, 14, 15] }, ]; export default function Demo() { return console.log("isolated:", keys)} />; }