4

My question is how to create a network visualization scheme such that the edges and/or arrowheads terminate at the borders of the nodes.

I am drawing a directed graph using D3.js based on the Curved Links base model with added "marker" arrowheads as described in this other question. The nodes in my visualization vary their size and opacity based on their properties. This introduces two problems: (1) The arrowheads do not point to the edge of the nodes when the nodes change size, and (2) the tails of the edges appear through the nodes when they are partially transparent.

For the first problem, there are a few solutions available: this one purports to get the arrow heads offset correctly, but it does not affect the link end terminations. There are also suggestions of solutions here, but I didn't see any actual complete working code there. This JS fiddle has exactly the arrowhead look that I'd like, but the code is rather opaque and not modular in a way I can figure out how to apply to my own case.

As I said, my links are defined based on the Curved Links example:

graph.links.forEach(function(link) {
    var s = nodes[link.source],
        t = nodes[link.target],
        i = {}, // intermediate node
    property1 = link.property1;
    nodes.push(i);
    links.push({source: s, target: i}, {source: i, target: t});
    bilinks.push([s, i, t, property1]);
});

Then, if my loose understanding of how D3 works is basically correct, the links are drawn each tick via the following code:

force.on("tick", function() {
  link.attr("d", function(d) {
    if (d[0] == d[2]) {
      return "M" + d[0].x + "," + d[0].y
        + "A" + "20,20 -50 1,1 " + (1.001 * d[2].x) + "," + (1.001 * d[2].y) 
        ;
    } else {
     return "M" + d[0].x + "," + d[0].y
        + "S" + d[1].x + "," + d[1].y
        + " " + d[2].x + "," + d[2].y;
    }
  });
  node.attr("transform", function(d) {
    return "translate(" + d.x + "," + d.y + ")";
  });
});

So my question is how to change this code in a way that achieves the generally desired (and I think normal) visualization scheme such that the edges and/or arrowheads terminate at the border of the nodes even as they change size.

I've created a JS Fiddle that includes all the necessary bits to see and solve the problem. It also includes an adjustment for getting the arrowheads to match the links they are on, and that capability needs to be compatible with the solution to this issue.

Community
  • 1
  • 1
Aaron Bramson
  • 1,176
  • 3
  • 20
  • 34
  • I guess, basically, you need to calculate the intersection point between the perimeter of the nodes and the links... dynamically. This is ok for straight lines, but for splined paths you would need to calculate the control point to get the right angle of approach as well. Does not sound like fun to me. – Cool Blue Oct 07 '15 at 09:17
  • Are the shapes always circles? – Ian Oct 08 '15 at 09:19
  • For my purposes the nodes will not always be circles, but if I can do it for circles then I can use the bounding circles around triangles and squares or whatever other shape. So it doesn't need to be exactly on the border: a little short of the border is better than a little too far. Essentially I just want to offset the ends of links by the "size" parameter of the nodes (not the "r" of the circle) on each end...and put arrowhead tips nearly flush with the tips of the links. – Aaron Bramson Oct 08 '15 at 10:16

2 Answers2

3

Since I wasn't getting any responses I went ahead and powered through answering my own question. As result, the answer I came up with is probably not the best because I'm still new to all this, but it works and it's similar to this answer...heavily adapted to handle the curved links and reflexive links.

The core of the necessary change is the following code:

force.on("tick", function() {
  link.attr("d", function(d) {
    diffX0 = d[0].x - d[1].x;
    diffY0 = d[0].y - d[1].y;
    diffX2 = d[2].x - d[1].x;
    diffY2 = d[2].y - d[1].y;
    pathLength01 = Math.sqrt((diffX0 * diffX0) + (diffY0 * diffY0));
    pathLength12 = Math.sqrt((diffX2 * diffX2) + (diffY2 * diffY2));
    offsetX0 = 1.00 * (diffX0 * d[0].group ) / pathLength01;
    offsetY0 = 1.00 * (diffY0 * d[0].group) / pathLength01;
    offsetX2 = (4.0 * (diffX2 / Math.abs(diffX2) )) + ((diffX2 * d[2].group) / pathLength12);
    offsetY2 = (4.0 * (diffY2 / Math.abs(diffY2) )) + ((diffY2 * d[2].group) / pathLength12);

    if (d[0] == d[2]) {
     return "M" + (d[0].x) + "," + (d[0].y - d[0].group)
        + "A" + "20,23 -50 1,1 "
        + " " + (d[2].x + (5.0 * 0.866) + (0.866 * d[2].group)) 
        + "," + (d[2].y + (5.0 * 0.5) + (0.5 * d[2].group));
    } else {
     return "M" + (d[0].x - offsetX0) + "," + (d[0].y - offsetY0)
        + "S" + (1.01 * d[1].x) + "," + (1.01 * d[1].y)
        + " " + (d[2].x - offsetX2) + "," + (d[2].y - offsetY2);
    }
  });
  node.attr("transform", function(d) {
    return "translate(" + d.x + "," + d.y + ")";
  });
});

This is designed to work with the arrowhead marker offset to 6 (i.e. .attr("refX", 6)) so that the end of the link is nearly in the middle of the arrowhead and the arrowhead extends about 4 units further toward the node. The arrowheads and the link tails are therefore offset by different amounts to the node border, so if you aren't using a directed graph you will need to adjust the target end's offset to match the source end and get them both right on the border.

Here is an updated JSFiddle that includes all the features necessary to do a directed force layout that includes:

  • Color circles by JSON node property with transparency
  • Color curved edges AND arrowheads by JSON link property
  • Links begin and end at node borders so they do not overlap the start/end nodes
  • Matches style for reflexive edges (that is, source and target are the same node)
  • Changing edge thickness and transparency is also supported

There are some other things that you'll want to tweak for your own application. For example, I added a radius variable to my node data that includes the proper scaling of the property (currently group) to the circle radius, then for the circle it's .attr("r", function(d) { return d.radius;}. I couldn't figure out a way to get the node's circle's r to use in the force function (which I prefer and I would love if somebody could figure out), so that was my work-around.

I think that wraps up a lot of visualization features that I expected to be standard for a tool like D3, but instead were impossible to find and somewhat difficult to implement. But now it's done, and I hope this will save some other people a lot of time in implementing directed networks in D3.

Community
  • 1
  • 1
Aaron Bramson
  • 1,176
  • 3
  • 20
  • 34
  • 1
    Your JSFiddle runs into a cross-origin scripting block. My fork: http://jsfiddle.net/spamguy/tz5oxfnL/ – spamguy Aug 25 '16 at 18:55
0

This has a very simple solution.

1) Draw your nodes at the last. So that your nodes will be on the top of the svg and will hide any section of links inside of it.

2) If you are updating your nodes and links, follow the same pattern and update your node at last.

Prasanna
  • 4,125
  • 18
  • 41
  • Nodes are often partially transparent, so this won't solve the problem. – Aaron Bramson Apr 16 '18 at 08:59
  • 1
    @AaronBramson in my case the nodes were solid, so after 3 days of frustration, I solved it with this solution and could find no other solution to work. I just answered here so that if in case anyone with solid nodes want the solution, they will have simple things to do – Prasanna Apr 17 '18 at 05:14