Skip to content

Commit

Permalink
Fix two bugs in selection.interrupt.
Browse files Browse the repository at this point in the history
If a transition is interrupted during its start event, it should stop; if it is
interrupted during its final frame, no end event should be dispatched. #20
  • Loading branch information
mbostock committed Mar 2, 2016
1 parent 56d18c0 commit 5592c9d
Show file tree
Hide file tree
Showing 2 changed files with 280 additions and 8 deletions.
16 changes: 9 additions & 7 deletions src/transition/schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export var CREATED = 0;
export var SCHEDULED = 1;
export var STARTING = 2;
export var STARTED = 3;
export var ENDED = 4;
export var ENDING = 4;
export var ENDED = 5;

export default function(node, name, id, index, group, timing) {
var schedules = node.__transition;
Expand Down Expand Up @@ -105,6 +106,7 @@ function create(node, id, self) {
// Note this must be done before the tween are initialized.
self.state = STARTING;
self.on.call("start", node, node.__data__, self.index, self.group);
if (self.state !== STARTING) return; // interrupted
self.state = STARTED;

// Initialize the tween, deleting null tween.
Expand All @@ -118,16 +120,16 @@ function create(node, id, self) {
}

function tick(elapsed) {
var t = elapsed / self.duration,
e = t >= 1 ? 1 : self.ease.call(null, t),
i, n;
var t = elapsed >= self.duration ? (self.state = ENDING, 1) : self.ease.call(null, elapsed / self.duration),
i = -1,
n = tween.length;

for (i = 0, n = tween.length; i < n; ++i) {
tween[i].call(null, e);
while (++i < n) {
tween[i].call(null, t);
}

// Dispatch the end event.
if (t >= 1) {
if (self.state === ENDING) {
self.state = ENDED;
self.timer.stop();
self.on.call("end", node, node.__data__, self.index, self.group);
Expand Down
272 changes: 271 additions & 1 deletion test/selection/interrupt-test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,280 @@
var tape = require("tape"),
jsdom = require("jsdom"),
d3_selection = require("d3-selection");
d3_selection = require("d3-selection"),
d3_timer = require("d3-timer");

require("../../");

tape("selection.interrupt() returns the selection", function(test) {
var document = jsdom.jsdom(),
selection = d3_selection.select(document);
test.equal(selection.interrupt(), selection);
test.end();
});

tape("selection.interrupt() cancels any pending transitions on the selected elements", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition1 = selection.transition(),
transition2 = transition1.transition();
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, true);
test.equal(selection.interrupt(), selection);
test.equal(root.__transition, undefined);
test.end();
});

tape("selection.interrupt() only cancels pending transitions with the null name", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition1 = selection.transition("foo"),
transition2 = selection.transition();
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, true);
test.equal(selection.interrupt(), selection);
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, false);
test.end();
});

tape("selection.interrupt(null) only cancels pending transitions with the null name", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition1 = selection.transition("foo"),
transition2 = selection.transition();
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, true);
test.equal(selection.interrupt(null), selection);
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, false);
test.end();
});

tape("selection.interrupt(undefined) only cancels pending transitions with the null name", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition1 = selection.transition("foo"),
transition2 = selection.transition();
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, true);
test.equal(selection.interrupt(undefined), selection);
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, false);
test.end();
});

tape("selection.interrupt(name) only cancels pending transitions with the specified name", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition1 = selection.transition("foo"),
transition2 = selection.transition();
test.equal(transition1._id in root.__transition, true);
test.equal(transition2._id in root.__transition, true);
test.equal(selection.interrupt("foo"), selection);
test.equal(transition1._id in root.__transition, false);
test.equal(transition2._id in root.__transition, true);
test.end();
});

tape("selection.interrupt() does nothing if there is no transition on the selected elements", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root);
test.equal(root.__transition, undefined);
test.equal(selection.interrupt(), selection);
test.equal(root.__transition, undefined);
test.end();
});

tape("selection.interrupt() dispatches an interrupt event to the started transition on the selected elements", function(test) {
var root = jsdom.jsdom().documentElement,
interrupts = 0,
selection = d3_selection.select(root),
transition = selection.transition().on("interrupt", function() { ++interrupts; });
d3_timer.timeout(function() {
var schedule = root.__transition[transition._id];
test.equal(schedule.state, 3); // STARTED
selection.interrupt();
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
test.equal(interrupts, 1);
test.end();
});
});

tape("selection.interrupt() destroys the schedule after dispatching the interrupt event", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition = selection.transition().on("interrupt", interrupted);

d3_timer.timeout(function() {
selection.interrupt();
});

function interrupted() {
test.equal(transition.delay(), 0);
test.equal(transition.duration(), 250);
test.equal(transition.on("interrupt"), interrupted);
test.end();
}
});

tape("selection.interrupt() does not dispatch an interrupt event to a starting transition", function(test) {
var root = jsdom.jsdom().documentElement,
interrupts = 0,
selection = d3_selection.select(root),
transition = selection.transition().on("interrupt", function() { ++interrupts; }).on("start", started);

function started() {
var schedule = root.__transition[transition._id];
test.equal(schedule.state, 2); // STARTING
selection.interrupt();
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
test.equal(interrupts, 0);
test.end();
}
});

tape("selection.interrupt() prevents a created transition from starting", function(test) {
var root = jsdom.jsdom().documentElement,
starts = 0,
selection = d3_selection.select(root),
transition = selection.transition().on("start", function() { ++starts; }),
schedule = root.__transition[transition._id];

test.equal(schedule.state, 0); // CREATED
selection.interrupt();
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);

d3_timer.timeout(function() {
test.equal(starts, 0);
test.end();
}, 10);
});

tape("selection.interrupt() prevents a scheduled transition from starting", function(test) {
var root = jsdom.jsdom().documentElement,
starts = 0,
selection = d3_selection.select(root),
transition = selection.transition().delay(50).on("start", function() { ++starts; }),
schedule = root.__transition[transition._id];

d3_timer.timeout(function() {
test.equal(schedule.state, 1); // SCHEDULED
selection.interrupt();
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
});

d3_timer.timeout(function() {
test.equal(starts, 0);
test.end();
}, 60);
});

tape("selection.interrupt() prevents a starting transition from initializing tweens", function(test) {
var root = jsdom.jsdom().documentElement,
tweens = 0,
selection = d3_selection.select(root),
transition = selection.transition().tween("tween", function() { ++tweens; }).on("start", started),
schedule = root.__transition[transition._id];

function started() {
test.equal(schedule.state, 2); // STARTING
selection.interrupt();
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
}

d3_timer.timeout(function() {
test.equal(tweens, 0);
test.end();
}, 10);
});

tape("selection.interrupt() during tween initialization prevents an active transition from continuing", function(test) {
var root = jsdom.jsdom().documentElement,
tweens = 0,
selection = d3_selection.select(root),
transition = selection.transition().tween("tween", function() { selection.interrupt(); return function() { ++tweens; }; }),
schedule = root.__transition[transition._id];

d3_timer.timeout(function() {
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
test.equal(tweens, 0);
test.end();
}, 10);
});

tape("selection.interrupt() prevents an active transition from continuing", function(test) {
var root = jsdom.jsdom().documentElement,
interrupted = false,
tweens = 0,
selection = d3_selection.select(root),
transition = selection.transition().tween("tween", function() { return function() { if (interrupted) ++tweens; }; }),
schedule = root.__transition[transition._id];

d3_timer.timeout(function() {
interrupted = true;
test.equal(schedule.state, 3); // STARTED
selection.interrupt();
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
}, 10);

d3_timer.timeout(function() {
test.equal(tweens, 0);
test.end();
}, 50);
});

tape("selection.interrupt() during the final tween invocation prevents the end event from being dispatched", function(test) {
var root = jsdom.jsdom().documentElement,
ends = 0,
selection = d3_selection.select(root),
transition = selection.transition().duration(50).tween("tween", tween).on("end", function() { ++ends; }),
schedule = root.__transition[transition._id];

function tween() {
return function(t) {
if (t >= 1) {
test.equal(schedule.state, 4); // ENDING
selection.interrupt();
}
};
}

d3_timer.timeout(function() {
test.equal(schedule.timer._call, null);
test.equal(schedule.state, 5); // ENDED
test.equal(root.__transition, undefined);
test.equal(ends, 0);
test.end();
}, 60);
});

tape("selection.interrupt() has no effect on an ended transition", function(test) {
var root = jsdom.jsdom().documentElement,
selection = d3_selection.select(root),
transition = selection.transition().duration(50).on("end", ended),
schedule = root.__transition[transition._id];

function ended() {
test.equal(schedule.state, 5); // ENDED
test.equal(schedule.timer._call, null);
selection.interrupt();
test.equal(schedule.state, 5); // ENDED
test.equal(schedule.timer._call, null);
test.equal(root.__transition, undefined);
test.end();
}
});

0 comments on commit 5592c9d

Please sign in to comment.