Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Selecting null: what is the reason behind selectAll(null) in D3?

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.

like image 263
Gerardo Furtado Avatar asked Sep 11 '17 01:09

Gerardo Furtado


1 Answers

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:

  1. The number of elements and the number of data points are the same;
  2. There are more elements than data points;
  3. 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:

enter image description here

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

like image 95
Gerardo Furtado Avatar answered Oct 03 '22 19:10

Gerardo Furtado