|
| 1 | +# TODO: how do you set the BACKGROUND COLOR of a GraphViz node ... fill=? |
| 2 | + |
| 3 | +# TODO: make this work in both Python 2 and 3 |
| 4 | + |
| 5 | +from collections import defaultdict |
| 6 | + |
| 7 | +ID = 0 |
| 8 | + |
| 9 | +class TreeNode: |
| 10 | + def __init__(self, dat): |
| 11 | + self.data = str(dat) # convert to string for easy display in GraphViz |
| 12 | + self.parent = None |
| 13 | + self.left = None |
| 14 | + self.right = None |
| 15 | + |
| 16 | + self.__penwidth = 1 # thickness of node border |
| 17 | + |
| 18 | + # HTML-like RGB hex values - e.g., "#bb0000" |
| 19 | + self.__color = None # border color |
| 20 | + self.__fill = None # internal node color |
| 21 | + |
| 22 | + # assign unique IDs in node creation order |
| 23 | + global ID |
| 24 | + self.id = 'n' + str(ID) |
| 25 | + ID += 1 |
| 26 | + |
| 27 | + def set_border_color(self, col): |
| 28 | + self.__color = col |
| 29 | + |
| 30 | + def set_fill(self, col): |
| 31 | + self.__fill = col |
| 32 | + |
| 33 | + def set_width(self, w): |
| 34 | + assert w > 0 |
| 35 | + self.__penwidth = w |
| 36 | + |
| 37 | + def __str__(self): |
| 38 | + ret = '%s[label="%s"' % (self.id, self.data) |
| 39 | + if self.__penwidth > 1: |
| 40 | + ret += ',penwidth=%d' % self.__penwidth |
| 41 | + if self.__color: |
| 42 | + ret += ',color="%s"' % self.__color |
| 43 | + if self.__fill: |
| 44 | + ret += ',fill="%s"' % self.__fill |
| 45 | + ret += ']' |
| 46 | + return ret |
| 47 | + |
| 48 | + |
| 49 | +# render a binary tree of TreeNode objects starting at root in a pretty |
| 50 | +# GraphViz format using the balanced tree hack from |
| 51 | +# http://www.graphviz.org/content/FaqBalanceTree |
| 52 | +def graphviz_render(root): |
| 53 | + print 'digraph G{' |
| 54 | + |
| 55 | + queue = [] # each element is (node, level #) |
| 56 | + |
| 57 | + # Key: level number |
| 58 | + # Value: sorted list of node IDs at that level (including phantom nodes) |
| 59 | + nodes_by_level = defaultdict(list) |
| 60 | + |
| 61 | + |
| 62 | + def print_phantom(parent_id, suffix): |
| 63 | + phantom_id = parent_id + '_phantom_' + suffix |
| 64 | + print '%s [label="",width=.1,style=invis]' % phantom_id |
| 65 | + print '%s->%s [style=invis]' % (parent_id, phantom_id) |
| 66 | + return phantom_id |
| 67 | + |
| 68 | + def bfs_visit(): |
| 69 | + # base case |
| 70 | + if not queue: |
| 71 | + return |
| 72 | + |
| 73 | + n, level = queue.pop(0) |
| 74 | + |
| 75 | + print n # current node |
| 76 | + if n.left or n.right: |
| 77 | + if n.left: |
| 78 | + print '%s->%s' % (n.id, n.left.id) |
| 79 | + queue.append((n.left, level+1)) |
| 80 | + nodes_by_level[level+1].append(n.left.id) |
| 81 | + else: |
| 82 | + # insert phantom to make tree look good |
| 83 | + ph_id = print_phantom(n.id, 'L') |
| 84 | + nodes_by_level[level+1].append(ph_id) |
| 85 | + |
| 86 | + # always insert invisible middle phantom |
| 87 | + ph_id = print_phantom(n.id, 'M') |
| 88 | + nodes_by_level[level+1].append(ph_id) |
| 89 | + |
| 90 | + if n.right: |
| 91 | + print '%s->%s' % (n.id, n.right.id) |
| 92 | + queue.append((n.right, level+1)) |
| 93 | + nodes_by_level[level+1].append(n.right.id) |
| 94 | + else: |
| 95 | + # insert phantom to make tree look good |
| 96 | + ph_id = print_phantom(n.id, 'R') |
| 97 | + nodes_by_level[level+1].append(ph_id) |
| 98 | + |
| 99 | + bfs_visit() # recurse! |
| 100 | + |
| 101 | + queue.append((root, 1)) |
| 102 | + bfs_visit() |
| 103 | + |
| 104 | + # make sure all nodes at the same level are vertically aligned |
| 105 | + for level in nodes_by_level: |
| 106 | + node_ids = nodes_by_level[level] |
| 107 | + if len(node_ids) > 1: |
| 108 | + print '{rank=same %s [style=invis]}' % '->'.join(node_ids) |
| 109 | + |
| 110 | + print '}' |
| 111 | + |
| 112 | + |
| 113 | +if __name__ == "__main__": |
| 114 | + # simple test tree |
| 115 | + a = TreeNode('a') |
| 116 | + |
| 117 | + b0 = TreeNode('b0') |
| 118 | + a.left = b0 |
| 119 | + |
| 120 | + b1 = TreeNode('b1') |
| 121 | + a.right = b1 |
| 122 | + |
| 123 | + c0 = TreeNode('c0') |
| 124 | + b0.left = c0 |
| 125 | + |
| 126 | + c1 = TreeNode('c1') |
| 127 | + b0.right = c1 |
| 128 | + b0.set_fill('red') |
| 129 | + |
| 130 | + c2 = TreeNode('c2') |
| 131 | + b1.left = c2 |
| 132 | + |
| 133 | + d1 = TreeNode('d1') |
| 134 | + c0.right = d1 |
| 135 | + |
| 136 | + d2 = TreeNode('d2') |
| 137 | + c2.left = d2 |
| 138 | + |
| 139 | + d3 = TreeNode('d3') |
| 140 | + c1.left = d3 |
| 141 | + |
| 142 | + d4 = TreeNode('d4') |
| 143 | + c1.right = d4 |
| 144 | + |
| 145 | + graphviz_render(a) |
| 146 | + |
| 147 | + |
| 148 | +''' |
| 149 | +/* balanced tree hack from http://www.graphviz.org/content/FaqBalanceTree */ |
| 150 | + |
| 151 | +/* |
| 152 | +digraph G { |
| 153 | + a -> b0 |
| 154 | + xb [label="",width=.1,style=invis] |
| 155 | + a -> xb [style=invis] |
| 156 | + a -> b1 |
| 157 | + |
| 158 | + {rank=same b0 -> xb -> b1 [style=invis]} |
| 159 | + |
| 160 | + b0 -> c0 |
| 161 | + xc [label="",width=.1,style=invis] |
| 162 | + b0 -> xc [style=invis] |
| 163 | + b0 -> c1 |
| 164 | + |
| 165 | + {rank=same c0 -> xc -> c1 [style=invis]} |
| 166 | +} |
| 167 | +*/ |
| 168 | +''' |
0 commit comments