Search code examples
animationd3.jstransition

d3: How to properly chain transitions on different selections


I am using V3 of the popular d3 library and basically want to have three transitions, followed by each other: The first transition should apply to the exit selection, the second to the update selection and the third to the enter selection. They should be chained in such a manner that when one of the selections is empty, its respective transition is skipped. I.e. when there is no exit selection, the update selection should start immediately. So far, I have come up with this code (using the delay function).

// DATA JOIN
var items = d3.select('#data').selectAll('.item');
items = items.data(data, function(d){ 
    return d.twitter_screenname;
});


// EXIT
items.exit().transition().duration(TRANSITION_DURATION).style('opacity', 0).remove();

// UPDATE
// Divs bewegen
items.transition().duration(TRANSITION_DURATION).delay(TRANSITION_DURATION * 1)
    .style('left', function(d, i) {
        return positions[i].left + "px";
    }).style('top', function(d, i) {
        return positions[i].top + "px";
    });

// ENTER
// Divs hinzufügen
var div = items.enter().append('div')
    .attr('class', 'item')
    .style('left', function(d, i) {
        return positions[i].left + "px";
    }).style('top', function(d, i) {
        return positions[i].top + "px";
    });

 div.style('opacity', 0)
    .transition().duration(TRANSITION_DURATION).delay(TRANSITION_DURATION * 2)
    .style('opacity', 1);

First of all it doesn't allow to "skip" transitions and secondly I think there is a better way than delay. I've looked at http://bl.ocks.org/mbostock/3903818 but I did not really understand what is happening.

Also, somehow just writing items.exit().transition().duration(TRANSITION_DURATION).remove() does not work with the items, probably because they are not SVG elements but divs.


Solution

  • Sure. Here are two ways.

    First, you could use an explicit delay, which you then compute using selection.empty to skip empty transitions. (This is only a minor modification of what you have already.)

    var div = d3.select("body").selectAll("div")
        .data(["enter", "update"], function(d) { return d || this.textContent; });
    
    // 2. update
    div.transition()
        .duration(duration)
        .delay(!div.exit().empty() * duration)
        .style("background", "orange");
    
    // 3. enter
    div.enter().append("div")
        .text(function(d) { return d; })
        .style("opacity", 0)
      .transition()
        .duration(duration)
        .delay((!div.exit().empty() + !div.enter().empty()) * duration)
        .style("background", "green")
        .style("opacity", 1);
    
    // 1. exit
    div.exit()
        .style("background", "red")
      .transition()
        .duration(duration)
        .style("opacity", 0)
        .remove();
    

    http://bl.ocks.org/mbostock/5779682

    One tricky thing here is that you have to create the transition on the updating elements before you create the transition on the entering elements; that’s because enter.append merges entering elements into the update selection, and you want to keep them separate; see the Update-only Transition example for details.

    Alternatively, you could use transition.transition to chain transitions, and transition.each to apply these chained transitions to existing selections. Within the context of transition.each, selection.transition inherits the existing transition rather than creating a new one.

    var div = d3.select("body").selectAll("div")
        .data(["enter", "update"], function(d) { return d || this.textContent; });
    
    // 1. exit
    var exitTransition = d3.transition().duration(750).each(function() {
      div.exit()
          .style("background", "red")
        .transition()
          .style("opacity", 0)
          .remove();
    });
    
    // 2. update
    var updateTransition = exitTransition.transition().each(function() {
      div.transition()
          .style("background", "orange");
    });
    
    // 3. enter
    var enterTransition = updateTransition.transition().each(function() {
      div.enter().append("div")
          .text(function(d) { return d; })
          .style("opacity", 0)
        .transition()
          .style("background", "green")
          .style("opacity", 1);
    });
    

    http://bl.ocks.org/mbostock/5779690

    I suppose the latter is a bit more idiomatic, although using transition.each to apply transitions to selections (rather than derive transitions with default parameters) isn’t a widely-known feature.