Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3v4 - on("end") called before transition.duration is over

Tags:

d3.js

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]?

like image 664
TommyF Avatar asked Feb 05 '23 12:02

TommyF


2 Answers

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>
like image 124
altocumulus Avatar answered Feb 14 '23 22:02

altocumulus


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="[email protected]" 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.

like image 27
Mark Avatar answered Feb 15 '23 00:02

Mark