d3.js - What is the purpose of selectAll that returns non-existing element

Viewed 1042

With d3.js I have following simple code that adds rectangles based off of dataset.

I do not have svg nor rect so I am appending it via .append().

            let dataset = [ 1, 2, 3, 4, 5 ]
            let svg = d3.select("body")
                        .append("svg")
                        .attr /* width, height */

            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr /* x, y, width, height, etc */

This works perfectly.

What I have question is selectAll("rect").

Why is this needed? If I remove it like below, the rect elements will go outside of body element.

But wasn't svg's append suppose to append to end of referenced element? (In this case svg?)

            svg.data(dataset)
               .enter()
               .append("rect")
               .attr /* x, y, width, height, etc */

I know selectAll returns reference.

If the element does not exist, does it return special reference?

1 Answers

D3 uses a paradigm for updates called the data join. It's optimized for incremental updates of data, which fits chart usage.

To use the data join, you operate on "selections". Instead of representing what you want to do, with selections, you represent how you want the data to transform.

let dataset = [ {name: "a", value: 1}, {name: "b", value: 2}, {name: "c", value: 3} ]
let svg = d3.select("body")
  .append("svg")

const selection = svg.selectAll("rect")
  .data(dataset, d => d.name) // data join, each "rect" will correspond to an data
// second argument is a key function to uniquely identify each data

selection
  .enter()
  .append("rect")
  /* attr: x, y, height, width */

selection represents the data join. Here, you tell D3 that you're making a selection. Right now, nothing exists in the DOM, though, so we get an empty selection.

When you define the selection:

  • the DOM has no "rect" nodes
  • but there is data

Looking at it another way:

         data
 data  &nodes  nodes
       +-----+
+--------------------+
|      |     |       |
|  a:1 |     |       |
|  b:2 |     |       |
|  c:3 |     |       |
|      |     |       |
+--------------------+
       +-----+

The enter selection will contain the data with no nodes. So this is what data you are manipulating when using enter.

Say we change the dataset now. How do we update the DOM nodes?

dataset =  [ {name: "a", value: 9}, {name: "c", value: 3}, {name: "d", value: 4} ]
// note how we need to select rect to get the existing data.
// this is the "update" selection
const selection = svg.selectAll("rect")
  .data(dataset, d => d.name)
  .attr("x", d => d.value) 

// the "enter" selection
selection.enter()
  .append("rect")

// the "exit" selection
selection.exit().remove()

What we have now are three selections. If we draw our diagram again, here's where our data is now:

         data
 data  &nodes  nodes
       +-----+
+--------------------+
|      |     |       |
| d:4  | a:9 |  b:2  |
|      |     |       |
|      |     |       |
|      |     |       |
+--------------------+
       +-----+

What happens at the time we do the data join? The data has changed. Now svg.selectAll("rect") takes a selection that already exists: D3 saved in in the DOM. This is why you need to pass a CSS selector to selectAll. D3 uses that selector to find the existing selection, saved in the DOM via a .__data__ attribute on each DOM node.

Now we're observing new data, and we compare it to what we have in the DOM:

  • a still has a data and it has a node from the previous data join. We know a is still there, but its value has changed since our data are objects with {name, value}. So it's in "data&nodes".
  • b has a node in the dom, but it doesn't exist in the corresponding new data. It's in "nodes".
  • d is a new item: it's data without a node. So it goes in "data".

Since c didn't update, it isn't in any selection.

Let's rename things in our graph:

 enter  update  exit
       +-----+
+--------------------+
|      |     |       |
| d:4  | a:9 |  b:2  |
|      |     |       |
|      |     |       |
|      |     |       |
+--------------------+
       +-----+

When you use a selection directly (the update selection), call enter(), or call exit(), you are always working on either of those three selections. It's much more efficient, especially for large data, to manage updates only when we need to, or to not recreate the nodes every time - only destroying when we need it.

Even if you only use the enter() transition, D3 will be more efficient by making sure only new items are created.

The idea is that you can put a snippet managing all three states in something like a render() function, and call it all the time: when you first generate the graph and when you update data. D3 manages all the data bookkeeping for you.

Related