Box Plot
Box plot charts summarize data distribution with quartiles and whiskers. This example uses Bar and ErrorBar with a custom bar
shape for the median line.import { RechartsDevtools } from '@recharts/devtools';
import {
Bar,
BarChart,
BarShapeProps,
CartesianGrid,
DefaultZIndexes,
ErrorBar,
Rectangle,
Scatter,
Tooltip,
TooltipContentProps,
TooltipIndex,
useRechartsTheme,
XAxis,
YAxis,
} from 'recharts';
type BoxPlotDatum = {
category: string;
min: number;
q1: number;
median: number;
q3: number;
max: number;
};
const data: ReadonlyArray<BoxPlotDatum> = [
{ category: 'A', min: 16, q1: 20, median: 24, q3: 29, max: 35 },
{ category: 'B', min: 12, q1: 15, median: 18, q3: 21, max: 27 },
{ category: 'C', min: 22, q1: 26, median: 30, q3: 34, max: 49 },
{ category: 'D', min: 9, q1: 13, median: 17, q3: 20, max: 26 },
{ category: 'E', min: 18, q1: 22, median: 25, q3: 28, max: 32 },
];
type OutlierDatum = {
category: string;
value: number;
};
const outliers: ReadonlyArray<OutlierDatum> = [
{ category: 'A', value: 10 },
{ category: 'A', value: 11 },
{ category: 'A', value: 12 },
{ category: 'A', value: 5 },
{ category: 'B', value: 0 },
{ category: 'A', value: 40 },
{ category: 'D', value: 8 },
{ category: 'E', value: 33 },
];
const boxDataKey: (entry: BoxPlotDatum) => [number, number] = entry => [entry.q1, entry.q3];
const whiskerDataKey: (entry: BoxPlotDatum) => [number, number] = entry => [entry.q3 - entry.min, entry.max - entry.q3];
const BoxShape = (props: BarShapeProps) => {
// @ts-expect-error Recharts does spread datum on the props but the types don't reflect that
const entry: BoxPlotDatum = props;
const quartileRange = entry.q3 - entry.q1;
const medianOffset =
quartileRange === 0 ? props.height / 2 : ((entry.q3 - entry.median) / quartileRange) * props.height;
const medianY = props.y + medianOffset;
return (
<g>
<Rectangle {...props} />
<line x1={props.x} x2={props.x + props.width} y1={medianY} y2={medianY} stroke="#1f2937" strokeWidth={2} />
</g>
);
};
const TooltipContent = (props: TooltipContentProps) => {
const { active, payload } = props;
const theme = useRechartsTheme();
if (active && payload && payload.length) {
const firstPayload = payload[0];
if (firstPayload == null) {
return null;
}
const entry: BoxPlotDatum = firstPayload.payload;
return (
<div
style={{
color: theme?.typography?.color,
...theme?.tooltip?.contentStyle,
padding: '0 1em',
borderRadius: '4px',
}}
>
<p style={{ margin: 0 }}>{`Category: ${entry.category}`}</p>
<p style={{ margin: 0 }}>{`Min: ${entry.min}`}</p>
<p style={{ margin: 0 }}>{`Q1: ${entry.q1}`}</p>
<p style={{ margin: 0 }}>{`Median: ${entry.median}`}</p>
<p style={{ margin: 0 }}>{`Q3: ${entry.q3}`}</p>
<p style={{ margin: 0 }}>{`Max: ${entry.max}`}</p>
</div>
);
}
return null;
};
export default function BoxPlotExample({ defaultIndex }: { defaultIndex?: TooltipIndex }) {
return (
<BarChart
data={data}
style={{ width: '100%', maxWidth: '700px', maxHeight: '70vh', aspectRatio: 1.618 }}
responsive
>
<XAxis dataKey="category" allowDuplicatedCategory={false} />
<YAxis width="auto" />
<CartesianGrid vertical={false} />
<Bar dataKey={boxDataKey} shape={BoxShape} fillOpacity={1}>
<ErrorBar dataKey={whiskerDataKey} width={0} zIndex={DefaultZIndexes.bar - 1} />
</Bar>
<Scatter data={outliers} dataKey="value" fill="#e11d48" stroke="none" />
<Tooltip content={TooltipContent} defaultIndex={defaultIndex} />
<RechartsDevtools />
</BarChart>
);
}