Skip to main content

D3 Fundamentals

Intro

Getting into the newer versions of the D3 library.

D3 API Documentation

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.

d3 selection

Both of these methods will select the first object that matches. D3 has additional selection methods.

D3: 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.

selection

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.

new element properties

_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.

new elements not joined

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);

D3 generated text

_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')

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);

data in li

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);

purple enter elements

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);

updated elements are green

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

Free 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.

Learn More

BidBear

bidbear.io

Bidbear 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.

Learn More