Align marker on node edges of D3 Force Layout

I am trying to implement a d3 strength mockup and cannot figure out how to correctly position my link markers.

Here is what I got so far:

var links = force_data.force_directed_data.links; var nodes = {}; // Compute the distinct nodes from the links. links.forEach(function (link) { link.source = nodes[link.source] || (nodes[link.source] = {name: link.source}); link.target = nodes[link.target] || (nodes[link.target] = {name: link.target}); }); console.log(nodes); var width = 1000, height = 1000; var force = d3.layout.force() .nodes(d3.values(nodes)) .links(links) .size([width, height]) .linkDistance(300) .charge(-120) .friction(0.9) .on("tick", tick) .start(); var svg = d3.select("#force-graph").append("svg") .attr("width", width) .attr("height", height); // Per-type markers, as they don't inherit styles. svg.append("defs").selectAll("marker") .data(["dominating"]) .enter().append("marker") .attr("id", function (d) { return d; }) .attr("viewBox", "0 -5 10 10") .attr("refX", 15) .attr("refY", -1.5) .attr("markerWidth", 12) .attr("markerHeight", 12) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5"); svg.append("defs").selectAll("marker") .data(["concomidant"]) .enter().append("marker") .attr("id", function (d) { return d; }) .attr("viewBox", "0 -5 10 10") .attr("refX", 15) .attr("refY", -1.5) .attr("markerWidth", 12) .attr("markerHeight", 12) .attr("orient", "auto-start-reverse") .append("path") .attr("d", "M0,-5L10,0L0,5"); var path = svg.append("g").selectAll("path") .data(force.links()) .enter().append("path") .attr("class", function (d) { return "link " + d.type; }) .attr("marker-end", function (d) { return "url(#" + d.type + ")"; }) .attr("marker-start", function (d) { if (d.type == "concomidant") { return "url(#" + d.type + ")"; } }); var circle = svg.append("g").selectAll("circle") .data(force.nodes()) .enter().append("circle") .attr("r", function (d) { return d.weight * 4; }) .call(force.drag); var text = svg.append("g").selectAll("text") .data(force.nodes()) .enter().append("text") .attr("x", 8) .attr("y", ".31em") .text(function (d) { return d.name; }); // Use elliptical arc path segments to doubly-encode directionality. function tick() { path.attr("d", linkArc); circle.attr("transform", transform); text.attr("transform", transform); } function linkArc(d) { var dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; } function transform(d) { return "translate(" + dx + "," + dy + ")"; } 

It works correctly to add different types of markers for different links, but if the nodes grow (depending on their weight), the markers are covered with nodes.

Here is a screenshot of what it looks like right now:

D3 Force Graph

Is there a way to place my markers exactly on the edge of the nodes?

+3
javascript force-layout
Dec 19 '16 at 16:11
source share
1 answer

Interest Ask. This works by setting the channel path usually, and then recounting the final position, pushing the length away with the radius of the circle.

First, in the marker definition, set refX and refY to 0 (this is the current path that it remains outside the circle):

  .attr("refX", 0) .attr("refY", 0) 

Then do:

 function tick() { // fit path like you've been doing path.attr("d", function(d){ var dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; }); // recalculate and back off the distance path.attr("d", function(d) { // length of current path var pl = this.getTotalLength(), // radius of circle plus marker head r = (d.target.weight) * 4 + 16.97, //16.97 is the "size" of the marker Math.sqrt(12**2 + 12 **2) // position close to where path intercepts circle m = this.getPointAtLength(pl - r); var dx = mx - d.source.x, dy = my - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + mx + "," + my; }); circle.attr("transform", transform); text.attr("transform", transform); } 

Launch Code:

 <!DOCTYPE html> <html> <head> <script data-require="d3@3.5.17" data-semver="3.5.17" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script> <style> path.link { fill: none; stroke: #666; stroke-width: 1.5px; } circle { fill: #ccc; stroke: #fff; stroke-width: 1.5px; } text { fill: #000; font: 10px sans-serif; pointer-events: none; } </style> </head> <body> <script> var links = [{ "source": "Harry", "target": "Sally", "value": "1.2" }, { "source": "Harry", "target": "Mario", "value": "1.3" }, { "source": "Sarah", "target": "Alice", "value": "0.2" }, { "source": "Eveie", "target": "Alice", "value": "0.5" }, { "source": "Peter", "target": "Alice", "value": "1.6" }, { "source": "Mario", "target": "Alice", "value": "0.4" }, { "source": "James", "target": "Alice", "value": "0.6" }, { "source": "Harry", "target": "Carol", "value": "0.7" }, { "source": "Harry", "target": "Nicky", "value": "0.8" }, { "source": "Bobby", "target": "Frank", "value": "0.8" }, { "source": "Alice", "target": "Mario", "value": "0.7" }, { "source": "Harry", "target": "Alice", "value": "0.5" }, { "source": "Sarah", "target": "Alice", "value": "1.9" }, { "source": "Roger", "target": "Alice", "value": "1.1" }]; var nodes = {}; // Compute the distinct nodes from the links. links.forEach(function(link) { link.type = "dominating"; link.source = nodes[link.source] || (nodes[link.source] = { name: link.source }); link.target = nodes[link.target] || (nodes[link.target] = { name: link.target }); }); var width = 500, height = 500; var force = d3.layout.force() .nodes(d3.values(nodes)) .links(links) .size([width, height]) .linkDistance(300) .charge(-120) .friction(0.9) .on("tick", tick) .start(); var svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height); // Per-type markers, as they don't inherit styles. svg.append("defs").selectAll("marker") .data(["dominating"]) .enter().append("marker") .attr("id", function(d) { return d; }) .attr("viewBox", "0 -5 10 10") .attr("refX", 0) .attr("refY", 0) .attr("markerWidth", 12) .attr("markerHeight", 12) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5"); svg.append("defs").selectAll("marker") .data(["concomidant"]) .enter().append("marker") .attr("id", function(d) { return d; }) .attr("viewBox", "0 -5 10 10") .attr("refX", 0) .attr("refY", 0) .attr("markerWidth", 12) .attr("markerHeight", 12) .attr("orient", "auto-start-reverse") .append("path") .attr("d", "M0,-5L10,0L0,5"); var path = svg.append("g").selectAll("path") .data(force.links()) .enter().append("path") .attr("class", function(d) { return "link " + d.type; }) .attr("marker-end", function(d) { return "url(#" + d.type + ")"; }) .attr("marker-start", function(d) { if (d.type == "concomidant") { return "url(#" + d.type + ")"; } }); var circle = svg.append("g").selectAll("circle") .data(force.nodes()) .enter().append("circle") .attr("r", function(d) { return d.weight * 4; }) .call(force.drag); var text = svg.append("g").selectAll("text") .data(force.nodes()) .enter().append("text") .attr("x", 8) .attr("y", ".31em") .text(function(d) { return d.name; }); function tick() { path.attr("d", function(d){ var dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; }); path.attr("d", function(d) { var pl = this.getTotalLength(), r = (d.target.weight) * 4 + 16.97, //16.97 is the "size" of the marker Math.sqrt(12**2 + 12 **2) m = this.getPointAtLength(pl - r); var dx = mx - d.source.x, dy = my - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + mx + "," + my; }); circle.attr("transform", transform); text.attr("transform", transform); } function transform(d) { return "translate(" + dx + "," + dy + ")"; } </script> </body> </html> 
+3
Dec 19 '16 at 18:48
source share



All Articles