I have a d3 network where points are connected by lines. I want to replace the lines with curved SVG paths. I have forgotten the math to calculate the control point's coordinates. Does anyone know how to do this?
For example, look at the image below:
There exist points A and B. I have them connected at present by a line L. I want to replace L with a curve, C. To do that I need to find a line that is perpendicular to the mid-point of line L, of length M (length set as a percentage of L), to be the control point of spline C. Then I need to define an SVG path to define C.
How do I do this in d3 with SVG? I've done this before in Raphael/SVG a long time ago, but the math escapes me. And I'm not sure how its done in D3.
Just to be clear for others, what we're talking about is a quadratic Bezier curve. That gives you a smooth curve between two points with one control point.
The basic method is:
Here's a JavaScript function to return the path you'll need:
function draw_curve(Ax, Ay, Bx, By, M) {
// Find midpoint J
var Jx = Ax + (Bx - Ax) / 2
var Jy = Ay + (By - Ay) / 2
// We need a and b to find theta, and we need to know the sign of each to make sure that the orientation is correct.
var a = Bx - Ax
var asign = (a < 0 ? -1 : 1)
var b = By - Ay
var bsign = (b < 0 ? -1 : 1)
var theta = Math.atan(b / a)
// Find the point that's perpendicular to J on side
var costheta = asign * Math.cos(theta)
var sintheta = asign * Math.sin(theta)
// Find c and d
var c = M * sintheta
var d = M * costheta
// Use c and d to find Kx and Ky
var Kx = Jx - c
var Ky = Jy + d
return "M" + Ax + "," + Ay +
"Q" + Kx + "," + Ky +
" " + Bx + "," + By
}
You can see this in action at this jsfiddle or the snippet (below).
Edit: If a quadratic curve doesn't fit, you can pretty easily adapt the function to do cubic Bezier or arc segments.
var adjacencyList = {
1: [2],
2: [3],
3: [1],
};
var nodes = d3.values(adjacencyList),
links = d3.merge(nodes.map(function(source) {
return source.map(function(target) {
return {
source: source,
target: adjacencyList[target]
};
});
}));
var w = 960,
h = 500;
var M = 50;
var vis = d3.select("#svg-container").append("svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([w, h])
.linkDistance(100)
.charge(-100)
.start();
var link = vis.selectAll(".link")
.data(links)
.enter().append("svg:path")
.attr("class", "link");
console.log(link)
var node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("svg:circle")
.attr("r", 5)
.call(force.drag);
force.on("tick", function() {
link.attr("d", function(d) {
return draw_curve(d.source.x, d.source.y, d.target.x, d.target.y, M);
});
node.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
function draw_curve(Ax, Ay, Bx, By, M) {
// side is either 1 or -1 depending on which side you want the curve to be on.
// Find midpoint J
var Jx = Ax + (Bx - Ax) / 2
var Jy = Ay + (By - Ay) / 2
// We need a and b to find theta, and we need to know the sign of each to make sure that the orientation is correct.
var a = Bx - Ax
var asign = (a < 0 ? -1 : 1)
var b = By - Ay
var bsign = (b < 0 ? -1 : 1)
var theta = Math.atan(b / a)
// Find the point that's perpendicular to J on side
var costheta = asign * Math.cos(theta)
var sintheta = asign * Math.sin(theta)
// Find c and d
var c = M * sintheta
var d = M * costheta
// Use c and d to find Kx and Ky
var Kx = Jx - c
var Ky = Jy + d
return "M" + Ax + "," + Ay +
"Q" + Kx + "," + Ky +
" " + Bx + "," + By
}
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #ccc;
fill: none
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.min.js"></script>
<body>
<div id="svg-container">
</div>
</body>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With