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 lior.barthat 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 andnode()isnull. - this
- In a
function () {}called by D3, the element being processed. In an arrow function,thisis 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
Pick the odd rows
In the first example add
all.filter(":nth-child(odd)").style("font-weight", "bold");before the remove.filteralso accepts a CSS selector: Quay and Chapel turn bold and Market stays plain. - 2
Misspell the selector on purpose
Change
d3.select("#chart")tod3.select("#chrt"). The first three lines print as before. Then the Console showsUncaught TypeError: Cannot read properties of null (reading 'tagName'). The empty selection swallowedappendand everyattr, and the first loud error is the line that reads from it.
Code & diagrams
Preview
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: nullThe arrow-function error appears because the code is a module: at the top level of a module `this` is `undefined`.
Preview
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: <b>Quay</b>
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).
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 perg, each holding its own rectangles, and the parent is thatg. This is what makes nested joins work (Topic 5.5). - ▸
select(singular) is special: it passes the parent's data to the child.selectAlldoes not. That is whyrow.select("rect")can usedin an attribute function androw.selectAll("rect")cannot. Topic 5.5 uses this.
Remember this
- 1
The problem this solves: changing 25 elements by hand means a loop with
document.querySelectorAll,forEachandsetAttributeevery 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
**
selecttakes the first match,selectAlltakes all of them.**d3.select("#list li")is one element (the firstliin 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
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
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 indexi, and the arraynodesof the whole selection. Inside afunction () {},thisis the current element. An arrow function has nothisof its own, sothisis not the element there (undefinedinside a module). Use(d, i, nodes) => nodes[i]when you want an arrow function and need the element. - 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 throwsCannot read properties of null. Checkselection.empty()when it matters. - 6
**
call,each,filter.**selection.call(fn, ...args)runsfn(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
What is the difference between select and selectAll?
What happens when you call attr("x", 5) on a selection that matched nothing? And attr("x")?
Why does (d, i, nodes) => this.textContent fail while function (d, i, nodes) { return this.textContent; } works?
Practice
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.
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,textandclassedand say what each returns.I know that
appendreturns the new elements, not the parents.I can say what an empty selection does for a setter and for a getter.