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
Comments
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? |
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. |
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 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 |
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 |
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 |
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? |
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. :) |
+1 to that whole last chunk of code and the /cc @rachelbinx, who has been doing a lot of d3 transitioning in MTV projects for the last 4 months. |
A first stab: jasondavies/d3@899f627fb41a159bb85bc03792b8dbd671b50af1.
|
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? |
And, thanks for the speedy implementation! :) |
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. :) |
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. |
Please take a look at #573, which makes it you can inherit transitions within the context of things.each(function(d, i, j) {
var thing = d3.select(this).attr("foo", 0);
d3.transition(thing).attr("foo", 42);
}); If I've modified d3.svg.axis to use Note: we could still introduce a "transitioner" class in the future. I guess we're discussing two different but related needs in this thread. |
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);
}); |
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.
The text was updated successfully, but these errors were encountered: