Radar Chart

Several measures of one thing, drawn as one shape.

'use client';

import { RadarChart } from '@/components/panelui/radar-chart';
import { Frame } from '@/components/panelui/frame';

const SCORES = [
  { skill: 'Speed', you: 82, team: 64 },
  { skill: 'Reach', you: 54, team: 72 },
  { skill: 'Depth', you: 71, team: 58 },
  { skill: 'Cost', you: 38, team: 66 },
  { skill: 'Care', you: 64, team: 49 },
  { skill: 'Risk', you: 46, team: 30 },
];

export default function RadarChartOverview() {
  return (
    <Frame variant="band" className="w-full max-w-sm">
      <Frame.Header>
        <Frame.Title>Your profile</Frame.Title>
        <Frame.Action>Q3</Frame.Action>
      </Frame.Header>
      <Frame.Panel className="p-5">
        <RadarChart data={SCORES} axisKey="skill" domain={[0, 100]} label="Your scores out of 100">
          <RadarChart.Grid />
          <RadarChart.Axis />
          <RadarChart.Series dataKey="you" name="You" showDots />
          <RadarChart.Tooltip />
        </RadarChart>
      </Frame.Panel>
    </Frame>
  );
}

Use it to show the profile of one thing across several measures, and to lay two profiles over each other. Each row of data is an axis, and each RadarChart.Series is a shape drawn from one of its keys.

The order of the axes changes the outline, and the outline is what people read. Keep the order the same between charts you compare. Three axes is the minimum, and past about eight the labels start to collide.

For data whose axes have no natural order, use a Bar Chart.

Installation

npx shadcn@latest add https://panelui.dev/r/web/radar-chart.json

The source is copied into components/panelui/, where you can edit it.

Usage

import { RadarChart } from "@/components/panelui/radar-chart"
<RadarChart data={scores} axisKey="skill" domain={[0, 100]}>
  <RadarChart.Grid />
  <RadarChart.Axis />
  <RadarChart.Series dataKey="you" showDots />
  <RadarChart.Tooltip />
</RadarChart>

Composition

<RadarChart data={…}>
  <RadarChart.Header />
  <RadarChart.Grid />
  <RadarChart.Axis />
  <RadarChart.Series dataKey="…" />
  <RadarChart.Tooltip />
  <RadarChart.Legend />
</RadarChart>
  • RadarChart.Header — The strip above the rings: title, value and caption, with the series key or your own control on the trailing side.
  • RadarChart.Grid — The rings and the spokes. Polygons by default, and circles with circular.
  • RadarChart.Axis — The axis names around the rings, each running away from the shape.
  • RadarChart.Series — One shape. Declare one per key. Later ones draw on top.
  • RadarChart.Tooltip — The reading for the nearest axis: the spoke lit, a dot per series, and each value in a readout. Without this part the chart does not show a reading.
  • RadarChart.Legend — A swatch and a name per series, in the bottom-left corner of the plot. On a chart with a header, use the header's legend prop instead.

Examples

Two profiles

Add a Series per key. Fill only the first with fillOpacity={0} on the others, because two translucent fills make a third colour that means nothing.

'use client';

import { RadarChart } from '@/components/panelui/radar-chart';

const SCORES = [
  { skill: 'Speed', you: 82, team: 64 },
  { skill: 'Reach', you: 54, team: 72 },
  { skill: 'Depth', you: 71, team: 58 },
  { skill: 'Cost', you: 38, team: 66 },
  { skill: 'Care', you: 64, team: 49 },
  { skill: 'Risk', you: 46, team: 30 },
];

export default function RadarChartProfiles() {
  return (
    <RadarChart data={SCORES} axisKey="skill" domain={[0, 100]} className="max-w-sm">
      <RadarChart.Header title="Against the team" legend />
      <RadarChart.Grid />
      <RadarChart.Axis />
      <RadarChart.Series dataKey="you" name="You" />
      <RadarChart.Series dataKey="team" name="Team" fillOpacity={0} />
      <RadarChart.Tooltip />
    </RadarChart>
  );
}

Fix the scale

Pass domain when there is a known maximum. A scale derived from the data moves with the data, so two charts stop being comparable and a small value can look large. A value past the domain draws past the outer ring.

'use client';

import { RadarChart } from '@/components/panelui/radar-chart';

const LOW = [
  { axis: 'North', value: 12 },
  { axis: 'East', value: 30 },
  { axis: 'South', value: 22 },
  { axis: 'West', value: 8 },
  { axis: 'Up', value: 18 },
];

export default function RadarChartDomain() {
  return (
    <div className="grid w-full max-w-2xl gap-4 sm:grid-cols-2">
      <RadarChart data={LOW} size={140}>
        <RadarChart.Header title="From the data" />
        <RadarChart.Grid />
        <RadarChart.Axis />
        <RadarChart.Series dataKey="value" />
      </RadarChart>
      <RadarChart data={LOW} size={140} domain={[0, 100]}>
        <RadarChart.Header title="Out of 100" />
        <RadarChart.Grid />
        <RadarChart.Axis />
        <RadarChart.Series dataKey="value" />
      </RadarChart>
    </div>
  );
}

Circular rings

circular draws the rings as circles, for reading a value off one spoke rather than comparing outlines. spokes={false} removes the spokes.

'use client';

import { RadarChart } from '@/components/panelui/radar-chart';

const SCORES = [
  { skill: 'Speed', you: 82, team: 64 },
  { skill: 'Reach', you: 54, team: 72 },
  { skill: 'Depth', you: 71, team: 58 },
  { skill: 'Cost', you: 38, team: 66 },
  { skill: 'Care', you: 64, team: 49 },
  { skill: 'Risk', you: 46, team: 30 },
];

export default function RadarChartCircular() {
  return (
    <RadarChart data={SCORES} axisKey="skill" domain={[0, 100]} className="max-w-sm">
      <RadarChart.Grid circular />
      <RadarChart.Axis />
      <RadarChart.Series dataKey="you" color="chart-2" showDots />
      <RadarChart.Tooltip />
    </RadarChart>
  );
}

Without labels

compact drops the room kept for axis labels, for a small chart beside a figure. Pair it with no Axis.

'use client';

import { RadarChart } from '@/components/panelui/radar-chart';

const SCORES = [
  { skill: 'Speed', you: 82, team: 64 },
  { skill: 'Reach', you: 54, team: 72 },
  { skill: 'Depth', you: 71, team: 58 },
  { skill: 'Cost', you: 38, team: 66 },
  { skill: 'Care', you: 64, team: 49 },
  { skill: 'Risk', you: 46, team: 30 },
];

export default function RadarChartCompact() {
  return (
    <RadarChart data={SCORES} axisKey="skill" domain={[0, 100]} size={120} compact className="max-w-40">
      <RadarChart.Grid rings={2} spokes={false} />
      <RadarChart.Series dataKey="you" />
    </RadarChart>
  );
}

Changing data

When data changes, each vertex travels to its new place. Keep domain fixed across the change, or the rings move too and nothing is comparable. A ref on the chart has replay(), which grows the shape again from the centre.

'use client';

import { useRef, useState } from 'react';
import { RadarChart, type RadarChartHandle } from '@/components/panelui/radar-chart';
import { Button } from '@/components/panelui/button';

const Q3 = [
  { skill: 'Speed', score: 82 },
  { skill: 'Reach', score: 54 },
  { skill: 'Depth', score: 71 },
  { skill: 'Cost', score: 38 },
  { skill: 'Care', score: 64 },
];
const Q4 = [
  { skill: 'Speed', score: 46 },
  { skill: 'Reach', score: 88 },
  { skill: 'Depth', score: 60 },
  { skill: 'Cost', score: 74 },
  { skill: 'Care', score: 30 },
];

export default function RadarChartChangingData() {
  const chart = useRef<RadarChartHandle>(null);
  const [later, setLater] = useState(false);

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-4">
      <RadarChart ref={chart} data={later ? Q4 : Q3} axisKey="skill" domain={[0, 100]}>
        <RadarChart.Grid />
        <RadarChart.Axis />
        <RadarChart.Series dataKey="score" showDots />
      </RadarChart>
      <div className="flex gap-2">
        <Button variant="outline" onClick={() => setLater((current) => !current)}>
          {later ? 'Show Q3' : 'Show Q4'}
        </Button>
        <Button variant="ghost" onClick={() => chart.current?.replay()}>
          Replay
        </Button>
      </div>
    </div>
  );
}

API Reference

RadarChart

PropTypeDefaultDescription
datareadonly RadarChartDatum[]—One row per axis, in the order they go round, with one key per series.
axisKeystring'axis'The key holding each row's axis label.
statusRadarChartStatus'ready'loading draws the rings and no shape, and the shapes grow when it turns ready.
sizenumber | null180Ring diameter in pixels, with room for the labels added around it. Pass null to fill the width at aspectRatio.
aspectRationumber1Width over height of the plot when size is null.
domainreadonly [number, number]—The values at the centre and at the outer ring. Defaults to zero and just above the largest value.
animationDurationnumber620Milliseconds for the shapes to grow.
compactbooleanfalseDrop the room for axis labels, for a chart without them.
labelstring—Names the chart for screen readers.
labelForDatum(datum: RadarChartDatum, index: number) => string—Writes one axis for screen readers, for values with a unit.

RadarChart.Grid

PropTypeDefaultDescription
ringsnumber—How many rings, including the outermost.
colorRadarChartColor—Which of the theme's roles to draw it in. Defaults to the border colour.
circularboolean—Draw the rings as circles rather than polygons through the spokes.
spokesboolean—Draw a line from the centre to the rim along each axis.

RadarChart.Axis

PropTypeDefaultDescription
colorRadarChartColor—Which of the theme's roles to draw the labels in. Defaults to the muted text colour.
fontSizenumber—Label size in pixels.
offsetnumber—Gap between the outer ring and the label, in pixels.
formatLabel(label: string, index: number) => string—Rewrite a label, usually to shorten it.

RadarChart.Series

PropTypeDefaultDescription
dataKeystring—The key in each row holding this series' value.
namestring—What the legend, the readout and screen readers call it. Defaults to dataKey.
colorRadarChartColor—Which of the theme's roles to draw it in. Defaults to the chart palette, in the order the series are declared.
strokeWidthnumber—Outline width in pixels.
fillOpacitynumber—Opacity of the fill. Pass 0 for an outline only.
showDotsboolean—Draw a dot at each vertex.

RadarChart.Tooltip

PropTypeDefaultDescription
formatValue(value: number, dataKey: string) => string—Format a value. Defaults to a compact number.
classNamestring—Added to the readout.

RadarChart.Legend

PropTypeDefaultDescription
formatName(dataKey: string) => string—Rewrite a series' name.

RadarChart.Header

PropTypeDefaultDescription
titleReactNode—Small line above the value: what the chart is of.
valueReactNode—The readout. The largest thing on the card, and the first thing read.
captionReactNode—One muted line under the value: a period, a comparison, a caveat.
legendboolean—Draw a swatch and a name per series along the trailing edge.

RadarChart, RadarChart.Header and RadarChart.Legend also take the props of the element they render, and a className. RadarChart.Tooltip takes a className for the readout. RadarChart.Grid, RadarChart.Axis and RadarChart.Series take only the props listed.

Notes

Sizing

size is the diameter of the outer ring, 180 pixels by default, and the plot adds room for the labels around it, so it is wider than it is tall. Pass size={null} to fill the container's width at aspectRatio. The drawing happens in the browser after it has been measured, so it is not in server-rendered HTML.

For labels that still crowd the shape, shorten them with formatLabel. A label with no room left ends in an ellipsis.

Colour

color on a series names a role in the theme. Without it, series take the chart colours in the order they are declared.

Missing values

A row without a number for a series is left out of that series' outline, and the outline joins the axes either side. A series with fewer than three numbers draws nothing.

Reading an axis

With RadarChart.Tooltip, hovering inside the rings reads the nearest axis. A click or a tap holds the reading until you press that axis again or press outside the rings. A touch drag does not read axes, so the page can still scroll.

Keyboard

The plot takes focus. The arrow keys move round the axes, Enter or Space holds the reading, and Escape clears it. Moving to another axis lets go of a held reading.

Screen readers

Each axis is given as text beside the drawing, with every series' value, and the reading is announced as it changes. label names the chart and labelForDatum writes an axis, for values with a unit.

Loading

With status="loading" the rings are drawn with no shape, and the shapes grow when it turns ready.

Reduced motion

With reduced motion set, the shapes appear at their full size, and a data change lands in one step.

Server Components

RadarChart is a client component. In a file that starts with "use client", write its parts with a dot, as on this page. A Server Component cannot read RadarChart.Header off the import, so each part is also exported by name: RadarChartHeader is RadarChart.Header, and so on for the others.

import { RadarChart, RadarChartHeader, RadarChartGrid, RadarChartAxis, RadarChartSeries, RadarChartTooltip, RadarChartLegend } from "@/components/panelui/radar-chart"

On this page