Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active July 3, 2016 10:36
Show Gist options
  • Save veltman/23460413ea085c024bf8 to your computer and use it in GitHub Desktop.
Save veltman/23460413ea085c024bf8 to your computer and use it in GitHub Desktop.
Transition hacking with chains

Similar to this transition hacking example, this turns 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.

This version respects chained transitions and delays by expecting an absolute time value in milliseconds rather than a relative time value between 0 and 1.

Caveat #1: This doesn't yet work if you have multiple transitions in a chain modifying the same property.

Caveat #2: this is probably a pretty bad idea! Maybe it can be done by tinkering directly with the internal timer instead?

d3.selection.prototype.record = function(realtime) {
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 = getTransitions.call(this);
pending.forEach(function(transition){
// Probably unnecessary?
var ease = transition.ease || id;
transition.tween.values().forEach(function(tween){
// Create a tweener with the tween function and the element's datum and index
// Tweens with no change return false
var tweener = (tween.call(node,d,i) || noop).bind(node);
// Function that calls the tweener with an eased time value
tweeners.push(function(t){
if (realtime) {
t = relativeTime(t,transition.duration,transition.delay);
}
tweener(ease(t));
});
});
});
});
// Return a function that takes relative time between 0 and 1,
// or an absolute number of ms
return function(t){
// Interrupt any active transitions on the selection
// Cancel any scheduled transitions
self.interrupt().transition();
// Apply every tweener function with the provided value t
// TODO: only call one tweener per attribute based on the time
tweeners.forEach(function(tweener){
tweener(t);
});
};
function getTransitions(){
return d3.entries(this.__transition__)
.filter(function(tr){
return tr.key !== "active" && tr.key !== "count";
})
.map(function(tr){
return tr.value;
});
}
function relativeTime(ms,duration,delay) {
return Math.min(1,Math.max(0,(ms - delay)/duration));
}
function noop() {}
function id(d) { return d; }
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
text-align: center;
font: 16px sans-serif;
}
button {
margin: 0 0.5em;
}
circle {
stroke: #000;
stroke-width: 1.5px;
}
</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 margin = {top: 100, right: 100, bottom: 100, left: 100},
width = 960 - margin.left - margin.right,
height = 440 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(d3.range(5))
.rangePoints([0, width]);
var y = x.copy()
.rangePoints([0, height]);
var color = d3.scale.linear()
.domain(d3.extent(x.domain()))
.range(["hsl(297,50%,47%)", "hsl(81,78%,61%)"])
.interpolate(d3.interpolateHcl);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dots = svg.selectAll("circle")
.data(x.domain())
.enter()
.append("circle")
.attr("r",20)
.attr("cx",0)
.attr("cy",y)
.style("fill",color);
// Declare a normal transition
dots.transition()
.duration(2500)
.delay(function(d){
return d * 100;
})
.attr("cx",x)
.transition()
.attr("r",40)
.filter(function(d){
return d % 2;
})
.transition()
.style("opacity",0);
// "Record" the transition into a function that can jump to any time
// `true` means it expects an absolute time in ms
var jumpToTime = dots.record(true);
d3.select("body").append("div")
.text("Jump to: ")
.selectAll("button")
.data(d3.range(0,8500,1000))
.enter()
.append("button")
.text(function(d){
return Math.round(d/1000) + " sec";
})
.on("click",jumpToTime);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment