Invoke a callback at the end of a transition
Asked Answered
S

9

104

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?

Spieler answered 21/5, 2012 at 20:25 Comment(0)
I
155

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().

Ils answered 21/5, 2012 at 20:33 Comment(7)
Thank you very much. This is a GREAT GREAT library, but it is not so easy to find the important information in the documentation.Spieler
So, my problem with this way of continuing from the end of the transition is that it runs your function N times (for N items in the set of transitioning elements). This is far from ideal sometimes.Henning
I have the same issue. Wish it would run the function once after the last removeSherlynsherm
How do you perform a callback only after all the transitions finished for a d3.selectAll() (instead after each element finishes)? In other words, I just want to callback one function once all of the elements finish transitioning.Braden
Hi , the first link to stack/group bar chart points to an Observable notebook which doesn't use any .each event listener, nor the "end" event. It doesn't seem to "chain" transitions. The second link points to a github which doesn't load for me.Pedicle
@StevenLu If you wish to run a function once when all selected elements have finished transitioning, use .end(), which returns a promise: selection.transition().attr(...).end().then(callback, callbackIfCanceled)Aracelyaraceous
Nice! I have no idea what I was working on that could have possibly been using d3 in 2013, but hell yeah!Henning
T
66

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") });
Toronto answered 25/12, 2013 at 14:35 Comment(8)
If the selection contains zero elements, the callback will never fire. One way to fix this is if (transition.size() === 0) { callback(); }Browder
if (!callback) callback = function(){}; why not return instantly, or throw an exception? An invalid callback does defeat the whole purpose of this rutine, why go through with it like a blind watchmaker? :)Davison
@Davison are there concrete proposals? Appreciate if you could share code examples.Toronto
@Toronto one can simply do nothing, since the user will experience the same effect: (no callback call at the end of the transition) function endall(transition, callback){ if(!callback) return; // ... } or, since it is most certanly an error to call this function without a callback, throwing an exception seams to be the appropriate way to handle the situation I think this case does not need too complicated Exception function endall(transition, callback){ if(!callback) throw "Missing callback argument!"; // .. }Davison
So when we have separate enter() and exit() transitions, and want to wait until all three have finished, we need to put code in the callback to make sure it's been invoked three times, right? D3 is so messy! I wish I'd chosen another library.Spread
@MichaelScheper unfortunately there are not a lot of alternatives like d3 that provide so rich customizations. If you use v3 - yes, if you use v4 - they added extra functionality that makes it easier, look at this answer https://mcmap.net/q/204553/-invoke-a-callback-at-the-end-of-a-transitionToronto
Thanks, @kashesandr. But as a comment on that answer notes, the callback is invoked for each element. It's also unreliable: if no elements match the selection, it doesn't get invoked—and according to the docs, if transitions are still pending, it will also never be invoked! And all this uncertainty is before enter() and exit() are even considered. So while D3 is good for making graphs out of data, it was a mistake for me to try to use it for interactive elements where the timing of events is important. My code has become riddled with race conditions. ☹ I should've just stuck with jQuery.Spread
I should add, I realise your answer solves some of the problems I griped about, and I can write a utility function to apply it. But I haven't found an elegant way to apply it and still allow additional customisation for each transition, especially when the transitions for new and old data are different. I'm sure I'll come up with something, but 'invoke this callback when all these transitions have finished' seems like a use case that should be supported out of the box, in a library as mature as D3. So it seems I've chosen the wrong library—not really D3's fault. Anyhoo, thanks for your help.Spread
N
44

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);
Nerveracking answered 23/7, 2016 at 3:54 Comment(3)
Beautiful. Event handlers are gross.Hugibert
There is also transition.remove() (link), which handles a common use case of transitioning an element from view: `"For each selected element, removes the element when the transition ends, as long as the element has no other active or pending transitions. If the element has other active or pending transitions, does nothing."Guitarist
It looks like this is called PER element that the transition is applied to, which is not what the question is in regards to from my understanding.Charissa
F
10

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();
        }
    });
Followup answered 24/7, 2014 at 19:23 Comment(1)
Thanks, that worked nicely for me. I was trying to customize the x-axis label orientation automatically after loading a discrete bar chart. The customization can't take effect before load, and this provided an event hook through which I could do this.Michelinemichell
S
8

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

Scurry answered 4/9, 2019 at 22:12 Comment(1)
This is a very nice way of handling things. I'll just say, for those of you like me who don't know all of v5 and would like to implement just this, you can import the new transition library using <script src="d3js.org/d3-transition.v1.min.js"></script>Aft
O
6

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.

Osmanli answered 28/5, 2015 at 13:3 Comment(5)
That's nice, I like it.Toronto
Thanks @kashesandr. This was indeed inspired by your answer to begin with!Osmanli
don't really think we need a drop function or passing of arguments, since the same effect can be achieved by a wrapper function or by utilizing bind. Otherwise I think it's a great solution +1Lightfingered
Works like a charm !Prosit
See this response, .end() has now been officially added - https://mcmap.net/q/204553/-invoke-a-callback-at-the-end-of-a-transitionScurry
B
0

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");
Baiel answered 24/1, 2017 at 18:8 Comment(0)
K
-2

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);
  });
Kellyekellyn answered 21/5, 2014 at 13:10 Comment(0)
I
-2

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)); 
Insolvency answered 2/2, 2015 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.