d3.js update pattern Updating the data: a basic example of enter, update and exit selections


Example

Creating a chart that displays a static dataset is relatively simple. For example, if we have this array of objects as the data:

var data = [
    {title: "A", value: 53},
    {title: "B", value: 12},
    {title: "C", value: 91},
    {title: "D", value: 24},
    {title: "E", value: 59}
];

We can create a bar chart where each bar represents a measure, named "title", and its width represents the value of that measure. As this dataset doesn't change, our bar chart has only an "enter" selection:

var bars = svg.selectAll(".bars")
    .data(data);

bars.enter()
    .append("rect")
    .attr("class", "bars")
    .attr("x", xScale(0))
    .attr("y", function(d){ return yScale(d.title)})
    .attr("width", 0)
    .attr("height", yScale.bandwidth())
    .transition()
    .duration(1000)
    .delay(function(d,i){ return i*200})
    .attr("width", function(d){ return xScale(d.value) - margin.left});

Here, we are setting the width of each bar to 0 and, after the transition, to its final value.

This enter selection, alone, is enough to create our chart, that you can see in this fiddle.

But what if my data changes?

In this case, we have to dynamically change our chart. The best way to do it is creating an "enter", an "update" and an "exit" selections. But, before that, we have to do some changes in the code.

First, we'll move the changing parts inside a function named draw():

function draw(){
    //changing parts
};

These "changing parts" include:

  1. The enter, update and exit selections;
  2. The domain of each scale;
  3. The transition of the axis;

Inside that draw() function, we call another function, that creates our data. Here, it's just a function that returns an array of 5 objects, choosing randomly 5 letters out of 10 (sorting alphabetically) and, for each one, a value between 0 and 99:

function getData(){
    var title = "ABCDEFGHIJ".split("");
    var data = [];
    for(var i = 0; i < 5; i++){
        var index = Math.floor(Math.random()*title.length);
        data.push({title: title[index],
            value: Math.floor(Math.random()*100)});
        title.splice(index,1);
    }
    data = data.sort(function(a,b){ return d3.ascending(a.title,b.title)});
    return data;
};

And now, let's move to our selections. But before that, a word of caution: to maintain what we call object constancy, we have to specify a key function as the second argument to selection.data:

var bars = svg.selectAll(".bars")
    .data(data, function(d){ return d.title});

Without that, our bars won't transition smoothly, and it'd be difficult to follow the changes in the axis (you can see that removing the second argument in the fiddle below).

So, after correctly setting our var bars, we can deal with our selections. This is the exit selection:

bars.exit()
    .transition()
    .duration(1000)
    .attr("width", 0)
    .remove();

And these are the enter and the update selections (in D3 v4.x, the update selection is merged with the enter selection using merge):

bars.enter()//this is the enter selection
    .append("rect")
    .attr("class", "bars")
    .attr("x", xScale(0) + 1)
    .attr("y", function(d){ return yScale(d.title)})
    .attr("width", 0)
    .attr("height", yScale.bandwidth())
    .attr("fill", function(d){ return color(letters.indexOf(d.title)+1)})
    .merge(bars)//and from now on, both the enter and the update selections
    .transition()
    .duration(1000)
    .delay(1000)
    .attr("y", function(d){ return yScale(d.title)})
    .attr("width", function(d){ return xScale(d.value) - margin.left});

Finally, we call the draw() function every time the button is clicked:

d3.select("#myButton").on("click", draw);

And this is the fiddle showing all these 3 selections in action.