Three Little Circles

Once upon a time, there were three little circles. This tutorial shows you how to manipulate them using selections.

Selecting Elements

The selectAll operator takes a selector string, such as “circle”, and returns a selection:

var circle = svg.selectAll("circle");

Once we have a selection, we can apply various operators to the selected elements. For example, we might change the fill color using style, and the radius and the y-position using attr:

circle.style("fill", "steelblue");
circle.attr("cy", 90);
circle.attr("r", 30);

We can also compute the attribute values dynamically, using functions rather than constants. For example, maybe we want to set the x-coordinate to a random value:

circle.attr("cx", function() {
  return Math.random() * w;
});

If you run this example multiple times, you’ll see that the attribute is recomputed as a number random number each time. Unlike Protovis, D3 doesn’t stash these functions internally; they are run once, immediately, and then your code continues. So you can run them again or redefine them however you like.

Binding Data

This is beginning to look a lot like jQuery. More commonly, though, we want to use data to drive the appearance of our circles. To do that, we need some data. For the sake of example, let’s imagine that each of these circles represents a number: 32, 57 and 112. The data operator binds these numbers to the circles:

circle.data([32, 57, 112]);

All data in D3 is specified as an array of values. Conveniently, this mirrors the concept of a selection, which is just an array of elements. Notice then how the first number (the first datum, 32) is bound to the first circle (the first element, on top), the second number is bound to the second circle, and so on.

Once data is bound, that data is accessible as an argument to our attribute and style functions. This means we visually encode data, or in other words, create a visualization! For example, here we set the x-position and radius using the data:

circle.attr("cx", function(d) {
  return d;
});

circle.attr("r", function(d) {
  return Math.sqrt(d);
});

There’s a second argument to each function you can use: it specifies the index of the element within its selection. This is a zero-based index, and it’s useful for computing offsets or as a simple way of identifying individual elements. The argument is optional; if you don’t specify it when declaring your function, it will be ignored. For example:

circle.attr("cx", function(d, i) {
  return i * 100 + 30;
});

Here we use the index i to position the elements sequentially only the x-dimension. Each element is separated by 100 pixels, with an offset of 30 pixels from the left side. In SVG, the origin is in the top-left corner.

Creating Elements

But what if we had four numbers to display, rather than three? We wouldn’t have enough circles to display all the numbers. When joining data to elements, D3 stores the leftover data in the enter selection. (The terms “enter” and “exit” are adopted from stage terminology.) Here, the fourth number 293 remains in the enter selection, because we only have three circle elements:

var circle = svg.selectAll("circle")
    .data([32, 57, 112, 293]);

Using the enter selection, we can create new circles for any missing data. Each new circle is already bound to the data, so we can use data to compute attributes and styles:

var enter = circle.enter().append("svg:circle");

enter.attr("cy", 90);

enter.attr("cx", 160);

enter.attr("r", function(d) {
  return Math.sqrt(d);
});

Taking this to the next logical step, then, what if we have no existing elements? Meaning, what if the document is empty? Say we start with an empty page, and we want to create new circles that correspond to our data? Then we’re joining data to an empty selection, and all of the data ends up in enter:

var enter = circle.enter().append("svg:circle");

enter.attr("cy", 90);

enter.attr("cx", function(d) {
  return d;
});

enter.attr("r", function(d) {
  return Math.sqrt(d);
});

This pattern is so common, you’ll often see the selectAll + data + enter + append operators called sequentially, one immediately after the other. Despite it being common, keep in mind that this is just one special case of a data join; we’ve already seen another common case (selecting elements for update) and we’ll see other interesting cases to consider in a bit.

Another technique you can use to make your code more concise is method chaining. Each operator in D3 returns the current selection, so you can apply multiple operators sequentially. For example, the above code can be rewritten:

svg.selectAll("circle")
    .data([32, 57, 112, 293])
  .enter().append("svg:circle")
    .attr("cy", 90)
    .attr("cx", String)
    .attr("r", Math.sqrt);

As you can see, the code is made even smaller using built-in JavaScript functions, rather than defining anonymous ones. The built-in String method, for example, is a shorthand way of using JavaScript’s default string conversion to compute the attribute value from the associated data. Similarly, we can plug in Math.sqrt to set the radius attribute as the square root of the associated data. This technique of plugging in reusable functions to compute attribute values is used extensively in D3, particularly in conjunction with scales and shapes.

Destroying Elements

Sometimes you have the opposite problem from creation: you have too many existing elements, and you want to remove them. You can select nodes and remove them, but more commonly, you’ll use the exit selection to let D3 determine which elements are exiting the stage. The exit selection is the opposite of the enter selection: it contains all elements for which there is no corresponding data.

var circle = svg.selectAll("circle")
    .data([32, 57]);

All that’s left to do, then, is to remove the exiting elements:

circle.exit().remove();

The enter, update and exit selections are computed by the data operator, and don’t change when you append or remove elements—at least until you call selectAll again. So, if you keep variables around that point to selections (such as circle, above), you’ll probably want to reselect after adding or removing elements.

All Together Now

Putting everything together, consider the three possible outcomes that result from joining data to elements:

  1. enter - incoming actors, entering the stage.
  2. update - persistent actors, staying on stage.
  3. exit - outgoing actors, exiting the stage.

When we use the default join-by-index, either the enter or exit selection will be empty (or both): if there are more data than elements, the extra data are in the enter selection; if there are fewer data than elements, the extra elements are in the exit selection. However, by specifying a key function to the data operator, we can control exactly how data is bound to elements. And in this case, we have both enter and exit.

var circle = svg.selectAll("circle")
    .data([32, 57, 293], String);

circle.enter().append("svg:circle")
    .attr("cy", 90)
    .attr("cx", String)
    .attr("r", Math.sqrt);

circle.exit().remove();

Want to learn more about selections and transitions? Read A Bar Chart, Part 2 for a practical example of using enter and exit to display realtime data.

Copyright © 2011 Mike Bostock
Fork me on GitHub