plotnine 0.15.0a5__py3-none-any.whl → 0.15.0a7__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 (64) hide show
  1. plotnine/_mpl/layout_manager/_engine.py +2 -2
  2. plotnine/_mpl/layout_manager/_layout_items.py +6 -2
  3. plotnine/_mpl/layout_manager/_layout_tree.py +221 -60
  4. plotnine/_mpl/layout_manager/_spaces.py +98 -28
  5. plotnine/_mpl/text.py +1 -7
  6. plotnine/_utils/__init__.py +13 -0
  7. plotnine/composition/__init__.py +11 -0
  8. plotnine/{plot_composition/_compose.py → composition/_arrange.py} +64 -119
  9. plotnine/composition/_beside.py +54 -0
  10. plotnine/composition/_plot_spacer.py +55 -0
  11. plotnine/composition/_stack.py +54 -0
  12. plotnine/geoms/geom_area.py +2 -2
  13. plotnine/geoms/geom_bar.py +1 -0
  14. plotnine/geoms/geom_bin_2d.py +6 -2
  15. plotnine/geoms/geom_boxplot.py +4 -0
  16. plotnine/geoms/geom_col.py +2 -2
  17. plotnine/geoms/geom_count.py +6 -2
  18. plotnine/geoms/geom_density_2d.py +6 -2
  19. plotnine/geoms/geom_dotplot.py +1 -1
  20. plotnine/geoms/geom_histogram.py +1 -1
  21. plotnine/geoms/geom_map.py +2 -2
  22. plotnine/geoms/geom_pointdensity.py +4 -0
  23. plotnine/geoms/geom_qq.py +4 -0
  24. plotnine/geoms/geom_qq_line.py +4 -0
  25. plotnine/geoms/geom_quantile.py +4 -0
  26. plotnine/geoms/geom_sina.py +3 -3
  27. plotnine/geoms/geom_smooth.py +4 -0
  28. plotnine/geoms/geom_violin.py +4 -0
  29. plotnine/ggplot.py +12 -26
  30. plotnine/scales/scale_manual.py +2 -0
  31. plotnine/stats/stat_bin.py +4 -0
  32. plotnine/stats/stat_bin_2d.py +4 -0
  33. plotnine/stats/stat_bindot.py +1 -0
  34. plotnine/stats/stat_boxplot.py +1 -1
  35. plotnine/stats/stat_count.py +1 -0
  36. plotnine/stats/stat_density.py +1 -1
  37. plotnine/stats/stat_density_2d.py +1 -0
  38. plotnine/stats/stat_ecdf.py +1 -1
  39. plotnine/stats/stat_ellipse.py +4 -0
  40. plotnine/stats/stat_function.py +4 -0
  41. plotnine/stats/stat_hull.py +4 -0
  42. plotnine/stats/stat_identity.py +4 -0
  43. plotnine/stats/stat_pointdensity.py +1 -0
  44. plotnine/stats/stat_qq.py +1 -0
  45. plotnine/stats/stat_qq_line.py +1 -0
  46. plotnine/stats/stat_quantile.py +1 -1
  47. plotnine/stats/stat_sina.py +1 -1
  48. plotnine/stats/stat_smooth.py +1 -0
  49. plotnine/stats/stat_sum.py +4 -0
  50. plotnine/stats/stat_summary.py +1 -1
  51. plotnine/stats/stat_summary_bin.py +1 -1
  52. plotnine/stats/stat_unique.py +4 -0
  53. plotnine/stats/stat_ydensity.py +1 -1
  54. plotnine/themes/elements/element_line.py +17 -9
  55. plotnine/themes/theme_void.py +1 -7
  56. plotnine/themes/themeable.py +24 -13
  57. {plotnine-0.15.0a5.dist-info → plotnine-0.15.0a7.dist-info}/METADATA +1 -1
  58. {plotnine-0.15.0a5.dist-info → plotnine-0.15.0a7.dist-info}/RECORD +62 -60
  59. plotnine/plot_composition/__init__.py +0 -10
  60. plotnine/plot_composition/_spacer.py +0 -32
  61. /plotnine/{plot_composition → composition}/_plotspec.py +0 -0
  62. {plotnine-0.15.0a5.dist-info → plotnine-0.15.0a7.dist-info}/WHEEL +0 -0
  63. {plotnine-0.15.0a5.dist-info → plotnine-0.15.0a7.dist-info}/licenses/LICENSE +0 -0
  64. {plotnine-0.15.0a5.dist-info → plotnine-0.15.0a7.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from dataclasses import dataclass, field, fields
16
16
  from functools import cached_property
17
17
  from typing import TYPE_CHECKING, cast
18
18
 
19
+ from plotnine.exceptions import PlotnineError
19
20
  from plotnine.facets import facet_grid, facet_null, facet_wrap
20
21
 
21
22
  from ._layout_items import LayoutItems
@@ -233,7 +234,7 @@ class _side_spaces(ABC):
233
234
  """
234
235
  The width of the tag including the margins
235
236
 
236
- The value is zero expect if all these are true:
237
+ The value is zero except if all these are true:
237
238
  - The tag is in the margin `theme(plot_tag_position = "margin")`
238
239
  - The tag at one one of the the following locations;
239
240
  left, right, topleft, topright, bottomleft or bottomright
@@ -245,13 +246,41 @@ class _side_spaces(ABC):
245
246
  """
246
247
  The height of the tag including the margins
247
248
 
248
- The value is zero expect if all these are true:
249
+ The value is zero except if all these are true:
249
250
  - The tag is in the margin `theme(plot_tag_position = "margin")`
250
251
  - The tag at one one of the the following locations;
251
252
  top, bottom, topleft, topright, bottomleft or bottomright
252
253
  """
253
254
  return 0
254
255
 
256
+ @property
257
+ def axis_title_clearance(self) -> float:
258
+ """
259
+ The distance between the axis title and the panel
260
+
261
+ Figure
262
+ ----------------------------
263
+ | Panel |
264
+ | ----------- |
265
+ | | | |
266
+ | | | |
267
+ | Y<--->| | |
268
+ | | | |
269
+ | | | |
270
+ | ----------- |
271
+ | |
272
+ ----------------------------
273
+
274
+ We use this value to when aligning axis titles in a
275
+ plot composition.
276
+ """
277
+
278
+ try:
279
+ return self.total - self.sum_upto("axis_title_alignment")
280
+ except AttributeError as err:
281
+ # There is probably an error in in the layout manager
282
+ raise PlotnineError("Side has no axis title") from err
283
+
255
284
 
256
285
  @dataclass
257
286
  class left_spaces(_side_spaces):
@@ -311,6 +340,14 @@ class left_spaces(_side_spaces):
311
340
  axis_title_y_margin_left: float = 0
312
341
  axis_title_y: float = 0
313
342
  axis_title_y_margin_right: float = 0
343
+ axis_title_alignment: float = 0
344
+ """
345
+ Space added to align the axis title with others in a composition
346
+
347
+ This value is calculated during the layout process. The amount is
348
+ the difference between the largest and smallest axis_title_clearance
349
+ among the items in the composition.
350
+ """
314
351
  axis_text_y_margin_left: float = 0
315
352
  axis_text_y: float = 0
316
353
  axis_text_y_margin_right: float = 0
@@ -385,18 +422,18 @@ class left_spaces(_side_spaces):
385
422
  return self.to_figure_space(self.sum_incl(item))
386
423
 
387
424
  @property
388
- def left_relative(self):
425
+ def panel_left_relative(self):
389
426
  """
390
427
  Left (relative to the gridspec) of the panels in figure dimensions
391
428
  """
392
429
  return self.total
393
430
 
394
431
  @property
395
- def left(self):
432
+ def panel_left(self):
396
433
  """
397
434
  Left of the panels in figure space
398
435
  """
399
- return self.to_figure_space(self.left_relative)
436
+ return self.to_figure_space(self.panel_left_relative)
400
437
 
401
438
  @property
402
439
  def plot_left(self):
@@ -491,18 +528,18 @@ class right_spaces(_side_spaces):
491
528
  return self.to_figure_space(1 - self.sum_upto(item))
492
529
 
493
530
  @property
494
- def right_relative(self):
531
+ def panel_right_relative(self):
495
532
  """
496
533
  Right (relative to the gridspec) of the panels in figure dimensions
497
534
  """
498
535
  return 1 - self.total
499
536
 
500
537
  @property
501
- def right(self):
538
+ def panel_right(self):
502
539
  """
503
540
  Right of the panels in figure space
504
541
  """
505
- return self.to_figure_space(self.right_relative)
542
+ return self.to_figure_space(self.panel_right_relative)
506
543
 
507
544
  @property
508
545
  def plot_right(self):
@@ -620,18 +657,18 @@ class top_spaces(_side_spaces):
620
657
  return self.to_figure_space(1 - self.sum_upto(item))
621
658
 
622
659
  @property
623
- def top_relative(self):
660
+ def panel_top_relative(self):
624
661
  """
625
662
  Top (relative to the gridspec) of the panels in figure dimensions
626
663
  """
627
664
  return 1 - self.total
628
665
 
629
666
  @property
630
- def top(self):
667
+ def panel_top(self):
631
668
  """
632
669
  Top of the panels in figure space
633
670
  """
634
- return self.to_figure_space(self.top_relative)
671
+ return self.to_figure_space(self.panel_top_relative)
635
672
 
636
673
  @property
637
674
  def plot_top(self):
@@ -674,6 +711,15 @@ class bottom_spaces(_side_spaces):
674
711
  axis_title_x_margin_bottom: float = 0
675
712
  axis_title_x: float = 0
676
713
  axis_title_x_margin_top: float = 0
714
+ axis_title_alignment: float = 0
715
+ """
716
+ Space added to align the axis title with others in a composition
717
+
718
+ This value is calculated during the layout process in a tree structure
719
+ that has convenient access to the sides/edges of the panels in the
720
+ composition. It's amount is the difference in height between this axis
721
+ text (and it's margins) and the tallest axis text (and it's margin).
722
+ """
677
723
  axis_text_x_margin_bottom: float = 0
678
724
  axis_text_x: float = 0
679
725
  axis_text_x_margin_top: float = 0
@@ -758,18 +804,18 @@ class bottom_spaces(_side_spaces):
758
804
  return self.to_figure_space(self.sum_incl(item))
759
805
 
760
806
  @property
761
- def bottom_relative(self):
807
+ def panel_bottom_relative(self):
762
808
  """
763
809
  Bottom (relative to the gridspec) of the panels in figure dimensions
764
810
  """
765
811
  return self.total
766
812
 
767
813
  @property
768
- def bottom(self):
814
+ def panel_bottom(self):
769
815
  """
770
816
  Bottom of the panels in figure space
771
817
  """
772
- return self.to_figure_space(self.bottom_relative)
818
+ return self.to_figure_space(self.panel_bottom_relative)
773
819
 
774
820
  @property
775
821
  def plot_bottom(self):
@@ -892,14 +938,14 @@ class LayoutSpaces:
892
938
  """
893
939
  Width [figure dimensions] of panels
894
940
  """
895
- return self.r.right - self.l.left
941
+ return self.r.panel_right - self.l.panel_left
896
942
 
897
943
  @property
898
944
  def panel_height(self) -> float:
899
945
  """
900
946
  Height [figure dimensions] of panels
901
947
  """
902
- return self.t.top - self.b.bottom
948
+ return self.t.panel_top - self.b.panel_bottom
903
949
 
904
950
  @property
905
951
  def tag_width(self) -> float:
@@ -945,6 +991,24 @@ class LayoutSpaces:
945
991
  """
946
992
  return self.b.tag_height
947
993
 
994
+ @property
995
+ def left_axis_title_clearance(self) -> float:
996
+ """
997
+ Distance between the left y-axis title and the panel
998
+
999
+ In figure dimensions.
1000
+ """
1001
+ return self.l.axis_title_clearance
1002
+
1003
+ @property
1004
+ def bottom_axis_title_clearance(self) -> float:
1005
+ """
1006
+ Distance between the bottom x-axis title and the panel
1007
+
1008
+ In figure dimensions.
1009
+ """
1010
+ return self.b.axis_title_clearance
1011
+
948
1012
  def increase_horizontal_plot_margin(self, dw: float):
949
1013
  """
950
1014
  Increase the plot_margin to the right & left of the panels
@@ -981,8 +1045,8 @@ class LayoutSpaces:
981
1045
 
982
1046
  This is the area in which the panels are drawn.
983
1047
  """
984
- x1, x2 = self.l.left, self.r.right
985
- y1, y2 = self.b.bottom, self.t.top
1048
+ x1, x2 = self.l.panel_left, self.r.panel_right
1049
+ y1, y2 = self.b.panel_bottom, self.t.panel_top
986
1050
  return ((x1, y1), (x2, y2))
987
1051
 
988
1052
  def _calculate_panel_spacing(self) -> GridSpecParams:
@@ -1003,10 +1067,10 @@ class LayoutSpaces:
1003
1067
  raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}")
1004
1068
 
1005
1069
  return GridSpecParams(
1006
- self.l.left_relative,
1007
- self.r.right_relative,
1008
- self.t.top_relative,
1009
- self.b.bottom_relative,
1070
+ self.l.panel_left_relative,
1071
+ self.r.panel_right_relative,
1072
+ self.t.panel_top_relative,
1073
+ self.b.panel_bottom_relative,
1010
1074
  wspace,
1011
1075
  hspace,
1012
1076
  )
@@ -1020,6 +1084,9 @@ class LayoutSpaces:
1020
1084
  ncol = self.plot.facet.ncol
1021
1085
  nrow = self.plot.facet.nrow
1022
1086
 
1087
+ left, right = self.l.panel_left, self.r.panel_right
1088
+ top, bottom = self.t.panel_top, self.b.panel_bottom
1089
+
1023
1090
  # Both spacings are specified as fractions of the figure width
1024
1091
  # Multiply the vertical by (W/H) so that the gullies along both
1025
1092
  # directions are equally spaced.
@@ -1027,8 +1094,8 @@ class LayoutSpaces:
1027
1094
  self.sh = theme.getp("panel_spacing_y") * self.W / self.H
1028
1095
 
1029
1096
  # width and height of axes as fraction of figure width & height
1030
- self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
1031
- self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
1097
+ self.w = ((right - left) - self.sw * (ncol - 1)) / ncol
1098
+ self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow
1032
1099
 
1033
1100
  # Spacing as fraction of axes width & height
1034
1101
  wspace = self.sw / self.w
@@ -1045,6 +1112,9 @@ class LayoutSpaces:
1045
1112
  ncol = facet.ncol
1046
1113
  nrow = facet.nrow
1047
1114
 
1115
+ left, right = self.l.panel_left, self.r.panel_right
1116
+ top, bottom = self.t.panel_top, self.b.panel_bottom
1117
+
1048
1118
  # Both spacings are specified as fractions of the figure width
1049
1119
  self.sw = theme.getp("panel_spacing_x")
1050
1120
  self.sh = theme.getp("panel_spacing_y") * self.W / self.H
@@ -1073,8 +1143,8 @@ class LayoutSpaces:
1073
1143
  ) + self.items.axis_ticks_y_max_width_at("all")
1074
1144
 
1075
1145
  # width and height of axes as fraction of figure width & height
1076
- self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
1077
- self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
1146
+ self.w = ((right - left) - self.sw * (ncol - 1)) / ncol
1147
+ self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow
1078
1148
 
1079
1149
  # Spacing as fraction of axes width & height
1080
1150
  wspace = self.sw / self.w
@@ -1085,8 +1155,8 @@ class LayoutSpaces:
1085
1155
  """
1086
1156
  Calculate spacing parts for facet_null
1087
1157
  """
1088
- self.w = self.r.right - self.l.left
1089
- self.h = self.t.top - self.b.bottom
1158
+ self.w = self.r.panel_right - self.l.panel_left
1159
+ self.h = self.t.panel_top - self.b.panel_bottom
1090
1160
  self.sw = 0
1091
1161
  self.sh = 0
1092
1162
  return 0, 0
plotnine/_mpl/text.py CHANGED
@@ -32,7 +32,7 @@ class StripText(Text):
32
32
  "clip_on": False,
33
33
  "zorder": 3.3,
34
34
  # Since the text can be rotated, it is simpler to anchor it at
35
- # the center, align it then do the rotation. Vertically,
35
+ # the center, align it, then do the rotation. Vertically,
36
36
  # center_baseline places the text in the visual center, but
37
37
  # only if it is one line. For multiline text, we are better
38
38
  # off with plain center.
@@ -45,12 +45,6 @@ class StripText(Text):
45
45
  self.draw_info = info
46
46
  self.patch = StripTextPatch(self)
47
47
 
48
- # self.set_horizontalalignment("center")
49
- # self.set_verticalalignment(
50
- # "center_baseline" if info.is_oneline else "center"
51
- # )
52
- # self.set_rotation_mode("anchor")
53
-
54
48
  # TODO: This should really be part of the unit conversions in the
55
49
  # margin class.
56
50
  @lru_cache(2)
@@ -1194,3 +1194,16 @@ def va_as_float(va: VerticalJustification | float) -> float:
1194
1194
  "center_baseline": 0.5,
1195
1195
  }
1196
1196
  return lookup[va] if isinstance(va, str) else va
1197
+
1198
+
1199
+ def has_alpha_channel(c: str | tuple) -> bool:
1200
+ """
1201
+ Return True if c a color with an alpha value
1202
+
1203
+ Either a 9 character hex string e.g. #AABBCC88 or
1204
+ an RGBA tuple e.g. (.6, .7, .8, .5)
1205
+ """
1206
+ if isinstance(c, str):
1207
+ return c.startswith("#") and len(c) == 9
1208
+ else:
1209
+ return color_utils.is_color_tuple(c) and len(c) == 4
@@ -0,0 +1,11 @@
1
+ from ._arrange import Arrange
2
+ from ._beside import Beside
3
+ from ._plot_spacer import plot_spacer
4
+ from ._stack import Stack
5
+
6
+ __all__ = (
7
+ "Arrange",
8
+ "Stack",
9
+ "Beside",
10
+ "plot_spacer",
11
+ )
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  from copy import deepcopy
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
  from io import BytesIO
7
7
  from typing import TYPE_CHECKING
8
8
 
@@ -24,37 +24,57 @@ if TYPE_CHECKING:
24
24
  from plotnine.ggplot import PlotAddable, ggplot
25
25
 
26
26
 
27
- class Compose:
27
+ @dataclass
28
+ class Arrange:
28
29
  """
29
- Arrange two or more plots
30
+ Base class for those that create plot compositions
31
+
32
+ As a user, you will never directly work with this class, except
33
+ the operators [`|`](`plotnine.composition.Beside`) and
34
+ [`/`](`plotnine.composition.Stack`) that are powered by subclasses
35
+ of this class.
30
36
 
31
37
  Parameters
32
38
  ----------
33
39
  operands:
34
40
  The objects to be put together (composed).
41
+
42
+ See Also
43
+ --------
44
+ plotnine.composition.Beside : To arrange plots side by side
45
+ plotnine.composition.Stack : To arrange plots vertically
46
+ plotnine.composition.plot_spacer : To add a blank space between plots
35
47
  """
36
48
 
37
- def __init__(self, operands: list[ggplot | Compose]):
38
- self.operands = operands
49
+ operands: list[ggplot | Arrange]
39
50
 
40
- # These are created in the _create_figure method
41
- self.figure: Figure
42
- self.plotspecs: list[plotspec]
43
- self.gridspec: p9GridSpec
51
+ # These are created in the _create_figure method
52
+ figure: Figure = field(init=False, repr=False)
53
+ plotspecs: list[plotspec] = field(init=False, repr=False)
54
+ gridspec: p9GridSpec = field(init=False, repr=False)
55
+
56
+ def __post_init__(self):
57
+ # The way we handle the plots has consequences that would
58
+ # prevent having a duplicate plot in the composition.
59
+ # Using copies prevents this.
60
+ self.operands = [
61
+ op if isinstance(op, Arrange) else deepcopy(op)
62
+ for op in self.operands
63
+ ]
44
64
 
45
65
  @abc.abstractmethod
46
- def __or__(self, rhs: ggplot | Compose) -> Compose:
66
+ def __or__(self, rhs: ggplot | Arrange) -> Arrange:
47
67
  """
48
68
  Add rhs as a column
49
69
  """
50
70
 
51
71
  @abc.abstractmethod
52
- def __truediv__(self, rhs: ggplot | Compose) -> Compose:
72
+ def __truediv__(self, rhs: ggplot | Arrange) -> Arrange:
53
73
  """
54
74
  Add rhs as a row
55
75
  """
56
76
 
57
- def __add__(self, rhs: ggplot | Compose | PlotAddable) -> Compose:
77
+ def __add__(self, rhs: ggplot | Arrange | PlotAddable) -> Arrange:
58
78
  """
59
79
  Add rhs to the composition
60
80
 
@@ -65,15 +85,15 @@ class Compose:
65
85
  """
66
86
  from plotnine import ggplot
67
87
 
68
- if not isinstance(rhs, (ggplot, Compose)):
88
+ if not isinstance(rhs, (ggplot, Arrange)):
69
89
  cmp = deepcopy(self)
70
90
  cmp.last_plot = cmp.last_plot + rhs
71
91
  return cmp
72
92
  return self.__class__([*self, rhs])
73
93
 
74
- def __sub__(self, rhs: ggplot | Compose) -> Compose:
94
+ def __sub__(self, rhs: ggplot | Arrange) -> Arrange:
75
95
  """
76
- Add the rhs besides the composition
96
+ Add the rhs onto the composition
77
97
 
78
98
  Parameters
79
99
  ----------
@@ -82,7 +102,7 @@ class Compose:
82
102
  """
83
103
  return self.__class__([self, rhs])
84
104
 
85
- def __and__(self, rhs: PlotAddable) -> Compose:
105
+ def __and__(self, rhs: PlotAddable) -> Arrange:
86
106
  """
87
107
  Add rhs to all plots in the composition
88
108
 
@@ -93,9 +113,9 @@ class Compose:
93
113
  """
94
114
  self = deepcopy(self)
95
115
 
96
- def add_other(op: Compose):
116
+ def add_other(op: Arrange):
97
117
  for item in op:
98
- if isinstance(item, Compose):
118
+ if isinstance(item, Arrange):
99
119
  add_other(item)
100
120
  else:
101
121
  item += rhs
@@ -103,7 +123,7 @@ class Compose:
103
123
  add_other(self)
104
124
  return self
105
125
 
106
- def __mul__(self, rhs: PlotAddable) -> Compose:
126
+ def __mul__(self, rhs: PlotAddable) -> Arrange:
107
127
  """
108
128
  Add rhs to the outermost nesting level of the composition
109
129
 
@@ -127,7 +147,7 @@ class Compose:
127
147
  """
128
148
  return len(self.operands)
129
149
 
130
- def __iter__(self) -> Iterator[ggplot | Compose]:
150
+ def __iter__(self) -> Iterator[ggplot | Arrange]:
131
151
  """
132
152
  Return an iterable of all the operands
133
153
  """
@@ -227,7 +247,7 @@ class Compose:
227
247
  from plotnine._mpl.gridspec import p9GridSpec
228
248
 
229
249
  def _make_plotspecs(
230
- cmp: Compose, parent_gridspec: p9GridSpec | None
250
+ cmp: Arrange, parent_gridspec: p9GridSpec | None
231
251
  ) -> Generator[plotspec]:
232
252
  """
233
253
  Return the plot specification for each subplot in the composition
@@ -289,7 +309,7 @@ class Compose:
289
309
 
290
310
  def draw(self, *, show: bool = False) -> Figure:
291
311
  """
292
- Render the composed plots
312
+ Render the arranged plots
293
313
 
294
314
  Parameters
295
315
  ----------
@@ -315,9 +335,15 @@ class Compose:
315
335
  )
316
336
  return figure
317
337
 
318
- def save(self, filename: str | Path | BytesIO, format: str | None = None):
338
+ def save(
339
+ self,
340
+ filename: str | Path | BytesIO,
341
+ format: str | None = None,
342
+ dpi: int | None = None,
343
+ **kwargs,
344
+ ):
319
345
  """
320
- Save a Compose object as an image file
346
+ Save a composition as an image file
321
347
 
322
348
  Parameters
323
349
  ----------
@@ -326,14 +352,25 @@ class Compose:
326
352
  format :
327
353
  Image format to use, automatically extract from
328
354
  file name extension.
329
- """
330
- figure = self.draw()
355
+ dpi :
356
+ DPI to use for raster graphics. If None, defaults to using
357
+ the `dpi` of theme to the first plot.
358
+ kwargs :
359
+ These are ignored. Here to "softly" match the API of
360
+ `ggplot.save()`.
361
+ """
362
+ from plotnine import theme
363
+
364
+ # To set the dpi, we only need to change the dpi of
365
+ # the last plot and theme gets added to the last plot
366
+ plot = (self + theme(dpi=dpi)) if dpi else self
367
+ figure = plot.draw()
331
368
  figure.savefig(filename, format=format)
332
369
 
333
370
 
334
371
  @dataclass
335
372
  class plot_composition_context:
336
- cmp: Compose
373
+ cmp: Arrange
337
374
  show: bool
338
375
 
339
376
  def __post_init__(self):
@@ -368,95 +405,3 @@ class plot_composition_context:
368
405
  plt.close(self.cmp.figure)
369
406
 
370
407
  self._rc_context.__exit__(exc_type, exc_value, exc_traceback)
371
-
372
-
373
- class OR(Compose):
374
- """
375
- Compose by adding a column
376
- """
377
-
378
- @property
379
- def nrow(self) -> int:
380
- return 1
381
-
382
- @property
383
- def ncol(self) -> int:
384
- return len(self)
385
-
386
- def __or__(self, rhs: ggplot | Compose) -> Compose:
387
- """
388
- Add rhs as a column
389
- """
390
- # This is adjacent or i.e. (OR | rhs) so we collapse the
391
- # operands into a single operation
392
- return OR([*self, rhs])
393
-
394
- def __truediv__(self, rhs: ggplot | Compose) -> Compose:
395
- """
396
- Add rhs as a row
397
- """
398
- return DIV([self, rhs])
399
-
400
-
401
- class DIV(Compose):
402
- """
403
- Compose by adding a row
404
- """
405
-
406
- @property
407
- def nrow(self) -> int:
408
- return len(self)
409
-
410
- @property
411
- def ncol(self) -> int:
412
- return 1
413
-
414
- def __truediv__(self, rhs: ggplot | Compose) -> Compose:
415
- """
416
- Add rhs as a row
417
- """
418
- # This is an adjacent div i.e. (DIV | rhs) so we collapse the
419
- # operands into a single operation
420
- return DIV([*self, rhs])
421
-
422
- def __or__(self, rhs: ggplot | Compose) -> Compose:
423
- """
424
- Add rhs as a column
425
- """
426
- return OR([self, rhs])
427
-
428
-
429
- class ADD(Compose):
430
- """
431
- Compose by adding
432
- """
433
-
434
- @property
435
- def nrow(self) -> int:
436
- from plotnine.facets.facet_wrap import wrap_dims
437
-
438
- return wrap_dims(len(self))[0]
439
-
440
- @property
441
- def ncol(self) -> int:
442
- from plotnine.facets.facet_wrap import wrap_dims
443
-
444
- return wrap_dims(len(self))[1]
445
-
446
- def __or__(self, rhs: ggplot | Compose) -> Compose:
447
- """
448
- Add rhs as a column
449
- """
450
- return OR([self, rhs])
451
-
452
- def __truediv__(self, rhs: ggplot | Compose) -> Compose:
453
- """
454
- Add rhs as a row
455
- """
456
- return DIV([self, rhs])
457
-
458
- def __sub__(self, rhs: ggplot | Compose) -> Compose:
459
- """
460
- Add rhs as a column
461
- """
462
- return OR([self, rhs])
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ._arrange import Arrange
7
+
8
+ if TYPE_CHECKING:
9
+ from plotnine.ggplot import ggplot
10
+
11
+
12
+ @dataclass
13
+ class Beside(Arrange):
14
+ """
15
+ Place plots or compositions side by side
16
+
17
+ **Usage**
18
+
19
+ plot | plot
20
+ plot | composition
21
+ composition | plot
22
+ composition | composition
23
+
24
+ Typically, you will use this class through the `|` operator.
25
+
26
+ See Also
27
+ --------
28
+ plotnine.composition.Stack : To arrange plots vertically
29
+ plotnine.composition.plot_spacer : To add a blank space between plots
30
+ """
31
+
32
+ @property
33
+ def nrow(self) -> int:
34
+ return 1
35
+
36
+ @property
37
+ def ncol(self) -> int:
38
+ return len(self)
39
+
40
+ def __or__(self, rhs: ggplot | Arrange) -> Arrange:
41
+ """
42
+ Add rhs as a column
43
+ """
44
+ # This is adjacent or i.e. (OR | rhs) so we collapse the
45
+ # operands into a single operation
46
+ return Beside([*self, rhs])
47
+
48
+ def __truediv__(self, rhs: ggplot | Arrange) -> Arrange:
49
+ """
50
+ Add rhs as a row
51
+ """
52
+ from ._stack import Stack
53
+
54
+ return Stack([self, rhs])