Split an SVG path lengthwise into multiple colours

The name of the pictureThe name of the pictureThe name of the pictureClash 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!





What if both of them have frequency > 50 ? Say red 80 and black 75.
– Kosh Very
Aug 5 at 2:02





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");
);



enter image description here



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.

Popular posts from this blog

Firebase Auth - with Email and Password - Check user already registered

Dynamically update html content plain JS

Creating a leaderboard in HTML/JS