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 fromp1
topn
. Ifstartp
is given, it touches one node of the edge, and the arrowhead goes fromp1
tostartp
. Ifstartp
is not given,p1
touches a node. Similarly forpn
andendp
.
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
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?
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))