D3 General Update Pattern
July 2023 Update: I just released an interactive course on web application security — check it out on educative.io!
Ian Johnson, @enjalot, is creating Building Blocks — “a simple web application for editing code samples that are compatible with http://bl.ocks.org which is the de facto medium for sharing d3.js code examples.” His kickstarter campaign for the project is already funded twice over with 22 days left to go. It’s currently easy to go to bl.ocks.org and quickly be inspired by various projects. However, it’s more difficult to directly play around with the code. Ian Johnson is going to change this.
Building Blocks will allow many users that were previously unfamiliar with D3 to easily get their hands dirty with the code. Likewise, it will let more experienced D3 developers tweak settings around to get a deeper understanding of what is actually happening.
To celebrate, I’m going to discuss the “General Update Pattern” in D3. This is an introductory pattern that is key to understanding how data is bound to the DOM in D3.
The code that we will be discussing is located at http://bl.ocks.org/mbostock/3808218. Let’s get started.
var alphabet = "abcdefghijklmnopqrstuvwxyz".split("");
var width = 960;
var height = 500;var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
We append an svg
element with a certain width and height to the body. We then append a g
element to that svg
. A g
element is used as a container to group other objects. We also translate that g
element 32px right and 250px down to give some padding and more or less vertically center it.
// The initial display.
update(alphabet);setInterval(function() {
update(
d3
.shuffle(alphabet)
.slice(0, Math.floor(Math.random() * 26))
.sort()
);
}, 1500);
We initially call the update function with the entire alphabet. Then, every 1.5 seconds, we call the function with a random sample of letters from the alphabet, in alphabetical order. The data binding occurs in the update function.
function update (data) {
// DATA JOIN
var text = svg
.selectAll("text")
.data(data); // "UPDATE SELECTION"
text.attr("class", "update"); // "ENTER SELECTION"
text
.enter()
.append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em"); // "ENTER + UPDATE SELECTION"
// I.E., THE NEW UPDATE SELECTION
text.text(function(d) { return d; }); // "EXIT SELECTION"
text.exit().remove();
}
A data-join is followed by operations on the “enter, update and exit” selections. Let’s break down each step.
// DATA JOIN
var text = svg
.selectAll("text")
.data(data);
This will select all text
elements within the svg
and attach data to them. By default, data is attached by index so the data at data[0] will be attached to the first text
element while data at data[3] will be attached to the fourth text
element, for example. The most confusing thing about this pattern is that these text elements may or may not exist yet.
In this example, the variable text
will return an object with enter
and exit
methods. It will also have a property that points to the array of selected text
elements. If the text
elements don’t exist, this array will be empty. However, it will still be the same length as the data array that we passed in and the passed in data is still associated with each index.
If all the text
elements do exist, the elements will get new data, again corresponding their indexes. If some text
elements exist but we received a new data array whose length is longer than the number of text
elements we currently have in the DOM, then the existing text
elements will receive new data and our variable text
will also keep track of the new additional indexes/data for the data that has been received yet does not have corresponding elements to house that data.
// UPDATE SELECTION
text.attr("class", "update");
After the data join, our variable text
itself is our “update selection”. It corresponds to already existing elements in the DOM that have received new data. On the initial run of our update function, this will do nothing since there are no existing text
elements yet.
// "ENTER SELECTION"
text
.enter()
.append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
This is our “enter selection”. It corresponds to the data that has been received yet does not have corresponding elements to house that data. You can think of it as saying, “for each piece of data in variable text
that does not have an existing element yet, do the stuff after the enter() call”.
On our initial call to the update function, this will run for every piece of inserted data. We append a new text
element to our g
element, give it a class of “enter”, and set the x and y attributes for each piece of data.
// "ENTER + UPDATE SELECTION"
// I.E., THE NEW UPDATE SELECTION
text.text(function(d) { return d; });
Again, our variable text
corresponds to our “update selection”. After we conducted our “enter” operations, the variable updates to include all newly inserted text
elements. This is because our “update selection” just corresponds to existing elements in the DOM that have received new data.
// "EXIT SELECTION"
text.exit().remove();
This is our “exit selection”. This corresponds to all existing DOM elements in our selection for which no new pieces of data were found. It is common to want to “remove” the “exit selection” elements from the DOM. On our initial run through, this selection will be empty.
To bring it all together, say we have 5 text
elements in our svg
with data “a”, “b”, “c”, “d”, “e” respectively. Then, we call our update function with another round of data – “j”, “k”, “l”.
Our “data join” will join “j” with our first text
element, “k” with our second, and “l” with our third. “d” will still be associated with the fourth element and “e” will still be associated with the last element.
Our “update selection” will correspond with just the elements that received new data — the first three elements.
We will have no “enter selection” because we have elements for all of our new data pieces. We do not need to append any new elements to represent our data.
Our “exit selection” will correspond to our last two elements — the elements associated with “d” and “e”. No new data was found for these elements so we remove them.
In our next iteration, say we call our update function with an array of values — “f”, “i”, “m”, “p”, “q”.
We again bind our new data to our existing text
elements. However, we have 5 pieces of data and currently only 3 text
elements. These 3 elements will correspond to “f”, “i” and “m” respectively. See http://bl.ocks.org/mbostock/3808221 for an example on how to add a key to the “data join” to override the default “by index” binding.
Our “update selection” again corresponds with just the existing elements that received new data — the first three elements.
Our “enter selection” corresponds to data pieces “p” and “q” — the data that has no current elements to house it. Thus, we append a new text
element for each of them.
Our “exit selection” will be empty because there was new data for all of our currently existing elements.
A firm understanding of the “General Update Pattern” is key in D3 and many find the pattern very counterintuitive at first so I hope this article clears up some confusion!
Originally published at quintonlouisaiken.com on July 27, 2015.