Dot.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. """
  2. altgraph.Dot - Interface to the dot language
  3. ============================================
  4. The :py:mod:`~altgraph.Dot` module provides a simple interface to the
  5. file format used in the
  6. `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
  7. program. The module is intended to offload the most tedious part of the process
  8. (the **dot** file generation) while transparently exposing most of its
  9. features.
  10. To display the graphs or to generate image files the
  11. `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
  12. package needs to be installed on the system, moreover the :command:`dot` and
  13. :command:`dotty` programs must be accesible in the program path so that they
  14. can be ran from processes spawned within the module.
  15. Example usage
  16. -------------
  17. Here is a typical usage::
  18. from altgraph import Graph, Dot
  19. # create a graph
  20. edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ]
  21. graph = Graph.Graph(edges)
  22. # create a dot representation of the graph
  23. dot = Dot.Dot(graph)
  24. # display the graph
  25. dot.display()
  26. # save the dot representation into the mydot.dot file
  27. dot.save_dot(file_name='mydot.dot')
  28. # save dot file as gif image into the graph.gif file
  29. dot.save_img(file_name='graph', file_type='gif')
  30. Directed graph and non-directed graph
  31. -------------------------------------
  32. Dot class can use for both directed graph and non-directed graph
  33. by passing ``graphtype`` parameter.
  34. Example::
  35. # create directed graph(default)
  36. dot = Dot.Dot(graph, graphtype="digraph")
  37. # create non-directed graph
  38. dot = Dot.Dot(graph, graphtype="graph")
  39. Customizing the output
  40. ----------------------
  41. The graph drawing process may be customized by passing
  42. valid :command:`dot` parameters for the nodes and edges. For a list of all
  43. parameters see the `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
  44. documentation.
  45. Example::
  46. # customizing the way the overall graph is drawn
  47. dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75)
  48. # customizing node drawing
  49. dot.node_style(1, label='BASE_NODE',shape='box', color='blue' )
  50. dot.node_style(2, style='filled', fillcolor='red')
  51. # customizing edge drawing
  52. dot.edge_style(1, 2, style='dotted')
  53. dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90')
  54. dot.edge_style(4, 5, arrowsize=2, style='bold')
  55. .. note::
  56. dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to
  57. display all graphics styles. To verify the output save it to an image file
  58. and look at it that way.
  59. Valid attributes
  60. ----------------
  61. - dot styles, passed via the :py:meth:`Dot.style` method::
  62. rankdir = 'LR' (draws the graph horizontally, left to right)
  63. ranksep = number (rank separation in inches)
  64. - node attributes, passed via the :py:meth:`Dot.node_style` method::
  65. style = 'filled' | 'invisible' | 'diagonals' | 'rounded'
  66. shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle'
  67. - edge attributes, passed via the :py:meth:`Dot.edge_style` method::
  68. style = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold'
  69. arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none'
  70. | 'tee' | 'vee'
  71. weight = number (the larger the number the closer the nodes will be)
  72. - valid `graphviz colors
  73. <http://www.research.att.com/~erg/graphviz/info/colors.html>`_
  74. - for more details on how to control the graph drawing process see the
  75. `graphviz reference
  76. <http://www.research.att.com/sw/tools/graphviz/refs.html>`_.
  77. """
  78. import os
  79. import warnings
  80. from altgraph import GraphError
  81. class Dot(object):
  82. """
  83. A class providing a **graphviz** (dot language) representation
  84. allowing a fine grained control over how the graph is being
  85. displayed.
  86. If the :command:`dot` and :command:`dotty` programs are not in the current
  87. system path their location needs to be specified in the contructor.
  88. """
  89. def __init__(
  90. self,
  91. graph=None,
  92. nodes=None,
  93. edgefn=None,
  94. nodevisitor=None,
  95. edgevisitor=None,
  96. name="G",
  97. dot="dot",
  98. dotty="dotty",
  99. neato="neato",
  100. graphtype="digraph",
  101. ):
  102. """
  103. Initialization.
  104. """
  105. self.name, self.attr = name, {}
  106. assert graphtype in ["graph", "digraph"]
  107. self.type = graphtype
  108. self.temp_dot = "tmp_dot.dot"
  109. self.temp_neo = "tmp_neo.dot"
  110. self.dot, self.dotty, self.neato = dot, dotty, neato
  111. # self.nodes: node styles
  112. # self.edges: edge styles
  113. self.nodes, self.edges = {}, {}
  114. if graph is not None and nodes is None:
  115. nodes = graph
  116. if graph is not None and edgefn is None:
  117. def edgefn(node, graph=graph):
  118. return graph.out_nbrs(node)
  119. if nodes is None:
  120. nodes = ()
  121. seen = set()
  122. for node in nodes:
  123. if nodevisitor is None:
  124. style = {}
  125. else:
  126. style = nodevisitor(node)
  127. if style is not None:
  128. self.nodes[node] = {}
  129. self.node_style(node, **style)
  130. seen.add(node)
  131. if edgefn is not None:
  132. for head in seen:
  133. for tail in (n for n in edgefn(head) if n in seen):
  134. if edgevisitor is None:
  135. edgestyle = {}
  136. else:
  137. edgestyle = edgevisitor(head, tail)
  138. if edgestyle is not None:
  139. if head not in self.edges:
  140. self.edges[head] = {}
  141. self.edges[head][tail] = {}
  142. self.edge_style(head, tail, **edgestyle)
  143. def style(self, **attr):
  144. """
  145. Changes the overall style
  146. """
  147. self.attr = attr
  148. def display(self, mode="dot"):
  149. """
  150. Displays the current graph via dotty
  151. """
  152. if mode == "neato":
  153. self.save_dot(self.temp_neo)
  154. neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo)
  155. os.system(neato_cmd)
  156. else:
  157. self.save_dot(self.temp_dot)
  158. plot_cmd = "%s %s" % (self.dotty, self.temp_dot)
  159. os.system(plot_cmd)
  160. def node_style(self, node, **kwargs):
  161. """
  162. Modifies a node style to the dot representation.
  163. """
  164. if node not in self.edges:
  165. self.edges[node] = {}
  166. self.nodes[node] = kwargs
  167. def all_node_style(self, **kwargs):
  168. """
  169. Modifies all node styles
  170. """
  171. for node in self.nodes:
  172. self.node_style(node, **kwargs)
  173. def edge_style(self, head, tail, **kwargs):
  174. """
  175. Modifies an edge style to the dot representation.
  176. """
  177. if tail not in self.nodes:
  178. raise GraphError("invalid node %s" % (tail,))
  179. try:
  180. if tail not in self.edges[head]:
  181. self.edges[head][tail] = {}
  182. self.edges[head][tail] = kwargs
  183. except KeyError:
  184. raise GraphError("invalid edge %s -> %s " % (head, tail))
  185. def iterdot(self):
  186. # write graph title
  187. if self.type == "digraph":
  188. yield "digraph %s {\n" % (self.name,)
  189. elif self.type == "graph":
  190. yield "graph %s {\n" % (self.name,)
  191. else:
  192. raise GraphError("unsupported graphtype %s" % (self.type,))
  193. # write overall graph attributes
  194. for attr_name, attr_value in sorted(self.attr.items()):
  195. yield '%s="%s";' % (attr_name, attr_value)
  196. yield "\n"
  197. # some reusable patterns
  198. cpatt = '%s="%s",' # to separate attributes
  199. epatt = "];\n" # to end attributes
  200. # write node attributes
  201. for node_name, node_attr in sorted(self.nodes.items()):
  202. yield '\t"%s" [' % (node_name,)
  203. for attr_name, attr_value in sorted(node_attr.items()):
  204. yield cpatt % (attr_name, attr_value)
  205. yield epatt
  206. # write edge attributes
  207. for head in sorted(self.edges):
  208. for tail in sorted(self.edges[head]):
  209. if self.type == "digraph":
  210. yield '\t"%s" -> "%s" [' % (head, tail)
  211. else:
  212. yield '\t"%s" -- "%s" [' % (head, tail)
  213. for attr_name, attr_value in sorted(self.edges[head][tail].items()):
  214. yield cpatt % (attr_name, attr_value)
  215. yield epatt
  216. # finish file
  217. yield "}\n"
  218. def __iter__(self):
  219. return self.iterdot()
  220. def save_dot(self, file_name=None):
  221. """
  222. Saves the current graph representation into a file
  223. """
  224. if not file_name:
  225. warnings.warn(DeprecationWarning, "always pass a file_name")
  226. file_name = self.temp_dot
  227. with open(file_name, "w") as fp:
  228. for chunk in self.iterdot():
  229. fp.write(chunk)
  230. def save_img(self, file_name=None, file_type="gif", mode="dot"):
  231. """
  232. Saves the dot file as an image file
  233. """
  234. if not file_name:
  235. warnings.warn(DeprecationWarning, "always pass a file_name")
  236. file_name = "out"
  237. if mode == "neato":
  238. self.save_dot(self.temp_neo)
  239. neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo)
  240. os.system(neato_cmd)
  241. plot_cmd = self.dot
  242. else:
  243. self.save_dot(self.temp_dot)
  244. plot_cmd = self.dot
  245. file_name = "%s.%s" % (file_name, file_type)
  246. create_cmd = "%s -T%s %s -o %s" % (
  247. plot_cmd,
  248. file_type,
  249. self.temp_dot,
  250. file_name,
  251. )
  252. os.system(create_cmd)