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
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
<!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="http://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> |
It is possible to do in curves without getPointsAtLength because its slow and this is needed me for morph split closed path to more for match another path to get best result morph looking. Has triangulation and i am not undetstand good. Look at flubberjs that uses triangulation or my minimorph on npm or github to know what i mean. Thanks for work. I will credit you if you help me