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 d3#2140.

Calling selection.interrupt repeatedly no longer cancels any scheduled (but
inactive) transitions, fixing d3#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 d3#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 d3#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.