If you’d like a Bostock example, have at it.

How does it work?

Let’s say I’m allowing the user to filter my datapoints - for example, maybe they only want to see circles for cities in Asia. While I could just hide the dots that represent those cities, we can also use the d3 general update pattern to add and remove elements.

First we’ll add just like usual: selecting by class, binding our data, and setting up the positions and colors of all of our circles.

Here’s some code, I’ll explain it below.

If you’ve got this down, you might want to read the sections at the VERY bottom to improve your visualization a bit.

The normal adding part. We’re just drawing circles for each city.

svg.selectAll(".city-circles")
  .data(datapoints)
  .enter().append("circle")
  .attr("class", "city-circle")
  .attr("r", 5)
  .attr("fill", function(d) {
    return colorScale(d.Continent);
  })
  .attr("cx", function(d) {
    return xPositionScale(d.GDP_per_capita);
  })
  .attr("cy", function(d) {
    return yPositionScale(d.life_expectancy);
  })

The actual updating part. I’m pretending we’re clicking a button to only show the cities in Asia.

$("#asia-button").on('click', function(d) {
  // get a selection of other data points
  var asiaDatapoints = datapoints.filter( function(d) {
    return d.Continent == "Asia"
  })

  // STEP ONE: Grab the circles, rebind the data
  // note 1: we're saving this as a variable
  // note 2: NO enter/append here!
  var circles = svg.selectAll(".city-circle").data(asiaDatapoints);

  // STEP TWO: Remove all of the cities not in Asia
  circles.exit().remove()

  // STEP THREE: Add in any new circles
  // (like, for example, maybe we had selected Africa before and
  // now we're selecting Asia, d3 needs to add in Beijing or whatnot)
  // and also STEP FOUR: Merge the new circles with any existing circles
  // and then reset the x/y position and any styles
  circles.enter().append("circle")
    .merge(circles)
    .attr("class", "city-circle")
    .transition()
    .attr("r", 5)
    .attr("fill", function(d) {
      return colorScale(d.Continent);
    })
    .attr("cx", function(d) {
      return xPositionScale(d.GDP_per_capita);
    })
    .attr("cy", function(d) {
      return yPositionScale(d.life_expectancy);
    })
})

Step Zero: Filter your data

Get the datapoints you’d like to graph.

var asiaDatapoints = datapoints.filter( function(d) {
  return d.Continent == "Asia"
})

You can also do this with nested data - you’ll probably do something like var selected = nested.filter( function(d) { d.key == "New York" }).

Step One: Grab the circles, rebind the data

var circles = svg.selectAll(".city-circle").data(asiaDatapoints);

We’re selecting all of the circles we drew earlier and attaching new data to it.

Step Two: Remove all of the cities not in Asia

circles.exit().remove()

YOu know how we always do enter().append("circle")? That technically means “all of the datapoints without a circle already on the page, go draw yourself a circle.”

When we do exit().remove() it’s the opposite - “all circles who no longer have data please remove yourself from the page.”

So in this case, all circles that don’t have data any more (any dot not in asia)

Step Three + Four: Rebind

First, we need to add a new circle for every new entering circle (see the section right above)…

circles.enter().append("circle")

Then, we say “okay, all you new circles? Let’s combine you with the existing ones in circle”. It’s like saying circle + circle.enter(), “existing circles + new circles”.

  .merge(circles)

Then we go through all of those new circles and make sure they have the right class, cx, cy, fill, and everything else.

  .attr("class", "city-circle")
  .transition()
  .attr("r", 5)
  .attr("fill", function(d) {
    return colorScale(d.Continent);
  })
  .attr("cx", function(d) {
    return xPositionScale(d.GDP_per_capita);
  })
  .attr("cy", function(d) {
    return yPositionScale(d.life_expectancy);
  })

SECRET TIP 1: Where do baby circles come from?

Your circles are probably all zooming into position from the top right, because that’s good ol’ 0,0. If you don’t like that, you can always set a default position for the entering ones.

In this example we’re starting them off at the middle of the page. You just need to make sure you do this before the merge line so it only affects the new ones.

If you wanted them to come from the bottom left, that’s a cy of height.

  circles.enter().append("circle")
    .attr("cx", width / 2) // set their cx to the middle
    .attr("cy", height / 2) // set their cy to the middle
    .merge(circles)
    .attr("class", "city-circle")
    .transition()
    .attr("r", 5)
    .attr("fill", function(d) {
      return colorScale(d.Continent);
    })
    .attr("cx", function(d) {
      return xPositionScale(d.GDP_per_capita);
    })
    .attr("cy", function(d) {
      return yPositionScale(d.life_expectancy);
    })

SECRET TIP 2: Keying your data

When you bind your data, you can give your circle a name, a way to identify itself - that way when you rebind data it can always remember which dot is Shanghai and which one is Beijing, instead of randomly picking a new Beijing dot every time.

Just do this whenever you bind - every time, the first bind and any rebindings.

svg.selectAll(".city-circles")
  .data(datapoints, function(d) {
    // Return any unique column
    // I'm using d.city_name, the city's name
    return d.city_name;
  })