1

I have a fully functional forced directed graph. I am trying to use directional arrowheads.

The size of each node is proportional to it's indegree and outdegree and the thickness of links varies according the below link property:

.attr("stroke-width",function(d) {return d.total_amt/60;})

I am trying to have the arrowheads in such a way that they are in proportion to the stroke width as well as the node size.

Using multiple markers is not the option as the variation in stroke width is not know as it is dependent on d.total_amount which is one of the link attributes.

So I am trying to mathematically compute the x2 and y2 values for the line:

               var nodeRadius = 20;
var lineX2 = function (d) {
    var length = Math.sqrt(Math.pow(d.target.y - d.source.y, 2) + Math.pow(d.target.x - d.source.x, 2));
    var scale = (length - nodeRadius) / length;
    var offset = (d.target.x - d.source.x) - (d.target.x - d.source.x) * scale;
    return d.target.x - offset;
};
var lineY2 = function (d) {
    var length = Math.sqrt(Math.pow(d.target.y - d.source.y, 2) + Math.pow(d.target.x - d.source.x, 2));
    var scale = (length - nodeRadius) / length;
    var offset = (d.target.y - d.source.y) - (d.target.y - d.source.y) * scale;
    return d.target.y - offset;
};



  var link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(d3GraphData.links)
    .enter().append("line")
    .attr("stroke-width",function(d) {return d.total_amt/60;})
    .attr("class", "link")
     .attr("x1", function (d) {
    return d.source.x;
})
    .attr("y1", function (d) {
    return d.source.y;
})
    .attr("x2", lineX2)
    .attr("y2", lineY2)
     .attr("marker-end", "url(#end)")

Below is the tick function for the nodes that uses lineX2 and lineY2

     function ticked() {
    link
      .attr("x1", function(d) {
        return d.source.x;
      })
      .attr("y1", function(d) {
        return d.source.y;
      })
        .attr("x2", lineX2)
        .attr("y2", lineY2)

    node
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      });
  }

Below is the marker definition:

svg.append("svg:defs").selectAll("marker")
    .data(["end"])      // Different link/path types can be defined here
  .enter().append("svg:marker")    // This section adds in the arrows
    .attr("id", String)
    .attr("viewBox", "0 0 10 10")
    .attr("refX", "4")
    .attr("refY", "3")
    .attr("markerUnits", "strokeWidth")
    .attr("markerWidth", "7")
    .attr("markerHeight", "2")
    .attr("orient", "auto")
  .append("svg:path")
   .attr("d", "M 0 0 L 10 5 L 0 10 z")

Right now this seems to partially work where the arrow heads do seem to be in proportion to the edge thickness.

But when the node sizes are small, the arrows don't seem to touch the outer edge of the node and terminate much earlier.

I did try using d3.scaleLinear() instead of nodeRadius in the computation of lineX2 and lineY2 but that made the whole graph very weird.

var minRadius = 5
       var maxRadius = 20
       var scale = (length - d3.scaleLinear().range([minRadius,maxRadius])) / length

weird graph

Also, even though the arrowheads seem to be proportional to the edge thickness, the console still throws the below error:

Error: <line> attribute x2: Expected length, "NaN".
(anonymous) @ d3.v4.min.js:2
797d3.v4.min.js:2 Error: <line> attribute y2: Expected length, "NaN". 

Below is the fiddle that demonstrates the problem

optimus_prime
  • 817
  • 2
  • 12
  • 34
  • Not an exact duplicate, but similar to the question [I answered here](http://stackoverflow.com/questions/41226734/align-marker-on-node-edges-d3-force-layout/41229068#41229068) – Mark Mar 02 '17 at 01:32
  • I will check it out tomorrow. But on taking a glance I found that you are using .Weight attribute which is not valid in d3 v4. – optimus_prime Mar 02 '17 at 03:00

1 Answers1

1

Taking a fresh look at this question this morning. Your math is very similar to what I use in the answer I linked in my comment. I think the issue is two fold:

  1. Your nodeRadius is fixed at 20 when sometimes the target node is as small as 5, this causes you to back-off too far
  2. You don't take into account the size of the marker in the "back-off".

A little trial and error led me to:

var lineX2 = function(d) {

  var mw = d.total_amt / 60, // marker width
  ms = Math.sqrt((mw * mw) + (mw * mw)) * 1.2; // marker size plus "fudge"

  var weight = d.target.inDegree ? d.target.inDegree : 0 + d.target.outDegree ? d.target.outDegree : 0;
  weight = weight >= 20 ? 20 : (weight < 5 ? 5 : weight);
  var r = weight + ms; // final radius

  var length = Math.sqrt(Math.pow(d.target.y - d.source.y, 2) + Math.pow(d.target.x - d.source.x, 2));
  var scale = (length - r) / length;
  var offset = (d.target.x - d.source.x) - (d.target.x - d.source.x) * scale;
      return d.target.x - offset;
};

Here it is running.

Mark
  • 106,305
  • 20
  • 172
  • 230