Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~20 min· topic 2 of 7

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. 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. 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. 3

    Smallest series first

    In the first example .order(d3.stackOrderAscending) puts the smaller series at the bottom, as printed in the smallest first line. On the district chart (step 2) the same call puts Westpark at the bottom and Harbour on top.

Code & diagrams

What d3.stack lays out: [y0, y1] for every item New tab

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: []
E-bikes and pedal bikes by month, as stacks and as a share New tab

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

Press Run to see the page.

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: 933651

Break 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.

terminal
$ Console
── what you'll see ──
Error: <rect> attribute height: Expected length, "NaN".

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.stack returns arrays that carry extra properties: each series has key and index, and each [y0, y1] has a .data property that points to the original row. That is why the code can read d.data.month.

  • ▸

    stackOffsetWiggle and stackOffsetSilhouette centre the stack for streamgraphs. They are hard to read: use them for shape and not for numbers.

Remember this

  1. 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. 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 a bandwidth()). The bars all start at zero, so every bar can be compared with every other.

  3. 3

    **d3.stack writes a [y0, y1] pair for every item.** You give it the keys (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 at y0 and ends at y1. 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. 4

    Order and offset change the layout. order decides which key goes at the bottom: stackOrderAscending puts the smallest series first, and the keys order (stackOrderNone) is the default. offset decides where each column starts: stackOffsetNone starts at zero, stackOffsetExpand rescales every column to add up to 1 (a 100% stack), and stackOffsetDiverging puts negative values below zero.

  5. 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. 6

    Missing values and negative numbers break a plain stack. A key that is missing in a row becomes NaN and 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, and stackOffsetDiverging for numbers that go below zero, as the first example prints.

Explain it without notes

01

What does d3.stack return for one month and one key?

02

When would you choose grouped bars over stacked bars?

03

What can a 100% stack show that a normal stack cannot, and what does it lose?

Practice

01

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"]).

02

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.