Search code examples
javascriptd3.jsobservablehq

d3.join() enter called instead of update?


I'm trying to make D3 update an SVG element once a second by passing a fresh Date object into the data() function, followed by join() to update the page. I'm actually doing this in an Observable notebook using Promises.tick() but I think the code below (full source) is a rough approximation of the relevant bits. My end goal is to make a clock but the code here is a minimal demo of the issue I have.

The problem with this code is that the update function in join() is never called -- every time around the loop the enter function is called instead, which results in a fresh text being appended each time. Since I have one datum and one element I would expect join's update to be called, not enter. I am aware that the key function on data() (example) is significant in determining what has changed but my understanding is that the default key is the array index, which should always be 0 here. At any rate, specifying a different key function (ranging from the identity function to returning a constant) hasn't helped.

while (true) {
  var data = [new Date()];

  svg
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .selectAll("text")
    .data(data)
    .join(
      enter => enter.append("text").text(d => "enter: " + d),
      update => update.text(d => "update: " + d)
    );

  await new Promise(resolve => setTimeout(resolve, 1000));
}

Solution

  • Look at this:

    svg
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      //etc...
    

    You are appending a new <g> element every time you run the loop, and all the selected elements (texts, or whatever) after that will reference that newly appended group, that is, the selectAll selects all texts inside that newly appended group. Obviously, there is none. That's why you always have only an enter selection, but no update selection.

    Have a look at this demo, reproducing your problem:

    var body = d3.select("body");
    
    function foo(data) {
      var div = body.append("div")
        .selectAll("p")
        .data(data)
        .join(
          enter => enter.append("p").html(d => "enter: " + d),
          update => update.html(d => "update: " + d)
        );
    };
    
    foo([Math.random()]);
    d3.interval(function() {
      foo([Math.random()])
    }, 1000)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>

    Supposing that you want to append the group only once, you can do something like this:

    var g = svg.selectAll(".myGroup")
      .data([true]);
    
    g = g.enter()
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .merge(g);
    
    g.selectAll("text")
      .data(data)
      .join(
        enter => enter.append("text").text(d => "enter: " + d),
        update => update.text(d => "update: " + d)
      );
    

    Here, data([true]) indicates that the data itself doesn't matter, it is just a single truthy value, for appending a single group element.

    Here is a demo showing how it works:

    var body = d3.select("body");
    
    function foo(data) {
      var div = body.selectAll(".myDiv")
        .data([true]);
    
      div = div.enter()
        .append("div")
        .attr("class", "myDiv")
        .merge(div);
    
      div.selectAll("p")
        .data(data)
        .join(
          enter => enter.append("p").html(d => "enter: " + d),
          update => update.html(d => "update: " + d)
        );
    };
    
    foo([Math.random()])
    d3.interval(function() {
      foo([Math.random()])
    }, 1000)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>