plotnine 0.15.3__py3-none-any.whl → 0.16.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. plotnine/_mpl/gridspec.py +50 -6
  2. plotnine/_mpl/layout_manager/__init__.py +2 -5
  3. plotnine/_mpl/layout_manager/_composition_layout_items.py +98 -0
  4. plotnine/_mpl/layout_manager/_composition_side_space.py +461 -0
  5. plotnine/_mpl/layout_manager/_engine.py +19 -58
  6. plotnine/_mpl/layout_manager/_grid.py +94 -0
  7. plotnine/_mpl/layout_manager/_layout_tree.py +402 -817
  8. plotnine/_mpl/layout_manager/{_layout_items.py → _plot_layout_items.py} +55 -278
  9. plotnine/_mpl/layout_manager/{_spaces.py → _plot_side_space.py} +111 -291
  10. plotnine/_mpl/layout_manager/_side_space.py +176 -0
  11. plotnine/_mpl/utils.py +259 -1
  12. plotnine/_utils/__init__.py +23 -3
  13. plotnine/_utils/context.py +9 -13
  14. plotnine/_utils/dataclasses.py +24 -0
  15. plotnine/_utils/ipython.py +4 -1
  16. plotnine/animation.py +13 -12
  17. plotnine/composition/__init__.py +6 -0
  18. plotnine/composition/_beside.py +13 -11
  19. plotnine/composition/_compose.py +263 -99
  20. plotnine/composition/_plot_annotation.py +75 -0
  21. plotnine/composition/_plot_layout.py +143 -0
  22. plotnine/composition/_plot_spacer.py +1 -1
  23. plotnine/composition/_stack.py +13 -11
  24. plotnine/composition/_types.py +28 -0
  25. plotnine/composition/_wrap.py +60 -0
  26. plotnine/facets/facet.py +9 -12
  27. plotnine/facets/facet_grid.py +2 -2
  28. plotnine/facets/facet_wrap.py +1 -1
  29. plotnine/geoms/geom.py +2 -2
  30. plotnine/geoms/geom_map.py +4 -5
  31. plotnine/geoms/geom_path.py +8 -7
  32. plotnine/geoms/geom_rug.py +6 -10
  33. plotnine/geoms/geom_text.py +5 -5
  34. plotnine/ggplot.py +63 -9
  35. plotnine/guides/guide.py +24 -6
  36. plotnine/guides/guide_colorbar.py +88 -46
  37. plotnine/guides/guide_legend.py +47 -20
  38. plotnine/guides/guides.py +2 -2
  39. plotnine/iapi.py +17 -1
  40. plotnine/scales/scale.py +1 -1
  41. plotnine/stats/binning.py +15 -43
  42. plotnine/stats/smoothers.py +7 -3
  43. plotnine/stats/stat.py +2 -2
  44. plotnine/stats/stat_density_2d.py +10 -6
  45. plotnine/stats/stat_pointdensity.py +8 -1
  46. plotnine/stats/stat_qq.py +5 -5
  47. plotnine/stats/stat_qq_line.py +6 -1
  48. plotnine/stats/stat_sina.py +19 -20
  49. plotnine/stats/stat_summary.py +4 -2
  50. plotnine/stats/stat_summary_bin.py +7 -1
  51. plotnine/themes/elements/element_line.py +2 -0
  52. plotnine/themes/elements/element_text.py +12 -1
  53. plotnine/themes/theme.py +18 -24
  54. plotnine/themes/themeable.py +17 -3
  55. plotnine/typing.py +9 -2
  56. {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/METADATA +3 -3
  57. {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/RECORD +60 -52
  58. {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/WHEEL +1 -1
  59. plotnine/composition/_plotspec.py +0 -50
  60. {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/licenses/LICENSE +0 -0
  61. {plotnine-0.15.3.dist-info → plotnine-0.16.0a2.dist-info}/top_level.txt +0 -0
@@ -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
@@ -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 Generator, Iterator
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
- items: list[ggplot | Compose]
103
+ # These are created in the ._create_figure
104
+ figure: Figure
105
+ _gridspec: p9GridSpec
87
106
  """
88
- The objects to be arranged (composed).
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
- # These are created in the _create_figure method
92
- figure: Figure = field(init=False, repr=False)
93
- plotspecs: list[plotspec] = field(init=False, repr=False)
94
- gridspec: p9GridSpec = field(init=False, repr=False)
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 __post_init__(self):
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__(self, rhs: ggplot | Compose | PlotAddable) -> Compose:
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
- if not isinstance(rhs, (ggplot, Compose)):
145
- cmp = deepcopy(self)
146
- cmp.last_plot = cmp.last_plot + rhs
147
- return cmp
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
- def add_other(cmp: Compose):
185
- for i, item in enumerate(cmp):
186
- if isinstance(item, Compose):
187
- add_other(item)
188
- else:
189
- cmp[i] = item + copy(rhs)
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 i, item in enumerate(self):
316
+ for item in self:
208
317
  if isinstance(item, ggplot):
209
- self[i] = item + copy(rhs)
318
+ item += copy(rhs)
210
319
 
211
320
  return self
212
321
 
213
322
  def __len__(self) -> int:
214
323
  """
215
- Number of operand
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.last_plot.theme._figure_size_px
366
+ figure_size_px = self.theme._figure_size_px
258
367
  return get_mimebundle(buf.getvalue(), format, figure_size_px)
259
368
 
260
- @property
261
- def nrow(self) -> int:
262
- """
263
- Number of rows in the composition
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
- @property
268
- def ncol(self) -> int:
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
- Number of cols in the composition
383
+ Recursively generate all plots under this composition
271
384
  """
272
- return 0
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
- if not hasattr(self, "figure"):
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
- def _make_plotspecs(
357
- cmp: Compose, parent_gridspec: p9GridSpec | None
358
- ) -> Generator[plotspec]:
359
- """
360
- Return the plot specification for each subplot in the composition
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
- def _draw_plots(self):
394
- """
395
- Draw all plots in the composition
396
- """
397
- for ps in self.plotspecs:
398
- ps.plot.draw()
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 PlotnineCompositionLayoutEngine
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._setup()
437
- self._draw_plots()
438
- figure.set_layout_engine(PlotnineCompositionLayoutEngine(self))
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