Rounded Bar corners

Recharts by default renders square bars. There is more than one way to round Bar's corners.

Round all corners

Pass radius={10} prop to a Bar to round all corners equally. This creates a uniform rounded rectangle. Large radius makes the bars look more like pills.

import {
  Bar,
  BarChart,
  BarProps,
  BarShapeProps,
  CartesianGrid,
  emptyTheme,
  RechartsThemeProvider,
  Rectangle,
  Tooltip,
  XAxis,
  YAxis,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

// #region Data and helper functions
type TimelineDataType = {
  name: string;
  type: string;
  outcome: 'success' | 'error' | 'pending';
  firstCycle: [number, number];
  secondCycle: [number, number];
};

const data: Array<TimelineDataType> = [
  {
    name: 'TEST 1',
    type: 'TR',
    outcome: 'success',
    firstCycle: [0, 3],
    secondCycle: [4.11, 14.11],
  },
  {
    name: 'TEST 2',
    type: 'MT',
    outcome: 'error',
    firstCycle: [0, 1.5],
    secondCycle: [9.11, 12.11],
  },
  {
    name: 'TEST 3',
    type: 'MT',
    outcome: 'success',
    firstCycle: [3, 5.37],
    secondCycle: [8.74, 14.48],
  },
  {
    name: 'TEST 4',
    type: 'MT',
    outcome: 'error',
    firstCycle: [5.37, 7.87],
    secondCycle: [9.61, 16.98],
  },
  {
    name: 'TEST 5',
    type: 'MT',
    outcome: 'success',
    firstCycle: [4.87, 8.24],
    secondCycle: [10.74, 17.35],
  },
  {
    name: 'TEST 6',
    type: 'MT',
    outcome: 'success',
    firstCycle: [3.24, 5.74],
    secondCycle: [8.61, 17.85],
  },
  {
    name: 'TEST 7',
    type: 'MT',
    outcome: 'success',
    firstCycle: [2.74, 9.11],
    secondCycle: [9.74, 18.22],
  },
  {
    name: 'TEST 8',
    type: 'MT',
    outcome: 'pending',
    firstCycle: [9.11, 10.61],
    secondCycle: [12.11, 19.72],
  },
];

const getBarColor = (outcome: TimelineDataType['outcome']) => {
  switch (outcome) {
    case 'success':
      return 'blue';
    case 'error':
      return 'red';
    default:
      return 'grey';
  }
};

const CustomFillRectangle = (props: BarShapeProps) => {
  // @ts-expect-error props.outcome is injected from the data array which Recharts doesn't know about
  const { outcome, isActive } = props;
  const barColor = getBarColor(outcome);
  return (
    <Rectangle {...props} stroke={isActive ? 'orange' : barColor} strokeWidth={isActive ? 3 : 1} fill={barColor} />
  );
};

const MyBar = (props: BarProps) => {
  return <Bar {...props} stackId="a" radius={25} activeBar shape={CustomFillRectangle} />;
};

// #endregion
export default function TimelineExample({ defaultIndex }: { defaultIndex?: number }) {
  return (
    <RechartsThemeProvider value={emptyTheme}>
      <BarChart
        layout="vertical"
        style={{ width: '100%', maxWidth: '700px', maxHeight: '70vh', aspectRatio: 1.618 }}
        responsive
        data={data}
        margin={{ bottom: 20 }}
      >
        <CartesianGrid strokeDasharray="2 2" />
        <Tooltip shared={false} defaultIndex={defaultIndex} />
        <XAxis type="number" height={50} label={{ value: 'Time (s)', position: 'insideBottomRight' }} />
        <YAxis
          type="category"
          dataKey="name"
          width="auto"
          label={{
            value: 'Test run',
            angle: -90,
            position: 'insideTopLeft',
            textAnchor: 'end',
          }}
        />
        <MyBar dataKey="firstCycle" />
        <MyBar dataKey="secondCycle" />
        <RechartsDevtools />
      </BarChart>
    </RechartsThemeProvider>
  );
}

Different radius for different corners

radius prop also accepts an array of four numbers. This way you can define specific radius for each corner.

import {
  Bar,
  BarChart,
  Legend,
  LegendPayload,
  Tooltip,
  XAxis,
  YAxis,
  RenderableText,
  TooltipValueType,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

// #region Data and helper functions
/**
 * Data source: https://www.populationpyramid.net/world/2024/
 * CSV columns: Age,M,F
 */
const rawData = `
100+,110838,476160
95-99,1141691,3389124
90-94,6038458,13078242
85-89,18342182,31348041
80-84,37166893,53013079
75-79,65570812,83217973
70-74,103998992,124048996
65-69,138182244,154357035
60-64,170525048,180992721
55-59,206686596,212285997
50-54,231342779,232097236
45-49,240153677,236696232
40-44,270991534,263180352
35-39,301744799,289424003
30-34,310384416,294303405
25-29,308889349,291429439
20-24,318912554,300510028
15-19,335882343,315258559
10-14,353666705,331681954
5-9,351991008,332121131
0-4,331889289,315450649
`
  .trim()
  .split('\n')
  .map(line => {
    const [age, m, f] = line.split(',');
    return { age, male: Number(m), female: Number(f) };
  });

const totalPopulation: number = rawData.reduce((sum, entry) => sum + entry.male + entry.female, 0);

const percentageData = rawData.map(entry => {
  return {
    age: entry.age,
    male: (entry.male / totalPopulation) * -100, // Negative for left side
    female: (entry.female / totalPopulation) * 100,
  };
});

function formatPercent(val: RenderableText | TooltipValueType): string {
  return `${Math.abs(Number(val)).toFixed(1)}%`;
}

function itemSorter(item: LegendPayload): number {
  // Make legend order match the chart bar order
  return item.value === 'Male' ? 0 : 1;
}
// #endregion

export default function PopulationPyramidExample({ defaultIndex }: { defaultIndex?: number }) {
  return (
    <BarChart
      data={percentageData}
      layout="vertical"
      style={{ width: '100%', maxWidth: '700px', maxHeight: '70vh', aspectRatio: 1 }}
      responsive
      stackOffset="sign"
      barCategoryGap={1}
    >
      <XAxis
        type="number"
        domain={[-10, 10]}
        tickFormatter={formatPercent}
        height={50}
        label={{
          value: '% of total population',
          position: 'insideBottom',
        }}
      />
      <YAxis
        width="auto"
        type="category"
        dataKey="age"
        name="Age group"
        label={{
          value: 'Age group',
          angle: -90,
          position: 'insideLeft',
          offset: 10,
        }}
      />
      <Bar
        stackId="age"
        name="Female"
        dataKey="female"
        fill="#ed7485"
        stroke="none"
        radius={[0, 5, 5, 0]}
        label={{ position: 'right', formatter: formatPercent }}
      />
      <Bar
        stackId="age"
        name="Male"
        dataKey="male"
        fill="#6ea1c7"
        stroke="none"
        radius={[0, 5, 5, 0]}
        label={{ position: 'right', formatter: formatPercent }}
      />
      <Tooltip formatter={formatPercent} defaultIndex={defaultIndex} />
      <Legend itemSorter={itemSorter} verticalAlign="top" align="right" />
      <RechartsDevtools />
    </BarChart>
  );
}

Round a bar stack

In a stacked bar chart, you will run into some complications:

  • Some data points may be smaller than the radius
  • Some data points may be omitted

To fix these, you may want to use BarStack component. This allows you to set radius of the whole stack.

import { BarChart, XAxis, YAxis, Tooltip, Bar, BarStack, TooltipIndex, lightTheme } from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

// #region Sample data
const rangedStackedBarData = [
  { name: 'A', value1: [100, 200], value2: [200, 250], value3: [250, 300] },
  { name: 'B', value1: [120, 180], value2: [130, 230], value3: [170, 270] },
  { name: 'C', value1: [90, 160], value2: [210, 310], value3: [340, 440] },
  { name: 'D', value1: [80, 140], value2: [140, 200], value3: [200, 220] },
];
// #endregion

const RangedStackedBarChart = ({
  isAnimationActive = true,
  defaultIndex,
}: {
  isAnimationActive?: boolean;
  defaultIndex?: TooltipIndex;
}) => (
  <BarChart
    style={{ width: '100%', maxWidth: '700px', maxHeight: '70vh', aspectRatio: 1.618 }}
    responsive
    data={rangedStackedBarData}
    id="recharts-ranged-stacked-bar-chart"
    margin={{
      top: 20,
      right: 20,
      bottom: 20,
      left: 20,
    }}
  >
    <XAxis dataKey="name" />
    <YAxis width="auto" />
    <Tooltip defaultIndex={defaultIndex} />
    <BarStack radius={25}>
      <Bar
        dataKey="value1"
        {...lightTheme.graphicalItems[0]}
        maxBarSize={50}
        isAnimationActive={isAnimationActive}
        activeBar={{ fillOpacity: 1 }}
      />
      <Bar
        dataKey="value2"
        {...lightTheme.graphicalItems[1]}
        maxBarSize={50}
        isAnimationActive={isAnimationActive}
        activeBar={{ fillOpacity: 1 }}
      />
      <Bar
        dataKey="value3"
        {...lightTheme.graphicalItems[2]}
        maxBarSize={50}
        isAnimationActive={isAnimationActive}
        activeBar={{ fillOpacity: 1 }}
      />
    </BarStack>
    <RechartsDevtools />
  </BarChart>
);

export default RangedStackedBarChart;