123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- """
- altgraph.Dot - Interface to the dot language
- ============================================
- The :py:mod:`~altgraph.Dot` module provides a simple interface to the
- file format used in the
- `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
- program. The module is intended to offload the most tedious part of the process
- (the **dot** file generation) while transparently exposing most of its
- features.
- To display the graphs or to generate image files the
- `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
- package needs to be installed on the system, moreover the :command:`dot` and
- :command:`dotty` programs must be accesible in the program path so that they
- can be ran from processes spawned within the module.
- Example usage
- -------------
- Here is a typical usage::
- from altgraph import Graph, Dot
- # create a graph
- edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ]
- graph = Graph.Graph(edges)
- # create a dot representation of the graph
- dot = Dot.Dot(graph)
- # display the graph
- dot.display()
- # save the dot representation into the mydot.dot file
- dot.save_dot(file_name='mydot.dot')
- # save dot file as gif image into the graph.gif file
- dot.save_img(file_name='graph', file_type='gif')
- Directed graph and non-directed graph
- -------------------------------------
- Dot class can use for both directed graph and non-directed graph
- by passing ``graphtype`` parameter.
- Example::
- # create directed graph(default)
- dot = Dot.Dot(graph, graphtype="digraph")
- # create non-directed graph
- dot = Dot.Dot(graph, graphtype="graph")
- Customizing the output
- ----------------------
- The graph drawing process may be customized by passing
- valid :command:`dot` parameters for the nodes and edges. For a list of all
- parameters see the `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
- documentation.
- Example::
- # customizing the way the overall graph is drawn
- dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75)
- # customizing node drawing
- dot.node_style(1, label='BASE_NODE',shape='box', color='blue' )
- dot.node_style(2, style='filled', fillcolor='red')
- # customizing edge drawing
- dot.edge_style(1, 2, style='dotted')
- dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90')
- dot.edge_style(4, 5, arrowsize=2, style='bold')
- .. note::
- dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to
- display all graphics styles. To verify the output save it to an image file
- and look at it that way.
- Valid attributes
- ----------------
- - dot styles, passed via the :py:meth:`Dot.style` method::
- rankdir = 'LR' (draws the graph horizontally, left to right)
- ranksep = number (rank separation in inches)
- - node attributes, passed via the :py:meth:`Dot.node_style` method::
- style = 'filled' | 'invisible' | 'diagonals' | 'rounded'
- shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle'
- - edge attributes, passed via the :py:meth:`Dot.edge_style` method::
- style = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold'
- arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none'
- | 'tee' | 'vee'
- weight = number (the larger the number the closer the nodes will be)
- - valid `graphviz colors
- <http://www.research.att.com/~erg/graphviz/info/colors.html>`_
- - for more details on how to control the graph drawing process see the
- `graphviz reference
- <http://www.research.att.com/sw/tools/graphviz/refs.html>`_.
- """
- import os
- import warnings
- from altgraph import GraphError
- class Dot(object):
- """
- A class providing a **graphviz** (dot language) representation
- allowing a fine grained control over how the graph is being
- displayed.
- If the :command:`dot` and :command:`dotty` programs are not in the current
- system path their location needs to be specified in the contructor.
- """
- def __init__(
- self,
- graph=None,
- nodes=None,
- edgefn=None,
- nodevisitor=None,
- edgevisitor=None,
- name="G",
- dot="dot",
- dotty="dotty",
- neato="neato",
- graphtype="digraph",
- ):
- """
- Initialization.
- """
- self.name, self.attr = name, {}
- assert graphtype in ["graph", "digraph"]
- self.type = graphtype
- self.temp_dot = "tmp_dot.dot"
- self.temp_neo = "tmp_neo.dot"
- self.dot, self.dotty, self.neato = dot, dotty, neato
- # self.nodes: node styles
- # self.edges: edge styles
- self.nodes, self.edges = {}, {}
- if graph is not None and nodes is None:
- nodes = graph
- if graph is not None and edgefn is None:
- def edgefn(node, graph=graph):
- return graph.out_nbrs(node)
- if nodes is None:
- nodes = ()
- seen = set()
- for node in nodes:
- if nodevisitor is None:
- style = {}
- else:
- style = nodevisitor(node)
- if style is not None:
- self.nodes[node] = {}
- self.node_style(node, **style)
- seen.add(node)
- if edgefn is not None:
- for head in seen:
- for tail in (n for n in edgefn(head) if n in seen):
- if edgevisitor is None:
- edgestyle = {}
- else:
- edgestyle = edgevisitor(head, tail)
- if edgestyle is not None:
- if head not in self.edges:
- self.edges[head] = {}
- self.edges[head][tail] = {}
- self.edge_style(head, tail, **edgestyle)
- def style(self, **attr):
- """
- Changes the overall style
- """
- self.attr = attr
- def display(self, mode="dot"):
- """
- Displays the current graph via dotty
- """
- if mode == "neato":
- self.save_dot(self.temp_neo)
- neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo)
- os.system(neato_cmd)
- else:
- self.save_dot(self.temp_dot)
- plot_cmd = "%s %s" % (self.dotty, self.temp_dot)
- os.system(plot_cmd)
- def node_style(self, node, **kwargs):
- """
- Modifies a node style to the dot representation.
- """
- if node not in self.edges:
- self.edges[node] = {}
- self.nodes[node] = kwargs
- def all_node_style(self, **kwargs):
- """
- Modifies all node styles
- """
- for node in self.nodes:
- self.node_style(node, **kwargs)
- def edge_style(self, head, tail, **kwargs):
- """
- Modifies an edge style to the dot representation.
- """
- if tail not in self.nodes:
- raise GraphError("invalid node %s" % (tail,))
- try:
- if tail not in self.edges[head]:
- self.edges[head][tail] = {}
- self.edges[head][tail] = kwargs
- except KeyError:
- raise GraphError("invalid edge %s -> %s " % (head, tail))
- def iterdot(self):
- # write graph title
- if self.type == "digraph":
- yield "digraph %s {\n" % (self.name,)
- elif self.type == "graph":
- yield "graph %s {\n" % (self.name,)
- else:
- raise GraphError("unsupported graphtype %s" % (self.type,))
- # write overall graph attributes
- for attr_name, attr_value in sorted(self.attr.items()):
- yield '%s="%s";' % (attr_name, attr_value)
- yield "\n"
- # some reusable patterns
- cpatt = '%s="%s",' # to separate attributes
- epatt = "];\n" # to end attributes
- # write node attributes
- for node_name, node_attr in sorted(self.nodes.items()):
- yield '\t"%s" [' % (node_name,)
- for attr_name, attr_value in sorted(node_attr.items()):
- yield cpatt % (attr_name, attr_value)
- yield epatt
- # write edge attributes
- for head in sorted(self.edges):
- for tail in sorted(self.edges[head]):
- if self.type == "digraph":
- yield '\t"%s" -> "%s" [' % (head, tail)
- else:
- yield '\t"%s" -- "%s" [' % (head, tail)
- for attr_name, attr_value in sorted(self.edges[head][tail].items()):
- yield cpatt % (attr_name, attr_value)
- yield epatt
- # finish file
- yield "}\n"
- def __iter__(self):
- return self.iterdot()
- def save_dot(self, file_name=None):
- """
- Saves the current graph representation into a file
- """
- if not file_name:
- warnings.warn(DeprecationWarning, "always pass a file_name")
- file_name = self.temp_dot
- with open(file_name, "w") as fp:
- for chunk in self.iterdot():
- fp.write(chunk)
- def save_img(self, file_name=None, file_type="gif", mode="dot"):
- """
- Saves the dot file as an image file
- """
- if not file_name:
- warnings.warn(DeprecationWarning, "always pass a file_name")
- file_name = "out"
- if mode == "neato":
- self.save_dot(self.temp_neo)
- neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo)
- os.system(neato_cmd)
- plot_cmd = self.dot
- else:
- self.save_dot(self.temp_dot)
- plot_cmd = self.dot
- file_name = "%s.%s" % (file_name, file_type)
- create_cmd = "%s -T%s %s -o %s" % (
- plot_cmd,
- file_type,
- self.temp_dot,
- file_name,
- )
- os.system(create_cmd)
|