Skip to content

Animating D3 Arcs, with an Arc for each bound data point

I’d like to be able to animate multiple (based on the data) arc charts from one percent (angle) to another in D3.js and can draw them fine initially.

However, after much hunting around, I’m stuck with the animation. Below is the code that does the original drawing and then two options for animation to subsequent values. I’m using groups for each Chart Node as I will be adding multiple elements to each.

  • Option 1 uses standard interpolation which I know doesn’t work properly as the shape is too complex. So the animation doesn’t follow the correct steps and also errors are reported to the console.
  • Option 2 uses the Arc Tween method, but this just reports errors.

To see each option working, comment out the other one.

Ideally, I’d like to be able to create an arc function to which I can pass the innerRadius, outerRadius and then the endAngle. For at least the endAngle, I want to be able to choose to pass a constant (e.g. 0) or Bound Data (e.g. d.pct).

index.html

<html lang="en">
<head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
</head>

<body>
    <div id="vis">
    </div>
    <script src = 'SOarc.js'></script>
</body>
</html>

SOarc.js

data = [
    {x:50, y: 250, pct: 0.25},
    {x:200, y: 250, pct: 0.50},
    {x:350, y: 250, pct: 0.75}]

radialScale = d3.scaleLinear()
  .domain([0, 1])
  .range([0, 2 * Math.PI]);

svg = d3.select("#vis")
    .append('svg')
    .attr('width', 500)
    .attr('height', 500)
    .attr('opacity', 1)

// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg
    .selectAll('g.chartGroup')
    .data(data)

// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
    .enter()
    .append('g')   
    .attr("class", "chartGroup")  
    .attr('transform', (d) => 'translate('+d.x+','+d.y+')');

// Add arcs to as per data
chartNodesEnter.append('path')
    .attr("class", "chart1")
    .attr('fill', "red")
    .attr('d', d3.arc()
        .startAngle(0)
        .endAngle((d) => radialScale(d.pct))
        .innerRadius(50+2)         // This is the size of the donut hole
        .outerRadius(50+8));

// Now animate to a different endAngle (90% in this example)

// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
svg.selectAll('.chart1')
    .transition().duration(3000).delay(0)
    .attr('d', d3.arc() 
        .startAngle(0)
        .endAngle(function(d){ return radialScale(0.9)})
        .innerRadius(50+2)         // This is the size of the donut hole
        .outerRadius(50+8)
    )

// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {n  …".

var arc = d3.arc()
    .innerRadius(50+2)
    .outerRadius(50+8)
    .startAngle(0);

// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
    return function(d) {
      var interpolate = d3.interpolate(d.endAngle, newAngle);
      return function(t) {
        d.endAngle = interpolate(t);
        return arc(d);
      };
    };
  }

// Animate to 90%
svg.selectAll('.chart1')
    .transition().duration(3000).delay(0)
    .attrTween("d", d => arcTween(radialScale(0.9)) );

Error: <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {n …". @ SOarc.js:68

Answer

Option 2 is the right way to do this but Mr. Bostock’s example is a little much for your simpler use case.

Let’s examine the simplest code which achieves your goal:

// create a arc generator with start angle of 0
var arc = d3
  .arc()
  .innerRadius(50 + 2)
  .outerRadius(50 + 8)
  .startAngle(0)
  .endAngle(0);

svg
  .selectAll('.chart1')
  .transition()
  .duration(3000)
  .delay(0)
  .attrTween('d', function(d,i) {
    // for each chart 
    // create an interpolator between start angle 0
    // and end angle of d.pct
    var interpolate = d3.interpolate(0, radialScale(d.pct));

    // attrTween is expecting a function to call for every iteration of t
    // so let's return such a function
    return function(t) {
      // assign end angle to interpolated value for t
      arc.endAngle(interpolate(t));
      // call arc and return intermediate `d` value
      return arc();
    };
  });

Here it is running:

<html lang="en">
  <head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
  </head>

  <body>
    <div id="vis"></div>
    <script>
      data = [
        { x: 50, y: 250, pct: 0.25 },
        { x: 200, y: 250, pct: 0.5 },
        { x: 350, y: 250, pct: 0.75 },
      ];

      radialScale = d3
        .scaleLinear()
        .domain([0, 1])
        .range([0, 2 * Math.PI]);

      svg = d3
        .select('#vis')
        .append('svg')
        .attr('width', 500)
        .attr('height', 500)
        .attr('opacity', 1);

      // Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
      chartNodes = svg.selectAll('g.chartGroup').data(data);

      // Position each using transform/ translate with coordinates specified in data
      chartNodesEnter = chartNodes
        .enter()
        .append('g')
        .attr('class', 'chartGroup')
        .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');

      // Add arcs to as per data
      chartNodesEnter
        .append('path')
        .attr('class', 'chart1')
        .attr('fill', 'red')
        .attr(
          'd',
          d3
            .arc()
            .startAngle(0)
            .endAngle((d) => radialScale(d.pct))
            .innerRadius(50 + 2) // This is the size of the donut hole
            .outerRadius(50 + 8)
        );

      // Now animate to a different endAngle (90% in this example)

      // Option 1 - Standard Interpolation - doesn't work with complex shapes
      // --------------------------------------------------------------------
      // Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
      // and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason

      /*
      svg
        .selectAll('.chart1')
        .transition()
        .duration(3000)
        .delay(0)
        .attr(
          'd',
          d3
            .arc()
            .startAngle(0)
            .endAngle(function (d) {
              return radialScale(0.9);
            })
            .innerRadius(50 + 2) // This is the size of the donut hole
            .outerRadius(50 + 8)
        );
      */

      // Option 2 - Tween Interpolation - Produces error
      // -----------------------------------------------
      // Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
      // Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {n  …".

      var arc = d3
        .arc()
        .innerRadius(50 + 2)
        .outerRadius(50 + 8)
        .startAngle(0)
        .endAngle(0);

      // Animate to end angle
      svg
        .selectAll('.chart1')
        .transition()
        .duration(3000)
        .delay(0)
        .attrTween('d', function(d,i) {
          var interpolate = d3.interpolate(0, radialScale(d.pct));
          return function(t) {
            arc.endAngle(interpolate(t));
            return arc();
          };
        });
    </script>
  </body>
</html>

New snippet for comments

Lots of options for variable arcs. The first thing that jumped into my head was to add your radiuses into your data binding and create the arcs like in this snippet.

<html lang="en">
  <head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
  </head>

  <body>
    <div id="vis"></div>
    <script>
      data = [
        { x: 50, y: 250, pct: 0.25, inner: 52, outer: 58 },
        { x: 200, y: 250, pct: 0.5, inner: 22, outer: 28 },
        { x: 350, y: 250, pct: 0.75, inner: 82, outer: 88 },
      ];

      radialScale = d3
        .scaleLinear()
        .domain([0, 1])
        .range([0, 2 * Math.PI]);

      svg = d3
        .select('#vis')
        .append('svg')
        .attr('width', 500)
        .attr('height', 500)
        .attr('opacity', 1);

      // Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
      chartNodes = svg.selectAll('g.chartGroup').data(data);

      // Position each using transform/ translate with coordinates specified in data
      chartNodesEnter = chartNodes
        .enter()
        .append('g')
        .attr('class', 'chartGroup')
        .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');

      // Add arcs to as per data
      chartNodesEnter
        .append('path')
        .attr('class', 'chart1')
        .attr('fill', 'red')
        .attr(
          'd',
          d3
            .arc()
            .startAngle(0)
            .endAngle((d) => radialScale(d.pct))
            .innerRadius(50 + 2) // This is the size of the donut hole
            .outerRadius(50 + 8)
        );

      // Now animate to a different endAngle (90% in this example)

      // Option 1 - Standard Interpolation - doesn't work with complex shapes
      // --------------------------------------------------------------------
      // Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
      // and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason

      /*
      svg
        .selectAll('.chart1')
        .transition()
        .duration(3000)
        .delay(0)
        .attr(
          'd',
          d3
            .arc()
            .startAngle(0)
            .endAngle(function (d) {
              return radialScale(0.9);
            })
            .innerRadius(50 + 2) // This is the size of the donut hole
            .outerRadius(50 + 8)
        );
      */

      // Option 2 - Tween Interpolation - Produces error
      // -----------------------------------------------
      // Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
      // Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {n  …".

      // Animate to end angle
      svg
        .selectAll('.chart1')
        .transition()
        .duration(3000)
        .delay(0)
        .attrTween('d', function(d,i) {
          var interpolate = d3.interpolate(0, radialScale(d.pct));
          var arc = d3
            .arc()
            .innerRadius(d.inner)
            .outerRadius(d.outer)
            .startAngle(0)
            .endAngle(0);
          return function(t) {
            arc.endAngle(interpolate(t));
            return arc();
          };
        });
    </script>
  </body>
</html>