More D3 Joins: Enter, Update, and Exit

Getting Started

In the previous lesson on the basics of D3 data joins, we introduced the code below which creates a bar chart to represent the num data array.
// The data array
let nums = [1, 2, 3, 4, 5, 6];

// This will create an empty selection since we have no rectangles yet!
let existing_rects = d3.select("svg").selectAll("rect");

// Perform the data join
let data_join_result = existing_rects.data(nums);

// Join the unmatched data items to new rectangles.
data_join_result.join("rect")
    .style("fill", "black")
    .attr('x', (d,i) => i*55)
    .attr('y', d => 300 - d*20)
    .attr("height", d => d*20)
    .attr("width", 50);
As a review, we accomplished this via a sequence of four steps:

  • 1. Get an array of data to visualize (in this case, the variable num).
  • 2. Select the existing rectangles (initially an empty selection since we haven't created any rectangles yet).
  • 3. Perform the data join to connect the data array to the selection of rectangles.
  • 4. Add rectangles for the new items found in the data join.

This works well enough when we start with a blank SVG element, but we don't always and with a blank visualization. In particular, when data is dynamic we need a slightly more complex approach that can update an existing chart.

Scroll through the rest of this page to see data joins work when data is dynamic.

Dynamic Data

Generally speaking, we can think of any update of a visualization (either the initial drawing of the shapes, or updates of the shapes to reflect a change in data) using a very similar workflow as in the basic data join. In fact, the first three steps are exactly the same!

  • 1. Get an array of data to visualize (in this case, the variable num.
  • 2. Select the existing rectangles (initially an empty selection, since the visualization has no rectangles at first, since we haven't created them yet).
  • 3. Perform the data join to connect the data array to the selection of rectangles.

The fourth step, however, becomes more complicated. Why? Because "adding new bars" is only one of three possible changes to the visualization. We might also need to remove bars from the visualization, or update the size of existing bars.

To see why this is the case, let's complicate our original example slightly. Instead of a simple array of numbers as our data, suppose we have data for a poll of kindergarten students about their favorite colors. The data might look like this:

// The data array
let votes = [{color: "red", votes: 1},
    {color: "blue", votes: 2},
    {color: "green", votes: 3},
    {color: "purple", votes: 4},
    {color: "silver", votes: 5},
    {color: "pink", votes: 6}];

An Updated Visualization: Color-Coded Bars

Updating our code for creating the visualization slightly, we can now create a color-coded bar chart.
// This will create an empty selection since we have no rectangles yet!
let existing_rects = d3.select("svg").selectAll("rect");

// Perform the data join
let data_join_result = existing_rects.data(votes);

// Join the unmatched data items to new rectangles.
data_join_result.join("rect")
    .style("fill", d => d.color)
    .attr('x', (d,i) => i*55)
    .attr('y', d => 300 - d.votes*20)
    .attr("height", d => d.votes*20)
    .attr("width", 50);

A New Vote → New Data!

Now let's imagine that we ask a brand new class of students about their favorite colors and gather the following results.
// The data array
let votes = [{color: "red", votes: 4},
    {color: "blue", votes: 3},
    {color: "black", votes: 2},
    {color: "orange", votes: 4}];

Notice that our data array is now quite different from our visualization. The data array has 4 items instead of 6, some colors have gone away while others have appeared, and even colors that remain (e.g., red) have a different number of votes.

To update our visualization to reflect the new data from the new poll, we need to do more than just add rectangles to the visualization. We need to add some new bars (black and orange), remove some others (green, purple, silver, and pink), and update the size of others (red and blue).

Luckily, this is something the D3 join process will do for us.

The First Step: Data and D3 Selection

The data join process begins by identifying two data structures: (1) the data array that you wish to visualize, and (2) a selection of graphical objects that represent the corresponding data in the existing visualization.
// The data array
let votes = [{color: "red", votes: 4},
    {color: "blue", votes: 3},
    {color: "black", votes: 2},
    {color: "orange", votes: 4}];

// Select the existing bar chart rectangles. We have six existing rectangles!
let existing_rects = d3.select("svg").selectAll("rect");

In our code, this means we need (1) the votes data array with our new voting data, and (2) the value of the existing_rects variable which contains a D3 selection of all of the rectangles that currently exist in our existing bar chart prior to our update for the new data.

The Second Step: Data Join to Determine the Enter, Update, and Exit Selections

Next, we D3 to perform a data join using the .data(...) method. This connects the data array to the D3 selection of graphical objects.
// Perform the data join
let data_join_result = existing_rects.data(votes, d => d.colors);
Notice the extra parameter to the .data(...) method: d=>d.colors? It's a key function that tells D3 to use the color attribute to find corresponding data elements and graphic objects. It identifies the "unique ID" for each data element in our array. The data join process creates three distinct sub-selections as a result:

  • 1. The enter selection, containing new data items for which new graphical objects must be added to the visualization. These are data points that should be entering the visualization.
  • 2. The exit selection, containing old graphical objects for which data no longer exits in the data array. These are graphical objects that should be exiting the visualization.
  • 3. The update selection, containing existing graphical objects for which there is also a corresponding data object. These are graphical objects that should remain in the visualization, but be updated to reflect any changes in data.

In this example, the enter selection contains Orange and Black: new colors which need to be added to the visualization. The exit selection contains Green, Purple, Silver, and Pink: colors that are currently represented in the visualization but which no longer exist in the updated data array. Finally, the update selection includes Red and Blue: colors which existed in the first data array and continue to exist in the new data array (even if the vote counts have changed).

The Final Step: Updating the Visualization

As the final step in updating our visualization, we must specify what to do for the three enter, exit, and update selections. This is done using the same .join(...) method we used in the basic example, but with more specific instructions. for the .join(...) method in D3 is apply the specified code to the enter selection.
// Update the visualization.
data_join_result.join(
    enter => enter.append("rect")
        .style("fill", d => d.color)
        .attr('x', (d,i) => i*55)
        .attr('y', d => 300 - d.votes*20)
        .attr("height", d => d.votes*20)
        .attr("width", 50),
    update => update
        .style("fill", d => d.color)
        .attr('y', d => 300 - d.votes*20)
        .attr("height", d => d.votes*20),
    exit => exit
        .remove()
);

Putting it All Together

Here are all the steps put together into a single script.
// The data array
let votes = [{color: "red", votes: 4},
    {color: "blue", votes: 3},
    {color: "black", votes: 2},
    {color: "orange", votes: 4}];

// Select the existing bar chart rectangles. We have six existing rectangles!
let existing_rects = d3.select("svg").selectAll("rect");

// Perform the data join
let data_join_result = existing_rects.data(votes, d=>d.color);

// Update the visualization.
data_join_result.join(
    enter => enter.append("rect")
        .style("fill", d => d.color)
        .attr('x', (d,i) => i*55)
        .attr('y', d => 300 - d.votes*20)
        .attr("height", d => d.votes*20)
        .attr("width", 50),
    update => update
        .style("fill", d => d.color)
        .attr('y', d => 300 - d.votes*20)
        .attr("height", d => d.votes*20),
    exit => exit
        .remove()
);

Next Steps...

Want to experiment with an interactive version of this program? Click here!

This lesson introduces the concepts of the enter, update, and exit selections using the .join(...) method introduced in recent versions of D3 as a simpler alternative to the original D3 data join API. An interactive version of this same program but using the legacy enter/update/exit API from D3's earlier versions is also available. Current versions of D3 support both the new and old APIs, and you'll see examples with both if you search online for documentation or tutorials.
All content authored by David Gotz. Copyright © 2021-2022. All rights reserved.