6

I am populating a div with child divs containing the values from an array. On first pass, the array looks like this:

arr_subpop_unique = ["CPL", "NAP", "NPL", "SAP", "SPL", "TPL", "UMW", "WMT", "XER"]

My selection enter/update/exit looks like this:

var sizemapHeader = d3.select("#d3-sizemap-hr").selectAll("div")
        .data(arr_subpop_unique)

    sizemapHeader.enter().append("div")
        .attr("class", "sizemap-hr-title ellipsis scroll_on_hover")
        .html(function(d,i){ return d; });

    sizemapHeader.exit().remove();

This works fine, giving me a div for each element containing a string.

When I run the function again, my data array updates to this:

    arr_subpop_unique = ["MAN_MADE", "NATURAL"]

The update returns two divs, however, they contain "CPL" and "NAP" (index values 0 and 1). Can someone explain why this would not replace the first two indicies with "MAN_MADE" and "NATURAL"?

Thanks for the help...trying to get the hang of enter/update/exit data joins in D3!

thefreeline
  • 621
  • 1
  • 12
  • 26

3 Answers3

6

It's worth noting that the chaining is a little bit asymmetrical because enter() has the side-effect of merging into the update selection. This means, if you add features on to the update selection after the append(), they will also be added on the enter nodes. On the other hand, the update selection is not merged into the enter selection. So any features chained after enter() will only be on the enter selection.

In the case of the OP question, there are two update nodes and zero enter nodes. So the HTML is added on the non-existent enter nodes and the HTML of the two update nodes is not updated.

So, yes, this works...

var sizemapHeader = d3.select("#d3-sizemap-hr").selectAll("div")
            .data(arr_subpop_unique)

//ENTER
sizemapHeader
.enter()
    .append("div")
        .attr("class", "sizemap-hr-title ellipsis scroll_on_hover")
//UPDATE
sizemapHeader
    .html(function (d, i) { return d; })

//EXIT
sizemapHeader
.exit()
    .remove();

But this doesn't...

var sizemapHeader = d3.select("#d3-sizemap-hr").selectAll("div")
            .data(arr_subpop_unique)

//ENTER
//sizemapHeader
    .enter()
    .append("div")
        .attr("class", "sizemap-hr-title ellipsis scroll_on_hover")

//UPDATE
sizemapHeader
        .html(function (d, i) { return d; })

//EXIT
sizemapHeader
.exit()
    .remove();

And this breaks in the initial update (when the update selection is empty)...

var sizemapHeader = d3.select("#d3-sizemap-hr").selectAll("div")
            .data(arr_subpop_unique)

//UPDATE
sizemapHeader
    .html(function (d, i) { return d; })

//ENTER
sizemapHeader
.enter()
    .append("div")
        .attr("class", "sizemap-hr-title ellipsis scroll_on_hover")

//EXIT
sizemapHeader
.exit()
    .remove();

When you trace your code, you will see that the value of sizemapHeader changes after .enter() is called on it.

The reason is buried in the wiki...

The enter selection merges into the update selection when you append or insert. Rather than applying the same operators to the enter and update selections separately, you can now apply them only once to the update selection after entering the nodes. If you find yourself removing an entire selection's elements only to reinsert most of them, do this instead. For example:

var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
Cool Blue
  • 6,438
  • 6
  • 29
  • 68
  • Nice explanation, I've always found this to be rather intuitive, but with your explanation, and the excerpt from the wiki, I can't really think why I ever just intuitively thought this. Anyway, it seems to be a bit of a sticking point with relatively new `d3` users, so it's nice to see another explanation, especially with the examples. – Ben Lyall Mar 31 '15 at 13:31
  • Thanks for calling out this section on the wiki. For some reason this was not very intuitive to me (I'm still rather new to this), but this really helps clarify things. It seems that I have been operating much to the way the wiki describes, "removing an entire selection's elements only to reinsert most of them". Understanding this will really help me take the next step forward. Much appreciated! – thefreeline Mar 31 '15 at 14:42
4

Just providing a different take on the answer @Plato provided...

In certain situations you may want the changed data to be recognised as "new", ie. be part of the enter selection, rather than just part of the update. In your example, it is considered part of the update selection because your data is being bound to DOM elements based on index (this is the default). You can change this behaviour by passing a second argument to the data call.

See https://github.com/mbostock/d3/wiki/Selections#data for more information. It's referred to as the key argument.

Here is an example:

var sizemapHeader = d3.select("#d3-sizemap-hr").selectAll("div")
    .data(arr_subpop_unique, function(d, i) { return d; });

sizemapHeader.enter().append("div")
    .attr("class", "sizemap-hr-title ellipsis scroll_on_hover")
    .html(function(d,i){ return d; });

sizemapHeader.exit().remove();

The second argument to the data function call will now ensure that the data is bound to the DOM elements by value, rather than by their index in the array.

The next time you call your function with new data, any value that is not currently bound to a DOM element, will be considered new and part of the enter selection.

Both cases are useful, depending on what you're trying to achieve.

Ben Lyall
  • 1,976
  • 1
  • 12
  • 14
  • 2
    Nice... The mere existence of a node is not enough to qualify it as an update node: it has to have the correct data bound. This forces the new data into the enter selection where it receives the html. If any of the incoming data matches the existing nodes, no problem: the original html is persevered. No wasteful rewrites to the DOM elements. This has got me me thinking that a key should always be used. If there is some existing divs with text, then you could even tweak it to `return d || this.innerHTML;` – Cool Blue Mar 31 '15 at 13:00
  • I've generally found that when I'm updating data, say by adding new data entries to the array or similar, I've always wanted to use a `key` function, otherwise you end up with data jumping around the place and transitions don't seem to be smooth, especially if you happen to also be changing the number of elements in the data array. That said, there are plenty of examples where it is not necessary to use the `key` function, so it's nice to have the option there. – Ben Lyall Mar 31 '15 at 13:33
  • Yes, Mike Bostock talks about [object constancy](http://bost.ocks.org/mike/constancy/) although I think it's a kind of strange example, I get what he means. I'm strugling with the weard behaviour of these objects but, if I use the key function as you suggest, all of my concerns are moot; everything just behaves rationally. So, that's why I like it. – Cool Blue Mar 31 '15 at 13:57
  • This has really been an enlightening conversation for me. I appreciate the quality feedback and the different approaches for how to handle data binding. I'm sure I'll be able to find good use for the key function (like the OP). – thefreeline Mar 31 '15 at 14:56
3

take a look at "General update pattern"
i think you aren't triggering d3's update selection

var sizemapHeader =
d3.select("#d3-sizemap-hr").selectAll("div")
  .data(arr_subpop_unique)

sizemapHeader
.enter()
  .append("div")
    .attr("class", "sizemap-hr-title ellipsis scroll_on_hover")

sizemapHeader
    .html(function(d,i){ return d; });

sizemapHeader
.exit()
  .remove();
Plato
  • 10,812
  • 2
  • 41
  • 61