component — charts

Radar Chart

A polar radar (spider) chart on the quebi Chart wrapper, overlaying one themed polygon per config key across a shared set of axes. Requires the recharts package.

  • chart
  • radar
  • spider
  • polar
  • recharts
  • data
  • visualization

05 examples

Default

One polygon per config key, over the axes named by `dataKey`.

Overlaid series

Two polygons on the same web. Click a legend item to focus one of them — the other fades rather than disappearing, so the shape stays comparable.

Outlines only

`fillOpacity={0}` drops the fills, which keeps three overlapping series readable where stacked translucency would not.

Circular grid with a radius axis

A circular web instead of a polygonal one, with the numeric radius axis shown.

Half radar

A start and end angle turn the web into a fan.

source

radar-chart.tsx

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { type ComponentProps, useMemo } from "react"
import {
  PolarAngleAxis,
  PolarGrid,
  PolarRadiusAxis,
  Radar,
  RadarChart as RadarChartPrimitive,
} from "recharts"
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"
import {
  type BaseChartProps,
  Chart,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  constructCategoryColors,
  DEFAULT_COLORS,
  getColorValue,
} from "@/components/chart"

/**
 * RadarChart — quebi design system
 *
 * A polar radar (spider) chart on the quebi `Chart` wrapper. `dataKey` names the
 * axis label on every row and each key in `config` becomes one overlaid polygon,
 * colored from the ink series palette. The grid, angle axis and radius axis
 * are themed; the interactive legend focuses a single series on click.
 *
 * Requires the `recharts` npm package as a peer dependency.
 */

export interface RadarChartProps<TValue extends ValueType, TName extends NameType>
  extends Omit<
    BaseChartProps<TValue, TName>,
    | "hideGridLines"
    | "hideXAxis"
    | "hideYAxis"
    | "xAxisProps"
    | "yAxisProps"
    | "cartesianGridProps"
    | "displayEdgeLabelsOnly"
    | "intervalType"
    | "layout"
  > {
  /** Hide the web of polar grid lines. */
  hidePolarGrid?: boolean
  /** Show the numeric radius axis. Hidden by default — the web already scales. */
  showRadiusAxis?: boolean
  /** Fill opacity of each polygon. Set to 0 for outline-only radars. */
  fillOpacity?: number
  gridType?: ComponentProps<typeof PolarGrid>["gridType"]
  outerRadius?: number | string
  startAngle?: number
  endAngle?: number
  polarGridProps?: ComponentProps<typeof PolarGrid>
  polarAngleAxisProps?: ComponentProps<typeof PolarAngleAxis>
  polarRadiusAxisProps?: ComponentProps<typeof PolarRadiusAxis>
  radarProps?: Partial<ComponentProps<typeof Radar>>
  chartProps?: Omit<ComponentProps<typeof RadarChartPrimitive>, "data">
}

export function RadarChart<TValue extends ValueType, TName extends NameType>({
  data = [],
  dataKey,
  colors = DEFAULT_COLORS,
  config,
  children,

  // Components
  tooltip = true,
  tooltipProps,

  legend = true,
  legendProps,

  hidePolarGrid = false,
  showRadiusAxis = false,
  fillOpacity = 0.2,
  gridType = "polygon",
  outerRadius = "72%",
  startAngle,
  endAngle,

  valueFormatter = (value: number) => value.toString(),

  polarGridProps,
  polarAngleAxisProps,
  polarRadiusAxisProps,
  radarProps,
  chartProps,
  overlays,
  ...props
}: RadarChartProps<TValue, TName>) {
  const configKeys = useMemo(() => Object.keys(config), [config])
  const categoryColors = useMemo(
    () => constructCategoryColors(configKeys, colors),
    [configKeys, colors],
  )
  const configEntries = useMemo(() => Object.entries(config), [config])

  return (
    <Chart config={config} data={data} dataKey={dataKey} layout="radial" {...props}>
      {({ onLegendSelect, selectedLegend }) => (
        <RadarChartPrimitive
          onClick={() => {
            onLegendSelect(null)
          }}
          data={data}
          outerRadius={outerRadius}
          startAngle={startAngle}
          endAngle={endAngle}
          margin={{
            bottom: 0,
            left: 0,
            right: 0,
            top: 0,
          }}
          {...chartProps}
        >
          {!hidePolarGrid && (
            <PolarGrid
              gridType={gridType}
              className="stroke-quebi-hairline"
              {...polarGridProps}
            />
          )}
          <PolarAngleAxis
            dataKey={dataKey}
            className="text-quebi-caption **:[text]:fill-quebi-fg-subtle"
            tickLine={false}
            {...polarAngleAxisProps}
          />
          <PolarRadiusAxis
            className="text-quebi-caption **:[text]:fill-quebi-fg-subtle"
            tick={showRadiusAxis}
            axisLine={false}
            tickFormatter={valueFormatter}
            {...polarRadiusAxisProps}
          />

          {legend && (
            <ChartLegend
              content={typeof legend === "boolean" ? <ChartLegendContent /> : legend}
              {...legendProps}
            />
          )}

          {tooltip && (
            <ChartTooltip
              content={
                typeof tooltip === "boolean" ? <ChartTooltipContent accessibilityLayer /> : tooltip
              }
              {...tooltipProps}
            />
          )}

          {!children
            ? configEntries.map(([category, values]) => {
                const color = getColorValue(values.color || categoryColors.get(category))
                const dimmed = selectedLegend !== null && selectedLegend !== category

                return (
                  <Radar
                    key={category}
                    name={category}
                    dataKey={category}
                    stroke={color}
                    strokeWidth={2}
                    strokeOpacity={dimmed ? 0.1 : 1}
                    fill={color}
                    fillOpacity={dimmed ? 0.02 : fillOpacity}
                    dot={false}
                    isAnimationActive
                    {...radarProps}
                  />
                )
              })
            : children}

          {overlays}
        </RadarChartPrimitive>
      )}
    </Chart>
  )
}