Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transition parameter inheritance #400

Closed
mbostock opened this issue Dec 7, 2011 · 15 comments
Closed

transition parameter inheritance #400

mbostock opened this issue Dec 7, 2011 · 15 comments
Milestone

Comments

@mbostock
Copy link
Member

mbostock commented Dec 7, 2011

With more complicated transitions, it's often necessary to create multiple transition objects. This is because the selectAll + data is used to compute the enter, update and exit separately, and transitions do not support the data operator (and enter, exit, append and insert). It's tempting to consider whether it would be possible to support the data operator, which would take effect immediately, and allow a transition to be created once regardless of its complexity.

@mbostock
Copy link
Member Author

For example, currently we have to do something like this:

var bar = svg.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect")
    .attr("class", "bar")
    // initialize entering bars

bar.transition()
    .duration(750)
    // transition entering + updating bars

bar.exit().transition()
    .duration(750)
    .remove()
    // transition exiting bars

svg.select(".x.axis").transition()
    .duration(750)
    .call(xAxis);

svg.select(".y.axis").transition()
    .duration(750)
    .call(yAxis);

The duplication of the duration (750) is annoying. Also, these transitions have different ids and now times, which isn't ideal.

I suppose what I want is something like this:

var transition = svg.transition()
    .duration(750);

var bar = transition.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect")
    .attr("class", "bar")
    // initialize entering bars

bar
    // transition entering + updating bars

bar.exit().remove()
    // transition exiting bars

transition.select(".x.axis").call(xAxis);
transition.select(".y.axis").call(yAxis);

But that's not quite right, because the bar.enter() part should be instantaneous, rather than part of the transition. I suppose you could go back to the underlying selection from a transition by saying transition.selection?

var transition = svg.transition()
    .duration(750);

var bar = transition.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect").selection()
    .attr("class", "bar")
    // initialize entering bars

bar
    // transition entering + updating bars

bar.exit().remove()
    // transition exiting bars

transition.select(".x.axis").call(xAxis);
transition.select(".y.axis").call(yAxis);

That seems a bit weird, though.

/cc @jasondavies to see if he has any ideas?

@mbostock
Copy link
Member Author

Hmm. Maybe another option is that, by default, transitions inherit id, name, delay and duration from parent nodes?

var transition = svg.transition().duration(750);
transition.select(".x.axis").call(xAxis);
transition.select(".y.axis").call(yAxis);

var bar = svg.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect")
    .attr("class", "bar")
    // initialize entering bars

bar.transition()
    // transition entering + updating bars

bar.exit().transition()
    .remove()
    // transition exiting bars

But I think we'd have to restrict this to transitions that haven't started yet, because you wouldn't want to select elements later and create another transition on child elements that inherits an old transition's settings.

@shawnbot
Copy link

FWIW, I really like the way that Flare Transitioner objects handled this because only required specifying the duration and easing once and allowed you to orchestrate multiple property transitions from a single object:

var t = new Transitioner(1); // 1 second
t.$(foo).height = 20;
t.$(bar).width = 100;
t.play();

I've found myself doing this in d3 to mimic that behavior, minus the ability to start and stop the entire thing with a single call:

var duration = 500, ease = "bounce",
function transition(selection) {
  return selection.transition(duration).ease(ease);
}

// move bars to their appropriate positions
transition(d3.selectAll(".bar"))
  .attr("x", function(d, i) { return i * 20; })
  .attr("y", function(d, i) { return d * 100; });

transition(d3.selectAll(".label"))
  // do something else with labels in the same amount of time

This way you can, for instance, conditionally define transition() as the identity function if you want to apply the changes immediately. So, while having selection.call() makes it easier to encapsulate selection-specific transitions, sometimes you've got a bunch of transformations to do on multiple selections, and code full of .transition().duration(duration).ease(ease) calls starts to become less maintainable or just plain ugly. And you still have to manage starting and stopping them if you have other code that works with the same selections, which can be a pain in the ass.

I do like the idea of having transitions cascade down, though. Does that mean that in your example you'd be able to stop all of the transformations simply by calling transition.stop()?

@mbostock
Copy link
Member Author

Yeah, that's generally what I've been doing as well, so a more accurate representation of my example is this:

var bar = svg.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect")
    .attr("class", "bar")
    // initialize entering bars

transition(bar)
    // transition entering + updating bars

transition(bar.exit())
    // transition exiting bars
    .remove()

transition(svg.select(".x.axis"))
    .call(xAxis);

transition(svg.select(".y.axis"))
    .call(yAxis);

function transition(selection) {
  return selection.transition().duration(750);
}

It's already the case that you can do subselections in transition, so the axes transition can also be written like this:

var svgUpdate = transition(svg);
svgUpdate.select(".x.axis").call(xAxis);
svgUpdate.select(".y.axis").call(yAxis);

The problem is that if you use the data operator, you can't continue this pattern of nested transitions. (Because transitions don't support the data operator.)

The cascading transitions are pretty flexible, because the subselections inherit the delay and duration. So if you have multiple svgs selected, and they have variable delays, and you then select some child elements of those svgs, they'll inherit the delays. That's a lot harder to do with the transition method (or a transitioner object) because it doesn't have the nested context in which to inherit the delay and duration.

The only way to stop transitions currently is to create a new transition that supersedes the existing one. This will stop any current transition and prevent any previously-scheduled ones from running. For example:

d3.selectAll(".bar,.label").transition(); // stops any previous transition

@jasondavies
Copy link
Contributor

I quite like your second suggestion of inheriting the parent transition settings by default. I assume that setting the delay or duration will automatically create a new id internally so it's unlikely this will break anything, aside from any code that doesn't currently set the duration/delay explicitly.

Your first suggestion is logical but I do agree handling enter is a bit tricky and I'd be concerned that there is a conceptual mix-up: on the one hand you have transitions that represent interpolation of attributes and styles over time to some target values, but then you also have immediate appending of elements via .enter().append(). I suppose you could have .enter().selection().append() (?) but I think that's even more confusing!

@mbostock
Copy link
Member Author

I thought some more about inheriting from the parent, and the problem is that it’s not specific enough: it’s possible to have multiple concurrent transitions on the same element (using transition.transition()), so there could be ambiguity as to which delay and duration you want to inherit. Also, it’s a bit ugly (and potentially expensive) to crawl up the parent nodes when constructing a new transition. And, if you wrote a custom selector function, the subtransition might not be a descendent (though that's a bit of a degenerate case).

And yeah, the idea that you might have to go back to a selection from a transition is unpleasant. There is a nice conceptual simplicity that transitions can only be created as leaf nodes (or at least, that you can only go from selection -> transition and not vice versa). That's similar to the restriction on method chaining, where you can only descend via select or append, and have to keep a local variable if you want to go back up to the parent.

Perhaps all we need is to codify the transition function pattern; perhaps this could be the "transitioner" like Shawn suggested, or a prototype transition. Thinking aloud, it could be something like this:

var t = d3.transitioner()
    .duration(750);

var bar = svg.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect")
    .attr("class", "bar")
    // initialize entering bars

bar.transition(t)
    // transition entering + updating bars

bar.exit().transition(t)
    // transition exiting bars
    .remove()

svg.select(".x.axis").transition(t)
    .call(xAxis);

svg.select(".y.axis").transition(t)
    .call(yAxis);

By using a transitioner, all the transitions could share an id and a reference time, as well as delay and duration functions (which are evaluated when each transition is created, not the transitioner). The transitioner could also capture a list of all elements that were added to the transition, so that you could cancel() the transition later as Shawn suggested.

The only thing I don't like about this idea is the name. I feel like calling it d3.transition(), but there's already a method with that name that does something else. I wonder if we can reuse it? Perhaps it's as simple as saying selection.transition(otherTransition), where the otherTransition is used to inherit id, time, delay and duration?

@jasondavies
Copy link
Contributor

I like it! I don't see any problem with reusing d3.transition() per se. I think this also means the same id can be shared across multiple selections that don't even have the same parents, so that might also be a win.

Being able to inherit from an arbitrary transition could also be potentially useful in loosely coupled code e.g. d3.svg.axis, which already appears to inherit the transition id. :)

@shawnbot
Copy link

+1 to that whole last chunk of code and the d3.transition() name.

/cc @rachelbinx, who has been doing a lot of d3 transitioning in MTV projects for the last 4 months.

@jasondavies
Copy link
Contributor

A first stab: jasondavies/d3@899f627fb41a159bb85bc03792b8dbd671b50af1.

  • What happens when delay/duration is modified on a "child" transition? Maybe delay and duration should always be set via d3.transition()?
  • It doesn't capture a list of elements for .cancel() yet.

@mbostock
Copy link
Member Author

Yeah, that looks about right. I was thinking of using private variables for delay and duration, and moving those methods to closures. Then transition.delay() could return the delay value/function, and transition.duration() could do the same. Maybe?

I think it's reasonable that if the inherited transition (parent) is modified after being inherited (to the child), those changes aren't propagated to the child. Is that what you meant?

@mbostock
Copy link
Member Author

And, thanks for the speedy implementation! :)

@jasondavies
Copy link
Contributor

No, I meant that there might be a problem due to both having the same id but different delay/duration, but I realise now that subgroups can all have different delays and durations so this question is moot. :)

@mbostock
Copy link
Member Author

Related: for implementing components, it would be nice to be able to derive a transition + a selection from an existing transition or selection. See for example d3.svg.axis:

selection.each(function(d, i, j) {
  var g = d3.select(this);

  // If selection is a transition, create subtransitions.
  var transition = selection.delay ? function(o) {
    var id = d3_transitionInheritId;
    try {
      d3_transitionInheritId = selection.id;
      return o.transition()
          .delay(selection[j][i].delay)
          .duration(selection[j][i].duration)
          .ease(selection.ease());
    } finally {
      d3_transitionInheritId = id;
    }
  } : Object;

  // do stuff here…

});

This should be easier.

@mbostock
Copy link
Member Author

Please take a look at #573, which makes it you can inherit transitions within the context of transition.each. For example, consider this:

things.each(function(d, i, j) {
  var thing = d3.select(this).attr("foo", 0);
  d3.transition(thing).attr("foo", 42);
});

If things is a transition, then d3.transition(thing) will inherit the id, delay, duration and easing function from the corresponding element in the parent transition. If, on the other hand, things is a selection, then d3.transition(thing) simply returns thing, in-effect resulting in an instantaneous transition with no overhead.

I've modified d3.svg.axis to use d3.transition(selection) rather than the above magical transition function, and I'm pretty happy with this solution. What do you all think?

Note: we could still introduce a "transitioner" class in the future. I guess we're discussing two different but related needs in this thread.

@mbostock
Copy link
Member Author

As it turns out, a transitioner object isn't needed since you can inherit using transition.each:

d3.transition().ease("bounce").duration(750).each(function() {
  var bar = svg.selectAll(".bar")
      .data(data, function(d) { return d.key; });

  bar.enter().append("rect")
      .attr("class", "bar")
      // initialize entering bars

  bar.transition()
      // transition entering + updating bars

  bar.exit().transition()
      // transition exiting bars
      .remove()

  svg.select(".x.axis").transition()
      .call(xAxis);

  svg.select(".y.axis").transition()
      .call(yAxis);
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants