graph.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. # vim: set fileencoding=utf-8 :
  2. from node import Node, Leaf
  3. def generate_graph(root, node_type, separator=' ', verbose=False):
  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. >>> l2 = Leaf(2)
  12. >>> print generate_graph(n0, Node)
  13. +
  14. ╭┴╮
  15. 0 1
  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. # print 'width of "%s":' % node.title(), width
  52. node_width[node] = width
  53. return width
  54. def node_middle(node):
  55. node_len = len(node)
  56. node_mid = int(node_len / 2)
  57. #if node_len % 2 == 1:
  58. # node_mid += 1
  59. middle = 0
  60. for i, child in enumerate(node):
  61. if i == node_mid:
  62. break
  63. middle += node_width[child]
  64. return middle + max(0, separator_len * (node_mid - 1))
  65. def format_lines(node):
  66. if not isinstance(node, node_type):
  67. # Leaf titles do not need to be centered, since the parent will
  68. # center those lines. And if there are no parents, the entire graph
  69. # consists of a single leaf, so in that case there still is no
  70. # reason to center it.
  71. return [node.title()]
  72. # At least one child, otherwise it would be a leaf.
  73. assert node[0]
  74. child_lines = [format_lines(child) for child in node]
  75. max_height = max(map(len, child_lines))
  76. # Assert that all child boxes are of equal height
  77. for lines in child_lines:
  78. additional_line = separator * len(lines[0])
  79. lines += [additional_line for i in range(max_height - len(lines))]
  80. assert len(child_lines[0]) == max_height
  81. from copy import deepcopy
  82. result = deepcopy(child_lines[0])
  83. for lines in child_lines[1:]:
  84. assert len(lines) == max_height
  85. for i, line in enumerate(lines):
  86. result[i] += separator + line
  87. line_width = node_width[node]
  88. box_widths = [len(lines[0]) for lines in child_lines]
  89. node_len = len(node)
  90. middle_node = int(node_len / 2)
  91. middle = sum([box_widths[i] for i in range(middle_node)]) \
  92. + max(middle_node - int(node_len % 2 == 0), 0) * separator_len
  93. title_line = center_text(node.title(), line_width, middle)
  94. node_mid = node_len / 2
  95. pipe_sign = '│'.decode('utf-8')
  96. dash_sign = '─'.decode('utf-8')
  97. cross_sign = '┼'.decode('utf-8')
  98. tsplit_dn_sign = '┬'.decode('utf-8')
  99. tsplit_up_sign = '┴'.decode('utf-8')
  100. left_sign = '╭'.decode('utf-8')
  101. right_sign = '╮'.decode('utf-8')
  102. if node_len == 1:
  103. # Unary operators
  104. edge_line = center_text(pipe_sign, box_widths[0], middle)
  105. elif node_len % 2:
  106. # n-ary operators (n is odd)
  107. edge_line = ''
  108. for i, child in enumerate(node):
  109. if i > 0:
  110. edge_line += dash_sign
  111. if i < middle_node:
  112. marker = (left_sign if i == 0 else tsplit_dn_sign)
  113. edge_line += center_text(marker, box_widths[i],
  114. middle=0, right=dash_sign)
  115. else:
  116. if i == middle_node:
  117. marker = cross_sign
  118. edge_line += center_text(marker, box_widths[i],
  119. middle=0, right=dash_sign,
  120. left=dash_sign)
  121. else:
  122. if i == node_len - 1:
  123. marker = right_sign
  124. else:
  125. marker = tsplit_dn_sign
  126. edge_line += center_text(marker, box_widths[i],
  127. middle=0, left=dash_sign)
  128. else:
  129. # n-ary operators (n is even)
  130. edge_line = ''
  131. for i, child in enumerate(node):
  132. if i > 0:
  133. if i == middle_node:
  134. edge_line += tsplit_up_sign
  135. else:
  136. edge_line += dash_sign
  137. if i < middle_node:
  138. marker = (left_sign if i == 0 else tsplit_sign)
  139. edge_line += center_text(marker, box_widths[i],
  140. middle=0, right=dash_sign)
  141. else:
  142. marker = (right_sign if i == node_len - 1 else tsplit_sign)
  143. edge_line += center_text(marker, box_widths[i],
  144. middle=0, left=dash_sign)
  145. try:
  146. assert len(title_line) == len(edge_line)
  147. except AssertionError:
  148. print '------------------'
  149. print 'line_width:', line_width
  150. print 'title_line:', title_line, 'len:', len(title_line)
  151. print 'edge_line: %s (%d)' % (edge_line.encode('utf-8'),
  152. len(edge_line))
  153. print 'lines:'
  154. print '\n'.join(map(lambda x: x + ' %d' % len(x), lines))
  155. raise Exception()
  156. # Add the line of this node before all child lines.
  157. return [title_line, edge_line] + result
  158. calculate_width(root)
  159. #if verbose:
  160. # print '------- node_width ---------'
  161. # for node, width in node_width.iteritems():
  162. # print node.title(), 'width:', width
  163. lines = format_lines(root)
  164. # Strip trailing whitespace.
  165. return '\n'.join(map(lambda x: x.rstrip(), lines)).encode('utf-8')
  166. def center_text(text, width, middle=0, left=' ', right=' '):
  167. """
  168. >>> print center_text('│', 1, 1)
  169. >>> center_text('+', 15, 11)
  170. ' + '
  171. """
  172. text_len = len(text)
  173. text_mid = text_len / 2
  174. #print '---------'
  175. #print 'text_len:', text_len
  176. #print 'text_mid:', text_mid
  177. #print 'width:', width
  178. #print 'middle:', middle
  179. #print '---------'
  180. # TODO: this code requires cleanup.
  181. if middle:
  182. # If this is true, the text is at the left.
  183. if text_mid > middle:
  184. text += left * (width - text_len)
  185. # If this is true, the text is at the right.
  186. elif text_mid > (width - middle):
  187. text = right * (width - text_len) + text
  188. # Else, the text has spacing on its left and right.
  189. else:
  190. text = left * (middle - text_mid) + text
  191. text += right * (width - len(text))
  192. return text
  193. spacing = width - text_len
  194. # Even number of spaces can be split equally.
  195. if spacing % 2 == 0:
  196. return left * (spacing / 2) + text + right * (spacing / 2)
  197. # For an odd number of space, put the extra space at the end.
  198. return left * (spacing / 2) + text + right * (spacing / 2 + 1)