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.
Files changed (60) 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 +1 -1
  14. plotnine/_utils/dataclasses.py +24 -0
  15. plotnine/animation.py +13 -12
  16. plotnine/composition/__init__.py +6 -0
  17. plotnine/composition/_beside.py +13 -11
  18. plotnine/composition/_compose.py +263 -99
  19. plotnine/composition/_plot_annotation.py +75 -0
  20. plotnine/composition/_plot_layout.py +143 -0
  21. plotnine/composition/_plot_spacer.py +1 -1
  22. plotnine/composition/_stack.py +13 -11
  23. plotnine/composition/_types.py +28 -0
  24. plotnine/composition/_wrap.py +60 -0
  25. plotnine/facets/facet.py +9 -12
  26. plotnine/facets/facet_grid.py +2 -2
  27. plotnine/facets/facet_wrap.py +1 -1
  28. plotnine/geoms/geom.py +2 -2
  29. plotnine/geoms/geom_map.py +4 -5
  30. plotnine/geoms/geom_path.py +8 -7
  31. plotnine/geoms/geom_rug.py +6 -10
  32. plotnine/geoms/geom_text.py +5 -5
  33. plotnine/ggplot.py +63 -9
  34. plotnine/guides/guide.py +24 -6
  35. plotnine/guides/guide_colorbar.py +88 -46
  36. plotnine/guides/guide_legend.py +47 -20
  37. plotnine/guides/guides.py +2 -2
  38. plotnine/iapi.py +17 -1
  39. plotnine/scales/scale.py +1 -1
  40. plotnine/stats/binning.py +15 -43
  41. plotnine/stats/smoothers.py +7 -3
  42. plotnine/stats/stat.py +2 -2
  43. plotnine/stats/stat_density_2d.py +10 -6
  44. plotnine/stats/stat_pointdensity.py +8 -1
  45. plotnine/stats/stat_qq.py +5 -5
  46. plotnine/stats/stat_qq_line.py +6 -1
  47. plotnine/stats/stat_sina.py +19 -20
  48. plotnine/stats/stat_summary.py +4 -2
  49. plotnine/stats/stat_summary_bin.py +7 -1
  50. plotnine/themes/elements/element_line.py +2 -0
  51. plotnine/themes/elements/element_text.py +12 -1
  52. plotnine/themes/theme.py +18 -24
  53. plotnine/themes/themeable.py +17 -3
  54. plotnine/typing.py +6 -1
  55. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/METADATA +2 -2
  56. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/RECORD +59 -51
  57. plotnine/composition/_plotspec.py +0 -50
  58. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/WHEEL +0 -0
  59. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
  60. {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 typing import TYPE_CHECKING
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)
@@ -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: IntArray = ids.apply(len_unique, axis=0).to_numpy()
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))
@@ -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.last_plot.theme.getp("dpi")}
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 figure is None:
202
- figure = p.draw()
203
- axs = figure.get_axes()
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 = p._build_objs.scales
207
+ scales = first_plot._build_objs.scales
206
208
  set_scale_limits(scales)
207
209
  else:
208
- plot = self._draw_animation_plot(p, figure, axs)
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
- plot.axs = plot.facet.setup(plot)
237
+ _ = plot.facet.setup(plot)
237
238
  plot._draw_layers()
238
239
  return plot
@@ -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
  )
@@ -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