Topic 6.4
Band and Point Scales
In one line
A band scale gives every category its own slot, with a start position and a width, so bars sit evenly with gaps between them. A point scale gives each category one position only. Both return undefined for a category they do not know.
Think of it like this
A row of parking bays. Each car gets one bay, the bays are equally wide, and a little space is left between them and at both ends. A band scale draws the bays. A point scale only drops a pin in the middle of each one.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- band
- A slot on a category axis: a start position and a width.
- step
- The distance from the start of one band to the start of the next.
- bandwidth
- The width of the part of a slot a bar may use. The rest is padding.
- padding
- Empty space between bars (inner) or at the two ends (outer).
- align
- Where leftover space goes: 0 at the start, 0.5 split, 1 at the end.
Step by step
01One slot per station
The first example gives the 25 stations 400 pixels of height: the step is 16.13, so each band starts about 16 pixels after the last. The domain is sorted by ride total, so Quay (58,584) is the top row.
02Gaps come from padding
With paddingInner(0.5) on three categories in 300 pixels, the step grows to 120 and each bar takes half of it: 60 pixels, with a 60 pixel gap. The code prints step and bandwidth for several settings, so you can see the formula: more padding, thinner bars. Zero padding gives bars that touch.
03Leftover space and rounding
With three categories in 300 pixels and paddingOuter(1), the step is 60. align(0) puts the bands at 0, 60 and 120 (all leftover space at the end). align(1) puts them at 120, 180 and 240. The example prints both.
04Groups inside bands
The second example makes months an outer band and e-bike/pedal an inner band inside each month's bandwidth: inner.range([0, outer.bandwidth()]). A bar's x is outer(month) + inner(type).
Try it yourself
- 1
Rounded bands
In the first example add
.round(true)after.paddingInner(0.2). Step and bandwidth are now whole numbers (16 and 13), and the 25 bars no longer fill exactly 400 pixels: the leftover space goes wherealignsays. - 2
Reverse the order
Reverse the range to
[400, 0]. The first station now sits at the bottom. The domain order stays, so aids.reverse()would also do it. Choose by what the reader should see first.
Code & diagrams
The band scale decides each row's start and height; a linear scale decides each bar's length.
Preview
Console
Nothing logged yet.Expected console output
step: 16.13 bandwidth: 12.90 first: S01 0 last: S25 387.10
no padding: step 100.0 bandwidth 100.0 starts 0.0, 100.0, 200.0
paddingInner(0.5): step 120.0 bandwidth 60.0 starts 0.0, 120.0, 240.0
padding(0.5): step 85.7 bandwidth 42.9 starts 42.9, 128.6, 214.3
outer 1, align 0: step 60.0 bandwidth 60.0 starts 0.0, 60.0, 120.0
outer 1, align 1: step 60.0 bandwidth 60.0 starts 120.0, 180.0, 240.0
unknown station: undefined
duplicates: [ 'a', 'b' ]
number 2: 100 text "2": undefined
empty domain: step 400 bandwidth 400 position undefined
TypeError: stationsOnly.unknown is not a functionEach month is a <g> moved to its outer band; the inner band places its two bars.
Preview
Console
Nothing logged yet.Expected console output
July: { ebike: 39251, pedal: 78552 } total ebike share: 33.3%
month band: 34.6 inner bar: 16.0 Jul pedal x: 300.2 ebike x: 317.0
point: 0 100 300 step 100 bandwidth 0
point padding(0.5): 37.5 one category: 150
bars drawn: 24Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Look up a station that is not in the domain
In the first example change .attr("y", (d) => y(d)) to .attr("y", (d) => y(d + "x")), as if the ids came with a stray character.
Myth vs fact
Myth
Bars should touch with no gap to look neat.
Fact
A small gap (20 to 30 percent) keeps neighbours separate. Without it, equal-height neighbours merge into one block.
Myth
A band scale works for any number of categories.
Fact
It always returns positions, but with 200 categories each bar is 2 pixels and unreadable. Show the top ones, group the rest, or use a different chart (Topic 2.3).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The domain order is the draw order and the label order. Sorting the data does not change it: sort the domain (
d3.sort, orband.domain(sortedIds)), or the bars stay in the file's order. - ▸
band.copy()keeps padding, align and round, so a copy used for a transition target starts with the same layout.
Remember this
- 1
The problem this solves: bars sit on categories (stations, months), not numbers. You need each bar's left edge (where its slot starts) and its width (how much of the slot it fills). Doing
i * 20by hand breaks when the number of stations changes or when you want gaps. - 2
**
scaleBand.** (Recall from Topic 6.1: the domain is what goes in, the range is the pixels that come out. Here the domain is a list of names, not two numbers.)d3.scaleBand().domain(ids).range([0, 500])cuts the range into one equal slot per domain item.band("S02")returns where that slot starts, andband.bandwidth()returns how wide the bar should be.band.step()is the distance between two slot starts. With 25 stations over 500 pixels, step and bandwidth are both 20 and S02 starts at 20. - 3
Padding.
paddingInner(0.2)keeps 20 percent of each step as a gap between bars: bandwidth becomes 80 percent of the step.paddingOuteradds space at the two ends, measured in steps.padding(p)sets both. D3's formula isstep = rangeWidth / (n - paddingInner + 2 × paddingOuter).align(0 to 1, default 0.5) decides where leftover space goes: 0 pushes the bars to the start, 1 to the end.round(true)snaps starts and widths to whole pixels, which keeps edges crisp but wastes a little space. - 4
Many categories. 25 stations need 25 slots. In a 560 pixel row each bar is about 20 pixels wide, and long names such as
Harbour Gatedo not fit under it. Turn the chart sideways (the band becomes the y scale, the names go on the left) rather than rotating 25 labels. Sort the domain by value first, so the chart reads as a ranking:d3.sort(ids, (a, b) => d3.descending(total.get(a), total.get(b))). - 5
**
scalePoint.** It is a band scale with a bandwidth of 0: one position per category, the centre of its slot, with padding only at the outer ends (default 0 so the first point sits at the start of the range). It suits dots and line chart categories. For a single category the point sits at the range's centre. - 6
Unknown and odd input. A category missing from the domain returns
undefined, which a plainsetAttributecall turns intoExpected length, "undefined"(D3's.attrremoves the attribute, so the mark jumps to 0). Duplicates in the domain are collapsed to one slot. Keys compare by value and type:1and"1"are different categories. An empty domain gives a step and bandwidth equal to the whole range (the formula divides byMath.max(1, n - …)), which draws nothing but looks like a full-width bar if you add one. A band scale has noinvertand nounknownsetter (calling.unknown(0)throws aTypeError).
Explain it without notes
What do step() and bandwidth() return, and how do they differ?
What does paddingOuter do that paddingInner does not?
How do you draw grouped bars?
Practice
Make a band scale for the six districts over 300 pixels with 10 percent padding. Print the bandwidth and the start of "Hillcrest" (the fifth).
Write a function that takes the ids you want to show and returns a band scale, but keeps the bar width at 20 pixels however many ids there are, by changing the range height.
Trade-offs
- ↔
Wide bands and small gaps look sturdy; thin bands with large gaps look like a comb and lose the grouping. Between 15 and 30 percent padding is the usual range.
Done when you can
I can make a band scale and read step() and bandwidth().
I can predict what padding, align and round do.
I can draw grouped bars with two bands.
I know what the scale returns for a missing key.