Scaling an arrowhead on a D3 force layout link marker
d3.js, force-layout, javascript, markers, svg
Solution
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;
});
});
Problem
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"); ```