How to smoothly transition custom shapes and paths in D3.js.

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 Andy Shora

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...

A very simple out-of-the-box interpolator in D3.js // 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.

Can you guess what these SVG Shapes will look like once rendered? <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?

value of the line, over time? What if we were to increase the width value of the rect, over time?

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. Andy Shora

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.

Simple shape changes don't require interpolators // #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.

We can use a custom interpolator to tween part of a path's data: 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'; }; })

Playing with Trigonometry and D3 Need to refamiliarise yourself with the basics? Can't remember how to convert degrees to radians? Fear not! I made a little D3 + Trigonometry Starter on JSBin. Clone and play!

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.

Drawing SVG Segments I always sketch custom paths, breaking down each line into its respective SVG Mini-language command. You can hack around your own trigonometry in this D3 Starter on JSBin.

We can use a custom interpolator to tween part of a path's data: // 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!