Clarifications in docs about graph_draw edge_control_points and splines?


Just putting this out there as I struggled with edge splines myself.

In the section about graph_tool.draw.graph_draw, in the list about edges properties, the doc has this to say about splines:

Control points of a Bézier spline used to draw the edge. Each spline segment requires 6 values corresponding to the (x,y) coordinates of the two intermediary control points and the final point.

There exist several different kinds of Bézier splines. In the case of graph-tool+cairo, these appear to be composite cubic Bézier splines. It became easier to understand the concept of control points once this was known to me.

Further down the graph_draw example uses the following control points

>>> for e in g.edges():
... d = sqrt(sum((pos[e.source()].a - pos[].a) ** 2)) / 5
... control[e] = [0.3, d, 0.7, d]

but the output doesn’t show any curvature for the edges. Changing the control point assignments to

... control[e] = [0.0, 0.0, 0.3, d, 0.7, d, 1.0, 0.0]

produces the desired curvature.

I might understand this incorrectly but in addition to the 3 pairs (x,y) for two control+end points it seems like the first two value in array should be the coordinates of the initial point, most likely (0, 0). Omitting those always produces no or unexpected results in my case. The control point array would thus supposedly hav the structure

control_points[e] = [x0, y0, s1cp1x, s1cp1y, s1cp2x, s1cp2y, s1endx, s1endy, s2cp1x, s2cp1y, …, sNcpendx, sNcpendy]

where si strands for segment i and cpj for control point j.

The following script shows how a spline segment is drawn. Copy-paste in an IPython notebook should work directly.

%matplotlib inline
import graph_tool.all as gt
from numpy import array, sqrt, dot
from scipy.interpolate import interp1d

# Create property maps and define some function
foo = gt.Graph()
foopos = foo.new_vertex_property('vector<double>')
edge_ctlpts = foo.new_edge_property('vector<double>')
edge_width = foo.new_edge_property('double')
edge_color = foo.new_edge_property('vector<double>')
vertex_fill_color = foo.new_vertex_property('vector<double>')
vertex_size = foo.new_vertex_property('double')
vertex_text = foo.new_vertex_property('string')

def add_vertex(pos, size, color, text):
    i = foo.add_vertex()
    foopos[i] = pos
    vertex_size[i] = size
    vertex_fill_color[i] = color
    vertex_text[i] = text
    return i

def add_vertex_and_edge(pos0, text0, pos1, text1, vsize, vfillcolor, epenwidth, ecolor):
    i0 = add_vertex(pos0, vsize, vfillcolor, text0)
    i1 = add_vertex(pos1, vsize, vfillcolor, text1)
    ei = foo.add_edge(i0, i1)
    edge_width[ei] = epenwidth
    edge_color[ei] = ecolor
    return (i0, i1, ei)

# Create two vertex, edge and its composite cubic Bézier control points
v0, v1, e01 = add_vertex_and_edge(pos0=[1, 4], text0='', pos1=[5, 2], text1='',
                                  vsize=10, vfillcolor=[0.640625, 0, 0, 0.9],
                                  epenwidth=4, ecolor=[0.179, 0.203,0.210, 0.4])
ctlpts = array([[0.0, 0.0], [0.2, -0.5], [0.3, 0.5], [0.5, 0.0], [0.7, -0.9], [0.8, 2.0], [1.0, 0.0]])
edge_ctlpts[e01] = ctlpts.flatten()

# Define fonction to convert from the relative coordinates
# of a control points to absolute vertex coordinates
def rel2abs(absx0y0, absx1y1, relxy):
    xdir = absx1y1 - absx0y0
    norm = sqrt(dot(xdir, xdir))
    ydir = dot([[0, -1.0/norm], [1.0/norm, 0]], xdir)
    return absx0y0 + xdir*relxy[0] + ydir*relxy[1]

# Draw the control points and end points for each segment
# together with the interpolation lines between control points
v = v0
for p in ctlpts:
    vlast, v = v, foo.add_vertex()
    e = foo.add_edge(vlast, v)
    foopos[v] = rel2abs(foopos[v0].a, foopos[v1].a, p)
    vertex_fill_color[v] = [1, 1, 1, 0]
    vertex_size[v] = 15
    edge_width[e] = 2
    edge_color[e] = [0.179, 0.203,0.210, 0.6]
    vertex_text[v] = repr(p.tolist())

### Drawing the spline ###
# The control points we've selected above create two spline
#segments each comprised of a cubic Bézier spline with
# two control points and an end point.

# We will focus on the second segment only and we will plot
# all the intermediate steps that lead to the position of
# the tip that draws its smooth path.
# The parameter t runs from 0 to 1 and will control the progression
# of the drawing tip from the beginning of the spline segment to its end.
t = 0.6

# To draw a cubic Bézier spline, we first need three interpolators:
# * from the initial point to the 1st control point,
# * from the first control point to the 2nd control point,
# * and from the 2nd control point to the end point.
f2a1 = interp1d([0, 1], array([ctlpts[3], ctlpts[4]]).T)
f2a2 = interp1d([0, 1], array([ctlpts[4], ctlpts[5]]).T)
f2a3 = interp1d([0, 1], array([ctlpts[5], ctlpts[6]]).T)

# Then we need two more interpolators:
# * between the values of the first and second interpolator from above,
# * and between the values of the second and third interpolator from above.
f2b1 = interp1d([0, 1], array([f2a1(t), f2a2(t)]).T)
f2b2 = interp1d([0, 1], array([f2a2(t), f2a3(t)]).T)

# Finally, the drawing tip that will actually trace
# the spline comprise a last interpolator between the above two.
f2c = interp1d([0, 1], array([f2b1(t), f2b2(t)]).T)

# With these 6 interpolators we can now draw
# the state of each intermedeate step, all driven by the parameter t.
#Draw the first order interpolators. These will appear green on the final plot
add_vertex_and_edge(pos0=rel2abs(foopos[v0].a, foopos[v1].a, f2a1(t)), text0='',
                    pos1=rel2abs(foopos[v0].a, foopos[v1].a, f2a2(t)), text1='',
                    vsize=5, vfillcolor=[0, 0, 0, 1],
                    epenwidth=1, ecolor=[0.179, 0.703, 0.210, 0.8])
add_vertex_and_edge(pos0=rel2abs(foopos[v0].a, foopos[v1].a, f2a2(t)), text0='',
                    pos1=rel2abs(foopos[v0].a, foopos[v1].a, f2a3(t)), text1='',
                    vsize=5, vfillcolor=[0, 0, 0, 1],
                    epenwidth=1, ecolor=[0.179, 0.703, 0.210, 0.8])

# the second degree interpolators. These will be blue.
add_vertex_and_edge(pos0=rel2abs(foopos[v0].a, foopos[v1].a, f2b1(t)), text0='',
                    pos1=rel2abs(foopos[v0].a, foopos[v1].a, f2b2(t)), text1='',
                    vsize=5, vfillcolor=[0, 0, 0, 1],
                    epenwidth=1, ecolor=[0.179, 0.203, 0.710, 0.8])
# and finally the third and last degree. This is the drawing tip tracing the actual spline.
# It will be plotted as a solid black dot.
i0 = foo.add_vertex()
foopos[i0] = rel2abs(foopos[v0].a, foopos[v1].a, f2c(t))
vertex_size[i0] = 9
vertex_fill_color[i0] = [0, 0, 0, 1]
vertex_text[i0] = 't='+repr(t)

# Draw the graph
gt.graph_draw(foo, pos=foopos,
              edge_control_points=edge_ctlpts, edge_pen_width = edge_width, edge_color=edge_color,
              vertex_size=vertex_size, vertex_fill_color=vertex_fill_color, vertex_anchor=1,
              vertex_text=vertex_text, vertex_text_position=0, vertex_text_offset=[-0.07, -0.1],
              inline=True, output_size=(600, 600), fit_view=(0, 0, 6, 6))


Ping to Tiago.

I just discovered this issue myself after struggling for a bit. Would you
please edit the documentation example in
to reflect this?