4 Update, Enter, and Exit

Read: IDVW2, Chapter 9, pp. 178-184; Chapter 12, pp. 231-249

4.2 Use exit selection to remove elements

a.k.a. more DOM elements than data values

We’ll start with six circles and remove some.

Open six_blue_circles.html in Chrome and open the JavaScript Console.

Let’s bind four data values to the six circles:

d3.select("svg")
  .selectAll("circle")
  .data([123, 52, 232, 90]);

Click the black triangle to view the _enter, _exit, and _groups fields.

We can store the selection in a variable:

const circ = d3.select("svg")
  .selectAll("circle")
  .data([123, 52, 232, 90]);

Let’s look at the exit selection:

circ.exit();

Try this:

circ.attr("fill", "red");

What happened and why?

Now try this:

circ.exit().attr("fill", "purple");

What happened and why?

What do you think this will do? Try it.

circ.exit().transition().duration(2000).remove();

Create a new variable circ2 and compare it to circ:

const circ2 = d3.selectAll("circle");

circ.data();

circ2.data();

circ.exit();

circ2.exit();

What’s going on?

4.3 Use enter selection to add elements

a.k.a. more data values than DOM elements

We’ll start with six_blue_circles.html in Chrome and add some circles.

First, let’s bind new data to the circles:

const circ = d3.select("svg")
  .selectAll("circle")
  .data([123, 52, 232, 90, 34, 12, 189, 110]);

And look at the enter selection:

circ.enter();

How many placeholders are in the enter selection?

Let’s add circles for each of these placeholders:

circ.enter()
    .append("circle")
      .attr("cx", "100")
      .attr("cy", (d, i) => i * 50 + 25)
      .attr("r", "20")
      .attr("fill", "blue");

Try this:

circ.transition()
  .duration(3000)
  .attr("cx", "400");

What do you need to do to act on all of the circles?

d3.select("svg")
  .selectAll("circle")
  .transition()
  .duration(2000)
  .attr("cy", (d, i) => (i * 50) + 25)
  .attr("cx", "200");

4.4 Data / enter / append sequence

We’ll start with nothing–not even an SVG–and add elements with the data / enter / append sequence.

Work in the Console on this page (help).

Open the JavaScript Console

The SVG will be added here:


d3.select("div#dea")
  .append("svg")
  .attr("width", "400")
  .attr("height", "250");

Create an array of values:

const specialdata = [75, 150, 200];

Add rectangles:

d3.select("svg")
  .selectAll("rect")
  .data(specialdata)
  .enter()
  .append("rect")
    .attr("x", d => d)
    .attr("y", d => d)
    .attr("width", "50")
    .attr("height", "30")
    .attr("fill", "pink");

4.4.1 Labels

Note that we can also label the rectangles with the data value:

  d3.select("svg")
    .selectAll("text")
    .data(specialdata)
    .enter()
    .append("text")
    .attr("x", d => d + 25)
    .attr("y", d => d + 25)
    .text(d => d)
    .attr("fill", "blue")
    .attr("text-anchor", "middle");

4.5 Exercise : horizontal bar chart

  1. Save a copy of this exercise and open it in your text editor.

Create a horizontal bar chart with bar widths equal to the data values stored in bardata. Do so by replacing all the “ADD’s” with constants or functions and then uncommenting those lines.

    const bardata = [300, 100, 150, 225, 75, 275];

It should look like this:

Solution

4.6 Merge selections

We now know how to work separately with the update, enter, and exit selections. Often in practice we wish to do the same thing or almost the same thing with the enter and update selections. That is where .merge() comes in. If we merge the update and enter selections we don’t have to repeat our code.

Open six_blue_circles.html in Chrome.

Run the following code in the Console:

const newdata = [123, 52, 232, 90, 34, 12, 189, 110];
const svg = d3.select("svg");
const circ = svg.selectAll("circle")
  .data(newdata);
  
circ.enter()  // 2 placeholders
  .append("circle")  // placeholders -> circles
     .attr("cx", "100")  // acts on enter selection only
     .attr("cy", (d, i) => (i - 5) * 50)
     .attr("r", "20")
     .attr("fill", "red")
  .merge(circ)
     .transition()
     .duration(2000)
     .attr("cx", "200");

Note the pattern:

  • Create a data array
  • Store the svg selection
  • Store the data bind in X
X.enter()
  .append(some shape)
  *add attributes*  // acts on enter selection only (no transitions!)
  .merge(X)
  *attributes, other stuff* // acts on enter and update selections

Do not include transitions in a stored selection!

4.7 Exercise : merge

Open this bar chart in Chrome and work in the Console. (You don’t have to download it.)

All of your solutions should begin with:

  • Creating a data array (ex. const dataset = [1, 2, 3];)
  • Selecting the svg and storing it (const svg = d3.select("svg");)
  • Binding the data and storing it (ex. const bars = svg.selectAll("rect").data(dataset));)
  1. Change the data to any six other values and update the lengths of the bars.

  2. Bind a new dataset, newbardata to the bars, update the bar lengths, and remove any extra bars.

    newbardata = [250, 125, 80, 100];

  3. Bind a new dataset, reallynewbardata, to the bars, then add additional bars so each data value has a bar. Make the outline (stroke) of the new bars a different color.

    reallynewbardata = [300, 100, 250, 50, 200, 150, 325, 275];

  4. Use .merge() to combine the update and enter selections into one selection and then transition the height of all of the bars to half their current height.

  5. Add text labels inside the bars at the right end with the length of the bar in pixels.

Solution

4.8 Groups

Open six_blue_circles.html in Chrome.

Run this code in the Console:

const specialdata = [100, 250, 300];

const bars = 
  d3.select("svg")
    .selectAll("rect")
    .data(specialdata)
    .enter()
    .append("rect")
      .attr("x", d => d)
      .attr("y", d => d)
      .attr("width", "50")
      .attr("height", "30")
      .attr("fill", "red");

What’s going on?

Refresh the page, and try the following instead:

const svg = d3.select("svg");

const specialdata = [100, 250, 300];

const bars = 
  d3.select("svg")
    .append("g")
      .attr("id", "rects")
    .selectAll("rect")
    .data(specialdata)
    .enter()
    .append("rect")
      .attr("x", d => d)
      .attr("y", d => d)
      .attr("width", "50")
      .attr("height", "30")
      .attr("fill", "red");

Compare:

d3.select("svg")
  .select("g#rects")
  .selectAll("rect")
  .attr("fill", "purple");

and

d3.select("svg")
  .selectAll("rect")
  .attr("fill", "purple");

4.9 General Update Pattern (with merge)

Open Developer Tools on this page.

Create a function in the Console:

function changedata(data) {
  d3.select("svg#gup")
    .selectAll("rect")
    .data(data)
    .attr("width", d => d);
    }

Test it out:

changedata([258, 373, 278, 9, 72, 96]);

What happens if there are too many data values?

changedata([196, 360, 283, 390, 46, 56, 152]);

Let’s use the enter selection to add new bars in this case:

function changedata(data) {
  const bars = d3.select("svg#gup") 
    .selectAll("rect")
    .data(data);    // bars is the update selection
    
  bars.enter()
    .append("rect")
      .attr("x", "30")  // until merge, acts on
      .attr("y", (d, i) => i * 50) // enter selection only
      .attr("height", "35")  
      .attr("fill", "lightgreen")
    .merge(bars) // merge in the update selection
      .attr("width", d => d); // acts on all bars
  }

What happens if we have more bars than data values?

changedata([325, 116, 25]);

Let’s add to the function to remove the extra bars in this case:

function changedata(data) {
  const bars = d3.select("svg#gup") 
    .selectAll("rect")
    .data(data);    // bars is the update selection
    
  bars.enter()
    .append("rect")
      .attr("x", "30")  // until merge, acts on
      .attr("y", (d, i) => i * 50) // enter selection only
      .attr("height", "35")  
      .attr("fill", "lightgreen")
    .merge(bars) // merge in the update selection
      .attr("width", d => d); // acts on all bars
      
  bars.exit()
    .remove();
  }

Try:

changedata([271, 49, 389]);

VOILA! We have created the D3 General Update Pattern!

It is covered in IDVW in the “Other Kinds of Data Updates” section on pp. 178-186 in Chapter 9. (The earlier part of Chapter 9 deals with data updates in which the number of DOM elements remains the same.)

Note that the General Update Pattern changed with D3 Version 4 so avoid examples from Version 3.

Also available here: general_update_pattern.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v7.min.js"></script>
  </head>

  <body>

<script id="s1">

// Create svg and initial bars

const svg = d3.select("body")
  .append("svg")
    .attr("width", "500")
    .attr("height", "400");

const bardata = [300, 100, 150, 225, 75, 275];

const bars = svg.selectAll("rect")
  .data(bardata);

bars.enter().append("rect")
  .attr("x", "30")
  .attr("y", (d, i) => i*50)
  .attr("width", d => d)
  .attr("height", "35")
  .attr("fill", "lightgreen");

// General Update Pattern

function update(data) {

  const bars = svg.selectAll("rect")    // data join
    .data(data);

    bars.enter()
      .append("rect")    // add new elements
        .attr("x", "30")
        .attr("y", (d, i) => i*50)
        .attr("width", d => d)
        .attr("height", "35")
        .attr("fill", "yellow")
      .merge(bars)    // merge
        .transition()
        .duration(2000)
        .attr("width", d => d)
        .attr("fill", "orange");

    bars.exit().remove();    // remove extra elements
    }

    </script>

  </body>

</html>

4.10 General Update Pattern (with join)

Optional

This is a newer method–introduced in v5–which simplifies the general update pattern by automatically adding elements for the enter selection, removing elements for the exit selection, merging the enter and update selections and then acting on them all with .join().

The update pattern above could be replaced with the following:

function changedata2(data) {
  const svg = d3.select("svg#gup") 
  svg.selectAll("rect")
    .data(data)
    .join("rect") // need "rect" for appending elements
      .attr("x", "30")  
      .attr("y", (d, i) => i * 50)
      .attr("height", "35")  
      .attr("fill", "lightgreen")
      .attr("width", d => d);
  }

Try:

changedata2([271, 49, 389]);

With .join() you can get more granular with enter, update, and exit selections as we did previously. changedata3() is equivalent to changedata2():

function changedata3(data) {
  const svg = d3.select("svg#gup") 
  svg.selectAll("rect")
    .data(data)
    .join(
      enter => enter.append("rect")
        .attr("x", "30")  
        .attr("y", (d, i) => i * 50)
        .attr("height", "35")
        .attr("width", d => d)
        .attr("fill", "lightgreen"),
      update => update
        .attr("width", d => d),
      exit => exit.remove()
      );
  }

Generally, however, unless you have transitions, you will not need to control the enter, exit and update selections separately.

4.11 Exercise: : functions

Open general_update_pattern.html and practice running the update() function with different datasets in the Console.

For example:

update([100, 200, 300]);

Solution

4.12 Exercise : vertical bar chart

  1. Save a copy of this exercise and open it in your text editor.

Create a vertical bar chart with bar heights equal to the data values stored in bardata. Do so by replacing all the “ADD’s” with constants or functions and then uncommenting those lines.

Solution