Invoke a callback at the end of a transition

Javascriptd3.jsTransition

Javascript Problem Overview


I need to make a FadeOut method (similar to jQuery) using D3.js. What I need to do is to set the opacity to 0 using transition().

d3.select("#myid").transition().style("opacity", "0");

The problem is that I need a callback to realize when the transition has finished. How can I implement a callback?

Javascript Solutions


Solution 1 - Javascript

You want to listen for the "end" event of the transition.

// d3 v5
d3.select("#myid").transition().style("opacity","0").on("end", myCallback);

// old way
d3.select("#myid").transition().style("opacity","0").each("end", myCallback);
  • This demo uses the "end" event to chain many transitions in order.
  • The donut example that ships with D3 also uses this to chain together multiple transitions.
  • Here's my own demo that changes the style of elements at the start and end of the transition.

From the documentation for transition.each([type],listener):

> If type is specified, adds a listener for transition events, supporting both "start" and "end" events. The listener will be invoked for each individual element in the transition, even if the transition has a constant delay and duration. The start event can be used to trigger an instantaneous change as each element starts to transition. The end event can be used to initiate multi-stage transitions by selecting the current element, this, and deriving a new transition. Any transitions created during the end event will inherit the current transition ID, and thus will not override a newer transition that was previously scheduled.

See this forum thread on the topic for more details.

Finally, note that if you just want to remove the elements after they have faded out (after the transition has finished), you can use transition.remove().

Solution 2 - Javascript

Mike Bostock's solution for v3 with a small update:

  function endall(transition, callback) { 
    if (typeof callback !== "function") throw new Error("Wrong callback in endall");
    if (transition.size() === 0) { callback() }
    var n = 0; 
    transition 
        .each(function() { ++n; }) 
        .each("end", function() { if (!--n) callback.apply(this, arguments); }); 
  } 

  d3.selectAll("g").transition().call(endall, function() { console.log("all done") });

Solution 3 - Javascript

Now, in d3 v4.0, there is a facility for explicitly attaching event handlers to transitions:

https://github.com/d3/d3-transition#transition_on

To execute code when a transition has completed, all you need is:

d3.select("#myid").transition().style("opacity", "0").on("end", myCallback);

Solution 4 - Javascript

A slightly different approach that works also when there are many transitions with many elements each running simultaneously:

var transitions = 0;

d3.select("#myid").transition().style("opacity","0").each( "start", function() {
		transitions++;
	}).each( "end", function() {
		if( --transitions === 0 ) {
			callbackWhenAllIsDone();
		}
	});

Solution 5 - Javascript

The following is another version of Mike Bostock's solution and inspired by @hughes' comment to @kashesandr's answer. It makes a single callback upon transition's end.

Given a drop function...

function drop(n, args, callback) {
    for (var i = 0; i < args.length - n; ++i) args[i] = args[i + n];
    args.length = args.length - n;
    callback.apply(this, args);
}

... we can extend d3 like so:

d3.transition.prototype.end = function(callback, delayIfEmpty) {
    var f = callback, 
        delay = delayIfEmpty,
        transition = this;
    
    drop(2, arguments, function() {
        var args = arguments;
        if (!transition.size() && (delay || delay === 0)) { // if empty
            d3.timer(function() {
                f.apply(transition, args);
                return true;
            }, typeof(delay) === "number" ? delay : 0);
        } else {                                            // else Mike Bostock's routine
            var n = 0; 
            transition.each(function() { ++n; }) 
                .each("end", function() { 
                    if (!--n) f.apply(transition, args); 
                });
        }
    });
    
    return transition;
}

As a JSFiddle.

Use transition.end(callback[, delayIfEmpty[, arguments...]]):

transition.end(function() {
    console.log("all done");
});

... or with an optional delay if transition is empty:

transition.end(function() {
    console.log("all done");
}, 1000);

... or with optional callback arguments:

transition.end(function(x) {
    console.log("all done " + x);
}, 1000, "with callback arguments");

d3.transition.end will apply the passed callback even with an empty transition if the number of milliseconds is specified or if the second argument is truthy. This will also forward any additional arguments to the callback (and only those arguments). Importantly, this will not by default apply the callback if transition is empty, which is probably a safer assumption in such a case.

Solution 6 - Javascript

As of D3 v5.8.0+, there is now an official way to do this using transition.end. The docs are here:

https://github.com/d3/d3-transition#transition_end

A working example from Bostock is here:

https://observablehq.com/@d3/transition-end

And the basic idea is that just by appending .end(), the transition will return a promise that won't resolve until all elements are done transitioning:

 await d3.selectAll("circle").transition()
      .duration(1000)
      .ease(d3.easeBounce)
      .attr("fill", "yellow")
      .attr("cx", r)
    .end();

See the version release notes for even more:

https://github.com/d3/d3/releases/tag/v5.8.0

Solution 7 - Javascript

Mike Bostock's solution improved by kashesandr + passing arguments to the callback function:

function d3_transition_endall(transition, callback, arguments) {
    if (!callback) callback = function(){};
    if (transition.size() === 0) {
        callback(arguments);
    }

    var n = 0;
    transition
        .each(function() {
            ++n;
        })
        .each("end", function() {
            if (!--n) callback.apply(this, arguments);
    });
}

function callback_function(arguments) {
        console.log("all done");
        console.log(arguments);
}

d3.selectAll("g").transition()
    .call(d3_transition_endall, callback_function, "some arguments");

Solution 8 - Javascript

Actually there's one more way to do this using timers.

var timer = null,
    timerFunc = function () {
      doSomethingAfterTransitionEnds();
    };

transition
  .each("end", function() {
    clearTimeout(timer);
    timer = setTimeout(timerFunc, 100);
  });

Solution 9 - Javascript

I solved a similar problem by setting a duration on transitions using a variable. Then I used setTimeout() to call the next function. In my case, I wanted a slight overlap between the transition and the next call, as you'll see in my example:

var transitionDuration = 400;

selectedItems.transition().duration(transitionDuration).style("opacity", .5);

setTimeout(function () {
  sortControl.forceSort();
}, (transitionDuration * 0.75)); 

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTonyView Question on Stackoverflow
Solution 1 - JavascriptPhrogzView Answer on Stackoverflow
Solution 2 - JavascriptkashesandrView Answer on Stackoverflow
Solution 3 - JavascriptericsocoView Answer on Stackoverflow
Solution 4 - JavascriptJesper WeView Answer on Stackoverflow
Solution 5 - JavascriptMilosView Answer on Stackoverflow
Solution 6 - JavascriptchrismarxView Answer on Stackoverflow
Solution 7 - Javascriptint_uaView Answer on Stackoverflow
Solution 8 - JavascriptifadeyView Answer on Stackoverflow
Solution 9 - JavascriptBrettView Answer on Stackoverflow