2

I am currently implementing arrows in my force layout as is done in this example (http://bl.ocks.org/mbostock/1153292) and that works perfectly. However, one will quickly realize that the location and size of the arrows are hard-coded here since the nodes never change size.

I have a graph were I dynamically change node sizes, and as such I would like the arrows to update accordingly since otherwise they are covered by the node or cover the node or are just not attached to the node.

There is only one post I have found (linking nodes of variable radius with arrows) that talks about this issue. However, it was not answered, and the answer that one poster gives of having the edges end at the radius of the node rather than the center is not something I want to do. It would require constantly recomputing edge positions, which given the number of edges I have is not practical.

I thought this would be relatively simple but haven't been able to figure it out. The change that I a currently working on is moving the marker creation below the generation of the nodes, since otherwise there is no ability to get node size data unless I want to run the size method I am using, which would be a massive waste of processing power (I have hundreds of nodes).

What I am trying (rough example, my code is a bit more complex)

var path = svg.append("svg:g").selectAll("path")
    .data(force.links())
  .enter().append("svg:path")
    .attr("class", function(d) { return d.target.nodeID; });

var circle = svg.append("svg:g").selectAll("circle")
    .data(force.nodes())
      .enter().append("svg:circle")
    .attr("r", nodeSize) //Dynamically determine size
    .call(force.drag);

// Per-node markers, as each node could potentially have a unique size
svg.append("svg:defs").selectAll("marker")
    .data(nodes, function(d) { return d.nodeID; })
  .enter().append("svg:marker")
    .attr("id", function(d) { return d.nodeID; })
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", function(d) { return d.r; }) //Offset by the radius of the node
    .attr("refY", 0)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
  .append("svg:path")
    .attr("d", "M0,-5L10,0L0,5");

//Now that we know radius data for nodes, add the arrows in
path.each(function(d) { 
    d.attr("marker-end", function(d) { return "url(#" + d.target.nodeID + ")"; })
});

Does anyone have an idea about the best way to go about this? Thanks in advance!

Update: per request, I have created a jsfiddle (http://jsfiddle.net/herbstmb/j5wJ7/). This is a basic force layout that I am trying to get the dynamic arrow sizes to work on.

Community
  • 1
  • 1
Matthew Herbst
  • 29,477
  • 23
  • 85
  • 128
  • Could you turn this code snippet into a working [fiddle](http://jsfiddle.net/)? It looks like not much is missing, and would make it easier for others to try to figure out a solution. – Jake Aug 06 '13 at 01:50
  • I've created one. Check the update in the post. It's a very basic force layout that I'm just trying to add the dynamically sized arrows to. – Matthew Herbst Aug 06 '13 at 05:35
  • 2
    The syntax error console is complaining about is on the previous line where you open a string with a single-quote and close it with a double-quote. Regarding your issue, I noticed you made a couple of comments about discarding potential solutions because of potential performance issues running on hundreds of nodes. You should probably give the simplest solution a try and test it out before discarding it. You'd be surprised how much you can do against a couple of hundreds points in a split second. Browsers are fast these days (even mobile, relatively speaking). – Scott Cameron Aug 06 '13 at 05:58
  • Thanks for the find. I do appreciate trying the simplest solution, and I actually believe that the one I am trying (even though I don't know how to do it) is that solution. The visualization already runs a little slow for me honestly, so I've been trying to find ways to improve the speed. Adding this new feature in is something that I hope will have little impact. I'll give it a try though! – Matthew Herbst Aug 06 '13 at 17:18

1 Answers1

5

This is an old question, but here is my solution. The idea is to draw the path connecting the nodes such that the end points are on the nodes' edges rather than at the nodes' centers. Starting from the Mobile Patent Suits example (http://bl.ocks.org/mbostock/1153292), I replaced the linkArc method with:

function drawCurve(d) {
    var sourceX = d.source.x;
    var sourceY = d.source.y;
    var targetX = d.target.x;
    var targetY = d.target.y;

    var theta = Math.atan((targetX - sourceX) / (targetY - sourceY));
    var phi = Math.atan((targetY - sourceY) / (targetX - sourceX));

    var sinTheta = d.source.r * Math.sin(theta);
    var cosTheta = d.source.r * Math.cos(theta);
    var sinPhi = d.target.r * Math.sin(phi);
    var cosPhi = d.target.r * Math.cos(phi);

    // Set the position of the link's end point at the source node
    // such that it is on the edge closest to the target node
    if (d.target.y > d.source.y) {
        sourceX = sourceX + sinTheta;
        sourceY = sourceY + cosTheta;
    }
    else {
        sourceX = sourceX - sinTheta;
        sourceY = sourceY - cosTheta;
    }

    // Set the position of the link's end point at the target node
    // such that it is on the edge closest to the source node
    if (d.source.x > d.target.x) {
        targetX = targetX + cosPhi;
        targetY = targetY + sinPhi;    
    }
    else {
        targetX = targetX - cosPhi;
        targetY = targetY - sinPhi;   
    }

    // Draw an arc between the two calculated points
    var dx = targetX - sourceX,
        dy = targetY - sourceY,
        dr = Math.sqrt(dx * dx + dy * dy);
    return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY;
}

Note that this code expects an "r," or radius, attribute to be in the node data. This would be equivalent to the size attribute in your jsfiddle. If you want to draw links as straight lines, you can return this string instead:

return "M" + sourceX + "," + sourceY + "L" + targetX + "," + targetY;

To place the points of the arrows at the correct positions, I changed the refX and refY attributes so that the point of the arrow is at the edge of the node:

svg.append("defs").selectAll("marker")
    .data(["suit", "licensing", "resolved"])
  .enter().append("marker")
    .attr("id", function(d) { return d; })
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 10)
    .attr("refY", 0)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
  .append("path")
    .attr("d", "M0,-5L10,0L0,5");
silentfuzzle
  • 159
  • 2
  • 8