Topic 7.2
Stacked and Grouped Bars and Areas
In one line
d3.stack adds up several series so that each bar or area starts where the one below it ends. You choose the order and the offset (plain, 100%, or above and below zero), and you learn which questions a stack answers and which it hides.
Think of it like this
A tray of fruit crates at the market. Side by side, you compare crate sizes easily. Stacked in one tower, you see the tower's total height at once, but you can only compare the bottom crate fairly: every crate above it starts at a different height.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- series
- One set of values over the categories, such as one district's rides in each month.
- key
- The name of a column that becomes a series, such as
"Harbour". - offset
- The rule for where each stacked column starts: at zero, rescaled to 100%, or above and below zero.
- grouped bars
- Bars of different series side by side, each starting at zero.
- stacked bars
- Bars of different series piled up, so the whole bar shows the total.
Step by step
01Start with grouped bars
The picture on the right is the data we use: rides per month for the six districts of Ardenvale, from station-month.csv joined with stations.csv. Each month has six bars, one per district, all growing from zero.
02Pile them up
One change: d3.stack().keys(keys)(wide) gives each district a [y0, y1] pair per month, and each rectangle goes from y(d[1]) to y(d[0]). The picture now has 12 bars instead of 72, and the height of a bar is the month's total (43,439 rides in January, 117,803 in July).
03Make every month 100%
One change: .offset(d3.stackOffsetExpand) and a y scale from 0 to 1. Every bar is now the same height, and the axis reads in percent. The totals are gone, and the shares are clear: Harbour is 29.8% of January and 25.2% of July.
Westpark has no top slice until May, because its three stations opened on 10 May (with one more in Hillcrest). A share chart can hide that a slice is new. Say it in a note.
04Join the tops: a stacked area
One change: draw each series with d3.area instead of rectangles (y0 from d[0], y1 from d[1]) on a point scale. The same pairs now make smooth-looking areas, and the summer hill is easy to see.
Try it yourself
- 1
Reverse the order
In the first example swap the keys to
["pedal", "ebike"]: pedal now sits at the bottom with[0, 2]and e-bike on top with[2, 5]. Same data, different layout, and the bottom series is now the one you can compare. - 2
Check that a middle series floats
In step 2 add
console.log(series[3].map((d) => d[0])). It prints the start of Riverside in each month, from 32,232 in January to 77,562 in July, while its own size goes from 6,489 to 16,720: the start grows 2.4 times and the size 2.6 times. - 3
Smallest series first
In the first example
.order(d3.stackOrderAscending)puts the smaller series at the bottom, as printed in thesmallest firstline. On the district chart (step 2) the same call puts Westpark at the bottom and Harbour on top.
Code & diagrams
Each series is an array of `[y0, y1]` pairs. The JSON shows `null` where D3 holds `NaN`. Nothing in `d3.stack` throws: bad input gives bad numbers, and the error appears later, in the SVG.
Console
(nothing was printed)Expected console output
ebike, pedal [["ebike",[[0,3],[0,5]]],["pedal",[[3,5],[5,6]]]]
pedal, ebike [["pedal",[[0,2],[0,1]]],["ebike",[[2,5],[1,6]]]]
expand (100%) [["ebike",[[0,0.6],[0,0.8333333333333334]]],["pedal",[[0.6,1],[0.8333333333333334,1]]]]
smallest first [["ebike",[[2,5],[1,6]]],["pedal",[[0,2],[0,1]]]]
missing value [["a",[[0,5],[0,2]]],["b",[[5,null],[2,6]]]]
missing value fixed [["a",[[0,5],[0,2]]],["b",[[5,5],[2,6]]]]
negatives, none [["a",[[0,5],[0,-2]]],["b",[[5,2],[-2,2]]]]
negatives, diverging [["a",[[0,5],[-2,0]]],["b",[[-3,0],[0,4]]]]
no rows [["a",[]]]
no keys: []Left: amounts. Right: shares. The left chart says summer is busy, the right one says the e-bike share rises all year, even in winter when fewer people ride.
Preview
Console
Nothing logged yet.Expected console output
e-bike share in January: 28.3% July: 33.3% December: 37.7%
e-bike rides: January 12304 July 39251
total in the year: 933651Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Leave a key out of one row
In step 2, add delete wide[0].Harbour; just before const series = .... January now has no Harbour number.
Myth vs fact
Myth
A stacked bar chart shows every series clearly.
Fact
Only the bottom series and the total are easy to read. Riverside's size grew 2.6 times from January to July, but its starting height grew 2.4 times, so position and size tell different stories.
Myth
A 100% stack is just a normal stack with a different axis.
Fact
It answers another question. Amounts and totals are gone. It shows that the e-bike share keeps rising (28.3% to 37.7%) even when rides fall in winter.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
d3.stackreturns arrays that carry extra properties: each series haskeyandindex, and each[y0, y1]has a.dataproperty that points to the original row. That is why the code can readd.data.month. - ▸
stackOffsetWiggleandstackOffsetSilhouettecentre the stack for streamgraphs. They are hard to read: use them for shape and not for numbers.
Remember this
- 1
The problem this solves: Priya wants to see each month's rides and what each district contributed. One bar per district per month is a grouped chart (72 bars). A stack puts the six districts inside one bar per month: 12 bars, and the total is the height.
- 2
Grouped bars use two band scales. The outer band places each month. The inner band, made with
d3.scaleBand(keys, [0, outer.bandwidth()]), places the six districts inside it (recall Topic 6.4: a band scale gives each category a slot and abandwidth()). The bars all start at zero, so every bar can be compared with every other. - 3
**
d3.stackwrites a[y0, y1]pair for every item.** You give it thekeys(the columns to pile up) and call it with one row per category. It returns one array per key, and each array holds one[y0, y1]per row: the bar starts aty0and ends aty1. The first example prints them for two small months. On the real data, January's Harbour is[0, 12925]and Old Town is[12925, 24773]: Old Town starts where Harbour ends. - 4
Order and offset change the layout.
orderdecides which key goes at the bottom:stackOrderAscendingputs the smallest series first, and the keys order (stackOrderNone) is the default.offsetdecides where each column starts:stackOffsetNonestarts at zero,stackOffsetExpandrescales every column to add up to 1 (a 100% stack), andstackOffsetDivergingputs negative values below zero. - 5
Stacks hide comparisons. Only the bottom series and the total are easy to compare across months. A middle series floats: Riverside starts at 32,232 rides in January and at 77,562 in July, so its position changes even when its size does not. A 100% stack answers a different question (share, not amount): in the second example the e-bike share rises from 28.3% to 37.7% over the year while the total goes up and down.
- 6
Missing values and negative numbers break a plain stack. A key that is missing in a row becomes
NaNand poisons every series above it. A negative number in a normal stack subtracts from the pile and gives nonsense. Use.value((d, key) => d[key] ?? 0)for missing, andstackOffsetDivergingfor numbers that go below zero, as the first example prints.
Explain it without notes
What does d3.stack return for one month and one key?
When would you choose grouped bars over stacked bars?
What can a 100% stack show that a normal stack cannot, and what does it lose?
Practice
Take the first example's months and print, for February, how many rides the pedal series covers (y1 - y0). Use d3.stack().keys(["ebike", "pedal"]).
Turn step 2 into a chart of the two largest districts and one "Other" series that adds the rest. Hint: build the wide table again with three keys.
Trade-offs
- ↔
Stacked bars show totals and parts in one picture, but only the bottom part is easy to compare. Grouped bars compare parts well and show totals badly.
Done when you can
I can say what
[y0, y1]means and read it from a printed stack.I can draw grouped and stacked bars with the same data.
I can make a 100% stack and say what it shows and hides.
I can handle a missing value and a negative value without breaking the chart.