Skip to content

Commit 5a939e1

Browse files
committed
Various improvements.
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.
1 parent 35cb09a commit 5a939e1

File tree

8 files changed

+91
-29
lines changed

8 files changed

+91
-29
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,9 @@ For example, to interpolate the fill attribute to blue, like [*transition*.attr]
307307

308308
```js
309309
transition.tween("attr.fill", function() {
310-
var node = this, i = d3.interpolateRgb(node.getAttribute("fill"), "blue");
310+
var i = d3.interpolateRgb(this.getAttribute("fill"), "blue");
311311
return function(t) {
312-
node.setAttribute("fill", i(t));
312+
this.setAttribute("fill", i(t));
313313
};
314314
});
315315
```
@@ -420,7 +420,7 @@ Immediately after creating a transition, such as by [*selection*.transition](#se
420420

421421
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”).
422422

423-
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.
423+
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.
424424

425425
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.
426426

src/transition/attrTween.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
11
import {namespace} from "d3-selection";
22

3+
function attrInterpolate(name, i) {
4+
return function(t) {
5+
this.setAttribute(name, i(t));
6+
};
7+
}
8+
9+
function attrInterpolateNS(fullname, i) {
10+
return function(t) {
11+
this.setAttributeNS(fullname.space, fullname.local, i(t));
12+
};
13+
}
14+
315
function attrTweenNS(fullname, value) {
16+
var t0, i0;
417
function tween() {
5-
var node = this, i = value.apply(node, arguments);
6-
return i && function(t) {
7-
node.setAttributeNS(fullname.space, fullname.local, i(t));
8-
};
18+
var i = value.apply(this, arguments);
19+
if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);
20+
return t0;
921
}
1022
tween._value = value;
1123
return tween;
1224
}
1325

1426
function attrTween(name, value) {
27+
var t0, i0;
1528
function tween() {
16-
var node = this, i = value.apply(node, arguments);
17-
return i && function(t) {
18-
node.setAttribute(name, i(t));
19-
};
29+
var i = value.apply(this, arguments);
30+
if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);
31+
return t0;
2032
}
2133
tween._value = value;
2234
return tween;

src/transition/schedule.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function init(node, id) {
3939

4040
export function set(node, id) {
4141
var schedule = get(node, id);
42-
if (schedule.state > STARTING) throw new Error("too late; already started");
42+
if (schedule.state > STARTED) throw new Error("too late; already running");
4343
return schedule;
4444
}
4545

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

137137
while (++i < n) {
138-
tween[i].call(null, t);
138+
tween[i].call(node, t);
139139
}
140140

141141
// Dispatch the end event.

src/transition/style.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {interpolateTransformCss as interpolateTransform} from "d3-interpolate";
22
import {style} from "d3-selection";
3+
import {set} from "./schedule";
34
import {tweenValue} from "./tween";
45
import interpolate from "./interpolate";
56

6-
function styleRemove(name, interpolate) {
7+
function styleNull(name, interpolate) {
78
var string00,
89
string10,
910
interpolate0;
@@ -16,7 +17,7 @@ function styleRemove(name, interpolate) {
1617
};
1718
}
1819

19-
function styleRemoveEnd(name) {
20+
function styleRemove(name) {
2021
return function() {
2122
this.style.removeProperty(name);
2223
};
@@ -49,12 +50,31 @@ function styleFunction(name, interpolate, value) {
4950
};
5051
}
5152

53+
function styleMaybeRemove(id, name) {
54+
var on0, on1, listener0, key = "style." + name, event = "end." + key, remove;
55+
return function() {
56+
var schedule = set(this, id),
57+
on = schedule.on,
58+
listener = schedule.value[key] == null ? remove || (remove = styleRemove(name)) : undefined;
59+
60+
// If this node shared a dispatch with the previous node,
61+
// just assign the updated shared dispatch and we’re done!
62+
// Otherwise, copy-on-write.
63+
if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);
64+
65+
schedule.on = on1;
66+
};
67+
}
68+
5269
export default function(name, value, priority) {
5370
var i = (name += "") === "transform" ? interpolateTransform : interpolate;
5471
return value == null ? this
55-
.styleTween(name, styleRemove(name, i))
56-
.on("end.style." + name, styleRemoveEnd(name))
57-
: this.styleTween(name, typeof value === "function"
58-
? styleFunction(name, i, tweenValue(this, "style." + name, value))
59-
: styleConstant(name, i, value), priority);
72+
.styleTween(name, styleNull(name, i))
73+
.on("end.style." + name, styleRemove(name))
74+
: typeof value === "function" ? this
75+
.styleTween(name, styleFunction(name, i, tweenValue(this, "style." + name, value)))
76+
.each(styleMaybeRemove(this._id, name))
77+
: this
78+
.styleTween(name, styleConstant(name, i, value), priority)
79+
.on("end.style." + name, null);
6080
}

src/transition/styleTween.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
function styleInterpolate(name, i, priority) {
2+
return function(t) {
3+
this.style.setProperty(name, i(t), priority);
4+
};
5+
}
6+
17
function styleTween(name, value, priority) {
8+
var t, i0;
29
function tween() {
3-
var node = this, i = value.apply(node, arguments);
4-
return i && function(t) {
5-
node.style.setProperty(name, i(t), priority);
6-
};
10+
var i = value.apply(this, arguments);
11+
if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);
12+
return t;
713
}
814
tween._value = value;
915
return tween;

test/transition/attr-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,15 @@ tape("transition.attr(name, value) creates an attrTween with the specified name"
289289
var root = jsdom().documentElement,
290290
selection = d3_selection.select(root).attr("fill", "red"),
291291
transition = selection.transition().attr("fill", "blue");
292-
test.equal(transition.attrTween("fill").call(root)(0.5), "rgb(128, 0, 128)");
292+
test.equal(transition.attrTween("fill").call(root).call(root, 0.5), "rgb(128, 0, 128)");
293293
test.end();
294294
});
295295

296296
tape("transition.attr(name, value) creates a tween with the name \"attr.name\"", function(test) {
297297
var root = jsdom().documentElement,
298298
selection = d3_selection.select(root).attr("fill", "red"),
299299
transition = selection.transition().attr("fill", "blue");
300-
transition.tween("attr.fill").call(root)(0.5);
300+
transition.tween("attr.fill").call(root).call(root, 0.5);
301301
test.equal(root.getAttribute("fill"), "rgb(128, 0, 128)");
302302
test.end();
303303
});

test/transition/style-test.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ tape("transition.style(name, value) immediately evaluates the specified function
5959
}, 125);
6060
});
6161

62+
tape("transition.style(name, value) recycles tweens ", function(test) {
63+
var document = jsdom("<h1 id='one' style='color:#f0f;'></h1><h1 id='two' style='color:#f0f;'></h1>"),
64+
one = document.querySelector("#one"),
65+
two = document.querySelector("#two"),
66+
transition = d3_selection.selectAll([one, two]).transition().style("color", "red");
67+
test.strictEqual(one.__transition[transition._id].tween, two.__transition[transition._id].tween);
68+
test.end();
69+
});
70+
6271
tape("transition.style(name, value) constructs an interpolator using the current value on start", function(test) {
6372
var root = jsdom().documentElement,
6473
ease = d3_ease.easeCubic,
@@ -88,6 +97,21 @@ tape("transition.style(name, null) creates an tween which removes the specified
8897
});
8998
});
9099

100+
tape("transition.style(name, null) creates an tween which removes the specified style post-start", function(test) {
101+
var root = jsdom().documentElement,
102+
selection = d3_selection.select(root).style("color", "red"),
103+
transition = selection.transition().style("color", () => null).on("start", started);
104+
105+
function started() {
106+
test.equal(root.style.getPropertyValue("color"), "red");
107+
}
108+
109+
d3_timer.timeout(function(elapsed) {
110+
test.equal(root.style.getPropertyValue("color"), "");
111+
test.end();
112+
});
113+
});
114+
91115
tape("transition.style(name, value) creates an tween which removes the specified style post-start if the specified function returns null", function(test) {
92116
var root = jsdom().documentElement,
93117
selection = d3_selection.select(root).style("color", "red"),
@@ -201,15 +225,15 @@ tape("transition.style(name, value) creates an styleTween with the specified nam
201225
var root = jsdom().documentElement,
202226
selection = d3_selection.select(root).style("color", "red"),
203227
transition = selection.transition().style("color", "blue");
204-
test.equal(transition.styleTween("color").call(root)(0.5), "rgb(128, 0, 128)");
228+
test.equal(transition.styleTween("color").call(root).call(root, 0.5), "rgb(128, 0, 128)");
205229
test.end();
206230
});
207231

208232
tape("transition.style(name, value) creates a tween with the name \"style.name\"", function(test) {
209233
var root = jsdom().documentElement,
210234
selection = d3_selection.select(root).style("color", "red"),
211235
transition = selection.transition().style("color", "blue");
212-
transition.tween("style.color").call(root)(0.5);
236+
transition.tween("style.color").call(root).call(root, 0.5);
213237
test.equal(root.style.getPropertyValue("color"), "rgb(128, 0, 128)");
214238
test.end();
215239
});

test/transition/tween-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ tape("transition.tween(name, value) passes the eased time to the interpolator",
4646

4747
function interpolate(t) {
4848
"use strict";
49-
test.equal(this, null);
49+
test.equal(this, root);
5050
test.equal(t, schedule.state === state.ENDING ? 1 : ease((d3_timer.now() - then) / duration));
5151
}
5252
});

0 commit comments

Comments
 (0)