Radar

Radar charts plot several measures on spokes around a center, so the shape of one profile can be compared with another.

LLMs

Code

Radar charts are drawn with TanStack Charts, through the kit's Chart components. They paint the chart with the theme's colors, grid and type, and add three palette props. The chart itself is a TanStack definition, written the way TanStack documents it.

Install

npx antuikit@latest init
npx antuikit@latest add tanstack-chart
The first command installs AntuikitProvider and the theme tokens. The second adds the chart components and TanStack Charts itself.

Your first radar chart

Draw the shape with radialArea inside a polar container. angle names the spoke, on a point scale whose domain lists the spokes in order. radius names the value. A closed curve joins the last spoke back to the first.
import { defineChart } from "@tanstack/charts"
import { angleGrid, focusGroupAngle, polar, radialArea, radialGrid } from "@tanstack/charts/polar"
import { scaleLinear } from "@tanstack/charts/scales/linear"
import { scalePoint } from "@tanstack/charts/scales/point"
import { tooltip } from "@tanstack/charts/tooltip"
import { curveLinearClosed } from "d3-shape"
import { Chart, chartColors, seriesTooltip } from "@/components/ui/tanstack-chart"
const tickets = [
{ month: "January", resolved: 186 },
{ month: "February", resolved: 305 },
{ month: "March", resolved: 237 },
{ month: "April", resolved: 273 },
{ month: "May", resolved: 209 },
{ month: "June", resolved: 214 },
]
const months = tickets.map((row) => row.month)
const definition = defineChart(
{
marks: [
polar({
radiusRatio: 0.76,
scales: {
angle: { scale: scalePoint<string>().domain(months), wrap: true },
radius: { scale: scaleLinear().domain([0, 320]) },
},
guides: [
radialGrid({
values: [80, 160, 240, 320],
shape: "polygon",
stroke: chartColors.grid,
}),
angleGrid({
values: months,
labelOffset: 10,
labelFill: chartColors.label,
labelFontSize: 12,
stroke: chartColors.grid,
}),
],
marks: [
radialArea(tickets, {
id: "Resolved",
angle: "month",
radius: "resolved",
key: "month",
z: () => "Resolved",
curve: curveLinearClosed,
fill: chartColors.series[0],
fillOpacity: 0.6,
stroke: chartColors.series[0],
strokeWidth: 1,
}),
],
}),
],
scales: { x: null, y: null },
color: { domain: ["Resolved"] },
margin: 0,
},
{
focus: focusGroupAngle,
tooltip: { use: tooltip, content: seriesTooltip },
},
)
export function TicketsChart() {
return (
<Chart
definition={definition}
ariaLabel="Tickets resolved by month, January to June"
height={280}
/>
)
}
curveLinearClosed comes from d3-shape. Add it to your dependencies if your package manager does not let you import it through TanStack Charts. The chart fills its container's width, so give it a height. ariaLabel is required.

Which component

All three take the same definition and the same palette props.
Chart components

One scale for every spoke

All spokes share the radius scale. Fix its domain and start it at 0, so the center means zero on every spoke and the shape does not change when the data does.
When the measures have different units, rescale them first. TanStack Charts' fold, normalize and select turn wide rows into one value per spoke between 0 and 1. Negate a measure where lower is better, so outward is better on every spoke.
import { normalize, select } from "@tanstack/charts"
import { fold } from "@tanstack/charts/transform/fold"
const folded = fold(rows, {
fields: events,
as: { key: "event", value: "result" },
})
// Timed events are negated so a faster time scores higher.
const normalized = normalize(folded, {
by: "event",
value: (row) => (timedEvents.has(row.event) ? -row.result : row.result),
basis: "extent",
as: "relativePerformance",
})
const profile = select(normalized, { by: "event", select: "first" })
// Then: radius: { scale: scaleLinear().domain([0, 1]) }

Spoke order

The point scale's domain sets the order of the spokes, clockwise from 12 o'clock. Write it out, rather than reading it from the rows, so every radar you compare uses the same order. Keep wrap: true, which stops the first and last spokes from landing on the same angle.

Several series

Add one radialArea per series, each with its own id and z. The later mark is drawn on top, so give the shape in front a lower fillOpacity, or draw outlines only with a fillOpacity of 0 and a wider strokeWidth.
radialArea(energy, {
id: "North wing",
angle: "month",
radius: "north",
key: "month",
z: () => "North wing",
curve: curveLinearClosed,
fillOpacity: 0,
stroke: chartColors.series[0],
strokeWidth: 2,
})
When the series sit in one long array, use one mark and set color to the group field. Each group gets its own shape. Make the key unique across groups, such as `${row.Country}:${row.event}`.

Grid and labels

  • radialGrid draws the rings. shape is "polygon" for straight rings or "circle" for round ones. fill and fillOpacity tint them.
  • angleGrid draws the spokes and their names. A stroke of "transparent" hides the spokes and keeps the names.
  • To print the ring values, set labels: true on radialGrid, and place them along one spoke with labelAngle and labelRotate.
  • Labels outside the circle need room. Lower radiusRatio, or compute it from the chart size by passing defineChart a function of { width, height }.
  • Add a radialDot on the same angle and radius to mark each value.

Colors

The chart takes the theme's chart colors without any setup. Change the palette on the chart itself, or paint one mark with fill and chartColors. The Color guideline covers which palette fits which data.
<Chart definition={definition} ariaLabel="..." palette="accent" />
// Name each series in the color domain, in the order of the legend.
color: { domain: ["Online", "In store"] }
Palette props
Taken by Chart, RendererChart and CanvasChart. Rows marked antuikit in the Version column are added by the kit.

Tooltip and focus

focus: focusGroupAngle makes one spoke the unit of focus, so the tooltip lists every series on it and the arrow keys move from spoke to spoke. seriesTooltip from the kit formats it: the spoke as the title, then one row per series. Add sort: "color-domain" to the tooltip options to list the series in the order of the color domain.

Animation

Use RendererChart with TanStack's motion() renderer to animate marks in and between data updates. It skips the animation when the reader has asked for reduced motion.
import { motion } from "@tanstack/charts/motion"
import { RendererChart } from "@/components/ui/tanstack-chart"
const renderer = motion({ initial: "always" })
<RendererChart definition={definition} renderer={renderer} ariaLabel="..." />

API

The options a radar chart uses most. TanStack Charts' documentation has the full reference.
radialArea
radialGrid and angleGrid
polar
Angle and radius scales