d3: How to properly chain transitions on different selections
Asked Answered
L

2

22

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.

Liquidator answered 13/6, 2013 at 11:53 Comment(0)
R
34

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.

Retral answered 14/6, 2013 at 5:43 Comment(3)
Correct me if I'm wrong, but I think there's a small error in your code using the delay()-Method. When there is no update transition (i.e. no elements change place), items.enter().empty() still equals false, so elemens exit, then, for duration milliseconds nothing happens, and then the enter transition starts. But if there is no visual update transition taking place, I want the exit transition to be followed immediately by the enter transition. I thus save the update transition as follows:Liquidator
var updatedItems = div.transition() .duration(duration) .delay(!div.exit().empty() * duration) and change .delay((!div.exit().empty() + !div.enter().empty()) * duration) into .delay((!div.exit().empty() + !updatedItems.empty()) * duration). This way it works as required.Liquidator
@Retral It's a shame bl.ocks.org is down. It was a great learning resource, and many helpful answers like this one relied on it.Alvarez
A
0

I'm a fool for contradicting @mbostock over his own library, but I think as of D3 version 7, the explicit delay method is the only option for sequential transitions involving different selections.

transition.transition() does allow you to chain sequential transitions for the same selection. The implementation shows that the scheduling of the transition is derived from the parent's delay + duration, causing it to happen after:

        schedule(node, name, id1, i, group, {
          time: inherit.time + inherit.delay + inherit.duration,
          delay: 0,
          duration: inherit.duration,
          ease: inherit.ease
        });

However, selection.transition(t) does not do this, it inherits the same scheduling as the passed transition t, so it animates concurrently, not sequentially.

Unfortunately all the transition selection methods like transition.select() call selection.transition(t) which means you get concurrent transitions.

Alvarez answered 20/7 at 0:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.