I've seen some D3 codes with a pattern like this for appending elements:
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle");
I really don't get this snippet. Why selecting null
?
The way I understand D3, if one is appending circles, it should be:
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle");
The same way, if one is appending HTML paragraphs it should be:
var circles = svg.selectAll("p")
.data(data)
.enter()
.append("p");
The same goes for classes: if one is appending elements with a class foo
, it should be selectAll(".foo")
.
However, selectAll(null)
does work! The elements get appended.
So, what's the meaning of that null
? What am I missing here?
Note: this is a self-answered question, trying to provide a "canonical" Q&A on a subject that has been touched on by many previous questions and not explained by the API. Most of the answer below is from an example I wrote in the extinct StackOverflow Documentation.
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.
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:
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:
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With