plotnine 0.15.2__py3-none-any.whl → 0.16.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1 -1
- plotnine/_utils/dataclasses.py +24 -0
- 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 +6 -1
- {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/METADATA +2 -2
- {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/RECORD +59 -51
- plotnine/composition/_plotspec.py +0 -50
- {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/WHEEL +0 -0
- {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
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
|
|
16
|
+
from functools import cached_property
|
|
17
|
+
from typing import TYPE_CHECKING, cast
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
21
|
+
from plotnine.typing import Side
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Note
|
|
25
|
+
# Margins around the plot are specified in figure coordinates
|
|
26
|
+
# We interpret that value to be a fraction of the width. So along
|
|
27
|
+
# the vertical direction we multiply by W/H to get equal space
|
|
28
|
+
# in both directions
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GridSpecParamsError(Exception):
|
|
32
|
+
"""
|
|
33
|
+
Error thrown when there isn't enough space for some panels
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class GridSpecParams:
|
|
39
|
+
"""
|
|
40
|
+
Gridspec Parameters
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
left: float
|
|
44
|
+
right: float
|
|
45
|
+
top: float
|
|
46
|
+
bottom: float
|
|
47
|
+
wspace: float
|
|
48
|
+
hspace: float
|
|
49
|
+
|
|
50
|
+
def validate(self):
|
|
51
|
+
"""
|
|
52
|
+
Return True if the params will create a non-empty area
|
|
53
|
+
"""
|
|
54
|
+
if not (self.top - self.bottom > 0 and self.right - self.left > 0):
|
|
55
|
+
raise GridSpecParamsError(
|
|
56
|
+
"The parameters of the gridspec do not create a regular "
|
|
57
|
+
"rectangle."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _side_space(ABC):
|
|
62
|
+
"""
|
|
63
|
+
Base class to for spaces
|
|
64
|
+
|
|
65
|
+
A *_space class does the book keeping for all the artists that may
|
|
66
|
+
fall on that side of the panels. The same name may appear in multiple
|
|
67
|
+
side classes (e.g. legend).
|
|
68
|
+
|
|
69
|
+
The amount of space for each artist is computed in figure coordinates.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
gridspec: p9GridSpec
|
|
73
|
+
"""
|
|
74
|
+
The gridspec (1x1) of the plot or composition
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def _calculate(self):
|
|
78
|
+
"""
|
|
79
|
+
Calculate the space taken up by each artist
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@cached_property
|
|
83
|
+
def side(self) -> Side:
|
|
84
|
+
"""
|
|
85
|
+
Side of the panel(s) that this class applies to
|
|
86
|
+
"""
|
|
87
|
+
return cast("Side", self.__class__.__name__.split("_")[0])
|
|
88
|
+
|
|
89
|
+
@cached_property
|
|
90
|
+
def parts(self) -> list[str]:
|
|
91
|
+
"""
|
|
92
|
+
The names of the part of the spaces
|
|
93
|
+
"""
|
|
94
|
+
return [
|
|
95
|
+
name
|
|
96
|
+
for name, value in self.__class__.__dict__.items()
|
|
97
|
+
if not (
|
|
98
|
+
name.startswith("_")
|
|
99
|
+
or callable(value)
|
|
100
|
+
or isinstance(value, property)
|
|
101
|
+
)
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def total(self) -> float:
|
|
106
|
+
"""
|
|
107
|
+
Total space
|
|
108
|
+
"""
|
|
109
|
+
return sum(getattr(self, name) for name in self.parts)
|
|
110
|
+
|
|
111
|
+
def sum_upto(self, item: str) -> float:
|
|
112
|
+
"""
|
|
113
|
+
Sum of space upto but not including item
|
|
114
|
+
|
|
115
|
+
Sums from the edge of the figure i.e. the "plot_margin".
|
|
116
|
+
"""
|
|
117
|
+
stop = self.parts.index(item)
|
|
118
|
+
return sum(getattr(self, name) for name in self.parts[:stop])
|
|
119
|
+
|
|
120
|
+
def sum_incl(self, item: str) -> float:
|
|
121
|
+
"""
|
|
122
|
+
Sum of space upto and including the item
|
|
123
|
+
|
|
124
|
+
Sums from the edge of the figure i.e. the "plot_margin".
|
|
125
|
+
"""
|
|
126
|
+
stop = self.parts.index(item) + 1
|
|
127
|
+
return sum(getattr(self, name) for name in self.parts[:stop])
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def offset(self) -> float:
|
|
131
|
+
"""
|
|
132
|
+
Distance in figure dimensions from the edge of the figure
|
|
133
|
+
|
|
134
|
+
Derived classes should override this method
|
|
135
|
+
|
|
136
|
+
The space/margin and size consumed by artists is in figure dimensions
|
|
137
|
+
but the exact position is relative to the position of the GridSpec
|
|
138
|
+
within the figure. The offset accounts for the position of the
|
|
139
|
+
GridSpec and allows us to accurately place artists using figure
|
|
140
|
+
coordinates.
|
|
141
|
+
|
|
142
|
+
Example of an offset
|
|
143
|
+
|
|
144
|
+
Figure
|
|
145
|
+
----------------------------------------
|
|
146
|
+
| |
|
|
147
|
+
| Plot GridSpec |
|
|
148
|
+
| -------------------------- |
|
|
149
|
+
| offset | | |
|
|
150
|
+
|<------->| X | |
|
|
151
|
+
| | Panels GridSpec | |
|
|
152
|
+
| | -------------------- | |
|
|
153
|
+
| | | | | |
|
|
154
|
+
| | | | | |
|
|
155
|
+
| | | | | |
|
|
156
|
+
| | | | | |
|
|
157
|
+
| | -------------------- | |
|
|
158
|
+
| | | |
|
|
159
|
+
| -------------------------- |
|
|
160
|
+
| |
|
|
161
|
+
----------------------------------------
|
|
162
|
+
"""
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
def to_figure_space(self, rel_value: float) -> float:
|
|
166
|
+
"""
|
|
167
|
+
Convert value relative to the gridspec to one in figure space
|
|
168
|
+
|
|
169
|
+
The result is meant to be used with transFigure transforms.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
rel_value :
|
|
174
|
+
Position relative to the position of the gridspec
|
|
175
|
+
"""
|
|
176
|
+
return self.offset + rel_value
|
plotnine/_mpl/utils.py
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, cast
|
|
4
5
|
|
|
5
6
|
from matplotlib.transforms import Affine2D, Bbox
|
|
6
7
|
|
|
8
|
+
from plotnine._utils import ha_as_float, va_as_float
|
|
9
|
+
|
|
7
10
|
from .transforms import ZEROS_BBOX
|
|
8
11
|
|
|
9
12
|
if TYPE_CHECKING:
|
|
13
|
+
from typing import Literal, Sequence
|
|
14
|
+
|
|
10
15
|
from matplotlib.artist import Artist
|
|
11
16
|
from matplotlib.axes import Axes
|
|
12
17
|
from matplotlib.backend_bases import RendererBase
|
|
13
18
|
from matplotlib.figure import Figure
|
|
14
19
|
from matplotlib.gridspec import SubplotSpec
|
|
20
|
+
from matplotlib.text import Text
|
|
15
21
|
from matplotlib.transforms import Transform
|
|
16
22
|
|
|
23
|
+
from plotnine.typing import HorizontalJustification, VerticalJustification
|
|
24
|
+
|
|
17
25
|
from .gridspec import p9GridSpec
|
|
18
26
|
|
|
19
27
|
|
|
@@ -144,3 +152,253 @@ def draw_bbox(bbox, figure, color="black", **kwargs):
|
|
|
144
152
|
**kwargs,
|
|
145
153
|
)
|
|
146
154
|
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class ArtistGeometry:
|
|
159
|
+
"""
|
|
160
|
+
Helper to calculate the position & extents (space) of an artist
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
figure: Figure
|
|
164
|
+
|
|
165
|
+
def __post_init__(self):
|
|
166
|
+
self.renderer = cast("RendererBase", self.figure._get_renderer()) # pyright: ignore
|
|
167
|
+
|
|
168
|
+
def bbox(self, artist: Artist) -> Bbox:
|
|
169
|
+
"""
|
|
170
|
+
Bounding box of artist in figure coordinates
|
|
171
|
+
"""
|
|
172
|
+
return bbox_in_figure_space(artist, self.figure, self.renderer)
|
|
173
|
+
|
|
174
|
+
def tight_bbox(self, artist: Artist) -> Bbox:
|
|
175
|
+
"""
|
|
176
|
+
Bounding box of artist and its children in figure coordinates
|
|
177
|
+
"""
|
|
178
|
+
return tight_bbox_in_figure_space(artist, self.figure, self.renderer)
|
|
179
|
+
|
|
180
|
+
def width(self, artist: Artist) -> float:
|
|
181
|
+
"""
|
|
182
|
+
Width of artist in figure space
|
|
183
|
+
"""
|
|
184
|
+
return self.bbox(artist).width
|
|
185
|
+
|
|
186
|
+
def tight_width(self, artist: Artist) -> float:
|
|
187
|
+
"""
|
|
188
|
+
Width of artist and its children in figure space
|
|
189
|
+
"""
|
|
190
|
+
return self.tight_bbox(artist).width
|
|
191
|
+
|
|
192
|
+
def height(self, artist: Artist) -> float:
|
|
193
|
+
"""
|
|
194
|
+
Height of artist in figure space
|
|
195
|
+
"""
|
|
196
|
+
return self.bbox(artist).height
|
|
197
|
+
|
|
198
|
+
def tight_height(self, artist: Artist) -> float:
|
|
199
|
+
"""
|
|
200
|
+
Height of artist and its children in figure space
|
|
201
|
+
"""
|
|
202
|
+
return self.tight_bbox(artist).height
|
|
203
|
+
|
|
204
|
+
def size(self, artist: Artist) -> tuple[float, float]:
|
|
205
|
+
"""
|
|
206
|
+
(width, height) of artist in figure space
|
|
207
|
+
"""
|
|
208
|
+
bbox = self.bbox(artist)
|
|
209
|
+
return (bbox.width, bbox.height)
|
|
210
|
+
|
|
211
|
+
def tight_size(self, artist: Artist) -> tuple[float, float]:
|
|
212
|
+
"""
|
|
213
|
+
(width, height) of artist and its children in figure space
|
|
214
|
+
"""
|
|
215
|
+
bbox = self.tight_bbox(artist)
|
|
216
|
+
return (bbox.width, bbox.height)
|
|
217
|
+
|
|
218
|
+
def left_x(self, artist: Artist) -> float:
|
|
219
|
+
"""
|
|
220
|
+
x value of the left edge of the artist
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
x |
|
|
224
|
+
---
|
|
225
|
+
"""
|
|
226
|
+
return self.bbox(artist).min[0]
|
|
227
|
+
|
|
228
|
+
def right_x(self, artist: Artist) -> float:
|
|
229
|
+
"""
|
|
230
|
+
x value of the left edge of the artist
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
| x
|
|
234
|
+
---
|
|
235
|
+
"""
|
|
236
|
+
return self.bbox(artist).max[0]
|
|
237
|
+
|
|
238
|
+
def top_y(self, artist: Artist) -> float:
|
|
239
|
+
"""
|
|
240
|
+
y value of the top edge of the artist
|
|
241
|
+
|
|
242
|
+
-y-
|
|
243
|
+
| |
|
|
244
|
+
---
|
|
245
|
+
"""
|
|
246
|
+
return self.bbox(artist).max[1]
|
|
247
|
+
|
|
248
|
+
def bottom_y(self, artist: Artist) -> float:
|
|
249
|
+
"""
|
|
250
|
+
y value of the bottom edge of the artist
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
| |
|
|
254
|
+
-y-
|
|
255
|
+
"""
|
|
256
|
+
return self.bbox(artist).min[1]
|
|
257
|
+
|
|
258
|
+
def max_width(self, artists: Sequence[Artist]) -> float:
|
|
259
|
+
"""
|
|
260
|
+
Return the maximum width of list of artists
|
|
261
|
+
"""
|
|
262
|
+
widths = [
|
|
263
|
+
bbox_in_figure_space(a, self.figure, self.renderer).width
|
|
264
|
+
for a in artists
|
|
265
|
+
]
|
|
266
|
+
return max(widths) if len(widths) else 0
|
|
267
|
+
|
|
268
|
+
def max_height(self, artists: Sequence[Artist]) -> float:
|
|
269
|
+
"""
|
|
270
|
+
Return the maximum height of list of artists
|
|
271
|
+
"""
|
|
272
|
+
heights = [
|
|
273
|
+
bbox_in_figure_space(a, self.figure, self.renderer).height
|
|
274
|
+
for a in artists
|
|
275
|
+
]
|
|
276
|
+
return max(heights) if len(heights) else 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass
|
|
280
|
+
class JustifyBoundaries:
|
|
281
|
+
"""
|
|
282
|
+
Limits about which text can be justified
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
plot_left: float
|
|
286
|
+
plot_right: float
|
|
287
|
+
plot_bottom: float
|
|
288
|
+
plot_top: float
|
|
289
|
+
panel_left: float
|
|
290
|
+
panel_right: float
|
|
291
|
+
panel_bottom: float
|
|
292
|
+
panel_top: float
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class TextJustifier:
|
|
296
|
+
"""
|
|
297
|
+
Justify Text
|
|
298
|
+
|
|
299
|
+
The justification methods reinterpret alignment values to be justification
|
|
300
|
+
about a span.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def __init__(self, figure: Figure, boundaries: JustifyBoundaries):
|
|
304
|
+
self.geometry = ArtistGeometry(figure)
|
|
305
|
+
self.boundaries = boundaries
|
|
306
|
+
|
|
307
|
+
def horizontally(
|
|
308
|
+
self,
|
|
309
|
+
text: Text,
|
|
310
|
+
ha: HorizontalJustification | float,
|
|
311
|
+
left: float,
|
|
312
|
+
right: float,
|
|
313
|
+
width: float | None = None,
|
|
314
|
+
):
|
|
315
|
+
"""
|
|
316
|
+
Horizontally Justify text between left and right
|
|
317
|
+
"""
|
|
318
|
+
rel = ha_as_float(ha)
|
|
319
|
+
if width is None:
|
|
320
|
+
width = self.geometry.width(text)
|
|
321
|
+
x = rel_position(rel, width, left, right)
|
|
322
|
+
text.set_x(x)
|
|
323
|
+
text.set_horizontalalignment("left")
|
|
324
|
+
|
|
325
|
+
def vertically(
|
|
326
|
+
self,
|
|
327
|
+
text: Text,
|
|
328
|
+
va: VerticalJustification | float,
|
|
329
|
+
bottom: float,
|
|
330
|
+
top: float,
|
|
331
|
+
height: float | None = None,
|
|
332
|
+
):
|
|
333
|
+
"""
|
|
334
|
+
Vertically Justify text between bottom and top
|
|
335
|
+
"""
|
|
336
|
+
rel = va_as_float(va)
|
|
337
|
+
|
|
338
|
+
if height is None:
|
|
339
|
+
height = self.geometry.height(text)
|
|
340
|
+
y = rel_position(rel, height, bottom, top)
|
|
341
|
+
text.set_y(y)
|
|
342
|
+
text.set_verticalalignment("bottom")
|
|
343
|
+
|
|
344
|
+
def horizontally_across_panel(
|
|
345
|
+
self, text: Text, ha: HorizontalJustification | float
|
|
346
|
+
):
|
|
347
|
+
"""
|
|
348
|
+
Horizontally Justify text accross the panel(s) width
|
|
349
|
+
"""
|
|
350
|
+
self.horizontally(
|
|
351
|
+
text, ha, self.boundaries.panel_left, self.boundaries.panel_right
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def horizontally_across_plot(
|
|
355
|
+
self, text: Text, ha: HorizontalJustification | float
|
|
356
|
+
):
|
|
357
|
+
"""
|
|
358
|
+
Horizontally Justify text across the plot's width
|
|
359
|
+
"""
|
|
360
|
+
self.horizontally(
|
|
361
|
+
text, ha, self.boundaries.plot_left, self.boundaries.plot_right
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def vertically_along_panel(
|
|
365
|
+
self, text: Text, va: VerticalJustification | float
|
|
366
|
+
):
|
|
367
|
+
"""
|
|
368
|
+
Horizontally Justify text along the panel(s) height
|
|
369
|
+
"""
|
|
370
|
+
self.vertically(
|
|
371
|
+
text, va, self.boundaries.panel_bottom, self.boundaries.panel_top
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def vertically_along_plot(
|
|
375
|
+
self, text: Text, va: VerticalJustification | float
|
|
376
|
+
):
|
|
377
|
+
"""
|
|
378
|
+
Vertically Justify text along the plot's height
|
|
379
|
+
"""
|
|
380
|
+
self.vertically(
|
|
381
|
+
text, va, self.boundaries.plot_bottom, self.boundaries.plot_top
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def horizontally_about(
|
|
385
|
+
self, text: Text, ratio: float, how: Literal["panel", "plot"]
|
|
386
|
+
):
|
|
387
|
+
"""
|
|
388
|
+
Horizontally Justify text across the panel or plot
|
|
389
|
+
"""
|
|
390
|
+
if how == "panel":
|
|
391
|
+
self.horizontally_across_panel(text, ratio)
|
|
392
|
+
else:
|
|
393
|
+
self.horizontally_across_plot(text, ratio)
|
|
394
|
+
|
|
395
|
+
def vertically_about(
|
|
396
|
+
self, text: Text, ratio: float, how: Literal["panel", "plot"]
|
|
397
|
+
):
|
|
398
|
+
"""
|
|
399
|
+
Vertically Justify text along the panel or plot
|
|
400
|
+
"""
|
|
401
|
+
if how == "panel":
|
|
402
|
+
self.vertically_along_panel(text, ratio)
|
|
403
|
+
else:
|
|
404
|
+
self.vertically_along_plot(text, ratio)
|
plotnine/_utils/__init__.py
CHANGED
|
@@ -35,7 +35,6 @@ if TYPE_CHECKING:
|
|
|
35
35
|
FloatArray,
|
|
36
36
|
FloatArrayLike,
|
|
37
37
|
HorizontalJustification,
|
|
38
|
-
IntArray,
|
|
39
38
|
Side,
|
|
40
39
|
VerticalJustification,
|
|
41
40
|
)
|
|
@@ -309,7 +308,7 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
|
|
|
309
308
|
def len_unique(x):
|
|
310
309
|
return len(np.unique(x))
|
|
311
310
|
|
|
312
|
-
ndistinct
|
|
311
|
+
ndistinct = ids.apply(len_unique, axis=0).to_numpy()
|
|
313
312
|
|
|
314
313
|
combs = np.array(np.hstack([1, np.cumprod(ndistinct[:-1])]))
|
|
315
314
|
mat = np.array(ids)
|
|
@@ -743,7 +742,7 @@ def ungroup(data: DataLike) -> DataLike:
|
|
|
743
742
|
"""Return an ungrouped DataFrame, or pass the original data back."""
|
|
744
743
|
|
|
745
744
|
if isinstance(data, DataFrameGroupBy):
|
|
746
|
-
return data.obj
|
|
745
|
+
return data.obj # pyright: ignore[reportReturnType]
|
|
747
746
|
|
|
748
747
|
return data
|
|
749
748
|
|
|
@@ -1207,3 +1206,24 @@ def has_alpha_channel(c: str | tuple) -> bool:
|
|
|
1207
1206
|
return c.startswith("#") and len(c) == 9
|
|
1208
1207
|
else:
|
|
1209
1208
|
return color_utils.is_color_tuple(c) and len(c) == 4
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def nextafter_range(rng: tuple[float, float]) -> tuple[float, float]:
|
|
1212
|
+
"""
|
|
1213
|
+
Expand floating-point range by a step to adjacent representable numbers
|
|
1214
|
+
|
|
1215
|
+
Parameters
|
|
1216
|
+
----------
|
|
1217
|
+
rng :
|
|
1218
|
+
A tuple (min, max) representing the range to expand.
|
|
1219
|
+
|
|
1220
|
+
Returns
|
|
1221
|
+
-------
|
|
1222
|
+
:
|
|
1223
|
+
A new tuple (lower, upper) where,
|
|
1224
|
+
- lower is moved 1 float toward -∞
|
|
1225
|
+
- upper is moved 1 float toward +∞
|
|
1226
|
+
"""
|
|
1227
|
+
from math import inf, nextafter
|
|
1228
|
+
|
|
1229
|
+
return (nextafter(rng[0], -inf), nextafter(rng[1], inf))
|
plotnine/_utils/context.py
CHANGED
|
@@ -116,7 +116,7 @@ class plot_composition_context:
|
|
|
116
116
|
# https://github.com/matplotlib/matplotlib/issues/24644
|
|
117
117
|
# When drawing the Composition, the dpi themeable is infective
|
|
118
118
|
# because it sets the rcParam after this figure is created.
|
|
119
|
-
rcParams = {"figure.dpi": self.cmp.
|
|
119
|
+
rcParams = {"figure.dpi": self.cmp.theme.getp("dpi")}
|
|
120
120
|
self._rc_context = mpl.rc_context(rcParams)
|
|
121
121
|
|
|
122
122
|
def __enter__(self) -> Self:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import fields
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def non_none_init_items(obj) -> Iterable[tuple[str, Any]]:
|
|
11
|
+
"""
|
|
12
|
+
Yield (name, value) pairs of dataclass fields of `obj` that:
|
|
13
|
+
|
|
14
|
+
1. Have `init=True` in their definition
|
|
15
|
+
2. Have a value that is not `None`
|
|
16
|
+
|
|
17
|
+
This function is shallow and does not recursively yield nested
|
|
18
|
+
dataclasses.
|
|
19
|
+
"""
|
|
20
|
+
return (
|
|
21
|
+
(f.name, value)
|
|
22
|
+
for f in fields(obj)
|
|
23
|
+
if f.init and (value := getattr(obj, f.name)) is not None
|
|
24
|
+
)
|
plotnine/animation.py
CHANGED
|
@@ -188,6 +188,7 @@ class PlotnineAnimation(ArtistAnimation):
|
|
|
188
188
|
"different limits from those of the first frame."
|
|
189
189
|
)
|
|
190
190
|
|
|
191
|
+
first_plot: ggplot | None = None
|
|
191
192
|
figure: Figure | None = None
|
|
192
193
|
axs: list[Axes] = []
|
|
193
194
|
artists = []
|
|
@@ -198,14 +199,15 @@ class PlotnineAnimation(ArtistAnimation):
|
|
|
198
199
|
# onto the figure and axes created by the first ggplot and
|
|
199
200
|
# they create the subsequent frames.
|
|
200
201
|
for frame_no, p in enumerate(plots):
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
if first_plot is None:
|
|
203
|
+
first_plot = p
|
|
204
|
+
figure = first_plot.draw()
|
|
205
|
+
axs = first_plot.figure.get_axes()
|
|
204
206
|
initialise_artist_offsets(len(axs))
|
|
205
|
-
scales =
|
|
207
|
+
scales = first_plot._build_objs.scales
|
|
206
208
|
set_scale_limits(scales)
|
|
207
209
|
else:
|
|
208
|
-
plot = self._draw_animation_plot(p,
|
|
210
|
+
plot = self._draw_animation_plot(p, first_plot)
|
|
209
211
|
check_scale_limits(plot.scales, frame_no)
|
|
210
212
|
|
|
211
213
|
artists.append(get_frame_artists(axs))
|
|
@@ -213,14 +215,11 @@ class PlotnineAnimation(ArtistAnimation):
|
|
|
213
215
|
if figure is None:
|
|
214
216
|
figure = plt.figure()
|
|
215
217
|
|
|
216
|
-
assert figure is not None
|
|
217
218
|
# Prevent Jupyter from plotting any static figure
|
|
218
219
|
plt.close(figure)
|
|
219
220
|
return figure, artists
|
|
220
221
|
|
|
221
|
-
def _draw_animation_plot(
|
|
222
|
-
self, plot: ggplot, figure: Figure, axs: list[Axes]
|
|
223
|
-
) -> ggplot:
|
|
222
|
+
def _draw_animation_plot(self, plot: ggplot, first_plot: ggplot) -> ggplot:
|
|
224
223
|
"""
|
|
225
224
|
Draw a plot/frame of the animation
|
|
226
225
|
|
|
@@ -229,10 +228,12 @@ class PlotnineAnimation(ArtistAnimation):
|
|
|
229
228
|
from ._utils.context import plot_context
|
|
230
229
|
|
|
231
230
|
plot = deepcopy(plot)
|
|
232
|
-
plot.figure = figure
|
|
233
|
-
plot.axs = axs
|
|
231
|
+
plot.figure = first_plot.figure
|
|
232
|
+
plot.axs = first_plot.axs
|
|
233
|
+
plot._gridspec = first_plot._sub_gridspec
|
|
234
|
+
plot._sub_gridspec = first_plot._sub_gridspec
|
|
234
235
|
with plot_context(plot):
|
|
235
236
|
plot._build()
|
|
236
|
-
|
|
237
|
+
_ = plot.facet.setup(plot)
|
|
237
238
|
plot._draw_layers()
|
|
238
239
|
return plot
|
plotnine/composition/__init__.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from ._beside import Beside
|
|
2
2
|
from ._compose import Compose
|
|
3
|
+
from ._plot_annotation import plot_annotation
|
|
4
|
+
from ._plot_layout import plot_layout
|
|
3
5
|
from ._plot_spacer import plot_spacer
|
|
4
6
|
from ._stack import Stack
|
|
7
|
+
from ._wrap import Wrap
|
|
5
8
|
|
|
6
9
|
__all__ = (
|
|
7
10
|
"Compose",
|
|
8
11
|
"Stack",
|
|
9
12
|
"Beside",
|
|
13
|
+
"Wrap",
|
|
14
|
+
"plot_annotation",
|
|
15
|
+
"plot_layout",
|
|
10
16
|
"plot_spacer",
|
|
11
17
|
)
|
plotnine/composition/_beside.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
from typing import TYPE_CHECKING
|
|
5
4
|
|
|
6
5
|
from ._compose import Compose
|
|
@@ -9,7 +8,6 @@ if TYPE_CHECKING:
|
|
|
9
8
|
from plotnine.ggplot import ggplot
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@dataclass(repr=False)
|
|
13
11
|
class Beside(Compose):
|
|
14
12
|
"""
|
|
15
13
|
Place plots or compositions side by side
|
|
@@ -26,25 +24,18 @@ class Beside(Compose):
|
|
|
26
24
|
See Also
|
|
27
25
|
--------
|
|
28
26
|
plotnine.composition.Stack : To arrange plots vertically
|
|
27
|
+
plotnine.composition.Wrap : To arrange plots in a grid
|
|
29
28
|
plotnine.composition.plot_spacer : To add a blank space between plots
|
|
30
29
|
plotnine.composition.Compose : For more on composing plots
|
|
31
30
|
"""
|
|
32
31
|
|
|
33
|
-
@property
|
|
34
|
-
def nrow(self) -> int:
|
|
35
|
-
return 1
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def ncol(self) -> int:
|
|
39
|
-
return len(self)
|
|
40
|
-
|
|
41
32
|
def __or__(self, rhs: ggplot | Compose) -> Compose:
|
|
42
33
|
"""
|
|
43
34
|
Add rhs as a column
|
|
44
35
|
"""
|
|
45
36
|
# This is adjacent or i.e. (OR | rhs) so we collapse the
|
|
46
37
|
# operands into a single operation
|
|
47
|
-
return Beside([*self, rhs])
|
|
38
|
+
return Beside([*self, rhs]) + self.layout + self.annotation
|
|
48
39
|
|
|
49
40
|
def __truediv__(self, rhs: ggplot | Compose) -> Compose:
|
|
50
41
|
"""
|
|
@@ -53,3 +44,14 @@ class Beside(Compose):
|
|
|
53
44
|
from ._stack import Stack
|
|
54
45
|
|
|
55
46
|
return Stack([self, rhs])
|
|
47
|
+
|
|
48
|
+
def __add__(self, rhs):
|
|
49
|
+
"""
|
|
50
|
+
Add rhs into the besides composition
|
|
51
|
+
"""
|
|
52
|
+
from plotnine import ggplot
|
|
53
|
+
|
|
54
|
+
if not isinstance(rhs, (ggplot, Compose)):
|
|
55
|
+
return super().__add__(rhs)
|
|
56
|
+
|
|
57
|
+
return self | rhs
|