Chart
A theming layer over recharts — container, tooltip, and legend chrome.
Usage
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
type ChartConfig,
} from '@acronis-platform/ui-react';
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts';Chart is a thin theming layer over recharts.
ChartContainer supplies the series colors and themes recharts' internals (grid,
axis, cursor) with the semantic token vocabulary; ChartTooltipContent and
ChartLegendContent give the tooltip and legend the Acronis look. You compose the
chart type itself — BarChart, LineChart, AreaChart, PieChart, … — from
recharts primitives.
Series colors come from a dataviz palette, never from a hand-written value:
config names the series, palette says which palette they're painted from, and
ChartContainer resolves each one into a --color-<key> custom property you
reference from the recharts fill / stroke props.
rechartsresolves as a dependency of@acronis-platform/ui-react; import the chart primitives directly fromrecharts.
Palettes
Four palettes, all resolving to the --ui-dataviz-* tokens. Pick by what the
color has to communicate:
palette | Use it when | Stops |
|---|---|---|
{ type: 'categorical' } (default) | Unrelated categories with no order between them. Color carries identity, not magnitude. | 16 hues |
{ type: 'sequential', ramp } | One quantity along an ordered scale — funnel stages, tiers, recency. ramp: blue · teal · orange · violet. | 8 stops, dark → light (darkest first) |
{ type: 'diverging', pair } | Two directions away from a midpoint — over/under, gain/loss. pair: blue-orange · teal-violet. | 6 stops, interleaved a–b strongest-first |
{ type: 'status' } | The value means a severity. Never decorative. | danger · critical · warning · success · info · neutral |
Series walk the palette in its defined order — the first entry in config
takes the first stop, and so on. So declare exactly the series the chart plots:
an entry for one it doesn't draw still consumes a color and shifts the rest.
(That's deliberate — a series then keeps its color when a sibling is toggled off.)
A series can be re-pointed with tone:
{ slot: 7 }— another hue of the categorical palette.{ status: 'danger' }— required under thestatuspalette, whose colors are chosen rather than walked.{ sameAs: 'actual' }— paint whatever another series paints. For a twin series that is the same metric drawn differently (a forecast tail, a projection band), where two hues would read as two metrics. An aliased series doesn't consume a stop.{ side: 'a' | 'b' }— diverging palette only. Pins the series to one of the two hue families. Allside: 'a'series share the a-hue stops (a3 → a2 → a1, strongest-first), and allside: 'b'series share the b-hue stops likewise. Series with nosidewalk the default interleaved ramp, skipping any stop already claimed by a sided series.
sequential takes no per-series override: its stops are a ramp, meaningful only
in relation to each other.
Examples
A bar chart with a themed tooltip and legend:
const config = {
desktop: { label: 'Desktop' },
mobile: { label: 'Mobile' },
} satisfies ChartConfig;
<ChartContainer config={config} className="h-[300px] w-[500px]">
<BarChart data={data}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>;Light and dark need no work: every palette token is a light-dark() pair that
follows the active theme on its own.
A chart whose colors carry meaning names the palette and the tone per series:
const config = {
failed: { label: 'Failed', tone: { status: 'danger' } },
degraded: { label: 'Degraded', tone: { status: 'warning' } },
healthy: { label: 'Healthy', tone: { status: 'success' } },
} satisfies ChartConfig;
<BarChart config={config} palette={{ type: 'status' }} … />;Formatting and hiding axes
Every cartesian chart — BarChart, LineChart, AreaChart, ComposedChart,
ScatterChart, ConfidenceCone, Histogram — shares the same axis knobs:
showXAxis/showYAxis(defaulttrue) — hide either axis (its ticks and title). Useful for compact/sparkline layouts.xTickFormatter/yTickFormatter— format each tick value. Unlike theunitsuffix (which only appends text), a formatter can transform the value: abbreviate it, prefix a currency symbol, or map a number to a label.
Ready-made formatters ship alongside the chart primitives; any
(value) => string function also works:
formatCompactNumber—146500 → "146.5K",1_500_000 → "1.5M".formatPercent—41.8 → "41.8%".createTickFormatter(options)— a factory overIntl.NumberFormatoptions (currency, fixed decimals, a specific locale).
import {
LineChart,
ScatterChart,
formatCompactNumber,
createTickFormatter,
} from '@acronis-platform/ui-react';
// Abbreviate large values — 146500 → "146.5K"
<LineChart config={config} data={data} dataKeys={['mrr']} xKey="month"
yTickFormatter={formatCompactNumber} />;
// Compact currency via the Intl factory — 146500 → "$146.5K"
<LineChart config={config} data={data} dataKeys={['mrr']} xKey="month"
yTickFormatter={createTickFormatter({
style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1,
})} />;
// Map values to labels
<ScatterChart config={config} series={series} xKey="hours" yKey="score"
yTickFormatter={(score) => (Number(score) >= 80 ? 'High' : 'Low')} />;
// Hide both axes for a sparkline
<LineChart config={config} data={data} dataKeys={['mrr']} xKey="month"
showXAxis={false} showYAxis={false} showGrid={false} showLegend={false} />;Tick spacing, domain & grid
The same shared props also tune tick placement, the value-axis domain, and the grid:
xAxisAngle— rotate X tick labels (e.g.-45) when they'd otherwise overlap. Combine it withxAxisLabel; the axis reserves room for both.xAxisInterval— thin dense ticks: a placement strategy (preserveStartEnd), or the number of ticks to skip between two rendered ones (0shows every tick,1every other one,2every third).yAxisTickCount— a hint for how many value-axis ticks to draw.yAxisDomain—auto(fit the data at both ends; the axis need not include 0),zero(anchor at 0), ordataMin-dataMax(tight, no padding).gridDashed,gridHorizontal,gridVertical— dash or trim the grid lines.
Note: omitting yAxisDomain leaves the domain to recharts, whose default is
already anchored at 0 — so zero is the explicit form of the default rather than a
change to it. Use auto when you want the axis to fit the data instead.
yAxisTickCount and yAxisDomain apply to whichever axis carries the values.
That's Y for every chart except ComposedChart with orientation="horizontal",
where the bars grow along X — there they drive the X axis, and xUnit (not
yUnit) carries the unit suffix.
<BarChart
config={config}
data={data}
dataKeys={['desktop']}
xKey="month"
xAxisAngle={-45}
yAxisDomain="zero"
yAxisTickCount={4}
gridDashed
/>;Accessibility
ChartContainer renders a plain <div> with no role and no accessible name of
its own — it spreads whatever props you pass, so role="img" plus an
aria-label (or an aria-labelledby pointing at the visible heading) is how a
chart gets named. No recharts-backed chart in the kit synthesizes a name from
config. The exception is CategoryBar, which is
plain DOM rather than a plot: it joins each segment's config label and
formatted value into a summary and applies it as role="img" plus aria-label,
which a caller can replace by passing an aria-label of their own.
An SVG plot is not readable as data, so the numbers have to exist as text
somewhere — a caption, a summary sentence, or an adjacent table. The chrome
carries part of that: ChartLegendContent renders each series' config label
as real DOM text beside its marker, and ChartTooltipContent names every series
next to its value. Don't hide the label and the indicator together, and never
let color be the only thing separating two series.
recharts' own accessibility layer is on by default, which gives the plot surface
tabindex="0" and role="application": focusing it opens the tooltip on the
first data point, and while it has focus ← / → walk that
tooltip across the others. Enter toggles the tooltip rather than
pinning it, and because focus has already opened it, the first Enter
closes it — press it again, or press an arrow key, to bring it back. That focus
stop is not visibly indicated — the container's outline-hidden rules, which
exist to strip recharts' stray outlines off layers and sectors, cover the
surface too.
A <Brush> traveller is the exception, because it is a genuine control:
recharts renders each of the two handles as role="slider" with tabindex="0",
moving the range one row per ← / → press. ChartContainer
restores an explicit :focus-visible outline in --ui-focus-primary for it —
an outline rather than the usual ring, because box-shadow doesn't paint on SVG
children.
- Name the handles.
brushAriaLabel— onBarChart,LineChart,AreaChartandComposedChart, the four types that take a brush — labels both travellers and defaults to'Chart range selector'. That default is not cosmetic: recharts' own fallback builds the name from anamefield on the data row, which none of these charts require, so without it the handles announce "Min value: undefined, Max value: undefined". - The handle's
aria-valuenowis recharts' pixel offset rather than a data index, so the announced number means nothing. The range captions under the strip — painted while a handle is focused or dragged — are the readable signal.
Direction (RTL)
Under dir="rtl" a chart mirrors in two halves, deliberately. The chrome around
the plot — the legend, the tooltip card, and whatever readouts you compose
alongside them — mirrors on its own, because it is ordinary flow layout built
from logical CSS utilities. The plot area does not.
That split is the point. recharts lays every mark out in physical SVG
coordinates, so a category axis that ran right-to-left would flip the reading
order of the data without flipping the geometry that encodes it — the bars, the
line, and the axis they belong to would stop agreeing. ChartContainer holds
the split in place by pinning .recharts-surface to direction: ltr.
That pin does real work; it is not a belt over an already-safe layout. recharts
puts each mark at a coordinate it has computed in physical space, but it
anchors the text with the direction-relative SVG keywords text-anchor: start
and end — see getTickTextAnchor in CartesianAxis, and its counterparts on
PolarAngleAxis and PolarRadiusAxis. The kit does the same where it sets an
anchor itself, for rotated X-axis ticks and for a Sankey node's label. Those two
keywords resolve against the inherited direction, so under an inherited
dir="rtl" the text mirrors about its anchor while the geometry it labels stays
put. Measured in Chromium against the built Storybook: end-anchored Y-axis
ticks move 24–39px and land inside the plot area, rotated X-axis ticks shift
about 15px, and a ReferenceLine label at the right edge overflows the surface.
Text anchored middle is direction-immune and measured a shift of exactly
zero — unrotated X-axis tick labels, axis titles and LabelList data labels are
all in that group.
The pin stopping at the surface is what keeps it safe: the tooltip and the
legend are HTML rendered outside .recharts-surface, so they never inherit it
and still mirror with the page.
The one thing inside the surface that is not SVG is the
Treemap's cell labels, which its cell renderer lays out
as HTML in a <foreignObject>. That is ordinary flow layout, not a computed SVG
coordinate, so the pin explicitly stops at the <foreignObject> boundary and
hands the page's direction back: under dir="rtl" a labelAlign start edge
resolves to the tile's right, mirroring with the page like every other block of
HTML in the kit.
This is what mainstream charting libraries do by default too: the plot keeps its
physical orientation and only the surrounding UI mirrors. The non-SVG widgets —
CategoryBar,
BarChart orientation="horizontal",
Metric, TrendIndicator —
sit outside the surface entirely, are plain DOM, and mirror fully instead.
If a chart genuinely has to read right-to-left, that is a decision about the
data, not about the page: reverse the category axis with recharts' own
reversed prop rather than expecting dir to do it.
API Reference
ChartContainer takes the chart config and the recharts plot as children:
Prop
Type
ChartLegendContent renders a circular dot marker for every series, matching
the indicator used in tooltip rows. Tooltip rows also use the same dot.
ChartTooltipContent and ChartLegendContent are passed to recharts' Tooltip /
Legend via their content prop. They accept the display options
hideLabel, hideIndicator, hideIcon, nameKey, and labelKey on top of
recharts' content props.
ChartTooltip and ChartLegend are direct re-exports of recharts' Tooltip and
Legend.