1
1
from __future__ import annotations
2
2
3
3
import collections
4
+ import re
4
5
from typing import Any , Callable , Dict , List , Optional , Tuple , TypeVar , Union
5
6
6
7
from bigtree .node import node
24
25
25
26
__all__ = [
26
27
"tree_to_dot" ,
28
+ "tree_to_pillow_graph" ,
27
29
"tree_to_pillow" ,
28
30
"tree_to_mermaid" ,
29
31
]
@@ -67,8 +69,10 @@ def tree_to_dot(
67
69
68
70
Export to image, dot file, etc.
69
71
70
- >>> graph.write_png("assets/docstr/tree.png")
71
- >>> graph.write_dot("assets/docstr/tree.dot")
72
+ >>> graph.write_png("assets/tree.png")
73
+ >>> graph.write_dot("assets/tree.dot")
74
+
75
+ 
72
76
73
77
Export to string
74
78
@@ -207,6 +211,193 @@ def _recursive_append(parent_name: Optional[str], child_node: T) -> None:
207
211
return _graph
208
212
209
213
214
+ @exceptions .optional_dependencies_image ("Pillow" )
215
+ def tree_to_pillow_graph (
216
+ tree : T ,
217
+ node_content : str = "{node_name}" ,
218
+ * ,
219
+ margin : Optional [Dict [str , int ]] = None ,
220
+ height_buffer : Union [int , float ] = 20 ,
221
+ width_buffer : Union [int , float ] = 10 ,
222
+ font_family : str = "" ,
223
+ font_size : int = 12 ,
224
+ font_colour : Union [Tuple [int , int , int ], str ] = "black" ,
225
+ text_align : str = "center" ,
226
+ bg_colour : Union [Tuple [int , int , int ], str ] = "white" ,
227
+ rect_margin : Optional [Dict [str , int ]] = None ,
228
+ rect_fill : Union [Tuple [int , int , int ], str ] = "white" ,
229
+ rect_outline : Union [Tuple [int , int , int ], str ] = "black" ,
230
+ rect_width : Union [float , int ] = 1 ,
231
+ ) -> Image .Image :
232
+ r"""Export tree to PIL.Image.Image object. Object can be
233
+ converted to other formats, such as jpg, or png. Image will look
234
+ like a tree/graph-like structure.
235
+
236
+ Customisations:
237
+
238
+ - To change the margin of tree within diagram, vary `margin`
239
+ - To change the margin of the text within node, vary `rect_margin`
240
+ - For more separation between nodes, change `height_buffer` and `width_buffer`
241
+
242
+ Examples:
243
+ >>> from bigtree import Node, tree_to_pillow_graph
244
+ >>> root = Node("a", age=90)
245
+ >>> b = Node("b", age=65, parent=root)
246
+ >>> c = Node("c", age=60, parent=root)
247
+ >>> d = Node("d", age=40, parent=b)
248
+ >>> e = Node("e", age=35, parent=b)
249
+ >>> pillow_image = tree_to_pillow_graph(root, node_content="{node_name}\nAge: {age}")
250
+
251
+ Export to image (PNG, JPG) file, etc.
252
+
253
+ >>> pillow_image.save("assets/tree_pillow_graph.png")
254
+ >>> pillow_image.save("assets/tree_pillow_graph.jpg")
255
+
256
+ 
257
+
258
+ Args:
259
+ tree (Node): tree to be exported
260
+ node_content (str): display text in node
261
+ margin (Dict[str, int]): margin of diagram
262
+ height_buffer (Union[int, float]): height buffer between node layers, in pixels
263
+ width_buffer (Union[int, float]) : width buffer between sibling nodes, in pixels
264
+ font_family (str): file path of font family, requires .ttf file, defaults to DejaVuSans
265
+ font_size (int): font size, defaults to 12
266
+ font_colour (Union[Tuple[int, int, int], str]): font colour, accepts tuple of RGB values or string, defaults to black
267
+ text_align (str): text align for multi-line text
268
+ bg_colour (Union[Tuple[int, int, int], str]): background of image, accepts tuple of RGB values or string, defaults to white
269
+ rect_margin (Dict[str, int]): (for rectangle) margin of text to rectangle, in pixels
270
+ rect_fill (Union[Tuple[int, int, int], str]): (for rectangle) colour to use for fill
271
+ rect_outline (Union[Tuple[int, int, int], str]): (for rectangle) colour to use for outline
272
+ rect_width (Union[float, int]): (for rectangle) line width, in pixels
273
+
274
+ Returns:
275
+ (PIL.Image.Image)
276
+ """
277
+ default_margin = {"t" : 10 , "b" : 10 , "l" : 10 , "r" : 10 }
278
+ default_rect_margin = {"t" : 5 , "b" : 5 , "l" : 5 , "r" : 5 }
279
+ if not margin :
280
+ margin = default_margin
281
+ else :
282
+ margin = {** default_margin , ** margin }
283
+ if not rect_margin :
284
+ rect_margin = default_rect_margin
285
+ else :
286
+ rect_margin = {** default_rect_margin , ** rect_margin }
287
+
288
+ # Initialize font
289
+ if not font_family :
290
+ from urllib .request import urlopen
291
+
292
+ dejavusans_url = "https://github.com/kayjan/bigtree/raw/master/assets/DejaVuSans.ttf?raw=true"
293
+ font_family = urlopen (dejavusans_url )
294
+ try :
295
+ font = ImageFont .truetype (font_family , font_size )
296
+ except OSError :
297
+ raise ValueError (
298
+ f"Font file { font_family } is not found, set `font_family` parameter to point to a valid .ttf file."
299
+ )
300
+
301
+ # Calculate image dimension from text, otherwise override with argument
302
+ _max_text_width = 0
303
+ _max_text_height = 0
304
+ _image = Image .new ("RGB" , (0 , 0 ))
305
+ _draw = ImageDraw .Draw (_image )
306
+
307
+ def get_node_text (_node : T , _node_content : str ) -> str :
308
+ pattern = re .compile (r"\{(.*)\}" )
309
+ matches = re .findall (pattern , _node_content )
310
+ for match in matches :
311
+ _node_content = _node_content .replace (
312
+ f"{{{ match } }}" ,
313
+ str (_node .get_attr (match )) if _node .get_attr (match ) else "" ,
314
+ )
315
+ return _node_content
316
+
317
+ for _ , _ , _node in yield_tree (tree ):
318
+ l , t , r , b = _draw .multiline_textbbox (
319
+ (0 , 0 ), get_node_text (_node , node_content ), font = font
320
+ )
321
+ _max_text_width = max (
322
+ _max_text_width , l + r + rect_margin .get ("l" , 0 ) + rect_margin .get ("r" , 0 )
323
+ )
324
+ _max_text_height = max (
325
+ _max_text_height , t + b + rect_margin .get ("t" , 0 ) + rect_margin .get ("b" , 0 )
326
+ )
327
+
328
+ # Get x, y, coordinates and height, width of diagram
329
+ from bigtree .utils .plot import reingold_tilford
330
+
331
+ tree = tree .copy ()
332
+ reingold_tilford (
333
+ tree ,
334
+ subtree_separation = _max_text_width + width_buffer ,
335
+ sibling_separation = _max_text_width + width_buffer ,
336
+ level_separation = _max_text_height + height_buffer ,
337
+ x_offset = 0.5 * _max_text_width + margin .get ("l" , 0 ),
338
+ y_offset = 0.5 * _max_text_height + margin .get ("t" , 0 ),
339
+ reverse = True , # top-left corner is (0, 0)
340
+ )
341
+
342
+ _width , _height = 0 , 0
343
+ _width_margin = 0.5 * _max_text_width + margin .get ("r" , 0 )
344
+ _height_margin = 0.5 * _max_text_height + margin .get ("b" )
345
+ for _ , _ , _node in yield_tree (tree ):
346
+ _width = max (_width , _node .get_attr ("x" ) + _width_margin )
347
+ _height = max (_height , _node .get_attr ("y" ) + _height_margin )
348
+ _width = int (round (_width + 0.5 , 0 ))
349
+ _height = int (round (_height + 0.5 , 0 ))
350
+
351
+ # Initialize and draw image
352
+ image = Image .new ("RGB" , (_width , _height ), bg_colour )
353
+ image_draw = ImageDraw .Draw (image )
354
+
355
+ for _ , _ , _node in yield_tree (tree ):
356
+ _x , _y = _node .get_attr ("x" ), _node .get_attr ("y" )
357
+ x1 , x2 = _x - 0.5 * _max_text_width , _x + 0.5 * _max_text_width
358
+ y1 , y2 = _y - 0.5 * _max_text_height , _y + 0.5 * _max_text_height
359
+ # Draw box
360
+ image_draw .rectangle (
361
+ [x1 , y1 , x2 , y2 ], fill = rect_fill , outline = rect_outline , width = rect_width
362
+ )
363
+ # Draw text
364
+ image_draw .text (
365
+ (x1 + rect_margin .get ("l" , 0 ), y1 + rect_margin .get ("t" , 0 )),
366
+ get_node_text (_node , node_content ),
367
+ font = font ,
368
+ fill = font_colour ,
369
+ align = text_align ,
370
+ )
371
+ # Draw line to parent
372
+ if _node .parent :
373
+ _child_x , _child_y = (
374
+ _node .get_attr ("x" ),
375
+ _node .get_attr ("y" ) - 0.5 * _max_text_height ,
376
+ )
377
+ _parent_x , _parent_y = (
378
+ _node .parent .get_attr ("x" ),
379
+ _node .parent .get_attr ("y" ) + 0.5 * _max_text_height ,
380
+ )
381
+ middle_y = (_child_y + _parent_y ) / 2
382
+ image_draw .line (
383
+ (_parent_x , _parent_y , _parent_x , middle_y ),
384
+ fill = rect_outline ,
385
+ width = rect_width ,
386
+ )
387
+ image_draw .line (
388
+ (_parent_x , middle_y , _child_x , middle_y ),
389
+ fill = rect_outline ,
390
+ width = rect_width ,
391
+ )
392
+ image_draw .line (
393
+ (_child_x , _child_y , _child_x , middle_y ),
394
+ fill = rect_outline ,
395
+ width = rect_width ,
396
+ )
397
+
398
+ return image
399
+
400
+
210
401
@exceptions .optional_dependencies_image ("Pillow" )
211
402
def tree_to_pillow (
212
403
tree : T ,
@@ -220,7 +411,7 @@ def tree_to_pillow(
220
411
** kwargs : Any ,
221
412
) -> Image .Image :
222
413
"""Export tree to PIL.Image.Image object. Object can be
223
- converted to other format , such as jpg, or png. Image will be
414
+ converted to other formats , such as jpg, or png. Image will be
224
415
similar format as `print_tree`, accepts additional keyword arguments
225
416
as input to `yield_tree`.
226
417
0 commit comments