Chart
Renders a TanStack Charts definition in the theme's series palette, with a tooltip and a legend that toggles series.
const config = { desktop: { label: "Desktop" }, mobile: { label: "Mobile" },} satisfies ChartConfig;
const definition = useMemo( () => defineChart({ marks: [ crosshair({ x: { band: { fill: colors.fillSubtle, fillOpacity: 1, radius: 4 } }, y: false, }), barY(rows, { x: "month", y: "visitors", z: "device", color: "device", layout: group({ scale: chartGroupScale(weights) }), radius: 4, inset: 1, }), ], scales: { x: { scale: () => scaleBand().padding(0.25) }, y: { scale: scaleLinear().domain([0, max]), grid: true }, }, color: chartColor(config), focus: "group-x", tooltip, }), [max, rows, weights]);
<ChartLegendContent config={config} onValueChange={setShown} value={shown} /><Chart animate={animate} ariaLabel="Monthly visitors" config={config} definition={definition} height={240}/>;Installation
$ npx shadcn@latest add @constella/chartUsage
import { Chart, chartColor } from "@/components/tanstack/chart";import type { ChartConfig } from "@/components/tanstack/chart";Build the chart with TanStack Charts' defineChart and pass it to Chart with an ariaLabel that names the metric and period. Marks, scales, focus and the tooltip's behavior stay in the definition; Chart draws it in the theme's palette, with axes, ticks and grid lines in textMuted.
<Chart ariaLabel="Monthly visitors, January to June" config={config} definition={definition} height={240}/>Keep the definition outside the component, or in a useMemo on the values it reads. A new definition rebuilds the chart.
Composition
Chart└── ChartTooltipContent └── ChartTooltip ├── ChartTooltipTitle └── ChartTooltipRow ├── ChartIndicator ├── ChartTooltipLabel └── ChartTooltipValueChartLegendContent└── ChartLegend └── ChartLegendItem └── ChartIndicatorConfig
Name each series once in a ChartConfig, keyed by its value in the data. Pass chartColor(config) as the definition's color, and the same config to Chart and ChartLegendContent, so the tooltip and legend name it and every part paints it alike. Hiding a series never repaints the rest.
const config = { desktop: { label: "Desktop" }, mobile: { label: "Mobile", color: colors.chart5 },} satisfies ChartConfig;A series without a color takes the next in the palette, in the config's order. Past six, fold the rest into one gray "Other" with chartFold, which sums their rows that share every other field:
const { config, rows } = chartFold(allConfig, allRows, { series: "device", value: "visitors",});| Series | Token | Hue |
|---|---|---|
| 1 | colors.chart1 | Indigo |
| 2 | colors.chart2 | Teal |
| 3 | colors.chart3 | Violet |
| 4 | colors.chart4 | Cyan |
| 5 | colors.chart5 | Orange |
| 6 | colors.chart6 | Pink |
Tooltip
Add TanStack's tooltip to the definition and Chart renders it as ChartTooltipContent, naming each series from config. Leave tooltip out for none. To change its indicator or drop its title, render it yourself:
<Chart renderTooltipBody={({ content }) => ( <ChartTooltipContent config={config} content={content} hideLabel indicator="dot" /> )} {...props}/>To change what it holds, compose the parts, here with a total under the stack.
<Chart renderTooltipBody={({ points, primaryPoint }) => ( <ChartTooltip> <ChartTooltipTitle>{points[0]?.datum.month}</ChartTooltipTitle> {points.map((point) => ( <ChartTooltipRow active={point === primaryPoint} key={point.key}> <ChartIndicator color={point.color} variant="line" /> <ChartTooltipLabel> {config[point.datum.device].label} </ChartTooltipLabel> <ChartTooltipValue>{point.datum.visitors}</ChartTooltipValue> </ChartTooltipRow> ))} <ChartTooltipRow> <ChartTooltipLabel>Total</ChartTooltipLabel> <ChartTooltipValue>{total(points)}</ChartTooltipValue> </ChartTooltipRow> </ChartTooltip> )} {...props}/>Legend
Use ChartLegendContent with config for a toggle per series; value lists the shown ones. Filter the definition's rows by it.
<ChartLegendContent config={config} onValueChange={(next) => { if (next.length > 0) { setShown(next); } }} value={shown}/>For your own items, compose ChartLegend, ChartLegendItem and ChartIndicator.
<ChartLegend onValueChange={setShown} value={shown}> <ChartLegendItem value="chrome"> <ChartIndicator color={colors.chart1} /> Chrome </ChartLegendItem></ChartLegend>Animate
Ease the values with useChartTween and pass animate to Chart for a change from a click, never a key press.
const [animate, setAnimate] = useState(false);const target = useMemo( () => Object.fromEntries( rows.map((row) => [ `${row.month}-${row.device}`, shown.includes(row.device) ? row.visitors : 0, ]) ), [shown]);const values = useChartTween(target, animate);
<ChartLegendContent config={config} onValueChange={(next, { event }) => { setShown(next); // A key press reports a `detail` of 0. setAnimate(event instanceof MouseEvent && event.detail > 0); }} value={shown}/><Chart animate={animate} definition={definition} {...props} />;Set the axis to where the change ends, and scale the eased values onto it. An axis that eases re-ticks every frame. animate then fades in the tick labels that arrive, move or change.
const { max = target.max } = useChartTween(target, animate);
barY(rows.map((row) => ({ ...row, visitors: (row.visitors * target.max) / max })), …);// scales.y{ scale: scaleLinear().domain([0, target.max]), grid: true }Group Scale
For grouped bars, ease each series' slot with chartGroupScale. Give it a weight from 0 to 1 per series, and put a closing series' rows first, so its bars sit under the ones that widen over them.
layout: group({ scale: chartGroupScale([ ["desktop", values.desktop], ["mobile", values.mobile], ]),}),Line
defineChart({ marks: [ crosshair({ x: true, y: false }), // A line per series, so a leaving one can fade alone. ...opacities.map(([device, opacity]) => lineY( rows.filter((row) => row.device === device), { id: `line-${device}`, x: "month", y: "visitors", color: "device", strokeOpacity: opacity, strokeWidth: 2, } ) ), ], clip: true, scales: { x: { scale: () => scalePoint().padding(0.25) }, y: { scale: scaleLinear().domain(domain), grid: true }, }, color: chartColor(config), focus: "group-x", maxFocusDistance: Number.POSITIVE_INFINITY, tooltip,});API Reference
Chart takes TanStack Charts' React Chart props, with sx for className and style. The rest wrap a single element and take sx.
| Part | Adds |
|---|---|
Chart | config, naming series in the default tooltip; renderTooltipBody, ChartTooltipContent by default; animate, fading in tick labels a change adds or moves. Sets every axis's tickLabels.fontSize to 12 unless the definition sets its own |
ChartTooltipContent | content, TanStack's title and rows, or its text; config; indicator, line by default; hideLabel |
ChartTooltipRow | active, the series under the pointer or keyboard |
ChartIndicator | color, the series' paint; variant, dot or line; hollow in a hidden legend item |
ChartLegendContent | config, and Chart Legend's props |
ChartLegend | Toggle Group's props, always multiple |
ChartLegendItem | Toggle Group Item's props |
chartColor | config in, the definition's color out: every series' domain and paint |
chartFold | config, rows, and { series, value }, the rows' field names; label, Other by default; the first six series and one other, summed |
chartTickFormat | locale in, an axis's ticks.format out: compact counts (12M), whole numbers only |
useChartTween | target, values by key; animate; returns them eased over dialog on out, or at once when animate is false or motion is reduced |
chartGroupScale | weights, each series' share of a slot from 0 to 1; padding, 0 by default, as each bar's inset: 1 already sets them 2px apart; a band scale for group({ scale }) |