graph.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # vim: set fileencoding=utf-8 :
  2. # XXX Used in doctests (we should use them in the __main__ section below too).
  3. def generate_graph(root, 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. >>> from node import Leaf, Node
  10. >>> l0, l1 = Leaf(0), Leaf(1)
  11. >>> n0 = Node('+', l0, l1)
  12. >>> l2 = Leaf(2)
  13. >>> print generate_graph(n0)
  14. +
  15. ╭┴╮
  16. 0 1
  17. >>> n1 = Node('-', l2)
  18. >>> print generate_graph(n1)
  19. -
  20. 2
  21. >>> n2 = Node('*', n0, n1)
  22. >>> print generate_graph(n2)
  23. *
  24. ╭─┴╮
  25. + -
  26. ╭┴╮ │
  27. 0 1 2
  28. """
  29. node_width = {}
  30. node_middle = {}
  31. separator_len = len(separator)
  32. def calculate_node_sizes(node):
  33. title = node.title()
  34. title_len = len(title)
  35. # Leaves do not have children and therefore the length of its title is
  36. # the width of the leaf.
  37. if not node.nodes:
  38. node_width[node] = title_len
  39. node_middle[node] = int((title_len - 1) / 2)
  40. return title_len
  41. node_len = len(node)
  42. width = 0
  43. middle = 0
  44. middle_pos = int(node_len / 2)
  45. for i, child in enumerate(node):
  46. tmp = calculate_node_sizes(child)
  47. if i < middle_pos:
  48. middle += tmp
  49. width += tmp
  50. middle += max(middle_pos - int(node_len % 2 == 0), 0) * separator_len
  51. # Add a separator between each node (thus n - 1 separators).
  52. width += separator_len * (node_len - 1)
  53. # If the title of the node is wider than the sum of its children, the
  54. # title's width should be used.
  55. if title_len > width:
  56. width = title_len
  57. middle = int(title_len / 2)
  58. # print 'width of "%s":' % node.title(), width
  59. node_width[node] = width
  60. node_middle[node] = middle
  61. return width
  62. def format_lines(node):
  63. if not node.nodes:
  64. # Leaf titles do not need to be centered, since the parent will
  65. # center those lines. And if there are no parents, the entire graph
  66. # consists of a single leaf, so in that case there still is no
  67. # reason to center it.
  68. return [node.title()]
  69. # At least one child, otherwise it would be a leaf.
  70. assert node[0]
  71. child_lines = [format_lines(child) for child in node]
  72. max_height = max(map(len, child_lines))
  73. # Assert that all child boxes are of equal height
  74. for lines in child_lines:
  75. additional_line = separator * len(lines[0])
  76. lines += [additional_line for i in range(max_height - len(lines))]
  77. assert len(child_lines[0]) == max_height
  78. from copy import deepcopy
  79. result = deepcopy(child_lines[0])
  80. for lines in child_lines[1:]:
  81. assert len(lines) == max_height
  82. for i, line in enumerate(lines):
  83. result[i] += separator + line
  84. line_width = node_width[node]
  85. # TODO: substitute box_widths with node_width
  86. box_widths = [len(lines[0]) for lines in child_lines]
  87. node_len = len(node)
  88. middle_node = int(node_len / 2)
  89. #middle = sum([box_widths[i] for i in range(middle_node)]) \
  90. # + max(middle_node - int(node_len % 2 == 0), 0) * separator_len
  91. middle = node_middle[node]
  92. title_line = center_text(node.title(), line_width, middle)
  93. pipe_sign = '│'.decode('utf-8')
  94. dash_sign = '─'.decode('utf-8')
  95. cross_sign = '┼'.decode('utf-8')
  96. tsplit_dn_sign = '┬'.decode('utf-8')
  97. tsplit_up_sign = '┴'.decode('utf-8')
  98. left_sign = '╭'.decode('utf-8')
  99. right_sign = '╮'.decode('utf-8')
  100. if node_len == 1:
  101. # Unary operators
  102. edge_line = center_text(pipe_sign, box_widths[0], middle)
  103. elif node_len % 2:
  104. # n-ary operators (n is odd)
  105. edge_line = ''
  106. for i, child in enumerate(node):
  107. if i > 0:
  108. edge_line += dash_sign
  109. if i < middle_node:
  110. marker = (left_sign if i == 0 else tsplit_dn_sign)
  111. edge_line += center_text(marker, box_widths[i],
  112. middle=0, right=dash_sign)
  113. else:
  114. if i == middle_node:
  115. marker = cross_sign
  116. edge_line += center_text(marker, box_widths[i],
  117. middle=0, right=dash_sign,
  118. left=dash_sign)
  119. else:
  120. if i == node_len - 1:
  121. marker = right_sign
  122. else:
  123. marker = tsplit_dn_sign
  124. edge_line += center_text(marker, box_widths[i],
  125. middle=0, left=dash_sign)
  126. else:
  127. # n-ary operators (n is even)
  128. edge_line = ''
  129. for i, child in enumerate(node):
  130. if i > 0:
  131. if i == middle_node:
  132. edge_line += tsplit_up_sign
  133. else:
  134. edge_line += dash_sign
  135. if i < middle_node:
  136. marker = (left_sign if i == 0 else tsplit_dn_sign)
  137. edge_line += center_text(marker, box_widths[i],
  138. middle=node_middle[child],
  139. right=dash_sign)
  140. else:
  141. if i == node_len - 1:
  142. marker = right_sign
  143. else:
  144. marker = tsplit_dn_sign
  145. edge_line += center_text(marker, box_widths[i],
  146. middle=node_middle[child],
  147. left=dash_sign)
  148. try:
  149. assert len(title_line) == len(edge_line)
  150. except AssertionError: # pragma: nocover
  151. print '------------------'
  152. print 'line_width:', line_width
  153. print 'title_line:', title_line, 'len:', len(title_line)
  154. print 'edge_line: %s (%d)' % (edge_line.encode('utf-8'),
  155. len(edge_line))
  156. print 'lines:'
  157. print '\n'.join(map(lambda x: x + ' %d' % len(x), lines))
  158. raise Exception()
  159. # Add the line of this node before all child lines.
  160. return [title_line, edge_line] + result
  161. calculate_node_sizes(root)
  162. if verbose: # pragma: nocover
  163. print '------- node_{width,middle} ---------'
  164. for node, width in node_width.iteritems():
  165. print node.title(), 'width:', width, 'middle:', node_middle[node]
  166. lines = format_lines(root)
  167. # Strip trailing whitespace.
  168. return '\n'.join(map(lambda x: x.rstrip(), lines)).encode('utf-8')
  169. def center_text(text, width, middle=0, left=' ', right=' '):
  170. """
  171. >>> print center_text('│', 1, 1)
  172. >>> center_text('+', 15, 11)
  173. ' + '
  174. >>> left = center_text('╭'.decode('utf-8'), 11, 8, right='─'.decode('utf-8'))
  175. >>> len(left) == 11
  176. True
  177. >>> right = center_text('╮'.decode('utf-8'), 3, 2, left='─'.decode('utf-8'))
  178. >>> len(right) == 3
  179. True
  180. >>> edge_line = left + '┴'.decode('utf-8') + right
  181. >>> len(edge_line) == 15
  182. True
  183. >>> title_line = center_text('+', 15, 11)
  184. >>> print '|%s|\\n|%s|' % (title_line, edge_line.encode('utf-8'))
  185. | + |
  186. | ╭──┴──╮|
  187. """
  188. text_len = len(text)
  189. text_mid = text_len / 2
  190. #print '---------'
  191. #print 'text_len:', text_len
  192. #print 'text_mid:', text_mid
  193. #print 'width:', width
  194. #print 'middle:', middle
  195. #print '---------'
  196. # TODO: this code requires cleanup.
  197. if middle:
  198. # If this is true, the text is at the left.
  199. if text_mid > middle:
  200. text += left * (width - text_len)
  201. # If this is true, the text is at the right.
  202. elif text_mid > (width - middle):
  203. text = right * (width - text_len) + text
  204. # Else, the text has spacing on its left and right.
  205. else:
  206. text = left * (middle - text_mid) + text
  207. text += right * (width - len(text))
  208. return text
  209. spacing = width - text_len
  210. # Even number of spaces can be split equally.
  211. if spacing % 2 == 0:
  212. return left * (spacing / 2) + text + right * (spacing / 2)
  213. # For an odd number of space, put the extra space at the end.
  214. return left * (spacing / 2) + text + right * (spacing / 2 + 1)