Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js: How to convert edges from lines to curved paths in a network visualization by drawing a quadratic Bezier curve?

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:

Example diagram of spline math

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.

like image 685
rjurney Avatar asked Dec 14 '22 20:12

rjurney


1 Answers

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:

  1. Find your A-B midpoint, call it J.
  2. Do some trig to find the point at the end of line segment M, call it K
  3. Use the SVG Q or T path commands to draw the quadratic Bezier curve, starting from A, going to B, with the control point K. (note that this won't look exactly like your diagram, but that can be tuned by changing the length of M).

enter image description here

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>
like image 181
Nick Young Avatar answered Dec 29 '22 00:12

Nick Young