Wind Rose Chart

A multi-series Nightingale chart. Each direction keeps an equal angle while stacked RadialBars grow radially with the value of each series.
import { Legend, PolarAngleAxis, PolarGrid, PolarRadiusAxis, RadialBar, RadialBarChart, Tooltip } from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

// #region Sample data
type WindRoseDatum = {
  direction: string;
  angle: number;
  low: number;
  medium: number;
  high: number;
};

const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];

const series = [
  { dataKey: 'low', name: '0-10 km/h', fill: '#82ca9d' },
  { dataKey: 'medium', name: '10-20 km/h', fill: '#ffc658' },
  { dataKey: 'high', name: '20+ km/h', fill: '#ff8042' },
];

function WindRoseLegend() {
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '0 16px', fontSize: 12 }}>
      {series.map(({ name, fill }) => (
        <span key={name} style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
          <span style={{ width: 10, height: 10, backgroundColor: fill }} />
          {name}
        </span>
      ))}
    </div>
  );
}

const data: Array<WindRoseDatum> = [
  { direction: 'N', angle: 0, low: 12, medium: 8, high: 3 },
  { direction: 'NE', angle: 45, low: 9, medium: 11, high: 4 },
  { direction: 'E', angle: 90, low: 15, medium: 9, high: 2 },
  { direction: 'SE', angle: 135, low: 7, medium: 6, high: 1 },
  { direction: 'S', angle: 180, low: 18, medium: 12, high: 5 },
  { direction: 'SW', angle: 225, low: 11, medium: 7, high: 2 },
  { direction: 'W', angle: 270, low: 14, medium: 10, high: 3 },
  { direction: 'NW', angle: 315, low: 8, medium: 5, high: 1 },
];

// #endregion
const maxTotal = Math.max(...data.map(entry => entry.low + entry.medium + entry.high));
const radiusDomain = [0, maxTotal];
const angleDomain = [0, 360];
const formatAngle = (angle: number) => directions[angle / 45] ?? '';
const formatTooltipLabel = (label: unknown) => formatAngle(Number(label));

const chartStyle = {
  width: '100%',
  maxWidth: '500px',
  maxHeight: '80vh',
  aspectRatio: 1,
};

export default function WindRoseChart({ isAnimationActive = true }: { isAnimationActive?: boolean }) {
  return (
    <RadialBarChart
      data={data}
      style={chartStyle}
      responsive
      layout="centric"
      cx="50%"
      cy="50%"
      innerRadius="10%"
      outerRadius="80%"
      startAngle={90}
      endAngle={-270}
    >
      <PolarGrid gridType="circle" />
      <PolarAngleAxis
        type="number"
        dataKey="angle"
        domain={angleDomain}
        tickCount={8}
        niceTicks="none"
        tickFormatter={formatAngle}
        axisLine={false}
        tickLine={false}
      />
      <PolarRadiusAxis domain={radiusDomain} tick={false} axisLine={false} />
      {series.map(entry => (
        <RadialBar
          key={entry.dataKey}
          dataKey={entry.dataKey}
          stackId="wind"
          name={entry.name}
          fill={entry.fill}
          isAnimationActive={isAnimationActive}
        />
      ))}
      <Legend content={WindRoseLegend} />
      <Tooltip labelFormatter={formatTooltipLabel} />
      <RechartsDevtools />
    </RadialBarChart>
  );
}