4 Update, Enter, and Exit

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

4.1 Lecture slides

D3 Data Bind

4.2 Remove some elements

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

We’ll start with six circles and remove some.

Download and open a fresh copy of six_blue_circles.html in Chrome.

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

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

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:

var circ = 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:

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

circ.data();

circ2.data();

circ.exit();

circ2.exit();

What’s going on?

4.3 Add some elements

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

We’ll start with six circles and add some.

Let’s bind new data to the circles:

var circ = 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?

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

4.4 Data / enter / append

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:


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

Create an array of values:

var specialdata = [75, 150, 200];

Add rectangles:

  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:

  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. Create a new html file (try to recreate the template without looking… or save a copy of this one) and open it in your text editor.

If you create a new file in RStudio, choose “Text File” and use the .html file extension when you save it. Do not choose “R HTML”.

Add a script that adds an svg element and horizontal bars of the lengths (in pixels) specified in bardata. Create the bars with the data / enter / append sequence.

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

Solution

4.6 Merge selections

a.k.a. combining update and enter selections with .merge()

Open six_blue_circles.html in Chrome. (You do not need to download it first.)

Run the following code in the Console:

var svg = d3.select("svg");
var circ = svg.selectAll("circle")
  .data([123, 52, 232, 90, 34, 12, 189, 110]);
  
var allcirc = 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");

Now try to predict what the following code will do. Were you right?

allcirc.transition() 
        .duration(3000)
        .attr("cx", "400")
        .attr("fill", "purple");

Refresh the page and then copy and paste the following into the Console and run.

var svg = d3.select("svg");
var circ = svg.selectAll("circle")
  .data([123, 52, 232, 90, 34, 12, 189, 110]); // update selection
  
var allcirc = 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);  // combines enter and update selections

And now, the following code (same as before). What changed? Why?

allcirc.transition() 
        .duration(3000)
        .attr("cx", "400")
        .attr("fill", "purple");

Note the pattern:

Store the data bind in X.

Y = X.enter().append() attributes .merge(X)

Do more stuff with Y.

Do not include transitions in a stored selection!

4.7 Exercise : merge

Open the bar chart you created in the previous exercise in Chrome, or this one and work in the Console. (You don’t have to download it.)

  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.

4.8 Groups

Open six_blue_circles.html in Chrome. (You do not need to download it first.)

Run this code in the Console:

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

var specialdata = [100, 250, 300];

var bars = 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:

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

var specialdata = [100, 250, 300];

var bars = 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

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) {
  var 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) {
  var 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]);

A fancy exit:

function changedata(data) {
  var 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()
    .attr("fill", "red")
    .transition()
    .duration(2000)
    .attr("width", "0")
    .remove();
  }
changedata([234, 129, 432, 286, 49, 372]);

changedata([401, 23, 173]);

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">
    <title>EDAV5_1</title>
    <script src="https://d3js.org/d3.v6.min.js"></script>
  </head>

  <body>

    <script id="s1">

// Create svg and initial bars

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

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

var 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) {

  var 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 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]);

4.11 Exercise : vertical bar chart

Change the bar chart in general_update_pattern.html to a vertical bar chart.