Version 5 of D3 introduced a new function .join() which makes data binding much easier to understand. I wrote about it in D3.js has just got easier!.

We've gone from:

var myData = [ 10, 40, 30, 50, 20 ];

var u = d3.select('.container')
.selectAll('circle')
.data(myData);

u.enter()
.append('circle')
.merge(u)
.attr('r', function(d) { return d; });

u.exit()
.remove();

in version 4 to:

var myData = [ 10, 40, 30, 50, 20 ];

d3.select('.container')
.selectAll('circle')
.data(myData)
.join('circle')
.attr('r', function(d) { return d; });

in version 5.

How do we specify exactly how elements should enter and exit the page?

For example we might want elements to:

  • fade in when they enter the page
  • fly off to the right when they exit the page

The .enter() and .exit() functions allow you to do this but what happens if we're using version 5's .join() instead?

The answer is that .join() has three parameters, each of which is a function that handles entering, updating and exiting elements.

Let's look at entering elements. Suppose we want entering elements to fade in. We pass an optional function into .join():

var myData = [ 10, 40, 30, 50, 20 ];

d3.select('.container')
.selectAll('circle')
.data(myData)
.join(
function(enter) {
return enter
.append('circle')
.style('opacity', 0);
}
)
.attr('r', function(d) { return d; });

This function has a single parameter (by convention named enter) which is a selection of entering elements.

Setting the style or attributes of this selection will apply to elements immediately after they're created. Thus any initial style (such as zero opacity) can be set here. Note the selection must be returned by this function.

.join() accepts a second parameter which specifies what happens to elements already on the page (not including entering elements). I don't tend to use this one much so usually write:

d3.select('g.container')
.selectAll('circle')
.data(myData)
.join(
function(enter) {
...
},
function(update) {
return update;
}
)

The third parameter of .join() is a function that specifies what happens to exiting elements.

Suppose we'd like elements to shrink, fade and fly off the page before being removed we'd write:

d3.select('g.container')
.selectAll('circle')
.data(myData)
.join(
function(enter) {
...
},
function(update) {
...
},
function(exit) {
return exit
.transition()
.duration(transitionDuration)
.attr('r', 0)
.style('opacity', 0)
.attr('cx', 1000)
.on('end', function() {
d3.select(this).remove();
});
}
)

Finally we can update elements that have just entered or are already on the page by chaining .attr() and .style() calls after .join().

Let's:

  • set the x coordinate
  • animate the radius to its new value
  • animate the opacity to 1
d3.select('g.container')
.selectAll('circle')
.data(myData)
.join(
function(enter) {
...
},
function(update) {
...
},
function(exit) {
...
}
)
.attr('cx', function(d, i) {
return i * circleSpacing;
})
.transition()
.duration(transitionDuration)
.attr('r', function(d) {
return d;
})
.style('opacity', 1);

Now entering elements fade in, updating elements grow or shrink and exiting elements fade, shrink and fly off the page:

See the complete example on CodePen:

See the Pen D3 v5 enter/exit/update w/transitions by Peter Cook (@createwithdata) on CodePen.

Summary

One of the advantages of D3's approaches to data binding was that it provided .enter() and .exit() functions that allow fine grained control over entering and exiting elements.

With the arrival of version 5's .join() we've shown how entering and exiting elements can still be controlled by passing functions into .join().

To summarise:

  • the first parameter specifies what happens to elements straight after creation
  • the second parameter specifies what happens to elements already on the page
  • the third parameter specifies what happens to exiting elements

.attr() and .style() calls chained after .join() apply to both entering elements and elements already on the page.

I think this approach is a lot easier to understand and I hope it makes learning D3 a bit easier!