Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active July 3, 2016 05:42
Show Gist options
  • Save veltman/d1b8c76c16ceab5d3b45 to your computer and use it in GitHub Desktop.
Save veltman/d1b8c76c16ceab5d3b45 to your computer and use it in GitHub Desktop.
Transition hacking

A somewhat crazy method for turning an asynchronous d3 transition into something that can be paused/fast-forwarded/rewound while still using the nice syntax, easing, interpolation, etc. of d3.transition.

After a transition is declared with normal syntax, calling .record() on a selection inspects each element in a selection for the internals of its most recent transition. It generates an array of tweeners based on the transitions' tweens, the easing, and the elements' data. It then returns a wrapper function that will loop through and apply all of those tweeners for any arbitrary time value t (between 0 and 1).

This operates on the entire set of elements in the transition, but you could make it per-element with some changes.

This version only records the most recent transition and ignores delays. For a version that respects delays and chained transitions, see this example.

d3.selection.prototype.record = function() {
var self = this,
tweeners = [];
// Get tweeners on each element in the selection
self.each(function(d,i){
// using .count should work as long as it's called right after the transition is created?
// can use .active if you wait for the "start" event
var node = this,
pending = this.__transition__[this.__transition__.count],
ease = pending.ease;
// Loop through the element's tweens
d3.values(pending.tween._).forEach(function(tween){
// Create a tweener with the tween function and the element's datum and index
var tweener = tween.call(node,d,i).bind(node);
// Function that calls the tweener with an eased time value
tweeners.push(function(t){
tweener(ease(t));
});
});
});
// Return a function that takes t between 0 and 1
return function(t){
// Interrupt any transitions on the selection
self.interrupt();
// Apply every tweener function with the provided value t
tweeners.forEach(function(tweener){
tweener(t);
});
};
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
text-align: center;
font: 16px sans-serif;
}
button {
margin: 0 0.5em;
}
circle {
stroke: none;
fill: #de1e3d;
opacity: 0.8;
}
</style>
<body>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.14/d3.min.js"></script>
<script src="d3-record.js"></script>
<script>
var width = 760,
height = 200,
margin = 100;
var svg = d3.select("body").append("svg")
.attr("width",width + margin * 2)
.attr("height",height + margin * 2)
.append("g")
.attr("transform","translate(" + margin + " " + margin + ")")
var dots = svg.selectAll("circle")
.data(d3.range(2))
.enter()
.append("circle")
.attr("r",50)
.attr("cx",function(d){
return d ? width : 0;
})
.attr("cy",height / 2);
// Declare a normal transition
dots.transition()
.duration(3000)
.style("fill",function(d){
return d ? "#0e88ba" : "#fc0";
})
.attr("cx",function(d){
return d ? 0 : width;
})
.attr("r",function(d){
return d ? 100 : 25;
});
// "Record" the transition into a function that can jump to any time
var jumpToTime = dots.record();
d3.select("body").append("div")
.text("Jump to: ")
.selectAll("button")
.data(d3.range(0,1.1,0.1))
.enter()
.append("button")
.text(function(d){
return (d * 100) + "%";
})
.on("click",jumpToTime);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment