Bezier curve animation using D3.js
January 16, 2015
TL;DR: Instead of trying to interpolate the path manually, interpolate stroke-dasharray
. Confused? Read on.
I was recently faced with the challenge of animating a Bezier curve using the data manipulation and visualization library D3.js, which I eventually accomplished for Shopify's live map of orders. We start off with a fairly rudimentary code to generate a spline:
var bezierLine = d3.svg.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
.interpolate("basis");
var svg = d3.select("#bezier-demo")
.append("svg")
.attr("width", 300)
.attr("height", 150);
svg.append('path')
.attr("d", bezierLine([[0, 40], [25, 70], [50, 100], [100, 50], [150, 20], [200, 130], [300, 120]]))
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none");
<div id="bezier-demo"></div>
If you have no idea what any of this code means, then I highly recommend you read this excellent tutorial on generating static lines and curves using D3.js.
Now what if we wanted to smoothly animate this curve? An naive approach would be to break the line into bits, and render them one chunk at a time. This approach works, but gets quite complicated as there is no built-in method to break up a spline into chunks. The only way to do this, then, would be to manually compute a Beizier curve. Doing it like that would be an interesting exercise for some, myself included, there's a far easier way to do it.
All SVG paths have a stroke-dasharray
property that was intended to make dashed or dotted lines. It's in the of whitespace or comma seperated values that indicate alternating
dashes and gaps. For instance, rendering our first bezier curve with a stroke-dasharray
of 10,5
will result
in a repeating pattern of 10 pixel dashes followed by 5 pixel gaps.
Perhaps now you'll see what we're going to do next. Instead of attempting to interpolate the path itself, we're going to interpolate
the stroke-dasharray
property. This will make the line appear to be animated without having to do any of the computation
manually ourselves.
var bezierLine = d3.svg.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
.interpolate("basis");
var svg = d3.select("#bezier-demo")
.append("svg")
.attr("width", 300)
.attr("height", 150);
svg.append('path')
.attr("d", bezierLine([[0, 40], [25, 70], [50, 100], [100, 50], [150, 20], [200, 130], [300, 120]]))
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none");
.transition()
.duration(2000)
.attrTween("stroke-dasharray", function() {
var len = this.getTotalLength();
return function(t) { return (d3.interpolateString("0," + len, len + ",0"))(t) };
});
<div id="bezier-demo"></div>
We only added a few extra lines of code (the call to transition
) to get our animation working.
We furst specify the duration, then we tell D3 to tween the stroke-dasharray
property according
to a function that we specify. In this case, we want it to transition starting from 0,len
to len,0
, where len
is the length of our line. This transition starts with the entire
line consisting of a single gap the length of the line (and thus completely invisible) to a single dash (and
thus completely visible).
The function we pass into attrTween
as its second argument has to be return another function,
which returns a string generated by yet another function d3.interpolateString
, which handles
the interpolation of our two strings for intermediate values.
For more detailed and descriptive information about D3.js transitions, consult the documentation.
For the record, I did not come across this technique until I finished coding up manual calcuations for animating the Bezier curve. I'll write another blog post about that method should someone express their interest in reading it.