Skip to content
UI Kit
Accent color

Radar chart

Several measures of one thing on spokes around a center, as one profile, a comparison, a linked list, small multiples on one scale or against a target. Arrow keys walk around the spokes.

One of 4 components in Statistical charts

5 variants

Variants

Profile

One shape on rings from zero, every spoke answers the pointer with its value.

Open in Studio

Compare

Two shapes on one web. A key in the legend brings its shape forward, the tooltip lists both values per spoke.

Open in Studio

With list

The shape and a list of the spokes with bars side by side, stacked on phones. Pointing at either lights up the same spoke.

Open in Studio

Small multiples

One small web per item on the same scale, a key with the spoke names in front, the average next to each name.

Open in Studio

Against a target

The shape against a dashed target outline. Spokes below the target get a hollow dot, the count stands above.

Open in Studio

Details

What Radar chart brings along, read from the registry and the source.

Variants
5
Version
1.0.0 · since 1.1.0
File
components/ui/radar-chart.tsx
Brings along
utilskitchartchart-figure
npm packages
None
Languages
English and German, 6 texts of its own

Props

The RadarChartProps interface from components/ui/radar-chart.tsx, with the comments from the code. Also takes every prop of Omit<ChartA11y, 'label'>, ChartFigureProps.

  • labeloptional
    string

    Accessible name of the chart. Default: the title.

  • variantoptional
    RadarVariant

    profilecomparewith-listmultiplestarget

  • axesrequired
    string[]

    Names of the spokes, clockwise from twelve o'clock. Five to eight work best.

  • seriesrequired
    RadarSeries[]

    profile, with-list, target: the first series. compare: two. multiples: one per small chart.

  • targetoptional
    number[]

    target: one target value per axis

  • maxoptional
    number

    Top of the scale. Default: a round value above the largest value.

  • formatoptional
    Intl.NumberFormatOptions

    Value format, e.g. { maximumFractionDigits: 1 }

  • targetLabeloptional
    string

    Name of the target outline in legend and tooltip

Built-in texts

Radar chart writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.

Show all 6 texts
avgEnglish avg. {v}German Ø {v}
targetEnglish TargetGerman Ziel
keyEnglish How to readGerman So lesen
ofEnglish of {max}German von {max}
axisEnglish AxisGerman Achse
gapEnglish {n} of {total} below targetGerman {n} von {total} unter Ziel

Accessibility

Read from the source of the component.

  • Radar chart handles the keys Escape, Arrow up, Arrow down, Arrow left, Arrow right, Home and End itself.
  • Sets aria-label and aria-pressed, so screen readers announce purpose and state.
  • Uses the role list.
  • Adds text for screen readers where icons and graphics stand alone.
  • Needs no client state. The server writes it in full, so it reads without JavaScript too.

Motion

  • With reduced motion (prefers-reduced-motion) it stays still. 1 places in the code switch motion off then.
  • No animations, it stands still from the start.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 402 lines
components/ui/radar-chart.tsx
'use client';

import { useState } from 'react';
import { cn, round, useTexts } from '@/lib/kit';
import { ChartRoot, ChartSurface, ChartTable, ChartTooltip, Swatch, fill, niceTicks, numberFormatter, polar, textWidth, useEntrance, useIntlLocale, useShown, useWidth, type ChartA11y, type TooltipRow } from '@/components/ui/chart';
import { ChartFigure, HALO, PickLegend, type ChartFigureProps } from '@/components/ui/chart-figure';

/*
 * Several measures of one thing on spokes around a center.
 *   profile    one shape, every spoke readable
 *   compare    two shapes, the legend brings one forward
 *   with-list  the shape and a list of the spokes with bars, both linked
 *   multiples  one small shape per item on the same scale, a key in front
 *   target     the shape against a target outline, misses marked
 *
 * All spokes share one scale from zero, so shapes stay comparable. The
 * pointer picks the nearest spoke by angle, arrow keys step around.
 * Readable without the shape: every value is in the table and the list.
 */

export type RadarVariant = 'profile' | 'compare' | 'with-list' | 'multiples' | 'target';

export interface RadarSeries {
  label: string;
  /** One value per axis, in axis order */
  values: number[];
}

export interface RadarChartProps extends Omit<ChartA11y, 'label'>, ChartFigureProps {
  /** Accessible name of the chart. Default: the title. */
  label?: string;
  variant?: RadarVariant;
  /** Names of the spokes, clockwise from twelve o'clock. Five to eight work best. */
  axes: string[];
  /** profile, with-list, target: the first series. compare: two. multiples: one per small chart. */
  series: RadarSeries[];
  /** target: one target value per axis */
  target?: number[];
  /** Top of the scale. Default: a round value above the largest value. */
  max?: number;
  /** Value format, e.g. { maximumFractionDigits: 1 } */
  format?: Intl.NumberFormatOptions;
  /** Name of the target outline in legend and tooltip */
  targetLabel?: string;
  className?: string;
}

const TEXTS = {
  en: { avg: 'avg. {v}', target: 'Target', key: 'How to read', of: 'of {max}', axis: 'Axis', gap: '{n} of {total} below target' },
  de: { avg: 'Ø {v}', target: 'Ziel', key: 'So lesen', of: 'von {max}', axis: 'Achse', gap: '{n} von {total} unter Ziel' },
};

const shapePath = (cx: number, cy: number, r: number, values: number[], max: number) =>
  values.map((v, i) => polar(cx, cy, (Math.max(0, Math.min(v, max)) / max) * r, (360 * i) / values.length)).map(([x, y], i) => `${i ? 'L' : 'M'}${x} ${y}`).join('') + 'Z';

const ringPath = (cx: number, cy: number, r: number, n: number) =>
  Array.from({ length: n }, (_, i) => polar(cx, cy, r, (360 * i) / n)).map(([x, y], i) => `${i ? 'L' : 'M'}${x} ${y}`).join('') + 'Z';

const SERIES_COLOR = ['var(--kitch-1)', 'var(--kitch-2)'];

/** Breaks a spoke label into lines no wider than maxW */
function wrap(text: string, maxW: number, size: number) {
  // Breaks at spaces and after hyphens (Preis-Leistung)
  const words = text.split(/ |(?<=-)/);
  const lines: string[] = [];
  let cur = '';
  for (const w of words) {
    const next = cur ? (cur.endsWith('-') ? cur + w : `${cur} ${w}`) : w;
    if (cur && textWidth(next, size) > maxW) {
      lines.push(cur);
      cur = w;
    } else cur = next;
  }
  if (cur) lines.push(cur);
  return lines;
}

interface RadarGeometry {
  width: number;
  height: number;
  cx: number;
  cy: number;
  r: number;
  font: number;
  lines: string[][];
}

/** Lays the web into a box of the given width: labels wrap, the radius takes what is left */
function geometry(axes: string[], width: number, { font = 12, maxR = 170, extraLine = false, minR = 48 } = {}): RadarGeometry {
  const n = axes.length;
  const maxW = width < 420 ? 76 : 120;
  const lines = axes.map((a) => (a ? wrap(a, maxW, font) : []));
  const lh = font + 3;
  let side = 0;
  let topPad = 10;
  let bottomPad = 10;
  axes.forEach((_, i) => {
    if (!lines[i].length) return;
    const deg = (360 * i) / n;
    const h = (lines[i].length + (extraLine ? 1 : 0)) * lh;
    const w = Math.max(...lines[i].map((l) => textWidth(l, font)));
    if (deg === 0) topPad = Math.max(topPad, h + 14);
    else if (deg === 180) bottomPad = Math.max(bottomPad, h + 14);
    else {
      side = Math.max(side, w + 16);
      // Labels near the top and bottom also need vertical room
      const c = Math.cos((deg * Math.PI) / 180);
      if (Math.abs(c) > 0.5) {
        if (c > 0) topPad = Math.max(topPad, h / 2 + 8);
        else bottomPad = Math.max(bottomPad, h / 2 + 8);
      }
    }
  });
  const r = Math.max(minR, Math.min(maxR, (width - 2 * side) / 2));
  return { width, height: Math.round(topPad + 2 * r + bottomPad), cx: Math.round(width / 2), cy: Math.round(topPad + r), r: Math.round(r), font, lines };
}

interface PlotProps {
  g: RadarGeometry;
  axes: string[];
  series: RadarSeries[];
  target?: number[];
  max: number;
  rings: number[];
  labels: boolean;
  active: number | null;
  focus: number | null;
  anim: boolean;
  fmt: (n: number) => string;
  ringLabels?: boolean;
  /** Value of the first series under each spoke label */
  values?: boolean;
  /** Only the web with its labels (key of the multiples) */
  bare?: boolean;
}

/** The web, rings, spokes, shapes and labels as one SVG */
function RadarPlot({ g, axes, series, target, max, rings, labels, active, focus, anim, fmt, ringLabels, values, bare }: PlotProps) {
  const n = axes.length;
  const { cx, cy, r } = g;
  const delay = (ms: number) => (anim ? ({ '--kitch-d': `${ms}ms` } as React.CSSProperties) : undefined);
  const lh = g.font + 3;
  // Ring values sit between the first two spokes, where shapes cross least
  const ringDeg = 180 / n;
  return (
    <svg width="100%" height={g.height} viewBox={`0 0 ${g.width} ${g.height}`} aria-hidden className="block overflow-visible">
      <g className={anim ? 'kitch-fade' : undefined}>
        {rings.map((v) => (
          <path key={v} d={ringPath(cx, cy, (v / max) * r, n)} fill="none" stroke={v === max ? 'var(--kitch-axis)' : 'var(--kitch-grid)'} strokeWidth="1" />
        ))}
        {axes.map((_, i) => {
          const [x, y] = polar(cx, cy, r, (360 * i) / n);
          return <line key={i} x1={cx} y1={cy} x2={x} y2={y} stroke={active === i ? 'var(--kitch-axis)' : 'var(--kitch-grid)'} strokeWidth={active === i ? 1.5 : 1} className="transition-[stroke] duration-200" />;
        })}
      </g>
      {target && !bare && (
        <path d={shapePath(cx, cy, r, target, max)} fill="none" stroke="var(--foreground)" strokeOpacity="0.7" strokeWidth="1.5" strokeDasharray="4 4" strokeLinejoin="round" className={anim ? 'kitch-fade' : undefined} style={delay(500)} />
      )}
      {!bare &&
        [...series.keys()].reverse().map((si) => {
          const s = series[si];
          const color = SERIES_COLOR[Math.min(si, 1)];
          const dim = focus !== null && focus !== si;
          return (
            <g key={si} className="kitch-series" opacity={dim ? 0.18 : 1}>
              <g className={anim ? 'kitch-pop' : undefined} style={{ ...delay(140 + si * 180), transformOrigin: `${cx}px ${cy}px`, transformBox: 'view-box' }}>
                <path d={shapePath(cx, cy, r, s.values, max)} fill={color} fillOpacity={si === 0 ? 0.16 : 0.1} stroke={color} strokeWidth="2" strokeLinejoin="round" />
              </g>
              {s.values.map((v, i) => {
                const [x, y] = polar(cx, cy, (Math.max(0, Math.min(v, max)) / max) * r, (360 * i) / n);
                const miss = target && si === 0 && v < target[i];
                return (
                  <circle key={i} cx={x} cy={y} r={active === i ? 5.5 : 4} fill={miss ? 'var(--kitch-surface)' : color} stroke={miss ? color : 'var(--kitch-surface)'} strokeWidth="2" className={cn('transition-[r] duration-200', anim && 'kitch-pop')} style={delay(500 + i * 50)} />
                );
              })}
            </g>
          );
        })}
      {ringLabels &&
        r >= 90 &&
        rings.map((v) => {
          const [x, y] = polar(cx, cy, (v / max) * r, ringDeg);
          return (
            <text key={v} x={x + 4} y={y} dy="0.34em" className="text-[10px] tabular-nums" fill="var(--muted-foreground)" style={HALO}>
              {fmt(v)}
            </text>
          );
        })}
      {labels &&
        axes.map((_, i) => {
          const deg = (360 * i) / n;
          const [x, y] = polar(cx, cy, r + 10, deg);
          const anchor = deg === 0 || deg === 180 ? 'middle' : deg < 180 ? 'start' : 'end';
          const lines = g.lines[i];
          const count = lines.length + (values ? 1 : 0);
          // Block of lines: above the top spoke, below the bottom one, centered beside the others
          const y0 = deg === 0 ? y - (count - 1) * lh - 4 : deg === 180 ? y + lh - 2 : y - ((count - 1) * lh) / 2;
          return (
            <text key={i} x={x} y={y0} dy="0.34em" textAnchor={anchor} style={{ fontSize: g.font }} className={cn('transition-[fill] duration-200', active === i ? 'fill-foreground' : 'fill-muted-foreground')}>
              {lines.map((l, k) => (
                <tspan key={k} x={x} dy={k ? lh : 0}>
                  {l}
                </tspan>
              ))}
              {values && series[0] && (
                <tspan x={x} dy={lh} className="fill-foreground font-semibold tabular-nums">
                  {fmt(series[0].values[i] ?? 0)}
                </tspan>
              )}
            </text>
          );
        })}
    </svg>
  );
}

/**
 * Spokes around a center: one profile, a comparison, a list beside it, small multiples or against a target.
 *   <RadarChart variant="profile" axes={['Cleanliness', 'Location', …]} series={[{ label: 'Guests', values: [9.1, 8.4, …] }]} max={10} />
 */
export function RadarChart(props: RadarChartProps) {
  const { variant = 'profile', axes, series, target, format, targetLabel, label, summary, labels, animate = true, title, value, delta, meta, source, framed, className } = props;
  const t = useTexts(TEXTS, undefined);
  const intl = useIntlLocale();
  const fmt = numberFormatter(intl, format);
  const [wrapRef, width] = useWidth<HTMLDivElement>(560);
  const [rootRef, anim] = useEntrance<HTMLDivElement>(animate);
  const [active, setActive] = useState<number | null>(null);
  const { shown, open, jump } = useShown(active);
  const [focus, setFocus] = useState<number | null>(null);

  const n = axes.length;
  const all = series.flatMap((s) => s.values).concat(target ?? []);
  const nice = niceTicks(0, props.max ?? Math.max(1, ...all), 5);
  const max = props.max ?? nice.max;
  const rings = props.max ? niceTicks(0, props.max, 5).ticks.filter((v) => v > 0 && v <= props.max!) : nice.ticks.filter((v) => v > 0);
  const name = label ?? title ?? axes.join(', ');
  const shownSeries = variant === 'compare' ? series.slice(0, 2) : series.slice(0, 1);

  const listBeside = variant === 'with-list' && width >= 620;
  const avail = Math.round(listBeside ? (width - 32) * 0.52 : width);
  const showValues = variant === 'profile' || variant === 'target';
  const g = geometry(axes, avail, { font: avail < 420 ? 11 : 12, extraLine: showValues });

  const rows = (i: number): TooltipRow[] => {
    const out: TooltipRow[] = shownSeries.map((s, si) => ({ key: s.label, label: s.label, value: fmt(s.values[i] ?? 0), color: SERIES_COLOR[Math.min(si, 1)], shape: 'line' }));
    if (target) out.push({ key: '__target', label: targetLabel ?? t.target, value: fmt(target[i] ?? 0), color: 'var(--foreground)', shape: 'line', dashed: true });
    return out;
  };

  const pick = (clientX: number, clientY: number, el: HTMLElement) => {
    const svg = el.querySelector('svg');
    if (!svg) return;
    const b = svg.getBoundingClientRect();
    const k = g.width / (b.width || g.width);
    const dx = (clientX - b.left) * k - g.cx;
    const dy = (clientY - b.top) * k - g.cy;
    if (Math.hypot(dx, dy) < 6) return;
    const deg = ((Math.atan2(dy, dx) * 180) / Math.PI + 90 + 360) % 360;
    setActive(Math.round(deg / (360 / n)) % n);
  };
  const onKeyDown = (e: React.KeyboardEvent) => {
    const cur = active ?? -1;
    let next: number | null = cur;
    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (cur + 1) % n;
    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = cur < 0 ? n - 1 : (cur - 1 + n) % n;
    else if (e.key === 'Home') next = 0;
    else if (e.key === 'End') next = n - 1;
    else if (e.key === 'Escape') next = null;
    else return;
    e.preventDefault();
    setActive(next);
  };

  const auto = shownSeries.map((s) => `${s.label}: ${axes.map((a, i) => `${a} ${fmt(s.values[i] ?? 0)}`).join(', ')}`).join('. ');
  const live = active !== null ? `${axes[active]}. ${rows(active).map((r) => `${r.label} ${r.value}`).join(', ')}` : '';
  const table = <ChartTable caption={name} head={[t.axis, ...shownSeries.map((s) => s.label), ...(target ? [targetLabel ?? t.target] : [])]} rows={axes.map((a, i) => [a, ...shownSeries.map((s) => fmt(s.values[i] ?? 0)), ...(target ? [fmt(target[i] ?? 0)] : [])])} />;

  // Tooltip anchored at the outer end of the active spoke
  const tip = shown !== null ? polar(g.cx, g.cy, g.r, (360 * shown) / n) : ([0, 0] as [number, number]);

  const misses = target ? axes.filter((_, i) => (series[0]?.values[i] ?? 0) < target[i]).length : 0;

  /* ---------- multiples: a key and one small web per series ---------- */
  if (variant === 'multiples') {
    // The key spans two cells, so key plus series fill whole rows more often
    const cols = width >= 900 ? 6 : width >= 480 ? 3 : 2;
    const cell = Math.floor((width - (cols - 1) * 12) / cols) - 26;
    const keyG = geometry(axes, Math.min(2 * cell + 36, 420), { font: 11, maxR: 64, minR: 34 });
    const smallBase = geometry(axes.map(() => ''), cell, { maxR: 72, minR: 36 });
    const smallG = { ...smallBase, width: cell, height: Math.round(2 * smallBase.r + 16), cy: Math.round(smallBase.r + 8) };
    return (
      <ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} className={className}>
        <ChartRoot anim={anim} rootRef={rootRef}>
          <div ref={wrapRef}>
            <ul role="list" aria-label={name} className="grid gap-3" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
              <li className="col-span-2 flex flex-col rounded-lg border border-dashed border-border-strong px-3 pb-3 pt-2.5">
                <p className="text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">{t.key}</p>
                <div className="flex flex-1 items-center justify-center">
                  <div className="w-full" style={{ maxWidth: keyG.width }}>
                    <RadarPlot g={keyG} axes={axes} series={[]} max={max} rings={rings} labels active={null} focus={null} anim={false} fmt={fmt} bare />
                  </div>
                </div>
              </li>
              {series.map((s, si) => {
                const avg = s.values.reduce((a, v) => a + v, 0) / Math.max(1, s.values.length);
                return (
                  <li key={s.label} className={cn('group flex flex-col rounded-lg border border-border bg-card px-3 pb-3 pt-2.5 transition-[border-color,box-shadow] duration-300 hover:border-border-strong hover:shadow-[var(--shadow-sm)]', anim && 'kitch-rise')} style={anim ? ({ '--kitch-d': `${120 + si * 90}ms` } as React.CSSProperties) : undefined}>
                    {/* Two columns on phones: name and average stacked, so no name gets cut */}
                    <p className={cn('flex w-full text-[13px]', cols === 2 ? 'flex-col gap-0.5' : 'items-baseline justify-between gap-2')}>
                      <span className="min-w-0 truncate font-medium text-foreground">{s.label}</span>
                      <span className="whitespace-nowrap tabular-nums text-muted-foreground">{fill(t.avg, { v: fmt(round(avg, 1)) })}</span>
                    </p>
                    <div className="mt-2 transition-transform duration-500 ease-[var(--ease-kit)] group-hover:scale-[1.04] motion-reduce:transition-none">
                      <RadarPlot g={smallG} axes={axes} series={[s]} max={max} rings={rings} labels={false} active={null} focus={null} anim={!!anim} fmt={fmt} />
                    </div>
                    <span className="sr-only">{axes.map((a, i) => `${a} ${fmt(s.values[i] ?? 0)}`).join(', ')}</span>
                  </li>
                );
              })}
            </ul>
          </div>
          <ChartTable caption={name} head={[t.axis, ...series.map((s) => s.label)]} rows={axes.map((a, i) => [a, ...series.map((s) => fmt(s.values[i] ?? 0))])} />
        </ChartRoot>
      </ChartFigure>
    );
  }

  const legendItems =
    variant === 'compare'
      ? shownSeries.map((s, i) => ({ label: s.label, key: <Swatch shape="line" color={SERIES_COLOR[i]} /> }))
      : variant === 'target'
        ? [
            { label: series[0]?.label ?? '', key: <Swatch shape="line" color={SERIES_COLOR[0]} /> },
            { label: targetLabel ?? t.target, key: <Swatch shape="line" color="var(--foreground)" dashed /> },
          ]
        : [];

  return (
    <ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} className={className}>
      <ChartRoot anim={anim} rootRef={rootRef}>
        {legendItems.length > 0 && (
          <div className="mb-3 flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
            <PickLegend active={variant === 'compare' ? focus : null} onActive={variant === 'compare' ? setFocus : () => {}} items={legendItems} />
            {variant === 'target' && <span className="text-[12px] text-muted-foreground">{fill(t.gap, { n: String(misses), total: String(n) })}</span>}
          </div>
        )}
        <div ref={wrapRef} className={cn(listBeside && 'grid grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] items-center gap-8')}>
          <div>
            <ChartSurface
              label={name}
              summary={summary ?? auto}
              labels={labels}
              live={live}
              table={table}
              onKeyDown={onKeyDown}
              onFocus={(e) => e.target === e.currentTarget && active === null && e.currentTarget.matches(':focus-visible') && setActive(0)}
              onBlur={() => setActive(null)}
              onPointerMove={(e) => pick(e.clientX, e.clientY, e.currentTarget)}
              onPointerDown={(e) => pick(e.clientX, e.clientY, e.currentTarget)}
              onPointerLeave={(e) => e.pointerType === 'mouse' && setActive(null)}
              className="mx-auto"
              style={{ maxWidth: g.width }}
            >
              <RadarPlot g={g} axes={axes} series={shownSeries} target={variant === 'target' ? target : undefined} max={max} rings={rings} labels active={active} focus={focus} anim={!!anim} fmt={fmt} ringLabels values={showValues} />
              <ChartTooltip open={open} jump={jump} x={tip[0]} y={Math.max(0, tip[1] - 30)} width={g.width} offset={14} title={shown !== null ? axes[shown] : ''} rows={shown !== null ? rows(shown) : []} />
            </ChartSurface>
          </div>
          {variant === 'with-list' && (
            <ul className={cn('grid gap-1', !listBeside && 'mt-6')}>
              {axes.map((a, i) => {
                const v = series[0]?.values[i] ?? 0;
                return (
                  <li key={a}>
                    <button
                      type="button"
                      aria-pressed={active === i}
                      onPointerEnter={(e) => e.pointerType === 'mouse' && setActive(i)}
                      onPointerLeave={(e) => e.pointerType === 'mouse' && setActive(null)}
                      onFocus={() => setActive(i)}
                      onBlur={() => setActive(null)}
                      onClick={() => setActive(active === i ? null : i)}
                      className={cn('grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1.5 rounded-md px-2 py-1.5 text-left text-[13px] transition-colors duration-200', active === i ? 'bg-accent' : 'hover:bg-accent/60')}
                    >
                      <span className={cn('truncate', active === i ? 'text-foreground' : 'text-muted-foreground')}>{a}</span>
                      <span className="font-medium tabular-nums text-foreground">
                        {fmt(v)} <span className="font-normal text-muted-foreground">{fill(t.of, { max: fmt(max) })}</span>
                      </span>
                      <span aria-hidden className="col-span-2 block h-1.5 overflow-hidden rounded-full bg-track">
                        <span className={cn('block h-full origin-left rounded-full bg-primary', anim && 'kitch-grow-x')} style={{ width: `${round((Math.min(v, max) / max) * 100, 1)}%`, ...(anim ? ({ '--kitch-d': `${200 + i * 70}ms` } as React.CSSProperties) : {}) }} />
                      </span>
                    </button>
                  </li>
                );
              })}
            </ul>
          )}
        </div>
      </ChartRoot>
    </ChartFigure>
  );
}

Frequent questions

How do I install Radar chart?

With aiw add @ai-website/radar-chart. The command puts components/ui/radar-chart.tsx into your project, the code is yours from then on. It brings along chart and chart-figure. It needs no further npm packages.

Which variants does Radar chart have?

5 variants, Profile, Compare, With list, Small multiples and Against a target. You pick one with the variant prop, for example variant="compare".

Can I translate the texts of Radar chart?

Yes. Radar chart carries 6 texts of its own in English and German, such as “Target”, “How to read” and “Axis”. The language follows the KitProvider, single texts change through the labels prop.

Can an AI assistant build with Radar chart?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Radar chart with aiw_components and install it with aiw_add @ai-website/radar-chart. Name the variant in your prompt, for example variant="profile", and the assistant writes your content straight into the props.

Can I adjust Radar chart without code?

Yes, in the Studio. All 5 variants are editable there, texts, lists and switches. The Studio writes the matching RadarChart code to copy.

Often searched

Jump to

Copied to the clipboard