graph.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. # vim: set fileencoding=utf-8 :
  2. from node import Node, Leaf
  3. def generate_graph(root, node_type, separator=' '):
  4. """
  5. Return a text-based, utf-8 graph of a tree-like structure. Each tree node
  6. is represented by a length-2 list. If a node has an attribute called
  7. ``title``, that attribute will be called. That way, the node can return a
  8. specific title, otherwise ``+`` is used.
  9. >>> l0, l1 = Leaf(0), Leaf(1)
  10. >>> n0 = Node('+', l0, l1)
  11. >>> print generate_graph(n0, Node)
  12. +
  13. ╭┴╮
  14. 0 1
  15. >>> l2 = Leaf(2)
  16. >>> n1 = Node('-', l2)
  17. >>> print generate_graph(n1, Node)
  18. -
  19. 2
  20. >>> n2 = Node('*', n0, n1)
  21. >>> print generate_graph(n2, Node)
  22. +
  23. ╭┴─╮
  24. + -
  25. ╭┴╮ │
  26. 0 1 2
  27. """
  28. node_width = {}
  29. separator_len = len(separator)
  30. def calculate_width(node):
  31. title = node.title()
  32. title_len = len(title)
  33. # Leaves do not have children and therefore the length of its title is
  34. # the width of the leaf.
  35. if not isinstance(node, node_type):
  36. node_width[node] = title_len
  37. return title_len
  38. width = 0
  39. for child in node:
  40. width += calculate_width(child)
  41. # Add a separator between each node (thus n - 1 separators).
  42. width += separator_len * len(node) - 1
  43. # Odd numbers of children should be minus 1, since the middle child
  44. # can be placed directly below the parent. With even numbers, there
  45. # is no middle child, so the space below the parent cannot be used.
  46. if len(node) % 2 == 1:
  47. width -= 1
  48. # If the title of the node is wider than the sum of its children, the
  49. # title's width should be used.
  50. width = max(title_len, width)
  51. node_width[node] = width
  52. return width
  53. def node_middle(node):
  54. node_len = len(node)
  55. node_mid = node_len / 2
  56. if node_len % 2 == 1:
  57. node_mid += 1
  58. middle = 0
  59. for i, child in enumerate(node):
  60. if i == node_mid:
  61. break
  62. middle += separator_len + node_width[child]
  63. return middle - separator_len
  64. def format_lines(node):
  65. if not isinstance(node, node_type):
  66. # Leaf titles do not need to be centered, since the parent will
  67. # center those lines. And if there are no parents, the entire graph
  68. # consists of a single leaf, so in that case there still is no
  69. # reason to center it.
  70. return [node.title()]
  71. # At least one child, otherwise it would be a leaf.
  72. assert node[0]
  73. line_width = node_width[node]
  74. lines = format_lines(node[0])
  75. for child in node[1:]:
  76. pos = len(lines[0])
  77. child_lines = format_lines(child)
  78. child_lines_len = len(child_lines)
  79. # A node cannot have zero child lines.
  80. assert child_lines
  81. for i, line in enumerate(lines):
  82. #print 'lines -> %d, "%s"' % (i, line)
  83. if i < child_lines_len:
  84. padding_right = ' ' * (line_width - pos \
  85. - len(child_lines[i]) \
  86. - separator_len)
  87. lines[i] += separator + child_lines[i] + padding_right
  88. else:
  89. # There are no more neighbor node on the right.
  90. lines[i] += ' ' * (line_width - pos)
  91. # Add the child nodes that do not have neighbor nodes on the left.
  92. for i, line in enumerate(child_lines[i+1:]):
  93. #print 'child_lines[i:] -> %d, "%s"' % (i, line)
  94. line = ' ' * (pos + separator_len) + line \
  95. + ' ' * (line_width - separator_len - pos - len(line))
  96. lines.append(line)
  97. # Validate that each line has an equal width
  98. try:
  99. for line in lines:
  100. assert len(line) == line_width
  101. except AssertionError:
  102. for l in lines:
  103. l = l.encode('utf-8')
  104. print l.replace(' ', '#'), len(l)
  105. l = line.encode('utf-8')
  106. print 'failed at:', l, len(l)
  107. print 'current node:', node.title(), 'width:', line_width
  108. raise
  109. # Place the title above the middle two child nodes, or when there is a
  110. # odd number of nodes, above the child node in the middle.
  111. middle = node_middle(node)
  112. #print 'node:', node.title(), 'middle:', middle, 'node_mid:', node_mid
  113. title_line = center_text(node.title(), line_width, middle)
  114. node_len = len(node)
  115. node_mid = node_len / 2
  116. edge_line = ''
  117. for i, child in enumerate(node):
  118. if isinstance(child, node_type):
  119. middle = node_middle(child)
  120. else:
  121. middle = 0
  122. if i < node_mid:
  123. marker = ('╭' if i == 0 else '┬')
  124. edge_line += center_text(marker.decode('utf-8'),
  125. node_width[child],
  126. middle, right='─'.decode('utf-8'))
  127. else:
  128. if i == node_mid:
  129. edge_line += '┴'.decode('utf-8')
  130. marker = ('╮' if i == node_len - 1 else '┬')
  131. edge_line += center_text(marker.decode('utf-8'),
  132. node_width[child],
  133. middle, left='─'.decode('utf-8'))
  134. #try:
  135. # assert len(title_line) == len(edge_line)
  136. #except AssertionError:
  137. # print 'title_line:', title_line, len(title_line)
  138. # print 'edge_line:', edge_line, len(edge_line)
  139. # raise
  140. # Add the line of this node before all child lines.
  141. return [title_line, edge_line] + lines
  142. calculate_width(root)
  143. lines = format_lines(root)
  144. return '\n'.join(lines).encode('utf-8')
  145. def center_text(text, width, middle=0, left=' ', right=' '):
  146. """
  147. >>> center_text('+', 15, 11)
  148. ' + '
  149. """
  150. text_len = len(text)
  151. text_mid = text_len / 2
  152. # TODO: this code requires cleanup.
  153. if middle:
  154. # If this is true, the text is at the left.
  155. if text_mid > middle:
  156. text += left * (width - text_len)
  157. # If this is true, the text is at the right.
  158. elif text_mid > (width - middle):
  159. text = right * (width - text_len) + text
  160. # Else, the text has spacing on its left and right.
  161. else:
  162. text = left * (middle - text_mid) + text
  163. text += right * (width - len(text))
  164. return text
  165. spacing = width - text_len
  166. # Even number of spaces can be split equally.
  167. if spacing % 2 == 0:
  168. return left * (spacing / 2) + text + right * (spacing / 2)
  169. # For an odd number of space, put the extra space at the end.
  170. return left * (spacing / 2) + text + right * (spacing / 2 + 1)