Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scaling an arrowhead on a D3 force layout link marker

I have the following code in a d3 force directed graph where I'm trying to vary the size of the links and their associated arrowheads based on a value (from 1-3). The stroke weight does change with the value but the arrowheads does not stay in the correct position. It tends to shift back from the end when the stroke weight changes from say 1 to a 3. Any ideas on how to keep the arrowheads (markers) properly aligned when changing the stroke value? Many thanks!

      var link = vis.selectAll("line.link")
      .data(json.links)
    .enter().append("svg:line")
      .attr("class", "link")
      .style("stroke-width", function(d) { return Math.sqrt(d.value); })
      .attr("x1", function(d) { return d.source.x; })
      .attr("y1", function(d) { return d.source.y; })
      .attr("x2", function(d) { return d.target.x; })
      .attr("y2", function(d) { return d.target.y; })
      .attr("marker-end", "url(#arrowGray)")
      .on("click", function(d) {
            link.style("stroke","#dddddd");
            node.style("stroke","#FFFFFF");
            d3.select(this).style("stroke","red");
            link.attr("marker-end", null);
            link.attr("marker-end", "url(#arrowGray)");
            d3.select(this).attr("marker-end", null);
            d3.select(this).attr("marker-end", "url(#arrowRed)");
            clickLink(d);
            });

    defs.append("svg:marker")
            .attr("id", "arrowGray")
            .attr("viewBox","0 0 10 10")
            .attr("refX","20")
            .attr("refY","5")
            .attr("markerUnits","strokeWidth")
            .attr("markerWidth","9")
            .attr("markerHeight","5")
            .attr("orient","auto")
            .append("svg:path")
            .attr("d","M 0 0 L 10 5 L 0 10 z")
            .attr("fill", "#BBBBBB");
like image 384
JHolmes Avatar asked Jun 20 '12 14:06

JHolmes


2 Answers

The Problem

This jsFiddle demonstrates the problem in question. The values of the marker def are in relation to the stoke width of the element it is attached to (the link lines in this case). See the markerUnits spec. By using strokeWidth for the markerUnits attribute the coordinates for the different size arrows will be slightly different. In short the appropriate values for one size arrow will not correctly translate to the other sizes.

Solution 1: Multiple Markers

As mentioned in the comments one solution would be to create a different marker for each strokeWidth that's needed. This would work only if you knew up front what sizes were needed.

Solution 2: Modify the Lines

Another option would be to modify the end point for the lines. Instead of allowing the lines to terminate in the center of a node, terminate it on the outside edge of the node. This jsFiddle demonstrates this. This alleviates the need to shift the arrow head, as we can now just draw it at the end of the line.

This solution involves some math to figure out what the x2 and y2 values of the line should be. Thus, it may not be ideal for systems with a large number of edges.

var nodeRadius = 10;
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.selectAll("line.link")
    .data(graph.links)
    .enter().append("svg:line")
    .attr("class", "link")
    .style("stroke-width", function (d) {
      return Math.sqrt(d.value);
    })
    .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(#arrowGray)")
    .on("click", function (d) {
        link.style("stroke", "#dddddd");
        node.style("stroke", "#FFFFFF");
        d3.select(this).style("stroke", "red");
        link.attr("marker-end", null);
        link.attr("marker-end", "url(#arrowGray)");
        d3.select(this).attr("marker-end", null);
        d3.select(this).attr("marker-end", "url(#arrowRed)");
    });

var defs = svg.append('defs');
defs.append("svg:marker")
    .attr("id", "arrowGray")
    .attr("viewBox", "0 0 10 10")
    .attr("refX", "10")
    .attr("refY", "5")
    .attr("markerUnits", "strokeWidth")
    .attr("markerWidth", "10")
    .attr("markerHeight", "5")
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M 0 0 L 10 5 L 0 10 z")
    .attr("fill", "#000");

var node = svg.selectAll(".node")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("class", "node")
    .attr("r", nodeRadius)
    .style("fill", function (d) {
        return color(d.group);
    })
    .call(force.drag);

node.append("title")
    .text(function (d) {
        return d.name;
    });

force.on("tick", function () {
    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;
    });
});
like image 102
Jonathan Dixon Avatar answered Nov 05 '22 07:11

Jonathan Dixon


Your refX is probably too large. Try setting it to something around 1.

like image 34
ZachB Avatar answered Nov 05 '22 07:11

ZachB