The Mathematics Behind Communication Explorer Arrows

Creating lines to represent one way communication.

D3 can draw a line between nodes using their force directed layout. Let’s call these two points (x1, y1) and (x2, y2). Now we need to determine the point (Xp, Yp) to plot the arrow at. We can identify the coordinates using trigonometry.

Note: boundarySpace is the amount of additional space we want between the edge of the target circle and the end of the line, which is set to 8px (large enough to accommodate a marker).


renderLinks: function(lines, settings){
    var that = this;
    lines.attr("x1", function(d) { return d.source.x.toFixed(2); })
        .attr("y1", function(d) { return d.source.y.toFixed(2); })
        .attr("x2", function(d) { return that.getLinkEndPoint(d, settings)[0].toFixed(2); })
        .attr("y2", function(d) { return that.getLinkEndPoint(d, settings)[1].toFixed(2); });
getLinkEndPoint: function(d, settings){
    var theta = Math.atan2( - d.source.y, - d.source.x),
    Py = parseInt(, 10) - ( + settings.boundarySpace) * Math.sin(theta),
    Px = parseInt(, 10) - ( + settings.boundarySpace) * Math.cos(theta);
    return [Py, Px];

Creating arcs to represent two way communication.
D3 can also draw an arc between the center point of two nodes. Lets call these points (x1, y1) and (x2, y2). Since D3 cannot draw an arrow on an arc for any possible target circle radius, we need to plot a line at the point (P1x, P1y) where the arc intersects the target circle and put the arrow on line instead of the arc. The point (P1x, P1y) can be determined using the native svg function getPointAtLength.

Next, we get another point along the arc at a very small distance away (PSx, PSy). Now that two points are known, the slope can be calculated and the line extended to point (P2x, P2y).


plotSlopeLinesAndArrows: function(paths, settings){
    var that = this;
    paths.each(function(l, i) {
        var pathLength = this.getTotalLength() - (parseInt(, 10) + settings.boundarySpace);
        if(pathLength > 0){
            var p1 = this.getPointAtLength(pathLength),
            pS = this.getPointAtLength(pathLength - 5),
            p2 = that.getSlopeLineEndPoint(point1, point2),
            strokeW = that.calculateLinkWidth(l.quantity),
            path = "M" + endPoint.x.toFixed(2) + "," + endPoint.y.toFixed(2) + " L" + point1.x.toFixed(2) +","+ point1.y.toFixed(2);
            $("#slope" + i).attr("d", path).attr("marker-end", function(d) {
                var marker = null;
                if( l.source.state === "hover" || l.source.state === "clicked" )    {
                    marker = that.getMarker("A", strokeW, true);
                    marker = that.getMarker("A", strokeW);
                return "url(#" + marker + ")";
            that.renderPhantomLink(l, i, point1);

getSlopeLineEndPoint: function(p1, p2){
     var slopeLineLength = 90,
         slope = (p1.y - p2.y) / (p1.x - p2.x),
         theta = Math.atan2(slope, 1),
         x2 = 0,
         y2 = 0;
     if(p1.x - p2.x > 0){
         y2 = p1.y - (slopeLineLength * Math.sin(theta));
         x2 = p1.x - (slopeLineLength * Math.cos(theta));
         y2 = p1.y + (slopeLineLength * Math.sin(theta));
         x2 = p1.x + (slopeLineLength * Math.cos(theta));
     return {'x': x2, 'y': y2};

Debug Mode:
All of this can be seen in action by looking at the GIF below. The red lines are the slope lines, which are drawn at edge of the target circle when there is two way communication. A marker is placed on them to give the desired effect..


Final Appearance:
By changing the CSS to hide the slope lines and the original arcs, the desired appearance can be created.


Taking a closer look, we can see that we have achieved a clean look between the end of the arcs, their marker, and the target circle for all possible radius and stroke sizes.