diff --git a/README.md b/README.md index 6bd38b7..50e5166 100644 --- a/README.md +++ b/README.md @@ -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)); }; }); ``` @@ -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. diff --git a/src/transition/attrTween.js b/src/transition/attrTween.js index 65e4f00..35cfbed 100644 --- a/src/transition/attrTween.js +++ b/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; diff --git a/src/transition/schedule.js b/src/transition/schedule.js index 1d7c104..d86d554 100644 --- a/src/transition/schedule.js +++ b/src/transition/schedule.js @@ -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; } @@ -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. diff --git a/src/transition/style.js b/src/transition/style.js index 0e376c5..7402ce7 100644 --- a/src/transition/style.js +++ b/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; @@ -16,7 +17,7 @@ function styleRemove(name, interpolate) { }; } -function styleRemoveEnd(name) { +function styleRemove(name) { return function() { this.style.removeProperty(name); }; @@ -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); } diff --git a/src/transition/styleTween.js b/src/transition/styleTween.js index c2a8ade..070087d 100644 --- a/src/transition/styleTween.js +++ b/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; diff --git a/test/transition/attr-test.js b/test/transition/attr-test.js index fecd5f9..6af348d 100644 --- a/test/transition/attr-test.js +++ b/test/transition/attr-test.js @@ -289,7 +289,7 @@ 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(); }); @@ -297,7 +297,7 @@ tape("transition.attr(name, value) creates a tween with the name \"attr.name\"", 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(); }); diff --git a/test/transition/style-test.js b/test/transition/style-test.js index ae954b5..4562100 100644 --- a/test/transition/style-test.js +++ b/test/transition/style-test.js @@ -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("

"), + 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, @@ -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"), @@ -201,7 +225,7 @@ 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(); }); @@ -209,7 +233,7 @@ tape("transition.style(name, value) creates a tween with the name \"style.name\" 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(); }); diff --git a/test/transition/tween-test.js b/test/transition/tween-test.js index 1856630..98c1914 100644 --- a/test/transition/tween-test.js +++ b/test/transition/tween-test.js @@ -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)); } });