diff --git a/README.md b/README.md
index bad872d..4b81a22 100644
--- a/README.md
+++ b/README.md
@@ -366,6 +366,27 @@ Specifies a factory for the transition [easing function](https://github.com/d3/d
### Control Flow
+The [paused](#transition_paused) and [progress](#transition_progress) of a transition is configurable in runtime.
+
+# transition.paused([value]) [<>](https://github.com/d3/d3-transition/blob/master/src/transition/paused.js "Source")
+
+To pause the transition animation, set the transition paused to `true`, or `false` to resume. The *value* may be specified either as a constant or a function.
+
+```js
+transition.paused(true);
+```
+
+If a *value* is not specified, returns the current value of the paused for the first (non-null) element in the transition. This is generally useful only if you know that the transition contains exactly one element.
+
+
+# transition.progress([value]) [<>](https://github.com/d3/d3-transition/blob/master/src/transition/progress.js "Source")
+
+The progress is a value between 0 (begin) to 1 (end). You can set or get the progress of the transition at any time.
+
+```js
+transition.progress(0.5);
+```
+
For advanced usage, transitions provide methods for custom control flow.
# transition.end() ยท [Source](https://github.com/d3/d3-transition/blob/master/src/transition/end.js)
@@ -377,6 +398,7 @@ Returns a promise that resolves when every selected element finishes transitioni
Adds or removes a *listener* to each selected element for the specified event *typenames*. The *typenames* is one of the following string event types:
* `start` - when the transition starts.
+* `progress` - notify when the transition progresses.
* `end` - when the transition ends.
* `interrupt` - when the transition is interrupted.
* `cancel` - when the transition is cancelled.
diff --git a/src/transition/index.js b/src/transition/index.js
index 355be71..0fb4fcb 100644
--- a/src/transition/index.js
+++ b/src/transition/index.js
@@ -19,6 +19,8 @@ import transition_textTween from "./textTween.js";
import transition_transition from "./transition.js";
import transition_tween from "./tween.js";
import transition_end from "./end.js";
+import transition_paused from "./paused.js";
+import transition_progress from "./progress.js";
var id = 0;
@@ -64,6 +66,8 @@ Transition.prototype = transition.prototype = {
tween: transition_tween,
delay: transition_delay,
duration: transition_duration,
+ paused: transition_paused,
+ progress: transition_progress,
ease: transition_ease,
easeVarying: transition_easeVarying,
end: transition_end,
diff --git a/src/transition/paused.js b/src/transition/paused.js
new file mode 100644
index 0000000..e4040ed
--- /dev/null
+++ b/src/transition/paused.js
@@ -0,0 +1,23 @@
+import {get} from "./schedule.js";
+
+function pausedFunction(id, value) {
+ return function() {
+ get(this, id).paused = Boolean(value.apply(this, arguments));
+ };
+}
+
+function pausedConstant(id, value) {
+ return value = Boolean(value), function() {
+ get(this, id).paused = value;
+ };
+}
+
+export default function(value) {
+ var id = this._id;
+
+ return arguments.length
+ ? this.each((typeof value === "function"
+ ? pausedFunction
+ : pausedConstant)(id, value))
+ : get(this.node(), id).paused;
+}
diff --git a/src/transition/progress.js b/src/transition/progress.js
new file mode 100644
index 0000000..4c3afbf
--- /dev/null
+++ b/src/transition/progress.js
@@ -0,0 +1,28 @@
+import {get} from "./schedule.js";
+
+function progressFunction(id, value) {
+ return function() {
+ get(this, id).progress = +value.apply(this, arguments);
+ };
+}
+
+function progressConstant(id, value) {
+ return value = +value, function() {
+ get(this, id).progress = value;
+ };
+}
+
+function abs(value) {
+ if (value < 0) value = -value;
+ return value;
+}
+
+export default function(value) {
+ var id = this._id;
+
+ return arguments.length
+ ? this.each((typeof value === "function"
+ ? progressFunction
+ : progressConstant)(id, value))
+ : abs(get(this.node(), id).progress);
+}
diff --git a/src/transition/schedule.js b/src/transition/schedule.js
index f4e88d7..290de8a 100644
--- a/src/transition/schedule.js
+++ b/src/transition/schedule.js
@@ -1,7 +1,7 @@
import {dispatch} from "d3-dispatch";
import {timer, timeout} from "d3-timer";
-var emptyOn = dispatch("start", "end", "cancel", "interrupt");
+var emptyOn = dispatch("start", "end", "cancel", "interrupt", "progress");
var emptyTween = [];
export var CREATED = 0;
@@ -127,7 +127,35 @@ function create(node, id, self) {
tween.length = j + 1;
}
+ function getProgress(elapsed) {
+ if (self.paused) {
+ if (self.progress >= 0) {
+ elapsed = self._lastprogress !== self.progress ? self.progress * self.duration : -1;
+ } else {
+ self.progress = elapsed / self.duration;
+ }
+ if (self._lastprogress !== self.progress) {
+ self.on.call("progress", node, node.__data__, self.index, self.group, self.progress);
+ self._lastprogress = self.progress;
+ }
+ } else if (self.progress >= 0) {
+ elapsed = elapsed - (self.progress * self.duration);
+ self.timer.restart(tick, 0, self.time + elapsed);
+ elapsed = self.progress = - (self.progress + 1e-10);
+ } else {
+ if (elapsed >= self.duration) {
+ self.progress = -1;
+ } else {
+ self.progress = - (elapsed / self.duration);
+ }
+ self.on.call("progress", node, node.__data__, self.index, self.group, -self.progress);
+ }
+ return elapsed;
+ }
+
function tick(elapsed) {
+ elapsed = getProgress(elapsed);
+ if (elapsed < 0) return;
var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1),
i = -1,
n = tween.length;
diff --git a/test/transition/paused-test.js b/test/transition/paused-test.js
new file mode 100644
index 0000000..d419ea5
--- /dev/null
+++ b/test/transition/paused-test.js
@@ -0,0 +1,199 @@
+var tape = require("tape"),
+ jsdom = require("../jsdom"),
+ d3_ease = require("d3-ease"),
+ d3_timer = require("d3-timer"),
+ d3_interpolate = require("d3-interpolate"),
+ d3_selection = require("d3-selection");
+
+require("../../");
+
+tape("transition.paused(true) allows pause the transition animation", function(test) {
+ var root = jsdom().documentElement,
+ ease = d3_ease.easeCubic,
+ duration = 100,
+ interpolate = d3_interpolate.interpolateNumber(0, 100),
+ selection = d3_selection.select(root).attr("t", 0),
+ transition = selection.transition().duration(duration).attr("t", 100).on("end", ended);
+ var beginTime = d3_timer.now();
+
+ d3_timer.timeout(function(elapsed) {
+ transition.paused(true);
+ test.strictEqual(root.__transition[transition._id].paused, true);
+ test.strictEqual(transition.paused(), true);
+ test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(elapsed / duration)));
+ }, 50);
+
+ d3_timer.timeout(function(elapsed) {
+ var progress = root.__transition[transition._id].progress;
+ test.strictEqual(transition.progress(), progress);
+ test.ok(progress >= 0.5);
+ test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(progress)));
+ transition.paused(false);
+ test.strictEqual(root.__transition[transition._id].paused, false);
+ test.strictEqual(transition.paused(), false);
+ }, 150);
+
+ function ended() {
+ var t = d3_timer.now() - beginTime;
+ test.ok(t > 150);
+ test.end();
+ }
+});
+
+tape("transition.progress() allows to get the progress of the transition animation", function(test) {
+ var root = jsdom().documentElement,
+ ease = d3_ease.easeCubic,
+ duration = 100,
+ interpolate = d3_interpolate.interpolateNumber(0, 100),
+ selection = d3_selection.select(root).attr("t", 0),
+ transition = selection.transition().duration(duration).attr("t", 100).on("end", ended);
+ var beginTime = d3_timer.now();
+ var oldProgress;
+
+ d3_timer.timeout(function(elapsed) {
+ // get the progress on runtime
+ var progress = -root.__transition[transition._id].progress;
+ test.strictEqual(transition.progress(), progress);
+ test.ok(progress >= 0.5);
+ test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(progress)));
+ transition.paused(true);
+ oldProgress = progress;
+ transition.progress(progress);
+ }, 50);
+
+ d3_timer.timeout(function(elapsed) {
+ var progress = root.__transition[transition._id].progress;
+ test.strictEqual(transition.progress(), progress);
+ test.strictEqual(oldProgress, progress);
+ test.ok(progress >= 0.5);
+ test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(progress)));
+ transition.paused(false);
+ }, 150);
+
+ function ended() {
+ var t = d3_timer.now() - beginTime;
+ test.ok(t > 150);
+ test.strictEqual(transition.progress(), 1);
+ test.end();
+ }
+});
+
+tape("transition.on(\"progress\", listener) event to notify animation progress", function(test) {
+ var root = jsdom().documentElement,
+ duration = 100,
+ selection = d3_selection.select(root).attr("t", 0),
+ transition = selection.transition().duration(duration).attr("t", 100)
+ .on("progress", onProgress)
+ .on("end", ended);
+ var beginTime = d3_timer.now();
+ var oldProgress;
+ var lastProgress = 0;
+
+ d3_timer.timeout(function(elapsed) {
+ // get the progress on runtime
+ var progress = -root.__transition[transition._id].progress;
+ test.strictEqual(transition.progress(), progress);
+ test.strictEqual(lastProgress, progress);
+ test.ok(progress >= 0.5);
+ transition.paused(true);
+ oldProgress = progress;
+ transition.progress(progress);
+ }, 50);
+
+ d3_timer.timeout(function(elapsed) {
+ var progress = root.__transition[transition._id].progress;
+ test.strictEqual(transition.progress(), progress);
+ test.strictEqual(oldProgress, progress);
+ test.strictEqual(lastProgress, progress);
+ test.ok(progress >= 0.5);
+ transition.paused(false);
+ }, 150);
+
+ function onProgress(data, index, grp, progress) {
+ test.ok(progress >= lastProgress, `${progress} >= ${lastProgress}`);
+ lastProgress = progress;
+ }
+
+ function ended() {
+ var t = d3_timer.now() - beginTime;
+ test.ok(t > 150);
+ test.strictEqual(transition.progress(), 1);
+ test.strictEqual(lastProgress, 1);
+ test.end();
+ }
+});
+
+tape("transition.on(\"progress\", listener) event should work on paused status", function(test) {
+ var root = jsdom().documentElement,
+ duration = 100,
+ selection = d3_selection.select(root).attr("t", 0),
+ transition = selection.transition().duration(duration).attr("t", 100).paused(true)
+ .on("progress", onProgress)
+ .on("end", ended);
+ var beginTime = d3_timer.now();
+ var progresses = [];
+
+ d3_timer.timeout(function(elapsed) {
+ test.ok(progresses.length);
+ test.strictEqual(transition.progress(), progresses[0]);
+ transition.progress(0.2);
+ }, 50);
+
+ d3_timer.timeout(function(elapsed) {
+ test.ok(progresses.length === 2, `progresses.length(${progresses.length}) === 2`);
+ test.strictEqual(transition.progress(), progresses[1]);
+ transition.progress(1);
+ }, 100);
+
+ function onProgress(data, index, grp, progress) {
+ progresses.push(progress);
+ }
+
+ function ended() {
+ var t = d3_timer.now() - beginTime;
+ test.ok(t >= 100);
+ test.ok(t < 150);
+ test.ok(progresses.length === 3, `progresses.length(${progresses.length}) === 3`);
+ test.strictEqual(progresses[2], 1);
+ test.strictEqual(transition.progress(), 1);
+ test.end();
+ }
+});
+
+tape("transition.progress(true) should pause the animation", function(test) {
+ var root = jsdom().documentElement,
+ duration = 2000,
+ selection = d3_selection.select(root).attr("t", 0),
+ transition = selection.transition().duration(duration).attr("t", 100)
+ .on("progress", onProgress)
+ .on("end", ended);
+ var needCheck = 0;
+ var lastProgress;
+ var lastProgress2;
+
+ d3_timer.timeout(function(elapsed) {
+ transition.paused(true);
+ lastProgress = transition.progress();
+ test.ok(lastProgress >= 0.25, lastProgress + " progress should >= 0.25");
+ test.ok(lastProgress <= 0.35, lastProgress + " progress should <= 0.35");
+ }, 600);
+
+ d3_timer.timeout(function(elapsed) {
+ needCheck = 1;
+ transition.paused(false);
+ }, 2100);
+
+ function onProgress(data, index, grp, progress) {
+ if (needCheck && needCheck <=2) {
+ if (needCheck === 2) lastProgress2 = progress;
+ test.ok(progress - lastProgress <= 0.1, "(progress - lastProgress) should <= 0.1 ");
+ needCheck++;
+ }
+ }
+
+ function ended() {
+ test.ok(typeof lastProgress2 === 'number', 'already checked');
+ test.strictEqual(transition.progress(), 1, 'progress should be end');
+ test.end();
+ }
+});