Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~19 min· topic 2 of 7

Topic 5.2

Selections

In one line

A selection is a list of page elements that D3 holds for you. You find elements with select and selectAll, change them with attr, style and text, add with append, delete with remove, and chain the calls. An empty selection is not an error: setters do nothing, and getters throw.

Think of it like this

A teacher with a class list. "Everyone wearing a red jumper, please stand up" picks out some children (the selection). "Put your hand up" is then said once, and every picked child does it. If nobody wears red, the teacher still says it and nothing happens. That is a selection: pick first, then give one instruction to the whole group.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

selection
D3's list of elements, with methods to change them all at once. It is not an array, but nodes() gives you one.
selector
A CSS pattern such as #list li or .bar that says which elements you mean.
chaining
Writing one call after another on the same line, each acting on what the last one returned.
setter and getter
A setter gives a value (attr("x", 5)). A getter leaves the value out and reads the current one (attr("x")).
empty selection
A selection that matched nothing. size() is 0 and node() is null.
this
In a function () {} called by D3, the element being processed. In an arrow function, this is not the element.

Step by step

01Pick, then change

The first example starts with a list of three station names and an empty SVG. select finds the first li, selectAll finds three. style, classed and attr change all three at once, and the attr value is a function of the index i, so the rows get data-row 0, 1 and 2.

02Create and delete

svg.append("rect") adds one <rect> and returns it, so the next .attr calls land on the rectangle. Then filter((d, i) => i === 1).remove() deletes the second list item: Market is gone, Quay and Chapel remain.

03See what an empty selection does

d3.select("#nope") matches nothing. attr, style, text and remove on it all run without a word. The console proves it: empty() true, size() 0, node() null.

The second example then reads from the empty selection: the getters throw TypeError: Cannot read properties of null. A setter is silent, a getter is not.

04this, arrows and call

Inside function () {...} the example reads this.textContent. The same code with an arrow function throws Cannot read properties of undefined (reading 'textContent'), because in a module an arrow function at the top level has no this. call(highlight, color) reuses a small recipe.

Try it yourself

  1. 1

    Pick the odd rows

    In the first example add all.filter(":nth-child(odd)").style("font-weight", "bold"); before the remove. filter also accepts a CSS selector: Quay and Chapel turn bold and Market stays plain.

  2. 2

    Misspell the selector on purpose

    Change d3.select("#chart") to d3.select("#chrt"). The first three lines print as before. Then the Console shows Uncaught TypeError: Cannot read properties of null (reading 'tagName'). The empty selection swallowed append and every attr, and the first loud error is the line that reads from it.

Code & diagrams

Pick, change, create and delete New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

select: 1 selectAll: 3
rows: 0,1,2
first text: Quay
appended: rect 120
after remove: Quay, Chapel
empty: true size: 0 node: null
Getters, `this`, `call`, `text` and `html` New tab

The arrow-function error appears because the code is a module: at the top level of a module `this` is `undefined`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

TypeError: Cannot read properties of null (reading 'getAttribute')
setter on empty is fine, append gives size 0
[ 'Quay is 1 of 3', 'Market is 2 of 3', 'Chapel is 3 of 3' ]
TypeError: Cannot read properties of undefined (reading 'textContent')
datum: undefined
call returns the same selection: true
each: 1 2 3
html: <b>Quay</b>
text: &lt;b&gt;Quay&lt;/b&gt;
SyntaxError: Failed to execute 'querySelectorAll' on 'Document': 'li:' is not a valid selector.

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

Read an attribute from a selector that matches nothing

In the first example, change d3.select("#list li") to d3.select("#lst li") (a typo).

terminal
$ Console
── what you'll see ──
select: 0 selectAll: 3
rows: 0,1,2
Uncaught TypeError: Cannot read properties of null (reading 'textContent')

Myth vs fact

Myth

A selection is an array of elements.

Fact

It is an object that wraps groups of elements. nodes() gives the plain array, size() the count, and node() the first element. Array methods such as map do not exist on it.

Myth

D3 throws an error when a selector finds nothing.

Fact

It does not. Setters on an empty selection do nothing, and your chart simply does not appear. Typos are found by looking at size().

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A selection is a list of groups, and each group has a parent node. d3.selectAll("#list li") has one group (parent: the document). d3.selectAll("g").selectAll("rect") has one group per g, each holding its own rectangles, and the parent is that g. This is what makes nested joins work (Topic 5.5).

  • ▸

    select (singular) is special: it passes the parent's data to the child. selectAll does not. That is why row.select("rect") can use d in an attribute function and row.selectAll("rect") cannot. Topic 5.5 uses this.

Remember this

  1. 1

    The problem this solves: changing 25 elements by hand means a loop with document.querySelectorAll, forEach and setAttribute every time. A D3 selection is that loop wrapped up, so one line (.attr("fill", "teal")) changes all of them, and you can chain the next change on the end.

  2. 2

    **select takes the first match, selectAll takes all of them.** d3.select("#list li") is one element (the first li in the list). d3.selectAll("#list li") is all three. Both accept any CSS selector, or an element you already hold (d3.select(document.body)). Use .size() to count what you got, .node() for the first element and .nodes() for all of them as an array.

  3. 3

    Setting and chaining. attr(name, value) sets an attribute, style(name, value) sets a CSS property, text(string) sets the text, classed("on", true) adds a class, property("value", x) sets a JavaScript property such as a form field's value. Each setter returns the same selection, so .attr(...).style(...).text(...) chains. append("rect") is different: it returns a new selection of the elements it created, one new child inside each selected element.

  4. 4

    A value can be a function. Pass a function instead of a value and D3 calls it once per element with three arguments: the element's bound data d (Topic 5.3), its index i, and the array nodes of the whole selection. Inside a function () {}, this is the current element. An arrow function has no this of its own, so this is not the element there (undefined inside a module). Use (d, i, nodes) => nodes[i] when you want an arrow function and need the element.

  5. 5

    Empty selections are silent for setters, and loud for getters. d3.select("#nope").attr("x", 5) finds nothing and does nothing: no error, no warning. This is useful (the code works whether or not the element exists) and dangerous (a typo in a selector makes a chart quietly vanish). But a getter, d3.select("#nope").attr("x"), has no element to read from and throws Cannot read properties of null. Check selection.empty() when it matters.

  6. 6

    **call, each, filter.** selection.call(fn, ...args) runs fn(selection, ...args) and returns the selection, so a repeated recipe ("style this like a station label") can be a named function inside a chain. each(function (d, i) {...}) runs code once per element. filter(selector or function) keeps some elements. remove() deletes the selected elements from the page. text() sets text safely: <b> stays literal. html() parses markup, which is dangerous with data from outside (see Pro below).

Explain it without notes

01

What is the difference between select and selectAll?

02

What happens when you call attr("x", 5) on a selection that matched nothing? And attr("x")?

03

Why does (d, i, nodes) => this.textContent fail while function (d, i, nodes) { return this.textContent; } works?

Practice

01

Given <ul id="list"><li>Quay</li><li>Market</li><li>Chapel</li></ul>, write the D3 line that gives every li the title Station: <its text> using this, and the line that deletes every li whose text is longer than 5 letters.

02

Write a function labelAll(selector, text) that sets the text of every match and returns how many elements it changed. Check that it returns 0 for a selector that matches nothing and does not throw.

Trade-offs

  • ↔

    Silent empty selections make code robust to missing elements and make typos hard to see. In development, log size() after the selector that matters.

Done when you can

  • I can select one element or many and count them with size().

  • I can chain attr, style, text and classed and say what each returns.

  • I know that append returns the new elements, not the parents.

  • I can say what an empty selection does for a setter and for a getter.