103

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?

VividD
  • 10,456
  • 6
  • 64
  • 111
Tony
  • 10,088
  • 20
  • 85
  • 139

9 Answers9

154

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

Machado
  • 8,965
  • 6
  • 43
  • 46
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 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. – Tony May 21 '12 at 20:41
  • 9
    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. – Steven Lu Oct 03 '13 at 05:44
  • 2
    I have the same issue. Wish it would run the function once after the last remove – canyon289 Jun 16 '15 at 01:38
  • 1
    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. – hobbes3 Aug 11 '16 at 10:33
  • 1
    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. – Nate Anderson Dec 31 '18 at 20:19
  • 1
    @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)` – BallpointBen Oct 13 '20 at 03:29
  • Nice! I have no idea what I was working on that could have possibly been using d3 in 2013, but hell yeah! – Steven Lu Oct 15 '20 at 20:44
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") });
kashesandr
  • 1,521
  • 28
  • 35
  • 5
    If the selection contains zero elements, the callback will never fire. One way to fix this is `if (transition.size() === 0) { callback(); }` – hughes Mar 24 '15 at 19:12
  • 1
    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? :) – prizma Nov 20 '16 at 22:30
  • @prizma are there concrete proposals? Appreciate if you could share code examples. – kashesandr Nov 21 '16 at 11:27
  • 1
    @kashesandr 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!"; // .. }` – prizma Nov 21 '16 at 12:15
  • 1
    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. – Michael Scheper Apr 19 '18 at 00:23
  • @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://stackoverflow.com/a/38537982/1061438 – kashesandr Apr 19 '18 at 08:51
  • 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. – Michael Scheper Apr 19 '18 at 18:10
  • 1
    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. – Michael Scheper Apr 19 '18 at 18:23
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);
ericsoco
  • 24,913
  • 29
  • 97
  • 127
  • Beautiful. Event handlers are gross. – KFunk Sep 08 '16 at 01:07
  • There is also `transition.remove()` ([link](https://github.com/d3/d3-transition#transition_remove)), 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." – brichins Nov 04 '16 at 18:35
  • 10
    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. – Taylor C. White Jul 04 '17 at 13:42
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();
        }
    });
Jesper We
  • 5,977
  • 2
  • 26
  • 40
  • 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. – whitestryder Sep 17 '15 at 16:51
7

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

chrismarx
  • 11,488
  • 9
  • 84
  • 97
  • 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 – DGill Dec 14 '19 at 20:42
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.

Milos
  • 2,728
  • 22
  • 24
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");
Community
  • 1
  • 1
int_ua
  • 1,646
  • 2
  • 18
  • 32
-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);
  });
ifadey
  • 1,062
  • 10
  • 12
-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)); 
Brett
  • 2,775
  • 4
  • 27
  • 32