plotnine 0.15.3__py3-none-any.whl → 0.16.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/_mpl/gridspec.py +50 -6
- plotnine/_mpl/layout_manager/__init__.py +2 -5
- plotnine/_mpl/layout_manager/_composition_layout_items.py +98 -0
- plotnine/_mpl/layout_manager/_composition_side_space.py +461 -0
- plotnine/_mpl/layout_manager/_engine.py +19 -58
- plotnine/_mpl/layout_manager/_grid.py +94 -0
- plotnine/_mpl/layout_manager/_layout_tree.py +402 -817
- plotnine/_mpl/layout_manager/{_layout_items.py → _plot_layout_items.py} +55 -278
- plotnine/_mpl/layout_manager/{_spaces.py → _plot_side_space.py} +111 -291
- plotnine/_mpl/layout_manager/_side_space.py +176 -0
- plotnine/_mpl/utils.py +259 -1
- plotnine/_utils/__init__.py +23 -3
- plotnine/_utils/context.py +9 -13
- plotnine/_utils/dataclasses.py +24 -0
- plotnine/_utils/ipython.py +4 -1
- plotnine/animation.py +13 -12
- plotnine/composition/__init__.py +6 -0
- plotnine/composition/_beside.py +13 -11
- plotnine/composition/_compose.py +263 -99
- plotnine/composition/_plot_annotation.py +75 -0
- plotnine/composition/_plot_layout.py +143 -0
- plotnine/composition/_plot_spacer.py +1 -1
- plotnine/composition/_stack.py +13 -11
- plotnine/composition/_types.py +28 -0
- plotnine/composition/_wrap.py +60 -0
- plotnine/facets/facet.py +9 -12
- plotnine/facets/facet_grid.py +2 -2
- plotnine/facets/facet_wrap.py +1 -1
- plotnine/geoms/geom.py +2 -2
- plotnine/geoms/geom_map.py +4 -5
- plotnine/geoms/geom_path.py +8 -7
- plotnine/geoms/geom_rug.py +6 -10
- plotnine/geoms/geom_text.py +5 -5
- plotnine/ggplot.py +63 -9
- plotnine/guides/guide.py +24 -6
- plotnine/guides/guide_colorbar.py +88 -46
- plotnine/guides/guide_legend.py +47 -20
- plotnine/guides/guides.py +2 -2
- plotnine/iapi.py +17 -1
- plotnine/scales/scale.py +1 -1
- plotnine/stats/binning.py +15 -43
- plotnine/stats/smoothers.py +7 -3
- plotnine/stats/stat.py +2 -2
- plotnine/stats/stat_density_2d.py +10 -6
- plotnine/stats/stat_pointdensity.py +8 -1
- plotnine/stats/stat_qq.py +5 -5
- plotnine/stats/stat_qq_line.py +6 -1
- plotnine/stats/stat_sina.py +19 -20
- plotnine/stats/stat_summary.py +4 -2
- plotnine/stats/stat_summary_bin.py +7 -1
- plotnine/themes/elements/element_line.py +2 -0
- plotnine/themes/elements/element_text.py +12 -1
- plotnine/themes/theme.py +18 -24
- plotnine/themes/themeable.py +17 -3
- plotnine/typing.py +9 -2
- {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/METADATA +3 -3
- {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/RECORD +60 -52
- {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/WHEEL +1 -1
- plotnine/composition/_plotspec.py +0 -50
- {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
|
|
56
|
+
LayoutTree (.nrow=1, .ncol=3)
|
|
51
57
|
|
|
|
52
58
|
----------------------------
|
|
53
59
|
| | |
|
|
54
|
-
LayoutSpaces LayoutSpaces
|
|
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
|
-
|
|
64
|
-
"""
|
|
65
|
-
Gridspec of the composition
|
|
95
|
+
## How it works
|
|
66
96
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
# Create subtree
|
|
168
|
+
nodes: list[PlotSideSpaces | LayoutTree] = []
|
|
106
169
|
for item in cmp:
|
|
107
170
|
if isinstance(item, ggplot):
|
|
108
|
-
nodes.append(
|
|
171
|
+
nodes.append(item._sidespaces)
|
|
109
172
|
else:
|
|
110
|
-
nodes.append(LayoutTree.create(item
|
|
173
|
+
nodes.append(LayoutTree.create(item))
|
|
174
|
+
|
|
175
|
+
return LayoutTree(cmp, nodes)
|
|
111
176
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
|
237
|
+
def bottom_most_spaces(self) -> list[bottom_space]:
|
|
174
238
|
"""
|
|
175
|
-
|
|
239
|
+
Bottom spaces of items in the last row
|
|
176
240
|
"""
|
|
177
|
-
return [
|
|
241
|
+
return [s for s in self.bottom_spaces_in_row(self.nrow - 1)]
|
|
178
242
|
|
|
179
243
|
@cached_property
|
|
180
|
-
|
|
181
|
-
def panel_lefts(self) -> Sequence[float]:
|
|
244
|
+
def top_most_spaces(self) -> list[top_space]:
|
|
182
245
|
"""
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
def panel_rights(self) -> Sequence[float]:
|
|
251
|
+
def left_most_spaces(self) -> list[left_space]:
|
|
189
252
|
"""
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
def panel_bottoms(self) -> Sequence[float]:
|
|
258
|
+
def right_most_spaces(self) -> list[right_space]:
|
|
196
259
|
"""
|
|
197
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
293
|
+
def horizontal_space(self) -> float:
|
|
269
294
|
"""
|
|
270
|
-
|
|
295
|
+
Horizontal non-panel space in this composition
|
|
271
296
|
"""
|
|
272
|
-
return
|
|
297
|
+
return sum(self.horizontal_spaces)
|
|
273
298
|
|
|
274
299
|
@property
|
|
275
|
-
def
|
|
300
|
+
def vertical_space(self) -> float:
|
|
276
301
|
"""
|
|
277
|
-
|
|
302
|
+
Vertical non-panel space in this composition
|
|
278
303
|
"""
|
|
279
|
-
return
|
|
304
|
+
return sum(self.vertical_spaces)
|
|
280
305
|
|
|
281
306
|
@property
|
|
282
|
-
def
|
|
307
|
+
def horizontal_spaces(self) -> Sequence[float]:
|
|
283
308
|
"""
|
|
284
|
-
|
|
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
|
|
314
|
+
return list(np.array(self.plot_widths) - self.panel_widths)
|
|
287
315
|
|
|
288
316
|
@property
|
|
289
|
-
def
|
|
290
|
-
"""
|
|
291
|
-
Heights [figure space] of the panels in this tree
|
|
317
|
+
def vertical_spaces(self) -> Sequence[float]:
|
|
292
318
|
"""
|
|
293
|
-
|
|
319
|
+
Vertical non-panel space by row
|
|
294
320
|
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
@
|
|
303
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
@
|
|
317
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
@
|
|
331
|
-
def
|
|
332
|
-
"""
|
|
333
|
-
The widths of the right tags in this tree
|
|
357
|
+
@property
|
|
358
|
+
def plot_widths(self) -> Sequence[float]:
|
|
334
359
|
"""
|
|
335
|
-
|
|
360
|
+
Widths [figure space] of the plots by column
|
|
336
361
|
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
353
|
-
"""
|
|
354
|
-
Return True if the left tags for the nodes are aligned
|
|
372
|
+
def plot_heights(self) -> Sequence[float]:
|
|
355
373
|
"""
|
|
356
|
-
|
|
357
|
-
return all(arr == arr[0])
|
|
374
|
+
Heights [figure space] of the plots along vertical dimension
|
|
358
375
|
|
|
359
|
-
|
|
360
|
-
|
|
376
|
+
For each row, the representative number is the height of
|
|
377
|
+
the tallest plot.
|
|
361
378
|
"""
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
369
|
-
"""
|
|
370
|
-
Return True if the bottom tags for the nodes are aligned
|
|
386
|
+
def panel_width_ratios(self) -> Sequence[float]:
|
|
371
387
|
"""
|
|
372
|
-
|
|
373
|
-
return all(arr == arr[0])
|
|
388
|
+
The relative widths of the panels in the composition
|
|
374
389
|
|
|
375
|
-
|
|
376
|
-
def top_tags_align(self) -> bool:
|
|
390
|
+
These are normalised to have a mean = 1.
|
|
377
391
|
"""
|
|
378
|
-
|
|
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
|
|
385
|
-
"""
|
|
386
|
-
Return True if the left axis titles align
|
|
395
|
+
def panel_height_ratios(self) -> Sequence[float]:
|
|
387
396
|
"""
|
|
388
|
-
|
|
389
|
-
return all(arr == arr[0])
|
|
397
|
+
The relative heights of the panels in the composition
|
|
390
398
|
|
|
391
|
-
|
|
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
|
-
|
|
397
|
-
return all(arr == arr[0])
|
|
401
|
+
return cast("Sequence[float]", self.cmp._layout.heights)
|
|
398
402
|
|
|
399
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
414
|
-
def left_axis_title_clearances(self) -> list[float]:
|
|
418
|
+
def top_spaces_in_row(self, r: int) -> list[top_space]:
|
|
415
419
|
"""
|
|
416
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
def set_left_margin_alignment(self, value: float):
|
|
433
|
+
def left_spaces_in_col(self, c: int) -> list[left_space]:
|
|
429
434
|
"""
|
|
430
|
-
|
|
435
|
+
The left_spaces plots in a given column
|
|
431
436
|
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
def set_right_margin_alignment(self, value: float):
|
|
448
|
+
def right_spaces_in_col(self, c: int) -> list[right_space]:
|
|
437
449
|
"""
|
|
438
|
-
|
|
450
|
+
The right_spaces of plots in a given column
|
|
439
451
|
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
def set_bottom_margin_alignment(self, value: float):
|
|
463
|
+
def iter_left_spaces(self) -> Iterator[list[left_space]]:
|
|
445
464
|
"""
|
|
446
|
-
|
|
465
|
+
Left spaces for each non-empty column
|
|
447
466
|
|
|
448
|
-
|
|
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
|
-
|
|
452
|
-
def set_top_margin_alignment(self, value: float):
|
|
474
|
+
def iter_right_spaces(self) -> Iterator[list[right_space]]:
|
|
453
475
|
"""
|
|
454
|
-
|
|
476
|
+
Right spaces for each non-empty column
|
|
455
477
|
|
|
456
|
-
|
|
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
|
-
|
|
460
|
-
def set_left_tag_alignment(self, value: float):
|
|
485
|
+
def iter_bottom_spaces(self) -> Iterator[list[bottom_space]]:
|
|
461
486
|
"""
|
|
462
|
-
|
|
487
|
+
Bottom spaces for each non-empty row
|
|
463
488
|
|
|
464
|
-
|
|
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
|
-
|
|
468
|
-
def set_right_tag_alignment(self, value: float):
|
|
496
|
+
def iter_top_spaces(self) -> Iterator[list[top_space]]:
|
|
469
497
|
"""
|
|
470
|
-
|
|
498
|
+
Top spaces for each non-empty row
|
|
471
499
|
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
def set_bottom_tag_alignment(self, value: float):
|
|
507
|
+
def align_panels(self):
|
|
477
508
|
"""
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
579
|
+
def align_axis_titles(self):
|
|
497
580
|
"""
|
|
581
|
+
Align the axis titles along the composing dimension
|
|
498
582
|
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
----------- -----------
|
|
588
|
-
|#####| | | | |
|
|
589
|
-
| |#####| |#####|#####|
|
|
590
|
-
| | | -> | | |
|
|
591
|
-
| | | | | |
|
|
592
|
-
| | | | | |
|
|
593
|
-
----------- -----------
|
|
611
|
+
Resize the widths of the plots & panels in the composition
|
|
594
612
|
"""
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
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
|
|
818
|
-
|
|
819
|
-
This method resizes (recursively) the contained compositions
|
|
625
|
+
Resize the heights of the plots & panels in the composition
|
|
820
626
|
"""
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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.
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
|
912
|
-
|
|
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
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
pass
|
|
650
|
+
def _draw_sub_gridspecs(tree: LayoutTree):
|
|
651
|
+
from ..utils import draw_bbox
|
|
934
652
|
|
|
935
|
-
|
|
936
|
-
|
|
653
|
+
def draw(t):
|
|
654
|
+
draw_bbox(
|
|
655
|
+
t.sub_gridspec.bbox_relative,
|
|
656
|
+
t.sub_gridspec.figure,
|
|
937
657
|
)
|
|
938
|
-
for
|
|
939
|
-
|
|
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
|
-
|
|
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)
|