D3 selections are a different way to look at data binding. Essentially, D3 maintains a mapping of data points to DOM elements, keeping track of exactly which data maps to which element. When data points are added, changed, or removed, the associated DOM elements can be programmatically added, updated, or removed correspondingly. This feature is extremely powerful, and allows you to add many different kinds of custom interactivity in your visualizations.

Note: The `selection.join` API used in this tutorial is only available to D3 v5 and later. For the older data binding pattern, please refer to previous versions of this tutorial.

Selections d3.selectAll

We’ve referenced d3.select() and d3.selectAll() a few times already but now, it’s really time to dig in. d3.select() will find one element, d3.selectAll will match all available elements.

With types, the functions might look something like:

d3.select(String selector) -> (d3.selection)

D3 selections are a group of elements that match a query or could match a query later (the elements may not have been constructed yet).

Joins selection.data() and selection.join()

Selections are used to map pieces of our data to elements in the DOM. Suppose we have some data:

var sales = [
  { product: 'Hoodie',  count: 7 },
  { product: 'Jacket',  count: 6 },
  { product: 'Snuggie', count: 9 },
];
    

And we want to map these to points on a scatterplot. We know we want each object in this array to turn into a <rect> tag, inside of our <svg> below:

<!-- before, empty graph -->
<svg>
</svg>
    
<!-- after, rects graph -->
<svg>
  <rect /><!-- { product: 'Hoodie',  count: 7 } -->
  <rect /><!-- { product: 'Jacket',  count: 6 } -->
  <rect /><!-- { product: 'Snuggie', count: 9 } -->
</svg>
    

To connect these, we’re going to create a selection and use .data() to bind our data to the selection.

var svg = d3.select('svg');
svg.size();
// 1 -- one <svg> element exists

var rects = svg.selectAll('rect')
    .data(sales);

rects.size();
// 0 -- no <rect> elements exist yet!
    

Okay, now we have a selection but still no elements! We have more work to do.

The selection.join() API allows us to define what happens when we join data with a selection. In other words, we use this API to define how to handle additions, changes, or removals to the data since the last join.

The selection.join() API takes 3 functions as arguments:

  • the first function will be called with a selection containing data points which do not have DOM elements yet
  • the second function will be called with a selection which contains all the data points
  • the third function will be called with a selection which contains data points which have been removed, but for which DOM elements still exist.

The second and third arguments are optional. This can be a bit confusing at first, but don’t worry. Continue reading, and I’ll explain how all this works through examples. Feel free to reference the official documentation.

In D3 selections, "enter" refers to data points which do not have a corresponding DOM element (data that was added since the last join) and "exit" refers to DOM elements which do not have a corresponding data point (data that was removed since the last join).

The `selection.enter()` and `selection.exit()` method of selections can be used to access these subsets - that was how we handled additions and removals before the `selection.join()` API existed.

Now, the "enter" and "exit" selections are automatically passed to the first and third arguments of `selection.join()` - we just need to provide functions to handle them.

Adding Elements

Again, our goal is to have a rectangle for each data point. We are starting with none and we have 4 new data points, so obviously the right thing to do is to add a new <rect> for each data point.

The way D3 looks at this is a more subtle: we want to add a <rect> per data point, but only for the new points since the last data join. Since this is the first data binding (there are no rects currently), everything is new, it’s straightforward to add new points. It’s important to keep in mind that for the next selection, things will be more complex since there will already be rects.

The part of a D3 selection that represents these element-less data-points is passed to the first argument in selection.join. The elements don’t add themselves, we have to create the elements that will match the selection ourselves. We use the same attribute editing helpers to configure each circle per its data point.

// recall that scales are functions that map from
// data space to screen space
var maxCount = d3.max(sales, (d, i) => d.count);
var x = d3.scaleLinear()
    .range([0, 300])
    .domain([0, maxCount]);
var y = d3.scaleOrdinal()
    .rangeRoundBands([0, 75])
    .domain(sales.map((d, i) => d.product));

rects.join(
    // NEW - handle data points w/o rectangles
    newRects => {
        newRects.append('rect')
        .attr('x', x(0))
        .attr('y', (d, i) => y(d.product))
        .attr('height', y.rangeBand())
        .attr('width', (d, i) => x(d.count));
    },
);
    
We're getting a little sneaky here! We're introducing an ordinal scale, one that's discrete instead of continuous.

The d3.scaleOrdinal() helps us create buckets for each element. In this case, that's one per product.

The domain is the 3 product names. The range is a little different, rangeRoundBands is a helper function that sets the range, but tells D3 to pick buckets that are whole pixel widths (no fractions).

So how does it turn out? Let’s take a look:

<svg width="300" height="100">
  <rect x="0" y="0"  height="25" width="233.33" />
  <rect x="0" y="25" height="25" width="200" />
  <rect x="0" y="50" height="25" width="300" />
<svg>
    
Check out how these attribute helpers can take immediate values as well as callbacks. Just like with d3.min, these callbacks use the same style of (d, i) parameters to represent the element and its index.

Removing Elements

Whereas “enter” selects elements that have added since the last data join, “exit” is the opposite, it applies to elements that have been removed.

Suppose we drop the first point from our source array, we can find and operate on the corresponding element in the DOM via the exit selection.

We can use the remove() method to immediately delete matched elements; it’s the opposite of append().

If you only want to delete matched elements, you may omit the argument entirely from selection.join() since calling remove() is the default behavior.

// define new logic for handling joins
rects.join(
    newRects => {
        newRects.append('rect')
        .attr('x', x(0))
        .attr('y', (d, i) => y(d.product))
        .attr('height', y.rangeBand())
        .attr('width', (d, i) => x(d.count));
    },
    rects => {},
    // NEW - delete elements whose data has been removed
    rectsToRemove => {
        rectsToRemove.remove();
    }
);

sales.pop(); // drops the last element
rects.data(sales); // join the data again
    

Identity and the Key Function

As a quick aside: Javascript object equality is very shallow. Objects are only equal if they are actually the same object (identity), not if they have the same values:

var obj1 = { value: 1 };
// true -- identity
obj1 == obj1;

var obj2 = { value: 2 };
var obj3 = { value: 2 };
// false -- huh? they have the same values!
obj2 == obj3;
    

But the example above works! It only removed one element from the DOM because we only removed one element from the array, and all the rest of the objects were the exact same.

What if we get a new page of data, with some overlap, but we no longer have the exact same object instances? Well, we will have to find some way to match objects to each other, and with D3, that’s where a key function comes in.

When we introduced selection.data() earlier, we left out the hidden second parameter, the key function. It’s another (d, i) callback.

This example keys objects on their date, so we can match elements across separate arrays.

var sales1 = [
  { product: 'Hoodie', count: 7 },
  { product: 'Jacket', count: 6 }
];

var sales2 = [
  { product: 'Jacket',  count: 6 }, // same
  { product: 'Snuggie', count: 9 }  // new
];

var rects = svg.selectAll('rect')
  .data(sales1, (d, i) => d.product)
  .join(enter => enter.append("rect"));

rects.size(); // 2 -- first join adds two new elements

// removes 1 element, adds 1 element
rects.data(sales2, (d, i) => d.product); 
    

Transitions selection.transition()

The key function is also important in case parts of our objects change – if we change a count, then we can update the appropriate element without having to delete and re-add the element, we can update it in place.

One of D3’s most visually pleasing features is its ability to help with transitions. The key function is critical here for object permanence.

Suppose we have per-product sales we want to update as more products are sold? We can use transitions to demonstrate this update.

Day 1
Product Sales (Cumulative)
Hoodie 10
Jacket 3
Snuggie 2
Day 2
Product Sales (Cumulative)
Hoodie 16
Jacket 7
Snuggie 8
function toggle() {
    sales = (sales == days[0]) ? days[1] : days[0];
    update();
}

function update() {
    svg.selectAll('rect')
    .data(sales, (d, i) => d.product)
    .join(
        enter => {
            enter.append('rect')
            .attr('x', x(0))
            .attr('y', (d, i) => y(d.product))
            .attr('height', y.bandwidth())
            .attr('width', (d, i) => x(d.count));
        },
        update => {
            update.attr('width', (d, i) => x(d.count));
        },
    );
};
    

Ok, but now time to make it pretty. That’s where selection.transition() comes in. In the above example, we were just using the plain update selection to change the values. Here, we’ll use transition() to make our transition much slicker.

transition() selections can have custom timing attributes like .duration() and .delay() and even a custom easing function .ease(), but the defaults are pretty nice.

function toggle() {
    sales = (sales == days[0]) ? days[1] : days[0];
    update();
}

function update() {
    svg.selectAll('rect')
    .data(sales, (d, i) => d.product)
    .join(
        enter => {
            enter.append('rect')
            .attr('x', x(0))
            .attr('y', (d, i) => y(d.product))
            .attr('height', y.bandwidth())
            .attr('width', (d, i) => x(d.count));
        },
        update => {
            // NEW!
            update.transition().duration(1000)
            .attr('width', (d, i) => x(d.count));
        },
    );
};
    

Ok! That was the basics of D3! We’ve got a few more complex examples, but they mostly build on what we’ve already shown.

Next