Skip to content

Commit

Permalink
Better interruption semantics.
Browse files Browse the repository at this point in the history
When a transition is interrupted, the interrupt event is dispatched immediately
(such as within a call to selection.interrupt). This is easier to understand and
guarantees that the interrupt event on an interrupted transition is dispatched
prior to the start event on an interrupting transition, fixing #2140.

Calling selection.interrupt repeatedly no longer cancels any scheduled (but
inactive) transitions, fixing #2141. Calling selection.interrupt when there is
no active transition now has no effect.

An interrupt event is only dispatched if the active transition is interrupted,
and not if a scheduled transition was cancelled, as when a delayed transition is
superceded by an earlier transition on the same element. These transitions are
cancelled silently, fixing #2144.

Lastly, transition event listeners now see the latest bound data, rather than
using the data that was captured shortly after the transition was scheduled.
Fixes #2142.
  • Loading branch information
mbostock committed Dec 8, 2014
1 parent bb52d62 commit d316211
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 94 deletions.
121 changes: 70 additions & 51 deletions d3.js
Original file line number Diff line number Diff line change
Expand Up @@ -989,33 +989,6 @@
return node;
};
}
d3_selectionPrototype.transition = function(name) {
var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || {
time: Date.now(),
ease: d3_ease_cubicInOut,
delay: 0,
duration: 250
};
for (var j = -1, m = this.length; ++j < m; ) {
subgroups.push(subgroup = []);
for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) d3_transitionNode(node, i, ns, id, transition);
subgroup.push(node);
}
}
return d3_transition(subgroups, ns, id);
};
d3_selectionPrototype.interrupt = function(name) {
var ns = d3_transitionNamespace(name);
return this.each(function() {
var lock = this[ns];
if (lock) ++lock.active;
});
};
function d3_selection_interrupt(that) {
var lock = that.__transition__;
if (lock) ++lock.active;
}
d3.select = function(node) {
var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ];
group.parentNode = d3_documentElement;
Expand Down Expand Up @@ -1425,7 +1398,7 @@
}
function mousedowned() {
var that = this, target = d3.event.target, dispatch = event.of(that, arguments), dragged = 0, subject = d3.select(d3_window).on(mousemove, moved).on(mouseup, ended), location0 = location(d3.mouse(that)), dragRestore = d3_event_dragSuppress();
d3_selection_interrupt(that);
d3_selection_interrupt.call(that);
zoomstarted(dispatch);
function moved() {
dragged = 1;
Expand Down Expand Up @@ -1474,7 +1447,7 @@
}
function moved() {
var touches = d3.touches(that), p0, l0, p1, l1;
d3_selection_interrupt(that);
d3_selection_interrupt.call(that);
for (var i = 0, n = touches.length; i < n; ++i, l1 = null) {
p1 = touches[i];
if (l1 = locations0[p1.identifier]) {
Expand Down Expand Up @@ -1511,7 +1484,7 @@
function mousewheeled() {
var dispatch = event.of(this, arguments);
if (mousewheelTimer) clearTimeout(mousewheelTimer); else translate0 = location(center0 = center || d3.mouse(this)),
d3_selection_interrupt(this), zoomstarted(dispatch);
d3_selection_interrupt.call(this), zoomstarted(dispatch);
mousewheelTimer = setTimeout(function() {
mousewheelTimer = null;
zoomended(dispatch);
Expand Down Expand Up @@ -8553,9 +8526,43 @@
});
d3.svg.symbolTypes = d3_svg_symbols.keys();
var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians);
function d3_transition(groups, namespace, id) {
d3_selectionPrototype.transition = function(name) {
var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || {
time: Date.now(),
ease: d3_ease_cubicInOut,
delay: 0,
duration: 250
};
for (var j = -1, m = this.length; ++j < m; ) {
subgroups.push(subgroup = []);
for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) d3_transitionNode(node, i, ns, id, transition);
subgroup.push(node);
}
}
return d3_transition(subgroups, ns, id);
};
d3_selectionPrototype.interrupt = function(name) {
return this.each(name == null ? d3_selection_interrupt : d3_selection_interruptNS(d3_transitionNamespace(name)));
};
var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace());
function d3_selection_interruptNS(ns) {
return function() {
var lock, active;
if ((lock = this[ns]) && (active = lock[lock.active])) {
if (--lock.count) {
delete lock[lock.active];
lock.active += .5;
} else {
delete this[ns];
}
active.event && active.event.interrupt.call(this, this.__data__, active.index);
}
};
}
function d3_transition(groups, ns, id) {
d3_subclass(groups, d3_transitionPrototype);
groups.namespace = namespace;
groups.namespace = ns;
groups.id = id;
return groups;
}
Expand Down Expand Up @@ -8758,13 +8765,16 @@
var id = this.id, ns = this.namespace;
if (arguments.length < 2) {
var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId;
d3_transitionInheritId = id;
d3_selection_each(this, function(node, i, j) {
d3_transitionInherit = node[ns][id];
type.call(node, node.__data__, i, j);
});
d3_transitionInherit = inherit;
d3_transitionInheritId = inheritId;
try {
d3_transitionInheritId = id;
d3_selection_each(this, function(node, i, j) {
d3_transitionInherit = node[ns][id];
type.call(node, node.__data__, i, j);
});
} finally {
d3_transitionInherit = inherit;
d3_transitionInheritId = inheritId;
}
} else {
d3_selection_each(this, function(node) {
var transition = node[ns][id];
Expand Down Expand Up @@ -8795,8 +8805,8 @@
function d3_transitionNamespace(name) {
return name == null ? "__transition__" : "__transition_" + name + "__";
}
function d3_transitionNode(node, i, namespace, id, inherit) {
var lock = node[namespace] || (node[namespace] = {
function d3_transitionNode(node, i, ns, id, inherit) {
var lock = node[ns] || (node[ns] = {
active: 0,
count: 0
}), transition = lock[id];
Expand All @@ -8807,21 +8817,28 @@
time: time,
delay: inherit.delay,
duration: inherit.duration,
ease: inherit.ease
ease: inherit.ease,
index: i
};
inherit = null;
++lock.count;
d3.timer(function(elapsed) {
var d = node.__data__, delay = transition.delay, duration, ease, timer = d3_timer_active, tweened = [];
var delay = transition.delay, duration, ease, timer = d3_timer_active, tweened = [];
timer.t = delay + time;
if (delay <= elapsed) return start(elapsed - delay);
timer.c = start;
function start(elapsed) {
if (lock.active > id) return stop(false);
if (lock.active > id) return stop();
var active = lock[lock.active];
if (active) {
--lock.count;
delete lock[lock.active];
active.event && active.event.interrupt.call(node, node.__data__, active.index);
}
lock.active = id;
transition.event && transition.event.start.call(node, d, i);
transition.event && transition.event.start.call(node, node.__data__, i);
transition.tween.forEach(function(key, value) {
if (value = value.call(node, d, i)) {
if (value = value.call(node, node.__data__, i)) {
tweened.push(value);
}
});
Expand All @@ -8833,16 +8850,18 @@
}, 0, time);
}
function tick(elapsed) {
if (lock.active !== id) return stop(false);
if (lock.active !== id) return 1;
var t = elapsed / duration, e = ease(t), n = tweened.length;
while (n > 0) {
tweened[--n].call(node, e);
}
if (t >= 1) return stop(true);
if (t >= 1) {
transition.event && transition.event.end.call(node, node.__data__, i);
return stop();
}
}
function stop(end) {
if (transition.event) transition.event[end ? "end" : "interrupt"].call(node, d, i);
if (--lock.count) delete lock[id]; else delete node[namespace];
function stop() {
if (--lock.count) delete lock[id]; else delete node[ns];
return 1;
}
}, 0, time);
Expand Down
10 changes: 5 additions & 5 deletions d3.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/behavior/zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ d3.behavior.zoom = function() {
location0 = location(d3.mouse(that)),
dragRestore = d3_event_dragSuppress();

d3_selection_interrupt(that);
d3_selection_interrupt.call(that);
zoomstarted(dispatch);

function moved() {
Expand Down Expand Up @@ -270,7 +270,7 @@ d3.behavior.zoom = function() {
p0, l0,
p1, l1;

d3_selection_interrupt(that);
d3_selection_interrupt.call(that);

for (var i = 0, n = touches.length; i < n; ++i, l1 = null) {
p1 = touches[i];
Expand Down Expand Up @@ -318,7 +318,7 @@ d3.behavior.zoom = function() {
function mousewheeled() {
var dispatch = event.of(this, arguments);
if (mousewheelTimer) clearTimeout(mousewheelTimer);
else translate0 = location(center0 = center || d3.mouse(this)), d3_selection_interrupt(this), zoomstarted(dispatch);
else translate0 = location(center0 = center || d3.mouse(this)), d3_selection_interrupt.call(this), zoomstarted(dispatch);
mousewheelTimer = setTimeout(function() { mousewheelTimer = null; zoomended(dispatch); }, 50);
d3_eventPreventDefault();
scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k);
Expand Down
26 changes: 18 additions & 8 deletions src/selection/interrupt.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ import "selection";

// TODO Interrupt transitions for all namespaces?
d3_selectionPrototype.interrupt = function(name) {
var ns = d3_transitionNamespace(name);
return this.each(function() {
var lock = this[ns];
if (lock) ++lock.active;
});
return this.each(name == null
? d3_selection_interrupt
: d3_selection_interruptNS(d3_transitionNamespace(name)));
};

function d3_selection_interrupt(that) {
var lock = that.__transition__;
if (lock) ++lock.active;
var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace());

function d3_selection_interruptNS(ns) {
return function() {
var lock, active;
if ((lock = this[ns]) && (active = lock[lock.active])) {
if (--lock.count) {
delete lock[lock.active];
lock.active += .5;
} else {
delete this[ns];
}
active.event && active.event.interrupt.call(this, this.__data__, active.index);
}
};
}
3 changes: 0 additions & 3 deletions src/selection/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ import "node";
import "size";
import "enter";

import "transition";
import "interrupt";

// TODO fast singleton implementation?
d3.select = function(node) {
var group = [typeof node === "string" ? d3_select(node, d3_document) : node];
Expand Down
17 changes: 10 additions & 7 deletions src/transition/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ d3_transitionPrototype.each = function(type, listener) {
if (arguments.length < 2) {
var inherit = d3_transitionInherit,
inheritId = d3_transitionInheritId;
d3_transitionInheritId = id;
d3_selection_each(this, function(node, i, j) {
d3_transitionInherit = node[ns][id];
type.call(node, node.__data__, i, j);
});
d3_transitionInherit = inherit;
d3_transitionInheritId = inheritId;
try {
d3_transitionInheritId = id;
d3_selection_each(this, function(node, i, j) {
d3_transitionInherit = node[ns][id];
type.call(node, node.__data__, i, j);
});
} finally {
d3_transitionInherit = inherit;
d3_transitionInheritId = inheritId;
}
} else {
d3_selection_each(this, function(node) {
var transition = node[ns][id];
Expand Down
43 changes: 28 additions & 15 deletions src/transition/transition.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import "../core/true";
import "../event/dispatch";
import "../event/timer";
import "../selection/selection";
import "../selection/transition";
import "../selection/interrupt";

function d3_transition(groups, namespace, id) {
function d3_transition(groups, ns, id) {
d3_subclass(groups, d3_transitionPrototype);

// Note: read-only!
groups.namespace = namespace;
groups.namespace = ns;
groups.id = id;

return groups;
Expand Down Expand Up @@ -51,8 +53,8 @@ function d3_transitionNamespace(name) {
return name == null ? "__transition__" : "__transition_" + name + "__";
}

function d3_transitionNode(node, i, namespace, id, inherit) {
var lock = node[namespace] || (node[namespace] = {active: 0, count: 0}),
function d3_transitionNode(node, i, ns, id, inherit) {
var lock = node[ns] || (node[ns] = {active: 0, count: 0}),
transition = lock[id];

if (!transition) {
Expand All @@ -63,16 +65,16 @@ function d3_transitionNode(node, i, namespace, id, inherit) {
time: time,
delay: inherit.delay,
duration: inherit.duration,
ease: inherit.ease
ease: inherit.ease,
index: i
};

inherit = null; // allow gc

++lock.count;

d3.timer(function(elapsed) {
var d = node.__data__,
delay = transition.delay,
var delay = transition.delay,
duration,
ease,
timer = d3_timer_active,
Expand All @@ -83,12 +85,21 @@ function d3_transitionNode(node, i, namespace, id, inherit) {
timer.c = start;

function start(elapsed) {
if (lock.active > id) return stop(false);
if (lock.active > id) return stop();

var active = lock[lock.active];
if (active) {
--lock.count;
delete lock[lock.active];
active.event && active.event.interrupt.call(node, node.__data__, active.index);
}

lock.active = id;
transition.event && transition.event.start.call(node, d, i);

transition.event && transition.event.start.call(node, node.__data__, i);

transition.tween.forEach(function(key, value) {
if (value = value.call(node, d, i)) {
if (value = value.call(node, node.__data__, i)) {
tweened.push(value);
}
});
Expand All @@ -104,7 +115,7 @@ function d3_transitionNode(node, i, namespace, id, inherit) {
}

function tick(elapsed) {
if (lock.active !== id) return stop(false);
if (lock.active !== id) return 1;

var t = elapsed / duration,
e = ease(t),
Expand All @@ -114,13 +125,15 @@ function d3_transitionNode(node, i, namespace, id, inherit) {
tweened[--n].call(node, e);
}

if (t >= 1) return stop(true);
if (t >= 1) {
transition.event && transition.event.end.call(node, node.__data__, i);
return stop();
}
}

function stop(end) {
if (transition.event) transition.event[end ? "end" : "interrupt"].call(node, d, i);
function stop() {
if (--lock.count) delete lock[id];
else delete node[namespace];
else delete node[ns];
return 1;
}
}, 0, time);
Expand Down
Loading

0 comments on commit d316211

Please sign in to comment.