Compare two metrics per bucket with a grouped bar chart
By the end of this recipe you'll have a paired-bar chart that puts target and actual side-by-side per bucket — the gap between them is the story.
Overview
Two-to-three bars per category, side-by-side. The reader compares within a bucket (target vs actual) and across buckets (which week was worst).
Grouped bars are for the "comparison of comparisons" question — actual vs target by week, this year vs last year by quarter, branch A vs branch B by product. The trick is that the eye does two reads at once: same colour across buckets, different colours within one. Both reads have to be obvious.
Skip grouped bars with more than three series per bucket — the cluster becomes visual noise. Above three, switch to small multiples (one little chart per series) or a line chart if the x-axis is ordered.
Default opinion: two series is the sweet spot. Three works; four turns the chart into a barcode.
The chart
Weekly target vs. actual, five weeks. Week 4 is the only one we missed — the gap is doing the work.
Required pieces
From @corelithzw/react.
Intended exports used here: Chart.Bar with group.
Chart.Bar in @corelithzw/react v0.2.
The React snippet
Pass series instead of yAccessor. Conditional colour catches the miss.
Customising
Three forks.
Overlay target as a tick
When target is constant, replace the second bar with a thin tick line. The chart reads cleaner — same information, less ink.
series={[
{ key: 'actual', color: 'brand' },
{ key: 'target', render: 'tick' },
]}
Add a third series
Three is the upper bound. Keep colours far apart on the wheel (brand, success, danger) so the eye can sort them in the cluster.
series={[
{ key: 'target', color: 'brand-300' },
{ key: 'actual', color: 'brand' },
{ key: 'forecast', color: 'success' },
]}
Annotate the miss
Drop a tiny in-chart label on the bar that missed. Reads better than dropping it in a footnote.
<Chart.Bar
data={data}
group
annotations={[
{ week: 'W4', text: 'Holiday week' },
]}
height={280}
/>
In context
Inside a weekly-review card. The chart sits next to a takeaway block.
Accessibility
Grouped means two reads — make both possible without colour.
- Role and label. Root
<svg>usesrole="img"+aria-labelledbyon inline<title>+<desc>. - Group + bar labels. Wrap each cluster in
<g aria-label="W4: target $65k, actual $48k, missed by $17k">; wrap each bar individually too. - Miss is more than colour. The danger bar carries a
data-state="miss"+ a small "miss" label so the message survives if the colour rule fails. - SR summary. A
.ck-sr-onlyparagraph lists hits and misses in order: "weeks 1, 2, 3, 5 met or beat target; week 4 missed by $17k". - Keyboard. Arrow left/right walks between clusters; Arrow up/down walks between bars within one cluster.