TanStack
Examples

Polar and Radar Charts

Polar geometry is available only from @tanstack/charts/polar. The container owns responsive center, angle, and radius ranges; granular D3 modules still own pie layout, configured scales, and curve factories.

ts
import {
  angleGrid,
  polar,
  radialArc,
  radialArea,
  radialDot,
  radialGrid,
  radialLine,
  radialRule,
  radialText,
} from '@tanstack/charts/polar'

The package root stays Cartesian-sized when this subpath is not imported.

Pie and donut

Use d3-shape's pie transform to turn totals into angular intervals. radialArc renders the intervals. A zero inner radius is a pie; a responsive nonzero inner radius is a donut.

ts
import { defineChart } from '@tanstack/charts'
import { polar, radialArc } from '@tanstack/charts/polar'
import { pie } from 'd3-shape'

interface Part {
  id: string
  value: number
  fill: string
}

const parts: Part[] = [
  { id: 'ingest', value: 42, fill: '#2563eb' },
  { id: 'query', value: 28, fill: '#7c3aed' },
  { id: 'alerts', value: 18, fill: '#db2777' },
  { id: 'other', value: 12, fill: '#f59e0b' },
]

const slices = pie<Part>()
  .sort(null)
  .value((part) => part.value)(parts)

function ring(innerRatio: number) {
  return polar({
    inset: 8,
    radiusRatio: 0.82,
    marks: [
      radialArc(slices, {
        key: (slice) => slice.data.id,
        startAngle: 'startAngle',
        endAngle: 'endAngle',
        padAngle: 'padAngle',
        innerRadius: ({ radius }) => radius * innerRatio,
        cornerRadius: 4,
        fill: (slice) => slice.data.fill,
      }),
    ],
  })
}

const pieChart = defineChart({
  marks: [ring(0)],
  guides: false,
  x: null,
  y: null,
})

const donutChart = defineChart({
  marks: [ring(0.58)],
  guides: false,
  x: null,
  y: null,
})

The same primitives cover labels, center content, padding, rounded corners, and concentric rings:

Keep pie().sort(null) when source order is semantic. Stable arc keys must come from the original row, not the generated slice index.

Partial-circle gauge

A gauge is the same composition over a restricted D3 pie interval. It is not a separate geometry implementation.

ts
import { defineChart } from '@tanstack/charts'
import { polar, radialArc } from '@tanstack/charts/polar'
import { pie } from 'd3-shape'

const value = 72
const gaugeParts = [
  { id: 'value', value, fill: '#ef4444' },
  { id: 'remainder', value: 100 - value, fill: '#e2e8f0' },
]
const gaugeSlices = pie<(typeof gaugeParts)[number]>()
  .sort(null)
  .value((part) => part.value)
  .startAngle(-Math.PI * 0.75)
  .endAngle(Math.PI * 0.75)(gaugeParts)

const gauge = defineChart({
  marks: [
    polar({
      radiusRatio: 0.84,
      marks: [
        radialArc(gaugeSlices, {
          key: (slice) => slice.data.id,
          startAngle: 'startAngle',
          endAngle: 'endAngle',
          innerRadius: ({ radius }) => radius * 0.72,
          cornerRadius: 999,
          fill: (slice) => slice.data.fill,
        }),
      ],
    }),
  ],
  guides: false,
  x: null,
  y: null,
})

Bound the input before layout and expose the exact value outside the arc. Arc length is useful for a compact status summary, not fine comparison.

Radar profile

Radar combines an inferred angle factory and a fixed radius instance with polar guides and radial marks. TanStack supplies both responsive ranges.

ts
import { defineChart } from '@tanstack/charts'
import {
  angleGrid,
  polar,
  radialArea,
  radialDot,
  radialGrid,
  radialLine,
} from '@tanstack/charts/polar'
import { scaleBand, scaleLinear } from 'd3-scale'
import { curveLinearClosed } from 'd3-shape'

const metrics = [
  'Latency',
  'Errors',
  'Traffic',
  'Saturation',
  'Availability',
] as const
const profile = metrics.map((metric, index) => ({
  metric,
  score: [74, 46, 88, 61, 93][index] ?? 0,
}))

const radar = defineChart({
  marks: [
    polar({
      radiusRatio: 0.72,
      angle: { scale: scaleBand<string> },
      radius: { scale: scaleLinear().domain([0, 100]).nice(4) },
      guides: [
        radialGrid({ ticks: 4, shape: 'polygon', labels: false }),
        angleGrid({
          labels: true,
          labelDx: ({ x }) => (x < -1 ? -3 : x > 1 ? 3 : 0),
          labelDy: ({ y }) => (y < -1 ? -2 : y > 1 ? 2 : 0),
        }),
      ],
      marks: [
        radialArea(profile, {
          angle: 'metric',
          radius: 'score',
          curve: curveLinearClosed,
          fill: '#7c3aed',
          fillOpacity: 0.22,
        }),
        radialLine(profile, {
          angle: 'metric',
          radius: 'score',
          curve: curveLinearClosed,
          stroke: '#8b5cf6',
          strokeWidth: 2,
        }),
        radialDot(profile, {
          angle: 'metric',
          radius: 'score',
          key: 'metric',
          r: 3,
          fill: '#8b5cf6',
        }),
      ],
    }),
  ],
  guides: false,
  x: null,
  y: null,
})

Use radar for a small, fixed set of compatible dimensions. Keep every domain and direction explicit, and do not rank profiles by apparent filled area.

Numeric polar line and scatter

Continuous D3 scales map numeric angle and radius values without changing the mark API. Repeat the first row at the angular endpoint when a line should close; independent scatter observations need no seam row.

ts
import { defineChart } from '@tanstack/charts'
import {
  angleGrid,
  polar,
  radialDot,
  radialGrid,
  radialLine,
} from '@tanstack/charts/polar'
import { scaleLinear } from 'd3-scale'

const trajectory = [
  { id: '0', angle: 0, radius: 52 },
  { id: '90', angle: 90, radius: 76 },
  { id: '180', angle: 180, radius: 43 },
  { id: '270', angle: 270, radius: 68 },
  { id: '360', angle: 360, radius: 52 },
]
const observations = [
  { id: 'a', angle: 34, radius: 61 },
  { id: 'b', angle: 142, radius: 82 },
  { id: 'c', angle: 248, radius: 47 },
]

const numericPolar = defineChart({
  marks: [
    polar({
      angle: { scale: scaleLinear().domain([0, 360]) },
      radius: { scale: scaleLinear().domain([0, 100]) },
      guides: [
        radialGrid({ values: [25, 50, 75, 100] }),
        angleGrid({ values: [0, 90, 180, 270], labels: false }),
      ],
      marks: [
        radialLine(trajectory, {
          angle: 'angle',
          radius: 'radius',
          key: 'id',
          stroke: '#0f766e',
        }),
        radialDot(observations, {
          angle: 'angle',
          radius: 'radius',
          key: 'id',
          r: 4.5,
          fill: '#e11d48',
        }),
      ],
    }),
  ],
  guides: false,
  x: null,
  y: null,
})

Radial magnitude and hierarchy

radialArc.generator accepts responsive D3 arc accessors, so variable-radius sectors, concentric radial bars, and hierarchy rings remain one primitive.

Coordinate and bundle boundary

polar() is a positionless container mark. It resolves one center and radius, copies configured angle/radius scales, paints guide backgrounds, child marks, then guide foreground labels, and emits ordinary scene nodes and focus points. The outer chart therefore uses x: null, y: null, and guides: false.

The polar entry uses D3 arc and radial path generators internally. Application source imports pie, configured scales, and curve factories directly from d3-shape or d3-scale. See Polar Marks for the complete API and Bundle Size and Performance for the isolated consumer budgets.

Production checks

  • Keep angle for cyclic order or part-to-whole intervals.
  • Use D3 pie output rather than reimplementing angle accumulation.
  • Give every mutable arc and point a stable source key.
  • Preserve original values for tooltips and accessible summaries.
  • Keep radar dimension domains, directions, and units explicit.
  • Verify labels around the full circumference at narrow widths.
  • Prefer aligned bars or dots when precise comparison is the primary task.