Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~21 min· topic 4 of 7

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. 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 where align says.

  2. 2

    Reverse the order

    Reverse the range to [400, 0]. The first station now sits at the bottom. The domain order stays, so a ids.reverse() would also do it. Choose by what the reader should see first.

Code & diagrams

A band scale for 25 stations, and its edge cases New tab

The band scale decides each row's start and height; a linear scale decides each bar's length.

Preview

Press Run to see the page.

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 function
Grouped bars with nested bands, and a point scale New tab

Each month is a <g> moved to its outer band; the inner band places its two bars.

Preview

Press Run to see the page.

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

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

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.

terminal
$ Page (screenshot checked)
── what you'll see ──
The Console prints the same lines as before and no `Error:` line. The picture shows one long bar in the top row, beside Quay, and no bar next to any other name: all 25 bars sit on top of each other at y = 0, and the longest, Quay's, hides the rest.

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, or band.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. 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 * 20 by hand breaks when the number of stations changes or when you want gaps.

  2. 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, and band.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. 3

    Padding. paddingInner(0.2) keeps 20 percent of each step as a gap between bars: bandwidth becomes 80 percent of the step. paddingOuter adds space at the two ends, measured in steps. padding(p) sets both. D3's formula is step = 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. 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 Gate do 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. 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. 6

    Unknown and odd input. A category missing from the domain returns undefined, which a plain setAttribute call turns into Expected length, "undefined" (D3's .attr removes the attribute, so the mark jumps to 0). Duplicates in the domain are collapsed to one slot. Keys compare by value and type: 1 and "1" are different categories. An empty domain gives a step and bandwidth equal to the whole range (the formula divides by Math.max(1, n - …)), which draws nothing but looks like a full-width bar if you add one. A band scale has no invert and no unknown setter (calling .unknown(0) throws a TypeError).

Explain it without notes

01

What do step() and bandwidth() return, and how do they differ?

02

What does paddingOuter do that paddingInner does not?

03

How do you draw grouped bars?

Practice

01

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

02

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.