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.
- plotnine/__init__.py +31 -37
- plotnine/_mpl/gridspec.py +265 -0
- plotnine/_mpl/layout_manager/__init__.py +6 -0
- plotnine/_mpl/layout_manager/_engine.py +87 -0
- plotnine/_mpl/layout_manager/_layout_items.py +957 -0
- plotnine/_mpl/layout_manager/_layout_tree.py +905 -0
- plotnine/_mpl/layout_manager/_spaces.py +1154 -0
- plotnine/_mpl/patches.py +70 -34
- plotnine/_mpl/text.py +159 -37
- plotnine/_mpl/utils.py +78 -10
- plotnine/_utils/__init__.py +35 -9
- plotnine/_utils/dev.py +45 -27
- plotnine/_utils/yippie.py +115 -0
- plotnine/animation.py +1 -1
- plotnine/coords/coord.py +3 -3
- plotnine/coords/coord_trans.py +1 -1
- plotnine/data/__init__.py +43 -8
- plotnine/data/anscombe-quartet.csv +45 -0
- plotnine/doctools.py +2 -2
- plotnine/facets/facet.py +34 -43
- plotnine/facets/facet_grid.py +14 -6
- plotnine/facets/facet_wrap.py +3 -5
- plotnine/facets/strips.py +20 -33
- plotnine/geoms/annotate.py +3 -3
- plotnine/geoms/annotation_logticks.py +2 -0
- plotnine/geoms/annotation_stripes.py +2 -0
- plotnine/geoms/geom.py +3 -3
- plotnine/geoms/geom_bar.py +10 -2
- plotnine/geoms/geom_col.py +6 -0
- plotnine/geoms/geom_crossbar.py +2 -3
- plotnine/geoms/geom_path.py +2 -2
- plotnine/geoms/geom_violin.py +24 -7
- plotnine/ggplot.py +95 -66
- plotnine/guides/guide.py +19 -20
- plotnine/guides/guide_colorbar.py +6 -6
- plotnine/guides/guide_legend.py +15 -16
- plotnine/guides/guides.py +8 -8
- plotnine/helpers.py +49 -0
- plotnine/iapi.py +33 -7
- plotnine/labels.py +8 -3
- plotnine/layer.py +4 -4
- plotnine/mapping/_env.py +2 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +14 -30
- plotnine/mapping/evaluation.py +7 -65
- plotnine/options.py +14 -7
- plotnine/plot_composition/__init__.py +10 -0
- plotnine/plot_composition/_compose.py +462 -0
- plotnine/plot_composition/_plotspec.py +50 -0
- plotnine/plot_composition/_spacer.py +32 -0
- plotnine/positions/position_dodge.py +1 -1
- plotnine/positions/position_dodge2.py +1 -1
- plotnine/positions/position_stack.py +1 -2
- plotnine/qplot.py +1 -2
- plotnine/scales/__init__.py +0 -6
- plotnine/scales/limits.py +7 -7
- plotnine/scales/scale.py +4 -4
- plotnine/scales/scale_continuous.py +2 -1
- plotnine/scales/scale_identity.py +10 -2
- plotnine/scales/scale_manual.py +6 -2
- plotnine/stats/binning.py +5 -2
- plotnine/stats/smoothers.py +3 -5
- plotnine/stats/stat.py +3 -3
- plotnine/stats/stat_bindot.py +1 -3
- plotnine/stats/stat_density.py +2 -2
- plotnine/stats/stat_qq_line.py +1 -1
- plotnine/stats/stat_sina.py +34 -1
- plotnine/themes/elements/__init__.py +3 -0
- plotnine/themes/elements/element_text.py +35 -24
- plotnine/themes/elements/margin.py +137 -61
- plotnine/themes/targets.py +3 -1
- plotnine/themes/theme.py +21 -7
- plotnine/themes/theme_538.py +0 -1
- plotnine/themes/theme_bw.py +0 -1
- plotnine/themes/theme_dark.py +0 -1
- plotnine/themes/theme_gray.py +32 -34
- plotnine/themes/theme_light.py +1 -1
- plotnine/themes/theme_matplotlib.py +28 -31
- plotnine/themes/theme_seaborn.py +36 -36
- plotnine/themes/theme_void.py +25 -27
- plotnine/themes/theme_xkcd.py +0 -1
- plotnine/themes/themeable.py +369 -169
- plotnine/typing.py +3 -3
- plotnine/watermark.py +3 -3
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/METADATA +8 -5
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/RECORD +89 -78
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/WHEEL +1 -1
- plotnine/_mpl/_plot_side_space.py +0 -888
- plotnine/_mpl/_plotnine_tight_layout.py +0 -293
- plotnine/_mpl/layout_engine.py +0 -110
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info/licenses}/LICENSE +0 -0
- {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)
|