d3 / d3-transition

Animated transitions for D3 selections.
https://d3js.org/d3-transition
ISC License
224 stars 65 forks source link

On end method of a group of concurrent transitions...is possible? #76

Closed ElPsyCongro closed 5 years ago

ElPsyCongro commented 6 years ago

Hi, I would like to know if it's posible to somehow group certain concurrent transitions and execute a function when all of them are finished. The thing is that I needed to do a transition to animate the modification of two attributes for the same d3 selection so I needed to use concurrent transitions the way that is here: https://bl.ocks.org/mbostock/5348789

But now I need to execute a specific function after all transitions are finished. Anyone know a way to achieve this?

mindrones commented 6 years ago

This is something I have to do sometimes too and I always end up doing something like this:

const size = selection.size();
const done = 0;
const finalFunction = () => {
  console.log('all done');
}

selection
.transition()
.delay(d => {
  // return a delay that depends on d
})
.duration(d => {
  // return a duration that depends on d
})
.on('end', d => {
  // ...
  done += 1;
  if (done === size) {
    finalFunction()
  }
}

but I too would like something cleaner.

It would be nice to have something generic so that we can detect if a particular transition is the first or the last one like:

const transition = selection.transition();

transition
.delay(d => {
  // return a delay that depends on d
})
.duration(d => {
  // return a duration that depends on d
})
.on('start', d => {
  if (transition.isFirst) { // ... }
  if (transition.isLast) { // ... }
})
.on('end', d => {
  if (transition.isFirst) { // ... }
  if (transition.isLast) { // ... }
})

so keeping that count on the transition object. Or it could be something like:

.on('start', (d, i, event) => {
  if (event.isFirst) { // ... }
  if (event.isLast) { // ... }
})

This new event object would have properties like:

I'm not sure this would be good design, just throwing ideas here, but sure this is something I'd use.

EDIT (*) we could attach delay and duration as attributes to the datum like in the snippet below but sometimes we need the datum to be immutable, or simply we don't want to pollute it with temporary values, as that would require us to clean it up at the end of the transition.

.duration(d => {
  d.duration = 2 * d.value;
  return d.duration;
})
.on('end', d => {
  // use d.duration
  delete d.duration;
})
mindrones commented 6 years ago

On second thought, supposing to have the transition index in time we wouldn't need isFirst and isLast, we could just compare index and size; on the other hand, I'm not sure what it should happen if 2 or more elements end the transition concurrently as if they get the same index, comparing index and size becomes useless. :thinking: Maybe index should be a count instead 😄

mindrones commented 6 years ago

Here's a quite naïve example of what I mean: https://bl.ocks.org/mindrones/123832bbf5b08ce4b28dab66fae21a69

mbostock commented 6 years ago

Now that await-async is a thing, I’ve been thinking that transitions could have some Promise-based APIs. One such idea that would address this issue is a transition.end method #77.

mbostock commented 5 years ago

Folding into #77.