How can I dynamically change the position and the size of direction arrows on a directed d3.js force layout?

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.

Answers:

Answer

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

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.