block by bycoffe 18441cddeb8fe147b719fab5e30b5d45

Split an SVG path into pieces

Full Screen

This is an example of how to split an SVG path into an arbitrary number of pieces.

It is the technique used for The winding path to 270 electoral votes

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  body {
    font-family: Helvetica;
    font-size: 16px;
  }
  ol li {
    margin-bottom: 10px;
  }
  li.highlight {
    background: yellow;
  }
  pre {
    display: inline;
    background: lightgray;
  }
  #chart {
    width: 960px;
    height: 350px;
  }
  path {
    fill: none;
    stroke: #999;
  }
  path.hidden {
    display: none;
  }
  path.init {
    stroke-width: 2;
  }
  path.piece {
    stroke-width: 12;
    stroke: none;
  }
  line.sep {
    stroke-width: 4;
    stroke: none;
  }
</style>

<body>

  <div id="chart"></div>

  <ol id="instructions">
    <li data-id="1" class="highlight">Draw an initial path.</li>
    <li data-id="2" >Determine how many pieces you want and what percentage of the full path each piece should account for. (Here we're using 20 pieces of random sizes.)</li>
    <li data-id="3">Get the location of each section's points along the overall path.</li>
    <li data-id="4">Use those points to draw a new path for each section.</li>
  </ol>

  <script src="//d3js.org/d3.v4.min.js"></script>

  <script>

    function splitPath() {
      var numPieces = 20,

          pieceSizes = [],
          pieces = [];

      for (var i=0; i<numPieces; i++) {
        pieceSizes.push({i: i, size: Math.floor(Math.random() * 20) + 5});
      }

      var size = pieceSizes.reduce(function(a, b) {
        return a + b.size;
      }, 0);

      var pieceSize = pLength / size;

      pieceSizes.forEach(function(x, j) {
        var segs = [];
        for (var i=0; i<=x.size+sampleInterval; i+=sampleInterval) {
          pt = p.getPointAtLength((i*pieceSize)+(cumu*pieceSize));
          segs.push([pt.x, pt.y]);
        }
        angle = Math.atan2(segs[1][1] - segs[0][1], segs[1][0] - segs[0][0]) * 180 / Math.PI;
        pieces.push({id: j, segs: segs, angle: angle});
        cumu += x.size;
      });

      return pieces;
    }

    var margin = {top: 0, right: 20, bottom: 0, left: 20},
        width = 960 - margin.left - margin.right,
        height = 350 - margin.top - margin.bottom,

        colors = d3.schemeCategory20b,

        pts = [],
        numPts = 7;

    colors.sort(function(a, b) {
      return Math.random() > .5 ? 1 : -1;
    });

    for (var i=0; i<numPts; i++) {
      pts.push([i*(width/numPts), 50]);
      pts.push([i*(width/numPts), height-50]);
      pts.push([i*(width/numPts)+50, height-50]);
      pts.push([i*(width/numPts)+50, 50]);
    }

    var path = d3.line()
                    .curve(d3.curveCardinal),

        svg = d3.select("#chart").append("svg")
                  .attr("width", width + margin.left + margin.right)
                  .attr("height", height + margin.top + margin.bottom),

        g = svg.append("g")
                    .attr("transform", "translate(" + margin.left + "," + margin.top + ")"),

        line = g.append("path")
                    .attr("class", "hidden init")
                    .attr("d", path(pts)),

        p = line.node(),
        pLength = p.getTotalLength(),

        cumu = 0,
        sampleInterval = .25;

    function showLine(callback) {
      line.classed("hidden", false)
        .attr("stroke-dasharray", pLength + " " + pLength)
        .attr("stroke-dashoffset", pLength)
        .transition()
          .duration(1500)
          .ease(d3.easeLinear)
          .attr("stroke-dashoffset", 0)
          .on("end", function() {
            callback();
          });
    }

    function drawSegments(pieces) {
      d3.selectAll("#instructions li").classed("highlight", function() {
        return this.getAttribute("data-id") === "4";
      });

      var lines = g.selectAll("path.piece")
                      .data(pieces)
                    .enter().append("path")
                      .attr("class", "piece")
                      .attr("d", function(d, i) {
                        return path(d.segs);
                      });

      var seps = g.selectAll("line.sep")
                      .data(pieces)
                    .enter().append("line")
                      .attr("class", "sep")
                      .attr("transform", function(d, i) {
                        return "translate(" + d.segs[0][0] + "," + d.segs[0][1] + ")rotate(" + (d.angle-90) + " 0 0)";
                      })
                      .attr("x1", -12)
                      .attr("y1", 0)
                      .attr("x2", 12)
                      .attr("y2", 0);
                        

      lines.transition()
        .duration(0)
        .delay(function(d, i) {
          return i * 250;
        })
        .style("stroke", function(d, i) {
          return colors[i];
        })
        .on("end", function(d, i) {
          if (i === pieces.length-1) {
            d3.selectAll("#instructions li").classed("highlight", false);
          }
        })

      seps.transition()
        .duration(0)
        .delay(function(d, i) {
          return i * 250;
        })
        .style("stroke", "#fff");
    }

    showLine(function() {
      d3.selectAll("#instructions li").classed("highlight", function() {
        return this.getAttribute("data-id") === "3";
      });

      var pieces = splitPath();

      var segments = g.selectAll("g.segment")
                    .data(pieces)
                  .enter().append("g"),

          pts = [];

      pieces.forEach(function(x) {
        x.segs.forEach(function(seg, i) {
          if (i > 0 && i % 2 === 0) {
            pts.push({id: x.id, seg: seg});
          }
        });
      });

     var dots = g.selectAll("circle")
                    .data(pts)
                  .enter().append("circle")
                    .attr("cx", function(d, i) {
                      return d.seg[0];
                    })
                    .attr("cy", function(d, i) {
                      return d.seg[1];
                    })
                    .style("fill", function(d, i, j) {
                      return colors[d.id];
                    })
                    .attr("r", 0);

      dots.transition()
            .duration(0)
            .delay(function(d, i) {
              return i * 10;
            })
            .attr("r", 3)
            .on("end", function(d, i, j) {
              if (i === pts.length-1) {
                drawSegments(pieces);
              }
            });

    });

  </script>

</body>