plotnine 0.14.4__py3-none-any.whl → 0.15.0.dev1__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 (62) hide show
  1. plotnine/__init__.py +31 -37
  2. plotnine/_mpl/gridspec.py +265 -0
  3. plotnine/_mpl/layout_manager/__init__.py +6 -0
  4. plotnine/_mpl/layout_manager/_engine.py +87 -0
  5. plotnine/_mpl/layout_manager/_layout_items.py +775 -0
  6. plotnine/_mpl/layout_manager/_layout_tree.py +625 -0
  7. plotnine/_mpl/layout_manager/_spaces.py +1007 -0
  8. plotnine/_mpl/utils.py +78 -10
  9. plotnine/_utils/__init__.py +4 -4
  10. plotnine/_utils/dev.py +45 -27
  11. plotnine/animation.py +1 -1
  12. plotnine/coords/coord_trans.py +1 -1
  13. plotnine/data/__init__.py +12 -8
  14. plotnine/doctools.py +1 -1
  15. plotnine/facets/facet.py +30 -39
  16. plotnine/facets/facet_grid.py +14 -6
  17. plotnine/facets/facet_wrap.py +3 -5
  18. plotnine/facets/strips.py +2 -7
  19. plotnine/geoms/geom_crossbar.py +2 -3
  20. plotnine/geoms/geom_path.py +1 -1
  21. plotnine/geoms/geom_text.py +3 -1
  22. plotnine/ggplot.py +94 -65
  23. plotnine/guides/guide.py +10 -8
  24. plotnine/guides/guide_colorbar.py +3 -3
  25. plotnine/guides/guide_legend.py +5 -5
  26. plotnine/guides/guides.py +3 -3
  27. plotnine/iapi.py +1 -0
  28. plotnine/labels.py +5 -0
  29. plotnine/options.py +14 -7
  30. plotnine/plot_composition/__init__.py +10 -0
  31. plotnine/plot_composition/_compose.py +427 -0
  32. plotnine/plot_composition/_plotspec.py +50 -0
  33. plotnine/plot_composition/_spacer.py +32 -0
  34. plotnine/positions/position_dodge.py +1 -1
  35. plotnine/positions/position_dodge2.py +1 -1
  36. plotnine/positions/position_stack.py +1 -2
  37. plotnine/qplot.py +1 -2
  38. plotnine/scales/__init__.py +0 -6
  39. plotnine/scales/scale.py +1 -1
  40. plotnine/stats/binning.py +1 -1
  41. plotnine/stats/smoothers.py +3 -5
  42. plotnine/stats/stat_density.py +1 -1
  43. plotnine/stats/stat_qq_line.py +1 -1
  44. plotnine/stats/stat_sina.py +1 -1
  45. plotnine/themes/elements/__init__.py +2 -0
  46. plotnine/themes/elements/element_text.py +34 -24
  47. plotnine/themes/elements/margin.py +73 -60
  48. plotnine/themes/targets.py +2 -0
  49. plotnine/themes/theme.py +13 -7
  50. plotnine/themes/theme_gray.py +27 -31
  51. plotnine/themes/theme_matplotlib.py +25 -28
  52. plotnine/themes/theme_seaborn.py +31 -34
  53. plotnine/themes/theme_void.py +17 -26
  54. plotnine/themes/themeable.py +286 -153
  55. {plotnine-0.14.4.dist-info → plotnine-0.15.0.dev1.dist-info}/METADATA +4 -3
  56. {plotnine-0.14.4.dist-info → plotnine-0.15.0.dev1.dist-info}/RECORD +59 -52
  57. {plotnine-0.14.4.dist-info → plotnine-0.15.0.dev1.dist-info}/WHEEL +1 -1
  58. plotnine/_mpl/_plot_side_space.py +0 -888
  59. plotnine/_mpl/_plotnine_tight_layout.py +0 -293
  60. plotnine/_mpl/layout_engine.py +0 -110
  61. {plotnine-0.14.4.dist-info → plotnine-0.15.0.dev1.dist-info/licenses}/LICENSE +0 -0
  62. {plotnine-0.14.4.dist-info → plotnine-0.15.0.dev1.dist-info}/top_level.txt +0 -0
plotnine/ggplot.py CHANGED
@@ -6,7 +6,15 @@ from io import BytesIO
6
6
  from itertools import chain
7
7
  from pathlib import Path
8
8
  from types import SimpleNamespace as NS
9
- from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, cast
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Dict,
13
+ Iterable,
14
+ Optional,
15
+ cast,
16
+ overload,
17
+ )
10
18
  from warnings import warn
11
19
 
12
20
  from ._utils import (
@@ -44,9 +52,11 @@ if TYPE_CHECKING:
44
52
  from typing_extensions import Self
45
53
 
46
54
  from plotnine import watermark
55
+ from plotnine._mpl.gridspec import p9GridSpec
47
56
  from plotnine.coords.coord import coord
48
57
  from plotnine.facets.facet import facet
49
58
  from plotnine.layer import layer
59
+ from plotnine.plot_composition import Compose
50
60
  from plotnine.typing import DataLike
51
61
 
52
62
  class PlotAddable(Protocol):
@@ -95,9 +105,7 @@ class ggplot:
95
105
 
96
106
  figure: Figure
97
107
  axs: list[Axes]
98
- theme: theme
99
- facet: facet
100
- coordinates: coord
108
+ _gridspec: p9GridSpec
101
109
 
102
110
  def __init__(
103
111
  self,
@@ -110,7 +118,7 @@ class ggplot:
110
118
  data, mapping = order_as_data_mapping(data, mapping)
111
119
  self.data = data
112
120
  self.mapping = mapping if mapping is not None else aes()
113
- self.facet = facet_null()
121
+ self.facet: facet = facet_null()
114
122
  self.labels = make_labels(self.mapping)
115
123
  self.layers = Layers()
116
124
  self.guides = guides()
@@ -147,6 +155,11 @@ class ggplot:
147
155
  Users should prefer this method instead of printing or repring
148
156
  the object.
149
157
  """
158
+ # Prevent against any modifications to the users
159
+ # ggplot object. Do the copy here as we may/may not
160
+ # assign a default theme
161
+ self = deepcopy(self)
162
+
150
163
  if is_inline_backend() or is_quarto_environment():
151
164
  # Take charge of the display because we have to make
152
165
  # adjustments for retina output.
@@ -167,18 +180,15 @@ class ggplot:
167
180
  format = get_option("figure_format") or ip.config.InlineBackend.get(
168
181
  "figure_format", "retina"
169
182
  )
170
- save_format = format
171
-
172
183
  # While jpegs can be displayed as retina, we restrict the output
173
184
  # of "retina" to png
174
185
  if format == "retina":
175
186
  self = copy(self)
176
187
  self.theme = self.theme.to_retina()
177
- save_format = "png"
178
188
 
179
- figure_size_px = self.theme._figure_size_px
180
189
  buf = BytesIO()
181
- self.save(buf, format=save_format, verbose=False)
190
+ self.save(buf, "png" if format == "retina" else format, verbose=False)
191
+ figure_size_px = self.theme._figure_size_px
182
192
  display_func = get_display_function(format, figure_size_px)
183
193
  display_func(buf.getvalue())
184
194
 
@@ -193,7 +203,7 @@ class ggplot:
193
203
  new = result.__dict__
194
204
 
195
205
  # don't make a deepcopy of data
196
- shallow = {"data", "figure", "_build_objs"}
206
+ shallow = {"data", "figure", "gs", "_build_objs"}
197
207
  for key, item in old.items():
198
208
  if key in shallow:
199
209
  new[key] = item
@@ -220,9 +230,20 @@ class ggplot:
220
230
  other.__radd__(self)
221
231
  return self
222
232
 
223
- def __add__(self, other: PlotAddable | list[PlotAddable] | None) -> ggplot:
233
+ @overload
234
+ def __add__(
235
+ self, rhs: PlotAddable | list[PlotAddable] | None
236
+ ) -> ggplot: ...
237
+
238
+ @overload
239
+ def __add__(self, rhs: ggplot | Compose) -> Compose: ...
240
+
241
+ def __add__(
242
+ self,
243
+ rhs: PlotAddable | list[PlotAddable] | None | ggplot | Compose,
244
+ ) -> ggplot | Compose:
224
245
  """
225
- Add to ggplot from a list
246
+ Add to ggplot
226
247
 
227
248
  Parameters
228
249
  ----------
@@ -230,8 +251,37 @@ class ggplot:
230
251
  Either an object that knows how to "radd"
231
252
  itself to a ggplot, or a list of such objects.
232
253
  """
254
+ from .plot_composition import ADD, Compose
255
+
256
+ if isinstance(rhs, (ggplot, Compose)):
257
+ return ADD([self, rhs])
258
+
233
259
  self = deepcopy(self)
234
- return self.__iadd__(other)
260
+ return self.__iadd__(rhs)
261
+
262
+ def __or__(self, rhs: ggplot | Compose) -> Compose:
263
+ """
264
+ Compose 2 plots columnwise
265
+ """
266
+ from .plot_composition import OR
267
+
268
+ return OR([self, rhs])
269
+
270
+ def __truediv__(self, rhs: ggplot | Compose) -> Compose:
271
+ """
272
+ Compose 2 plots rowwise
273
+ """
274
+ from .plot_composition import DIV
275
+
276
+ return DIV([self, rhs])
277
+
278
+ def __sub__(self, rhs: ggplot | Compose) -> Compose:
279
+ """
280
+ Compose 2 plots columnwise
281
+ """
282
+ from .plot_composition import OR
283
+
284
+ return OR([self, rhs])
235
285
 
236
286
  def __rrshift__(self, other: DataLike) -> ggplot:
237
287
  """
@@ -248,7 +298,7 @@ class ggplot:
248
298
  raise TypeError(msg.format(type(other)))
249
299
  return self
250
300
 
251
- def draw(self, show: bool = False) -> Figure:
301
+ def draw(self, *, show: bool = False) -> Figure:
252
302
  """
253
303
  Render the complete plot
254
304
 
@@ -262,23 +312,17 @@ class ggplot:
262
312
  :
263
313
  Matplotlib figure
264
314
  """
265
- from ._mpl.layout_engine import PlotnineLayoutEngine
266
-
267
- # Do not draw if drawn already.
268
- # This prevents a needless error when reusing
269
- # figure & axes in the jupyter notebook.
270
- if hasattr(self, "figure"):
271
- return self.figure
315
+ from ._mpl.layout_manager import PlotnineLayoutEngine
272
316
 
273
- # Prevent against any modifications to the users
274
- # ggplot object. Do the copy here as we may/may not
275
- # assign a default theme
276
- self = deepcopy(self)
277
317
  with plot_context(self, show=show):
318
+ if not hasattr(self, "figure"):
319
+ self._create_figure()
320
+ figure = self.figure
321
+
278
322
  self._build()
279
323
 
280
324
  # setup
281
- self.figure, self.axs = self.facet.setup(self)
325
+ self.axs = self.facet.setup(self)
282
326
  self.guides._setup(self)
283
327
  self.theme.setup(self)
284
328
 
@@ -289,51 +333,24 @@ class ggplot:
289
333
  self.guides.draw()
290
334
  self._draw_figure_texts()
291
335
  self._draw_watermarks()
336
+ self._draw_figure_background()
292
337
 
293
338
  # Artist object theming
294
339
  self.theme.apply()
295
- self.figure.set_layout_engine(PlotnineLayoutEngine(self))
340
+ figure.set_layout_engine(PlotnineLayoutEngine(self))
296
341
 
297
- return self.figure
342
+ return figure
298
343
 
299
- def _draw_using_figure(self, figure: Figure, axs: list[Axes]) -> ggplot:
344
+ def _create_figure(self):
300
345
  """
301
- Draw onto already created figure and axes
302
-
303
- This is can be used to draw animation frames,
304
- or inset plots. It is intended to be used
305
- after the key plot has been drawn.
306
-
307
- Parameters
308
- ----------
309
- figure :
310
- Matplotlib figure
311
- axs :
312
- Array of Axes onto which to draw the plots
346
+ Create gridspec for the panels
313
347
  """
314
- from ._mpl.layout_engine import PlotnineLayoutEngine
348
+ import matplotlib.pyplot as plt
315
349
 
316
- self = deepcopy(self)
317
- self.figure = figure
318
- self.axs = axs
319
- with plot_context(self):
320
- self._build()
350
+ from ._mpl.gridspec import p9GridSpec
321
351
 
322
- # setup
323
- self.figure, self.axs = self.facet.setup(self)
324
- self.guides._setup(self)
325
- self.theme.setup(self)
326
-
327
- # drawing
328
- self._draw_layers()
329
- self._draw_breaks_and_labels()
330
- self.guides.draw()
331
-
332
- # artist theming
333
- self.theme.apply()
334
- self.figure.set_layout_engine(PlotnineLayoutEngine(self))
335
-
336
- return self
352
+ self.figure = plt.figure()
353
+ self._gridspec = p9GridSpec(1, 1, self.figure)
337
354
 
338
355
  def _build(self):
339
356
  """
@@ -491,6 +508,7 @@ class ggplot:
491
508
  title = self.labels.get("title", "")
492
509
  subtitle = self.labels.get("subtitle", "")
493
510
  caption = self.labels.get("caption", "")
511
+ tag = self.labels.get("tag", "")
494
512
 
495
513
  # Get the axis labels (default or specified by user)
496
514
  # and let the coordinate modify them e.g. flip
@@ -508,6 +526,9 @@ class ggplot:
508
526
  if caption:
509
527
  targets.plot_caption = figure.text(0, 0, caption)
510
528
 
529
+ if tag:
530
+ targets.plot_tag = figure.text(0, 0, tag)
531
+
511
532
  if labels.x:
512
533
  targets.axis_title_x = figure.text(0, 0, labels.x)
513
534
 
@@ -521,6 +542,14 @@ class ggplot:
521
542
  for wm in self.watermarks:
522
543
  wm.draw(self.figure)
523
544
 
545
+ def _draw_figure_background(self):
546
+ from matplotlib.patches import Rectangle
547
+
548
+ rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000)
549
+ self.figure.add_artist(rect)
550
+ self._gridspec.patch = rect
551
+ self.theme.targets.plot_background = rect
552
+
524
553
  def _save_filename(self, ext: str) -> Path:
525
554
  """
526
555
  Make a filename for use by the save method
@@ -572,7 +601,7 @@ class ggplot:
572
601
  fig_kwargs: Dict[str, Any] = {"format": format, **kwargs}
573
602
 
574
603
  if limitsize is None:
575
- limitsize = cast(bool, get_option("limitsize"))
604
+ limitsize = cast("bool", get_option("limitsize"))
576
605
 
577
606
  # filename, depends on the object
578
607
  if filename is None:
@@ -598,7 +627,7 @@ class ggplot:
598
627
  raise PlotnineError("You must specify both width and height")
599
628
  else:
600
629
  width, height = cast(
601
- tuple[float, float], self.theme.getp("figure_size")
630
+ "tuple[float, float]", self.theme.getp("figure_size")
602
631
  )
603
632
 
604
633
  if limitsize and (width > 25 or height > 25):
plotnine/guides/guide.py CHANGED
@@ -76,7 +76,7 @@ class guide(ABC, metaclass=Register):
76
76
  self.plot_layers: Layers
77
77
  self.plot_mapping: aes
78
78
  self._elements_cls = GuideElements
79
- self.elements = cast(GuideElements, None)
79
+ self.elements = cast("GuideElements", None)
80
80
  self.guides_elements: GuidesElements
81
81
 
82
82
  def legend_aesthetics(self, layer):
@@ -132,14 +132,14 @@ class guide(ABC, metaclass=Register):
132
132
  pos = self.elements.position
133
133
  just_view = asdict(self.guides_elements.justification)
134
134
  if isinstance(pos, str):
135
- just = cast(float, just_view[pos])
135
+ just = cast("float", just_view[pos])
136
136
  return (pos, just)
137
137
  else:
138
138
  # If no justification is given for an inside legend,
139
139
  # we use the position of the legend
140
140
  if (just := just_view["inside"]) is None:
141
141
  just = pos
142
- just = cast(tuple[float, float], just)
142
+ just = cast("tuple[float, float]", just)
143
143
  return (pos, just)
144
144
 
145
145
  def train(
@@ -191,9 +191,9 @@ class GuideElements:
191
191
  def title(self):
192
192
  ha = self.theme.getp(("legend_title", "ha"))
193
193
  va = self.theme.getp(("legend_title", "va"), "center")
194
- _margin = self.theme.getp(("legend_title", "margin"))
194
+ _margin = self.theme.getp(("legend_title", "margin")).pt
195
195
  _loc = get_opposite_side(self.title_position)[0]
196
- margin = _margin.get_as(_loc, "pt") if _margin else 0
196
+ margin = getattr(_margin, _loc)
197
197
  top_or_bottom = self.title_position in ("top", "bottom")
198
198
  is_blank = self.theme.T.is_blank("legend_title")
199
199
 
@@ -218,9 +218,11 @@ class GuideElements:
218
218
 
219
219
  @cached_property
220
220
  def _text_margin(self) -> float:
221
- _margin = self.theme.getp((f"legend_text_{self.guide_kind}", "margin"))
222
- _loc = get_opposite_side(self.text_position)
223
- return _margin.get_as(_loc[0], "pt") if _margin else 0
221
+ _margin = self.theme.getp(
222
+ (f"legend_text_{self.guide_kind}", "margin")
223
+ ).pt
224
+ _loc = get_opposite_side(self.text_position)[0]
225
+ return getattr(_margin, _loc)
224
226
 
225
227
  @cached_property
226
228
  def title_position(self) -> SidePosition:
@@ -75,8 +75,8 @@ class guide_colorbar(guide):
75
75
  self.nbin = 300 # if self.display == "gradient" else 300
76
76
 
77
77
  def train(self, scale: scale, aesthetic=None):
78
- self.nbin = cast(int, self.nbin)
79
- self.title = cast(str, self.title)
78
+ self.nbin = cast("int", self.nbin)
79
+ self.title = cast("str", self.title)
80
80
 
81
81
  if not isinstance(scale, scale_continuous):
82
82
  warn("colorbar guide needs continuous scales", PlotnineWarning)
@@ -213,7 +213,7 @@ class guide_colorbar(guide):
213
213
  auxbox = DPICorAuxTransformBox(IdentityTransform())
214
214
 
215
215
  # title
216
- title = cast(str, self.title)
216
+ title = cast("str", self.title)
217
217
  props = {"ha": elements.title.ha, "va": elements.title.va}
218
218
  title_box = TextArea(title, textprops=props)
219
219
  targets.legend_title = title_box._text # type: ignore
@@ -226,10 +226,10 @@ class guide_legend(guide):
226
226
  ncol = int(np.ceil(nbreak / 15))
227
227
 
228
228
  if nrow is None:
229
- ncol = cast(int, ncol)
229
+ ncol = cast("int", ncol)
230
230
  nrow = int(np.ceil(nbreak / ncol))
231
231
  elif ncol is None:
232
- nrow = cast(int, nrow)
232
+ nrow = cast("int", nrow)
233
233
  ncol = int(np.ceil(nbreak / nrow))
234
234
 
235
235
  return nrow, ncol
@@ -255,7 +255,7 @@ class guide_legend(guide):
255
255
  elements = self.elements
256
256
 
257
257
  # title
258
- title = cast(str, self.title)
258
+ title = cast("str", self.title)
259
259
  title_box = TextArea(title)
260
260
  targets.legend_title = title_box._text # type: ignore
261
261
 
@@ -403,7 +403,7 @@ class GuideElementsLegend(GuideElements):
403
403
  dimensions are big enough.
404
404
  """
405
405
  # Note the different height sizes for the entries
406
- guide = cast(guide_legend, self.guide)
406
+ guide = cast("guide_legend", self.guide)
407
407
  min_size = (
408
408
  self.theme.getp("legend_key_width"),
409
409
  self.theme.getp("legend_key_height"),
@@ -452,7 +452,7 @@ class GuideElementsLegend(GuideElements):
452
452
  If legend is horizontal, then key heights must be equal, so we
453
453
  use the maximum
454
454
  """
455
- hs = [h for h, _ in self._key_dimensions]
455
+ hs = [h for _, h in self._key_dimensions]
456
456
  if self.is_horizontal:
457
457
  return [max(hs)] * len(hs)
458
458
  return hs
plotnine/guides/guides.py CHANGED
@@ -342,8 +342,8 @@ class guides:
342
342
  if isinstance(position, str) and isinstance(just, (float, int)):
343
343
  setattr(legends, position, outside_legend(aob, just))
344
344
  else:
345
- position = cast(tuple[float, float], position)
346
- just = cast(tuple[float, float], just)
345
+ position = cast("tuple[float, float]", position)
346
+ just = cast("tuple[float, float]", just)
347
347
  legends.inside.append(inside_legend(aob, just, position))
348
348
 
349
349
  return legends
@@ -467,7 +467,7 @@ class GuidesElements:
467
467
  if just is None:
468
468
  just = (0.5, 0.5)
469
469
  elif just in VALID_JUSTIFICATION_WORDS:
470
- just = ensure_xy_location(just) # pyright: ignore[reportArgumentType]
470
+ just = ensure_xy_location(just)
471
471
  elif isinstance(just, (float, int)):
472
472
  just = (just, just)
473
473
  return just[idx]
plotnine/iapi.py CHANGED
@@ -76,6 +76,7 @@ class labels_view:
76
76
  title: Optional[str] = None
77
77
  caption: Optional[str] = None
78
78
  subtitle: Optional[str] = None
79
+ tag: Optional[str] = None
79
80
 
80
81
  def update(self, other: labels_view):
81
82
  """
plotnine/labels.py CHANGED
@@ -90,6 +90,11 @@ class labs:
90
90
  The caption at the bottom of the plot.
91
91
  """
92
92
 
93
+ tag: str | None = None
94
+ """
95
+ A plot tag
96
+ """
97
+
93
98
  def __post_init__(self):
94
99
  kwargs: dict[str, str] = {
95
100
  f.name: value
plotnine/options.py CHANGED
@@ -10,14 +10,11 @@ if TYPE_CHECKING:
10
10
  from plotnine import theme
11
11
  from plotnine.typing import FigureFormat
12
12
 
13
- close_all_figures = False
14
- """
15
- Development flag, e.g. set to `True` to prevent
16
- the queuing up of figures when errors happen.
17
- """
18
-
19
13
  current_theme: Optional[theme | Type[theme]] = None
20
- """Theme used when none is added to the ggplot object"""
14
+ """Theme used when none is added to the ggplot object
15
+
16
+ Another way to do it, to set a default theme using `theme_set()`.
17
+ """
21
18
 
22
19
  base_family: str = "sans-serif"
23
20
  """
@@ -77,6 +74,11 @@ def get_option(name: str) -> Any:
77
74
  ----------
78
75
  name :
79
76
  Name of the option
77
+
78
+ Notes
79
+ -----
80
+ See [reference](/reference/#options) for a list of all the available
81
+ options.
80
82
  """
81
83
  d = globals()
82
84
 
@@ -103,6 +105,11 @@ def set_option(name: str, value: Any) -> Any:
103
105
  -------
104
106
  :
105
107
  Old value of the option
108
+
109
+ Notes
110
+ -----
111
+ See [reference](/reference/#options) for a list of all the available
112
+ options.
106
113
  """
107
114
  d = globals()
108
115
 
@@ -0,0 +1,10 @@
1
+ from ._compose import ADD, DIV, OR, Compose
2
+ from ._spacer import spacer
3
+
4
+ __all__ = (
5
+ "Compose",
6
+ "ADD",
7
+ "DIV",
8
+ "OR",
9
+ "spacer",
10
+ )