Below is another version of the Mike Bostock solution and is inspired by @hughes comment on @kashesandr's answer. It makes one callback at the end of the transition .
For the 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 expand d3 like this:
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 routine var n = 0; transition.each(function() { ++n; }) .each("end", function() { if (!--n) f.apply(transition, args); }); } }); return transition; }
Like JSFiddle .
Use transition.end(callback[, delayIfEmpty[, arguments...]]) :
transition.end(function() { console.log("all done"); });
... or with extra delay if transition 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 true. It also redirects any additional arguments to callback (and only those arguments). It is important to note that it does not use callback by default if transition empty, which is probably a safer assumption in this case.