Out-of-the-box Interpolation in D3.js
Let's talk interpolation for a moment.
Interpolation is simply a function which turns variable A into variable B. A could be a number, a string, an array of Point objects, it could be pretty much any data structure in JavaScript. The single most important requirement to perform the interpolation, is that the structure of A must match the structure of B.
Think about it this way:
- Turn a small ball into a large ball - easy. Just increase the radius property.
- Turn a line into a curve - achievable. The initial line can be a curve, with the control point of the curve set to the mid-point. The control point can shoft out to generate a curve.
- Turn two a bunch of balls into a batman symbol - WOW. That's near impossible, as the structures don't match. You simply can't generate sensible intermediary states.
The single most important requirement to perform interpolation in D3.js, is that the structure of A must match the structure of B
What does an interpolator look like?
An interpolator is a function which takes your start state A, and your end state B, and returns a function.
This function can be passed a time (from 0 to 1) and returns an intermediary state between A and B. Your interpolator is the function D3 will be calling every 16ms during a transition, in order to tween your shape through lots of intermediary shapes from A to B.
Let's show how a very simple interpolator in D3 works...
// create an interpolator which tweens number a into number b over time
let a = 1;
let b = 2;
let myInterpolator = d3.interpolateNumber(a, b);
// myInterpolator would now be equivalent to:
function interpolate(t) {
return a * (1 - t) + b * t;
}
// * note how the start and end numbers a and b
// are now stored within the interpolator function
// you could now call the myInterpolator function,
// passing a value of t (from 0-1) and you'd get back
// a number inbetween
let c = myInterpolator(0.5);
// c = 1.5
We don't always need to use interpolators.
First, let's think about the SVG nodes that D3 is generating.
Simple geometric shapes have very simple fingerprints, you could take a look at a circle or a rect node and tell what it's going to look like just from the properties and values.
<svg height="200" width="200">
<line x1="0" y1="190" x2="50" y2="190" style="fill:tomato" />
<rect width="10" height="10" style="fill:powderblue" />
<circle cx="100" cy="100" r="50" style="fill:thistle" />
</svg>
Did you get it right?
It doesn't take Sherlock Holmes to tell that this is what you were imagining. Bonus points if you knew what color thistle was!
Mmmm... those properties look very tweenable!
- What if we were to increase the x2 value of the line, over time?
- What if we were to increase the width value of the rect, over time?
- What if we were to decrease the r value of the circle, over time?
Wait, we have just the thing to do that! Our interpolator function.
Simple geometric shapes can be tweened without using interpolators.
Simple: Tweening numeric SVG properties
Simple geometric shapes can be tweened without using interpolators.
Note: I'm using a .each('end', startTweenFunc); call in this demo to repeat it.
// #rect-1 has already been created with width = 0
// you can inspect it to see what's changing
d3.select('#rect-1').transition().duration(1000).attr('width', 250);
// alternative - using a pre-made D3 interpolator
d3.select('#rect-1').transition().duration(1000)
.attrTween('width', function() {
return d3.interpolateNumber(0, 250);
});
// alternative - writing our own custom interpolator to do the same thing
d3.select('#rect-1').transition().duration(1000)
.attrTween('width', function() {
return function(t) { return t * 250; };
});
Advanced: Tweening parts of the path's data property
Advanced tweening can be done by writing our own custom interpolator functions.
Don't worry, once you understand how the path is being constructed via the SVG Mini-language, you'll know which parts of the string need changing over time to achieve your desired transition.
d3.select('#mouth').transition().duration(1000)
.attrTween('d', function() {
// M x y - move cursor to x, y
// s x2 y2 x y - draw a smooth curve using control point x2, y2, to end point x, y
// (it's a lower case s so use relative coords)
return function(t) { return 'M 50 200 s 100 ' + (t * 200 - 100) + ' 200 0'; };
})
Advanced: Tweening paths with some trigonometry
Doing something radial? Then you'll need to brush up on your trigonometry.
The thing to remember when tweening shapes with curves, is that you can't ask D3 to interpolate the whole path data with something like d3.interpolateString, as there are some parts of the SVG Mini-language which need to change in a non-uniform way.
In this example, we have to make sure Math.sin() and Math.cos() are evaluated within our interpolator, as the values they are producing do not change at the same rate as our time, t. We are essentially using these trig functions as our own easing function to tween parts of our path.
// helper function to generate the segment as a path
function generateSVGSegment(x, y, r, startAngle, endAngle) {
// convert angles to Radians
startAngle *= (Math.PI / 180);
endAngle *= (Math.PI / 180);
var largeArc = endAngle - startAngle <= Math.PI ? 0 : 1; // 1 if angle > 180 degrees
var sweepFlag = 1; // is arc to be drawn in +ve direction?
return ['M', x, y, 'L', x + Math.sin(startAngle) * r, y - (Math.cos(startAngle) * r),
'A', r, r, 0, largeArc, sweepFlag, x + Math.sin(endAngle) * r, y - (Math.cos(endAngle) * r), 'Z'
].join(' ');
}
// our custom interpolator, which returns an interpolator function
// which when called with a time (0-1), generates a segment sized according to time
function interpolateSVGSegment(x, y, r, startAngle, endAngle) {
return function(t) {
return generateSVGSegment(x, y, r, startAngle, startAngle + ((endAngle - startAngle) * t));
};
}
// we're ready to kick it off
d3.select('#pie-1').transition().duration(1000)
.attrTween('d', function() {
return interpolateSVGSegment(150, 175, 100, 0, 270);
});
That's D3 shape tweening, done.
So, shapes are nice and tween-able if they have distinct properties, or have path commands which share the same structure.
Interpolating numbers on their own, simple structures like arrays of values, or strings containing numbers can all be done with D3's out-of-the-box interpolator functions.
If you're looking for some more advanced path tweening, such as changing just a subset of a path's d (data) property (like fanning out a pie chart segment), then writing a custom interpolator is the solution. Use these interpolators inside a call to attrTween on the d (data) property.
To source of all the demos on this page are in /js/shapes.js. I've also made a D3 Starter on JSBin to accompany this article, which you can clone and go nuts with! As usual, send me a tweet or leave a comment below if you need a hand!
by Andy Shora
@andyshoraI'm a Front End Web Developer based in London. I love to build sites which are clean and have great performance, and I dabble with whatever technologies are most suitable for the job. Send me tweets @andyshora