Split an SVG path lengthwise into multiple colours
Clash Royale CLAN TAG#URR8PPP
Split an SVG path lengthwise into multiple colours
I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d)
return "translate(" + -2.5 + "," + 0.0 + ")"; );
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d)
return "translate(" + 2.5 + "," + 0.0 + ")"; );
It would be great if anyone has some advice.
Thanks!
The idea is to represent the relative size of each class, so they would be normalised. In your example, the values would be normalised to 80 / (80+75) and 75 / (80+75).
– Isaac Monteath
Aug 5 at 2:45
2 Answers
2
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = color: ["red", "black"], percent: 0.30, width: 15 ;
percent
is the amount color[0]
takes from the stroke width.
percent
color[0]
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) =>
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
);
The pathPoints
parameters
pathPoints
path that needs to be stroked, can be generated by d3.line
path example from SO answer
d3.line
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties@0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp)
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p =>
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
);
var format1d = d3.format(".1f");
var createPath = (forward, backward) =>
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `$format1d(p[0]),$format1d(p[1])`).join(' ') + 'z';
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
var duoProp = color: ["red", "black"], percent: 0.30, width: 15 ;
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) =>
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
);
</script>
</body>
</html>
Thank you very much for the detailed answer. I've managed to apply it to my application but I'm struggling to extend it to more than two colours. Ideally, I'd like to specify a dict like 'red': 0.2, 'green': 0.3, 'blue': 0.5 (where the values are percentages). Do you have any hints on how I could do this? Thanks!
– Isaac Monteath
Aug 5 at 22:12
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp)
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d)
return props.getPropertiesAtLength(d);
);
// incorporate the percentage
var pFactor = function pFactor(percent)
return (percent - 0.5) * duoProp.width;
;
// for each path segment, calculate offset points
tProps.forEach(function (p)
// create array to store modified points
p.x_arr = ;
p.y_arr = ;
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc)
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
);
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
);
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward)
var fp = tProps.map(function (p)
return forward(p);
);
var bp = tProps.map(function (p)
return backward(p);
);
bp.reverse();
return 'M' + fp.concat(bp).map(function (p)
return format1d(p[0]) + "," + format1d(p[1]);
).join(' ') + 'z';
;
// create a path for each projected point
var paths = ;
for(var i=0; i <= duoProp.percents.length; i++)
paths.push(createPath(function (p) return [p.x_arr[i], p.y_arr[i]]; , function (p) return [p.x_arr[i+1], p.y_arr[i+1]]; ));
return paths;
// generate the line
var duoProp = color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 ;
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) =>
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
);
Note that the percents
array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.
percents
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
What if both of them have frequency > 50 ? Say red 80 and black 75.
– Kosh Very
Aug 5 at 2:02