Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get arrowheads to point at outer edge of node in D3

Tags:

svg

d3.js

I'm new to D3 and I'm trying to create an interactive network visualization. I've copied large parts of this example, but I have changed the curved lines to straight ones by using SVG "lines" rather than "paths", and I've also scaled the nodes according to the data they represent. The problem is that my arrowheads (created with SVG markers) are at the ends of the lines. Since some of the nodes are large, the arrows get hidden behind them. I'd like my arrowheads to show up right at the outside edge of the node they point to.

Here is how I'm creating the markers and links:

svg.append("svg:defs").selectAll("marker")
    .data(["prereq", "coreq"])
    .enter().append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 15)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M0,-5L10,0L0,5");

var link = svg.selectAll(".link")
    .data(force.links())
    .enter().append("line")
    .attr("class", "link")
    .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });

I noticed that the "refX" attribute specifies how far from the end of the line the arrowhead should show up. How can I make this dependent on the radius of the node it's pointing to? If I can't do that, could I instead change the endpoints of the lines themselves? I'm guessing I would do that in this function, which resets the endpoints of the lines as everything moves:

function tick() {
        link
            .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; });

        circle.attr("transform", function(d) {
            return "translate(" + d.x + "," + d.y + ")";
        });

        text.attr("transform", function(d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    }

Which approach makes more sense, and how would I implement it?

like image 923
FrancesKR Avatar asked May 21 '13 00:05

FrancesKR


2 Answers

Thanks Lars Kotthoff, I got this to work following the advice from the other question! First I switched from using lines to paths. I don't think I actually had to do that, but it made it easier to follow the other examples I was looking at because they used paths.

Then, I added a "radius" field to my nodes. I just did this when I set the radius attribute, by adding it as an actual field rather than returning the value immediately:

var circle = svg.append("svg:g").selectAll("circle")
                    .data(force.nodes())
                    .enter().append("svg:circle")
                    .attr("r", function(d) {
                        if (d.logic != null) {
                            d.radius = 5;
                        } else {
                            d.radius = node_scale(d.classSize);
                        }
                        return d.radius;

I then edited my tick() function to take this radius into account. This required a bit of simple geometry...

function tick(e) {

        path.attr("d", function(d) {
            // Total difference in x and y from source to target
            diffX = d.target.x - d.source.x;
            diffY = d.target.y - d.source.y;

            // Length of path from center of source node to center of target node
            pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));

            // x and y distances from center to outside edge of target node
            offsetX = (diffX * d.target.radius) / pathLength;
            offsetY = (diffY * d.target.radius) / pathLength;

            return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
        });

Basically, the triangle formed by the path, it's total x change (diffX), and it's total y change (diffY) is a similar triangle to that formed by the segment of the path inside the target node (i.e. the node radius), the x change inside the target node (offsetX), and the y change inside the target node (offsetY). This means that the ratio of the target node radius to the total path length is equal to the ratio of offsetX to diffX and to the ratio of offsetY to diffY.

I also changed the refX value to 10 for the arrows. I'm not sure why that was necessary but now it seems to work!

like image 197
FrancesKR Avatar answered Nov 17 '22 04:11

FrancesKR


I answered the same question over here. The answer uses vector math, it's quite useful for other calculations as well.

like image 2
andsens Avatar answered Nov 17 '22 04:11

andsens