Search code examples
d3.jsgraphvizdotsplinecubic-spline

How to draw a graphviz spline in d3


Background

graphviz is great for laying out graphs. When I layout my graph with graphviz "dot" it produces a file with nodes and edges tagged with positions. E.g. an edge:

"a" -> "b"   [pos="e,152.13,411.67 91.566,463.4 108.12,449.26 127.94,432.34 144.37,418.3"];

The Graphviz spline language is the following:

 spline       =   (endp)? (startp)? point (triple)+
 and triple   =   point point point
 and endp     =   "e,%f,%f"
 and startp   =   "s,%f,%f"

If a spline has points p1 p2 p3 ... pn, (n = 1 (mod 3)), the points correspond to the control points of a cubic B-spline from p1 to pn. If startp is given, it touches one node of the edge, and the arrowhead goes from p1 to startp. If startp is not given, p1 touches a node. Similarly for pn and endp.

So decomposing the string above,

e,152.13,411.67   // This is an endpoint. 
91.566,463.4      // p1
108.12,449.26     // the following three points are control points for a cubic b-spline
127.94,432.34
144.37,418.3

Question

I want to draw this spline via d3. d3 seems to have cubic b-splines with the following signature:

d3.curveBasis(context)

Produces a cubic basis spline using the specified control points. The first and last points are triplicated such that the spline starts at the first point and ends at the last point, and is tangent to the line between the first and second points, and to the line between the penultimate and last points.

So how do I take the string

[pos="e,152.13,411.67 91.566,463.4 108.12,449.26 127.94,432.34 144.37,418.3"];

And draw the spline that graphviz would draw in d3?


Solution

  • Turns out I was overthinking this.

    If you move the e (endpoint) to the end of the array of points, d3.curveBasis() renders this perfectly.

    # G is a networkx graph and A is a pygraphviz graph
    for e in G.edges():
        points = A.get_edge(e[0], e[1]).attr["pos"].lstrip('e,').split(' ')
        points.append(points.pop(0))
        G.edges[e[0], e[1]].update({'points': [{'x':float(p.split(',')[0]), 'y':float(p.split(',')[1])} for p in points]})
    

    And later:

    var lineGenerator = d3.line().y(d => d.y).x(d => d.x).curve(d3.curveBasis);
    
    link.enter()
        .append('path')
        .attr('d', l => lineGenerator(l.points))