''' 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 `_ 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 `_ 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 `_ 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 `_ - for more details on how to control the graph drawing process see the `graphviz reference `_. ''' 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)