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.