2
2
3
3
import collections
4
4
import re
5
- from typing import Any , Callable , Dict , List , Optional , Tuple , TypeVar , Union
5
+ from typing import Any , Callable , Dict , List , Optional , Set , Tuple , TypeVar , Union
6
6
7
7
from bigtree .node import node
8
8
from bigtree .tree .export .stdout import yield_tree
22
22
23
23
Image = ImageDraw = ImageFont = MagicMock ()
24
24
25
+ try :
26
+ import matplotlib as mpl
27
+ from matplotlib .colors import Normalize
28
+ except ImportError : # pragma: no cover
29
+ from unittest .mock import MagicMock
30
+
31
+ mpl = MagicMock ()
32
+ Normalize = MagicMock ()
33
+
25
34
26
35
__all__ = [
27
36
"tree_to_dot" ,
@@ -211,6 +220,21 @@ def _recursive_append(parent_name: Optional[str], child_node: T) -> None:
211
220
return _graph
212
221
213
222
223
+ def _load_font (font_family : str , font_size : int ) -> ImageFont .truetype :
224
+ if not font_family :
225
+ from urllib .request import urlopen
226
+
227
+ dejavusans_url = "https://github.com/kayjan/bigtree/raw/master/assets/DejaVuSans.ttf?raw=true"
228
+ font_family = urlopen (dejavusans_url )
229
+ try :
230
+ font = ImageFont .truetype (font_family , font_size )
231
+ except OSError :
232
+ raise ValueError (
233
+ f"Font file { font_family } is not found, set `font_family` parameter to point to a valid .ttf file."
234
+ )
235
+ return font
236
+
237
+
214
238
@exceptions .optional_dependencies_image ("Pillow" )
215
239
def tree_to_pillow_graph (
216
240
tree : T ,
@@ -225,9 +249,10 @@ def tree_to_pillow_graph(
225
249
text_align : str = "center" ,
226
250
bg_colour : Union [Tuple [int , int , int ], str ] = "white" ,
227
251
rect_margin : Optional [Dict [str , int ]] = None ,
228
- rect_fill : Union [Tuple [int , int , int ], str ] = "white" ,
252
+ rect_fill : Union [Tuple [int , int , int ], str , mpl .colors .Colormap ] = "white" ,
253
+ rect_cmap_attr : Optional [str ] = None ,
229
254
rect_outline : Union [Tuple [int , int , int ], str ] = "black" ,
230
- rect_width : Union [ float , int ] = 1 ,
255
+ rect_width : int = 1 ,
231
256
) -> Image .Image :
232
257
r"""Export tree to PIL.Image.Image object. Object can be
233
258
converted to other formats, such as jpg, or png. Image will look
@@ -267,13 +292,21 @@ def tree_to_pillow_graph(
267
292
text_align (str): text align for multi-line text
268
293
bg_colour (Union[Tuple[int, int, int], str]): background of image, accepts tuple of RGB values or string, defaults to white
269
294
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
295
+ rect_fill (Union[Tuple[int, int, int], str, mpl.colormap]): (for rectangle) colour to use for fill
296
+ rect_cmap_attr (str): (for rectangle) if rect_fill is a colormap, attribute of node to retrieve fill from colormap,
297
+ must be a float/int attribute
271
298
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
299
+ rect_width (int): (for rectangle) line width, in pixels
273
300
274
301
Returns:
275
302
(PIL.Image.Image)
276
303
"""
304
+ use_cmap = isinstance (rect_fill , mpl .colors .Colormap )
305
+ if use_cmap and rect_cmap_attr is None :
306
+ raise ValueError (
307
+ "`rect_cmap_attr` cannot be None if rect_fill is mpl.colormaps"
308
+ )
309
+
277
310
default_margin = {"t" : 10 , "b" : 10 , "l" : 10 , "r" : 10 }
278
311
default_rect_margin = {"t" : 5 , "b" : 5 , "l" : 5 , "r" : 5 }
279
312
if not margin :
@@ -286,19 +319,10 @@ def tree_to_pillow_graph(
286
319
rect_margin = {** default_rect_margin , ** rect_margin }
287
320
288
321
# Initialize font
289
- if not font_family :
290
- from urllib .request import urlopen
322
+ font = _load_font (font_family , font_size )
291
323
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
324
+ # Iterate tree once to obtain attributes
325
+ # Calculate image dimension from text, get range for colourmap if applicable
302
326
_max_text_width = 0
303
327
_max_text_height = 0
304
328
_image = Image .new ("RGB" , (0 , 0 ))
@@ -309,11 +333,11 @@ def get_node_text(_node: T, _node_content: str) -> str:
309
333
matches = re .findall (pattern , _node_content )
310
334
for match in matches :
311
335
_node_content = _node_content .replace (
312
- f"{{{ match } }}" ,
313
- str (_node .get_attr (match )) if _node .get_attr (match ) else "" ,
336
+ f"{{{ match } }}" , str (_node .get_attr (match , "" ))
314
337
)
315
338
return _node_content
316
339
340
+ cmap_range : Set [Union [float , int ]] = set ()
317
341
for _ , _ , _node in yield_tree (tree ):
318
342
l , t , r , b = _draw .multiline_textbbox (
319
343
(0 , 0 ), get_node_text (_node , node_content ), font = font
@@ -324,6 +348,15 @@ def get_node_text(_node: T, _node_content: str) -> str:
324
348
_max_text_height = max (
325
349
_max_text_height , t + b + rect_margin .get ("t" , 0 ) + rect_margin .get ("b" , 0 )
326
350
)
351
+ if use_cmap :
352
+ cmap_range .add (_node .get_attr (rect_cmap_attr , 0 ))
353
+
354
+ cmap_dict = {}
355
+ if use_cmap :
356
+ norm = Normalize (vmin = min (cmap_range ), vmax = max (cmap_range ))
357
+ cmap_range_list = [norm (c ) for c in cmap_range ]
358
+ cmap_colour_list = rect_fill (cmap_range_list ) # type: ignore
359
+ cmap_dict = dict (zip (cmap_range_list , cmap_colour_list ))
327
360
328
361
# Get x, y, coordinates and height, width of diagram
329
362
from bigtree .utils .plot import reingold_tilford
@@ -357,8 +390,13 @@ def get_node_text(_node: T, _node_content: str) -> str:
357
390
x1 , x2 = _x - 0.5 * _max_text_width , _x + 0.5 * _max_text_width
358
391
y1 , y2 = _y - 0.5 * _max_text_height , _y + 0.5 * _max_text_height
359
392
# Draw box
393
+ _rect_fill = rect_fill
394
+ if use_cmap :
395
+ _rect_fill = mpl .colors .rgb2hex (
396
+ cmap_dict [norm (_node .get_attr (rect_cmap_attr , 0 ))]
397
+ )
360
398
image_draw .rectangle (
361
- [x1 , y1 , x2 , y2 ], fill = rect_fill , outline = rect_outline , width = rect_width
399
+ [x1 , y1 , x2 , y2 ], fill = _rect_fill , outline = rect_outline , width = rect_width
362
400
)
363
401
# Draw text
364
402
image_draw .text (
@@ -445,17 +483,7 @@ def tree_to_pillow(
445
483
(PIL.Image.Image)
446
484
"""
447
485
# Initialize font
448
- if not font_family :
449
- from urllib .request import urlopen
450
-
451
- dejavusans_url = "https://github.com/kayjan/bigtree/raw/master/assets/DejaVuSans.ttf?raw=true"
452
- font_family = urlopen (dejavusans_url )
453
- try :
454
- font = ImageFont .truetype (font_family , font_size )
455
- except OSError :
456
- raise ValueError (
457
- f"Font file { font_family } is not found, set `font_family` parameter to point to a valid .ttf file."
458
- )
486
+ font = _load_font (font_family , font_size )
459
487
460
488
# Initialize text
461
489
image_text = []
0 commit comments