Part Five: Enter, exit and the Update Pattern

Please note this article is based on version 3 of D3.

In my last article I showed how D3 can join arrays of data to HTML and SVG elements. As a brief recap, if you have some SVG elements:

<svg width="800" height="140" id="ex4">
  <circle cx="100" cy="70"></circle>
  <circle cx="250" cy="70"></circle>
  <circle cx="400" cy="70"></circle>
  <circle cx="550" cy="70"></circle>
  <circle cx="700" cy="70"></circle>
</svg>

and an array:

var cities = [
  {name: 'London', population: 8416500, continent: 'Europe'},
  {name: 'New York City', population: 8419000, continent: 'North America'},
  {name: 'Paris', population: 2241000, continent: 'Europe'},
  {name: 'Shanghai', population: 24150000, continent: 'Asia'},
  {name: 'Tokyo', population: 13297000, continent: 'Asia'},
];

you can join the array to the circle elements using:

var mySelection = d3.selectAll('circle').data(cities);

This allows you to manipulate the circles according to the data:

mySelection
  .attr('r', function(d) {
    return radiusScale(d.population);
  })
  .style('fill', function(d) {
    return colour[d.continent];
  });

Enter, exit and the update selection

In the above example we have 5 circles and 5 cities so there’s a perfect match between SVG elements and array values.

However, what if the array grows or shrinks?

Fortunately D3 defines a couple of functions .enter() and .exit() which tell us about HTML/SVG elements that must be created or removed to keep the array and HTML/SVG elements in sync.

For example, if we add a city to our cities array:

cities.push({name: 'Madrid', population: 3165000, continent: 'Europe'});

and perform the data join:

mySelection = d3.selectAll('circle').data(cities);

we can add the missing circle(s) using:

mySelection.enter().append('circle');

If instead of adding cities we removed some cities:

cities.pop(); // cities.length is now 4

we’d remove the surplus circle(s) using:

mySelection.exit().remove();

.enter() and .append() go hand in hand, as do .exit() and .remove().

General update pattern

There’s quite a lot to take in and remember when dealing with D3 updates so I like to use a commonly used pattern that encapsulates enter, exit and updates in a single function. It’s known as the general update pattern and is recommended by D3’s creator Mike Bostock.

It means I can do things like:

var cities = [
  {name: 'London', population: 8416500, continent: 'Europe'},
  {name: 'New York City', population: 8419000, continent: 'North America'},
  {name: 'Paris', population: 2241000, continent: 'Europe'}
];
update(cities); // 3 circles are added to the page

cities.push( {name: 'Shanghai', population: 24150000, continent: 'Asia'} );
update(cities); // a new circle is created

cities.pop();
update(cities); // a circle is removed

Each time update() is called, it syncs the HTML/SVG elements with my data.

The update function consists of 4 stages:

  • perform the data join
  • use .enter().append() to create elements if there is a shortfall
  • use .exit().remove() to remove elements if there is a surplus
  • update style, attributes etc. of existing and new elements

and might look something like:

function update(cities) {

  // Perform the data join
  var selection = d3.select('#my-circles')
    .selectAll('circle')
    .data(cities);

  // Remove surplus elements
  selection.exit()
    .remove();

  // Add new elements
  selection.enter()
    .append('circle');

  // Update existing AND new elements
  selection
    .attr('r', function(d) {
      return radiusScale(d.population);
    })
    .style('fill', function(d) {
      return colour[d.continent];
    });

}

This is generally the same pattern no matter what data visualisation I’m creating.

Note that version 4 of D3 uses a slightly different approach for existing and new elements (using the new .merge() function):

// Remove surplus elements
selection.exit()
  .remove();

// Add new elements and update existing and new elements
selection.enter()
  .append('circle')
  .merge(selection)
  .attr('r', function(d) {
    return radiusScale(d.population);
  })
  .style('fill', function(d) {
    return colour[d.continent];
  });

Summary

By using the general update pattern we encapsulate all of the complexity of adding, removing and updating elements in a single function. Not only is this good practice, but I think it helps to understand how .enter(), .exit() and updates relate to each other.

I recommend using this pattern whenever you can, especially if the visualisation is dynamic (for example, it’s interactive, or the data can update).

Stay in touch

I’ll be publishing more D3 must knows over the coming months and covering subjects such as scale functions, data joins, enter/exit and lots more. If you’d like to be the first to read the next articles then please add your name to my mailing list and you’ll receive an update when they’re published.