plotnine 0.14.5__py3-none-any.whl → 0.15.0.dev1__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 +775 -0
- plotnine/_mpl/layout_manager/_layout_tree.py +625 -0
- plotnine/_mpl/layout_manager/_spaces.py +1007 -0
- plotnine/_mpl/utils.py +78 -10
- plotnine/_utils/__init__.py +4 -4
- 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 +2 -7
- plotnine/geoms/geom_crossbar.py +2 -3
- plotnine/geoms/geom_path.py +1 -1
- plotnine/ggplot.py +94 -65
- plotnine/guides/guide.py +10 -8
- plotnine/guides/guide_colorbar.py +3 -3
- plotnine/guides/guide_legend.py +5 -5
- plotnine/guides/guides.py +3 -3
- plotnine/iapi.py +1 -0
- plotnine/labels.py +5 -0
- plotnine/options.py +14 -7
- plotnine/plot_composition/__init__.py +10 -0
- plotnine/plot_composition/_compose.py +427 -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 +34 -24
- plotnine/themes/elements/margin.py +73 -60
- plotnine/themes/targets.py +2 -0
- plotnine/themes/theme.py +13 -7
- plotnine/themes/theme_gray.py +27 -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 +286 -153
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev1.dist-info}/METADATA +4 -3
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev1.dist-info}/RECORD +58 -51
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev1.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.dev1.dist-info/licenses}/LICENSE +0 -0
- {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev1.dist-info}/top_level.txt +0 -0
|
@@ -1,888 +0,0 @@
|
|
|
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, fields
|
|
16
|
-
from functools import cached_property
|
|
17
|
-
from itertools import chain
|
|
18
|
-
from typing import TYPE_CHECKING, cast
|
|
19
|
-
|
|
20
|
-
from matplotlib._tight_layout import get_subplotspec_list
|
|
21
|
-
|
|
22
|
-
from ..facets import facet_grid, facet_null, facet_wrap
|
|
23
|
-
from .utils import bbox_in_figure_space, tight_bbox_in_figure_space
|
|
24
|
-
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from dataclasses import Field
|
|
27
|
-
from typing import (
|
|
28
|
-
Generator,
|
|
29
|
-
Iterator,
|
|
30
|
-
Literal,
|
|
31
|
-
Sequence,
|
|
32
|
-
TypeAlias,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
from matplotlib.artist import Artist
|
|
36
|
-
from matplotlib.axes import Axes
|
|
37
|
-
from matplotlib.axis import Tick
|
|
38
|
-
from matplotlib.text import Text
|
|
39
|
-
|
|
40
|
-
from .layout_engine import LayoutPack
|
|
41
|
-
|
|
42
|
-
AxesLocation: TypeAlias = Literal[
|
|
43
|
-
"all", "first_row", "last_row", "first_col", "last_col"
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
# Note
|
|
47
|
-
# Margins around the plot are specified in figure coordinates
|
|
48
|
-
# We interpret that value to be a fraction of the width. So along
|
|
49
|
-
# the vertical direction we multiply by W/H to get equal space
|
|
50
|
-
# in both directions
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@dataclass
|
|
54
|
-
class WHSpaceParts:
|
|
55
|
-
"""
|
|
56
|
-
Width-Height Spaces
|
|
57
|
-
|
|
58
|
-
We need these in one places for easy access
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
W: float # Figure width
|
|
62
|
-
H: float # Figure height
|
|
63
|
-
w: float # Axes width w.r.t figure in [0, 1]
|
|
64
|
-
h: float # Axes height w.r.t figure in [0, 1]
|
|
65
|
-
sw: float # horizontal spacing btn panels w.r.t figure
|
|
66
|
-
sh: float # vertical spacing btn panels w.r.t figure
|
|
67
|
-
wspace: float # mpl.subplotpars.wspace
|
|
68
|
-
hspace: float # mpl.subplotpars.hspace
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def aspect_ratio(self) -> float:
|
|
72
|
-
"""
|
|
73
|
-
Aspect ratio of the panels
|
|
74
|
-
"""
|
|
75
|
-
return (self.h * self.H) / (self.w * self.W)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@dataclass
|
|
79
|
-
class _side_spaces(ABC):
|
|
80
|
-
"""
|
|
81
|
-
Base class to for spaces
|
|
82
|
-
|
|
83
|
-
A *_space class should track the size taken up by all the objects that
|
|
84
|
-
may fall on that side of the panel. The same name may appear in multiple
|
|
85
|
-
side classes (e.g. legend).
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
pack: LayoutPack
|
|
89
|
-
|
|
90
|
-
def __post_init__(self):
|
|
91
|
-
self._calculate()
|
|
92
|
-
|
|
93
|
-
def _calculate(self):
|
|
94
|
-
"""
|
|
95
|
-
Calculate the space taken up by each artist
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def total(self) -> float:
|
|
100
|
-
"""
|
|
101
|
-
Total space
|
|
102
|
-
"""
|
|
103
|
-
return sum(getattr(self, f.name) for f in fields(self)[1:])
|
|
104
|
-
|
|
105
|
-
def sum_upto(self, item: str) -> float:
|
|
106
|
-
"""
|
|
107
|
-
Sum of space upto but not including item
|
|
108
|
-
|
|
109
|
-
Sums starting at the edge of the figure i.e. the "plot_margin".
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
def _fields_upto(item: str) -> Generator[Field, None, None]:
|
|
113
|
-
for f in fields(self)[1:]:
|
|
114
|
-
if f.name == item:
|
|
115
|
-
break
|
|
116
|
-
yield f
|
|
117
|
-
|
|
118
|
-
return sum(getattr(self, f.name) for f in _fields_upto(item))
|
|
119
|
-
|
|
120
|
-
@cached_property
|
|
121
|
-
def _legend_size(self) -> tuple[float, float]:
|
|
122
|
-
"""
|
|
123
|
-
Return size of legend in figure coordinates
|
|
124
|
-
|
|
125
|
-
We need this to accurately justify the legend by proportional
|
|
126
|
-
values e.g. 0.2, instead of just left, right, top, bottom &
|
|
127
|
-
center.
|
|
128
|
-
"""
|
|
129
|
-
return (0, 0)
|
|
130
|
-
|
|
131
|
-
@cached_property
|
|
132
|
-
def _legend_width(self) -> float:
|
|
133
|
-
"""
|
|
134
|
-
Return width of legend in figure coordinates
|
|
135
|
-
"""
|
|
136
|
-
return self._legend_size[0]
|
|
137
|
-
|
|
138
|
-
@cached_property
|
|
139
|
-
def _legend_height(self) -> float:
|
|
140
|
-
"""
|
|
141
|
-
Return height of legend in figure coordinates
|
|
142
|
-
"""
|
|
143
|
-
return self._legend_size[1]
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@dataclass
|
|
147
|
-
class left_spaces(_side_spaces):
|
|
148
|
-
"""
|
|
149
|
-
Space in the figure for artists on the left of the panels
|
|
150
|
-
|
|
151
|
-
Ordered from the edge of the figure and going inwards
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
plot_margin: float = 0
|
|
155
|
-
legend: float = 0
|
|
156
|
-
legend_box_spacing: float = 0
|
|
157
|
-
axis_title_y: float = 0
|
|
158
|
-
axis_title_y_margin_right: float = 0
|
|
159
|
-
axis_ylabels: float = 0
|
|
160
|
-
axis_yticks: float = 0
|
|
161
|
-
|
|
162
|
-
def _calculate(self):
|
|
163
|
-
theme = self.pack.theme
|
|
164
|
-
pack = self.pack
|
|
165
|
-
|
|
166
|
-
self.plot_margin = theme.getp("plot_margin_left")
|
|
167
|
-
if pack.legends and pack.legends.left:
|
|
168
|
-
self.legend = self._legend_width
|
|
169
|
-
self.legend_box_spacing = theme.getp("legend_box_spacing")
|
|
170
|
-
|
|
171
|
-
if pack.axis_title_y:
|
|
172
|
-
self.axis_title_y_margin_right = theme.getp(
|
|
173
|
-
("axis_title_y", "margin")
|
|
174
|
-
).get_as("r", "fig")
|
|
175
|
-
self.axis_title_y = bbox_in_figure_space(
|
|
176
|
-
pack.axis_title_y, pack.figure, pack.renderer
|
|
177
|
-
).width
|
|
178
|
-
|
|
179
|
-
# Account for the space consumed by the axis
|
|
180
|
-
self.axis_ylabels = max_ylabels_width(pack, "first_col")
|
|
181
|
-
self.axis_yticks = max_yticks_width(pack, "first_col")
|
|
182
|
-
|
|
183
|
-
# Adjust plot_margin to make room for ylabels that protude well
|
|
184
|
-
# beyond the axes
|
|
185
|
-
# NOTE: This adjustment breaks down when the protrusion is large
|
|
186
|
-
protrusion = max_xlabels_left_protrusion(pack)
|
|
187
|
-
adjustment = protrusion - (self.total - self.plot_margin)
|
|
188
|
-
if adjustment > 0:
|
|
189
|
-
self.plot_margin += adjustment
|
|
190
|
-
|
|
191
|
-
@cached_property
|
|
192
|
-
def _legend_size(self) -> tuple[float, float]:
|
|
193
|
-
if not (self.pack.legends and self.pack.legends.left):
|
|
194
|
-
return (0, 0)
|
|
195
|
-
|
|
196
|
-
bbox = bbox_in_figure_space(
|
|
197
|
-
self.pack.legends.left.box, self.pack.figure, self.pack.renderer
|
|
198
|
-
)
|
|
199
|
-
return bbox.width, bbox.height
|
|
200
|
-
|
|
201
|
-
def edge(self, item: str) -> float:
|
|
202
|
-
"""
|
|
203
|
-
Distance w.r.t figure width from the left edge of the figure
|
|
204
|
-
"""
|
|
205
|
-
return self.sum_upto(item)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@dataclass
|
|
209
|
-
class right_spaces(_side_spaces):
|
|
210
|
-
"""
|
|
211
|
-
Space in the figure for artists on the right of the panels
|
|
212
|
-
|
|
213
|
-
Ordered from the edge of the figure and going inwards
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
plot_margin: float = 0
|
|
217
|
-
legend: float = 0
|
|
218
|
-
legend_box_spacing: float = 0
|
|
219
|
-
right_strip_width: float = 0
|
|
220
|
-
|
|
221
|
-
def _calculate(self):
|
|
222
|
-
pack = self.pack
|
|
223
|
-
theme = self.pack.theme
|
|
224
|
-
|
|
225
|
-
self.plot_margin = theme.getp("plot_margin_right")
|
|
226
|
-
if pack.legends and pack.legends.right:
|
|
227
|
-
self.legend = self._legend_width
|
|
228
|
-
self.legend_box_spacing = theme.getp("legend_box_spacing")
|
|
229
|
-
|
|
230
|
-
self.right_strip_width = get_right_strip_width(pack)
|
|
231
|
-
|
|
232
|
-
# Adjust plot_margin to make room for ylabels that protude well
|
|
233
|
-
# beyond the axes
|
|
234
|
-
# NOTE: This adjustment breaks down when the protrusion is large
|
|
235
|
-
protrusion = max_xlabels_right_protrusion(pack)
|
|
236
|
-
adjustment = protrusion - (self.total - self.plot_margin)
|
|
237
|
-
if adjustment > 0:
|
|
238
|
-
self.plot_margin += adjustment
|
|
239
|
-
|
|
240
|
-
@cached_property
|
|
241
|
-
def _legend_size(self) -> tuple[float, float]:
|
|
242
|
-
if not (self.pack.legends and self.pack.legends.right):
|
|
243
|
-
return (0, 0)
|
|
244
|
-
|
|
245
|
-
bbox = bbox_in_figure_space(
|
|
246
|
-
self.pack.legends.right.box, self.pack.figure, self.pack.renderer
|
|
247
|
-
)
|
|
248
|
-
return bbox.width, bbox.height
|
|
249
|
-
|
|
250
|
-
def edge(self, item: str) -> float:
|
|
251
|
-
"""
|
|
252
|
-
Distance w.r.t figure width from the right edge of the figure
|
|
253
|
-
"""
|
|
254
|
-
return 1 - self.sum_upto(item)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
@dataclass
|
|
258
|
-
class top_spaces(_side_spaces):
|
|
259
|
-
"""
|
|
260
|
-
Space in the figure for artists above the panels
|
|
261
|
-
|
|
262
|
-
Ordered from the edge of the figure and going inwards
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
plot_margin: float = 0
|
|
266
|
-
plot_title: float = 0
|
|
267
|
-
plot_title_margin_bottom: float = 0
|
|
268
|
-
plot_subtitle: float = 0
|
|
269
|
-
plot_subtitle_margin_bottom: float = 0
|
|
270
|
-
legend: float = 0
|
|
271
|
-
legend_box_spacing: float = 0
|
|
272
|
-
top_strip_height: float = 0
|
|
273
|
-
|
|
274
|
-
def _calculate(self):
|
|
275
|
-
pack = self.pack
|
|
276
|
-
theme = self.pack.theme
|
|
277
|
-
W, H = theme.getp("figure_size")
|
|
278
|
-
F = W / H
|
|
279
|
-
|
|
280
|
-
self.plot_margin = theme.getp("plot_margin_top") * F
|
|
281
|
-
if pack.plot_title:
|
|
282
|
-
self.plot_title = bbox_in_figure_space(
|
|
283
|
-
pack.plot_title, pack.figure, pack.renderer
|
|
284
|
-
).height
|
|
285
|
-
self.plot_title_margin_bottom = (
|
|
286
|
-
theme.getp(("plot_title", "margin")).get_as("b", "fig") * F
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
if pack.plot_subtitle:
|
|
290
|
-
self.plot_subtitle = bbox_in_figure_space(
|
|
291
|
-
pack.plot_subtitle, pack.figure, pack.renderer
|
|
292
|
-
).height
|
|
293
|
-
self.plot_subtitle_margin_bottom = (
|
|
294
|
-
theme.getp(("plot_subtitle", "margin")).get_as("b", "fig") * F
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
if pack.legends and pack.legends.top:
|
|
298
|
-
self.legend = self._legend_height
|
|
299
|
-
self.legend_box_spacing = theme.getp("legend_box_spacing") * F
|
|
300
|
-
|
|
301
|
-
self.top_strip_height = get_top_strip_height(pack)
|
|
302
|
-
|
|
303
|
-
# Adjust plot_margin to make room for ylabels that protude well
|
|
304
|
-
# beyond the axes
|
|
305
|
-
# NOTE: This adjustment breaks down when the protrusion is large
|
|
306
|
-
protrusion = max_ylabels_top_protrusion(pack)
|
|
307
|
-
adjustment = protrusion - (self.total - self.plot_margin)
|
|
308
|
-
if adjustment > 0:
|
|
309
|
-
self.plot_margin += adjustment
|
|
310
|
-
|
|
311
|
-
@cached_property
|
|
312
|
-
def _legend_size(self) -> tuple[float, float]:
|
|
313
|
-
if not (self.pack.legends and self.pack.legends.top):
|
|
314
|
-
return (0, 0)
|
|
315
|
-
|
|
316
|
-
bbox = bbox_in_figure_space(
|
|
317
|
-
self.pack.legends.top.box, self.pack.figure, self.pack.renderer
|
|
318
|
-
)
|
|
319
|
-
return bbox.width, bbox.height
|
|
320
|
-
|
|
321
|
-
def edge(self, item: str) -> float:
|
|
322
|
-
"""
|
|
323
|
-
Distance w.r.t figure height from the top edge of the figure
|
|
324
|
-
"""
|
|
325
|
-
return 1 - self.sum_upto(item)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
@dataclass
|
|
329
|
-
class bottom_spaces(_side_spaces):
|
|
330
|
-
"""
|
|
331
|
-
Space in the figure for artists below the panels
|
|
332
|
-
|
|
333
|
-
Ordered from the edge of the figure and going inwards
|
|
334
|
-
"""
|
|
335
|
-
|
|
336
|
-
plot_margin: float = 0
|
|
337
|
-
plot_caption: float = 0
|
|
338
|
-
plot_caption_margin_top: float = 0
|
|
339
|
-
legend: float = 0
|
|
340
|
-
legend_box_spacing: float = 0
|
|
341
|
-
axis_title_x: float = 0
|
|
342
|
-
axis_title_x_margin_top: float = 0
|
|
343
|
-
axis_xlabels: float = 0
|
|
344
|
-
axis_xticks: float = 0
|
|
345
|
-
|
|
346
|
-
def _calculate(self):
|
|
347
|
-
pack = self.pack
|
|
348
|
-
theme = self.pack.theme
|
|
349
|
-
W, H = theme.getp("figure_size")
|
|
350
|
-
F = W / H
|
|
351
|
-
|
|
352
|
-
self.plot_margin = theme.getp("plot_margin_bottom") * F
|
|
353
|
-
|
|
354
|
-
if pack.plot_caption:
|
|
355
|
-
self.plot_caption = bbox_in_figure_space(
|
|
356
|
-
pack.plot_caption, pack.figure, pack.renderer
|
|
357
|
-
).height
|
|
358
|
-
self.plot_caption_margin_top = (
|
|
359
|
-
theme.getp(("plot_caption", "margin")).get_as("t", "fig") * F
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
if pack.legends and pack.legends.bottom:
|
|
363
|
-
self.legend = self._legend_height
|
|
364
|
-
self.legend_box_spacing = theme.getp("legend_box_spacing") * F
|
|
365
|
-
|
|
366
|
-
if pack.axis_title_x:
|
|
367
|
-
self.axis_title_x = bbox_in_figure_space(
|
|
368
|
-
pack.axis_title_x, pack.figure, pack.renderer
|
|
369
|
-
).height
|
|
370
|
-
self.axis_title_x_margin_top = (
|
|
371
|
-
theme.getp(("axis_title_x", "margin")).get_as("t", "fig") * F
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
# Account for the space consumed by the axis
|
|
375
|
-
self.axis_xticks = max_xticks_height(pack, "last_row")
|
|
376
|
-
self.axis_xlabels = max_xlabels_height(pack, "last_row")
|
|
377
|
-
|
|
378
|
-
# Adjust plot_margin to make room for ylabels that protude well
|
|
379
|
-
# beyond the axes
|
|
380
|
-
# NOTE: This adjustment breaks down when the protrusion is large
|
|
381
|
-
protrusion = max_ylabels_bottom_protrusion(pack)
|
|
382
|
-
adjustment = protrusion - (self.total - self.plot_margin)
|
|
383
|
-
if adjustment > 0:
|
|
384
|
-
self.plot_margin += adjustment
|
|
385
|
-
|
|
386
|
-
@cached_property
|
|
387
|
-
def _legend_size(self) -> tuple[float, float]:
|
|
388
|
-
if not (self.pack.legends and self.pack.legends.bottom):
|
|
389
|
-
return (0, 0)
|
|
390
|
-
|
|
391
|
-
bbox = bbox_in_figure_space(
|
|
392
|
-
self.pack.legends.bottom.box, self.pack.figure, self.pack.renderer
|
|
393
|
-
)
|
|
394
|
-
return bbox.width, bbox.height
|
|
395
|
-
|
|
396
|
-
def edge(self, item: str) -> float:
|
|
397
|
-
"""
|
|
398
|
-
Distance w.r.t figure height from the bottom edge of the figure
|
|
399
|
-
"""
|
|
400
|
-
return self.sum_upto(item)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
@dataclass
|
|
404
|
-
class LRTBSpaces:
|
|
405
|
-
"""
|
|
406
|
-
Space for components in all directions around the panels
|
|
407
|
-
"""
|
|
408
|
-
|
|
409
|
-
pack: LayoutPack
|
|
410
|
-
|
|
411
|
-
def __post_init__(self):
|
|
412
|
-
self.l = left_spaces(self.pack)
|
|
413
|
-
self.r = right_spaces(self.pack)
|
|
414
|
-
self.t = top_spaces(self.pack)
|
|
415
|
-
self.b = bottom_spaces(self.pack)
|
|
416
|
-
|
|
417
|
-
@property
|
|
418
|
-
def left(self):
|
|
419
|
-
"""
|
|
420
|
-
Left of the panels in figure space
|
|
421
|
-
"""
|
|
422
|
-
return self.l.total
|
|
423
|
-
|
|
424
|
-
@property
|
|
425
|
-
def right(self):
|
|
426
|
-
"""
|
|
427
|
-
Right of the panels in figure space
|
|
428
|
-
"""
|
|
429
|
-
return 1 - self.r.total
|
|
430
|
-
|
|
431
|
-
@property
|
|
432
|
-
def top(self):
|
|
433
|
-
"""
|
|
434
|
-
Top of the panels in figure space
|
|
435
|
-
"""
|
|
436
|
-
return 1 - self.t.total
|
|
437
|
-
|
|
438
|
-
@property
|
|
439
|
-
def bottom(self):
|
|
440
|
-
"""
|
|
441
|
-
Bottom of the panels in figure space
|
|
442
|
-
"""
|
|
443
|
-
return self.b.total
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def calculate_panel_spacing(
|
|
447
|
-
pack: LayoutPack, spaces: LRTBSpaces
|
|
448
|
-
) -> WHSpaceParts:
|
|
449
|
-
"""
|
|
450
|
-
Spacing between the panels (wspace & hspace)
|
|
451
|
-
|
|
452
|
-
Both spaces are calculated from a fraction of the width.
|
|
453
|
-
This ensures that the same fraction gives equals space
|
|
454
|
-
in both directions.
|
|
455
|
-
"""
|
|
456
|
-
if isinstance(pack.facet, facet_wrap):
|
|
457
|
-
return _calculate_panel_spacing_facet_wrap(pack, spaces)
|
|
458
|
-
elif isinstance(pack.facet, facet_grid):
|
|
459
|
-
return _calculate_panel_spacing_facet_grid(pack, spaces)
|
|
460
|
-
elif isinstance(pack.facet, facet_null):
|
|
461
|
-
return _calculate_panel_spacing_facet_null(pack, spaces)
|
|
462
|
-
return WHSpaceParts(0, 0, 0, 0, 0, 0, 0, 0)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def _calculate_panel_spacing_facet_grid(
|
|
466
|
-
pack: LayoutPack, spaces: LRTBSpaces
|
|
467
|
-
) -> WHSpaceParts:
|
|
468
|
-
"""
|
|
469
|
-
Calculate spacing parts for facet_grid
|
|
470
|
-
"""
|
|
471
|
-
pack.facet = cast(facet_grid, pack.facet)
|
|
472
|
-
theme = pack.theme
|
|
473
|
-
|
|
474
|
-
ncol = pack.facet.ncol
|
|
475
|
-
nrow = pack.facet.nrow
|
|
476
|
-
|
|
477
|
-
W, H = theme.getp("figure_size")
|
|
478
|
-
|
|
479
|
-
# Both spacings are specified as fractions of the figure width
|
|
480
|
-
# Multiply the vertical by (W/H) so that the gullies along both
|
|
481
|
-
# directions are equally spaced.
|
|
482
|
-
sw = theme.getp("panel_spacing_x")
|
|
483
|
-
sh = theme.getp("panel_spacing_y") * W / H
|
|
484
|
-
|
|
485
|
-
# width and height of axes as fraction of figure width & height
|
|
486
|
-
w = ((spaces.right - spaces.left) - sw * (ncol - 1)) / ncol
|
|
487
|
-
h = ((spaces.top - spaces.bottom) - sh * (nrow - 1)) / nrow
|
|
488
|
-
|
|
489
|
-
# Spacing as fraction of axes width & height
|
|
490
|
-
wspace = sw / w
|
|
491
|
-
hspace = sh / h
|
|
492
|
-
|
|
493
|
-
return WHSpaceParts(W, H, w, h, sw, sh, wspace, hspace)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def _calculate_panel_spacing_facet_wrap(
|
|
497
|
-
pack: LayoutPack, spaces: LRTBSpaces
|
|
498
|
-
) -> WHSpaceParts:
|
|
499
|
-
"""
|
|
500
|
-
Calculate spacing parts for facet_wrap
|
|
501
|
-
"""
|
|
502
|
-
pack.facet = cast(facet_wrap, pack.facet)
|
|
503
|
-
theme = pack.theme
|
|
504
|
-
|
|
505
|
-
ncol = pack.facet.ncol
|
|
506
|
-
nrow = pack.facet.nrow
|
|
507
|
-
|
|
508
|
-
W, H = theme.getp("figure_size")
|
|
509
|
-
|
|
510
|
-
# Both spacings are specified as fractions of the figure width
|
|
511
|
-
sw = theme.getp("panel_spacing_x")
|
|
512
|
-
sh = theme.getp("panel_spacing_y") * W / H
|
|
513
|
-
|
|
514
|
-
# A fraction of the strip height
|
|
515
|
-
# Effectively slides the strip
|
|
516
|
-
# +ve: Away from the panel
|
|
517
|
-
# 0: Top of the panel
|
|
518
|
-
# -ve: Into the panel
|
|
519
|
-
# Where values <= -1, put the strip completely into
|
|
520
|
-
# the panel. We do not worry about larger -ves.
|
|
521
|
-
strip_align_x = theme.getp("strip_align_x")
|
|
522
|
-
|
|
523
|
-
# Only interested in the proportion of the strip that
|
|
524
|
-
# does not overlap with the panel
|
|
525
|
-
if strip_align_x > -1:
|
|
526
|
-
sh += spaces.t.top_strip_height * (1 + strip_align_x)
|
|
527
|
-
|
|
528
|
-
if pack.facet.free["x"]:
|
|
529
|
-
sh += max_xlabels_height(pack)
|
|
530
|
-
sh += max_xticks_height(pack)
|
|
531
|
-
if pack.facet.free["y"]:
|
|
532
|
-
sw += max_ylabels_width(pack)
|
|
533
|
-
sw += max_yticks_width(pack)
|
|
534
|
-
|
|
535
|
-
# width and height of axes as fraction of figure width & height
|
|
536
|
-
w = ((spaces.right - spaces.left) - sw * (ncol - 1)) / ncol
|
|
537
|
-
h = ((spaces.top - spaces.bottom) - sh * (nrow - 1)) / nrow
|
|
538
|
-
|
|
539
|
-
# Spacing as fraction of axes width & height
|
|
540
|
-
wspace = sw / w
|
|
541
|
-
hspace = sh / h
|
|
542
|
-
|
|
543
|
-
return WHSpaceParts(W, H, w, h, sw, sh, wspace, hspace)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
def _calculate_panel_spacing_facet_null(
|
|
547
|
-
pack: LayoutPack, spaces: LRTBSpaces
|
|
548
|
-
) -> WHSpaceParts:
|
|
549
|
-
"""
|
|
550
|
-
Calculate spacing parts for facet_null
|
|
551
|
-
"""
|
|
552
|
-
W, H = pack.theme.getp("figure_size")
|
|
553
|
-
w = spaces.right - spaces.left
|
|
554
|
-
h = spaces.top - spaces.bottom
|
|
555
|
-
return WHSpaceParts(W, H, w, h, 0, 0, 0, 0)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
def filter_axes(axs: list[Axes], get: AxesLocation = "all") -> list[Axes]:
|
|
559
|
-
"""
|
|
560
|
-
Return subset of axes
|
|
561
|
-
"""
|
|
562
|
-
if get == "all":
|
|
563
|
-
return axs
|
|
564
|
-
|
|
565
|
-
# e.g. is_first_row, is_last_row, ..
|
|
566
|
-
pred_method = f"is_{get}"
|
|
567
|
-
return [
|
|
568
|
-
ax
|
|
569
|
-
for spec, ax in zip(get_subplotspec_list(axs), axs)
|
|
570
|
-
if getattr(spec, pred_method)()
|
|
571
|
-
]
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
def max_width(pack: LayoutPack, artists: Sequence[Artist]) -> float:
|
|
575
|
-
"""
|
|
576
|
-
Return the maximum width of list of artists
|
|
577
|
-
"""
|
|
578
|
-
widths = [
|
|
579
|
-
bbox_in_figure_space(a, pack.figure, pack.renderer).width
|
|
580
|
-
for a in artists
|
|
581
|
-
]
|
|
582
|
-
return max(widths) if len(widths) else 0
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def max_height(pack: LayoutPack, artists: Sequence[Artist]) -> float:
|
|
586
|
-
"""
|
|
587
|
-
Return the maximum height of list of artists
|
|
588
|
-
"""
|
|
589
|
-
heights = [
|
|
590
|
-
bbox_in_figure_space(a, pack.figure, pack.renderer).height
|
|
591
|
-
for a in artists
|
|
592
|
-
]
|
|
593
|
-
return max(heights) if len(heights) else 0
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
def get_top_strip_height(pack: LayoutPack) -> float:
|
|
597
|
-
"""
|
|
598
|
-
Height taken up by the top strips
|
|
599
|
-
"""
|
|
600
|
-
if not pack.strip_text_x:
|
|
601
|
-
return 0
|
|
602
|
-
|
|
603
|
-
artists = [
|
|
604
|
-
st.patch if st.patch.get_visible() else st
|
|
605
|
-
for st in pack.strip_text_x
|
|
606
|
-
if st.patch.position == "top"
|
|
607
|
-
]
|
|
608
|
-
return max_height(pack, artists)
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
def get_right_strip_width(pack: LayoutPack) -> float:
|
|
612
|
-
"""
|
|
613
|
-
Width taken up by the right strips
|
|
614
|
-
"""
|
|
615
|
-
if not pack.strip_text_y:
|
|
616
|
-
return 0
|
|
617
|
-
|
|
618
|
-
artists = [
|
|
619
|
-
st.patch if st.patch.get_visible() else st
|
|
620
|
-
for st in pack.strip_text_y
|
|
621
|
-
if st.patch.position == "right"
|
|
622
|
-
]
|
|
623
|
-
return max_width(pack, artists)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def get_xaxis_ticks(pack: LayoutPack, ax: Axes) -> Iterator[Tick]:
|
|
627
|
-
"""
|
|
628
|
-
Return all XTicks that will be shown
|
|
629
|
-
"""
|
|
630
|
-
is_blank = pack.theme.T.is_blank
|
|
631
|
-
major, minor = [], []
|
|
632
|
-
|
|
633
|
-
if not is_blank("axis_ticks_major_x"):
|
|
634
|
-
major = ax.xaxis.get_major_ticks()
|
|
635
|
-
|
|
636
|
-
if not is_blank("axis_ticks_minor_x"):
|
|
637
|
-
minor = ax.xaxis.get_minor_ticks()
|
|
638
|
-
|
|
639
|
-
return chain(major, minor)
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
def get_yaxis_ticks(pack: LayoutPack, ax: Axes) -> Iterator[Tick]:
|
|
643
|
-
"""
|
|
644
|
-
Return all YTicks that will be shown
|
|
645
|
-
"""
|
|
646
|
-
is_blank = pack.theme.T.is_blank
|
|
647
|
-
major, minor = [], []
|
|
648
|
-
|
|
649
|
-
if not is_blank("axis_ticks_major_y"):
|
|
650
|
-
major = ax.yaxis.get_major_ticks()
|
|
651
|
-
|
|
652
|
-
if not is_blank("axis_ticks_minor_y"):
|
|
653
|
-
minor = ax.yaxis.get_minor_ticks()
|
|
654
|
-
|
|
655
|
-
return chain(major, minor)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
def get_xaxis_tick_pads(pack: LayoutPack, ax: Axes) -> Iterator[float]:
|
|
659
|
-
"""
|
|
660
|
-
Return XTicks paddings
|
|
661
|
-
"""
|
|
662
|
-
# In plotnine tick padding are specified as a margin to the
|
|
663
|
-
# the axis_text.
|
|
664
|
-
is_blank = pack.theme.T.is_blank
|
|
665
|
-
major, minor = [], []
|
|
666
|
-
if not is_blank("axis_text_y"):
|
|
667
|
-
h = pack.figure.get_figheight() * 72
|
|
668
|
-
major = [(t.get_pad() or 0) / h for t in ax.xaxis.get_major_ticks()]
|
|
669
|
-
minor = [(t.get_pad() or 0) / h for t in ax.xaxis.get_minor_ticks()]
|
|
670
|
-
return chain(major, minor)
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
def get_yaxis_tick_pads(pack: LayoutPack, ax: Axes) -> Iterator[float]:
|
|
674
|
-
"""
|
|
675
|
-
Return YTicks paddings
|
|
676
|
-
"""
|
|
677
|
-
# In plotnine tick padding are specified as a margin to the
|
|
678
|
-
# the axis_text.
|
|
679
|
-
is_blank = pack.theme.T.is_blank
|
|
680
|
-
major, minor = [], []
|
|
681
|
-
if not is_blank("axis_text_y"):
|
|
682
|
-
w = pack.figure.get_figwidth() * 72
|
|
683
|
-
major = [(t.get_pad() or 0) / w for t in ax.yaxis.get_major_ticks()]
|
|
684
|
-
minor = [(t.get_pad() or 0) / w for t in ax.yaxis.get_minor_ticks()]
|
|
685
|
-
return chain(major, minor)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
def _text_is_visible(text: Text) -> bool:
|
|
689
|
-
"""
|
|
690
|
-
Return True if text is visible and is not empty
|
|
691
|
-
"""
|
|
692
|
-
return text.get_visible() and text._text # type: ignore
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
def get_xaxis_labels(pack: LayoutPack, ax: Axes) -> Iterator[Text]:
|
|
696
|
-
"""
|
|
697
|
-
Return all x-axis labels that will be shown
|
|
698
|
-
"""
|
|
699
|
-
is_blank = pack.theme.T.is_blank
|
|
700
|
-
major, minor = [], []
|
|
701
|
-
|
|
702
|
-
if not is_blank("axis_text_x"):
|
|
703
|
-
major = ax.xaxis.get_major_ticks()
|
|
704
|
-
minor = ax.xaxis.get_minor_ticks()
|
|
705
|
-
|
|
706
|
-
return (
|
|
707
|
-
tick.label1
|
|
708
|
-
for tick in chain(major, minor)
|
|
709
|
-
if _text_is_visible(tick.label1)
|
|
710
|
-
)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
def get_yaxis_labels(pack: LayoutPack, ax: Axes) -> Iterator[Text]:
|
|
714
|
-
"""
|
|
715
|
-
Return all y-axis labels that will be shown
|
|
716
|
-
"""
|
|
717
|
-
is_blank = pack.theme.T.is_blank
|
|
718
|
-
major, minor = [], []
|
|
719
|
-
|
|
720
|
-
if not is_blank("axis_text_y"):
|
|
721
|
-
major = ax.yaxis.get_major_ticks()
|
|
722
|
-
minor = ax.yaxis.get_minor_ticks()
|
|
723
|
-
|
|
724
|
-
return (
|
|
725
|
-
tick.label1
|
|
726
|
-
for tick in chain(major, minor)
|
|
727
|
-
if _text_is_visible(tick.label1)
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
def max_xticks_height(
|
|
732
|
-
pack: LayoutPack,
|
|
733
|
-
axes_loc: AxesLocation = "all",
|
|
734
|
-
) -> float:
|
|
735
|
-
"""
|
|
736
|
-
Return maximum height[inches] of x ticks
|
|
737
|
-
"""
|
|
738
|
-
heights = [
|
|
739
|
-
tight_bbox_in_figure_space(
|
|
740
|
-
tick.tick1line, pack.figure, pack.renderer
|
|
741
|
-
).height
|
|
742
|
-
for ax in filter_axes(pack.axs, axes_loc)
|
|
743
|
-
for tick in get_xaxis_ticks(pack, ax)
|
|
744
|
-
]
|
|
745
|
-
return max(heights) if len(heights) else 0
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
def max_xlabels_height(
|
|
749
|
-
pack: LayoutPack,
|
|
750
|
-
axes_loc: AxesLocation = "all",
|
|
751
|
-
) -> float:
|
|
752
|
-
"""
|
|
753
|
-
Return maximum height[inches] of x tick labels
|
|
754
|
-
"""
|
|
755
|
-
heights = [
|
|
756
|
-
tight_bbox_in_figure_space(label, pack.figure, pack.renderer).height
|
|
757
|
-
+ pad
|
|
758
|
-
for ax in filter_axes(pack.axs, axes_loc)
|
|
759
|
-
for label, pad in zip(
|
|
760
|
-
get_xaxis_labels(pack, ax), get_xaxis_tick_pads(pack, ax)
|
|
761
|
-
)
|
|
762
|
-
]
|
|
763
|
-
return max(heights) if len(heights) else 0
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
def max_yticks_width(
|
|
767
|
-
pack: LayoutPack,
|
|
768
|
-
axes_loc: AxesLocation = "all",
|
|
769
|
-
) -> float:
|
|
770
|
-
"""
|
|
771
|
-
Return maximum width[inches] of y ticks
|
|
772
|
-
"""
|
|
773
|
-
widths = [
|
|
774
|
-
tight_bbox_in_figure_space(
|
|
775
|
-
tick.tick1line, pack.figure, pack.renderer
|
|
776
|
-
).width
|
|
777
|
-
for ax in filter_axes(pack.axs, axes_loc)
|
|
778
|
-
for tick in get_yaxis_ticks(pack, ax)
|
|
779
|
-
]
|
|
780
|
-
return max(widths) if len(widths) else 0
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
def max_ylabels_width(
|
|
784
|
-
pack: LayoutPack,
|
|
785
|
-
axes_loc: AxesLocation = "all",
|
|
786
|
-
) -> float:
|
|
787
|
-
"""
|
|
788
|
-
Return maximum width[inches] of y tick labels
|
|
789
|
-
"""
|
|
790
|
-
widths = [
|
|
791
|
-
tight_bbox_in_figure_space(label, pack.figure, pack.renderer).width
|
|
792
|
-
+ pad
|
|
793
|
-
for ax in filter_axes(pack.axs, axes_loc)
|
|
794
|
-
for label, pad in zip(
|
|
795
|
-
get_yaxis_labels(pack, ax), get_yaxis_tick_pads(pack, ax)
|
|
796
|
-
)
|
|
797
|
-
]
|
|
798
|
-
return max(widths) if len(widths) else 0
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
def max_ylabels_top_protrusion(
|
|
802
|
-
pack: LayoutPack,
|
|
803
|
-
axes_loc: AxesLocation = "all",
|
|
804
|
-
) -> float:
|
|
805
|
-
"""
|
|
806
|
-
Return maximum height[inches] above the axes of y tick labels
|
|
807
|
-
"""
|
|
808
|
-
|
|
809
|
-
def get_artist_top_y(a: Artist) -> float:
|
|
810
|
-
xy = bbox_in_figure_space(a, pack.figure, pack.renderer).max
|
|
811
|
-
return xy[1]
|
|
812
|
-
|
|
813
|
-
extras = []
|
|
814
|
-
for ax in filter_axes(pack.axs, axes_loc):
|
|
815
|
-
ax_top = get_artist_top_y(ax)
|
|
816
|
-
for label in get_yaxis_labels(pack, ax):
|
|
817
|
-
label_top = get_artist_top_y(label)
|
|
818
|
-
extras.append(max(0, label_top - ax_top))
|
|
819
|
-
|
|
820
|
-
return max(extras) if len(extras) else 0
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
def max_ylabels_bottom_protrusion(
|
|
824
|
-
pack: LayoutPack,
|
|
825
|
-
axes_loc: AxesLocation = "all",
|
|
826
|
-
) -> float:
|
|
827
|
-
"""
|
|
828
|
-
Return maximum height[inches] below the axes of y tick labels
|
|
829
|
-
"""
|
|
830
|
-
|
|
831
|
-
def get_artist_bottom_y(a: Artist) -> float:
|
|
832
|
-
xy = bbox_in_figure_space(a, pack.figure, pack.renderer).min
|
|
833
|
-
return xy[1]
|
|
834
|
-
|
|
835
|
-
extras = []
|
|
836
|
-
for ax in filter_axes(pack.axs, axes_loc):
|
|
837
|
-
ax_bottom = get_artist_bottom_y(ax)
|
|
838
|
-
for label in get_yaxis_labels(pack, ax):
|
|
839
|
-
label_bottom = get_artist_bottom_y(label)
|
|
840
|
-
protrusion = abs(min(label_bottom - ax_bottom, 0))
|
|
841
|
-
extras.append(protrusion)
|
|
842
|
-
|
|
843
|
-
return max(extras) if len(extras) else 0
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
def max_xlabels_left_protrusion(
|
|
847
|
-
pack: LayoutPack,
|
|
848
|
-
axes_loc: AxesLocation = "all",
|
|
849
|
-
) -> float:
|
|
850
|
-
"""
|
|
851
|
-
Return maximum width[inches] of x tick labels to the left of the axes
|
|
852
|
-
"""
|
|
853
|
-
|
|
854
|
-
def get_artist_left_x(a: Artist) -> float:
|
|
855
|
-
xy = bbox_in_figure_space(a, pack.figure, pack.renderer).min
|
|
856
|
-
return xy[0]
|
|
857
|
-
|
|
858
|
-
extras = []
|
|
859
|
-
for ax in filter_axes(pack.axs, axes_loc):
|
|
860
|
-
ax_left = get_artist_left_x(ax)
|
|
861
|
-
for label in get_xaxis_labels(pack, ax):
|
|
862
|
-
label_left = get_artist_left_x(label)
|
|
863
|
-
protrusion = abs(min(label_left - ax_left, 0))
|
|
864
|
-
extras.append(protrusion)
|
|
865
|
-
|
|
866
|
-
return max(extras) if len(extras) else 0
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
def max_xlabels_right_protrusion(
|
|
870
|
-
pack: LayoutPack,
|
|
871
|
-
axes_loc: AxesLocation = "all",
|
|
872
|
-
) -> float:
|
|
873
|
-
"""
|
|
874
|
-
Return maximum width[inches] of x tick labels to the right of the axes
|
|
875
|
-
"""
|
|
876
|
-
|
|
877
|
-
def get_artist_right_x(a: Artist) -> float:
|
|
878
|
-
xy = bbox_in_figure_space(a, pack.figure, pack.renderer).max
|
|
879
|
-
return xy[0]
|
|
880
|
-
|
|
881
|
-
extras = []
|
|
882
|
-
for ax in filter_axes(pack.axs, axes_loc):
|
|
883
|
-
ax_right = get_artist_right_x(ax)
|
|
884
|
-
for label in get_xaxis_labels(pack, ax):
|
|
885
|
-
label_right = get_artist_right_x(label)
|
|
886
|
-
extras.append(max(0, label_right - ax_right))
|
|
887
|
-
|
|
888
|
-
return max(extras) if len(extras) else 0
|