Selecting null: what is the reason behind selectAll(null) in D3?
Solution 1:
tl;dr
The objective of using selectAll(null)
is to guarantee that the "enter" selection always corresponds to the elements in the data array, containing one element for every element in the data.
The "enter" selection
To answer your question, we have to briefly explain what is an "enter" selection in D3.js. As you probably know, one of the main features of D3 is the ability of binding data to DOM elements.
In D3.js, when one binds data to DOM elements, three situations are possible:
- The number of elements and the number of data points are the same;
- There are more elements than data points;
- There are more data points than elements;
In the situation #3, all the data points without a corresponding DOM element belong to the "enter" selection.
Thus, In D3.js, "enter" selections are selections that, after joining elements to the data, contains all the data that don't match any DOM element. If we use an append function in an "enter" selection, D3 will create new elements, binding that data for us.
This is a Venn diagram explaining the possible situations regarding number of data points/number of DOM elements:
Binding data to already existing DOM elements
Let's break your proposed snippet for appending circles.
This...
var circles = svg.selectAll("circle")
.data(data)
... binds the data to a selection containing all circles. In D3 lingo, that's the "update" selection.
Then, this...
.enter()
.append("circle");
... represents the "enter" selection, creating a circle for each data point that doesn't match a selected element.
Sure, when there is no element (or a given class) in the selection, using that element (or that class) in the selectAll
method will work as intended. So, in your snippet, if there is no <circle>
element in the svg
selection, selectAll("circle")
can be used to append a circle for each data point in the data array.
Here is a simple example. There is no <p>
in the <body>
, and our "enter" selection will contain all the elements in the data array:
var body = d3.select("body");
var data = ["red", "blue", "green"];
var p = body.selectAll("p")
.data(data)
.enter()
.append("p")
.text(d=> "I am a " + d + " paragraph!")
.style("color", String)
<script src="https://d3js.org/d3.v4.min.js"></script>
But what happens if we already have a paragraph in that page? Let's have a look:
var body = d3.select("body");
var data = ["red", "blue", "green"];
var p = body.selectAll("p")
.data(data)
.enter()
.append("p")
.text(d=> "I am a " + d + " paragraph!")
.style("color", String)
<script src="https://d3js.org/d3.v4.min.js"></script>
<p>Look Ma, I'm a paragraph!</p>
The result is clear: the red paragraph disappeared! Where is it?
The first data element, "red", was bound to the already existing paragraph. Then, just two paragraphs were created (our "enter" selection), the blue one and the green one.
That happened because, when we used selectAll("p")
, we selected, well, <p>
elements! And there was already one <p>
element in that page.
Selecting null
However, if we use selectAll(null)
, nothing will be selected! It doesn't matter that there is already a paragraph in that page, our "enter" selection will always have all the elements in the data array.
Let's see it working:
var body = d3.select("body");
var data = ["red", "blue", "green"];
var p = body.selectAll(null)
.data(data)
.enter()
.append("p")
.text(d=> "I am a " + d + " paragraph!")
.style("color", String)
<script src="https://d3js.org/d3.v4.min.js"></script>
<p>Look Ma, I'm a paragraph!</p>
And that's the purpose of selecting null: we guarantee that there is no match between the selected elements and the data array.
Selecting null and performance
Since we are not selecting anything, selectAll(null)
is by far the fastest way to append new elements: we don't have to traverse the DOM searching for anything.
Here is a comparison, using jsPerf:
https://jsperf.com/selecting-null/1
In this very simple scenario, selectAll(null)
was substantially faster. In a real page, full of DOM elements, the difference may be even bigger.
When NOT to use selectAll(null)
As we just explained, selectAll(null)
won't match any existing DOM element. It's a nice pattern for a fast code that always append all the elements in the data array.
However, if you plan to update your elements, that is, if you plan to have an "update" (and an "exit") selection, do not use selectAll(null)
. In that case, select the element (or the class) you plan to update.
So, if you want to update circles according to a changing data array, you would do something like this:
//this is the "update" selection
var circles = svg.selectAll("circle")
.data(data);
//this is the "enter" selection
circles.enter()
.append("circle")
.attr("foo", ...
//this is the "exit" selection
circles.exit().remove();
//updating the elements
circles.attr("foo", ...
In that case, if you use selectAll(null)
, the circles will be constantly appended to the selection, piling up, and no circle will be removed or updated.
PS: Just as a historical curiosity, the creation of the selectAll(null)
pattern can be traced back to these comments by Mike Bostock and others: https://github.com/d3/d3-selection/issues/79