Giter Club home page Giter Club logo

d3-transition's Introduction

d3-transition

A transition is a selection-like interface for animating changes to the DOM. Instead of applying changes instantaneously, transitions smoothly interpolate the DOM from its current state to the desired target state over a given duration.

Resources

d3-transition's People

Contributors

curran avatar dependabot[bot] avatar fil avatar inokawa avatar mbostock avatar sghall avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

d3-transition's Issues

Tests!

  • selection.transition
  • selection.interrupt
  • d3.transition
  • transition.select
  • transition.selectAll
  • transition.filter
  • transition.merge
  • transition.selection
  • d3.active
  • transition.attr
  • transition.attrTween
  • transition.style
  • transition.styleTween
  • transition.text
  • transition.remove
  • transition.tween
  • transition.delay
  • transition.duration
  • transition.ease
  • transition.on
  • transition.each
  • transition.call
  • transition.empty
  • transition.nodes
  • transition.node
  • transition.size

More rigorous type checking?

We should consider checking for some invalid types earlier, if possible, rather than deferring errors until the transition starts.

  • transition.attrTween’s value should be a function that returns a function (interpolator)
  • transition.styleTween’s value should be a function that returns a function (interpolator)
  • transtion.tween’s value should be a function that returns a function
  • transition.on’s listener should be a function
  • transition.ease’s value should be a function

Disallow modifications to transitions after scheduling?

For example, what happens if you call transition.on("end", listener) after the transition ends? Currently it throws an exception when it tries to access schedule.dispatch because the schedule is undefined.

Similar bad stuff can happen if you call transition.tween(name, tween) after the first tick: the scheduler assumes that the tweens array contains and already-initialized tweens, so adding a new uninitialized tween will crash (tween.call is not a function).

I recall some cases where it was convenient to defer setting the transition duration until the transition starts—say if you want to compute the duration automatically when zooming the viewport. But probably we should ignore any modifications to the tweens and timing after the transition starts. It should always be valid to add or remove event listeners, though if you try to listen for the end event after the transition ends, obviously you won’t ever receive an event.

Rewrite with fewer closures.

For performance, we should experiment with rewriting schedule.js, styleTween.js and attrTween.js to not use closures. I think it should be possible without changing the public API…

Removing tweens?

Seems like the following should be valid for removing tweens:

  • transition.attrTween(name, null)
  • transition.styleTween(name, null)
  • transition.tween(name, null)

selection.transition(transitionInstance) doesn't inherit timing properties

From the selection.transition([name]) docs:

If the name is a transition instance, the returned transition has the same id and name as the specified transition. If a transition with the same id already exists on a selected element, the existing transition is returned for that element. Otherwise, the timing of the returned transition is inherited from the existing transition of the same id on the nearest ancestor of each selected element.

And the example given:

var t = d3.transition()
    .duration(750)
    .ease(d3.easeLinear);

d3.selectAll(".apple").transition(t)
    .style("fill", "red");

d3.selectAll(".orange").transition(t)
    .style("fill", "orange");

My understanding is that the transitions applied to ".orange" and ".apple" elements should inherit the 750ms duration and linear easing from transition t. However, they appear use the d3 default transition properties instead.

See this Fiddle for example.

Am I misunderstanding how to use transition inheritance, or are the docs incorrect?

transition.selection?

We have selection.transition; it’d be nice to have the reverse, transition.selection.

Looks like it will require exposing the Selection constructor for use in d3-transition, though?

Reuse transition tweens & interpolators in common case.

When creating interpolators in transition.style and transition.attr, we could check if the start and end values are the same as the previous node, and if so, reuse the previous interpolator. This is a very simple heuristic that would optimize the common case of selecting multiple nodes that share the same start & end values.

Likewise, when creating a tween in transition.styleTween and transition.attrTween, we could check if the current interpolator is the same instance as the previous interpolator, and if so reuse the previous tween.

Implement transitions.

  • d3.transition
  • selection.transition
  • selection.interrupt
  • transition.select
  • transition.selectAll
  • transition.filter
  • transition.transition
  • transition.call
  • transition.nodes
  • transition.node
  • transition.size
  • transition.empty
  • transition.each
  • transition.on
  • transition.attr
  • transition.attrTween
  • transition.style
  • transition.styleTween
  • transition.text
  • transition.remove
  • transition.tween
  • transition.delay
  • transition.duration
  • transition.ease

Copy-on-write for transition dispatches.

Most of the time the dispatches are the same for all nodes in a selection, so it’s a shame that we create a new one for each node. I bet there’s a simple way to use copy-on-write so that in the common case selected nodes can share a single dispatch object. For example, iterating over the nodes, and if the next node’s dispatch is the same as the previous node’s, then we already updated the dispatch and we don’t need to do anything.

Transition behavior is undefined if an error is thrown.

Value function passed to .tween will be called in infinite loop if the last call throws exception.

Code to reproduce:

d3.select("body")
    .transition().duration(1000)
    .tween("crash", function () {
        return function (t) {
            if (t == 0.5) {
                throw "this is fine";
            }

            if (t == 1) {
                throw "infinite loop";
            }
        };
    });

Document transition lifecycle.

  1. Transition is scheduled, and the lock is initialized.
  2. After the specified delay, which must be at least 1 tick, the start event is dispatched.
  3. Tweens are initialized.
  4. After other active timers for this tick are invoked, the newly-initialized tweens are invoked.
  5. After each tick, tweens are invoked until the specified duration is exceeded.
  6. The end event is dispatched.
  7. The lock is released.

Like, the invocation of other timers between 3 and 4 might be surprising: it’s an optimization such that reads from the DOM to initialize tweens happen in one pass, and then writes to the DOM on first tween invocation happen in the second pass, rather than interleaving reads and writes. (See d3/d3#1576.) We obviously don’t want to wait two ticks before starting the transition, so we push the first invocation to the end of the current tick.

Also, the name active on the transition lock is a little confusing, because it can refer to a transition that previously ended successfully before another scheduled transition starts.

Also, we need to document that within a given tick (and given the above exception), transitions are called back in the order they were scheduled (or, by id? is that the same?).

Related d3/d3#1336.

Deferred first tick may cause priority inversion.

Related d3/d3#1576.

Consider the looped transition from bl.ocks.org/7df5eb012ff76589419d:

selection.transition()
    .duration(2500)
    .delay(function(d) { return d * 40; })
    .each("start", slideRight);

function slideRight() {
  d3.activeTransition(this)
      .attr("cx", width)
    .transition()
      .each("start", slideLeft);
}

function slideLeft() {
  d3.activeTransition(this)
      .attr("cx", 0)
    .transition()
      .each("start", slideRight);
}

Say the page is moved to a background tab during a slide-right transition, which we will call transition A. If the page is returned to the foreground after five or more seconds (any delay greater than twice the transition duration), then transition A immediately ends, setting all the circles to the far right in its last tick. During the same frame, since it was previously scheduled at the start of transition A, a slide-left transition B then starts. A new slide-right transition C is also scheduled to follow B.

The problem is that when the slide-left transition B starts, its first (and only) tick—when it would set the circles to the left—is deferred to the end of the current frame due to the optimization in d3/d3#1576. Thus, the slide-right transition C which was intended to run after B instead starts and initializes its tweens before transition B gets its only tick. And thus the slide-right transition C may be a no-op:

screen shot 2015-11-13 at 10 06 46 am

Note that if we change the code ever-so-slightly to use end events rather than start events, the inversion does not occur:

select.transition()
    .duration(2500)
    .delay(function(d) { return d * 40; })
    .attr("cx", width)
    .each("end", slideLeft);

function slideLeft() {
  d3.activeTransition(this).transition()
      .attr("cx", 0)
      .each("end", slideRight);
}

function slideRight() {
  d3.activeTransition(this).transition()
      .attr("cx", width)
      .each("end", slideLeft);
}

In this case, when the page is paused and resumed, the slide-right transition A immediately ends, setting all the circles to the far right. Then the slide-left transition B is scheduled and starts at the end of the current frame. When the slide-left transition B starts, it initializes its tweens and schedules its first tick again at the end of the current frame. The tick is then invoked, setting the circles to the far left, and transition B dispatches an end event. Then, a new slide-right transition C is scheduled and starts, again at the end of the current frame. Thus, the relative ordering is correctly preserved.

Expose d3.timerReplace(callback, delay, then).

Rather than exposing the active timer, it seems cleaner to expose a timerReplace method that redefines the active timer. Then we could reasonably extract a separate d3-timer module.

Note: d3_timer_replace was removed in d3/d3@b093c2d, but I think that was just a micro-optimization.

Use d3.event.timeStamp instead of Date.now?

That way, if multiple transitions are started independently during the response to an event, they are guaranteed to be concurrent.

However, we’d have to fallback to Date.now if d3.event is undefined. And we’d probably want to use the sourceEvent rather than d3.event. Although, we haven’t implemented any behaviors yet (drag, zoom, brush), so currently there is no difference.

An API for interrupting all descendant elements?

This is a little awkward:

selection
    .interrupt()
  .selectAll("*")
    .interrupt();

I wonder if I should change selection.interrupt to do this by default (probably a bad idea) or we need a new method, say, selection.interruptAll (although that name seems to imply it affects all transitions, and not all descendants, which is not great)?

transition.cancel?

We have a way of interrupting and cancelling all transitions on a given selection, but what about just cancelling a single transition? It would just stop the timer and remove it from the pending list for the selected elements. (And, if the transition were active, maybe it would interrupt?)

Transitions should use computed value for end, too?

When transition 'left' or 'right' style from negative % to positive % sets wrong values. When do the same for 'top' or 'bottom' it sets she same value all the animation frames.

Code to reproduce:

<div style="position: absolute"></div>
var div = d3.select("div");
div
    .style("left", "-50%")
    .transition().duration(1000)
    .style("left", "50%")
    .tween("log", function () {
        return function (t) {
            console.log(div.style("left")); // will be something like -5470.53px to 523px
            console.log(div.node().style.left); // will be something like -522.996% to 50%
        };
    });

I used https://d3js.org/d3.v4.js for testing.

transition.append?

It could create a placeholder node object that is materialized and appended to the parent when the transition starts, a bit like enter nodes. Or even simpler, it creates the DOM node immediately, but doesn’t append it to the DOM until the transition starts. That might perform well enough…

(If we use placeholder objects rather than elements, we’d need to make sure chained transition.append worked, of course, and define the behavior if a child transition starts before the parent transition. In that case, the child nodes are probably off-screen?)

selection.transition(transition) only considers the first node’s timing.

It’s tempting to replace code like this in 3.x:

d3.selectAll("p")
  .transition()
    .delay(function(d, i) { return i * 20; })
    .duration(750)
    .each(update);

function update() {
  d3.select(this)
    .append("div")
      .text("Hello")
    .transition()
      .style("color", "red");
}

With this in 4.0:

var selection = d3.selectAll("p");

var transition = selection.transition()
    .delay(function(d, i) { return i * 20; })
    .duration(750);

selection.append("div")
    .text("Hello")
  .transition(transition)
    .style("color", "red");

However, the replacement doesn’t work as intended because selection.transition(transition) creates a new transition that inherits timing parameters from the transition’s first node, not from the corresponding parent p of the appended div!

Instead you need to say:

var selection = d3.selectAll("p");

var transition = selection.transition()
    .delay(function(d, i) { return i * 250; })
    .duration(750);

selection.append("div")
    .text("Hello")
    .each(function() {
      var parent = d3.select(this.parentNode).transition(transition);
      d3.select(this).transition(parent).style("color", "red");
    });

This seems pretty bogus.

Inspect transition state after start? And end?

Related #24, when transitions start, currently the transitions are initialized in-place, so there’s no way to retrieve the set tween after the transition starts. There’s some risky code when retrieving tweens that checks tween.name against the request name—but it’d be possible for the tween initializer to return a named function, and then have return the initialized value! For example:

var t = d3.transition().tween("foo", function() {
  function foo() {}; // tween.name = "foo"
  foo.value = 42; // tween.value
  return foo;
});
console.log(t.tween("foo")); // function() { function foo() {}; foo.value = 42; return foo; }
setTimeout(function() { console.log(t.tween("foo")); }, 50); // 42

So, oops!

Moreover, after a transition ends (or is interrupted or cancelled), the transition state is deleted from the DOM. Maybe it’s fine to say that transitions are destructive and just fix the above bug to return null, but would it be useful to be able to inspect the transition state after the transition ends? In order to do that, the transition object itself would need to retain the schedule for each node, rather than only storing it in the DOM. Which it could probably do as transition._schedules, or something.

Remove magic transition.each inheritance.

I.e., d3_transitionInheritId. Part of this involves removing d3.transition(selection[, name]). This method is too different from d3.transition([name]), the much-more frequent form.

Like d3.maybeTransition(selection[, name])? It might be better to expose the context transition within transition.each.

Any pending transition should cancel remove, not just same-named.

It’s currently implemented as:

function remove() {
  var parent = this.parentNode;
  if (parent && !this[key].pending.length) parent.removeChild(this);
}

But what if this transition has a name? It seems like the removal should be cancelled if there is any pending transition on the element, not just if there is a pending transition of the same name.

More bad ideas?

  • transition.text(value) - does interpolation, rather than setting on start
  • transition.html(value) - like transition.text, but for inner HTML
  • transition.property(name, value) - interpolate arbitrary values (e.g., for canvas)
  • transition.propertyTween(name, value) - interpolate arbitrary values (e.g., for canvas)
  • transition.styleInterpolate(name, interpolator) - like styleTween, when shared by all nodes
  • transition.attrInterpolate(name, interpolator) - like attrTween, when shared by all nodes

Reselect a transition.

There should be a way to “reselect” a transition. #15 covers the case of selecting the active transition on a given node via d3.activeTransition(node), but I wonder if there’s a more general solution. This also feels related to #4, in that I want to remove the magic inheritance that happens inside transition.each(callback) in D3 3.x.

Essentially, I want the equivalent of this pattern, but for transitions instead of selections:

selection.each(function(d, i) {
  d3.select(this)
});

I suppose one solution would be to retain some magic inheritance in transition.each such that you could say this:

transition.each(function(d, i) {
  d3.transit(this) // returns transition, but isolated to this node
});

Alternatively, maybe you explicitly reference the transition you want to reselect, and then we don’t need any magic inheritance. Like, maybe selection.transition takes a transition, and returns a transition for the selected nodes with the given transition’s key and id?

transition.each(function(d, i) {
  d3.select(this).transition(transition)
});

Or if we just want to support the single-node case:

transition.each(function(d, i) {
  transition.transit(this)
});

Or if we invert the relationship:

transition.each(function(d, i) {
  transition.reselect(d3.select(this))
});

Still feels a bit weird.

Transitions in the past… should be discouraged.

Say you do something like this:

function start(d, i) {
  var t = d3.select(this).transition()
      .delay(i * 1)
      .on("start", repeat);

  function repeat() {
    t = t
        .style("background-color", "black")
      .transition()
        .style("background-color", "red")
        .on("start", repeat);
  }
}

If the page gets backgrounded, it basically goes into an infinite loop when the page returns to the foreground because the transitions are infinitely chained: it’s creating transitions in the past, trying to get back up to the present, but it tries to replay everything rather than jumping instantly to now. We probably should have a safety check that prevents a transition from being scheduled if it’s already done? Or what? What should we do in this case?

Remove out-in mode.

We should only support in, out, and in-out. The out-in mode doesn’t make sense (assuming that #1 is fixed).

interrupt should cancel all scheduled transitions, too.

The recommended pattern for interrupting any active transition and canceling any schedule transition is:

selection
    .interrupt()
    .transition();

However, note that this does not have the desired effect if the transition that is used to cancel scheduled transition is subsequently modified via transition.each:

selection
    .interrupt()
    .transition()
    .each(function() { d3.select(this).transition().delay(750); }); // oops!

Possibly, selection.interrupt should automatically cancel any scheduled transitions (a non-backwards-compatible change). I can’t think of many cases where you don’t want to cancel scheduled transitions when interrupting, but given that I didn’t already implement it this way I wonder if I’m overlooking something, or if the current behavior was simply done because it was the easiest to implement and I thought that scheduling a no-op transition was sufficient (and also common).

Alternatively, we could introduce a new selection.cancel method. Would this only clear scheduled but not-yet-active transitions, or would it also interrupt the active transition?

Dispatch events using selection.dispatch?

Rather than listening to the transition object, it would make a lot of sense to use selection.on to listen for transition events, since they happen on a per-element basis anyway. So instead of this:

selection.transition()
    .on("start", function() { console.log("started!"); })
    .style("color", "red");

Something like this:

selection
    .on("d3:transitionstart", function() { console.log("started!"); })
  .transition()
    .style("color", "red");

Or maybe if transition.on is an alias for selection.on:

selection.transition()
    .on("d3:transitionstart", function() { console.log("started!"); })
    .style("color", "red");

This would allow us to remove transition.on and the related machinery, which is great. It might add a little bit of overhead since we’d be dispatching a CustomEvent on every start, end and interrupt. But maybe it’s negligible?

Another potential downside is that you can only (easily) listen to all transition events on a given element, rather than listening to transition events for a particular transition.

This is related to ongoing work in d3-drag, where I need to decide between listening to the drag behavior for events, or having the drag behavior dispatch (custom) drag events using selection.dispatch.

Schedule a following transition.

Related d3/d3#2423.

If there were an easy way to say “schedule a transition after the currently-active transition, if any”, it would be easier to create looped transitions. Currently, you have to remember this syntax:

selection.transition()
    .duration(2500)
    .delay(function(d) { return d * 40; })
    .each(slide);

function slide() {
  var circle = d3.select(this);
  (function repeat() {
    circle = circle.transition()
        .attr("cx", width)
      .transition()
        .attr("cx", 0)
        .each("end", repeat);
  })();
}

An alternative:

selection.transition()
    .duration(2500)
    .delay(function(d) { return d * 40; })
    .each("start", slide);

function slide() {
  d3.select(this).activeTransition()
      .attr("cx", width)
    .transition()
      .attr("cx", 0)
    .transition()
      .each("start", slide);
}

Or, at the expense of repeating the tweens in the first transition:

selection.transition()
    .duration(2500)
    .delay(function(d) { return d * 40; })
    .attr("cx", width)
    .each("end", repeat);

function repeat() {
  d3.select(this).transitionAfter()
      .attr("cx", 0)
    .transition()
      .attr("cx", width)
      .each("end", repeat);
}

Transition must evaluate new values immediately.

Currently transition.{attr,style,text,…} are built on transition.tween, and they don’t evaluate the new value until the tween is initialized. That’s bad; many examples (such as this one) rely on synchronous evaluation of transition.attr and the like, so that external state can be modified during the initialization of a transition.

I’m not suggesting we change transition.{attrTween,styleTween,tween,…}: those can’t be evaluated until the transition starts, by design. By the transition methods that only compute the destination value must be evaluated earlier.

Rename transition.each(type, listener).

Seems like transition.on(type, listener) would be a better name, and avoid the confusion with transition.each(listener). I suppose there’s a risk that it would look like you’re trying to transition listeners on selected elements, but that’s obviously not the sort of thing that could be transitioned.

style(name, null) should tween to default value.

Related d3/d3#2396.

Rather than removing the inline style at the start of the transition, it should remove the inline style, compute the new (cascaded) value, and then tween to that value. Then the style should be removed at the transition end.

Decide on how we’re handling errors.

Places where errors could be thrown:

  • dispatching an interrupt event
  • dispatching a start event
  • initializing a tween
  • easing time
  • invoking a tween
  • dispatching an end event

Related: in d3-timer, if any timer throws an error, no more timers execute for that frame.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.