plotnine 0.14.5__py3-none-any.whl → 0.15.0.dev2__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 +916 -0
- plotnine/_mpl/layout_manager/_layout_tree.py +625 -0
- plotnine/_mpl/layout_manager/_spaces.py +1007 -0
- plotnine/_mpl/patches.py +1 -1
- plotnine/_mpl/text.py +59 -24
- plotnine/_mpl/utils.py +78 -10
- plotnine/_utils/__init__.py +5 -5
- plotnine/_utils/dev.py +45 -27
- plotnine/animation.py +1 -1
- plotnine/coords/coord_trans.py +1 -1
- plotnine/data/__init__.py +12 -8
- plotnine/doctools.py +1 -1
- plotnine/facets/facet.py +30 -39
- plotnine/facets/facet_grid.py +14 -6
- plotnine/facets/facet_wrap.py +3 -5
- plotnine/facets/strips.py +7 -9
- plotnine/geoms/geom_crossbar.py +2 -3
- plotnine/geoms/geom_path.py +1 -1
- plotnine/ggplot.py +94 -65
- plotnine/guides/guide.py +12 -10
- plotnine/guides/guide_colorbar.py +3 -3
- plotnine/guides/guide_legend.py +12 -13
- plotnine/guides/guides.py +3 -3
- plotnine/iapi.py +5 -2
- plotnine/labels.py +5 -0
- plotnine/mapping/aes.py +4 -3
- plotnine/options.py +14 -7
- plotnine/plot_composition/__init__.py +10 -0
- plotnine/plot_composition/_compose.py +436 -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/scale.py +1 -1
- plotnine/stats/binning.py +1 -1
- plotnine/stats/smoothers.py +3 -5
- plotnine/stats/stat_density.py +1 -1
- plotnine/stats/stat_qq_line.py +1 -1
- plotnine/stats/stat_sina.py +1 -1
- plotnine/themes/elements/__init__.py +2 -0
- plotnine/themes/elements/element_text.py +35 -24
- plotnine/themes/elements/margin.py +73 -60
- plotnine/themes/targets.py +3 -1
- plotnine/themes/theme.py +13 -7
- plotnine/themes/theme_gray.py +28 -31
- plotnine/themes/theme_matplotlib.py +25 -28
- plotnine/themes/theme_seaborn.py +31 -34
- plotnine/themes/theme_void.py +17 -26
- plotnine/themes/themeable.py +290 -157
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/METADATA +4 -3
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/RECORD +61 -54
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.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.0.dev2.dist-info/licenses}/LICENSE +0 -0
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routines to adjust subplot params so that subplots are
|
|
3
|
+
nicely fit in the figure. In doing so, only axis labels, tick labels, axes
|
|
4
|
+
titles and offsetboxes that are anchored to axes are currently considered.
|
|
5
|
+
|
|
6
|
+
Internally, this module assumes that the margins (left margin, etc.) which are
|
|
7
|
+
differences between `Axes.get_tightbbox` and `Axes.bbox` are independent of
|
|
8
|
+
Axes position. This may fail if `Axes.adjustable` is `datalim` as well as
|
|
9
|
+
such cases as when left or right margin are affected by xlabel.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC
|
|
15
|
+
from dataclasses import dataclass, field, fields
|
|
16
|
+
from functools import cached_property
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from plotnine.facets import facet_grid, facet_null, facet_wrap
|
|
20
|
+
|
|
21
|
+
from ._layout_items import LayoutItems
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from dataclasses import Field
|
|
25
|
+
from typing import Generator
|
|
26
|
+
|
|
27
|
+
from plotnine import ggplot
|
|
28
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Note
|
|
32
|
+
# Margins around the plot are specified in figure coordinates
|
|
33
|
+
# We interpret that value to be a fraction of the width. So along
|
|
34
|
+
# the vertical direction we multiply by W/H to get equal space
|
|
35
|
+
# in both directions
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GridSpecParams:
|
|
40
|
+
"""
|
|
41
|
+
Gridspec Parameters
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
left: float
|
|
45
|
+
right: float
|
|
46
|
+
top: float
|
|
47
|
+
bottom: float
|
|
48
|
+
wspace: float
|
|
49
|
+
hspace: float
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def valid(self) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Return True if the params will create a non-empty area
|
|
55
|
+
"""
|
|
56
|
+
return self.top - self.bottom > 0 and self.right - self.left > 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class _side_spaces(ABC):
|
|
61
|
+
"""
|
|
62
|
+
Base class to for spaces
|
|
63
|
+
|
|
64
|
+
A *_space class does the book keeping for all the artists that may
|
|
65
|
+
fall on that side of the panels. The same name may appear in multiple
|
|
66
|
+
side classes (e.g. legend).
|
|
67
|
+
|
|
68
|
+
The amount of space for each artist is computed in figure coordinates.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
items: LayoutItems
|
|
72
|
+
alignment_margin: float = 0
|
|
73
|
+
"""
|
|
74
|
+
A margin added to align this plot with others in a composition
|
|
75
|
+
|
|
76
|
+
This value is calculated during the layout process in a tree structure
|
|
77
|
+
that has convenient access to the sides/edges of the panels in the
|
|
78
|
+
composition.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __post_init__(self):
|
|
82
|
+
self._calculate()
|
|
83
|
+
|
|
84
|
+
def _calculate(self):
|
|
85
|
+
"""
|
|
86
|
+
Calculate the space taken up by each artist
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def total(self) -> float:
|
|
91
|
+
"""
|
|
92
|
+
Total space
|
|
93
|
+
"""
|
|
94
|
+
return sum(getattr(self, f.name) for f in fields(self)[1:])
|
|
95
|
+
|
|
96
|
+
def sum_upto(self, item: str) -> float:
|
|
97
|
+
"""
|
|
98
|
+
Sum of space upto but not including item
|
|
99
|
+
|
|
100
|
+
Sums from the edge of the figure i.e. the "plot_margin".
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def _fields_upto(item: str) -> Generator[Field, None, None]:
|
|
104
|
+
for f in fields(self)[1:]:
|
|
105
|
+
if f.name == item:
|
|
106
|
+
break
|
|
107
|
+
yield f
|
|
108
|
+
|
|
109
|
+
return sum(getattr(self, f.name) for f in _fields_upto(item))
|
|
110
|
+
|
|
111
|
+
def sum_incl(self, item: str) -> float:
|
|
112
|
+
"""
|
|
113
|
+
Sum of space upto and including the item
|
|
114
|
+
|
|
115
|
+
Sums from the edge of the figure i.e. the "plot_margin".
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def _fields_upto(item: str) -> Generator[Field, None, None]:
|
|
119
|
+
for f in fields(self)[1:]:
|
|
120
|
+
yield f
|
|
121
|
+
if f.name == item:
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
return sum(getattr(self, f.name) for f in _fields_upto(item))
|
|
125
|
+
|
|
126
|
+
@cached_property
|
|
127
|
+
def _legend_size(self) -> tuple[float, float]:
|
|
128
|
+
"""
|
|
129
|
+
Return size of legend in figure coordinates
|
|
130
|
+
|
|
131
|
+
We need this to accurately justify the legend by proportional
|
|
132
|
+
values e.g. 0.2, instead of just left, right, top, bottom &
|
|
133
|
+
center.
|
|
134
|
+
"""
|
|
135
|
+
return (0, 0)
|
|
136
|
+
|
|
137
|
+
@cached_property
|
|
138
|
+
def _legend_width(self) -> float:
|
|
139
|
+
"""
|
|
140
|
+
Return width of legend in figure coordinates
|
|
141
|
+
"""
|
|
142
|
+
return self._legend_size[0]
|
|
143
|
+
|
|
144
|
+
@cached_property
|
|
145
|
+
def _legend_height(self) -> float:
|
|
146
|
+
"""
|
|
147
|
+
Return height of legend in figure coordinates
|
|
148
|
+
"""
|
|
149
|
+
return self._legend_size[1]
|
|
150
|
+
|
|
151
|
+
@cached_property
|
|
152
|
+
def gs(self) -> p9GridSpec:
|
|
153
|
+
"""
|
|
154
|
+
The gridspec of the plot
|
|
155
|
+
"""
|
|
156
|
+
return self.items.plot._gridspec
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def offset(self) -> float:
|
|
160
|
+
"""
|
|
161
|
+
Distance in figure dimensions from the edge of the figure
|
|
162
|
+
|
|
163
|
+
Derived classes should override this method
|
|
164
|
+
|
|
165
|
+
The space/margin and size consumed by artists is in figure dimensions
|
|
166
|
+
but the exact position is relative to the position of the GridSpec
|
|
167
|
+
within the figure. The offset accounts for the position of the
|
|
168
|
+
GridSpec and allows us to accurately place artists using figure
|
|
169
|
+
coordinates.
|
|
170
|
+
|
|
171
|
+
Example of an offset
|
|
172
|
+
|
|
173
|
+
Figure
|
|
174
|
+
----------------------------------------
|
|
175
|
+
| |
|
|
176
|
+
| Plot GridSpec |
|
|
177
|
+
| -------------------------- |
|
|
178
|
+
| offset | | |
|
|
179
|
+
|<------->| X | |
|
|
180
|
+
| | Panels GridSpec | |
|
|
181
|
+
| | -------------------- | |
|
|
182
|
+
| | | | | |
|
|
183
|
+
| | | | | |
|
|
184
|
+
| | | | | |
|
|
185
|
+
| | | | | |
|
|
186
|
+
| | -------------------- | |
|
|
187
|
+
| | | |
|
|
188
|
+
| -------------------------- |
|
|
189
|
+
| |
|
|
190
|
+
----------------------------------------
|
|
191
|
+
"""
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
def to_figure_space(self, rel_value: float) -> float:
|
|
195
|
+
"""
|
|
196
|
+
Convert value relative to the gridspec to one in figure space
|
|
197
|
+
|
|
198
|
+
The result is meant to be used with transFigure transforms.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
rel_value :
|
|
203
|
+
Position relative to the position of the gridspec
|
|
204
|
+
"""
|
|
205
|
+
return self.offset + rel_value
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class left_spaces(_side_spaces):
|
|
210
|
+
"""
|
|
211
|
+
Space in the figure for artists on the left of the panel area
|
|
212
|
+
|
|
213
|
+
Ordered from the edge of the figure and going inwards
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
plot_margin: float = 0
|
|
217
|
+
plot_tag_margin_left: float = 0
|
|
218
|
+
plot_tag: float = 0
|
|
219
|
+
plot_tag_margin_right: float = 0
|
|
220
|
+
legend: float = 0
|
|
221
|
+
legend_box_spacing: float = 0
|
|
222
|
+
axis_title_y_margin_left: float = 0
|
|
223
|
+
axis_title_y: float = 0
|
|
224
|
+
axis_title_y_margin_right: float = 0
|
|
225
|
+
axis_text_y: float = 0
|
|
226
|
+
axis_ticks_y: float = 0
|
|
227
|
+
|
|
228
|
+
def _calculate(self):
|
|
229
|
+
theme = self.items.plot.theme
|
|
230
|
+
calc = self.items.calc
|
|
231
|
+
items = self.items
|
|
232
|
+
|
|
233
|
+
# If the plot_tag is in the margin, it is included in the layout.
|
|
234
|
+
# So we make space for it, including any margins it may have.
|
|
235
|
+
plot_tag_in_layout = theme.getp(
|
|
236
|
+
"plot_tag_location"
|
|
237
|
+
) == "margin" and "left" in theme.getp("plot_tag_position")
|
|
238
|
+
|
|
239
|
+
self.plot_margin = theme.getp("plot_margin_left")
|
|
240
|
+
|
|
241
|
+
if items.plot_tag and plot_tag_in_layout:
|
|
242
|
+
m = theme.get_margin("plot_tag").fig
|
|
243
|
+
self.plot_tag_margin_left = m.l
|
|
244
|
+
self.plot_tag = calc.width(items.plot_tag)
|
|
245
|
+
self.plot_tag_margin_right = m.r
|
|
246
|
+
|
|
247
|
+
if items.legends and items.legends.left:
|
|
248
|
+
self.legend = self._legend_width
|
|
249
|
+
self.legend_box_spacing = theme.getp("legend_box_spacing")
|
|
250
|
+
|
|
251
|
+
if items.axis_title_y:
|
|
252
|
+
m = theme.get_margin("axis_title_y").fig
|
|
253
|
+
self.axis_title_y_margin_left = m.l
|
|
254
|
+
self.axis_title_y = calc.width(items.axis_title_y)
|
|
255
|
+
self.axis_title_y_margin_right = m.r
|
|
256
|
+
|
|
257
|
+
# Account for the space consumed by the axis
|
|
258
|
+
self.axis_text_y = items.axis_text_y_max_width_at("first_col")
|
|
259
|
+
self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col")
|
|
260
|
+
|
|
261
|
+
# Adjust plot_margin to make room for ylabels that protude well
|
|
262
|
+
# beyond the axes
|
|
263
|
+
# NOTE: This adjustment breaks down when the protrusion is large
|
|
264
|
+
protrusion = items.axis_text_x_left_protrusion("all")
|
|
265
|
+
adjustment = protrusion - (self.total - self.plot_margin)
|
|
266
|
+
if adjustment > 0:
|
|
267
|
+
self.plot_margin += adjustment
|
|
268
|
+
|
|
269
|
+
@cached_property
|
|
270
|
+
def _legend_size(self) -> tuple[float, float]:
|
|
271
|
+
if not (self.items.legends and self.items.legends.left):
|
|
272
|
+
return (0, 0)
|
|
273
|
+
|
|
274
|
+
return self.items.calc.size(self.items.legends.left.box)
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def offset(self) -> float:
|
|
278
|
+
"""
|
|
279
|
+
Distance from left of the figure to the left of the plot gridspec
|
|
280
|
+
|
|
281
|
+
----------------(1, 1)
|
|
282
|
+
| ---- |
|
|
283
|
+
| dx | | |
|
|
284
|
+
|<--->| | |
|
|
285
|
+
| | | |
|
|
286
|
+
| ---- |
|
|
287
|
+
(0, 0)----------------
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
return self.gs.bbox_relative.x0
|
|
291
|
+
|
|
292
|
+
def x1(self, item: str) -> float:
|
|
293
|
+
"""
|
|
294
|
+
Lower x-coordinate in figure space of the item
|
|
295
|
+
"""
|
|
296
|
+
return self.to_figure_space(self.sum_upto(item))
|
|
297
|
+
|
|
298
|
+
def x2(self, item: str) -> float:
|
|
299
|
+
"""
|
|
300
|
+
Higher x-coordinate in figure space of the item
|
|
301
|
+
"""
|
|
302
|
+
return self.to_figure_space(self.sum_incl(item))
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def left_relative(self):
|
|
306
|
+
"""
|
|
307
|
+
Left (relative to the gridspec) of the panels in figure dimensions
|
|
308
|
+
"""
|
|
309
|
+
return self.total
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def left(self):
|
|
313
|
+
"""
|
|
314
|
+
Left of the panels in figure space
|
|
315
|
+
"""
|
|
316
|
+
return self.to_figure_space(self.left_relative)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def plot_left(self):
|
|
320
|
+
"""
|
|
321
|
+
Distance up to the left-most artist in figure space
|
|
322
|
+
"""
|
|
323
|
+
return self.x1("legend")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass
|
|
327
|
+
class right_spaces(_side_spaces):
|
|
328
|
+
"""
|
|
329
|
+
Space in the figure for artists on the right of the panel area
|
|
330
|
+
|
|
331
|
+
Ordered from the edge of the figure and going inwards
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
plot_margin: float = 0
|
|
335
|
+
plot_tag_margin_right: float = 0
|
|
336
|
+
plot_tag: float = 0
|
|
337
|
+
plot_tag_margin_left: float = 0
|
|
338
|
+
legend: float = 0
|
|
339
|
+
legend_box_spacing: float = 0
|
|
340
|
+
strip_text_y_width_right: float = 0
|
|
341
|
+
|
|
342
|
+
def _calculate(self):
|
|
343
|
+
items = self.items
|
|
344
|
+
theme = self.items.plot.theme
|
|
345
|
+
calc = self.items.calc
|
|
346
|
+
# If the plot_tag is in the margin, it is included in the layout.
|
|
347
|
+
# So we make space for it, including any margins it may have.
|
|
348
|
+
plot_tag_in_layout = theme.getp(
|
|
349
|
+
"plot_tag_location"
|
|
350
|
+
) == "margin" and "right" in theme.getp("plot_tag_position")
|
|
351
|
+
|
|
352
|
+
self.plot_margin = theme.getp("plot_margin_right")
|
|
353
|
+
|
|
354
|
+
if items.plot_tag and plot_tag_in_layout:
|
|
355
|
+
m = theme.get_margin("plot_tag").fig
|
|
356
|
+
self.plot_tag_margin_right = m.r
|
|
357
|
+
self.plot_tag = calc.width(items.plot_tag)
|
|
358
|
+
self.plot_tag_margin_left = m.l
|
|
359
|
+
|
|
360
|
+
if items.legends and items.legends.right:
|
|
361
|
+
self.legend = self._legend_width
|
|
362
|
+
self.legend_box_spacing = theme.getp("legend_box_spacing")
|
|
363
|
+
|
|
364
|
+
self.strip_text_y_width_right = items.strip_text_y_width("right")
|
|
365
|
+
|
|
366
|
+
# Adjust plot_margin to make room for ylabels that protude well
|
|
367
|
+
# beyond the axes
|
|
368
|
+
# NOTE: This adjustment breaks down when the protrusion is large
|
|
369
|
+
protrusion = items.axis_text_x_right_protrusion("all")
|
|
370
|
+
adjustment = protrusion - (self.total - self.plot_margin)
|
|
371
|
+
if adjustment > 0:
|
|
372
|
+
self.plot_margin += adjustment
|
|
373
|
+
|
|
374
|
+
@cached_property
|
|
375
|
+
def _legend_size(self) -> tuple[float, float]:
|
|
376
|
+
if not (self.items.legends and self.items.legends.right):
|
|
377
|
+
return (0, 0)
|
|
378
|
+
|
|
379
|
+
return self.items.calc.size(self.items.legends.right.box)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def offset(self):
|
|
383
|
+
"""
|
|
384
|
+
Distance from right of the figure to the right of the plot gridspec
|
|
385
|
+
|
|
386
|
+
---------------(1, 1)
|
|
387
|
+
| ---- |
|
|
388
|
+
| | | -dx |
|
|
389
|
+
| | |<--->|
|
|
390
|
+
| | | |
|
|
391
|
+
| ---- |
|
|
392
|
+
(0, 0)---------------
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
return self.gs.bbox_relative.x1 - 1
|
|
396
|
+
|
|
397
|
+
def x1(self, item: str) -> float:
|
|
398
|
+
"""
|
|
399
|
+
Lower x-coordinate in figure space of the item
|
|
400
|
+
"""
|
|
401
|
+
return self.to_figure_space(1 - self.sum_incl(item))
|
|
402
|
+
|
|
403
|
+
def x2(self, item: str) -> float:
|
|
404
|
+
"""
|
|
405
|
+
Higher x-coordinate in figure space of the item
|
|
406
|
+
"""
|
|
407
|
+
return self.to_figure_space(1 - self.sum_upto(item))
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def right_relative(self):
|
|
411
|
+
"""
|
|
412
|
+
Right (relative to the gridspec) of the panels in figure dimensions
|
|
413
|
+
"""
|
|
414
|
+
return 1 - self.total
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def right(self):
|
|
418
|
+
"""
|
|
419
|
+
Right of the panels in figure space
|
|
420
|
+
"""
|
|
421
|
+
return self.to_figure_space(self.right_relative)
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def plot_right(self):
|
|
425
|
+
"""
|
|
426
|
+
Distance up to the right-most artist in figure space
|
|
427
|
+
"""
|
|
428
|
+
return self.x2("legend")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@dataclass
|
|
432
|
+
class top_spaces(_side_spaces):
|
|
433
|
+
"""
|
|
434
|
+
Space in the figure for artists above the panel area
|
|
435
|
+
|
|
436
|
+
Ordered from the edge of the figure and going inwards
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
plot_margin: float = 0
|
|
440
|
+
plot_tag_margin_top: float = 0
|
|
441
|
+
plot_tag: float = 0
|
|
442
|
+
plot_tag_margin_bottom: float = 0
|
|
443
|
+
plot_title_margin_top: float = 0
|
|
444
|
+
plot_title: float = 0
|
|
445
|
+
plot_title_margin_bottom: float = 0
|
|
446
|
+
plot_subtitle_margin_top: float = 0
|
|
447
|
+
plot_subtitle: float = 0
|
|
448
|
+
plot_subtitle_margin_bottom: float = 0
|
|
449
|
+
legend: float = 0
|
|
450
|
+
legend_box_spacing: float = 0
|
|
451
|
+
strip_text_x_height_top: float = 0
|
|
452
|
+
|
|
453
|
+
def _calculate(self):
|
|
454
|
+
items = self.items
|
|
455
|
+
theme = self.items.plot.theme
|
|
456
|
+
calc = self.items.calc
|
|
457
|
+
W, H = theme.getp("figure_size")
|
|
458
|
+
F = W / H
|
|
459
|
+
# If the plot_tag is in the margin, it is included in the layout.
|
|
460
|
+
# So we make space for it, including any margins it may have.
|
|
461
|
+
plot_tag_in_layout = theme.getp(
|
|
462
|
+
"plot_tag_location"
|
|
463
|
+
) == "margin" and "top" in theme.getp("plot_tag_position")
|
|
464
|
+
|
|
465
|
+
self.plot_margin = theme.getp("plot_margin_top") * F
|
|
466
|
+
|
|
467
|
+
if items.plot_tag and plot_tag_in_layout:
|
|
468
|
+
m = theme.get_margin("plot_tag").fig
|
|
469
|
+
self.plot_tag_margin_top = m.t
|
|
470
|
+
self.plot_tag = calc.height(items.plot_tag)
|
|
471
|
+
self.plot_tag_margin_bottom = m.b
|
|
472
|
+
|
|
473
|
+
if items.plot_title:
|
|
474
|
+
m = theme.get_margin("plot_title").fig
|
|
475
|
+
self.plot_title_margin_top = m.t * F
|
|
476
|
+
self.plot_title = calc.height(items.plot_title)
|
|
477
|
+
self.plot_title_margin_bottom = m.b * F
|
|
478
|
+
|
|
479
|
+
if items.plot_subtitle:
|
|
480
|
+
m = theme.get_margin("plot_subtitle").fig
|
|
481
|
+
self.plot_subtitle_margin_top = m.t * F
|
|
482
|
+
self.plot_subtitle = calc.height(items.plot_subtitle)
|
|
483
|
+
self.plot_subtitle_margin_bottom = m.b * F
|
|
484
|
+
|
|
485
|
+
if items.legends and items.legends.top:
|
|
486
|
+
self.legend = self._legend_height
|
|
487
|
+
self.legend_box_spacing = theme.getp("legend_box_spacing") * F
|
|
488
|
+
|
|
489
|
+
self.strip_text_x_height_top = items.strip_text_x_height("top")
|
|
490
|
+
|
|
491
|
+
# Adjust plot_margin to make room for ylabels that protude well
|
|
492
|
+
# beyond the axes
|
|
493
|
+
# NOTE: This adjustment breaks down when the protrusion is large
|
|
494
|
+
protrusion = items.axis_text_y_top_protrusion("all")
|
|
495
|
+
adjustment = protrusion - (self.total - self.plot_margin)
|
|
496
|
+
if adjustment > 0:
|
|
497
|
+
self.plot_margin += adjustment
|
|
498
|
+
|
|
499
|
+
@cached_property
|
|
500
|
+
def _legend_size(self) -> tuple[float, float]:
|
|
501
|
+
if not (self.items.legends and self.items.legends.top):
|
|
502
|
+
return (0, 0)
|
|
503
|
+
|
|
504
|
+
return self.items.calc.size(self.items.legends.top.box)
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def offset(self) -> float:
|
|
508
|
+
"""
|
|
509
|
+
Distance from top of the figure to the top of the plot gridspec
|
|
510
|
+
|
|
511
|
+
----------------(1, 1)
|
|
512
|
+
| ^ |
|
|
513
|
+
| |-dy |
|
|
514
|
+
| v |
|
|
515
|
+
| ---- |
|
|
516
|
+
| | | |
|
|
517
|
+
| | | |
|
|
518
|
+
| | | |
|
|
519
|
+
| ---- |
|
|
520
|
+
| |
|
|
521
|
+
(0, 0)----------------
|
|
522
|
+
"""
|
|
523
|
+
return self.gs.bbox_relative.y1 - 1
|
|
524
|
+
|
|
525
|
+
def y1(self, item: str) -> float:
|
|
526
|
+
"""
|
|
527
|
+
Lower y-coordinate in figure space of the item
|
|
528
|
+
"""
|
|
529
|
+
return self.to_figure_space(1 - self.sum_incl(item))
|
|
530
|
+
|
|
531
|
+
def y2(self, item: str) -> float:
|
|
532
|
+
"""
|
|
533
|
+
Higher y-coordinate in figure space of the item
|
|
534
|
+
"""
|
|
535
|
+
return self.to_figure_space(1 - self.sum_upto(item))
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def top_relative(self):
|
|
539
|
+
"""
|
|
540
|
+
Top (relative to the gridspec) of the panels in figure dimensions
|
|
541
|
+
"""
|
|
542
|
+
return 1 - self.total
|
|
543
|
+
|
|
544
|
+
@property
|
|
545
|
+
def top(self):
|
|
546
|
+
"""
|
|
547
|
+
Top of the panels in figure space
|
|
548
|
+
"""
|
|
549
|
+
return self.to_figure_space(self.top_relative)
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def plot_top(self):
|
|
553
|
+
"""
|
|
554
|
+
Distance up to the top-most artist in figure space
|
|
555
|
+
"""
|
|
556
|
+
return self.y2("legend")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@dataclass
|
|
560
|
+
class bottom_spaces(_side_spaces):
|
|
561
|
+
"""
|
|
562
|
+
Space in the figure for artists below the panel area
|
|
563
|
+
|
|
564
|
+
Ordered from the edge of the figure and going inwards
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
plot_margin: float = 0
|
|
568
|
+
plot_tag_margin_bottom: float = 0
|
|
569
|
+
plot_tag: float = 0
|
|
570
|
+
plot_tag_margin_top: float = 0
|
|
571
|
+
plot_caption_margin_bottom: float = 0
|
|
572
|
+
plot_caption: float = 0
|
|
573
|
+
plot_caption_margin_top: float = 0
|
|
574
|
+
legend: float = 0
|
|
575
|
+
legend_box_spacing: float = 0
|
|
576
|
+
axis_title_x_margin_bottom: float = 0
|
|
577
|
+
axis_title_x: float = 0
|
|
578
|
+
axis_title_x_margin_top: float = 0
|
|
579
|
+
axis_text_x: float = 0
|
|
580
|
+
axis_ticks_x: float = 0
|
|
581
|
+
|
|
582
|
+
def _calculate(self):
|
|
583
|
+
items = self.items
|
|
584
|
+
theme = self.items.plot.theme
|
|
585
|
+
calc = self.items.calc
|
|
586
|
+
W, H = theme.getp("figure_size")
|
|
587
|
+
F = W / H
|
|
588
|
+
# If the plot_tag is in the margin, it is included in the layout.
|
|
589
|
+
# So we make space for it, including any margins it may have.
|
|
590
|
+
plot_tag_in_layout = theme.getp(
|
|
591
|
+
"plot_tag_location"
|
|
592
|
+
) == "margin" and "bottom" in theme.getp("plot_tag_position")
|
|
593
|
+
|
|
594
|
+
self.plot_margin = theme.getp("plot_margin_bottom") * F
|
|
595
|
+
|
|
596
|
+
if items.plot_tag and plot_tag_in_layout:
|
|
597
|
+
m = theme.get_margin("plot_tag").fig
|
|
598
|
+
self.plot_tag_margin_bottom = m.b
|
|
599
|
+
self.plot_tag = calc.height(items.plot_tag)
|
|
600
|
+
self.plot_tag_margin_top = m.t
|
|
601
|
+
|
|
602
|
+
if items.plot_caption:
|
|
603
|
+
m = theme.get_margin("plot_caption").fig
|
|
604
|
+
self.plot_caption_margin_bottom = m.b * F
|
|
605
|
+
self.plot_caption = calc.height(items.plot_caption)
|
|
606
|
+
self.plot_caption_margin_top = m.t * F
|
|
607
|
+
|
|
608
|
+
if items.legends and items.legends.bottom:
|
|
609
|
+
self.legend = self._legend_height
|
|
610
|
+
self.legend_box_spacing = theme.getp("legend_box_spacing") * F
|
|
611
|
+
|
|
612
|
+
if items.axis_title_x:
|
|
613
|
+
m = theme.get_margin("axis_title_x").fig
|
|
614
|
+
self.axis_title_x_margin_bottom = m.b * F
|
|
615
|
+
self.axis_title_x = calc.height(items.axis_title_x)
|
|
616
|
+
self.axis_title_x_margin_top = m.t * F
|
|
617
|
+
|
|
618
|
+
# Account for the space consumed by the axis
|
|
619
|
+
self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row")
|
|
620
|
+
self.axis_text_x = items.axis_text_x_max_height_at("last_row")
|
|
621
|
+
|
|
622
|
+
# Adjust plot_margin to make room for ylabels that protude well
|
|
623
|
+
# beyond the axes
|
|
624
|
+
# NOTE: This adjustment breaks down when the protrusion is large
|
|
625
|
+
protrusion = items.axis_text_y_bottom_protrusion("all")
|
|
626
|
+
adjustment = protrusion - (self.total - self.plot_margin)
|
|
627
|
+
if adjustment > 0:
|
|
628
|
+
self.plot_margin += adjustment
|
|
629
|
+
|
|
630
|
+
@cached_property
|
|
631
|
+
def _legend_size(self) -> tuple[float, float]:
|
|
632
|
+
if not (self.items.legends and self.items.legends.bottom):
|
|
633
|
+
return (0, 0)
|
|
634
|
+
|
|
635
|
+
return self.items.calc.size(self.items.legends.bottom.box)
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def offset(self) -> float:
|
|
639
|
+
"""
|
|
640
|
+
Distance from bottom of the figure to the bottom of the plot gridspec
|
|
641
|
+
|
|
642
|
+
----------------(1, 1)
|
|
643
|
+
| |
|
|
644
|
+
| ---- |
|
|
645
|
+
| | | |
|
|
646
|
+
| | | |
|
|
647
|
+
| | | |
|
|
648
|
+
| ---- |
|
|
649
|
+
| ^ |
|
|
650
|
+
| |dy |
|
|
651
|
+
| v |
|
|
652
|
+
(0, 0)----------------
|
|
653
|
+
"""
|
|
654
|
+
return self.gs.bbox_relative.y0
|
|
655
|
+
|
|
656
|
+
def y1(self, item: str) -> float:
|
|
657
|
+
"""
|
|
658
|
+
Lower y-coordinate in figure space of the item
|
|
659
|
+
"""
|
|
660
|
+
return self.to_figure_space(self.sum_upto(item))
|
|
661
|
+
|
|
662
|
+
def y2(self, item: str) -> float:
|
|
663
|
+
"""
|
|
664
|
+
Higher y-coordinate in figure space of the item
|
|
665
|
+
"""
|
|
666
|
+
return self.to_figure_space(self.sum_incl(item))
|
|
667
|
+
|
|
668
|
+
@property
|
|
669
|
+
def bottom_relative(self):
|
|
670
|
+
"""
|
|
671
|
+
Bottom (relative to the gridspec) of the panels in figure dimensions
|
|
672
|
+
"""
|
|
673
|
+
return self.total
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def bottom(self):
|
|
677
|
+
"""
|
|
678
|
+
Bottom of the panels in figure space
|
|
679
|
+
"""
|
|
680
|
+
return self.to_figure_space(self.bottom_relative)
|
|
681
|
+
|
|
682
|
+
@property
|
|
683
|
+
def plot_bottom(self):
|
|
684
|
+
"""
|
|
685
|
+
Distance up to the bottom-most artist in figure space
|
|
686
|
+
"""
|
|
687
|
+
return self.y1("legend")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@dataclass
|
|
691
|
+
class LayoutSpaces:
|
|
692
|
+
"""
|
|
693
|
+
Compute the all the spaces required in the layout
|
|
694
|
+
|
|
695
|
+
These are:
|
|
696
|
+
|
|
697
|
+
1. The space of each artist between the panel and the edge of the
|
|
698
|
+
figure.
|
|
699
|
+
2. The space in-between the panels
|
|
700
|
+
|
|
701
|
+
From these values, we put together the grid-spec parameters required
|
|
702
|
+
by matplotblib to position the axes. We also use the values to adjust
|
|
703
|
+
the coordinates of all the artists that occupy these spaces, placing
|
|
704
|
+
them in their final positions.
|
|
705
|
+
"""
|
|
706
|
+
|
|
707
|
+
plot: ggplot
|
|
708
|
+
|
|
709
|
+
l: left_spaces = field(init=False)
|
|
710
|
+
"""All subspaces to the left of the panels"""
|
|
711
|
+
|
|
712
|
+
r: right_spaces = field(init=False)
|
|
713
|
+
"""All subspaces to the right of the panels"""
|
|
714
|
+
|
|
715
|
+
t: top_spaces = field(init=False)
|
|
716
|
+
"""All subspaces above the top of the panels"""
|
|
717
|
+
|
|
718
|
+
b: bottom_spaces = field(init=False)
|
|
719
|
+
"""All subspaces below the bottom of the panels"""
|
|
720
|
+
|
|
721
|
+
W: float = field(init=False, default=0)
|
|
722
|
+
"""Figure Width [inches]"""
|
|
723
|
+
|
|
724
|
+
H: float = field(init=False, default=0)
|
|
725
|
+
"""Figure Height [inches]"""
|
|
726
|
+
|
|
727
|
+
w: float = field(init=False, default=0)
|
|
728
|
+
"""Axes width w.r.t figure in [0, 1]"""
|
|
729
|
+
|
|
730
|
+
h: float = field(init=False, default=0)
|
|
731
|
+
"""Axes height w.r.t figure in [0, 1]"""
|
|
732
|
+
|
|
733
|
+
sh: float = field(init=False, default=0)
|
|
734
|
+
"""horizontal spacing btn panels w.r.t figure"""
|
|
735
|
+
|
|
736
|
+
sw: float = field(init=False, default=0)
|
|
737
|
+
"""vertical spacing btn panels w.r.t figure"""
|
|
738
|
+
|
|
739
|
+
gsparams: GridSpecParams = field(init=False)
|
|
740
|
+
"""Grid spacing btn panels w.r.t figure"""
|
|
741
|
+
|
|
742
|
+
def __post_init__(self):
|
|
743
|
+
self.items = LayoutItems(self.plot)
|
|
744
|
+
self.W, self.H = self.plot.theme.getp("figure_size")
|
|
745
|
+
|
|
746
|
+
# Calculate the spacing along the edges of the panel area
|
|
747
|
+
# (spacing required by plotnine)
|
|
748
|
+
self.l = left_spaces(self.items)
|
|
749
|
+
self.r = right_spaces(self.items)
|
|
750
|
+
self.t = top_spaces(self.items)
|
|
751
|
+
self.b = bottom_spaces(self.items)
|
|
752
|
+
|
|
753
|
+
def get_gridspec_params(self) -> GridSpecParams:
|
|
754
|
+
# Calculate the gridspec params
|
|
755
|
+
# (spacing required by mpl)
|
|
756
|
+
self.gsparams = self._calculate_panel_spacing()
|
|
757
|
+
|
|
758
|
+
# Adjust the spacing parameters for the desired aspect ratio
|
|
759
|
+
# It is simpler to adjust for the aspect ratio than to calculate
|
|
760
|
+
# the final parameters that are true to the aspect ratio in
|
|
761
|
+
# one-short
|
|
762
|
+
if (ratio := self.plot.facet._aspect_ratio()) is not None:
|
|
763
|
+
current_ratio = self.aspect_ratio
|
|
764
|
+
if ratio > current_ratio:
|
|
765
|
+
# Increase aspect ratio, taller panels
|
|
766
|
+
self._reduce_width(ratio)
|
|
767
|
+
elif ratio < current_ratio:
|
|
768
|
+
# Increase aspect ratio, wider panels
|
|
769
|
+
self._reduce_height(ratio)
|
|
770
|
+
|
|
771
|
+
return self.gsparams
|
|
772
|
+
|
|
773
|
+
@property
|
|
774
|
+
def plot_width(self) -> float:
|
|
775
|
+
"""
|
|
776
|
+
Width [figure dimensions] of the whole plot
|
|
777
|
+
"""
|
|
778
|
+
return self.plot._gridspec.bbox_relative.width
|
|
779
|
+
|
|
780
|
+
@property
|
|
781
|
+
def plot_height(self) -> float:
|
|
782
|
+
"""
|
|
783
|
+
Height [figure dimensions] of the whole plot
|
|
784
|
+
"""
|
|
785
|
+
return self.plot._gridspec.bbox_relative.height
|
|
786
|
+
|
|
787
|
+
@property
|
|
788
|
+
def panel_width(self) -> float:
|
|
789
|
+
"""
|
|
790
|
+
Width [figure dimensions] of panels
|
|
791
|
+
"""
|
|
792
|
+
return self.r.right - self.l.left
|
|
793
|
+
|
|
794
|
+
@property
|
|
795
|
+
def panel_height(self) -> float:
|
|
796
|
+
"""
|
|
797
|
+
Height [figure dimensions] of panels
|
|
798
|
+
"""
|
|
799
|
+
return self.t.top - self.b.bottom
|
|
800
|
+
|
|
801
|
+
def increase_horizontal_plot_margin(self, dw: float):
|
|
802
|
+
"""
|
|
803
|
+
Increase the plot_margin to the right & left of the panels
|
|
804
|
+
"""
|
|
805
|
+
self.l.plot_margin += dw
|
|
806
|
+
self.r.plot_margin += dw
|
|
807
|
+
|
|
808
|
+
def increase_vertical_plot_margin(self, dh: float):
|
|
809
|
+
"""
|
|
810
|
+
Increase the plot_margin to the above & below of the panels
|
|
811
|
+
"""
|
|
812
|
+
self.t.plot_margin += dh
|
|
813
|
+
self.b.plot_margin += dh
|
|
814
|
+
|
|
815
|
+
@property
|
|
816
|
+
def plot_area_coordinates(
|
|
817
|
+
self,
|
|
818
|
+
) -> tuple[tuple[float, float], tuple[float, float]]:
|
|
819
|
+
"""
|
|
820
|
+
Lower-left and upper-right coordinates of the plot area
|
|
821
|
+
|
|
822
|
+
This is the area surrounded by the plot_margin.
|
|
823
|
+
"""
|
|
824
|
+
x1, x2 = self.l.x2("plot_margin"), self.r.x1("plot_margin")
|
|
825
|
+
y1, y2 = self.b.y2("plot_margin"), self.t.y1("plot_margin")
|
|
826
|
+
return ((x1, y1), (x2, y2))
|
|
827
|
+
|
|
828
|
+
@property
|
|
829
|
+
def panel_area_coordinates(
|
|
830
|
+
self,
|
|
831
|
+
) -> tuple[tuple[float, float], tuple[float, float]]:
|
|
832
|
+
"""
|
|
833
|
+
Lower-left and upper-right coordinates of the panel area
|
|
834
|
+
|
|
835
|
+
This is the area in which the panels are drawn.
|
|
836
|
+
"""
|
|
837
|
+
x1, x2 = self.l.left, self.r.right
|
|
838
|
+
y1, y2 = self.b.bottom, self.t.top
|
|
839
|
+
return ((x1, y1), (x2, y2))
|
|
840
|
+
|
|
841
|
+
def _calculate_panel_spacing(self) -> GridSpecParams:
|
|
842
|
+
"""
|
|
843
|
+
Spacing between the panels (wspace & hspace)
|
|
844
|
+
|
|
845
|
+
Both spaces are calculated from a fraction of the width.
|
|
846
|
+
This ensures that the same fraction gives equals space
|
|
847
|
+
in both directions.
|
|
848
|
+
"""
|
|
849
|
+
if isinstance(self.plot.facet, facet_wrap):
|
|
850
|
+
wspace, hspace = self._calculate_panel_spacing_facet_wrap()
|
|
851
|
+
elif isinstance(self.plot.facet, facet_grid):
|
|
852
|
+
wspace, hspace = self._calculate_panel_spacing_facet_grid()
|
|
853
|
+
elif isinstance(self.plot.facet, facet_null):
|
|
854
|
+
wspace, hspace = self._calculate_panel_spacing_facet_null()
|
|
855
|
+
else:
|
|
856
|
+
raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}")
|
|
857
|
+
|
|
858
|
+
return GridSpecParams(
|
|
859
|
+
self.l.left_relative,
|
|
860
|
+
self.r.right_relative,
|
|
861
|
+
self.t.top_relative,
|
|
862
|
+
self.b.bottom_relative,
|
|
863
|
+
wspace,
|
|
864
|
+
hspace,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
def _calculate_panel_spacing_facet_grid(self) -> tuple[float, float]:
|
|
868
|
+
"""
|
|
869
|
+
Calculate spacing parts for facet_grid
|
|
870
|
+
"""
|
|
871
|
+
theme = self.plot.theme
|
|
872
|
+
|
|
873
|
+
ncol = self.plot.facet.ncol
|
|
874
|
+
nrow = self.plot.facet.nrow
|
|
875
|
+
|
|
876
|
+
# Both spacings are specified as fractions of the figure width
|
|
877
|
+
# Multiply the vertical by (W/H) so that the gullies along both
|
|
878
|
+
# directions are equally spaced.
|
|
879
|
+
self.sw = theme.getp("panel_spacing_x")
|
|
880
|
+
self.sh = theme.getp("panel_spacing_y") * self.W / self.H
|
|
881
|
+
|
|
882
|
+
# width and height of axes as fraction of figure width & height
|
|
883
|
+
self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
|
|
884
|
+
self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
|
|
885
|
+
|
|
886
|
+
# Spacing as fraction of axes width & height
|
|
887
|
+
wspace = self.sw / self.w
|
|
888
|
+
hspace = self.sh / self.h
|
|
889
|
+
return (wspace, hspace)
|
|
890
|
+
|
|
891
|
+
def _calculate_panel_spacing_facet_wrap(self) -> tuple[float, float]:
|
|
892
|
+
"""
|
|
893
|
+
Calculate spacing parts for facet_wrap
|
|
894
|
+
"""
|
|
895
|
+
facet = self.plot.facet
|
|
896
|
+
theme = self.plot.theme
|
|
897
|
+
|
|
898
|
+
ncol = facet.ncol
|
|
899
|
+
nrow = facet.nrow
|
|
900
|
+
|
|
901
|
+
# Both spacings are specified as fractions of the figure width
|
|
902
|
+
self.sw = theme.getp("panel_spacing_x")
|
|
903
|
+
self.sh = theme.getp("panel_spacing_y") * self.W / self.H
|
|
904
|
+
|
|
905
|
+
# A fraction of the strip height
|
|
906
|
+
# Effectively slides the strip
|
|
907
|
+
# +ve: Away from the panel
|
|
908
|
+
# 0: Top of the panel
|
|
909
|
+
# -ve: Into the panel
|
|
910
|
+
# Where values <= -1, put the strip completely into
|
|
911
|
+
# the panel. We do not worry about larger -ves.
|
|
912
|
+
strip_align_x = theme.getp("strip_align_x")
|
|
913
|
+
|
|
914
|
+
# Only interested in the proportion of the strip that
|
|
915
|
+
# does not overlap with the panel
|
|
916
|
+
if strip_align_x > -1:
|
|
917
|
+
self.sh += self.t.strip_text_x_height_top * (1 + strip_align_x)
|
|
918
|
+
|
|
919
|
+
if facet.free["x"]:
|
|
920
|
+
self.sh += self.items.axis_text_x_max_height_at(
|
|
921
|
+
"all"
|
|
922
|
+
) + self.items.axis_ticks_x_max_height_at("all")
|
|
923
|
+
if facet.free["y"]:
|
|
924
|
+
self.sw += self.items.axis_text_y_max_width_at(
|
|
925
|
+
"all"
|
|
926
|
+
) + self.items.axis_ticks_y_max_width_at("all")
|
|
927
|
+
|
|
928
|
+
# width and height of axes as fraction of figure width & height
|
|
929
|
+
self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
|
|
930
|
+
self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
|
|
931
|
+
|
|
932
|
+
# Spacing as fraction of axes width & height
|
|
933
|
+
wspace = self.sw / self.w
|
|
934
|
+
hspace = self.sh / self.h
|
|
935
|
+
return (wspace, hspace)
|
|
936
|
+
|
|
937
|
+
def _calculate_panel_spacing_facet_null(self) -> tuple[float, float]:
|
|
938
|
+
"""
|
|
939
|
+
Calculate spacing parts for facet_null
|
|
940
|
+
"""
|
|
941
|
+
self.w = self.r.right - self.l.left
|
|
942
|
+
self.h = self.t.top - self.b.bottom
|
|
943
|
+
self.sw = 0
|
|
944
|
+
self.sh = 0
|
|
945
|
+
return 0, 0
|
|
946
|
+
|
|
947
|
+
def _reduce_height(self, ratio: float):
|
|
948
|
+
"""
|
|
949
|
+
Reduce the height of axes to get the aspect ratio
|
|
950
|
+
"""
|
|
951
|
+
# New height w.r.t figure height
|
|
952
|
+
h1 = ratio * self.w * (self.W / self.H)
|
|
953
|
+
|
|
954
|
+
# Half of the total vertical reduction w.r.t figure height
|
|
955
|
+
dh = (self.h - h1) * self.plot.facet.nrow / 2
|
|
956
|
+
|
|
957
|
+
# Reduce plot area height
|
|
958
|
+
self.gsparams.top -= dh
|
|
959
|
+
self.gsparams.bottom += dh
|
|
960
|
+
self.gsparams.hspace = self.sh / h1
|
|
961
|
+
|
|
962
|
+
# Add more vertical plot margin
|
|
963
|
+
self.increase_vertical_plot_margin(dh)
|
|
964
|
+
|
|
965
|
+
def _reduce_width(self, ratio: float):
|
|
966
|
+
"""
|
|
967
|
+
Reduce the width of axes to get the aspect ratio
|
|
968
|
+
"""
|
|
969
|
+
# New width w.r.t figure width
|
|
970
|
+
w1 = (self.h * self.H) / (ratio * self.W)
|
|
971
|
+
|
|
972
|
+
# Half of the total horizontal reduction w.r.t figure width
|
|
973
|
+
dw = (self.w - w1) * self.plot.facet.ncol / 2
|
|
974
|
+
|
|
975
|
+
# Reduce width
|
|
976
|
+
self.gsparams.left += dw
|
|
977
|
+
self.gsparams.right -= dw
|
|
978
|
+
self.gsparams.wspace = self.sw / w1
|
|
979
|
+
|
|
980
|
+
# Add more horizontal margin
|
|
981
|
+
self.increase_horizontal_plot_margin(dw)
|
|
982
|
+
|
|
983
|
+
@property
|
|
984
|
+
def aspect_ratio(self) -> float:
|
|
985
|
+
"""
|
|
986
|
+
Default aspect ratio of the panels
|
|
987
|
+
"""
|
|
988
|
+
return (self.h * self.H) / (self.w * self.W)
|
|
989
|
+
|
|
990
|
+
@cached_property
|
|
991
|
+
def gs(self) -> p9GridSpec:
|
|
992
|
+
"""
|
|
993
|
+
The gridspec
|
|
994
|
+
"""
|
|
995
|
+
return self.plot._gridspec
|
|
996
|
+
|
|
997
|
+
def to_figure_space(
|
|
998
|
+
self,
|
|
999
|
+
position: tuple[float, float],
|
|
1000
|
+
) -> tuple[float, float]:
|
|
1001
|
+
"""
|
|
1002
|
+
Convert position from gridspec space to figure space
|
|
1003
|
+
"""
|
|
1004
|
+
_x, _y = position
|
|
1005
|
+
x = self.l.plot_left + (self.r.plot_right - self.l.plot_left) * _x
|
|
1006
|
+
y = self.b.plot_bottom + (self.t.plot_top - self.b.plot_bottom) * _y
|
|
1007
|
+
return (x, y)
|