Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flush a d3 v4 transition

Does someone know of a way to 'flush' a transition. I have a transition defined as follows:

this.paths.attr('transform', null)
  .transition()
  .duration(this.duration)
  .ease(d3.easeLinear)
  .attr('transform', 'translate(' + this.xScale(translationX) + ', 0)')

I am aware I can do

this.paths.interrupt();

to stop the transition, but that doesn't finish my animation. I would like to be able to 'flush' the transition which would immediately finish the animation.

like image 573
Gabriel Avatar asked Jan 22 '18 14:01

Gabriel


2 Answers

Andrew's answer is a great one. However, just for the sake of curiosity, I believe it can be done without extending prototypes, using .on("interrupt" as the listener.

Here I'm shamelessly copying Andrew code for the transitions and this answer for getting the target attribute.

selection.on("click", function() {
    d3.select(this).interrupt()
})

transition.on("interrupt", function() {
    var elem = this;
    var targetValue = d3.active(this)
        .attrTween("cx")
        .call(this)(1);
    d3.select(this).attr("cx", targetValue)
})

Here is the demo:

var svg = d3.select("svg")

var circle = svg.selectAll("circle")
  .data([1, 2, 3, 4, 5, 6, 7, 8])
  .enter()
  .append("circle")
  .attr("cx", 50)
  .attr("cy", function(d) {
    return d * 50
  })
  .attr("r", 20)
  .on("click", function() {
    d3.select(this).interrupt()
  })

circle
  .transition()
  .delay(function(d) {
    return d * 500;
  })
  .duration(function(d) {
    return d * 5000;
  })
  .attr("cx", 460)
  .on("interrupt", function() {
    var elem = this;
    var targetValue = d3.active(this)
      .attrTween("cx")
      .call(this)(1);
    d3.select(this).attr("cx", targetValue)
  })
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="500"></svg>

PS: Unlike Andrew's answer, since I'm using d3.active(node) here, the click only works if the transition had started already.

like image 195
Gerardo Furtado Avatar answered Nov 03 '22 21:11

Gerardo Furtado


If I understand correctly (and I might not) there is no out of the box solution for this without going under the hood a bit. However, I believe you could build the functionality in a relatively straightforward manner if selection.interrupt() is of the form you are looking for.

To do so, you'll want to create a new method for d3 selections that access the transition data (located at: selection.node().__transition). The transition data includes the data on the tweens, the timer, and other transition details, but the most simple solution would be to set the duration to zero which will force the transition to end and place it in its end state:

The __transition data variable can have empty slots (of a variable number), which can cause grief in firefox (as far as I'm aware, when using forEach loops), so I've used a keys approach to get the non-empty slot that contains the transition.

d3.selection.prototype.finish = function() {
    var slots = this.node().__transition;
    var keys = Object.keys(slots);
    keys.forEach(function(d,i) {
        if(slots[d]) slots[d].duration = 0;
    })  
}

If working with delays, you can also trigger the timer callback with something like: if(slots[d]) slots[d].timer._call();, as setting the delay to zero does not affect the transition.

Using this code block you call selection.finish() which will force the transition to its end state, click a circle to invoke the method:

d3.selection.prototype.finish = function() {
  var slots = this.node().__transition;
  var keys = Object.keys(slots);
  keys.forEach(function(d,i) {
    if(slots[d]) slots[d].timer._call(); 
  })	
}
	
var svg = d3.select("body")
   .append("svg")
   .attr("width", 500)
   .attr("height", 500);
	
var circle = svg.selectAll("circle")
   .data([1,2,3,4,5,6,7,8])
   .enter()
   .append("circle")
   .attr("cx",50)
   .attr("cy",function(d) { return d * 50 })
   .attr("r",20)
   .on("click", function() {  d3.select(this).finish() })
	
circle
   .transition()
   .delay(function(d) { return d * 500; })
   .duration(function(d) { return d* 5000; })
   .attr("cx", 460)
   .on("end", function() {
      d3.select(this).attr("fill","steelblue"); // to visualize end event
   })
	
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js"></script>

Of course, if you wanted to keep the method d3-ish, return the selection so you can chain additional methods on after. And for completeness, you'll want to ensure that there is a transition to finish. With these additions, the new method might look something like:

d3.selection.prototype.finish = function() {
  // check if there is a transition to finish:
  if (this.node().__transition) {
      // if there is transition data in any slot in the transition array, call the timer callback:
      var slots = this.node().__transition;
      var keys = Object.keys(slots);
      keys.forEach(function(d,i) {
        if(slots[d]) slots[d].timer._call(); 
   })   
  }
  // return the selection:
  return this;
}

Here's a bl.ock of this more complete implementation.


The above is for version 4 and 5 of D3. To replicate this in version 3 is a little more difficult as timers and transitions were reworked a bit for version 4. In version three they are a bit less friendly, but the behavior can be achieved with slight modification. For completeness, here's a block of a d3v3 example.

like image 45
Andrew Reid Avatar answered Nov 03 '22 20:11

Andrew Reid