D3.js tutorial - Part 7 - Data binding

In D3.js, D3 stands for Data-Driven Document. It means that charts can be created from data. This concept is the key of D3.js, and the main feature is data binding.

Let's take a simple illustration, assume you have a set of data to display in a bar chart. You can create a loop to iterate over your data and display each bar. D3.js offers a better option: data binding. With this feature, you can associate your set of data to you chart in a more convinient way.

Simple example

Let's start with a very simple example (from Scrimba):

// Set of data
var dataset = ['A', 'B', 'C', 'D', 'E'];

// For each data from dataset, create a new section
d3.select('body')
    .selectAll('p')
    .data(dataset)
    .enter()
    .append('p')
    .text('Paragraph');

You can see that, without loop, the previous example displays 5 paragraphs:

What may seems weird here is that the selection is done before appending the paragraphs. selectAll('p') is called before append('p'). Let's detail the source code.

How it works?

If you still don't understand this section, don't worry! Just accept the idea that D3.js will create new elements according to the size of the dataset.

First, we select the body (for later appending the paragrahs). Then we select all the existing paragraphs. Since there no existing paragraphs, selectAll('p') returns a new empty selection. Here is the object returned:

_groups: [NodeList(0)]
_parents: [body]
__proto__: Object

When calling data(dataset), the previous empty selection is joined to a 5 elements array. .data(dataset) returns 5 empty new selection:

_groups: Array(1)
    0: (5) [empty × 5]
    length: 1
    __proto__: Array(0)
_parents: [body]
_enter: [Array(5)]
_exit: [Array(0)]
__proto__: Object

enter() identifies any DOM elements that needs to be added when the joined array is longer than the selection. That the case here, since our selections are empty.

_groups: Array(1)
    0: (5) [rt, rt, rt, rt, rt]
    length: 1
__proto__: Array(0)
_parents: [body]
__proto__: Object

When calling the append('p'), the <p> tag is appended to the 5 pending element:

_groups: Array(1)
    0: (5) [p, p, p, p, p]
    length: 1
__proto__: Array(0)
_parents: [body]
__proto__: Object

You can see here that 5 <p> elements are created in the DOM.

If it's stil not clear, you can find a more detailed explanation here: Thinking with Joins.

Data binding

Five new elements are created, but our dataset is not linked to our elements. Fortunately, the .text() function can be called with an anomymous function as parameter. Here is the syntax to link our dataset to our <p> tags.

// Set of data
var dataset = ['A', 'B', 'C', 'D', 'E'];

// For each data from dataset, create a new section
d3.select('body')
    .selectAll('p')
    .data(dataset)
    .enter()
    .append('p')
    .text(function(id) { return 'Section ' + id; });

This is how data are linked with new elements:

Some remarks

The join only exists when the data (dataset) functions are called. Do not imagine that the charts will update automatically when the data changes. We will have to associate the data again with the update () function this time.

Anonymous function can be written as arrow functions. This syntax:

.text(function(id) { return 'Section ' + id; });

is equivalent to:

.text((id) => 'Section ' + id; );

In the following, I'll use arrow functions syntax.

The more you'll practice, the more you'll understand why this mecanism is more convinient than iterating over datasets.

See also


Last update : 05/14/2021