SimpleCharts
Usage
Examples
AreaChart
Standard
Standard (overlaid) variant — each series draws from the baseline independently with transparency, useful when stacking would be misleading.
With metric
The metric variant: Metric bricks sit to the left of the legend in a row between the
header and the plot.
Filterable
Clicking a legend row isolates that series and clicking another adds it back.
Zoom
Drag across the plot to choose a range, then confirm in the popover.
Loading
Loading skeleton state composed from the same primitives as a populated chart.
Empty
Empty state — LineChartEmpty keeps the dashed plot frame and puts the message inside it.
BarList
Default
The interactive shape: click a row to filter down to it, and the header's clear button
appears while that filter holds. Filtering recomputes max from what is left, which is why
a lone row reads 100%.
Colored
color tints the bar with a palette hue at 16% — enough to distinguish the rows while still
letting the row's own hover state show through it.
Custom colors
For a hue the palette does not carry, pass a bg-* utility on the bar instead — the
explicit class wins the merge against the variant's own fill.
Percentage
Values that are already percentages: max={100}, and the value slot prints the number
itself rather than a share derived from it.
Selectable
selected marks the active row — the hover tint and aria-current — and never touches the
bar, whose width stays value / max whatever is selected.
Truncated labels
Long labels truncate at the row width. A tooltip on the label needs pointer-events-auto,
since the row's slots are transparent to the pointer by default.
Truncated labels with tooltip
The same labels under OverflowTooltip, which shows the full text only when it is actually
cut off — the right choice when most labels fit.
Data variants
One card, three datasets behind a settings menu: raw counts, percentages and fractions each
need their own max, so switching the source switches that too.
Loading
BarListSkeleton stands in for the list while data loads, taking a row count so the card
holds the height it will end up at.
Overflow
A value above max caps the bar at full width and the label at 100%, rather than
overrunning the row.
Invalid max
max={0} is not a division the component will guess at: every row renders empty at 0%, with
a single warning in development.
Percent digits
digits adds decimal places, for a set of shares too close together to tell apart as whole
numbers.
Percent variants
The three percent treatments — split colours the number and the % differently and is the
Figma default, muted sets both back, inherit leaves them to the row.
Chart
Default
The frame with everything it owns — title, hover-revealed actions, and a body slot standing in for the chart.
With always visible actions
alwaysVisible pins the actions open, which is what a card does while a filter is applied:
the control that clears it must not need hovering to be found.
Custom size
The card is a full-width block with a minimum height; give it a width and height directly when a dashboard slot demands a particular size.
Long title
ChartTitle stays on one line and truncates, so the actions keep their place however long
the title runs.
Empty
ChartEmpty centres a default 'No data' in the body — the frame and the title stay, so the
card does not vanish from the grid when its data does.
Empty with custom message
Pass children to say why it is empty. A card emptied by a filter should say so, since the fix is different from having no data at all.
Vertical Stacked Columns Chart
Default
Findings by verdict over a week: the legend carries each verdict and its total for the period, a column per day stacks the verdicts in legend order with the first on top. Hover a column for the day's breakdown, or a legend item to fade the other verdicts out.
Adaptive width
Column width follows the chart width and the number of steps: up to 32px, a fixed 22px between columns below that, and 16px at least. The same week in a narrow card keeps 32px columns; two weeks in it narrow them.
Card height
The plot follows the card height: set a height on <Chart> and the columns stretch or
shrink with it. The card goes down to 128px — header, legend and a 72px plot.
Clickable legend
Legend items become buttons when they get an onClick — here a verdict opens its list, as
on the Agentic Overview. The item's data-key carries the series key, so one handler serves
every item. Hover and keyboard focus fade the other verdicts out alike.
Loading
Loading: skeleton legend items over LineChartEmpty's dashed plot frame.
Empty
Empty: LineChartEmpty keeps the dashed plot frame and puts the message inside it.
HorizontalBarStack
Default
The full shape: a headline value with its delta, a three-part band, and the legend that names the parts.
No delta
The same without the delta — a value with no comparison to make yet.
No value
No header at all: the band and its legend alone, for a card whose total is already stated somewhere else.
With remainder
A total above the sum of the parts fills the difference with a grey tail — 91 of 120,
where the 29 nobody has accounted for is part of what the card says.
Legend off
legend={false} leaves the band on its own, for a card too small to carry names or one
whose legend is already nearby.
Palette
All six chart colours in one band, which is the check that they stay tellable apart at the widths a real breakdown gives them.
Loading
HorizontalBarStackSkeleton stands in while the card loads, holding the header and band
heights so nothing shifts when data arrives.
Selectable
onSelect turns the legend into a filter — the chosen series stays bright, the rest fade,
and clicking it again clears. Hover always wins, so pointing at any series brings it forward
regardless of what is selected.
LineChart
Default
One series with the full chrome — grid, both axes, and a tooltip that follows the pointer along the line.
Default multi
Three series with a legend composed from LineChartLegendItem rows. The dot in each row is
the same one the tooltip uses, so the colours cannot drift apart.
With metric
The metric variant: the shared Metric bricks sit to the left of the legend in a row of
their own between the header and the plot. Pure composition — the chart is untouched and the
card simply grows by that row.
Curves
Straight joins tell the truth about where the samples are; monotone smooths between them
and reads better for a rate. Mix the two only when the series mean genuinely different
things.
Line styling
A dashed series against a solid one — the way to separate a threshold or a projection from measured data — and a CSS colour string for a series whose colour is decided elsewhere.
Edge cases
The three shapes that break naive charts: a sparkline with no chrome at all, a single data point, and a series with null gaps, which has to break the line rather than bridge it.
Custom tooltip
Composing the hover popover yourself, a LineChartHoverPopoverRow per series, for when a
value needs its unit rather than the default name-and-number.
Legend placements
Legend placement is structural rather than a prop: JSX order picks top or bottom, and
wrapping the body and the legend in an HStack picks left or right, where a vertical
orientation stacks its rows.
Filterable
Clicking a legend row isolates that series and clicking another adds it back, with the header's clear control appearing while anything is hidden.
Cross chart hover sync
Two cards sharing a pointer, and it takes two mechanisms: syncId lines up the cursor and
tooltip through recharts, while activeKey shares the series highlight through our own
context.
Zoom
Drag across the plot to choose a range, then confirm in the popover — Enter or the button
emits onZoomChange, while Escape, an outside click or a new drag dismisses it. Slicing the
data to that range is yours, and so is the way back out.
Loading
There is no loading prop: the state is composed from the same primitives as a populated
chart — Skeleton chips inside the legend and a bare LineChartEmpty for the dashed plot —
with aria-busy on the Chart left to the caller.
Refreshing
Each panel loads for two seconds before swapping to data, and the refresh button replays it. The skeleton and the real chart share an outer layout, so nothing but the content moves on the swap.
Empty
LineChartEmpty keeps the dashed plot frame and puts the message inside it, so a card with
no data still reads as a chart rather than as something broken.
Long time range
A thousand hourly samples in one card: drag to zoom and the header's control restores the full range. The story slices the data itself, exactly as a consumer has to.
Metric
Gallery
Every brick combination on its own card, mirroring the Figma options board: value alone,
value with a delta in each sentiment, and the three MetricTotal connectors: a slash, the
word "of", and the word "total" after the number.
Loading
MetricSkeleton in place of the stat while the card loads, so the number arriving does not
resize the card.
Sparkline
MetricSparkline after Metric draws the trend over the period, edge to edge along the
card's bottom. Hover it for a ruler on the nearest point and a popover with its date and value.
Sparkline colors
The sparkline's color is set per chart, on purpose apart from the delta's sentiment:
brand (default), positive, negative, and neutral.
Overview
Default
One card per chart, at its default: a BarList top-five, a PieChart share-of-total, a
LineChart over time, a HorizontalBarStack composition and a bare Metric.
PieChart
Default
The interactive shape: click a legend row to filter to that slice, with the header's clear control appearing while the filter holds.
Selectable
Selection kept in state rather than by filtering the data — the chosen row stays lit and the other slices fade, so the whole remains visible while one part is emphasised.
Truncated labels
Legend labels truncate at the row width, since the legend takes whatever horizontal space the donut leaves.
Truncated labels with tooltip
The same rows under OverflowTooltip, which reveals the full label only when it is actually
cut off.
Loading
PieChartSkeleton takes a row count so the placeholder legend is as long as the real one,
and the card does not resize when data lands.
Single slice
One slice is a full ring at 100% — legible, but ask whether a Metric says it better, since
there is no share left to compare against.
Two slices
Two slices is where a donut earns its place: the split is readable at a glance without reading either number.
Custom colors
Colours passed per datum rather than taken from the palette, for a breakdown whose categories already have colours elsewhere in the product.
Zero total
Every value zero: the ring renders empty and the percents read 0% rather than the chart dividing by nothing or disappearing.
Width variants
The same chart at 280, 400 and 560px. The donut holds its size and the legend takes the rest, which is what sets the practical minimum width.
Percent variants
The three percent treatments, matching BarList — split colours number and symbol
differently, muted sets both back, inherit leaves them to the row.
Palette
The six chart colours around one ring, for checking adjacent slices stay tellable apart at small sizes.