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
plotnine/composition/_compose.py
CHANGED
|
@@ -2,9 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
from copy import copy, deepcopy
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
5
|
from io import BytesIO
|
|
7
|
-
from typing import TYPE_CHECKING, overload
|
|
6
|
+
from typing import TYPE_CHECKING, cast, overload
|
|
7
|
+
|
|
8
|
+
from plotnine.themes.theme import theme, theme_get
|
|
8
9
|
|
|
9
10
|
from .._utils.context import plot_composition_context
|
|
10
11
|
from .._utils.ipython import (
|
|
@@ -13,21 +14,25 @@ from .._utils.ipython import (
|
|
|
13
14
|
is_inline_backend,
|
|
14
15
|
)
|
|
15
16
|
from .._utils.quarto import is_knitr_engine, is_quarto_environment
|
|
17
|
+
from ..composition._plot_annotation import plot_annotation
|
|
18
|
+
from ..composition._plot_layout import plot_layout
|
|
19
|
+
from ..composition._types import ComposeAddable
|
|
16
20
|
from ..options import get_option
|
|
17
|
-
from ._plotspec import plotspec
|
|
18
21
|
|
|
19
22
|
if TYPE_CHECKING:
|
|
20
23
|
from pathlib import Path
|
|
21
|
-
from typing import
|
|
24
|
+
from typing import Iterator
|
|
22
25
|
|
|
23
26
|
from matplotlib.figure import Figure
|
|
24
27
|
|
|
25
28
|
from plotnine._mpl.gridspec import p9GridSpec
|
|
29
|
+
from plotnine._mpl.layout_manager._composition_side_space import (
|
|
30
|
+
CompositionSideSpaces,
|
|
31
|
+
)
|
|
26
32
|
from plotnine.ggplot import PlotAddable, ggplot
|
|
27
33
|
from plotnine.typing import FigureFormat, MimeBundle
|
|
28
34
|
|
|
29
35
|
|
|
30
|
-
@dataclass
|
|
31
36
|
class Compose:
|
|
32
37
|
"""
|
|
33
38
|
Base class for those that create plot compositions
|
|
@@ -56,6 +61,11 @@ class Compose:
|
|
|
56
61
|
: Arrange operands side by side _and_ at the same nesting level.
|
|
57
62
|
Also powered by the subclass [](`~plotnine.composition.Beside`).
|
|
58
63
|
|
|
64
|
+
`+`
|
|
65
|
+
|
|
66
|
+
: Arrange operands in a 2D grid.
|
|
67
|
+
Powered by the subclass [](`~plotnine.composition.Wrap`).
|
|
68
|
+
|
|
59
69
|
### 2. Plot Modifying Operators
|
|
60
70
|
|
|
61
71
|
The modify all or some of the plots in a composition.
|
|
@@ -76,32 +86,67 @@ class Compose:
|
|
|
76
86
|
|
|
77
87
|
: Add right hand side to the last plot in the composition.
|
|
78
88
|
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
items :
|
|
92
|
+
The objects to be arranged (composed)
|
|
93
|
+
|
|
94
|
+
|
|
79
95
|
See Also
|
|
80
96
|
--------
|
|
81
97
|
plotnine.composition.Beside : To arrange plots side by side
|
|
82
98
|
plotnine.composition.Stack : To arrange plots vertically
|
|
99
|
+
plotnine.composition.Wrap : To arrange in a grid
|
|
83
100
|
plotnine.composition.plot_spacer : To add a blank space between plots
|
|
84
101
|
"""
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
# These are created in the ._create_figure
|
|
104
|
+
figure: Figure
|
|
105
|
+
_gridspec: p9GridSpec
|
|
87
106
|
"""
|
|
88
|
-
|
|
107
|
+
Gridspec (1x1) that contains the annotations and the composition items
|
|
108
|
+
|
|
109
|
+
plot_layout's theme parameter affects this gridspec.
|
|
89
110
|
"""
|
|
90
111
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
_sub_gridspec: p9GridSpec
|
|
113
|
+
"""
|
|
114
|
+
Gridspec (nxn) that contains the composed [ggplot | Compose] items
|
|
115
|
+
|
|
116
|
+
-------------------
|
|
117
|
+
| title |<----- ._gridspec
|
|
118
|
+
| subtitle |
|
|
119
|
+
| |
|
|
120
|
+
| ------------- |
|
|
121
|
+
| | | |<-+------ ._sub_gridspec
|
|
122
|
+
| | | | |
|
|
123
|
+
| ------------- |
|
|
124
|
+
| |
|
|
125
|
+
| caption |
|
|
126
|
+
-------------------
|
|
127
|
+
"""
|
|
128
|
+
_sidespaces: CompositionSideSpaces
|
|
95
129
|
|
|
96
|
-
def
|
|
130
|
+
def __init__(self, items: list[ggplot | Compose]):
|
|
97
131
|
# The way we handle the plots has consequences that would
|
|
98
132
|
# prevent having a duplicate plot in the composition.
|
|
99
133
|
# Using copies prevents this.
|
|
100
134
|
self.items = [
|
|
101
|
-
op if isinstance(op, Compose) else deepcopy(op)
|
|
102
|
-
for op in self.items
|
|
135
|
+
op if isinstance(op, Compose) else deepcopy(op) for op in items
|
|
103
136
|
]
|
|
104
137
|
|
|
138
|
+
self._layout = plot_layout()
|
|
139
|
+
"""
|
|
140
|
+
Every composition gets initiated with an empty plot_layout whose
|
|
141
|
+
attributes are either dynamically generated before the composition
|
|
142
|
+
is drawn, or they are overwritten by a layout added by the user.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
self._annotation = plot_annotation()
|
|
146
|
+
"""
|
|
147
|
+
The annotations around the composition
|
|
148
|
+
"""
|
|
149
|
+
|
|
105
150
|
def __repr__(self):
|
|
106
151
|
"""
|
|
107
152
|
repr
|
|
@@ -118,6 +163,61 @@ class Compose:
|
|
|
118
163
|
return ""
|
|
119
164
|
return super().__repr__()
|
|
120
165
|
|
|
166
|
+
@property
|
|
167
|
+
def layout(self) -> plot_layout:
|
|
168
|
+
"""
|
|
169
|
+
The plot_layout of this composition
|
|
170
|
+
"""
|
|
171
|
+
self.items
|
|
172
|
+
return self._layout
|
|
173
|
+
|
|
174
|
+
@layout.setter
|
|
175
|
+
def layout(self, value: plot_layout):
|
|
176
|
+
"""
|
|
177
|
+
Add (or merge) a plot_layout to this composition
|
|
178
|
+
"""
|
|
179
|
+
self._layout = copy(self.layout)
|
|
180
|
+
self._layout.update(value)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def annotation(self) -> plot_annotation:
|
|
184
|
+
"""
|
|
185
|
+
The plot_annotation of this composition
|
|
186
|
+
"""
|
|
187
|
+
return self._annotation
|
|
188
|
+
|
|
189
|
+
@annotation.setter
|
|
190
|
+
def annotation(self, value: plot_annotation):
|
|
191
|
+
"""
|
|
192
|
+
Add (or merge) a plot_annotation to this composition
|
|
193
|
+
"""
|
|
194
|
+
self._annotation = copy(self.annotation)
|
|
195
|
+
self._annotation.update(value)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def nrow(self) -> int:
|
|
199
|
+
return cast("int", self.layout.nrow)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def ncol(self) -> int:
|
|
203
|
+
return cast("int", self.layout.ncol)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def theme(self) -> theme:
|
|
207
|
+
"""
|
|
208
|
+
Theme for this composition
|
|
209
|
+
|
|
210
|
+
This is the default theme plus combined with theme from the
|
|
211
|
+
annotation.
|
|
212
|
+
"""
|
|
213
|
+
if not getattr(self, "_theme", None):
|
|
214
|
+
self._theme = theme_get() + self.annotation.theme
|
|
215
|
+
return self._theme
|
|
216
|
+
|
|
217
|
+
@theme.setter
|
|
218
|
+
def theme(self, value: theme):
|
|
219
|
+
self._theme = value
|
|
220
|
+
|
|
121
221
|
@abc.abstractmethod
|
|
122
222
|
def __or__(self, rhs: ggplot | Compose) -> Compose:
|
|
123
223
|
"""
|
|
@@ -130,7 +230,10 @@ class Compose:
|
|
|
130
230
|
Add rhs as a row
|
|
131
231
|
"""
|
|
132
232
|
|
|
133
|
-
def __add__(
|
|
233
|
+
def __add__(
|
|
234
|
+
self,
|
|
235
|
+
rhs: ggplot | Compose | PlotAddable | ComposeAddable,
|
|
236
|
+
) -> Compose:
|
|
134
237
|
"""
|
|
135
238
|
Add rhs to the composition
|
|
136
239
|
|
|
@@ -141,10 +244,13 @@ class Compose:
|
|
|
141
244
|
"""
|
|
142
245
|
from plotnine import ggplot
|
|
143
246
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return
|
|
247
|
+
self = deepcopy(self)
|
|
248
|
+
|
|
249
|
+
if isinstance(rhs, ComposeAddable):
|
|
250
|
+
return rhs.__radd__(self)
|
|
251
|
+
elif not isinstance(rhs, (ggplot, Compose)):
|
|
252
|
+
self.last_plot = self.last_plot + rhs
|
|
253
|
+
return self
|
|
148
254
|
|
|
149
255
|
t1, t2 = type(self).__name__, type(rhs).__name__
|
|
150
256
|
msg = f"unsupported operand type(s) for +: '{t1}' and '{t2}'"
|
|
@@ -179,16 +285,19 @@ class Compose:
|
|
|
179
285
|
rhs:
|
|
180
286
|
What to add.
|
|
181
287
|
"""
|
|
288
|
+
from plotnine import theme
|
|
289
|
+
|
|
182
290
|
self = deepcopy(self)
|
|
183
291
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
292
|
+
if isinstance(rhs, theme):
|
|
293
|
+
self.annotation.theme = self.annotation.theme + rhs
|
|
294
|
+
|
|
295
|
+
for i, item in enumerate(self):
|
|
296
|
+
if isinstance(item, Compose):
|
|
297
|
+
self[i] = item & rhs
|
|
298
|
+
else:
|
|
299
|
+
item += copy(rhs)
|
|
190
300
|
|
|
191
|
-
add_other(self)
|
|
192
301
|
return self
|
|
193
302
|
|
|
194
303
|
def __mul__(self, rhs: PlotAddable) -> Compose:
|
|
@@ -204,15 +313,15 @@ class Compose:
|
|
|
204
313
|
|
|
205
314
|
self = deepcopy(self)
|
|
206
315
|
|
|
207
|
-
for
|
|
316
|
+
for item in self:
|
|
208
317
|
if isinstance(item, ggplot):
|
|
209
|
-
|
|
318
|
+
item += copy(rhs)
|
|
210
319
|
|
|
211
320
|
return self
|
|
212
321
|
|
|
213
322
|
def __len__(self) -> int:
|
|
214
323
|
"""
|
|
215
|
-
Number of
|
|
324
|
+
Number of operands
|
|
216
325
|
"""
|
|
217
326
|
return len(self.items)
|
|
218
327
|
|
|
@@ -254,22 +363,30 @@ class Compose:
|
|
|
254
363
|
|
|
255
364
|
buf = BytesIO()
|
|
256
365
|
self.save(buf, "png" if format == "retina" else format)
|
|
257
|
-
figure_size_px = self.
|
|
366
|
+
figure_size_px = self.theme._figure_size_px
|
|
258
367
|
return get_mimebundle(buf.getvalue(), format, figure_size_px)
|
|
259
368
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"""
|
|
265
|
-
return 0
|
|
369
|
+
def iter_sub_compositions(self):
|
|
370
|
+
for item in self:
|
|
371
|
+
if isinstance(item, Compose):
|
|
372
|
+
yield item
|
|
266
373
|
|
|
267
|
-
|
|
268
|
-
|
|
374
|
+
def iter_plots(self):
|
|
375
|
+
from plotnine import ggplot
|
|
376
|
+
|
|
377
|
+
for item in self:
|
|
378
|
+
if isinstance(item, ggplot):
|
|
379
|
+
yield item
|
|
380
|
+
|
|
381
|
+
def iter_plots_all(self):
|
|
269
382
|
"""
|
|
270
|
-
|
|
383
|
+
Recursively generate all plots under this composition
|
|
271
384
|
"""
|
|
272
|
-
|
|
385
|
+
for plot in self.iter_plots():
|
|
386
|
+
yield plot
|
|
387
|
+
|
|
388
|
+
for cmp in self.iter_sub_compositions():
|
|
389
|
+
yield from cmp.iter_plots_all()
|
|
273
390
|
|
|
274
391
|
@property
|
|
275
392
|
def last_plot(self) -> ggplot:
|
|
@@ -322,80 +439,63 @@ class Compose:
|
|
|
322
439
|
def _to_retina(self):
|
|
323
440
|
from plotnine import ggplot
|
|
324
441
|
|
|
442
|
+
self.theme = self.theme.to_retina()
|
|
443
|
+
|
|
325
444
|
for item in self:
|
|
326
445
|
if isinstance(item, ggplot):
|
|
327
446
|
item.theme = item.theme.to_retina()
|
|
328
447
|
else:
|
|
329
448
|
item._to_retina()
|
|
330
449
|
|
|
331
|
-
def _create_gridspec(self, figure, nest_into):
|
|
332
|
-
"""
|
|
333
|
-
Create the gridspec for this composition
|
|
334
|
-
"""
|
|
335
|
-
from plotnine._mpl.gridspec import p9GridSpec
|
|
336
|
-
|
|
337
|
-
self.gridspec = p9GridSpec(
|
|
338
|
-
self.nrow, self.ncol, figure, nest_into=nest_into
|
|
339
|
-
)
|
|
340
|
-
|
|
341
450
|
def _setup(self) -> Figure:
|
|
342
451
|
"""
|
|
343
452
|
Setup this instance for the building process
|
|
344
453
|
"""
|
|
345
|
-
|
|
346
|
-
self._create_figure()
|
|
347
|
-
|
|
454
|
+
self._create_figure()
|
|
348
455
|
return self.figure
|
|
349
456
|
|
|
350
457
|
def _create_figure(self):
|
|
458
|
+
"""
|
|
459
|
+
Create figure & gridspecs for all sub compositions
|
|
460
|
+
"""
|
|
461
|
+
if hasattr(self, "figure"):
|
|
462
|
+
return
|
|
463
|
+
|
|
351
464
|
import matplotlib.pyplot as plt
|
|
352
465
|
|
|
466
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
467
|
+
|
|
468
|
+
figure = plt.figure()
|
|
469
|
+
self._generate_gridspecs(
|
|
470
|
+
figure, p9GridSpec(1, 1, figure, nest_into=None)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def _generate_gridspecs(self, figure: Figure, container_gs: p9GridSpec):
|
|
353
474
|
from plotnine import ggplot
|
|
354
475
|
from plotnine._mpl.gridspec import p9GridSpec
|
|
355
476
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
# This gridspec contains a composition group e.g.
|
|
363
|
-
# (p2 | p3) of p1 | (p2 | p3)
|
|
364
|
-
ss_or_none = parent_gridspec[0] if parent_gridspec else None
|
|
365
|
-
cmp._create_gridspec(self.figure, ss_or_none)
|
|
366
|
-
|
|
367
|
-
# Each subplot in the composition will contain one of:
|
|
368
|
-
# 1. A plot
|
|
369
|
-
# 2. A plot composition
|
|
370
|
-
# 3. Nothing
|
|
371
|
-
# Iterating over the gridspec yields the SubplotSpecs for each
|
|
372
|
-
# "subplot" in the grid. The SubplotSpec is the handle that
|
|
373
|
-
# allows us to set it up for a plot or to nest another gridspec
|
|
374
|
-
# in it.
|
|
375
|
-
for item, subplot_spec in zip(cmp, cmp.gridspec): # pyright: ignore[reportArgumentType]
|
|
376
|
-
if isinstance(item, ggplot):
|
|
377
|
-
yield plotspec(
|
|
378
|
-
item,
|
|
379
|
-
self.figure,
|
|
380
|
-
cmp.gridspec,
|
|
381
|
-
subplot_spec,
|
|
382
|
-
p9GridSpec(1, 1, self.figure, nest_into=subplot_spec),
|
|
383
|
-
)
|
|
384
|
-
elif item:
|
|
385
|
-
yield from _make_plotspecs(
|
|
386
|
-
item,
|
|
387
|
-
p9GridSpec(1, 1, self.figure, nest_into=subplot_spec),
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
self.figure = plt.figure()
|
|
391
|
-
self.plotspecs = list(_make_plotspecs(self, None))
|
|
477
|
+
self.figure = figure
|
|
478
|
+
self._gridspec = container_gs
|
|
479
|
+
self.layout._setup(self)
|
|
480
|
+
self._sub_gridspec = p9GridSpec.from_layout(
|
|
481
|
+
self.layout, figure=figure, nest_into=container_gs[0]
|
|
482
|
+
)
|
|
392
483
|
|
|
393
|
-
|
|
394
|
-
""
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
for
|
|
398
|
-
|
|
484
|
+
# Iterating over the gridspec yields the SubplotSpecs for each
|
|
485
|
+
# "subplot" in the grid. The SubplotSpec is the handle for the
|
|
486
|
+
# area in the grid; it allows us to put a plot or a nested
|
|
487
|
+
# composion in that area.
|
|
488
|
+
for item, subplot_spec in zip(self, self._sub_gridspec):
|
|
489
|
+
# This container gs will contain a plot or a composition,
|
|
490
|
+
# i.e. it will be assigned to one of:
|
|
491
|
+
# 1. ggplot._gridspec
|
|
492
|
+
# 2. compose._gridspec
|
|
493
|
+
_container_gs = p9GridSpec(1, 1, figure, nest_into=subplot_spec)
|
|
494
|
+
if isinstance(item, ggplot):
|
|
495
|
+
item.figure = figure
|
|
496
|
+
item._gridspec = _container_gs
|
|
497
|
+
else:
|
|
498
|
+
item._generate_gridspecs(figure, _container_gs)
|
|
399
499
|
|
|
400
500
|
def show(self):
|
|
401
501
|
"""
|
|
@@ -430,14 +530,78 @@ class Compose:
|
|
|
430
530
|
:
|
|
431
531
|
Matplotlib figure
|
|
432
532
|
"""
|
|
433
|
-
from .._mpl.layout_manager import
|
|
533
|
+
from .._mpl.layout_manager import PlotnineLayoutEngine
|
|
534
|
+
|
|
535
|
+
def _draw(cmp):
|
|
536
|
+
figure = cmp._setup()
|
|
537
|
+
cmp._draw_plots()
|
|
434
538
|
|
|
539
|
+
for sub_cmp in cmp.iter_sub_compositions():
|
|
540
|
+
_draw(sub_cmp)
|
|
541
|
+
|
|
542
|
+
return figure
|
|
543
|
+
|
|
544
|
+
# As the plot border and plot background apply to the entire
|
|
545
|
+
# composition and not the sub compositions, the theme of the
|
|
546
|
+
# whole composition is applied last (outside _draw).
|
|
435
547
|
with plot_composition_context(self, show):
|
|
436
|
-
figure = self
|
|
437
|
-
self.
|
|
438
|
-
|
|
548
|
+
figure = _draw(self)
|
|
549
|
+
self.theme._setup(
|
|
550
|
+
self.figure,
|
|
551
|
+
None,
|
|
552
|
+
self.annotation.title,
|
|
553
|
+
self.annotation.subtitle,
|
|
554
|
+
)
|
|
555
|
+
self._draw_annotation()
|
|
556
|
+
self._draw_composition_background()
|
|
557
|
+
self.theme.apply()
|
|
558
|
+
figure.set_layout_engine(PlotnineLayoutEngine(self))
|
|
559
|
+
|
|
439
560
|
return figure
|
|
440
561
|
|
|
562
|
+
def _draw_plots(self):
|
|
563
|
+
"""
|
|
564
|
+
Draw all plots in the composition
|
|
565
|
+
"""
|
|
566
|
+
from plotnine import ggplot
|
|
567
|
+
|
|
568
|
+
for item in self:
|
|
569
|
+
if isinstance(item, ggplot):
|
|
570
|
+
item.draw()
|
|
571
|
+
|
|
572
|
+
def _draw_composition_background(self):
|
|
573
|
+
"""
|
|
574
|
+
Draw the background rectangle of the composition
|
|
575
|
+
"""
|
|
576
|
+
from matplotlib.patches import Rectangle
|
|
577
|
+
|
|
578
|
+
rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000)
|
|
579
|
+
self.figure.add_artist(rect)
|
|
580
|
+
self._gridspec.patch = rect
|
|
581
|
+
self.theme.targets.plot_background = rect
|
|
582
|
+
|
|
583
|
+
def _draw_annotation(self):
|
|
584
|
+
"""
|
|
585
|
+
Draw the items in the annotation
|
|
586
|
+
|
|
587
|
+
Note that, this method puts the artists on the figure, and
|
|
588
|
+
the layout manager moves them to their final positions.
|
|
589
|
+
"""
|
|
590
|
+
if self.annotation.empty():
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
figure = self.theme.figure
|
|
594
|
+
targets = self.theme.targets
|
|
595
|
+
|
|
596
|
+
if title := self.annotation.title:
|
|
597
|
+
targets.plot_title = figure.text(0, 0, title)
|
|
598
|
+
|
|
599
|
+
if subtitle := self.annotation.subtitle:
|
|
600
|
+
targets.plot_subtitle = figure.text(0, 0, subtitle)
|
|
601
|
+
|
|
602
|
+
if caption := self.annotation.caption:
|
|
603
|
+
targets.plot_caption = figure.text(0, 0, caption)
|
|
604
|
+
|
|
441
605
|
def save(
|
|
442
606
|
self,
|
|
443
607
|
filename: str | Path | BytesIO,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .. import theme
|
|
7
|
+
from .._utils.dataclasses import non_none_init_items
|
|
8
|
+
from ..composition._types import ComposeAddable
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ._compose import Compose
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(kw_only=True)
|
|
15
|
+
class plot_annotation(ComposeAddable):
|
|
16
|
+
"""
|
|
17
|
+
Annotate a composition
|
|
18
|
+
|
|
19
|
+
This applies to only the top-level composition. When a composition
|
|
20
|
+
with an annotation is added to larger composition, the annotation
|
|
21
|
+
of the sub-composition becomes irrelevant.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
title: str | None = None
|
|
25
|
+
"""
|
|
26
|
+
The title of the composition
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
subtitle: str | None = None
|
|
30
|
+
"""
|
|
31
|
+
The subtitle of the composition
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
caption: str | None = None
|
|
35
|
+
"""
|
|
36
|
+
The caption of the composition
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
theme: theme = field(default_factory=theme) # pyright: ignore[reportUnboundVariable]
|
|
40
|
+
"""
|
|
41
|
+
Theme to use for the plot title, subtitle, caption, margin and background
|
|
42
|
+
|
|
43
|
+
It also controls the [](`~plotnine.themes.themeables.figure_size`) of the
|
|
44
|
+
composition. The default theme is the same as the default one used for the
|
|
45
|
+
plots, which you can change with [](`~plotnine.theme_set`).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __radd__(self, cmp: Compose) -> Compose:
|
|
49
|
+
"""
|
|
50
|
+
Add plot annotation to composition
|
|
51
|
+
"""
|
|
52
|
+
cmp.annotation = self
|
|
53
|
+
return cmp
|
|
54
|
+
|
|
55
|
+
def update(self, other: plot_annotation):
|
|
56
|
+
"""
|
|
57
|
+
Update this annotation with the contents of other
|
|
58
|
+
"""
|
|
59
|
+
for name, value in non_none_init_items(other):
|
|
60
|
+
if name == "theme":
|
|
61
|
+
self.theme = self.theme + value
|
|
62
|
+
else:
|
|
63
|
+
setattr(self, name, value)
|
|
64
|
+
|
|
65
|
+
def empty(self) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Whether the annotation has any content
|
|
68
|
+
"""
|
|
69
|
+
for name, value in non_none_init_items(self):
|
|
70
|
+
if name == "theme":
|
|
71
|
+
return len(value.themeables) == 0
|
|
72
|
+
else:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
return True
|