Acronis UIKit
Components

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.

recharts resolves as a dependency of @acronis-platform/ui-react; import the chart primitives directly from recharts.

Palettes

Four palettes, all resolving to the --ui-dataviz-* tokens. Pick by what the color has to communicate:

paletteUse it whenStops
{ 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 the status palette, 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. All side: 'a' series share the a-hue stops (a3 → a2 → a1, strongest-first), and all side: 'b' series share the b-hue stops likewise. Series with no side walk 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 (default true) — hide either axis (its ticks and title). Useful for compact/sparkline layouts.
  • xTickFormatter / yTickFormatter — format each tick value. Unlike the unit suffix (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:

  • formatCompactNumber146500 → "146.5K", 1_500_000 → "1.5M".
  • formatPercent41.8 → "41.8%".
  • createTickFormatter(options) — a factory over Intl.NumberFormat options (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 with xAxisLabel; 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 (0 shows every tick, 1 every other one, 2 every third).
  • yAxisTickCount — a hint for how many value-axis ticks to draw.
  • yAxisDomainauto (fit the data at both ends; the axis need not include 0), zero (anchor at 0), or dataMin-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 — on BarChart, LineChart, AreaChart and ComposedChart, 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 a name field 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-valuenow is 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.

Edit on GitHub

On this page