Using .enter and .append

Why do we need .enter?

When we first started learning about SVGs, we thought we knew everything!

We’d make a chart using rect elements, designing each and every data point manually.

Open example in new window

<svg width="420" height="80">
  <rect x="0" y="0" height="15" width="300"></rect>
  <rect x="0" y="20" height="15" width="50"></rect>
  <rect x="0" y="40" height="15" width="150"></rect>
  <rect x="0" y="60" height="15" width="355"></rect>
</svg>

Open example in new window

We could have rested there, but we were bigger! We were badder! We were programmers!

Instead of going into each and every rect to manually set the width, we decided to use d3. We’d bind our data, and then use that data to set the width.

Let’s use d3 to automatically set the rect width using data.

Open example in new window

<svg width="420" height="80">
  <rect x="0" y="0" height="15"></rect>
  <rect x="0" y="20" height="15"></rect>
  <rect x="0" y="40" height="15"></rect>
  <rect x="0" y="60" height="15"></rect>
</svg>
<script>
  var datapoints = [ 300, 50, 150, 355 ];
  d3.selectAll("rect")
    .data(datapoints)
    .attr('width', function(d) {
      return d;
    })
</script>

Open example in new window

That way we could change the data from being [ 300, 50, 150, 355 ] to being [ 20, 80, 1, 9 ] to being [ 1, 0, 0, 400 ] and the chart would just update automatically!

So cool! So fun!

…..until we get more data!

The Problem

If you have four rect elements but five data points, what happens?

Even after you bind, you still only have four rectangles.

Open example in new window

<svg width="420" height="100">
  <rect x="0" y="0" height="15"></rect>
  <rect x="0" y="20" height="15"></rect>
  <rect x="0" y="40" height="15"></rect>
  <rect x="0" y="60" height="15"></rect>
</svg>
<script>
  var datapoints = [ 10, 20, 30, 40, 150 ];
  d3.selectAll("rect")
    .data(datapoints)
    .attr('width', function(d) {
      return d;
    })
</script>

Open example in new window

So the four data points that have corresponding rects work out, but the fifth data point without a rect just plain doesn’t! Now we have two options

  1. Suck it up and add another rect
  2. Sob quietly and figure out another method using d3

Method 1 sounds okay until you realize what happens if you need more data points. What if you have eight, or thirty, or two hundred?

Are you going to add each one of those <rect> elements in there? You probably don’t want to do that manually.

For Method 2 I usually skip the crying part and just go straight to d3. Let’s do that.

Enter .enter and .append

After you bring data, .enter() is a way of talking about all of the data points that don’t have an element yet.

<svg width="420" height="100">
  <rect></rect>
  <rect></rect>
  <rect></rect>
</svg>
var rectangles = d3.selectAll('rect').data([2, 4, 8, 16, 32]);

We have 3 elements, but only 5 data points.

data point rect
2 Yes
4 Yes
8 Yes
16 No!
32 No!

As a result, we’re left with two data points not represented. If we try to change attributes with rectangles, it will only affect the first three!

In order to access the two other elements, we user .enter().

// This is the three data points that have elements
var rectangles = d3.selectAll('rect').data(datapoints);

// And this is the two data points without elements
var new_rects = rectangles.enter();

Even though we can access them now, there are still only three rect elements on the page. With five data points, we need two more rect elements.

In order to add the elements, you use .enter()’s best friend: .append. .append will… append a new element for each of your data points.

In this case, we need to use new_rects.enter().append('rect'). .enter() accesses the lonely data points, and then append will add a rectangle for each of them.

var rectangles = d3.selectAll('rect').data(datapoints);
var new_rects = rectangles.enter();
new_rects.append("rect");

new_rects loops through those two lonely data points, and adds a rect for each of them.

Now you have five rectangles!

BUT.

Your first three rectangles are in var rectangles, and your second (new) three rectangles are in var new_rects. They need heights and widths and all of that, but because they’re in different variables, you’re going to have to set them both separately.

You’ll need to give rectangles a width, and new_rects a width. Separately.

You’ll need to give rectangles a fill color, and new_rects a fill color. Separately.

This is stupid! Can’t we just get them all on the same team?

The answer is yes,

Here’s the secret: delete all of the rects inside of the SVG. Now all of the rects are new.

If they’re all new, that means they’re all represented by .enter(). new_rects will have five, and rectangles will have zero!

Usually it looks like this:

Open example in new window

<svg width="420" height="100" class="chart">
</svg>
<script>
var datapoints = [10, 70, 30, 40, 90];
var svg = d3.select(".chart"); // Grab the svg container
var new_rects = svg.selectAll('rect') // Get all the rectangles in the svg
                  .data(datapoints) // Bind the 5 data points
                  .enter() // Grab the 5 'new' data points
                  .append('rect') // Add a rectangles for each 'new' data point
                  .attr('x', 0) // Begin setting attributes
                  .attr('y', function(d, i) {
                    // i is an index, 0, 1, 2, 3
                    return i * 20;  // this spaces them out evenly
                  })
                  .attr('height', 10)
                  .attr('width', function(d) {
                    return d * 2; // data point * 2 pixels wide
                  });
</script>

Open example in new window

The other big thing you need to make a note of that the attr function isn’t just function(d) { } any more, but function(d, i) { }. i is an index, and it keeps track of how many elements you’ve gone through.

You can can use this with a little multiplication to space things out. Let’s look at what happens each time we go through attr:

time through data point d index i y
First 10 0 0 * 20 = 0
Second 70 1 1 * 20 = 20
Third 30 2 2 * 20 = 40
Fourth 40 3 4 * 20 = 60
Fifth 90 4 4 * 20 = 80

Each time we go through to a new data point, i gets one bigger. We multiply that out and voila! We have the y offset that we need.

Review

You know what’s the best part of this? The fact that now that we have enter and append we can add a million things to our chart. With five data points, we can automatically add not only five rectangles, but five of anything else! Like, oh, I don’t know, annotations?

For example!!!

Open example in new window

<svg height="200" width="500"></svg>

<script>
var datapoints = [
  {'name': 'New York', 'population': 19	},
  {'name': 'Texas', 'population': 26 },
  {'name': 'California', 'population': 38 },
  {'name': 'Florida', 'population': 20 },
  {'name': 'Illinois', 'population': 12	}
];

var svg = d3.select('svg');
var rectangles = svg.selectAll('rect')
                    .data(datapoints)
                    .enter()
                    .append('rect')
                    .attr('x', 75)
                    .attr('y', function(d, i) { return i * 30; })
                    .attr('height', 20)
                    .attr('width', function(d) { return d['population'] * 3 ; });

var annotations = svg.selectAll('text')
                    .data(datapoints)
                    .enter()
                    .append('text')
                    .attr('x', 65)
                    .attr('y', function(d, i) { return i * 30 + 15; })
                    .text(function(d) { return d['name']; })
                    .attr('font-size', 12)
                    .attr('text-anchor', 'end');

</script>

Open example in new window

Want to hear when I release new things?
My infrequent and sporadic newsletter can help with that.