Chart draws data as composable marks — lines, bars, areas and radial arcs — on shared, automatically laid out axes.
The mark factories — line, bar, area, arc, autoPositionedLabel and
sankey — are plain functions rather than components, so both frameworks import
them from the same place and build the same array.
A chart needs an ariaLabel and at least one mark. Each mark takes the rows it
draws and the names of the fields to read.
The label becomes the chart's accessible name, which is how a test finds it —
getByRole('img', { name }). Give two charts in one panel names where neither
contains the other, or a query for one of them matches both.
The examples below are complete but for useSampleDataset — that is this site's
own loader for Observable's sample data, standing in for wherever your rows come
from.
In React, wrap the marks array in useMemo. The definition it produces is what
the renderer diffs against, so a new array on every render rebuilds the scene on
every render.
That is the whole of building a chart. Everything past it is one of five subjects, each on its own page:
definition reaches the TanStack Charts
definition underneath the chart, for what the props don't cover — the mark types,
transforms and faceting this iteration doesn't wrap, and options like clip and
cursor.
An object is laid over the definition the chart built from its other props, so everything you didn't mention stays as it was. Options merge all the way down; an array, a scale or a function replaces what it lands on.
That axis keeps the scale and the tick format the chart inferred for it — only the count moves.
A function is handed that same definition and returns the one to draw, for what a merge can't say — adding to the marks rather than replacing them, or answering with a definition of your own:
In React, memoize the object or the function the same way you memoize marks —
a new one on every render rebuilds the scene on every render.
The function is the only form that replaces. An object inherits every key it
doesn't mention — x, y, theme, motion, focus, focusRing,
maxFocusDistance and tooltip included — so a whole definition handed over as
an object still picks up whatever it left out. For a radial or sankey definition
that means the cartesian axes it deliberately has none of.
buildChartDefinition builds the same definition outside a chart, for code that
needs one in hand before it renders.
PlumaChart extends BoxA longer description of the chart, for detail the label can't carry.
The chart's accessible name — what the chart shows, in the words a reader would use. Required: without it the chart is an unlabelled image to a screen reader.
Derives the height from the measured width instead of fixing it. Ignored
when height is set.
The TanStack Charts definition underneath the
chart, for what the props above don't reach — clip, cursor, other mark
types, transforms, faceting, custom scales.
An object is laid over the definition Pluma built from the other props,
option by option, so a chart keeps its defaults and sets only what it came
for: definition={{ clip: true }} is a chart that clips to its plot and
nothing else changed. Objects merge all the way down, and anything else — an
array, a scale, a function — replaces what was under it.
A function is handed Pluma's definition and returns the one to draw, for
what a merge can't say: adding to marks rather than replacing them, reading
a default before changing it, or returning a definition of your own and
ignoring Pluma's altogether. It is the only form that replaces. An object
inherits every key it doesn't mention, x, y, theme, motion, focus,
focusRing, maxFocusDistance and tooltip included — so a whole
definition passed as an object still picks up whatever it left out, which for
a radial or sankey definition means the cartesian axes it deliberately has
none of.
Default:the chart's `ariaLabel`, as a slug
What the downloaded files are called, without an extension.
Default:['png', 'pdf', 'csv']
Which files the export menu offers, in the order it lists them. See
ChartExportFormat.
'csv' needs rows to write, so it is dropped from the menu on a chart that
has none — one whose only marks are annotations, and one whose definition
is a builder function, which is free to draw marks Pluma never saw. A chart
left with no formats at all draws no button.
Default:'group'
What hovering or keyboard-walking the chart focuses: the whole column at
the pointer, or the single nearest point. See ChartFocusMode.
Default:320, or 40 for `variant: 'sparkline'`
The chart's height in pixels.
Default:'top'
Where the legend sits. 'top' and 'bottom' are a band across the chart,
with entries packed along it and wrapped as needed; 'left' and 'right'
are a column beside the plot, one entry per row.
Reach for a column when the chart has a handful of series with names too long to sit side by side, or when the plot is round — a donut has no top edge for a band to belong to. It costs the plot the width the column takes, so on a narrow chart a band is still the better trade.
A column gives every series its own row and doesn't wrap, so it needs 22px of chart height per series, less 4px — an 18px row with a 4px gap between one row and the next. Past what the chart is tall the rows run out of room to be drawn in — use a band, which wraps, for more series than that.
That makes height answer to the legend on a chart with several series,
which is a problem where the plot wants to be smaller than the column beside
it. A radial mark has ArcOptions.outerRadius for exactly this: size
the surface for the legend and the ring for itself.
Writes a number beside each legend entry's name — the series' share, its total, a rate. Called once per series with that series' values added together, the total across every series, and the share of it.
Totals cover every series the chart was given, hidden ones included, so a
share stays put when the reader takes a series off the plot. A drawing already
measured as a proportion of what it plots is the exception — a radial mark, and
a stack with stackOffset: 'share' — where the totals cover the series still on
the plot and a hidden one reads as zero, so the figures re-proportion the way
the slices or the columns do. Without this the legend names series and nothing
else.
Default:measured from the labels each side draws
The space around the plot, in pixels — one number for every side, or the
sides to set. See ChartMargin.
A side that is set replaces the room the chart would have measured out for the labels drawn on it rather than adding to it, so it has to be wide enough to hold them — and a single number, which sets all four, has to clear every side at once. Sides left out keep what was measured for them.
That is what makes it worth having: pinning a side is how a column of charts is held to one plot width whatever each one's labels happen to be, which measuring per chart can't do. A legend beside the plot keeps the column it reserves on that side, since that is the room it is drawn in rather than space around it.
The layers to draw, from line and bar, painted in order.
Called with the point under the pointer or keyboard cursor, or null when it leaves.
Called with the full hidden set whenever a legend entry is pressed.
Called with the clicked or keyboard-activated point, or null for an empty click.
Default:the `data-viz-categorical` tokens, overridable per slot with
the `--pluma-c-chart-series-*` CSS variables
The colors series are assigned from, in order.
Default:'definition'
What order the tooltip reads its rows in: the order the marks and series are
declared, or the order the focused points sit on the plot. See
ChartTooltipSeriesOrder.
Default:'default'
How much of the chart around the marks is drawn. 'sparkline' takes all of
it away and shrinks the chart to sit beside a figure rather than under a
heading. See ChartVariant.
Default:measured from the container, and re-measured as it resizes
The chart's width in pixels, for a chart that shouldn't follow its container — a fixed column in a dashboard, or an export at an exact size.
Default:false
Whether the chart draws itself in rather than appearing finished, and moves between shapes when its data changes — bars growing out of the baseline, lines revealing along their length, points and axis ticks traveling to where the new numbers put them.
Worth it where the chart is the page's subject and the reader is watching it arrive, or where it restates the same measure as the reader changes a filter or a date range: the tween is what carries which bar became which, and without it a chart that redraws is just a different picture. Leave it off for a chart that is one figure among many — a sparkline in a table, a tile on a dashboard of twelve — where a dozen entrances at once is noise, and for anything redrawing faster than it can finish.
Readers who ask for reduced motion get the finished chart with no animation, and so does a chart that has only been resized.
Default:false
Whether the chart offers its reader a download — a menu in the top corner
with the chart as a picture and as the numbers behind it. See
BaseChartProps.exportFormats for what it offers.
Worth it wherever the chart is the answer to a question somebody asked: a report they will send on, a dashboard tile whose numbers belong in a spreadsheet next. Leave it off where the chart is a small part of a page's furniture — a sparkline in a table, a trend behind a headline figure — which a download button in the corner would be larger than.
The code that writes a file is loaded when the reader asks for one, so a chart that offers this costs nothing until it is used.
Default:true
Whether the chart responds to the pointer and the keyboard at all — the dot
it paints on the nearest point, the keyboard cursor that walks between them,
and the onFocusChange / onSelect callbacks those drive.
Turning it off makes the chart a picture. Worth it only when the numbers are already readable elsewhere and the chart is too small to interrogate — a sparkline in a table cell, sitting beside the value it qualifies. It takes the tooltip with it, since a tooltip has nothing to open onto.
Default:false
Whether to render a color legend beside the plot. Has no effect until a mark
declares series, since there is nothing to label before that.
The legend is interactive: each entry takes its series off the plot and puts it back when pressed again, and the axes recompute from what's left. Leave it off where the chart is a fixed summary rather than something to interrogate.
Default:false
Whether pointing at or keyboard-focusing a legend entry highlights that series on the plot.
It draws no highlight of its own: it hands the series to the same mark options
the pointer already answers with, so a mark responds exactly as far as it
opted in. A mark with withHoverColor swaps the whole series to its hover
color; one with withUnfocusedDim recedes every other series; a mark with
neither doesn't move. Turn at least one of them on, or there is nothing for
this to show.
What it deliberately doesn't do is read a value: no tooltip opens and no crosshair is drawn, because an entry names a series rather than a place on the plot. Reading stays the pointer's job.
Worth it on a chart with enough series that finding one by color is work.
Default:true
Whether hovering or focusing the chart shows a tooltip for the nearest point.
The horizontal axis' guide and scale.
The vertical axis' guide and scale — or several of them, for quantities in units that have no scale in common.
Give each one an id and point a mark at it with yAxis: '<id>'; a mark that
names none is measured against the first. Every axis is drawn down the left
unless it asks for position: 'right', and axes sharing a side stack outward
in the order they were declared. They are drawn at the same tick heights and
share one grid, so scale, withGrid and tickCount come from the set
rather than from each.
More than one axis makes a chart harder to read: nothing about the plot says which line belongs to which side, and the ratio between the ranges is a choice that can make almost any pair of series look correlated. Worth it for a quantity that genuinely qualifies another — a rate over the volume it is a rate of. Reach for stacked charts when they are separate subjects.