Commit aa38e9f0 authored by Tiago Peixoto's avatar Tiago Peixoto
Browse files

Ditch graphviz python module in favor of libgvc + ctypes

By using directly the C bindings to graphviz, we are able to return the
drawed graph as a string buffer in whatever format, which was not
possible with the SWIGified python interface.

Returning the drawed graph as a buffer is useful in doing animations,
among other things.
parent 7bedab86
...@@ -36,41 +36,77 @@ Contents ...@@ -36,41 +36,77 @@ Contents
++++++++ ++++++++
""" """
import sys, os, os.path, time, warnings, tempfile import sys
import os
import os.path
import time
import warnings
import ctypes
import ctypes.util
from .. import _degree, _prop, PropertyMap, _check_prop_vector,\ from .. import _degree, _prop, PropertyMap, _check_prop_vector,\
_check_prop_scalar, _check_prop_writable, group_vector_property,\ _check_prop_scalar, _check_prop_writable, group_vector_property,\
ungroup_vector_property ungroup_vector_property
from .. decorators import _limit_args from .. decorators import _limit_args
import numpy.random import numpy.random
from numpy import * from numpy import *
import copy
from .. dl_import import dl_import from .. dl_import import dl_import
dl_import("import libgraph_tool_layout") dl_import("import libgraph_tool_layout")
try:
import gv
except ImportError:
warnings.warn("error importing gv module... graph_draw() will not work.",
ImportWarning)
try: try:
import matplotlib.cm import matplotlib.cm
import matplotlib.colors import matplotlib.colors
from pylab import imread
except ImportError: except ImportError:
warnings.warn("error importing matplotlib module... " + \ warnings.warn("error importing matplotlib module... " + \
"graph_draw() will not work.", ImportWarning) "graph_draw() will not work.", ImportWarning)
try:
libname = ctypes.util.find_library("c")
libc = ctypes.CDLL(libname)
libc.open_memstream.restype = ctypes.POINTER(ctypes.c_char)
except OSError:
pass
try:
libname = ctypes.util.find_library("gvc")
if libname is None:
raise OSError()
libgv = ctypes.CDLL(libname)
# properly set the return types of certain functions
ptype = ctypes.POINTER(ctypes.c_char)
libgv.gvContext.restype = ptype
libgv.agopen.restype = ptype
libgv.agnode.restype = ptype
libgv.agedge.restype = ptype
libgv.agget.restype = ptype
# create a context to use the whole time (if we keep freeing and recreating
# it, we will hit a memory leak in graphviz)
gvc = libgv.gvContext()
except OSError:
warnings.warn("error importing graphviz C library (libgvc)... " + \
"graph_draw() will not work.", ImportWarning)
__all__ = ["graph_draw", "arf_layout", "random_layout"] __all__ = ["graph_draw", "arf_layout", "random_layout"]
def aset(elem, attr, value):
v = str(value)
libgv.agsafeset(elem, str(attr), v, v)
def aget(elem, attr):
return ctypes.string_at(libgv.agget(elem, str(attr)))
def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
ratio="fill", overlap="prism", sep=None, splines=False, ratio="fill", overlap="prism", sep=None, splines=False,
vsize=0.105, penwidth=1.0, elen=None, gprops={}, vprops={}, vsize=0.105, penwidth=1.0, elen=None, gprops={}, vprops={},
eprops={}, vcolor="#a40000", ecolor="#2e3436", eprops={}, vcolor="#a40000", ecolor="#2e3436", vcmap=None,
vcmap=None, vnorm=True, ecmap=None, vnorm=True, ecmap=None, enorm=True, vorder=None, eorder=None,
enorm=True, vorder=None, eorder=None, output="", output="", output_format="auto", fork=False,
output_format="auto", returngv=False, fork=False, return_string=False, seed=0):
return_bitmap=False, seed=0):
r"""Draw a graph using graphviz. r"""Draw a graph using graphviz.
Parameters Parameters
...@@ -198,16 +234,14 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -198,16 +234,14 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
"cmapx". If the value is "auto", the format is guessed from the 'output' "cmapx". If the value is "auto", the format is guessed from the 'output'
parameter, or 'xlib' if it is empty. If the value is None, no output is parameter, or 'xlib' if it is empty. If the value is None, no output is
produced. produced.
returngv : bool (default: False)
Return the graph object used internally with the gv module.
fork : bool (default: False) fork : bool (default: False)
If True, the program is forked before drawing. This is used as a If True, the program is forked before drawing. This is used as a
work-around for a bug in graphviz, where the exit() function is called, work-around for a bug in graphviz, where the exit() function is called,
which would cause the calling program to end. This is always assumed which would cause the calling program to end. This is always assumed
'True', if output_format = 'xlib'. 'True', if output_format = 'xlib'.
return_bitmap : bool (default: False) return_string : bool (default: False)
If True, a bitmap (:class:`~numpy.ndarray`) of the rendered graph is If True, a string containing the rendered graph as binary data is
returned. returned (defaults to png format).
Returns Returns
------- -------
...@@ -264,10 +298,9 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -264,10 +298,9 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
not os.access(os.path.dirname(output), os.W_OK): not os.access(os.path.dirname(output), os.W_OK):
raise IOError("cannot write to " + os.path.dirname(output)) raise IOError("cannot write to " + os.path.dirname(output))
if g.is_directed(): has_layout = False
gvg = gv.digraph("G") try:
else: gvg = libgv.agopen("G", 1 if g.is_directed() else 0)
gvg = gv.graph("G")
if layout is None: if layout is None:
layout = "neato" if g.num_vertices() <= 1000 else "sfdp" layout = "neato" if g.num_vertices() <= 1000 else "sfdp"
...@@ -297,33 +330,33 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -297,33 +330,33 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
penwidth = s penwidth = s
# main graph properties # main graph properties
gv.setv(gvg, "outputorder", "edgesfirst") aset(gvg, "outputorder", "edgesfirst")
gv.setv(gvg, "mode", "major") aset(gvg, "mode", "major")
if overlap == False: if overlap == False:
overlap = "false" overlap = "false"
else: else:
overlap = "true" overlap = "true"
if isinstance(overlap, str): if isinstance(overlap, str):
gv.setv(gvg, "overlap", overlap) aset(gvg, "overlap", overlap)
if sep != None: if sep != None:
gv.setv(gvg, "sep", str(sep)) aset(gvg, "sep", sep)
if splines: if splines:
gv.setv(gvg, "splines", "true") aset(gvg, "splines", "true")
gv.setv(gvg, "ratio", str(ratio)) aset(gvg, "ratio", ratio)
# size is in centimeters... convert to inches # size is in centimeters... convert to inches
gv.setv(gvg, "size", "%f,%f" % (size[0] / 2.54, size[1] / 2.54)) aset(gvg, "size", "%f,%f" % (size[0] / 2.54, size[1] / 2.54))
if maxiter != None: if maxiter != None:
gv.setv(gvg, "maxiter", str(maxiter)) aset(gvg, "maxiter", maxiter)
seed = numpy.random.randint(sys.maxint) seed = numpy.random.randint(sys.maxint)
gv.setv(gvg, "start", "%d" % seed) aset(gvg, "start", "%d" % seed)
# apply all user supplied graph properties # apply all user supplied graph properties
for k, val in gprops.iteritems(): for k, val in gprops.iteritems():
if isinstance(val, PropertyMap): if isinstance(val, PropertyMap):
gv.setv(gvg, k, str(val[g])) aset(gvg, k, val[g])
else: else:
gv.setv(gvg, k, str(val)) aset(gvg, k, val)
# normalize color properties # normalize color properties
if vcolor != None and not isinstance(vcolor, str): if vcolor != None and not isinstance(vcolor, str):
...@@ -348,7 +381,8 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -348,7 +381,8 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
if minmax[0] == minmax[1]: if minmax[0] == minmax[1]:
minmax[1] += 1 minmax[1] += 1
if enorm: if enorm:
enorm = matplotlib.colors.normalize(vmin=minmax[0], vmax=minmax[1]) enorm = matplotlib.colors.normalize(vmin=minmax[0],
vmax=minmax[1])
else: else:
enorm = lambda x: x enorm = lambda x: x
...@@ -358,46 +392,43 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -358,46 +392,43 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
if ecmap is None: if ecmap is None:
ecmap = matplotlib.cm.jet ecmap = matplotlib.cm.jet
nodes = {}
# add nodes # add nodes
if vorder != None: if vorder != None:
vertices = sorted(g.vertices(), lambda a, b: cmp(vorder[a], vorder[b])) vertices = sorted(g.vertices(), lambda a, b: cmp(vorder[a], vorder[b]))
else: else:
vertices = g.vertices() vertices = g.vertices()
for v in vertices: for v in vertices:
n = gv.node(gvg, str(g.vertex_index[v])) n = libgv.agnode(gvg, str(int(v)))
if type(vsize) == PropertyMap: if type(vsize) == PropertyMap:
vw = vh = vsize[v] vw = vh = vsize[v]
else: else:
vw = vh = vsize vw = vh = vsize
gv.setv(n, "shape", "circle") aset(n, "shape", "circle")
gv.setv(n, "width", "%g" % vw) aset(n, "width", "%g" % vw)
gv.setv(n, "height", "%g" % vh) aset(n, "height", "%g" % vh)
gv.setv(n, "style", "filled") aset(n, "style", "filled")
gv.setv(n, "color", ecolor if isinstance(ecolor, str) else "#2e3436") aset(n, "color", ecolor if isinstance(ecolor, str) else "#2e3436")
# apply color # apply color
if isinstance(vcolor, str): if isinstance(vcolor, str):
gv.setv(n, "fillcolor", vcolor) aset(n, "fillcolor", vcolor)
else: else:
color = tuple([int(c * 255.0) for c in vcmap(vnorm(vcolor[v]))]) color = tuple([int(c * 255.0) for c in vcmap(vnorm(vcolor[v]))])
gv.setv(n, "fillcolor", "#%.2x%.2x%.2x%.2x" % color) aset(n, "fillcolor", "#%.2x%.2x%.2x%.2x" % color)
gv.setv(n, "label", "") aset(n, "label", "")
# user supplied position # user supplied position
if pos != None: if pos != None:
gv.setv(n, "pos", "%f,%f" % (pos[0][v], pos[1][v])) aset(n, "pos", "%f,%f" % (pos[0][v], pos[1][v]))
gv.setv(n, "pin", str(pin)) aset(n, "pin", pin)
# apply all user supplied properties # apply all user supplied properties
for k, val in vprops.iteritems(): for k, val in vprops.iteritems():
if isinstance(val, PropertyMap): if isinstance(val, PropertyMap):
gv.setv(n, k, str(val[v])) aset(n, k, val[v])
else: else:
gv.setv(n, k, str(val)) aset(n, k, val)
nodes[v] = n
# add edges # add edges
if eorder != None: if eorder != None:
...@@ -405,50 +436,54 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -405,50 +436,54 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
else: else:
edges = g.edges() edges = g.edges()
for e in edges: for e in edges:
ge = gv.edge(nodes[e.source()], ge = libgv.agedge(gvg,
nodes[e.target()]) libgv.agnode(gvg, str(int(e.source()))),
gv.setv(ge, "arrowsize", "0.3") libgv.agnode(gvg, str(int(e.target()))))
aset(ge, "arrowsize", "0.3")
if g.is_directed(): if g.is_directed():
gv.setv(ge, "arrowhead", "vee") aset(ge, "arrowhead", "vee")
# apply color # apply color
if isinstance(ecolor, str): if isinstance(ecolor, str):
gv.setv(ge, "color", ecolor) aset(ge, "color", ecolor)
else: else:
color = tuple([int(c * 255.0) for c in ecmap(enorm(ecolor[e]))]) color = tuple([int(c * 255.0) for c in ecmap(enorm(ecolor[e]))])
gv.setv(ge, "color", "#%.2x%.2x%.2x%.2x" % color) aset(ge, "color", "#%.2x%.2x%.2x%.2x" % color)
# apply edge length # apply edge length
if elen != None: if elen != None:
if isinstance(elen, PropertyMap): if isinstance(elen, PropertyMap):
gv.setv(ge, "len", str(elen[e])) aset(ge, "len", elen[e])
else: else:
gv.setv(ge, "len", str(elen)) aset(ge, "len", elen)
# apply width # apply width
if penwidth != None: if penwidth != None:
if isinstance(penwidth, PropertyMap): if isinstance(penwidth, PropertyMap):
gv.setv(ge, "penwidth", str(penwidth[e])) aset(ge, "penwidth", penwidth[e])
else: else:
gv.setv(ge, "penwidth", str(penwidth)) aset(ge, "penwidth", penwidth)
# apply all user supplied properties # apply all user supplied properties
for k, v in eprops.iteritems(): for k, v in eprops.iteritems():
if isinstance(v, PropertyMap): if isinstance(v, PropertyMap):
gv.setv(ge, k, str(v[e])) aset(ge, k, v[e])
else: else:
gv.setv(ge, k, str(v)) aset(ge, k, v)
gv.layout(gvg, layout) libgv.gvLayout(gvc, gvg, layout)
gv.render(gvg, "dot", "/dev/null") # retrieve positions has_layout = True
retv = libgv.gvRender(gvc, gvg, "dot", None) # retrieve positions only
if pos == None: if pos == None:
pos = (g.new_vertex_property("double"), g.new_vertex_property("double")) pos = (g.new_vertex_property("double"),
for n, n_gv in nodes.iteritems(): g.new_vertex_property("double"))
p = gv.getv(n_gv, "pos") for v in g.vertices():
n = libgv.agnode(gvg, str(int(v)))
p = aget(n, "pos")
p = p.split(",") p = p.split(",")
pos[0][n] = float(p[0]) pos[0][v] = float(p[0])
pos[1][n] = float(p[1]) pos[1][v] = float(p[1])
# I don't get this, but it seems necessary # I don't get this, but it seems necessary
pos[0].a /= 100 pos[0].a /= 100
...@@ -456,14 +491,17 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -456,14 +491,17 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
pos = group_vector_property(pos) pos = group_vector_property(pos)
if return_bitmap: if return_string:
# This is a not-so-nice hack which obtains an image buffer from a png if output_format == "auto":
# file. It is a pity that graphviz does not give access to its internal output_format = "png"
# buffers. buf = ctypes.c_char_p()
tmp = tempfile.mkstemp(suffix=".png")[1] buf_len = ctypes.c_size_t()
gv.render(gvg, "png", tmp) fstream = libc.open_memstream(ctypes.byref(buf),
img = imread(tmp) ctypes.byref(buf_len))
os.remove(tmp) libgv.gvRender(gvc, gvg, output_format, fstream)
libc.fclose(fstream)
data = copy.copy(ctypes.string_at(buf, buf_len.value))
libc.free(buf)
else: else:
if output_format == "auto": if output_format == "auto":
if output == "": if output == "":
...@@ -471,27 +509,26 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None, ...@@ -471,27 +509,26 @@ def graph_draw(g, pos=None, size=(15, 15), pin=False, layout=None, maxiter=None,
elif output != None: elif output != None:
output_format = output.split(".")[-1] output_format = output.split(".")[-1]
# if using xlib we need to fork the process, otherwise good ol' graphviz # if using xlib we need to fork the process, otherwise good ol'
# will call exit() when the window is closed # graphviz will call exit() when the window is closed
if output_format == "xlib" or fork: if output_format == "xlib" or fork:
pid = os.fork() pid = os.fork()
if pid == 0: if pid == 0:
gv.render(gvg, output_format, output) libgv.gvRenderFilename(gvc, gvg, output_format, output)
os._exit(0) # since we forked, it's good to be sure os._exit(0) # since we forked, it's good to be sure
if output_format != "xlib": if output_format != "xlib":
os.wait() os.wait()
elif output != None: elif output != None:
gv.render(gvg, output_format, output) libgv.gvRenderFilename(gvc, gvg, output_format, output)
ret = [pos] ret = [pos]
if return_bitmap: if return_string:
ret.append(img) ret.append(data)
if returngv: finally:
ret.append(gv) if has_layout:
else: libgv.gvFreeLayout(gvc, gvg)
gv.rm(gvg) libgv.agclose(gvg)
del gvg
if len(ret) > 1: if len(ret) > 1:
return tuple(ret) return tuple(ret)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment