Chart
A dependency-free plotting primitive for trends, funnels and pies. Compose the named parts into one measured SVG; the legend focuses and toggles series, and every hue re-themes.
Installation#
Usage#
Chart is composed from named parts, like Card. The root takes the data, an optional index key for category labels, and a config that names each series’ label and color. The parts (ChartGrid, ChartArea, ChartLine, ChartTooltip) read it from context and draw into one shared, pixel-measured SVG.
import {
Chart,
ChartGrid,
ChartArea,
ChartLine,
ChartXAxis,
ChartTooltip,
} from "@/components/ui/chart"
const revenue = [
{ month: "Jan", revenue: 4200 },
{ month: "Feb", revenue: 4600 },
// …
]
export function RevenueChart() {
return (
<Chart
data={revenue}
index="month"
config={{ revenue: { label: "Revenue", color: "blue" } }}
padding={{ bottom: 28, top: 12 }}
className="h-72"
>
<ChartGrid />
<ChartArea dataKey="revenue" />
<ChartLine dataKey="revenue" />
<ChartXAxis />
<ChartTooltip />
</Chart>
)
}Load animation#
Every chart plays a one-shot reveal the first time it mounts: the ChartLine self-draws left to right, the ChartArea fill rises from the baseline, and each ChartBar grows up from the zero line in a left-to-right cascade. It fires once, never on hover or re-sort, and honors prefers-reduced-motion (every layer simply appears drawn). Pass animate={false} on the Chart root to opt out. Press Replay to watch it again.
Line#
Layer a ChartLine per series; each resolves its hue from config, or takes an explicit color. The shared ChartTooltip lists every series at the hovered category, and a crosshair tracks the active point.
Area#
ChartArea fills the space under the trend with a currentColor gradient that fades to the baseline, so the wash is just the series hue at low opacity: a blue line yields a soft blue-100/200 fill. Pair it with a ChartLine for a crisp edge.
Smooth curves#
Pass curve="monotone" to ChartLine, ChartArea or ChartAreas to join the points with a monotone cubic instead of straight segments. It is smooth, yet it still passes through every point and never swings above a peak or below a trough the data does not have: a plateau stays flat and an extreme sits exactly on its point, so the curve never invents a value. Gaps, stacking, dashed, markEnd, the draw-in and the hover dot all follow the curve. Set the same curve on the area and the line drawn over it.
Missing data#
A point with a non-numeric (or absent) value is a gap. By default the ChartLine and ChartArea break at the gap, so a dropout reads honestly. Pass connectNulls to bridge across it instead.
Bar#
ChartBar draws a bar per category from a 0-anchored baseline. Each bar stands square on the baseline and rounds only at its value end (the bottom, for a negative bar), and ChartGrid draws the zero line a step stronger, so the bars stand on a floor. Hovering a column brings it forward and dims the rest, so the active value reads at a glance.
Value labels & highlight#
Pass label to ChartBar to print each value just past its bar end. It returns inline content, so a figure can carry a glyph. To single one datum out, draw the series in neutral (the foreground at 10% on every theme) and pass its index to highlight, so that one bar takes the brand. Hovering a column also lifts its x-axis tick to the foreground.
Highlight & active bar#
highlight on ChartBar paints chosen bars in highlightColor (the brand by default) and leaves the rest in the series hue: an index for today, a list, a predicate over the row, or "active" for the bar under the pointer. The chart reports that bar through onActiveIndexChange (and takes a controlled activeIndex), so a legend or a headline can follow it. Here the latest month is lit until another is hovered. Swapping the highlight recolors the bars in place, so the grow-in never replays, and the tooltip swatch takes the hue the bar is wearing. While a column is hovered the others dim, except neutral bars: already faint, they hold their ink and the hovered one steps up instead, so no bar ever disappears.
Grouped & stacked bars#
ChartBars draws several bar series in one band. By default they render grouped side by side; set stack on Chart to stack them, and the y-axis grows to the per-category total automatically. Grouped series rotate distinct hues; stacked ones take shades of one blue, since their order already tells them apart. A color in config always wins.
Stacked area#
ChartAreas stacks several area series into a band chart when stack is set on Chart: each series sits on the cumulative total below it as a flat band with its own edge line. A stack reads by its order, so series that name no color take shades of the house blue, dark at the floor to light at the top (palette="tonal", the default whenever stack is set). Without stack the series overlap from the baseline.
Legend#
Put ChartLegend inside the Chart and it keys the chart’s own series, sits above the plot (or below it, with placement="bottom") and becomes the chart’s control surface, as in Mixpanel. Hover a key and its series comes forward while the rest recede; click it to hide or show the series, and the scale and the stack refit around what is left; Alt+click shows it alone. The last series showing never hides. Past max keys, the rest fold into a +N more menu with the same controls.
Outside a chart, ChartLegend is a plain key: feed it the same config (or explicit items) and each swatch resolves its hue exactly like the chart, palette fallback included.
Give an item a value and it prints after the label in the foreground, so the key doubles as a summary of each series. Inside a chart, value="sum", "average" or "last" summarises each series from the data, in its own format.
Driving it from outside#
The series state is controllable, so a table under the chart can run it, the way Mixpanel pairs every insight with its breakdown. hiddenSeries with onHiddenSeriesChange takes the checkboxes, and focusedSeries with onFocusedSeriesChange lights the plan under the pointer. A series and the period it is compared against always stay lit together.
Dual axis & per-series format#
Give a series axis: "right" in config to scale it against a secondary y-axis (so two metrics at different magnitudes share one chart), and render a second ChartYAxis with side="right". Each series can also carry its own format, so the tooltip shows, say, currency for revenue and a plain count for orders.
Forecast & end marker#
ChartLine takes dashed for projected data and markEnd to pin a filled dot on the last point (a “current value” marker). Split actuals and projection into two series that share the seam point, and the solid line meets its dashed continuation cleanly.
Incomplete period#
Today’s count is still climbing, so drawn like a finished day it reads as a collapse. Pass partial on Chart (or a number, for several trailing periods) and the open period draws as provisional, Mixpanel’s treatment: a line dashes from the last complete point on the exact same curve, its area fades, a bar hatches, and the tooltip marks the period with partialLabel.
Compare to previous period#
Name the earlier period in a series’ config with compare and the pair reads as one story. The earlier series becomes a ghost: it borrows the hue, takes no palette slot, and its ChartLine draws thin, faint and dashed with no prop set. The tooltip prints the change beside the current value, green up and red down; give a down-is-good metric (churn, refunds, latency) inverted and the colors flip. In ChartBars the ghost stands beside its partner, faint.
Reference lines#
ChartReferenceLine draws a horizontal threshold (y) or vertical marker (x) across the plot, with an optional label. Compose it after the data so it annotates on top, for a target, limit, or average. It defaults to a muted dashed line; pass a color to emphasize it.
Annotations#
ChartAnnotation pins a note to a category: a launch, an outage, a release, so the jump in the line explains itself. Its marker sits in a lane above the plot (the plot steps down to make room on its own) with a faint rule down to the baseline, and hovering it opens the note. Mixpanel numbers its markers; pass any short label, or none for a dot, and a color for a note that is bad news. The text also reaches screen readers beside the data table.
Tooltip rows#
footer on ChartTooltip adds rows under the series, below a hairline, for a figure the chart does not draw: here the net of income and expenses. Build each row with ChartTooltipItem (a swatch, a label, a value) so it lines up with the rows above and its swatch keeps its hue in the portaled bubble. labelFormatter receives the whole row and its index after the label, so the heading can say more than the axis (a full month name).
Tooltip order & totals#
With several series, the tooltip reads best as a ranking. sort="desc" orders the rows by their value at the hovered category, so the leader comes first however the lines cross (a comparison row stays under its series). total adds the column’s sum below a hairline (a string names it), and maxItems folds the tail into a +N more line. While a legend key holds the focus, the other rows recede with their series.
Axis labels that fit#
interval="auto" on ChartXAxis measures its labels against the plot’s width and skips just enough of them that none collide, re-thinning as the chart resizes. It counts back from the last category, so the latest day or month always carries its name. minTickGap sets the clear space between labels (12px), and minInterval keeps a month of days reading by the week however wide the plot. A number still labels every Nth category from the first.
Axis titles & compact figures#
ChartYAxis prints compact figures by default (12K, 1.4M), the way Mixpanel and Attio label an axis; pass tickFormatter for currency or units. Both axes take a label: the y title turns to run along the outer edge, the x title centers under the ticks, as in Attio’s report builder. Give the chart the room for them in padding.
Negative values#
The domain anchors to zero, and bars and areas grow from the value baseline (the y-zero line), so positive values rise above it and negative values drop below. No extra config: pass data that crosses zero and it reads correctly.
Fitted domain#
A full chart starts its y-axis at zero, which is right for bars and totals. A balance or a rate moves in a band far from zero, and a zero floor flattens it into a line. Pass domain="fit" and the axis wraps the data’s own extent in nice rounded bounds, with ticks on a nice step, so the grid and the labels still land on round values. It never crosses zero when the data does not. A [min, max] pair still fixes the domain exactly.
Funnel#
ChartFunnel draws conversion through ordered steps, as Mixpanel and Attio do. Each step is a column, and above it a faint ghost rises to the step before, so what dropped off reads as the empty part of the column. Each step carries its conversion from the first, and ChartTooltip adds the conversion and drop-off rows by itself. Give the chart about 32px of padding.top for the first label.
Pie & donut#
ChartPie draws each row as a slice, clockwise from twelve o’clock, with a clean gap between slices. innerRadius sets the hole (0.62 by default, a donut; 0 for a pie), and its children center in the hole. Hovering a slice lifts it out a step while the rest recede, with its value and share in the tooltip. A ChartLegend in the same chart keys the slices in their hues and lights them from the key.
Category bar#
CategoryBar breaks one whole into its parts in a single line: credits by tool, storage by file type, time by project. It takes the same rows as a pie (each one a category, named by index, sized by dataKey), so where a donut spends a square, this sits under a figure or across a card. Each segment’s width is its share, with a clean gap between segments, and CategoryBarLegend keys them underneath with the share after each name. Hovering a segment or a key lights it and recedes the rest, with its amount and share in the tooltip. Give the long tail’s Other the gray hue.
size sets the thickness (6, 8 or 12px), palette="tonal" paints shades of the house blue instead of distinct hues, and the legend’s value prints the share, the amount (through valueFormatter), both, or nothing.
Sparkline#
Pass sparkline for a chromeless trend: zero padding (it bleeds to the edge), points spanning the full width, and currentColor so a single text-* utility sets the hue. This is exactly what StatSparkline renders under the hood.
Sparkline bars#
A sparkline can draw bars too, for a week of orders beside a figure. Once a ChartBar is in it, the categories sit in bands (the first and last bars keep their full width) and the scale runs from zero, so the shortest bar still reads as a bar. Add ChartTooltip for a hover readout, and highlight to light today.
Colors#
Every series takes a semantic hue: blue (the default), teal, purple, orange, pink, the status roles, or current to inherit the ambient color. Series without a color follow the chart’s palette: categorical rotates distinct hues led by blue, for lines that cross and must be told apart; tonal spreads the blue ramp (blue-800 to blue-200) as far apart as the count allows, and is the default for a stack.
Loading & empty#
Pass loading to render a Skeleton in place of the plot (it also sets aria-busy). When data is empty, the chart shows a muted message; override it with the empty prop.
Accessibility#
The SVG is decorative (aria-hidden): pixels do not read well to a screen reader. Instead, every full chart renders a visually-hidden data table as its accessible equivalent, so assistive tech gets the real numbers, row by row. Pass label to name the chart, it becomes the table’s caption. loading sets aria-busy. Sparklines stay decorative (no table), since they are labelled by their surrounding context, like a Stat’s value.
// "label" names the chart and captions the visually-hidden data table
// that screen readers read in place of the (aria-hidden) SVG.
<Chart data={revenue} index="month" label="Monthly revenue"
config={{ revenue: { label: "Revenue", color: "blue" } }} className="h-72">
<ChartGrid />
<ChartArea dataKey="revenue" />
<ChartLine dataKey="revenue" />
<ChartXAxis />
<ChartTooltip valueFormatter={(v) => `$${v.toLocaleString("en-US")}`} />
</Chart>API reference#
Chart takes data (an array of rows, or a bare number[]), index, config, domain ("auto", the default, from zero; "fit" around the data; or a fixed [min, max]), sparkline, stack (stack multi-series bars and size the domain to the total), palette ("categorical" hues or "tonal" shades of blue for series with no color; tonal by default on a stack), padding, animate (the load reveal, on by default), crosshair (the hover tracking line, which paints behind the data so the trend stays on top; defaults on for full charts, off for sparklines), activeIndex and onActiveIndexChange (the hovered category, controlled or reported), hiddenSeries, defaultHiddenSeries and onHiddenSeriesChange (the series switched off), focusedSeries and onFocusedSeriesChange (the series in focus), partial (trailing periods still in progress), loading, empty, and label (accessible name + data-table caption), and forwards div props, a ref included (merged with the one it measures through). The series parts ( ChartLine, ChartArea, ChartBar) take a dataKey and optional color; ChartBar also takes label (a value label per bar) and highlight with highlightColor (bars painted in another hue); ChartLine and ChartArea also take connectNulls and curve ("linear" or "monotone"); ChartLine adds dashed, markEnd and variant ("comparison" draws the faint dashed ghost, the default for a series another one compares against). ChartBars and ChartAreas draw several bar / area series (grouped, or stacked when stack is set) from a keys list, and ChartAreas takes a curve too. ChartGrid, ChartXAxis (with interval, a number or "auto", plus minTickGap and minInterval), and ChartYAxis (with side="right" for the secondary axis and compact figures by default) are the chrome, both axes with an optional label title, and ChartGrid takes baseline (the stronger zero line, on by default); per-series config entries take format, axis, compare (the key of the period it is measured against) and inverted (down is good); ChartAnnotation pins a note (index, label, title, description, color) to a category; ChartFunnel draws steps with their drop-off (dataKey, barRatio, radius, label); ChartPie draws a pie or a donut (dataKey, innerRadius, gap, colors, valueFormatter, tooltip, and children for the hole); CategoryBar splits one whole in a single bar (data, index, dataKey, colors, palette, size, valueFormatter, tooltip, animate, label) with CategoryBarLegend under it (value: share, value, both or none); ChartReferenceLine adds a y/x threshold with an optional label; ChartTooltip adds the hover bubble with valueFormatter, labelFormatter (label, row, index), footer, sort, total, maxItems and partialLabel, whose rows are ChartTooltipItems (color, label, value, change, inverted, ghost, dimmed); and ChartLegend is the key: inside a chart it lists the series (or a pie’s slices) and focuses, hides and isolates them, with placement, max, interactive and value; outside it is a plain key fed the same config, or items with an optional value. Every part accepts className, merged last.