Selections And Data Binding - D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

Selections And Data Binding

This chapter covers D3’s “selections” and how they can be combined with data to expressively create and manipulate HTML documents. We’ll use selectors all throughout our D3 code so the content in this chapter is fundamental to using D3.

warning

Those reads familiar with jQuery are strongly encouraged not to skip this chapter. Even though D3’s selectors are similar, they deviate in a few key areas.

Selections

Selectors are objects that represent collections (or sets) of DOM elements. This allows us to modify properties on the selector and have those changes applied to all the DOM elements in it. This is an extremely powerful idea! It’s now no longer our job to be explicit in how the program should go about changing all the elements. Instead, we only need to specify which elements should change and their associated changes. Say we wanted to change the background of a bunch of <div> tags. Instead of having an array of DOM elements and modifying each one directly:

1 // `divs` is just an array of DOM elements

2 for(var i = 0; i < divs.length; i++){

3 divs[i].style.backgroundColor = 'blue';

4 }

error

Live version http://jsbin.com/UdiDiQu/2/edit

We can apply modifications to each <div> element using the selector.

1 // `divs` is a selector object

2 divs.style('background-color', 'blue');

error

Live version: http://jsbin.com/ADeReRo/1/edit

The last two example snippets perform exactly the same task. The latter example also introduced our first selector method, style, which changes the background color of all the divs in the selection to blue. We’ll talk about this method more below. For now, just understand that selectors give us a vocabulary for talking about groups of DOM elements and that their methods apply to each containing DOM element within the selector.

So how do we create new selectors? Fortunately, selectors in D3 follow the same convention used in CSS. (This should also feel very familiar if you have ever used jQuery.) In the same way a CSS rule applies to a set of DOM elements, we can collect DOM elements to put in our selector using the same rules. Here’s an example of a snippet of CSS which applies to all <div> tags that have the class ‘foo’:

1 div.foo{

2 /* CSS applied to all <div>'s of class "foo" */

3 }

With selectors, how we specify which elements we want to select is exactly the same. We can still just use div.foo by passing it to d3.selectAll. The result, is a new selector object.

1 var fooDivs = d3.selectAll('div.foo');

The selector object fooDivs now contains all the divs on the page that have class foo. It’s helpful to think about the d3.selectAll searching the entire DOM for all the elements that are “divs” and of the class the class “foo”. As a refresher, here’s a few CSS rules and their associated meaning.

CSS rule

Meaning

.foo

select every DOM element that has class foo

#foo

select every DOM element that has id foo

div.foo

select all the <div> tags that have class foo

div#foo

select all the <div> tags that have id foo

div .foo

select every DOM element that has class foo that’s inside a <div> tag

div #foo

select every DOM element that has id foo that’s inside a <div> tag

div p .foo

select every DOM element that has class foo that’s inside a <p> tag that’s inside a <div> tag

warning

Unlike CSS specified in stylesheets, a selector object only contains the elements that matched the selection rule when the selection was first created. In the example above, if new <div> tags were added to the DOM with class foo they would not automatically be in the fooDivs selector.

Selector methods

The methods on selectors apply to all its containing DOM elements, minus a few exceptions like the selector size() method which returns the number of elements in the selector.

style

Take a look at the following example that changes the background color of every <div> on the page to blue using the style() selector method:

1 // change all of the divs to have a background color of blue

2 d3.selectAll('div')

3 .style('background-color', 'blue');

If the code in our <body> tag was the following before running the code.

1 <body>

2 <div>Hello</div>

3 <div>World</div>

4 <div>!</div>

5 </body>

After the DOM loads and our JavaScript runs, the DOM now looks like this:

1 <body>

2 <div style="background-color: blue;">Hello</div>

3 <div style="background-color: blue;">World</div>

4 <div style="background-color: blue;">!</div>

5 </body>

error

Live version: http://jsbin.com/iYASaLoX/1/edit

tip

Quick testing

A quick way to test out small snippets of D3 code is to pull up the Chrome Developer Tools while visiting d3js.org. the global d3 object will be available there.

As it stands all our divs will become blue, but what if we wanted to apply a different style to each div? The style() method (along with most other selector methods) can be passed an accessor function instead of a value. This accessor function will be called for each element in the selector and its result will be used to set the ‘background-color’ (or whatever other style property we specify.)

Working from our previous example, we can randomly change the color of every <div> to red or blue using this technique. We’re just passing the style method a function instead of a string.

1 // blue and red the divs!

2 d3.selectAll('div')

3 .style('background-color', function(d, i) {

4 if(Math.random() > 0.5) {

5 return 'red';

6 }else{

7 return 'blue';

8 }

9 });

error

Live version: http://jsbin.com/agibaYo/1/edit

The accessor we pass to style also gets called with two arguments, here named d and i. i is the index of the current element in the selector. If we wanted to instead color only the first div blue and the rest green we can use the i property to check the index and apply the color accordingly.

1 d3.selectAll('div')

2 .style('background-color', function(d, i){

3 // make only the first div `blue`, the rest `green`

4 if(i === 0){

5 return 'blue';

6 }else{

7 return 'green';

8 }

9 });

error

Live version: http://jsbin.com/uTOQAtIT/2/edit

The first d argument stands for datum we’ll come back to this when we talk about data binding, but for the time being, let’s just ignore it.

tip

You can make these two arguments whatever you like but d and i are the common convention in the D3 community.

attr

Another selector method that operates almost identically to style() is attr(). It follows the same calling conventions but is used to modify attributes of the selected DOM elements (instead of only working on the style attribute like the style() method).

Given a <body> with the following content:

1 <body>

2 <div>Hello</div>

3 <div>World</div>

4 <div>!</div>

5 </body>

We can set the width of all of the <div> elements to 100% by using the attr() method, like so:

1 d3.selectAll('div')

2 .attr('width', '100%');

Applying this to the previous HTML would result in the following DOM:

1 <body>

2 <div width="100%">Hello</div>

3 <div width="100%">World</div>

4 <div width="100%">!</div>

5 </body>

error

Live version: http://jsbin.com/uNiTovuJ/1/edit?html,output

For convenience, selector methods (like style() and attr()) can be passed an object hash’s instead of individual key/value pairs.

This allows us to shrink the following code into a single call:

1 var divs = d3.selectAll('div');

2 divs.attr('width', '100%');

3 divs.attr('height', '100%');

4 divs.attr('background-color', function(d, i) {

5 i % 2 === 0 ? 'red' : 'blue'

6 });

The above code becomes just this:

1 d3.selectAll('div').style({

2 'width': '100%',

3 'height': '100%',

4 'background-color': function(d, i){

5 return i % 2 === 0 ? 'red' : 'blue';

6 }

7 });

Applying this to the previous DOM from above would result in the following:

1 <body>

2 <div style="width: 100%; height: 100%; background-color: red;">Hello</div>

3 <div style="width: 100%; height: 100%; background-color: blue;">World</div>

4 <div style="width: 100%; height: 100%; background-color: red;">!</div>

5 </body>

error

Live version: http://jsbin.com/uNiTovuJ/2/edit

Most selector methods return a reference to the original selector. That is, in JavaScript they return the self object. This allows us to easily chain selector method calls:

1 d3.selectAll('divs')

2 .style(...).attr(...);

information

Selectors are not simply arrays

Although it is convenient to think of selectors as arrays, they are not and we cannot simply use the [] API to access individual elements in the selector.

Data binding

warning

Data binding and joining with selections is probably one of the hardest concepts to wrap your brain around in D3 so don’t get discouraged if you have to read through this section a few times to make sure you really get how they work. It will also provide the foundation for almost everything that comes later.

Data binding might sound like a complicated term, but it really only means updating what gets shown based on some piece of information (or data.) As a simple example, image I was building a football scoreboard application. The act of updating a teams score on the score board when a player makes a point would be an example of data binding. Some part of the user interface was updated based on some piece of data.

When talking about data binding within HTML, we usually mean changing the contents of some visible property of a DOM element based on some variable. This could mean updating the text within a DOM element or changing one of its style properties, like its opacity. Since this task is so common in data visualization, D3 uses a new way of performing data binding. Unlike with jQuery’s selections, D3 lets us easily associate data with DOM elements which makes modifying those elements a lot easier and, more importantly, take less code to write. The trade off is that we have to really understand how selections operate under the hood to get their benefit.

Up until now, what we’ve described of D3 selections has been a bit of a white lie. Selections are slightly more than just collections of DOM elements. They are actually collections of data and DOM element pairs. Here’s what a selector looks like in jQuery. It’s just a collection of DOM elements.

jQuery style selector

jQuery style selector

Here’s what a D3 selector looks like.

D3 style selector

D3 style selector

The dashed blue lines represent empty data elements for each of the three <div> tags.

If we were to then “bind” a new array of data to those data/element pairs, say [18, 4, 7], we would now have a selector that looks like the following.

And here’s the D3 code that would produce this selector

1 var selector = d3.selectAll('div').data([18, 4, 7]);

18 is now associated with the first <div> tag, 4 with the second, and 7 with the third.

As you’ve just seen, the selector data() method takes an array of data and binds each data item in that array to each DOM element in selector. To demonstrate this, let’s build a simple bar chart. We’ll start off with this HTML.

1 <body>

2 <style>

3 .bar{

4 height: 10px;

5 background-color: blue;

6 border-bottom: 1px solid white;

7 }

8 </style>

9 <div class="bar"></div>

10 <div class="bar"></div>

11 <div class="bar"></div>

12 </body>

And the JavaScript:

1 var data = [10, 30, 60];

2 d3.selectAll('.bar').data(data)

3 .style('width', function(d){

4 return d + '%';

5 });

error

Live version: http://jsbin.com/AlIruJuW/1/edit

Remember that style and attr will apply style or attribute changes to each DOM element in the selector. If they’re passed a function, that function will be run for each element. Now that we know selectors are really collections of data/element pairs, it’s more appropriate to say these accessor functions are actually getting called for each data/element pair. The first argument to this function (often labeled d) is the data item (or datum) for the current data/element pair. The second, recalling from the previous section, is the current index within the selector.

But what if we don’t know ahead of time how many bars we’re going to have in our bar chart? This is where the enter() method comes in.

.enter() method

So you may have noticed in the previous example, data() seemed to be returning the original selector. Although it is updating the data/element pairs in the original, it is also returning an entirely new selector. The old selector and the new selector both happen to have the same number of data/element pairs but they wont always. We could have an empty selector and create a new one by binding a data array of length 3 to it. Our data selector would now look like the following:

But our original selector would still be empty. Selections returned from calling data() come with a method called enter() which also returns a new selector. This new selector will contain all the data/element pairs that do not yet contain elements. In our case, it will seem identical to the selector originally returned from data() since in the beginning all the data/element pairs contain no elements. As we’ll see a bit later, this wont always be the case. Our original selector might have more or less data/element pairs than the selector returned from data(). If there were fewer, calling enter() would return an empty selector.

We can now add our missing DOM elements using append('div') on the selector returned from enter(). Our selector now looks like the following.

To summarize, we can use .enter() and .append() to create new DOM elements for lonely data items. Let’s update our bar chart example above to incorporate this new feature. Since we’ll be creating the DOM elements, we don’t need to have them hard coded in the <body> tag anymore.

1 <body>

2 <style>

3 .bar{

4 height: 10px;

5 background-color: blue;

6 border-bottom: 1px solid white;

7 }

8 </style>

9 </body>

1 var data = [10, 30, 60];

2 d3.select('body').selectAll('.bar').data(data)

3 .enter().append('div')

4 .style('width', function(d){ return d + '%'; })

5 .attr('class', 'bar');

Notice in the code we also had to select the body tag first. Selections can be performed relative to other selections. In this case, we need to perform the selection relative to the body so that D3 knows where any new <div> tags will be added. We also need to specify the CSS class so that our CSS rule for each .bar can take effect.

error

Live version: http://jsbin.com/eNOCuTeL/1/edit

The result looks the same but now our code is more flexible. To illustrate this, let’s walk through an example of creating a bar chart with an arbitrary number of bars. In this example, we’ll use d3.range(n) which simply produces an array of the specified length n; d3.range(3) returns the array[0, 1, 2] d3.range(4) return the array [0, 1, 2, 3], and so on.

1 var data = d3.range(100);

2 d3.select('body').selectAll('.bar').data(data)

3 .enter().append('div')

4 .style('width', function(d){ return d + '%'; })

5 .attr('class', 'bar');

error

Live version: http://jsbin.com/eNOCuTeL/3/edit

Remember that enter() produces a new selector of all the data/elements pairs that have no element in the selector returned from data(). So, if we reran the code above by copying and pasting it bellow itself, the result would appear the same and no new <div> tags would be added. This is because the d3.select('body').selectAll('.bar') would return 100 data/element pairs. Binding them to a new array of length 100 would simple replace all the old data values in the data/element pairs and since none of the data/element pairs would be missing an element, the enter() would return an empty selector.

Lets walk through a simply example of rerunning an .enter(). Notice how running the second selection with different data updated the first 3 <div> tags created in the first selection and then adds one more.

1 d3.select('body').selectAll('.bar').data([18, 4, 7])

2 .enter().append('div')

3 .attr('class', 'bar')

4 .style('width', function(d){ return d + '%'; });

5

6 d3.select('body').selectAll('.bar').data([18, 4, 7, 11])

7 .enter().append('div')

8 .attr('class', 'bar')

9 .style('width', function(d){ return d + '%'; });

Here’s what the document looks like after the first selection but before the second.

Here’s the document after the second selection.

error

Live version: http://jsbin.com/iHeQoXuP/1/edit

The second data() selection looks like the following before the .enter() occurs. The dotted border represents what the .enter() selection will be.

information

Root DOM elements

If we do not include the first line of d3.select('body'), D3 won’t know where to place our elements, so it picks the topmost DOM element (or the root element), which is the <html> element.

We need to specify where we want to place our D3 chart. This does not need to be the <body> element and can be any element in the DOM.

exit()

exit() works similar to enter() but unlike .enter() returns a new selector of data/element pairs without elements.

Here’s a quick example of using exit() assuming the DOM currently has three <div class="bar"></div> tags.

1 d3.select('body').selectAll('.bar').data([8, 3]).exit().remove();

error

Live version: http://jsbin.com/UpIgiwah/1/edit

In this example we’ve also introduced the remove() selector method which, like append(), simply removes each DOM elements in the selector. Here the DOM originally had 3 <div> tags with the .bar class. Those elements are then paired with an array with only two data items. Because the 3rd element is now no longer paired with a data item exit() will create a new selector with just it. remove() then removes it.

General update pattern

Let’s walk through a slightly more complicated example of using enter(), exit(), and regular selectAll() selections to create a dynamically updating bar chart. Assuming we’ve taken care of the styles, we’ll first need to create a selector for our future bars. This tells D3 where within the DOM to add new elements if we call append().

1 var bars = d3.select('body').selectAll('.bar');

Next, we’ll create a function called update() that will be responsible for periodically updating our bar chart with new data.

1 function update(){

2 // our update code will go here!

3 }

4 setInterval(update, 800);

Inside of it, we’ll use d3.range(n) again to create a new array of length n as our data. So we can demonstrate adding or removing bars, we’ll give n a random value between 0 and 100 each time update() is called.

1 function update(){

2 var n = Math.round(Math.random() * 100);

3 var data = d3.range(n);

4 bars = bars.data(data);

5 }

Next, we need to add or remove elements depending on if there’s a data item for each.

1 // if data.length is now larger, add new divs for those data items

2 bars.enter().append('div')

3 .attr('class', 'bar');

4 // if data.length is now smaller, remove the divs that would no longer

5 // have a data item.

6 bars.exit().remove();

The very last step is to update all the still remaining <div> tags. This includes the <div> tags that didn’t get removed and any recently added <div> tags.

1 bars

2 .style('width', function(d){ return d / (n-1) * 100 + '%' })

3 .style('height', 100 / n + '%');

error

Live ve