JSFiddle - React, Tailwind, and code Playground

HTML

<h1><pre></pre></h1>
<p>Even with zero circles, <strong>d3.transition.end</strong> will apply its <strong>callback</strong> after <strong>delayIfEmpty</strong> milliseconds. Passing falsey second argument will skip it, and the value of <strong>0</strong> will apply it without delay.</p>
<input type="number" min="0" value="3" onclick="showCircles(this.value)">

CSS

html, body, svg {
    margin: 0;
    width: 100%;
    height: 100%;
    background: gold;
    text-align: center;
}

circle {
    fill: olive;
}

input[type="number"] {
    outline: none;
    font: bold 2em monospace;
    background: gold;
    border: none;
    width: 2em;
}

p {
    font-family: monospace;
    width: 50%;
    margin-left: auto;
    margin-right: auto;
}

JavaScript

// re: http://stackoverflow.com/a/20773846/1409907

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);
}

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 rutine
            var n = 0; 
            transition.each(function() { ++n; }) 
                .each("end", function() { 
                    if (!--n) f.apply(transition, args); 
                });
        }
    });
    
    return transition;
}

function showCircles(testSize) {
    
    var msg = d3.select("pre").text("...");
    
    d3.selectAll("svg").remove();

    var svg = d3.select("body").append("svg");
   
    var circles = svg.selectAll("circle")
        .data(d3.range(testSize))
        .enter()
        .append("circle")
        .attr({ r: 10, cx: "20%", cy: function(d,i){return i * 25 + 50;}});
    
    var callCount = 0;
    
    circles.transition()
        .delay(500)
        .duration(1500)
        .attr("cx", "80%")
        .end(function(x) {
            console.log(arguments);
            msg.text("all done (called " + (++callCount) + " times " + x +")");
        }, 1500, "with args");
}

var initValue = parseInt(d3.select("input[type='number']").node().value);
showCircles(initValue);