4

I want to transition through different sets of data and thought the following approach would work:

function next(keyIndex)
{
    if(keyIndex < 3)
    {
        d3.selectAll(".dot")
           .data(dataForKey(keyIndex))
           .transition().duration(3000)
           .call(position)
           .on("end", next(keyIndex+1));
    }
}

but the .on("end") call gets invoked before the duration(3000) is finished thus the data basically transitions from the first to the last dataset.
Is there a way to properly transition from dataset[0] -> dataset[1] -> dataset[2]?

TommyF
  • 6,660
  • 8
  • 37
  • 61
  • I don't understand your question. Could you provide a fiddle? – malwin Dec 15 '16 at 11:27
  • My question would be how I can call the `next()` function only after the current transition has finished (after 3s in this example). `.on("end"...)` does not work since it apparently doesn't wait for the duration of the current transition to be completed before calling the callback method. – TommyF Dec 15 '16 at 12:07

2 Answers2

4

Note, I was going to close this as a duplicate because I think I understand the mistake of the OP. I created an answer, instead, because I'm just guessing at the problem and wanted to provide a bit of explanation.


Your contention that .on("end") call gets invoked before the duration(3000) is incorrect. The end callback does in fact fire after 3 seconds. My guess is you are miss understanding this statement in the documentation:

When a specified transition event is dispatched on a selected node, the specified listener will be invoked for the transitioning element, being passed the current datum d and index i, with the this context as the current DOM element. Listeners always see the latest datum for their element, but the index is a property of the selection and is fixed when the listener is assigned; to update the index, re-assign the listener.

What this is saying is that the event will fire for each element in the transition, so 10 elements means 10 events. The first one will occur after 3 seconds, the rest 0 to 1 milliseconds after.

See this code snippet:

<!DOCTYPE html>
<html>

<head>
  <script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>

  <svg>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
    <circle class="dot"></circle>
  </svg>

  <script>
    dataForKey = {
      0: d3.range(10),
      1: d3.range(10),
      2: d3.range(10),
      3: d3.range(10)
    };

    var date = new Date();

    next(1);

    function next(keyIndex) {
      if (keyIndex < 3) {
        d3.selectAll(".dot")
          .data(dataForKey[keyIndex])
          .transition().duration(3000)
          //.call(position)
          .on("end", function() {
            console.log('end=', new Date().getTime() - date.getTime());
            date = new Date();
            next(keyIndex + 1)
          });
      }
    }
  </script>

</body>

</html>

The console output you'll see will be something like:

end= 3008 //<-- first element after 3 seconds
end= 0 //<-- other 9 events after 0 to 1 milliseconds
end= 0
end= 0
end= 0
end= 0
end= 0
end= 0
end= 0
end= 0

Now I think your question becomes, how do I fire a callback after all transitions have ended. This has been covered many times here.

EDITS

Thanks for the fiddle, always helps to reproduce the issue. And the code is different in the fiddle then what you posted in the question.

Your problem is actually much simpler; .on('end', func) expects a function as the second arguement and you aren't giving it a function, you are calling a function and giving it the return value of the function (nothing). Just wrap it in an anon function:

.on("end", function(d){
  next(keyIndex + 1)
});

Updated fiddle.

Community
  • 1
  • 1
Mark
  • 106,305
  • 20
  • 172
  • 230
  • Thanks for your help Mark, I obviously have misunderstood something important. I hacked together a fiddle to illustrate my problem further: https://jsfiddle.net/zvt0y188/1/ My expectation would have been that it takes 3 seconds from 1:1 to 2:2, another 3 seconds from 2:2 to 3:3 etc. Instead it results in only one 3s transition from 1:1 to 9:9. – TommyF Dec 15 '16 at 13:26
  • @TommyF All that from me and your problem is actually much simpler. See edits above. – Mark Dec 15 '16 at 14:25
  • Yes, that was it, thank you for the help, it is much appreciated!! – TommyF Dec 15 '16 at 14:31
4

Your main problem is, that you do not supply a proper callback function to .on("end",...). When calling it like .on("end", next(keyIndex+1)), it will not work as expected, because the statement next(keyIndex+1) will be immediately executed, whereby starting the next iteration step before the previous step has ended. In the next step the transition, which was just started, will be cancelled and replaced by the new transition. That way you won't have a chain of transitions having the specified durations, but some interrupted ones immediately followed by a last, non-interrupted one, which will transition all the way to the final values.

The solution is to wrap your call to next() in a function expression to provide a real callback which will be called when the end event fires.

function next(keyIndex) {
  if(keyIndex < 3) {
    d3.selectAll(".dot")
      .data(dataForKey(keyIndex))
      .transition().duration(3000)
      .call(position)
      .on("end", function() { next(keyIndex+1) });  // use a function wrapper
  }
}

For a working demo have a look at the following snippet:

   var svg = d3.select("svg"),
    margin = {top: 40, right: 40, bottom: 40, left: 40},
    width = svg.attr("width") - margin.left - margin.right,
    height = svg.attr("height") - margin.top - margin.bottom,
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var xScale = d3.scaleLinear().domain([0, 10]).range([0, width]),
    yScale = d3.scaleLinear().domain([0, 10]).range([height, 0]);

    var xAxis = d3.axisBottom().scale(xScale).ticks(12, d3.format(",d")),
    yAxis = d3.axisLeft().scale(yScale);
    
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis);
         
    // inital draw
    var dot = svg.append("g")
      .attr("class", "dots")
    .selectAll(".dot")
      .data(dataForKey(1))
    .enter().append("circle")
      .attr("class", "dot")
      .style("fill", "#000000")
   .attr("cx", function(d) { return xScale(d.x); })
        .attr("cy", function(d) { return yScale(d.y); })
        .attr("r", function(d) { return 5; });
        
    // updated Data
    next(1);
    
    function dataForKey(key)
    {
     return [{x:key, y:key}];
    }
    
    function next(keyIndex) {
      if (keyIndex < 10) {
        d3.selectAll(".dot")
          .data(dataForKey(keyIndex))
          .transition().duration(3000)
     .attr("cx", function(d) { return xScale(d.x); })
          .attr("cy", function(d) { return yScale(d.y); })
          .on("end", function() { next(keyIndex + 1) });
      }
    }
<script src="https://d3js.org/d3.v4.js"></script>

<svg width="300" height="300"></svg>
altocumulus
  • 21,179
  • 13
  • 61
  • 84