Graphviz is an excellent and free graph visualization program that provides a simple way to generate graphs for pretty much anything with a variety of layouts. The Graphviz layout programs take descriptions of graphs in a simple text language. These descriptions can be created and edited manually but are more usually generated programatically – especially for large data sets.
This post provides an example of using the Visitor pattern to generate a directed graph visualization of a trie using the Graphviz dot layout manager. Full python source code listing of this example is available here.
A Trie class is used to represent the trie data structure. This class is composed of a set of nodes representing the trie nodes. These nodes are anchored via a root node. The code for adding data to the trie is omitted for clarity but is available in the full source listing.
class Trie: def __init__(self): self.root = Node() def walk(self, visitor): self.root.walk(visitor)
From a visitor pattern perspective, the Node.walk() method is used to traverse the nodes and to pass each node encountered to the visitor object.
class Node: def walk(self, visitor): visitor.visitNode(self) for nextNode in self.children.values(): nextNode.walk(visitor)
The GraphvizVisitor class is a Visitor subclass that generates the appropriate dot language representation for each visited node.
# Visitor interface class Visitor: def visitNode(self, node): pass # Visitor subclass that generates Graphviz dot file contents. class GraphvizVisitor: def __init__(self, dotGen): self.dotGen = dotGen def visitNode(self, node): if node.isOutputNode: dotGen.setNodeProperty(node.name, 'shape', 'doublecircle') for edge, nextNode in node.children.items(): dotGen.addLink(node.name, nextNode.name, edge)
The dotGen object is an instance of the DotFileGenerator class which is used as a simple utility class to generate the dot file.
class DotFileGenerator: def __init__(self, name): self.name = name self.outf = open(name+'.dot', 'w') print >> self.outf, 'digraph %s {' % (self.name) print >> self.outf, 'trankdir="LR";' print >> self.outf, 'tnode [shape = "circle"];' def setNodeProperty(self, nodeName, property, value): print >> self.outf, 't%s [%s = %s];' %(nodeName, property, value) def addLink(self, lhs, rhs, label): print >> self.outf, 't%s -> %s [label = %s];' % (lhs, rhs, label) def close(self): self.outf.close()
Consider the following example to illustrate the generated output:
# Build a trie trie = Trie() trie.addPattern("abc") trie.addPattern("abcdef") trie.addPattern("abcxyz") trie.addPattern("abdef") # Generate the dot file contents by walking the trie dotGen = DotFileGenerator('trie') visitor = GraphvizVisitor(dotGen) trie.walk(visitor) dotGen.close()
This produces the following dot file contents:
digraph trie { rankdir="LR"; node [shape = "circle"]; 0 -> 1 [label = a]; 1 -> 2 [label = b]; 2 -> 3 [label = c]; 2 -> 10 [label = d]; 3 [shape = doublecircle]; 3 -> 7 [label = x]; 3 -> 4 [label = d]; 7 -> 8 [label = y]; 8 -> 9 [label = z]; 9 [shape = doublecircle]; 4 -> 5 [label = e]; 5 -> 6 [label = f]; 6 [shape = doublecircle]; 10 -> 11 [label = e]; 11 -> 12 [label = f]; 12 [shape = doublecircle]; }
After passing this dot file through Graphviz, we get: