Home/ Cookbook/ Charts/ Grouped bars

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.

Charts Intermediate ~10 min React · @corelithzw/react

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.

Target vs actual, five weeks Five bucket-groups of two bars. Actual met or beat target in four weeks; missed in week four. $80k$60k$40k$20k W1 W2 W3 W4 W5
Target Actual Below target
Weeks 1, 2, 3 and 5 hit or exceeded target. Week 4 missed: target $65k, actual $48k — a $17k gap.

Required pieces

From @corelithzw/react.

Intended exports used here: Chart.Bar with group.

Roadmap. Grouped mode ships with Chart.Bar in @corelithzw/react v0.2.

The React snippet

Pass series instead of yAccessor. Conditional colour catches the miss.

TargetVsActual.tsx@corelithzw/react

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.

Week 4 missed target
−$17k

Accessibility

Grouped means two reads — make both possible without colour.

  • Role and label. Root <svg> uses role="img" + aria-labelledby on 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-only paragraph 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.