Skip to content

Commit

Permalink
Various improvements.
Browse files Browse the repository at this point in the history
Interpolators are now reused across elements with transition.{attr,style},
improving performance. This is possible because transitions now invoke tween
functions with this as the current node (like most other functions).

Fix #49 by removing styles at the end of transitions where appropriate.

Fix #89 by removing event listeners where appropriate.
  • Loading branch information
mbostock committed Jan 25, 2019
1 parent 35cb09a commit 5a939e1
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 29 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -307,9 +307,9 @@ For example, to interpolate the fill attribute to blue, like [*transition*.attr]

```js
transition.tween("attr.fill", function() {
var node = this, i = d3.interpolateRgb(node.getAttribute("fill"), "blue");
var i = d3.interpolateRgb(this.getAttribute("fill"), "blue");
return function(t) {
node.setAttribute("fill", i(t));
this.setAttribute("fill", i(t));
};
});
```
Expand Down Expand Up @@ -420,7 +420,7 @@ Immediately after creating a transition, such as by [*selection*.transition](#se

Shortly after creation, either at the end of the current frame or during the next frame, the transition is scheduled. At this point, the delay and `start` event listeners may no longer be changed; attempting to do so throws an error with the message “too late: already scheduled” (or if the transition has ended, “transition not found”).

When the transition subsequently starts, it interrupts the active transition of the same name on the same element, if any, dispatching an `interrupt` event to registered listeners. (Note that interrupts happen on start, not creation, and thus even a zero-delay transition will not immediately interrupt the active transition: the old transition is given a final frame. Use [*selection*.interrupt](#selection_interrupt) to interrupt immediately.) The starting transition also cancels any pending transitions of the same name on the same element that were created before the starting transition. The transition then dispatches a `start` event to registered listeners. This is the last moment at which the transition may be modified: after starting, the transition’s timing, tweens, and listeners may no longer be changed; attempting to do so throws an error with the message “too late: already started” (or if the transition has ended, “transition not found”). The transition initializes its tweens immediately after starting.
When the transition subsequently starts, it interrupts the active transition of the same name on the same element, if any, dispatching an `interrupt` event to registered listeners. (Note that interrupts happen on start, not creation, and thus even a zero-delay transition will not immediately interrupt the active transition: the old transition is given a final frame. Use [*selection*.interrupt](#selection_interrupt) to interrupt immediately.) The starting transition also cancels any pending transitions of the same name on the same element that were created before the starting transition. The transition then dispatches a `start` event to registered listeners. This is the last moment at which the transition may be modified: the transition’s timing, tweens, and listeners may not be changed when it is running; attempting to do so throws an error with the message “too late: already running” (or if the transition has ended, “transition not found”). The transition initializes its tweens immediately after starting.

During the frame the transition starts, but *after* all transitions starting this frame have been started, the transition invokes its tweens for the first time. Batching tween initialization, which typically involves reading from the DOM, improves performance by avoiding interleaved DOM reads and writes.

Expand Down
28 changes: 20 additions & 8 deletions src/transition/attrTween.js
@@ -1,22 +1,34 @@
import {namespace} from "d3-selection";

function attrInterpolate(name, i) {
return function(t) {
this.setAttribute(name, i(t));
};
}

function attrInterpolateNS(fullname, i) {
return function(t) {
this.setAttributeNS(fullname.space, fullname.local, i(t));
};
}

function attrTweenNS(fullname, value) {
var t0, i0;
function tween() {
var node = this, i = value.apply(node, arguments);
return i && function(t) {
node.setAttributeNS(fullname.space, fullname.local, i(t));
};
var i = value.apply(this, arguments);
if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);
return t0;
}
tween._value = value;
return tween;
}

function attrTween(name, value) {
var t0, i0;
function tween() {
var node = this, i = value.apply(node, arguments);
return i && function(t) {
node.setAttribute(name, i(t));
};
var i = value.apply(this, arguments);
if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);
return t0;
}
tween._value = value;
return tween;
Expand Down
4 changes: 2 additions & 2 deletions src/transition/schedule.js
Expand Up @@ -39,7 +39,7 @@ export function init(node, id) {

export function set(node, id) {
var schedule = get(node, id);
if (schedule.state > STARTING) throw new Error("too late; already started");
if (schedule.state > STARTED) throw new Error("too late; already running");
return schedule;
}

Expand Down Expand Up @@ -135,7 +135,7 @@ function create(node, id, self) {
n = tween.length;

while (++i < n) {
tween[i].call(null, t);
tween[i].call(node, t);
}

// Dispatch the end event.
Expand Down
34 changes: 27 additions & 7 deletions src/transition/style.js
@@ -1,9 +1,10 @@
import {interpolateTransformCss as interpolateTransform} from "d3-interpolate";
import {style} from "d3-selection";
import {set} from "./schedule";
import {tweenValue} from "./tween";
import interpolate from "./interpolate";

function styleRemove(name, interpolate) {
function styleNull(name, interpolate) {
var string00,
string10,
interpolate0;
Expand All @@ -16,7 +17,7 @@ function styleRemove(name, interpolate) {
};
}

function styleRemoveEnd(name) {
function styleRemove(name) {
return function() {
this.style.removeProperty(name);
};
Expand Down Expand Up @@ -49,12 +50,31 @@ function styleFunction(name, interpolate, value) {
};
}

function styleMaybeRemove(id, name) {
var on0, on1, listener0, key = "style." + name, event = "end." + key, remove;
return function() {
var schedule = set(this, id),
on = schedule.on,
listener = schedule.value[key] == null ? remove || (remove = styleRemove(name)) : undefined;

// If this node shared a dispatch with the previous node,
// just assign the updated shared dispatch and we’re done!
// Otherwise, copy-on-write.
if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);

schedule.on = on1;
};
}

export default function(name, value, priority) {
var i = (name += "") === "transform" ? interpolateTransform : interpolate;
return value == null ? this
.styleTween(name, styleRemove(name, i))
.on("end.style." + name, styleRemoveEnd(name))
: this.styleTween(name, typeof value === "function"
? styleFunction(name, i, tweenValue(this, "style." + name, value))
: styleConstant(name, i, value), priority);
.styleTween(name, styleNull(name, i))
.on("end.style." + name, styleRemove(name))
: typeof value === "function" ? this
.styleTween(name, styleFunction(name, i, tweenValue(this, "style." + name, value)))
.each(styleMaybeRemove(this._id, name))
: this
.styleTween(name, styleConstant(name, i, value), priority)
.on("end.style." + name, null);
}
14 changes: 10 additions & 4 deletions src/transition/styleTween.js
@@ -1,9 +1,15 @@
function styleInterpolate(name, i, priority) {
return function(t) {
this.style.setProperty(name, i(t), priority);
};
}

function styleTween(name, value, priority) {
var t, i0;
function tween() {
var node = this, i = value.apply(node, arguments);
return i && function(t) {
node.style.setProperty(name, i(t), priority);
};
var i = value.apply(this, arguments);
if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);
return t;
}
tween._value = value;
return tween;
Expand Down
4 changes: 2 additions & 2 deletions test/transition/attr-test.js
Expand Up @@ -289,15 +289,15 @@ tape("transition.attr(name, value) creates an attrTween with the specified name"
var root = jsdom().documentElement,
selection = d3_selection.select(root).attr("fill", "red"),
transition = selection.transition().attr("fill", "blue");
test.equal(transition.attrTween("fill").call(root)(0.5), "rgb(128, 0, 128)");
test.equal(transition.attrTween("fill").call(root).call(root, 0.5), "rgb(128, 0, 128)");
test.end();
});

tape("transition.attr(name, value) creates a tween with the name \"attr.name\"", function(test) {
var root = jsdom().documentElement,
selection = d3_selection.select(root).attr("fill", "red"),
transition = selection.transition().attr("fill", "blue");
transition.tween("attr.fill").call(root)(0.5);
transition.tween("attr.fill").call(root).call(root, 0.5);
test.equal(root.getAttribute("fill"), "rgb(128, 0, 128)");
test.end();
});
28 changes: 26 additions & 2 deletions test/transition/style-test.js
Expand Up @@ -59,6 +59,15 @@ tape("transition.style(name, value) immediately evaluates the specified function
}, 125);
});

tape("transition.style(name, value) recycles tweens ", function(test) {
var document = jsdom("<h1 id='one' style='color:#f0f;'></h1><h1 id='two' style='color:#f0f;'></h1>"),
one = document.querySelector("#one"),
two = document.querySelector("#two"),
transition = d3_selection.selectAll([one, two]).transition().style("color", "red");
test.strictEqual(one.__transition[transition._id].tween, two.__transition[transition._id].tween);
test.end();
});

tape("transition.style(name, value) constructs an interpolator using the current value on start", function(test) {
var root = jsdom().documentElement,
ease = d3_ease.easeCubic,
Expand Down Expand Up @@ -88,6 +97,21 @@ tape("transition.style(name, null) creates an tween which removes the specified
});
});

tape("transition.style(name, null) creates an tween which removes the specified style post-start", function(test) {
var root = jsdom().documentElement,
selection = d3_selection.select(root).style("color", "red"),
transition = selection.transition().style("color", () => null).on("start", started);

function started() {
test.equal(root.style.getPropertyValue("color"), "red");
}

d3_timer.timeout(function(elapsed) {
test.equal(root.style.getPropertyValue("color"), "");
test.end();
});
});

tape("transition.style(name, value) creates an tween which removes the specified style post-start if the specified function returns null", function(test) {
var root = jsdom().documentElement,
selection = d3_selection.select(root).style("color", "red"),
Expand Down Expand Up @@ -201,15 +225,15 @@ tape("transition.style(name, value) creates an styleTween with the specified nam
var root = jsdom().documentElement,
selection = d3_selection.select(root).style("color", "red"),
transition = selection.transition().style("color", "blue");
test.equal(transition.styleTween("color").call(root)(0.5), "rgb(128, 0, 128)");
test.equal(transition.styleTween("color").call(root).call(root, 0.5), "rgb(128, 0, 128)");
test.end();
});

tape("transition.style(name, value) creates a tween with the name \"style.name\"", function(test) {
var root = jsdom().documentElement,
selection = d3_selection.select(root).style("color", "red"),
transition = selection.transition().style("color", "blue");
transition.tween("style.color").call(root)(0.5);
transition.tween("style.color").call(root).call(root, 0.5);
test.equal(root.style.getPropertyValue("color"), "rgb(128, 0, 128)");
test.end();
});
2 changes: 1 addition & 1 deletion test/transition/tween-test.js
Expand Up @@ -46,7 +46,7 @@ tape("transition.tween(name, value) passes the eased time to the interpolator",

function interpolate(t) {
"use strict";
test.equal(this, null);
test.equal(this, root);
test.equal(t, schedule.state === state.ENDING ? 1 : ease((d3_timer.now() - then) / duration));
}
});
Expand Down

0 comments on commit 5a939e1

Please sign in to comment.