Tweening Custom Shapes and Paths in D3.js

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

← andyshora.comworth a tweet?+1

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:

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!

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'; };
  })

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.

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!

by Andy Shora

I'm a Front-end Web Developer based in London. I currently work with some very talented people over at QuantumBlack doing data viz. I also 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