plotnine 0.15.2__py3-none-any.whl → 0.16.0a1__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 (60) hide show
  1. plotnine/_mpl/gridspec.py +50 -6
  2. plotnine/_mpl/layout_manager/__init__.py +2 -5
  3. plotnine/_mpl/layout_manager/_composition_layout_items.py +98 -0
  4. plotnine/_mpl/layout_manager/_composition_side_space.py +461 -0
  5. plotnine/_mpl/layout_manager/_engine.py +19 -58
  6. plotnine/_mpl/layout_manager/_grid.py +94 -0
  7. plotnine/_mpl/layout_manager/_layout_tree.py +402 -817
  8. plotnine/_mpl/layout_manager/{_layout_items.py → _plot_layout_items.py} +55 -278
  9. plotnine/_mpl/layout_manager/{_spaces.py → _plot_side_space.py} +111 -291
  10. plotnine/_mpl/layout_manager/_side_space.py +176 -0
  11. plotnine/_mpl/utils.py +259 -1
  12. plotnine/_utils/__init__.py +23 -3
  13. plotnine/_utils/context.py +1 -1
  14. plotnine/_utils/dataclasses.py +24 -0
  15. plotnine/animation.py +13 -12
  16. plotnine/composition/__init__.py +6 -0
  17. plotnine/composition/_beside.py +13 -11
  18. plotnine/composition/_compose.py +263 -99
  19. plotnine/composition/_plot_annotation.py +75 -0
  20. plotnine/composition/_plot_layout.py +143 -0
  21. plotnine/composition/_plot_spacer.py +1 -1
  22. plotnine/composition/_stack.py +13 -11
  23. plotnine/composition/_types.py +28 -0
  24. plotnine/composition/_wrap.py +60 -0
  25. plotnine/facets/facet.py +9 -12
  26. plotnine/facets/facet_grid.py +2 -2
  27. plotnine/facets/facet_wrap.py +1 -1
  28. plotnine/geoms/geom.py +2 -2
  29. plotnine/geoms/geom_map.py +4 -5
  30. plotnine/geoms/geom_path.py +8 -7
  31. plotnine/geoms/geom_rug.py +6 -10
  32. plotnine/geoms/geom_text.py +5 -5
  33. plotnine/ggplot.py +63 -9
  34. plotnine/guides/guide.py +24 -6
  35. plotnine/guides/guide_colorbar.py +88 -46
  36. plotnine/guides/guide_legend.py +47 -20
  37. plotnine/guides/guides.py +2 -2
  38. plotnine/iapi.py +17 -1
  39. plotnine/scales/scale.py +1 -1
  40. plotnine/stats/binning.py +15 -43
  41. plotnine/stats/smoothers.py +7 -3
  42. plotnine/stats/stat.py +2 -2
  43. plotnine/stats/stat_density_2d.py +10 -6
  44. plotnine/stats/stat_pointdensity.py +8 -1
  45. plotnine/stats/stat_qq.py +5 -5
  46. plotnine/stats/stat_qq_line.py +6 -1
  47. plotnine/stats/stat_sina.py +19 -20
  48. plotnine/stats/stat_summary.py +4 -2
  49. plotnine/stats/stat_summary_bin.py +7 -1
  50. plotnine/themes/elements/element_line.py +2 -0
  51. plotnine/themes/elements/element_text.py +12 -1
  52. plotnine/themes/theme.py +18 -24
  53. plotnine/themes/themeable.py +17 -3
  54. plotnine/typing.py +6 -1
  55. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/METADATA +2 -2
  56. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/RECORD +59 -51
  57. plotnine/composition/_plotspec.py +0 -50
  58. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/WHEEL +0 -0
  59. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
  60. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
- import abc
4
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
5
4
  from functools import cached_property
6
- from typing import TYPE_CHECKING, cast
5
+ from typing import TYPE_CHECKING, Iterator, cast
7
6
 
8
7
  import numpy as np
9
8
 
10
- from plotnine.composition import Beside
11
-
12
- from ._spaces import LayoutSpaces
9
+ from ._grid import Grid
10
+ from ._plot_side_space import PlotSideSpaces
13
11
 
14
12
  if TYPE_CHECKING:
15
- from typing import Sequence
13
+ from typing import Sequence, TypeAlias
16
14
 
17
- from plotnine import ggplot
18
15
  from plotnine._mpl.gridspec import p9GridSpec
16
+ from plotnine._mpl.layout_manager._plot_side_space import (
17
+ bottom_space,
18
+ left_space,
19
+ right_space,
20
+ top_space,
21
+ )
19
22
  from plotnine.composition import Compose
20
23
 
24
+ Node: TypeAlias = "PlotSideSpaces | LayoutTree"
25
+
21
26
 
22
27
  @dataclass
23
28
  class LayoutTree:
@@ -25,9 +30,9 @@ class LayoutTree:
25
30
  A Tree representation of the composition
26
31
 
27
32
  The purpose of this class (and its subclasses) is to align and
28
- and resize plots in a composition.
33
+ and resize plots in a composition. For example,
29
34
 
30
- For example, this composition;
35
+ This composition:
31
36
 
32
37
  (p1 | p2) | (p3 / p4)
33
38
 
@@ -47,44 +52,108 @@ class LayoutTree:
47
52
 
48
53
  and the tree would have this structure;
49
54
 
50
- ColumnsTree
55
+
56
+ LayoutTree (.nrow=1, .ncol=3)
51
57
  |
52
58
  ----------------------------
53
59
  | | |
54
- LayoutSpaces LayoutSpaces RowsTree
60
+ LayoutSpaces LayoutSpaces LayoutTree (.nrow=2, .ncol=1)
55
61
  |
56
62
  -------------
57
63
  | |
58
64
  LayoutSpaces LayoutSpaces
59
65
 
66
+ This composition:
67
+
68
+ (p1 + p2 + p4 + p5 + p6) + plot_layout(ncol=3)
69
+
70
+ would look like this:
71
+
72
+ -----------------------------
73
+ | | | |
74
+ | | | |
75
+ | p1 | p2 | p3 |
76
+ | | | |
77
+ |---------|---------|---------|
78
+ | | | |
79
+ | p4 | p5 | |
80
+ | | | |
81
+ | | | |
82
+ -----------------------------
83
+
84
+ and have this structure
85
+
86
+
87
+ LayoutTree (.nrow=3, .ncol=2)
88
+ |
89
+ -------------------------------------------------------
90
+ | | | | |
91
+ LayoutSpaces LayoutSpaces LayoutSpaces LayoutSpaces LayoutSpaces
92
+
60
93
  Each composition is a tree or subtree
61
- """
62
94
 
63
- gridspec: p9GridSpec
64
- """
65
- Gridspec of the composition
95
+ ## How it works
66
96
 
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.
97
+ Initially (and if the composition does not have annotation texts), the
98
+ sub_gridspec occupies all the space available to it with the contained
99
+ items (ggplot / Compose) having equal sizes.
100
+
101
+ But if the full plot / composition occupy the same space, their panels
102
+ may have different sizes because they have to share that space with the
103
+ texts (title, subtitle, caption, axis title, axis text, tag), legends
104
+ and plot margins that surround the panels.
105
+
106
+ We align the panels, axis titles and tags by adding *_alignment margins;
107
+ and resize the panels by
108
+
109
+ Taking the sizes of these elements into account, we align the panels
110
+ in the composition by changing the width and/or height of the gridspec.
72
111
 
73
112
  The information about the size (width & height) of the panels is in the
74
113
  LayoutSpaces.
75
114
  """
76
115
 
77
- nodes: list[LayoutSpaces | LayoutTree]
116
+ cmp: Compose
117
+ """
118
+ Composition that this tree represents
119
+ """
120
+
121
+ nodes: list[PlotSideSpaces | LayoutTree]
78
122
  """
79
123
  The spaces or tree of spaces in the composition that the tree
80
124
  represents.
81
125
  """
82
126
 
127
+ sub_gridspec: p9GridSpec = field(init=False, repr=False)
128
+ """
129
+ Gridspec (nxn) that contains the composed items
130
+ """
131
+
132
+ def __post_init__(self):
133
+ self.sub_gridspec = self.cmp._sub_gridspec
134
+ self.grid = Grid["Node"](
135
+ self.nrow,
136
+ self.ncol,
137
+ self.nodes,
138
+ order="row_major" if self.cmp.layout.byrow else "col_major",
139
+ )
140
+
141
+ @property
142
+ def ncol(self) -> int:
143
+ """
144
+ Number of columns
145
+ """
146
+ return cast("int", self.cmp.layout.ncol)
147
+
148
+ @property
149
+ def nrow(self) -> int:
150
+ """
151
+ Number of rows
152
+ """
153
+ return cast("int", self.cmp.layout.nrow)
154
+
83
155
  @staticmethod
84
- def create(
85
- cmp: Compose,
86
- lookup_spaces: dict[ggplot, LayoutSpaces],
87
- ) -> LayoutTree:
156
+ def create(cmp: Compose) -> LayoutTree:
88
157
  """
89
158
  Create a LayoutTree for this composition
90
159
 
@@ -92,37 +161,41 @@ class LayoutTree:
92
161
  ----------
93
162
  cmp :
94
163
  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
164
  """
103
165
  from plotnine import ggplot
104
166
 
105
- nodes: list[LayoutSpaces | LayoutTree] = []
167
+ # Create subtree
168
+ nodes: list[PlotSideSpaces | LayoutTree] = []
106
169
  for item in cmp:
107
170
  if isinstance(item, ggplot):
108
- nodes.append(lookup_spaces[item])
171
+ nodes.append(item._sidespaces)
109
172
  else:
110
- nodes.append(LayoutTree.create(item, lookup_spaces))
173
+ nodes.append(LayoutTree.create(item))
174
+
175
+ return LayoutTree(cmp, nodes)
111
176
 
112
- if isinstance(cmp, Beside):
113
- return ColumnsTree(cmp.gridspec, nodes)
114
- else:
115
- return RowsTree(cmp.gridspec, nodes)
177
+ @cached_property
178
+ def sub_compositions(self) -> list[LayoutTree]:
179
+ """
180
+ LayoutTrees of the direct sub compositions of this one
181
+ """
182
+ return [item for item in self.nodes if isinstance(item, LayoutTree)]
116
183
 
117
- def harmonise(self):
184
+ def arrange_layout(self):
118
185
  """
119
186
  Align and resize plots in composition to look good
187
+
188
+ Aligning changes the *_alignment attributes of the side_spaces.
189
+ Resizing, changes the parameters of the sub_gridspec.
190
+
191
+ Note that we expect that this method will be called only on the
192
+ tree for the top-level composition, and it is called for its
193
+ side-effects.
120
194
  """
121
195
  self.align_axis_titles()
122
196
  self.align()
123
197
  self.resize()
124
198
 
125
- @abc.abstractmethod
126
199
  def align(self):
127
200
  """
128
201
  Align all the edges in this composition & contained compositions
@@ -130,8 +203,10 @@ class LayoutTree:
130
203
  This function mutates the layout spaces, specifically the
131
204
  margin_alignments along the sides of the plot.
132
205
  """
206
+ self.align_tags()
207
+ self.align_panels()
208
+ self.align_sub_compositions()
133
209
 
134
- @abc.abstractmethod
135
210
  def resize(self):
136
211
  """
137
212
  Resize panels and the entire plots
@@ -139,6 +214,9 @@ class LayoutTree:
139
214
  This function mutates the composition gridspecs; specifically the
140
215
  width_ratios and height_ratios.
141
216
  """
217
+ self.resize_widths()
218
+ self.resize_heights()
219
+ self.resize_sub_compositions()
142
220
 
143
221
  def align_sub_compositions(self):
144
222
  """
@@ -148,20 +226,6 @@ class LayoutTree:
148
226
  for tree in self.sub_compositions:
149
227
  tree.align()
150
228
 
151
- @abc.abstractmethod
152
- def align_axis_titles(self):
153
- """
154
- Align the axis titles along the composing dimension
155
-
156
- Since the alignment value used to for this purpose is one of
157
- the fields in the _side_space, it affects the space created
158
- for the panel.
159
-
160
- We could align the titles within self.align but we would have
161
- to store the value outside the _side_space and pick it up when
162
- setting the position of the texts!
163
- """
164
-
165
229
  def resize_sub_compositions(self):
166
230
  """
167
231
  Resize panels in the compositions contained in this one
@@ -170,907 +234,428 @@ class LayoutTree:
170
234
  tree.resize()
171
235
 
172
236
  @cached_property
173
- def sub_compositions(self) -> list[LayoutTree]:
237
+ def bottom_most_spaces(self) -> list[bottom_space]:
174
238
  """
175
- LayoutTrees of the direct sub compositions of this one
239
+ Bottom spaces of items in the last row
176
240
  """
177
- return [item for item in self.nodes if isinstance(item, LayoutTree)]
241
+ return [s for s in self.bottom_spaces_in_row(self.nrow - 1)]
178
242
 
179
243
  @cached_property
180
- @abc.abstractmethod
181
- def panel_lefts(self) -> Sequence[float]:
244
+ def top_most_spaces(self) -> list[top_space]:
182
245
  """
183
- Left values [figure space] of nodes in this tree
246
+ Top spaces of items in the top row
184
247
  """
248
+ return [s for s in self.top_spaces_in_row(0)]
185
249
 
186
250
  @cached_property
187
- @abc.abstractmethod
188
- def panel_rights(self) -> Sequence[float]:
251
+ def left_most_spaces(self) -> list[left_space]:
189
252
  """
190
- Right values [figure space] of nodes in this tree
253
+ Left spaces of items in the last column
191
254
  """
255
+ return [s for s in self.left_spaces_in_col(0)]
192
256
 
193
257
  @cached_property
194
- @abc.abstractmethod
195
- def panel_bottoms(self) -> Sequence[float]:
258
+ def right_most_spaces(self) -> list[right_space]:
196
259
  """
197
- Bottom values [figure space] of nodes in this tree
198
- """
199
-
200
- @cached_property
201
- @abc.abstractmethod
202
- def panel_tops(self) -> Sequence[float]:
203
- """
204
- Top values [figure space] of nodes in this tree
260
+ Right spaces of items the last column
205
261
  """
262
+ return [s for s in self.right_spaces_in_col(self.ncol - 1)]
206
263
 
207
264
  @property
208
- def panel_lefts_align(self) -> bool:
209
- """
210
- Return True if panel lefts for the nodes are aligned
211
- """
212
- arr = np.array(self.panel_lefts)
213
- return all(arr == arr[0])
214
-
215
- @property
216
- def panel_rights_align(self) -> bool:
217
- """
218
- Return True if panel rights for the nodes are aligned
219
- """
220
- arr = np.array(self.panel_rights)
221
- return all(arr == arr[0])
222
-
223
- @property
224
- def panel_bottoms_align(self) -> bool:
225
- """
226
- Return True if panel bottoms for the nodes are aligned
227
- """
228
- arr = np.array(self.panel_bottoms)
229
- return all(arr == arr[0])
230
-
231
- @property
232
- def panel_tops_align(self) -> bool:
233
- """
234
- Return True if panel tops for the nodes are aligned
235
- """
236
- arr = np.array(self.panel_tops)
237
- return all(arr == arr[0])
238
-
239
- @property
240
- @abc.abstractmethod
241
265
  def panel_width(self) -> float:
242
266
  """
243
- A representative width for panels of the nodes
267
+ A width of all panels in this composition
244
268
  """
269
+ return sum(self.panel_widths)
245
270
 
246
271
  @property
247
- @abc.abstractmethod
248
272
  def panel_height(self) -> float:
249
273
  """
250
- A representative height for panels of the nodes
274
+ A height of all panels in this composition
251
275
  """
276
+ return sum(self.panel_heights)
252
277
 
253
278
  @property
254
- @abc.abstractmethod
255
279
  def plot_width(self) -> float:
256
280
  """
257
- A representative width for plots of the nodes
281
+ A width of all plots in this tree/composition
258
282
  """
283
+ return self.sub_gridspec.width
259
284
 
260
285
  @property
261
- @abc.abstractmethod
262
286
  def plot_height(self) -> float:
263
287
  """
264
- A representative for height for plots of the nodes
288
+ A height of all plots in this tree/composition
265
289
  """
290
+ return self.sub_gridspec.height
266
291
 
267
292
  @property
268
- def plot_widths(self) -> Sequence[float]:
293
+ def horizontal_space(self) -> float:
269
294
  """
270
- Widths [figure space] of nodes in this tree
295
+ Horizontal non-panel space in this composition
271
296
  """
272
- return [node.plot_width for node in self.nodes]
297
+ return sum(self.horizontal_spaces)
273
298
 
274
299
  @property
275
- def plot_heights(self) -> Sequence[float]:
300
+ def vertical_space(self) -> float:
276
301
  """
277
- Heights [figure space] of nodes in this tree
302
+ Vertical non-panel space in this composition
278
303
  """
279
- return [node.plot_height for node in self.nodes]
304
+ return sum(self.vertical_spaces)
280
305
 
281
306
  @property
282
- def panel_widths(self) -> Sequence[float]:
307
+ def horizontal_spaces(self) -> Sequence[float]:
283
308
  """
284
- Widths [figure space] of the panels in this tree
309
+ Horizontal non-panel space by column
310
+
311
+ For each column, the representative number for the horizontal
312
+ space to left & right of the widest panel.
285
313
  """
286
- return [node.panel_width for node in self.nodes]
314
+ return list(np.array(self.plot_widths) - self.panel_widths)
287
315
 
288
316
  @property
289
- def panel_heights(self) -> Sequence[float]:
290
- """
291
- Heights [figure space] of the panels in this tree
317
+ def vertical_spaces(self) -> Sequence[float]:
292
318
  """
293
- return [node.panel_height for node in self.nodes]
319
+ Vertical non-panel space by row
294
320
 
295
- @cached_property
296
- @abc.abstractmethod
297
- def left_tag_width(self) -> float:
298
- """
299
- A representative width [figure space] for the left tags of the nodes
321
+ For each row, the representative number for the vertical
322
+ space is above & below the tallest panel.
300
323
  """
324
+ return list(np.array(self.plot_heights) - self.panel_heights)
301
325
 
302
- @cached_property
303
- @abc.abstractmethod
304
- def right_tag_width(self) -> float:
305
- """
306
- A representative width [figure space] for the right tags of the nodes
326
+ @property
327
+ def panel_widths(self) -> Sequence[float]:
307
328
  """
329
+ Widths [figure space] of panels by column
308
330
 
309
- @cached_property
310
- @abc.abstractmethod
311
- def bottom_tag_height(self) -> float:
312
- """
313
- A representative height [figure space] for the top tags of the nodes
331
+ For each column, the representative number for the panel width
332
+ is the maximum width among all panels in the column.
314
333
  """
334
+ # This method is used after aligning the panels. Therefore, the
335
+ # wides panel_width (i.e. max()) is the good representative width
336
+ # of the column.
337
+ w = self.plot_width / self.ncol
338
+ return [
339
+ max(node.panel_width for node in col if node) if any(col) else w
340
+ for col in self.grid.iter_cols()
341
+ ]
315
342
 
316
- @cached_property
317
- @abc.abstractmethod
318
- def top_tag_height(self) -> float:
319
- """
320
- A representative height [figure space] for the top tags of the nodes
343
+ @property
344
+ def panel_heights(self) -> Sequence[float]:
321
345
  """
346
+ Heights [figure space] of panels by row
322
347
 
323
- @cached_property
324
- def left_tag_widths(self) -> list[float]:
348
+ For each row, the representative number for the panel height
349
+ is the maximum height among all panels in the row.
325
350
  """
326
- The widths of the left tags in this tree
327
- """
328
- return [node.left_tag_width for node in self.nodes]
351
+ h = self.plot_height / self.nrow
352
+ return [
353
+ max([node.panel_height for node in row if node]) if any(row) else h
354
+ for row in self.grid.iter_rows()
355
+ ]
329
356
 
330
- @cached_property
331
- def right_tag_widths(self) -> list[float]:
332
- """
333
- The widths of the right tags in this tree
357
+ @property
358
+ def plot_widths(self) -> Sequence[float]:
334
359
  """
335
- return [node.right_tag_width for node in self.nodes]
360
+ Widths [figure space] of the plots by column
336
361
 
337
- @cached_property
338
- def bottom_tag_heights(self) -> list[float]:
339
- """
340
- The heights of the bottom tags in this tree
362
+ For each column, the representative number is the width of
363
+ the widest plot.
341
364
  """
342
- return [node.bottom_tag_height for node in self.nodes]
343
-
344
- @cached_property
345
- def top_tag_heights(self) -> list[float]:
346
- """
347
- The heights of the top tags in this tree
348
- """
349
- return [node.top_tag_height for node in self.nodes]
365
+ w = self.sub_gridspec.width / self.ncol
366
+ return [
367
+ max([node.plot_width if node else w for node in col])
368
+ for col in self.grid.iter_cols()
369
+ ]
350
370
 
351
371
  @property
352
- def left_tags_align(self) -> bool:
353
- """
354
- Return True if the left tags for the nodes are aligned
372
+ def plot_heights(self) -> Sequence[float]:
355
373
  """
356
- arr = np.array(self.left_tag_widths)
357
- return all(arr == arr[0])
374
+ Heights [figure space] of the plots along vertical dimension
358
375
 
359
- @property
360
- def right_tags_align(self) -> bool:
376
+ For each row, the representative number is the height of
377
+ the tallest plot.
361
378
  """
362
- Return True if the right tags for the nodes are aligned
363
- """
364
- arr = np.array(self.right_tag_widths)
365
- return all(arr == arr[0])
379
+ h = self.sub_gridspec.height / self.nrow
380
+ return [
381
+ max([node.plot_height if node else h for node in row])
382
+ for row in self.grid.iter_rows()
383
+ ]
366
384
 
367
385
  @property
368
- def bottom_tags_align(self) -> bool:
369
- """
370
- Return True if the bottom tags for the nodes are aligned
386
+ def panel_width_ratios(self) -> Sequence[float]:
371
387
  """
372
- arr = np.array(self.bottom_tag_heights)
373
- return all(arr == arr[0])
388
+ The relative widths of the panels in the composition
374
389
 
375
- @property
376
- def top_tags_align(self) -> bool:
390
+ These are normalised to have a mean = 1.
377
391
  """
378
- Return True if the top tags for the nodes are aligned
379
- """
380
- arr = np.array(self.top_tag_heights)
381
- return all(arr == arr[0])
392
+ return cast("Sequence[float]", self.cmp._layout.widths)
382
393
 
383
394
  @property
384
- def left_axis_titles_align(self) -> bool:
385
- """
386
- Return True if the left axis titles align
395
+ def panel_height_ratios(self) -> Sequence[float]:
387
396
  """
388
- arr = np.array(self.left_axis_title_clearances)
389
- return all(arr == arr[0])
397
+ The relative heights of the panels in the composition
390
398
 
391
- @property
392
- def bottom_axis_titles_align(self) -> bool:
393
- """
394
- Return True if the bottom axis titles align
399
+ These are normalised to have a mean = 1.
395
400
  """
396
- arr = np.array(self.bottom_axis_title_clearances)
397
- return all(arr == arr[0])
401
+ return cast("Sequence[float]", self.cmp._layout.heights)
398
402
 
399
- @cached_property
400
- @abc.abstractmethod
401
- def left_axis_title_clearance(self) -> float:
402
- """
403
- Distance between the left y-axis title and the panel
403
+ def bottom_spaces_in_row(self, r: int) -> list[bottom_space]:
404
404
  """
405
+ The bottom_spaces of plots in a given row
405
406
 
406
- @cached_property
407
- @abc.abstractmethod
408
- def bottom_axis_title_clearance(self) -> float:
409
- """
410
- Distance between the left x-axis title and the panel
407
+ If an item in the row is a compositions, then it is the
408
+ bottom_spaces in the bottom row of that composition.
411
409
  """
410
+ spaces: list[bottom_space] = []
411
+ for node in self.grid[r, :]:
412
+ if isinstance(node, PlotSideSpaces):
413
+ spaces.append(node.b)
414
+ elif isinstance(node, LayoutTree):
415
+ spaces.extend(node.bottom_most_spaces)
416
+ return spaces
412
417
 
413
- @cached_property
414
- def left_axis_title_clearances(self) -> list[float]:
418
+ def top_spaces_in_row(self, r: int) -> list[top_space]:
415
419
  """
416
- Distances between the left y-axis titles and the panels
417
- """
418
- return [node.left_axis_title_clearance for node in self.nodes]
420
+ The top_spaces of plots in a given row
419
421
 
420
- @cached_property
421
- def bottom_axis_title_clearances(self) -> list[float]:
422
+ If an item in the row is a compositions, then it is the
423
+ top_spaces in the top row of that composition.
422
424
  """
423
- Distances between the bottom x-axis titles and the panels
424
- """
425
- return [node.bottom_axis_title_clearance for node in self.nodes]
425
+ spaces: list[top_space] = []
426
+ for node in self.grid[r, :]:
427
+ if isinstance(node, PlotSideSpaces):
428
+ spaces.append(node.t)
429
+ elif isinstance(node, LayoutTree):
430
+ spaces.extend(node.top_most_spaces)
431
+ return spaces
426
432
 
427
- @abc.abstractmethod
428
- def set_left_margin_alignment(self, value: float):
433
+ def left_spaces_in_col(self, c: int) -> list[left_space]:
429
434
  """
430
- Set a margin to align the left of the panels in this composition
435
+ The left_spaces plots in a given column
431
436
 
432
- In figure dimenstions
437
+ If an item in the column is a compositions, then it is the
438
+ left_spaces in the left most column of that composition.
433
439
  """
440
+ spaces: list[left_space] = []
441
+ for node in self.grid[:, c]:
442
+ if isinstance(node, PlotSideSpaces):
443
+ spaces.append(node.l)
444
+ elif isinstance(node, LayoutTree):
445
+ spaces.extend(node.left_most_spaces)
446
+ return spaces
434
447
 
435
- @abc.abstractmethod
436
- def set_right_margin_alignment(self, value: float):
448
+ def right_spaces_in_col(self, c: int) -> list[right_space]:
437
449
  """
438
- Set a margin to align the right of the panels in this composition
450
+ The right_spaces of plots in a given column
439
451
 
440
- In figure dimenstions
452
+ If an item in the column is a compositions, then it is the
453
+ right_spaces in the right most column of that composition.
441
454
  """
455
+ spaces: list[right_space] = []
456
+ for node in self.grid[:, c]:
457
+ if isinstance(node, PlotSideSpaces):
458
+ spaces.append(node.r)
459
+ elif isinstance(node, LayoutTree):
460
+ spaces.extend(node.right_most_spaces)
461
+ return spaces
442
462
 
443
- @abc.abstractmethod
444
- def set_bottom_margin_alignment(self, value: float):
463
+ def iter_left_spaces(self) -> Iterator[list[left_space]]:
445
464
  """
446
- Set a margin to align the bottom of the panels in this composition
465
+ Left spaces for each non-empty column
447
466
 
448
- In figure dimenstions
467
+ Will not return an empty list.
449
468
  """
469
+ for c in range(self.ncol):
470
+ spaces = self.left_spaces_in_col(c)
471
+ if spaces:
472
+ yield spaces
450
473
 
451
- @abc.abstractmethod
452
- def set_top_margin_alignment(self, value: float):
474
+ def iter_right_spaces(self) -> Iterator[list[right_space]]:
453
475
  """
454
- Set a margin to align the top of the panels in this composition
476
+ Right spaces for each non-empty column
455
477
 
456
- In figure dimenstions
478
+ Will not return an empty list.
457
479
  """
480
+ for c in range(self.ncol):
481
+ spaces = self.right_spaces_in_col(c)
482
+ if spaces:
483
+ yield spaces
458
484
 
459
- @abc.abstractmethod
460
- def set_left_tag_alignment(self, value: float):
485
+ def iter_bottom_spaces(self) -> Iterator[list[bottom_space]]:
461
486
  """
462
- Set the space to align the left tags in this composition
487
+ Bottom spaces for each non-empty row
463
488
 
464
- In figure dimenstions
489
+ Will not return an empty list.
465
490
  """
491
+ for r in range(self.nrow):
492
+ spaces = self.bottom_spaces_in_row(r)
493
+ if spaces:
494
+ yield spaces
466
495
 
467
- @abc.abstractmethod
468
- def set_right_tag_alignment(self, value: float):
496
+ def iter_top_spaces(self) -> Iterator[list[top_space]]:
469
497
  """
470
- Set the space to align the right tags in this composition
498
+ Top spaces for each non-empty row
471
499
 
472
- In figure dimenstions
500
+ Will not return an empty list.
473
501
  """
502
+ for r in range(self.nrow):
503
+ spaces = self.top_spaces_in_row(r)
504
+ if spaces:
505
+ yield spaces
474
506
 
475
- @abc.abstractmethod
476
- def set_bottom_tag_alignment(self, value: float):
507
+ def align_panels(self):
477
508
  """
478
- Set the space to align the bottom tags in this composition
509
+ Align the edges of the panels in the composition
510
+ """
511
+ for spaces in self.iter_bottom_spaces():
512
+ bottoms = [space.panel_bottom for space in spaces]
513
+ high = max(bottoms)
514
+ diffs = [high - b for b in bottoms]
515
+ for space, diff in zip(spaces, diffs):
516
+ space.margin_alignment += diff
479
517
 
480
- In figure dimenstions
481
- """
518
+ for spaces in self.iter_top_spaces():
519
+ tops = [space.panel_top for space in spaces]
520
+ low = min(tops)
521
+ diffs = [b - low for b in tops]
522
+ for space, diff in zip(spaces, diffs):
523
+ space.margin_alignment += diff
482
524
 
483
- @abc.abstractmethod
484
- def set_top_tag_alignment(self, value: float):
485
- """
486
- Set the space to align the top tags in this composition
525
+ for spaces in self.iter_left_spaces():
526
+ lefts = [space.panel_left for space in spaces]
527
+ high = max(lefts)
528
+ diffs = [high - l for l in lefts]
529
+ for space, diff in zip(spaces, diffs):
530
+ space.margin_alignment += diff
487
531
 
488
- In figure dimenstions
489
- """
532
+ for spaces in self.iter_right_spaces():
533
+ rights = [space.panel_right for space in spaces]
534
+ low = min(rights)
535
+ diffs = [r - low for r in rights]
536
+ for space, diff in zip(spaces, diffs):
537
+ space.margin_alignment += diff
538
+
539
+ def align_tags(self):
540
+ """
541
+ Align the tags in the composition
542
+ """
543
+ for spaces in self.iter_bottom_spaces():
544
+ heights = [
545
+ space.tag_height + space.tag_alignment for space in spaces
546
+ ]
547
+ high = max(heights)
548
+ diffs = [high - h for h in heights]
549
+ for space, diff in zip(spaces, diffs):
550
+ space.tag_alignment += diff
551
+
552
+ for spaces in self.iter_top_spaces():
553
+ heights = [
554
+ space.tag_height + space.tag_alignment for space in spaces
555
+ ]
556
+ high = max(heights)
557
+ diffs = [high - h for h in heights]
558
+ for space, diff in zip(spaces, diffs):
559
+ space.tag_alignment += diff
490
560
 
491
- @abc.abstractmethod
492
- def set_left_axis_title_alignment(self, value: float):
493
- """
494
- Set the space to align the left axis titles in this composition
561
+ for spaces in self.iter_left_spaces():
562
+ widths = [
563
+ space.tag_width + space.tag_alignment for space in spaces
564
+ ]
565
+ high = max(widths)
566
+ diffs = [high - w for w in widths]
567
+ for space, diff in zip(spaces, diffs):
568
+ space.tag_alignment += diff
569
+
570
+ for spaces in self.iter_right_spaces():
571
+ widths = [
572
+ space.tag_width + space.tag_alignment for space in spaces
573
+ ]
574
+ high = max(widths)
575
+ diffs = [high - w for w in widths]
576
+ for space, diff in zip(spaces, diffs):
577
+ space.tag_alignment += diff
495
578
 
496
- In figure dimenstions
579
+ def align_axis_titles(self):
497
580
  """
581
+ Align the axis titles along the composing dimension
498
582
 
499
- @abc.abstractmethod
500
- def set_bottom_axis_title_alignment(self, value: float):
501
- """
502
- Set the space to align the bottom axis titles in this composition
583
+ Since the alignment value used to for this purpose is one of
584
+ the fields in the _side_space, it affects the space created
585
+ for the panel.
503
586
 
504
- In figure dimenstions
587
+ We could align the titles within self.align but we would have
588
+ to store the value outside the _side_space and pick it up when
589
+ setting the position of the texts!
505
590
  """
506
591
 
592
+ for spaces in self.iter_bottom_spaces():
593
+ clearances = [space.axis_title_clearance for space in spaces]
594
+ high = max(clearances)
595
+ diffs = [high - b for b in clearances]
596
+ for space, diff in zip(spaces, diffs):
597
+ space.axis_title_alignment += diff
507
598
 
508
- @dataclass
509
- class ColumnsTree(LayoutTree):
510
- """
511
- Tree with columns at the outermost level
512
-
513
- e.g. p1 | (p2 / p3)
514
-
515
-
516
- -------------------
517
- | | |
518
- | | |
519
- | | |
520
- | | |
521
- | |---------|
522
- | | |
523
- | | |
524
- | | |
525
- | | |
526
- -------------------
527
- """
528
-
529
- def align(self):
530
- self.align_top_tags()
531
- self.align_bottom_tags()
532
- self.align_panel_tops()
533
- self.align_panel_bottoms()
534
- self.align_sub_compositions()
599
+ for spaces in self.iter_left_spaces():
600
+ clearances = [space.axis_title_clearance for space in spaces]
601
+ high = max(clearances)
602
+ diffs = [high - l for l in clearances]
603
+ for space, diff in zip(spaces, diffs):
604
+ space.axis_title_alignment += diff
535
605
 
536
- def align_axis_titles(self):
537
- self.align_bottom_axis_titles()
538
606
  for tree in self.sub_compositions:
539
607
  tree.align_axis_titles()
540
608
 
541
- def resize(self):
542
- """
543
- Resize the widths of gridspec so that panels have equal widths
544
- """
545
- # The new width of each panel is the average width of all
546
- # the panels plus all the space to the left and right
547
- # of the panels.
548
- plot_widths = np.array(self.plot_widths)
549
- panel_widths = np.array(self.panel_widths)
550
- non_panel_space = plot_widths - panel_widths
551
- new_plot_widths = panel_widths.mean() + non_panel_space
552
- width_ratios = new_plot_widths / new_plot_widths.min()
553
- self.gridspec.set_width_ratios(width_ratios)
554
- self.resize_sub_compositions()
555
-
556
- def align_panel_bottoms(self):
557
- """
558
- Align the immediate bottom edges this composition
559
-
560
- ----------- -----------
561
- | | | | | |
562
- | | | | | |
563
- | | | -> | | |
564
- | |#####| |#####|#####|
565
- |#####| | | | |
566
- ----------- -----------
567
- """
568
- # If panels are aligned and have a non-zero margin_alignment,
569
- # aligning them again will set that value to zero and undoes
570
- # the alignment.
571
- if self.panel_bottoms_align:
572
- return
573
-
574
- values = max(self.panel_bottoms) - np.array(self.panel_bottoms)
575
- for item, value in zip(self.nodes, values):
576
- if isinstance(item, LayoutSpaces):
577
- item.b.margin_alignment = value
578
- else:
579
- item.set_bottom_margin_alignment(value)
580
-
581
- del self.panel_bottoms
582
-
583
- def align_panel_tops(self):
609
+ def resize_widths(self):
584
610
  """
585
- Align the immediate top edges in this composition
586
-
587
- ----------- -----------
588
- |#####| | | | |
589
- | |#####| |#####|#####|
590
- | | | -> | | |
591
- | | | | | |
592
- | | | | | |
593
- ----------- -----------
611
+ Resize the widths of the plots & panels in the composition
594
612
  """
595
- if self.panel_tops_align:
596
- return
597
-
598
- values = np.array(self.panel_tops) - min(self.panel_tops)
599
- for item, value in zip(self.nodes, values):
600
- if isinstance(item, LayoutSpaces):
601
- item.t.margin_alignment = value
602
- else:
603
- item.set_top_margin_alignment(value)
604
-
605
- del self.panel_tops
606
-
607
- def align_bottom_tags(self):
608
- if self.bottom_tags_align:
609
- return
610
-
611
- values = cast(
612
- "Sequence[float]",
613
- max(self.bottom_tag_heights) - np.array(self.bottom_tag_heights),
613
+ # The scaling calculation to get the new panel width is
614
+ # straight-forward because the ratios have a mean of 1.
615
+ # So the multiplication preserves the total panel width.
616
+ new_panel_widths = np.mean(self.panel_widths) * np.array(
617
+ self.panel_width_ratios
614
618
  )
615
- for item, value in zip(self.nodes, values):
616
- if isinstance(item, LayoutSpaces):
617
- item.l.tag_alignment = value
618
- else:
619
- item.set_bottom_tag_alignment(value)
620
-
621
- def align_top_tags(self):
622
- if self.top_tags_align:
623
- return
624
-
625
- values = cast(
626
- "Sequence[float]",
627
- max(self.top_tag_heights) - np.array(self.top_tag_heights),
628
- )
629
- for item, value in zip(self.nodes, values):
630
- if isinstance(item, LayoutSpaces):
631
- item.t.tag_alignment = value
632
- else:
633
- item.set_top_tag_alignment(value)
634
-
635
- def align_bottom_axis_titles(self):
636
- if self.bottom_axis_titles_align:
637
- pass
638
-
639
- values = max(self.bottom_axis_title_clearances) - np.array(
640
- self.bottom_axis_title_clearances
641
- )
642
- # We ignore 0 values since they can undo values
643
- # set to align this composition with an outer one.
644
- for item, value in zip(self.nodes, values):
645
- if value == 0:
646
- continue
647
- if isinstance(item, LayoutSpaces):
648
- item.b.axis_title_alignment = value
649
- else:
650
- item.set_bottom_axis_title_alignment(value)
651
-
652
- @cached_property
653
- def panel_lefts(self):
654
- left_item = self.nodes[0]
655
- if isinstance(left_item, LayoutSpaces):
656
- return [left_item.l.panel_left]
657
- else:
658
- return left_item.panel_lefts
659
-
660
- @cached_property
661
- def panel_rights(self):
662
- right_item = self.nodes[-1]
663
- if isinstance(right_item, LayoutSpaces):
664
- return [right_item.r.panel_right]
665
- else:
666
- return right_item.panel_rights
667
-
668
- @cached_property
669
- def panel_bottoms(self):
670
- values = []
671
- for item in self.nodes:
672
- if isinstance(item, LayoutSpaces):
673
- values.append(item.b.panel_bottom)
674
- else:
675
- values.append(max(item.panel_bottoms))
676
- return values
677
-
678
- @cached_property
679
- def panel_tops(self):
680
- values = []
681
- for item in self.nodes:
682
- if isinstance(item, LayoutSpaces):
683
- values.append(item.t.panel_top)
684
- else:
685
- values.append(min(item.panel_tops))
686
- return values
687
-
688
- @property
689
- def panel_width(self) -> float:
690
- return sum(self.panel_widths)
691
-
692
- @property
693
- def panel_height(self) -> float:
694
- return float(np.mean(self.panel_heights))
695
-
696
- @property
697
- def plot_width(self) -> float:
698
- return sum(self.plot_widths)
699
-
700
- @property
701
- def plot_height(self) -> float:
702
- return max(self.plot_heights)
703
-
704
- @cached_property
705
- def left_tag_width(self) -> float:
706
- return self.left_tag_widths[0]
707
-
708
- @cached_property
709
- def right_tag_width(self) -> float:
710
- return self.right_tag_widths[-1]
711
-
712
- @cached_property
713
- def bottom_tag_height(self) -> float:
714
- return max(self.bottom_tag_heights)
715
-
716
- @cached_property
717
- def top_tag_height(self) -> float:
718
- return max(self.top_tag_heights)
719
-
720
- @cached_property
721
- def left_axis_title_clearance(self) -> float:
722
- return self.left_axis_title_clearances[0]
723
-
724
- @cached_property
725
- def bottom_axis_title_clearance(self) -> float:
726
- return max(self.bottom_axis_title_clearances)
727
-
728
- def set_left_margin_alignment(self, value: float):
729
- left_item = self.nodes[0]
730
- if isinstance(left_item, LayoutSpaces):
731
- left_item.l.margin_alignment = value
732
- else:
733
- left_item.set_left_margin_alignment(value)
734
-
735
- def set_right_margin_alignment(self, value: float):
736
- right_item = self.nodes[-1]
737
- if isinstance(right_item, LayoutSpaces):
738
- right_item.r.margin_alignment = value
739
- else:
740
- right_item.set_right_margin_alignment(value)
741
-
742
- def set_bottom_margin_alignment(self, value: float):
743
- for item in self.nodes:
744
- if isinstance(item, LayoutSpaces):
745
- item.b.margin_alignment = value
746
- else:
747
- item.set_bottom_margin_alignment(value)
748
-
749
- def set_top_margin_alignment(self, value: float):
750
- for item in self.nodes:
751
- if isinstance(item, LayoutSpaces):
752
- item.t.margin_alignment = value
753
- else:
754
- item.set_top_margin_alignment(value)
755
-
756
- def set_bottom_tag_alignment(self, value: float):
757
- for item in self.nodes:
758
- if isinstance(item, LayoutSpaces):
759
- item.l.tag_alignment = value
760
- else:
761
- item.set_bottom_tag_alignment(value)
762
-
763
- def set_top_tag_alignment(self, value: float):
764
- for item in self.nodes:
765
- if isinstance(item, LayoutSpaces):
766
- item.t.tag_alignment = value
767
- else:
768
- item.set_top_tag_alignment(value)
769
-
770
- def set_bottom_axis_title_alignment(self, value: float):
771
- for item in self.nodes:
772
- if isinstance(item, LayoutSpaces):
773
- item.b.axis_title_alignment = value
774
- else:
775
- item.set_bottom_axis_title_alignment(value)
776
-
777
- def set_left_axis_title_alignment(self, value: float):
778
- left_item = self.nodes[0]
779
- if isinstance(left_item, LayoutSpaces):
780
- left_item.l.axis_title_alignment = value
781
- else:
782
- left_item.set_left_axis_title_alignment(value)
783
-
784
-
785
- @dataclass
786
- class RowsTree(LayoutTree):
787
- """
788
- Tree with rows at the outermost level
789
-
790
- e.g. p1 / (p2 | p3)
791
-
792
- -------------------
793
- | |
794
- | |
795
- | |
796
- |-------------------|
797
- | | |
798
- | | |
799
- | | |
800
- -------------------
801
- """
619
+ new_plot_widths = new_panel_widths + self.horizontal_spaces
620
+ width_ratios = new_plot_widths / new_plot_widths.max()
621
+ self.sub_gridspec.set_width_ratios(width_ratios)
802
622
 
803
- def align(self):
804
- self.align_left_tags()
805
- self.align_right_tags()
806
- self.align_panel_lefts()
807
- self.align_panel_rights()
808
- self.align_sub_compositions()
809
-
810
- def align_axis_titles(self):
811
- self.align_left_axis_titles()
812
- for tree in self.sub_compositions:
813
- tree.align_axis_titles()
814
-
815
- def resize(self):
623
+ def resize_heights(self):
816
624
  """
817
- Resize the heights of gridspec so that panels have equal heights
818
-
819
- This method resizes (recursively) the contained compositions
625
+ Resize the heights of the plots & panels in the composition
820
626
  """
821
- # The new height of each panel is the average width of all
822
- # the panels plus all the space above and below the panels.
823
- plot_heights = np.array(self.plot_heights)
824
- panel_heights = np.array(self.panel_heights)
825
- non_panel_space = plot_heights - panel_heights
826
- new_plot_heights = panel_heights.mean() + non_panel_space
627
+ new_panel_heights = np.mean(self.panel_heights) * np.array(
628
+ self.panel_height_ratios
629
+ )
630
+ new_plot_heights = new_panel_heights + self.vertical_spaces
827
631
  height_ratios = new_plot_heights / new_plot_heights.max()
828
- self.gridspec.set_height_ratios(height_ratios)
829
- self.resize_sub_compositions()
830
-
831
- def align_panel_lefts(self):
832
- """
833
- Align the immediate left edges in this composition
834
-
835
- ----------- -----------
836
- |# | | # |
837
- |# | | # |
838
- |# | | # |
839
- |-----------| -> |-----------|
840
- | # | | # |
841
- | # | | # |
842
- | # | | # |
843
- ----------- -----------
844
- """
845
- if self.panel_lefts_align:
846
- return
632
+ self.sub_gridspec.set_height_ratios(height_ratios)
847
633
 
848
- values = max(self.panel_lefts) - np.array(self.panel_lefts)
849
- for item, value in zip(self.nodes, values):
850
- if isinstance(item, LayoutSpaces):
851
- item.l.margin_alignment = value
852
- else:
853
- item.set_left_margin_alignment(value)
854
-
855
- del self.panel_lefts
856
634
 
857
- def align_panel_rights(self):
858
- """
859
- Align the immediate right edges in this composition
860
-
861
- ----------- -----------
862
- | # | | # |
863
- | # | | # |
864
- | # | | # |
865
- |-----------| -> |-----------|
866
- | #| | # |
867
- | #| | # |
868
- | #| | # |
869
- ----------- -----------
870
- """
871
- if self.panel_rights_align:
872
- return
635
+ # For debugging
636
+ def _draw_gridspecs(tree: LayoutTree):
637
+ from ..utils import draw_bbox
873
638
 
874
- values = np.array(self.panel_rights) - min(self.panel_rights)
875
- for item, value in zip(self.nodes, values):
876
- if isinstance(item, LayoutSpaces):
877
- item.r.margin_alignment = value
878
- else:
879
- item.set_right_margin_alignment(value)
880
-
881
- del self.panel_rights
882
-
883
- def align_left_tags(self):
884
- """
885
- Make all the left tags takeup the same amount of space
886
-
887
-
888
- Given
889
-
890
- V
891
- ------------------------------------
892
- | plot_margin | tag | artists |
893
- |------------------------------------|
894
- | plot_margin | A long tag | artists |
895
- ------------------------------------
896
-
897
- V
898
- ------------------------------------
899
- | plot_margin | #######tag | artists |
900
- |------------------------------------|
901
- | plot_margin | A long tag | artists |
902
- ------------------------------------
903
- """
904
- if self.left_tags_align:
905
- return
906
-
907
- values = cast(
908
- "Sequence[float]",
909
- max(self.left_tag_widths) - np.array(self.left_tag_widths),
639
+ def draw(t):
640
+ draw_bbox(
641
+ t.cmp._gridspec.bbox_relative,
642
+ t.cmp._gridspec.figure,
910
643
  )
911
- for item, value in zip(self.nodes, values):
912
- if isinstance(item, LayoutSpaces):
913
- item.l.tag_alignment = value
914
- else:
915
- item.set_left_tag_alignment(value)
644
+ for subtree in t.sub_compositions:
645
+ draw(subtree)
916
646
 
917
- def align_right_tags(self):
918
- if self.right_tags_align:
919
- return
647
+ draw(tree)
920
648
 
921
- values = cast(
922
- "Sequence[float]",
923
- max(self.right_tag_widths) - np.array(self.right_tag_widths),
924
- )
925
- for item, value in zip(self.nodes, values):
926
- if isinstance(item, LayoutSpaces):
927
- item.r.tag_alignment = value
928
- else:
929
- item.set_right_tag_alignment(value)
930
649
 
931
- def align_left_axis_titles(self):
932
- if self.left_axis_titles_align:
933
- pass
650
+ def _draw_sub_gridspecs(tree: LayoutTree):
651
+ from ..utils import draw_bbox
934
652
 
935
- values = max(self.left_axis_title_clearances) - np.array(
936
- self.left_axis_title_clearances
653
+ def draw(t):
654
+ draw_bbox(
655
+ t.sub_gridspec.bbox_relative,
656
+ t.sub_gridspec.figure,
937
657
  )
938
- for item, value in zip(self.nodes, values):
939
- if value == 0:
940
- continue
941
- if isinstance(item, LayoutSpaces):
942
- item.l.axis_title_alignment = value
943
- else:
944
- item.set_left_axis_title_alignment(value)
658
+ for subtree in t.sub_compositions:
659
+ draw(subtree)
945
660
 
946
- @cached_property
947
- def panel_lefts(self):
948
- values = []
949
- for item in self.nodes:
950
- if isinstance(item, LayoutSpaces):
951
- values.append(item.l.panel_left)
952
- else:
953
- values.append(max(item.panel_lefts))
954
- return values
955
-
956
- @cached_property
957
- def panel_rights(self):
958
- values = []
959
- for item in self.nodes:
960
- if isinstance(item, LayoutSpaces):
961
- values.append(item.r.panel_right)
962
- else:
963
- values.append(min(item.panel_rights))
964
- return values
965
-
966
- @cached_property
967
- def panel_bottoms(self):
968
- bottom_item = self.nodes[-1]
969
- if isinstance(bottom_item, LayoutSpaces):
970
- return [bottom_item.b.panel_bottom]
971
- else:
972
- return bottom_item.panel_bottoms
973
-
974
- @cached_property
975
- def panel_tops(self):
976
- top_item = self.nodes[0]
977
- if isinstance(top_item, LayoutSpaces):
978
- return [top_item.t.panel_top]
979
- else:
980
- return top_item.panel_tops
981
-
982
- @property
983
- def panel_width(self) -> float:
984
- return float(np.mean(self.panel_widths))
985
-
986
- @property
987
- def panel_height(self) -> float:
988
- return sum(self.panel_heights)
989
-
990
- @property
991
- def plot_width(self) -> float:
992
- return max(self.plot_widths)
993
-
994
- @property
995
- def plot_height(self) -> float:
996
- return sum(self.plot_heights)
997
-
998
- @cached_property
999
- def left_tag_width(self) -> float:
1000
- return max(self.left_tag_widths)
1001
-
1002
- @cached_property
1003
- def right_tag_width(self) -> float:
1004
- return max(self.right_tag_widths)
1005
-
1006
- @cached_property
1007
- def top_tag_height(self) -> float:
1008
- return self.top_tag_heights[0]
1009
-
1010
- @cached_property
1011
- def bottom_tag_height(self) -> float:
1012
- return self.bottom_tag_heights[-1]
1013
-
1014
- @cached_property
1015
- def left_axis_title_clearance(self) -> float:
1016
- return max(self.left_axis_title_clearances)
1017
-
1018
- @cached_property
1019
- def bottom_axis_title_clearance(self) -> float:
1020
- return self.bottom_axis_title_clearances[-1]
1021
-
1022
- def set_left_margin_alignment(self, value: float):
1023
- for item in self.nodes:
1024
- if isinstance(item, LayoutSpaces):
1025
- item.l.margin_alignment = value
1026
- else:
1027
- item.set_left_margin_alignment(value)
1028
-
1029
- def set_right_margin_alignment(self, value: float):
1030
- for item in self.nodes:
1031
- if isinstance(item, LayoutSpaces):
1032
- item.r.margin_alignment = value
1033
- else:
1034
- item.set_right_margin_alignment(value)
1035
-
1036
- def set_bottom_margin_alignment(self, value: float):
1037
- bottom_item = self.nodes[-1]
1038
- if isinstance(bottom_item, LayoutSpaces):
1039
- bottom_item.b.margin_alignment = value
1040
- else:
1041
- bottom_item.set_bottom_margin_alignment(value)
1042
-
1043
- def set_top_margin_alignment(self, value: float):
1044
- top_item = self.nodes[0]
1045
- if isinstance(top_item, LayoutSpaces):
1046
- top_item.t.margin_alignment = value
1047
- else:
1048
- top_item.set_top_margin_alignment(value)
1049
-
1050
- def set_left_tag_alignment(self, value: float):
1051
- for item in self.nodes:
1052
- if isinstance(item, LayoutSpaces):
1053
- item.l.tag_alignment = value
1054
- else:
1055
- item.set_left_tag_alignment(value)
1056
-
1057
- def set_right_tag_alignment(self, value: float):
1058
- for item in self.nodes:
1059
- if isinstance(item, LayoutSpaces):
1060
- item.r.tag_alignment = value
1061
- else:
1062
- item.set_right_tag_alignment(value)
1063
-
1064
- def set_left_axis_title_alignment(self, value: float):
1065
- for item in self.nodes:
1066
- if isinstance(item, LayoutSpaces):
1067
- item.l.axis_title_alignment = value
1068
- else:
1069
- item.set_left_axis_title_alignment(value)
1070
-
1071
- def set_bottom_axis_title_alignment(self, value: float):
1072
- bottom_item = self.nodes[-1]
1073
- if isinstance(bottom_item, LayoutSpaces):
1074
- bottom_item.b.axis_title_alignment = value
1075
- else:
1076
- bottom_item.set_bottom_axis_title_alignment(value)
661
+ draw(tree)