RadarChart
A typed radar chart for comparing entities across shared quantitative axes.
Usage
import { RadarChart } from '@acronis-platform/ui-react';
import type { ChartConfig } from '@acronis-platform/ui-react';RadarChart is a typed composition over the shared Chart
primitives. Give it data, a per-series config, the series to plot
(dataKeys), and the angular axis key (angleKey) — it renders a themed recharts
radar (polar grid, spoke labels, tooltip, legend included), so you don't
hand-compose recharts children. Its default web uses outerRadius={78} to match
the Figma geometry. The legend and value labels do not change the polygon radius;
outer labels stay 4px from the polygon vertices.
Series are columns of a shared row (like a bar or line chart), plotted over one
polar angular axis. The plot surface is 187px high by default (219px with the
legend), and the web is centred on that plot band rather than on the whole box —
so the bottom category label clears the legend instead of disappearing behind it.
Turning on showLabels widens the gap between the polygon and the category
labels, and the box grows with it (240px, or 272px with the legend). Pass a
height class via className to override any of it (e.g. className="h-[380px]").
Series colors come from the palette prop, not from config — see
Palettes. Each config entry maps a data key to a
label and an optional tone that re-points that one series within the palette,
resolved into the --color-<key> custom properties the areas stroke and fill
from.
Examples
Two series compared across shared axes, with a themed tooltip and legend:
const data = [
{ subject: 'Math', alice: 120, bob: 110 },
{ subject: 'English', alice: 86, bob: 130 },
{ subject: 'Physics', alice: 85, bob: 90 },
{ subject: 'History', alice: 65, bob: 85 },
];
const config = {
alice: { label: 'Alice' },
bob: { label: 'Bob' },
} satisfies ChartConfig;
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
className="h-[380px] w-[420px]"
/>;Draw the web as smooth circles instead of straight polygon rings with
gridType="circle":
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
gridType="circle"
/>The chart shows data-point dots by default, matching the Figma design. Tune the
areas with fillOpacity / strokeWidth / showDots (sized by dotRadius), and
toggle the chrome with showGrid / showTooltip / showLegend. The legend is
always rendered below the chart.
The value scale
A radar has no scale of its own by default: recharts stretches the web so the
largest value in the data touches the outer ring, which makes an area readable
only against its neighbours — two charts of the same metric aren't comparable.
showRadiusAxis draws the scale, and radiusAxisDomain="fixed" with
radiusAxisDomainMax pins the outer ring to the metric's own maximum, so the
areas read as absolute profiles:
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
showRadiusAxis
radiusAxisAngle={60}
radiusAxisDomain="fixed"
radiusAxisDomainMax={150}
radiusAxisTickCount={4}
/>radiusAxisAngle rotates the scale. Point it between two spokes: along one,
its outermost tick lands on that category's own label. The ticks sit over the
areas at any angle — a radar's plot area is its areas — so the angle buys
clearance from the category labels, not from the fills.
radiusAxisOrientation picks the side its labels sit on, and
radiusAxisTickCount sets how many ticks it asks for — which is also how many
rings the grid draws, shown or not.
radiusAxisDomainMax has to be positive: a 0 or negative maximum can't bound a
scale growing outward from 0, so it falls back to the data's own top rather than
collapsing the web.
The domain applies whether or not the scale is drawn, so a chart can be scaled to a known maximum without showing the ticks:
<RadarChart
config={config}
data={data}
dataKeys={['alice']}
angleKey="subject"
radiusAxisDomain="fixed"
radiusAxisDomainMax={150}
/>For a metric where less is better (latency, error rate), radiusAxisReversed
puts the maximum at the centre so a good profile still reads as a large area.
Grid and angle axis
radialLines={false} drops the web's spokes and keeps its concentric rings.
The categorical axis around the web is configurable too — angleAxisOrientation
moves its labels inside the web, angleAxisLineType matches its outline to a
circular grid, and angleAxisLine / angleTickLine / angleTickSize control the
rest:
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
gridType="circle"
angleAxisLineType="circle"
angleAxisOrientation="inner"
angleTickLine={false}
/>showAngleAxis={false} hides all of that chrome at once. The axis itself stays —
it's also what names each category — so the tooltip keeps its header.
Geometry
cx / cy move the centre, startAngle / endAngle rotate the sweep,
innerRadius / outerRadius size the web (an inner radius separates the series
where they all bottom out), and margin insets the plot area:
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
innerRadius={40}
outerRadius="70%"
startAngle={45}
endAngle={-315}
/>Passing margin replaces all four sides — a side you leave out is 0, not the
default 5 — so name every side you need.
Per-series styling
seriesSettings overrides the chart-level styling for one series, keyed by its
dataKeys entry — color, stroke, fillOpacity, strokeWidth, dot,
dotRadius, and activeDot. Every series not named keeps the chart-level
values, and an overridden color carries into that series' legend swatch and
tooltip dot — as does stroke on its own, which recharts reads first, so set
color too if the marker should keep following the fill:
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
seriesSettings={{
alice: { fillOpacity: 0.05, strokeWidth: 3, dot: true, dotRadius: 4 },
}}
/>Every plotted series needs a value in every row. recharts places a missing one at the centre of the web rather than leaving a hole, so it reads as a zero — drop the whole category row instead of leaving a series' value out of it.
Custom tooltip
Replace the tooltip with a configured ChartTooltipContent — imported from the
same library, so you never compose recharts yourself. Its formatter renders
each row and labelFormatter the header, giving you custom formatting, per-series
content, and extra fields:
import { RadarChart, ChartTooltipContent } from '@acronis-platform/ui-react';
<RadarChart
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
tooltipContent={
<ChartTooltipContent
formatter={(value, name) => `${name}: ${value.toLocaleString()}`}
/>
}
/>;Animation
Charts render statically by default. Opt in to an entrance animation with
animate, and tune it with animationDuration / animationBegin /
animationEasing:
<RadarChart
config={config}
data={data}
dataKeys={['alice']}
angleKey="subject"
animate
animationDuration={800}
animationEasing="ease-out"
/>animate honors prefers-reduced-motion: for a visitor who has asked their
system to reduce motion, the series render at their final geometry with no
animation (the same applies when rendering on the server).
Data labels
Annotate each point with its value using showLabels. labelFormatter takes the
same formatters as the axes, so a label and its axis read alike:
<RadarChart
config={config}
data={data}
dataKeys={['alice']}
angleKey="subject"
showLabels
labelFormatter={formatCompactNumber}
/>When value labels are on, the category labels around the web are automatically
pushed further out so the two don't overlap at the top vertex. Pass an explicit
angleTickSize to override.
labelPosition moves them; the default is top, which sits each value above its
vertex on the chart surface. An inside* position draws it over the area
instead. An area is a translucent fill, so what sits behind the label there is
the surface tinted by the series color rather than the color itself — the label
keeps the theme-inverting on-surface token, which stays legible over the tint in
both themes:
<RadarChart
config={config}
data={data}
dataKeys={['alice']}
angleKey="subject"
showLabels
labelPosition="insideEnd"
/>Labels are off by default — with more than one series they collide easily, so turn them on for a single series or a chart with room to spare.
Accessibility
RadarChart sets no role and no aria-* of its own, and the SVG the web is drawn
into has no accessible name. That gap belongs to this component rather than
recharts: recharts takes title, desc and role as first-class chart props and
writes them straight into the plot's <svg>, but RadarChart passes none of them
down — it spreads the props you give it onto its own root <div> and hands the
recharts chart only the data it computes itself, so the <title> / <desc> pair
recharts always emits stays empty. recharts' accessibility layer is on by default
and is not switched off here, so the plot is also a tab stop carrying
role="application": a focus stop that announces neither a name nor any data. The
component exposes no way to turn that off.
So put the name on the root, which spreads the props you pass. Choose the role
before copying the snippet: role="img" names the chart but also makes its subtree
presentational, and it does not remove the focusable plot inside it — wrapping the
chart in a <figure> with a <figcaption> is often the better fit here.
aria-labelledby works the same way as aria-label.
<RadarChart
role="img"
aria-label="Scores by subject — Alice: Math 120, English 86, Physics 85, History 65. Bob: Math 110, English 130, Physics 90, History 85."
config={config}
data={data}
dataKeys={['alice', 'bob']}
angleKey="subject"
/>Either way, a name is not a text alternative. A radar is a visual encoding of
numbers, so the numbers themselves have to be reachable as text — a caption, a
summary sentence, or a table beside the chart. The legend and the tooltip are
ordinary HTML laid out beside the SVG rather than inside it, so the legend already
names every series in the document. The spoke labels the angle axis draws — and the
radius-axis ticks when showRadiusAxis is on, and the data labels when showLabels
is — are real <text> rather than pixels, but they sit in the SVG with no structural
role, so a category and its value are never associated with each other there.
Direction (RTL)
Under dir="rtl" the chrome around the plot — the legend, the tooltip, and any
readouts you compose alongside them — mirrors on its own, because it is ordinary
flow layout built from logical CSS utilities. The plot area deliberately stays
left-to-right: recharts places every mark in physical SVG coordinates, so mirroring
it would flip the reading order of the data without flipping the geometry that
encodes it. ChartContainer holds that in place by pinning .recharts-surface to
direction: ltr. The web's startAngle / endAngle sweep and the order the
categories are placed in don't depend on the pin — an angle is not a reading
direction.
The labels around the web do. PolarAngleAxis anchors each spoke label with the
direction-relative SVG keywords text-anchor: start / end, chosen from which side
of the centre the spoke falls on, and PolarRadiusAxis does the same for its ticks
when showRadiusAxis is on. Without the pin an inherited dir="rtl" would mirror
each of those across its own anchor and push it back over the web. The spoke labels
at the very top and bottom are anchored middle and would not move, which is what
makes the failure look like a partial one. The showLabels values sit at
labelPosition="top" by default, also middle, and are direction-immune either
way. The pin stops at the surface, so the legend and the tooltip — HTML outside
it — still mirror with the page.
API Reference
Prop
Type