D3 Fundamentals
Intro
Getting into the newer versions of the D3 library.
Selection Methods
.select
// native
document.querySelector('p')
// D3
d3.select('p')
At first glance these appear to the same thing, and they are similar. However d3.select
returns a object that contains additional methods, and could be considered easier to work with. For example it also returns the parent element as an item in the Select object that is returned.
Both of these methods will select the first object that matches. D3 has additional selection methods.
Selection Principles
If a method selects or creates an element, then you will be returned a new selection.
If a method manipulates a selection, then you will be returned the same selection, with the manipulations included.
Transformation Methods
Transformation methods will always return a selection. It will be the previous selection + the changes the transformation made to it. This is a very important concept to understand in D3.
.append
We now have a basic idea of how to select items using D3. D3 can also add nodes to the DOM. Which if you think about it, of course it can, how else could it draw a graph on the screen if it couldn't generate node elements. But in fact D3 can add any element to the DOM. The most common transformation method is .append
const body = d3.select("body").append('p');
console.log(body);
Doing this we have created a paragraph tag in the body below the D3 script. And we have also selected the paragraph tag. This technique of adding methods on the end is called method chaining and is a very common syntax in D3.
You can also break the chain like so
const body = d3.select("body")
const p = body.append('p')
console.log(body);
This provides the same result.
.classed
You could add a class to an element using the .append
method like this.
const el = d3.select("body")
.append('p')
.attr('class', 'foo') // highlight-line
.text('Hello World')
console.log(el);
However this replaces any other classes that may have been present. There is a D3 method specifically for adding or removing classes.
const el = d3.select("body")
.append('p')
.classed('foo', true) // highlight-line
.classed('bar', true) // highlight-line
.text('Hello World')
console.log(el);
The boolean argument specifies whether we want to add or remove the class. If the class already exists on the element nothing happens. If the boolean is false the specified class will be removed. In this way we can modify the classes of elements without destroying previously assigned classes.
.style
The style method can be used to transform the style of the selected elements (nodes).
const el = d3
.select("body")
.append("p")
.classed("foo", true)
.classed("bar", true)
.text("Hello World")
.style("color", "blue"); // highlight-line
console.log(el);
Data Methods
There are many kinds of data, but D3 is designed two work with only two types, text and numbers.
.data
The process of associating a piece of data with an element is known as joining data. D3 can accomplish this easily with the .data
method.
Given the following HTML
<ul>
<li>Hello</li>
<li>Hello</li>
<li>Hello</li>
<li>Hello</li>
<li>Hello</li>
</ul>
const data = [10, 20, 30, 40, 50];
const el = d3.selectAll("li")
.data(data)
console.log(el);
We can see that our el
selection has a few properties we haven't seen before _enter
and _exit
along with the _groups
array that we expect. More on these new properties later.
and then if we dig into our _groups
array we can see that D3 has added over a hundred new properties to this element, including __data__
which is our associated datapoint.
_enter
The _enter
selection has to do with joining data to elements, specifically if the number of elements we have selected does not match the number of datapoints. We want these numbers to match. If there are too many elements we want to remove them, and if there are not enough we want to add them.
When you use the .join
method and there aren't enough elements selected for the data, the spare data points go into the _enter
selection. The _enter
selection contains the items that have not been joined. The next step is to create elements that can be joined with the remaining data.
.join
The .join
method looks for items in the _enter
selection and then creates a new element for each of the items there. So given an original list like this:
<ul>
<li>Hello</li>
<li>Hello</li>
</ul>
const data = [10, 20, 30, 40, 50];
const el = d3.selectAll("li")
.data(data)
.join('li')
console.log(el);
We will get three additional <li>
elements in the dom, although they will be empty and not joined directly to the previous items.
The reason that these new <li>
tags are inserted into the wrong place is because they are inserted below our current selections parent element, which is currently the <html>
tag. We need to update our selection so that the parent in the selection is our <ul>
.
The way the parent selection works is by referencing the previous selection. However in this case we have no previous selection, so it defaults to the <html>
tag.
const data = [10, 20, 30, 40, 50];
const el = d3.select("ul").selectAll("li") // highlight-line
.data(data)
.join('li')
console.log(el);
Our elements are now inserted in the proper place. We can also use D3 to apply a particular text to these elements, which can overwrite all the manually entered text.
<ul>
<li>Manual Text</li>
</ul>
const data = [10, 20, 30, 40, 50];
const el = d3.select("ul").selectAll("li")
.data(data)
.join('li')
.text('D3 generated text')
console.log(el);
_exit
The exit selection is similar to the enter selection, but it captures extra elements that don't match the data. When you call the .data
method D3 creates an _exit
selection. Just as the .join
method added additional elements before, it can also remove unnecessary elements.
<ul>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
</ul>
const data = [10, 20, 30, 40, 50];
const el = d3.select("ul").selectAll("li")
.data(data)
.join('li')
.text('D3 generated text')
The .join
method handles joining our elements to our data so that they are equally numbered, whether too few or too many.
Displaying Data
Because our D3 methods are able to accept functions, we can dynamically insert our data into the elements that we have generated.
const data = [10, 20, 30, 40, 50];
const el = d3
.select("ul")
.selectAll("li")
.data(data)
.join("li")
.text(function (d){
return d
});
Where d
is the value of the data at each datapoint. Because of ES6 syntax this can be shortened to this.
const data = [10, 20, 30, 40, 50];
const el = d3
.select("ul")
.selectAll("li")
.data(data)
.join("li")
.text((d)=>{
return d
});
console.log(el);
Which can be shortened even further to this:
const data = [10, 20, 30, 40, 50];
const el = d3
.select("ul")
.selectAll("li")
.data(data)
.join("li")
.text(d => d);
Which is the common syntax that you will see in all the examples.
Enter, Update & Exit (new pattern)
We can manipulate the data when it is joined, based on which part of the selection it lives. The order of the functions within the join manipulation matters. The first will always be provided the enter
selection, the second the update
selection, and the third the exit
selection.
Enter
Because the .join
method is aware of either missing elements or extra elements due to the selection.enter
and selection.exit
sub-selections, we can actually manipulate these individual selections. Let's say we have three <li>
elements like so.
<ul>
<li>Manual Text</li>
<li>Manual Text</li>
<li>Manual Text</li>
</ul>
const el = d3
.select("ul")
.selectAll("li")
.data(data)
.join(enter => {
return enter
.append("li")
.style("color", "purple");
})
.text(d => d);
Update
Then we can also style the elements that are being updated, and delete the extra elements that are selected with selection.exit
using the .remove()
method.
const data = [10, 20, 30, 40, 50];
const el = d3
.select("ul")
.selectAll("li")
.data(data)
.join(
enter => {
return enter
.append("li")
.style("color", "purple")
},
update => update.style('color', 'green'),
exit => exit.remove()
)
.text(d => d);
Enter, Update & Exit (old pattern)
This is the old pattern for manipulating the enter, update and exit selections. However it is extremely common in the online examples and is therefore worth understanding.
const data = [10, 20, 30, 40, 50];
const el = d3
.select("ul")
.selectAll("li")
.data(data)
.text(d => d);
// targeting the _enter selection
el.enter()
.append(`li`)
.text(d => d)
// targeting the _exit selection
el.exit().remove()
Here we are dealing with the 3 different selections separately, as opposed to using the .join
method to simplify this process.
That wraps up the fundamentals. In the next article we will work on some actual visualizations.
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.