plotnine 0.14.5__py3-none-any.whl → 0.15.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. plotnine/__init__.py +31 -37
  2. plotnine/_mpl/gridspec.py +265 -0
  3. plotnine/_mpl/layout_manager/__init__.py +6 -0
  4. plotnine/_mpl/layout_manager/_engine.py +87 -0
  5. plotnine/_mpl/layout_manager/_layout_items.py +957 -0
  6. plotnine/_mpl/layout_manager/_layout_tree.py +905 -0
  7. plotnine/_mpl/layout_manager/_spaces.py +1154 -0
  8. plotnine/_mpl/patches.py +70 -34
  9. plotnine/_mpl/text.py +159 -37
  10. plotnine/_mpl/utils.py +78 -10
  11. plotnine/_utils/__init__.py +35 -9
  12. plotnine/_utils/dev.py +45 -27
  13. plotnine/_utils/yippie.py +115 -0
  14. plotnine/animation.py +1 -1
  15. plotnine/coords/coord.py +3 -3
  16. plotnine/coords/coord_trans.py +1 -1
  17. plotnine/data/__init__.py +43 -8
  18. plotnine/data/anscombe-quartet.csv +45 -0
  19. plotnine/doctools.py +2 -2
  20. plotnine/facets/facet.py +34 -43
  21. plotnine/facets/facet_grid.py +14 -6
  22. plotnine/facets/facet_wrap.py +3 -5
  23. plotnine/facets/strips.py +20 -33
  24. plotnine/geoms/annotate.py +3 -3
  25. plotnine/geoms/annotation_logticks.py +2 -0
  26. plotnine/geoms/annotation_stripes.py +2 -0
  27. plotnine/geoms/geom.py +3 -3
  28. plotnine/geoms/geom_bar.py +10 -2
  29. plotnine/geoms/geom_col.py +6 -0
  30. plotnine/geoms/geom_crossbar.py +2 -3
  31. plotnine/geoms/geom_path.py +2 -2
  32. plotnine/geoms/geom_violin.py +24 -7
  33. plotnine/ggplot.py +95 -66
  34. plotnine/guides/guide.py +19 -20
  35. plotnine/guides/guide_colorbar.py +6 -6
  36. plotnine/guides/guide_legend.py +15 -16
  37. plotnine/guides/guides.py +8 -8
  38. plotnine/helpers.py +49 -0
  39. plotnine/iapi.py +33 -7
  40. plotnine/labels.py +8 -3
  41. plotnine/layer.py +4 -4
  42. plotnine/mapping/_env.py +2 -2
  43. plotnine/mapping/_eval_environment.py +85 -0
  44. plotnine/mapping/aes.py +14 -30
  45. plotnine/mapping/evaluation.py +7 -65
  46. plotnine/options.py +14 -7
  47. plotnine/plot_composition/__init__.py +10 -0
  48. plotnine/plot_composition/_compose.py +462 -0
  49. plotnine/plot_composition/_plotspec.py +50 -0
  50. plotnine/plot_composition/_spacer.py +32 -0
  51. plotnine/positions/position_dodge.py +1 -1
  52. plotnine/positions/position_dodge2.py +1 -1
  53. plotnine/positions/position_stack.py +1 -2
  54. plotnine/qplot.py +1 -2
  55. plotnine/scales/__init__.py +0 -6
  56. plotnine/scales/limits.py +7 -7
  57. plotnine/scales/scale.py +4 -4
  58. plotnine/scales/scale_continuous.py +2 -1
  59. plotnine/scales/scale_identity.py +10 -2
  60. plotnine/scales/scale_manual.py +6 -2
  61. plotnine/stats/binning.py +5 -2
  62. plotnine/stats/smoothers.py +3 -5
  63. plotnine/stats/stat.py +3 -3
  64. plotnine/stats/stat_bindot.py +1 -3
  65. plotnine/stats/stat_density.py +2 -2
  66. plotnine/stats/stat_qq_line.py +1 -1
  67. plotnine/stats/stat_sina.py +34 -1
  68. plotnine/themes/elements/__init__.py +3 -0
  69. plotnine/themes/elements/element_text.py +35 -24
  70. plotnine/themes/elements/margin.py +137 -61
  71. plotnine/themes/targets.py +3 -1
  72. plotnine/themes/theme.py +21 -7
  73. plotnine/themes/theme_538.py +0 -1
  74. plotnine/themes/theme_bw.py +0 -1
  75. plotnine/themes/theme_dark.py +0 -1
  76. plotnine/themes/theme_gray.py +32 -34
  77. plotnine/themes/theme_light.py +1 -1
  78. plotnine/themes/theme_matplotlib.py +28 -31
  79. plotnine/themes/theme_seaborn.py +36 -36
  80. plotnine/themes/theme_void.py +25 -27
  81. plotnine/themes/theme_xkcd.py +0 -1
  82. plotnine/themes/themeable.py +369 -169
  83. plotnine/typing.py +3 -3
  84. plotnine/watermark.py +3 -3
  85. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/METADATA +8 -5
  86. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/RECORD +89 -78
  87. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/WHEEL +1 -1
  88. plotnine/_mpl/_plot_side_space.py +0 -888
  89. plotnine/_mpl/_plotnine_tight_layout.py +0 -293
  90. plotnine/_mpl/layout_engine.py +0 -110
  91. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info/licenses}/LICENSE +0 -0
  92. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,905 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ from dataclasses import dataclass
5
+ from functools import cached_property
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+
10
+ from plotnine.plot_composition import OR
11
+
12
+ from ._spaces import LayoutSpaces
13
+
14
+ if TYPE_CHECKING:
15
+ from typing import Sequence
16
+
17
+ from plotnine import ggplot
18
+ from plotnine._mpl.gridspec import p9GridSpec
19
+ from plotnine.plot_composition import Compose
20
+
21
+
22
+ @dataclass
23
+ class LayoutTree:
24
+ """
25
+ A Tree representation of the composition
26
+
27
+ The purpose of this class (and its subclasses) is to align and
28
+ and resize plots in a composition.
29
+
30
+ For example, this composition;
31
+
32
+ (p1 | p2) | (p3 / p4)
33
+
34
+ where p1, p2, p3 & p4 are ggplot objects would look like this;
35
+
36
+ -----------------------------
37
+ | | | |
38
+ | | | |
39
+ | | | |
40
+ | | | |
41
+ | | |---------|
42
+ | | | |
43
+ | | | |
44
+ | | | |
45
+ | | | |
46
+ -----------------------------
47
+
48
+ and the tree would have this structure;
49
+
50
+ ColumnsTree
51
+ |
52
+ ----------------------------
53
+ | | |
54
+ LayoutSpaces LayoutSpaces RowsTree
55
+ |
56
+ -------------
57
+ | |
58
+ LayoutSpaces LayoutSpaces
59
+
60
+ Each composition is a tree or subtree
61
+ """
62
+
63
+ gridspec: p9GridSpec
64
+ """
65
+ Gridspec of the composition
66
+
67
+ Originally this gridspec occupies all the space available to it so the
68
+ subplots are of equal sizes. As each subplot contains full ggplot,
69
+ differences in texts and legend sizes may make the panels (panel area)
70
+ have unequal sizes. We can resize the panels, by changing the height
71
+ and width ratios of this (composition) gridspec.
72
+
73
+ The information about the size (width & height) of the panels is in the
74
+ LayoutSpaces.
75
+ """
76
+
77
+ nodes: list[LayoutSpaces | LayoutTree]
78
+ """
79
+ The spaces or tree of spaces in the composition that the tree
80
+ represents.
81
+ """
82
+
83
+ @staticmethod
84
+ def create(
85
+ cmp: Compose,
86
+ lookup_spaces: dict[ggplot, LayoutSpaces],
87
+ ) -> LayoutTree:
88
+ """
89
+ Create a LayoutTree for this composition
90
+
91
+ Parameters
92
+ ----------
93
+ cmp :
94
+ Composition
95
+ lookup_spaces :
96
+ A table to lookup the LayoutSpaces for each plot.
97
+
98
+ Notes
99
+ -----
100
+ LayoutTree works by modifying the `.gridspec` of the compositions,
101
+ and the `LayoutSpaces` of the plots.
102
+ """
103
+ from plotnine import ggplot
104
+
105
+ nodes: list[LayoutSpaces | LayoutTree] = []
106
+ for item in cmp:
107
+ if isinstance(item, ggplot):
108
+ nodes.append(lookup_spaces[item])
109
+ else:
110
+ nodes.append(LayoutTree.create(item, lookup_spaces))
111
+
112
+ if isinstance(cmp, OR):
113
+ return ColumnsTree(cmp.gridspec, nodes)
114
+ else:
115
+ return RowsTree(cmp.gridspec, nodes)
116
+
117
+ def harmonise(self):
118
+ """
119
+ Align and resize plots in composition to look good
120
+ """
121
+ self.align()
122
+ self.resize()
123
+
124
+ @abc.abstractmethod
125
+ def align(self):
126
+ """
127
+ Align all the edges in this composition & contained compositions
128
+
129
+ This function mutates the layout spaces, specifically the
130
+ margin_alignments along the sides of the plot.
131
+ """
132
+
133
+ @abc.abstractmethod
134
+ def resize(self):
135
+ """
136
+ Resize panels and the entire plots
137
+
138
+ This function mutates the composition gridspecs; specifically the
139
+ width_ratios and height_ratios.
140
+ """
141
+
142
+ def align_sub_compositions(self):
143
+ """
144
+ Align the compositions contained in this one
145
+ """
146
+ # Recurse into the contained compositions
147
+ for tree in self.sub_compositions:
148
+ tree.align()
149
+
150
+ def resize_sub_compositions(self):
151
+ """
152
+ Resize panels in the compositions contained in this one
153
+ """
154
+ for tree in self.sub_compositions:
155
+ tree.resize()
156
+
157
+ @cached_property
158
+ def sub_compositions(self) -> list[LayoutTree]:
159
+ """
160
+ LayoutTrees of the direct sub compositions of this one
161
+ """
162
+ return [item for item in self.nodes if isinstance(item, LayoutTree)]
163
+
164
+ @cached_property
165
+ @abc.abstractmethod
166
+ def lefts(self) -> Sequence[float]:
167
+ """
168
+ Left values [figure space] of nodes in this tree
169
+ """
170
+
171
+ @cached_property
172
+ @abc.abstractmethod
173
+ def rights(self) -> Sequence[float]:
174
+ """
175
+ Right values [figure space] of nodes in this tree
176
+ """
177
+
178
+ @cached_property
179
+ @abc.abstractmethod
180
+ def bottoms(self) -> Sequence[float]:
181
+ """
182
+ Bottom values [figure space] of nodes in this tree
183
+ """
184
+
185
+ @cached_property
186
+ @abc.abstractmethod
187
+ def tops(self) -> Sequence[float]:
188
+ """
189
+ Top values [figure space] of nodes in this tree
190
+ """
191
+
192
+ @property
193
+ def lefts_align(self) -> bool:
194
+ """
195
+ Return True if panel lefts for the nodes are aligned
196
+ """
197
+ arr = np.array(self.lefts)
198
+ return all(arr == arr[0])
199
+
200
+ @property
201
+ def rights_align(self) -> bool:
202
+ """
203
+ Return True if panel rights for the nodes are aligned
204
+ """
205
+ arr = np.array(self.rights)
206
+ return all(arr == arr[0])
207
+
208
+ @property
209
+ def bottoms_align(self) -> bool:
210
+ """
211
+ Return True if panel bottoms for the nodes are aligned
212
+ """
213
+ arr = np.array(self.bottoms)
214
+ return all(arr == arr[0])
215
+
216
+ @property
217
+ def tops_align(self) -> bool:
218
+ """
219
+ Return True if panel tops for the nodes are aligned
220
+ """
221
+ arr = np.array(self.tops)
222
+ return all(arr == arr[0])
223
+
224
+ @property
225
+ @abc.abstractmethod
226
+ def panel_width(self) -> float:
227
+ """
228
+ A representative width for panels of the nodes
229
+ """
230
+
231
+ @property
232
+ @abc.abstractmethod
233
+ def panel_height(self) -> float:
234
+ """
235
+ A representative height for panels of the nodes
236
+ """
237
+
238
+ @property
239
+ @abc.abstractmethod
240
+ def plot_width(self) -> float:
241
+ """
242
+ A representative width for plots of the nodes
243
+ """
244
+
245
+ @property
246
+ @abc.abstractmethod
247
+ def plot_height(self) -> float:
248
+ """
249
+ A representative for height for plots of the nodes
250
+ """
251
+
252
+ @property
253
+ def plot_widths(self) -> Sequence[float]:
254
+ """
255
+ Widths [figure space] of nodes in this tree
256
+ """
257
+ return [node.plot_width for node in self.nodes]
258
+
259
+ @property
260
+ def plot_heights(self) -> Sequence[float]:
261
+ """
262
+ Heights [figure space] of nodes in this tree
263
+ """
264
+ return [node.plot_height for node in self.nodes]
265
+
266
+ @property
267
+ def panel_widths(self) -> Sequence[float]:
268
+ """
269
+ Widths [figure space] of the panels in this tree
270
+ """
271
+ return [node.panel_width for node in self.nodes]
272
+
273
+ @property
274
+ def panel_heights(self) -> Sequence[float]:
275
+ """
276
+ Heights [figure space] of the panels in this tree
277
+ """
278
+ return [node.panel_height for node in self.nodes]
279
+
280
+ @cached_property
281
+ @abc.abstractmethod
282
+ def left_tag_width(self) -> float:
283
+ """
284
+ A representative width [figure space] for the left tags of the nodes
285
+ """
286
+
287
+ @cached_property
288
+ @abc.abstractmethod
289
+ def right_tag_width(self) -> float:
290
+ """
291
+ A representative width [figure space] for the right tags of the nodes
292
+ """
293
+
294
+ @cached_property
295
+ @abc.abstractmethod
296
+ def bottom_tag_height(self) -> float:
297
+ """
298
+ A representative height [figure space] for the top tags of the nodes
299
+ """
300
+
301
+ @cached_property
302
+ @abc.abstractmethod
303
+ def top_tag_height(self) -> float:
304
+ """
305
+ A representative height [figure space] for the top tags of the nodes
306
+ """
307
+
308
+ @cached_property
309
+ def left_tag_widths(self) -> list[float]:
310
+ """
311
+ The widths of the left tags in this tree
312
+ """
313
+ return [node.left_tag_width for node in self.nodes]
314
+
315
+ @cached_property
316
+ def right_tag_widths(self) -> list[float]:
317
+ """
318
+ The widths of the right tags in this tree
319
+ """
320
+ return [node.right_tag_width for node in self.nodes]
321
+
322
+ @cached_property
323
+ def bottom_tag_heights(self) -> list[float]:
324
+ """
325
+ The heights of the bottom tags in this tree
326
+ """
327
+ return [node.bottom_tag_height for node in self.nodes]
328
+
329
+ @cached_property
330
+ def top_tag_heights(self) -> list[float]:
331
+ """
332
+ The heights of the top tags in this tree
333
+ """
334
+ return [node.top_tag_height for node in self.nodes]
335
+
336
+ @property
337
+ def left_tags_align(self) -> bool:
338
+ """
339
+ Return True if the left tags for the nodes are aligned
340
+ """
341
+ arr = np.array(self.left_tag_widths)
342
+ return all(arr == arr[0])
343
+
344
+ @property
345
+ def right_tags_align(self) -> bool:
346
+ """
347
+ Return True if the right tags for the nodes are aligned
348
+ """
349
+ arr = np.array(self.right_tag_widths)
350
+ return all(arr == arr[0])
351
+
352
+ @property
353
+ def bottom_tags_align(self) -> bool:
354
+ """
355
+ Return True if the bottom tags for the nodes are aligned
356
+ """
357
+ arr = np.array(self.bottom_tag_heights)
358
+ return all(arr == arr[0])
359
+
360
+ @property
361
+ def top_tags_align(self) -> bool:
362
+ """
363
+ Return True if the top tags for the nodes are aligned
364
+ """
365
+ arr = np.array(self.top_tag_heights)
366
+ return all(arr == arr[0])
367
+
368
+ @abc.abstractmethod
369
+ def set_left_margin_alignment(self, value: float):
370
+ """
371
+ Set a margin to align the left of the panels in this composition
372
+
373
+ In figure dimenstions
374
+ """
375
+
376
+ @abc.abstractmethod
377
+ def set_right_margin_alignment(self, value: float):
378
+ """
379
+ Set a margin to align the right of the panels in this composition
380
+
381
+ In figure dimenstions
382
+ """
383
+
384
+ @abc.abstractmethod
385
+ def set_bottom_margin_alignment(self, value: float):
386
+ """
387
+ Set a margin to align the bottom of the panels in this composition
388
+
389
+ In figure dimenstions
390
+ """
391
+
392
+ @abc.abstractmethod
393
+ def set_top_margin_alignment(self, value: float):
394
+ """
395
+ Set a margin to align the top of the panels in this composition
396
+
397
+ In figure dimenstions
398
+ """
399
+
400
+ @abc.abstractmethod
401
+ def set_left_tag_alignment(self, value: float):
402
+ """
403
+ Set the space to align the left tags in this composition
404
+
405
+ In figure dimenstions
406
+ """
407
+
408
+ @abc.abstractmethod
409
+ def set_right_tag_alignment(self, value: float):
410
+ """
411
+ Set the space to align the right tags in this composition
412
+
413
+ In figure dimenstions
414
+ """
415
+
416
+ @abc.abstractmethod
417
+ def set_bottom_tag_alignment(self, value: float):
418
+ """
419
+ Set the space to align the bottom tags in this composition
420
+
421
+ In figure dimenstions
422
+ """
423
+
424
+ @abc.abstractmethod
425
+ def set_top_tag_alignment(self, value: float):
426
+ """
427
+ Set the space to align the top tags in this composition
428
+
429
+ In figure dimenstions
430
+ """
431
+
432
+
433
+ @dataclass
434
+ class ColumnsTree(LayoutTree):
435
+ """
436
+ Tree with columns at the outermost level
437
+
438
+ e.g. p1 | (p2 / p3)
439
+
440
+
441
+ -------------------
442
+ | | |
443
+ | | |
444
+ | | |
445
+ | | |
446
+ | |---------|
447
+ | | |
448
+ | | |
449
+ | | |
450
+ | | |
451
+ -------------------
452
+ """
453
+
454
+ def align(self):
455
+ self.align_top_tags()
456
+ self.align_bottom_tags()
457
+ self.align_tops()
458
+ self.align_bottoms()
459
+ self.align_sub_compositions()
460
+
461
+ def resize(self):
462
+ """
463
+ Resize the widths of gridspec so that panels have equal widths
464
+ """
465
+ # The new width of each panel is the average width of all
466
+ # the panels plus all the space to the left and right
467
+ # of the panels.
468
+ plot_widths = np.array(self.plot_widths)
469
+ panel_widths = np.array(self.panel_widths)
470
+ non_panel_space = plot_widths - panel_widths
471
+ new_plot_widths = panel_widths.mean() + non_panel_space
472
+ width_ratios = new_plot_widths / new_plot_widths.min()
473
+ self.gridspec.set_width_ratios(width_ratios)
474
+ self.resize_sub_compositions()
475
+
476
+ def align_bottoms(self):
477
+ """
478
+ Align the immediate bottom edges this composition
479
+
480
+ ----------- -----------
481
+ | | | | | |
482
+ | | | | | |
483
+ | | | -> | | |
484
+ | |#####| |#####|#####|
485
+ |#####| | | | |
486
+ ----------- -----------
487
+ """
488
+ # If panels are aligned and have a non-zero margin_alignment,
489
+ # aligning them again will set that value to zero and undoes
490
+ # the alignment.
491
+ if self.bottoms_align:
492
+ return
493
+
494
+ values = max(self.bottoms) - np.array(self.bottoms)
495
+ for item, value in zip(self.nodes, values):
496
+ if isinstance(item, LayoutSpaces):
497
+ item.b.margin_alignment = value
498
+ else:
499
+ item.set_bottom_margin_alignment(value)
500
+
501
+ del self.bottoms
502
+
503
+ def align_tops(self):
504
+ """
505
+ Align the immediate top edges in this composition
506
+
507
+ ----------- -----------
508
+ |#####| | | | |
509
+ | |#####| |#####|#####|
510
+ | | | -> | | |
511
+ | | | | | |
512
+ | | | | | |
513
+ ----------- -----------
514
+ """
515
+ if self.tops_align:
516
+ return
517
+
518
+ values = np.array(self.tops) - min(self.tops)
519
+ for item, value in zip(self.nodes, values):
520
+ if isinstance(item, LayoutSpaces):
521
+ item.t.margin_alignment = value
522
+ else:
523
+ item.set_top_margin_alignment(value)
524
+
525
+ del self.tops
526
+
527
+ def align_bottom_tags(self):
528
+ if self.bottom_tags_align:
529
+ return
530
+
531
+ values = max(self.bottom_tag_heights) - np.array(
532
+ self.bottom_tag_heights
533
+ )
534
+ for item, value in zip(self.nodes, values):
535
+ if isinstance(item, LayoutSpaces):
536
+ item.l.tag_alignment = value
537
+ else:
538
+ item.set_bottom_tag_alignment(value)
539
+
540
+ def align_top_tags(self):
541
+ if self.top_tags_align:
542
+ return
543
+
544
+ values = max(self.top_tag_heights) - np.array(self.top_tag_heights)
545
+ for item, value in zip(self.nodes, values):
546
+ if isinstance(item, LayoutSpaces):
547
+ item.t.tag_alignment = value
548
+ else:
549
+ item.set_top_tag_alignment(value)
550
+
551
+ @cached_property
552
+ def lefts(self):
553
+ left_item = self.nodes[0]
554
+ if isinstance(left_item, LayoutSpaces):
555
+ return [left_item.l.left]
556
+ else:
557
+ return left_item.lefts
558
+
559
+ @cached_property
560
+ def rights(self):
561
+ right_item = self.nodes[-1]
562
+ if isinstance(right_item, LayoutSpaces):
563
+ return [right_item.r.right]
564
+ else:
565
+ return right_item.rights
566
+
567
+ @cached_property
568
+ def bottoms(self):
569
+ values = []
570
+ for item in self.nodes:
571
+ if isinstance(item, LayoutSpaces):
572
+ values.append(item.b.bottom)
573
+ else:
574
+ values.append(max(item.bottoms))
575
+ return values
576
+
577
+ @cached_property
578
+ def tops(self):
579
+ values = []
580
+ for item in self.nodes:
581
+ if isinstance(item, LayoutSpaces):
582
+ values.append(item.t.top)
583
+ else:
584
+ values.append(min(item.tops))
585
+ return values
586
+
587
+ @property
588
+ def panel_width(self) -> float:
589
+ return sum(self.panel_widths)
590
+
591
+ @property
592
+ def panel_height(self) -> float:
593
+ return float(np.mean(self.panel_heights))
594
+
595
+ @property
596
+ def plot_width(self) -> float:
597
+ return sum(self.plot_widths)
598
+
599
+ @property
600
+ def plot_height(self) -> float:
601
+ return max(self.plot_heights)
602
+
603
+ @cached_property
604
+ def left_tag_width(self) -> float:
605
+ return self.left_tag_widths[0]
606
+
607
+ @cached_property
608
+ def right_tag_width(self) -> float:
609
+ return self.right_tag_widths[-1]
610
+
611
+ @cached_property
612
+ def bottom_tag_height(self) -> float:
613
+ return max(self.bottom_tag_heights)
614
+
615
+ @cached_property
616
+ def top_tag_height(self) -> float:
617
+ return max(self.top_tag_heights)
618
+
619
+ def set_left_margin_alignment(self, value: float):
620
+ left_item = self.nodes[0]
621
+ if isinstance(left_item, LayoutSpaces):
622
+ left_item.l.margin_alignment = value
623
+ else:
624
+ left_item.set_left_margin_alignment(value)
625
+
626
+ def set_right_margin_alignment(self, value: float):
627
+ right_item = self.nodes[-1]
628
+ if isinstance(right_item, LayoutSpaces):
629
+ right_item.r.margin_alignment = value
630
+ else:
631
+ right_item.set_right_margin_alignment(value)
632
+
633
+ def set_bottom_margin_alignment(self, value: float):
634
+ for item in self.nodes:
635
+ if isinstance(item, LayoutSpaces):
636
+ item.b.margin_alignment = value
637
+ else:
638
+ item.set_bottom_margin_alignment(value)
639
+
640
+ def set_top_margin_alignment(self, value: float):
641
+ for item in self.nodes:
642
+ if isinstance(item, LayoutSpaces):
643
+ item.t.margin_alignment = value
644
+ else:
645
+ item.set_top_margin_alignment(value)
646
+
647
+ def set_bottom_tag_alignment(self, value: float):
648
+ for item in self.nodes:
649
+ if isinstance(item, LayoutSpaces):
650
+ item.l.tag_alignment = value
651
+ else:
652
+ item.set_bottom_tag_alignment(value)
653
+
654
+ def set_top_tag_alignment(self, value: float):
655
+ for item in self.nodes:
656
+ if isinstance(item, LayoutSpaces):
657
+ item.t.tag_alignment = value
658
+ else:
659
+ item.set_top_tag_alignment(value)
660
+
661
+
662
+ @dataclass
663
+ class RowsTree(LayoutTree):
664
+ """
665
+ Tree with rows at the outermost level
666
+
667
+ e.g. p1 / (p2 | p3)
668
+
669
+ -------------------
670
+ | |
671
+ | |
672
+ | |
673
+ |-------------------|
674
+ | | |
675
+ | | |
676
+ | | |
677
+ -------------------
678
+ """
679
+
680
+ def align(self):
681
+ self.align_left_tags()
682
+ self.align_right_tags()
683
+ self.align_lefts()
684
+ self.align_rights()
685
+ self.align_sub_compositions()
686
+
687
+ def resize(self):
688
+ """
689
+ Resize the heights of gridspec so that panels have equal heights
690
+
691
+ This method resizes (recursively) the contained compositions
692
+ """
693
+ # The new height of each panel is the average width of all
694
+ # the panels plus all the space above and below the panels.
695
+ plot_heights = np.array(self.plot_heights)
696
+ panel_heights = np.array(self.panel_heights)
697
+ non_panel_space = plot_heights - panel_heights
698
+ new_plot_heights = panel_heights.mean() + non_panel_space
699
+ height_ratios = new_plot_heights / new_plot_heights.max()
700
+ self.gridspec.set_height_ratios(height_ratios)
701
+ self.resize_sub_compositions()
702
+
703
+ def align_lefts(self):
704
+ """
705
+ Align the immediate left edges in this composition
706
+
707
+ ----------- -----------
708
+ |# | | # |
709
+ |# | | # |
710
+ |# | | # |
711
+ |-----------| -> |-----------|
712
+ | # | | # |
713
+ | # | | # |
714
+ | # | | # |
715
+ ----------- -----------
716
+ """
717
+ if self.lefts_align:
718
+ return
719
+
720
+ values = max(self.lefts) - np.array(self.lefts)
721
+ for item, value in zip(self.nodes, values):
722
+ if isinstance(item, LayoutSpaces):
723
+ item.l.margin_alignment = value
724
+ else:
725
+ item.set_left_margin_alignment(value)
726
+
727
+ del self.lefts
728
+
729
+ def align_rights(self):
730
+ """
731
+ Align the immediate right edges in this composition
732
+
733
+ ----------- -----------
734
+ | # | | # |
735
+ | # | | # |
736
+ | # | | # |
737
+ |-----------| -> |-----------|
738
+ | #| | # |
739
+ | #| | # |
740
+ | #| | # |
741
+ ----------- -----------
742
+ """
743
+ if self.rights_align:
744
+ return
745
+
746
+ values = np.array(self.rights) - min(self.rights)
747
+ for item, value in zip(self.nodes, values):
748
+ if isinstance(item, LayoutSpaces):
749
+ item.r.margin_alignment = value
750
+ else:
751
+ item.set_right_margin_alignment(value)
752
+
753
+ del self.rights
754
+
755
+ def align_left_tags(self):
756
+ """
757
+ Make all the left tags takeup the same amount of space
758
+
759
+
760
+ Given
761
+
762
+ V
763
+ ------------------------------------
764
+ | plot_margin | tag | artists |
765
+ |------------------------------------|
766
+ | plot_margin | A long tag | artists |
767
+ ------------------------------------
768
+
769
+ V
770
+ ------------------------------------
771
+ | plot_margin | #######tag | artists |
772
+ |------------------------------------|
773
+ | plot_margin | A long tag | artists |
774
+ ------------------------------------
775
+ """
776
+ if self.left_tags_align:
777
+ return
778
+
779
+ values = max(self.left_tag_widths) - np.array(self.left_tag_widths)
780
+ for item, value in zip(self.nodes, values):
781
+ if isinstance(item, LayoutSpaces):
782
+ item.l.tag_alignment = value
783
+ else:
784
+ item.set_left_tag_alignment(value)
785
+
786
+ def align_right_tags(self):
787
+ if self.right_tags_align:
788
+ return
789
+
790
+ values = max(self.right_tag_widths) - np.array(self.right_tag_widths)
791
+ for item, value in zip(self.nodes, values):
792
+ if isinstance(item, LayoutSpaces):
793
+ item.r.tag_alignment = value
794
+ else:
795
+ item.set_right_tag_alignment(value)
796
+
797
+ @cached_property
798
+ def lefts(self):
799
+ values = []
800
+ for item in self.nodes:
801
+ if isinstance(item, LayoutSpaces):
802
+ values.append(item.l.left)
803
+ else:
804
+ values.append(max(item.lefts))
805
+ return values
806
+
807
+ @cached_property
808
+ def rights(self):
809
+ values = []
810
+ for item in self.nodes:
811
+ if isinstance(item, LayoutSpaces):
812
+ values.append(item.r.right)
813
+ else:
814
+ values.append(min(item.rights))
815
+ return values
816
+
817
+ @cached_property
818
+ def bottoms(self):
819
+ bottom_item = self.nodes[-1]
820
+ if isinstance(bottom_item, LayoutSpaces):
821
+ return [bottom_item.b.bottom]
822
+ else:
823
+ return bottom_item.bottoms
824
+
825
+ @cached_property
826
+ def tops(self):
827
+ top_item = self.nodes[0]
828
+ if isinstance(top_item, LayoutSpaces):
829
+ return [top_item.t.top]
830
+ else:
831
+ return top_item.tops
832
+
833
+ @property
834
+ def panel_width(self) -> float:
835
+ return float(np.mean(self.panel_widths))
836
+
837
+ @property
838
+ def panel_height(self) -> float:
839
+ return sum(self.panel_heights)
840
+
841
+ @property
842
+ def plot_width(self) -> float:
843
+ return max(self.plot_widths)
844
+
845
+ @property
846
+ def plot_height(self) -> float:
847
+ return sum(self.plot_heights)
848
+
849
+ @cached_property
850
+ def left_tag_width(self) -> float:
851
+ return max(self.left_tag_widths)
852
+
853
+ @cached_property
854
+ def right_tag_width(self) -> float:
855
+ return max(self.right_tag_widths)
856
+
857
+ @cached_property
858
+ def top_tag_height(self) -> float:
859
+ return self.top_tag_heights[0]
860
+
861
+ @cached_property
862
+ def bottom_tag_height(self) -> float:
863
+ return self.bottom_tag_heights[-1]
864
+
865
+ def set_left_margin_alignment(self, value: float):
866
+ for item in self.nodes:
867
+ if isinstance(item, LayoutSpaces):
868
+ item.l.margin_alignment = value
869
+ else:
870
+ item.set_left_margin_alignment(value)
871
+
872
+ def set_right_margin_alignment(self, value: float):
873
+ for item in self.nodes:
874
+ if isinstance(item, LayoutSpaces):
875
+ item.r.margin_alignment = value
876
+ else:
877
+ item.set_right_margin_alignment(value)
878
+
879
+ def set_bottom_margin_alignment(self, value: float):
880
+ bottom_item = self.nodes[-1]
881
+ if isinstance(bottom_item, LayoutSpaces):
882
+ bottom_item.b.margin_alignment = value
883
+ else:
884
+ bottom_item.set_bottom_margin_alignment(value)
885
+
886
+ def set_top_margin_alignment(self, value: float):
887
+ top_item = self.nodes[0]
888
+ if isinstance(top_item, LayoutSpaces):
889
+ top_item.t.margin_alignment = value
890
+ else:
891
+ top_item.set_top_margin_alignment(value)
892
+
893
+ def set_left_tag_alignment(self, value: float):
894
+ for item in self.nodes:
895
+ if isinstance(item, LayoutSpaces):
896
+ item.l.tag_alignment = value
897
+ else:
898
+ item.set_left_tag_alignment(value)
899
+
900
+ def set_right_tag_alignment(self, value: float):
901
+ for item in self.nodes:
902
+ if isinstance(item, LayoutSpaces):
903
+ item.r.tag_alignment = value
904
+ else:
905
+ item.set_right_tag_alignment(value)