ggh4x-python 0.3.1.9000__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. ggh4x/__init__.py +140 -0
  2. ggh4x/_aimed_text_grob.py +432 -0
  3. ggh4x/_borrowed_ggplot2.py +273 -0
  4. ggh4x/_cli.py +84 -0
  5. ggh4x/_datasets.py +106 -0
  6. ggh4x/_download.py +111 -0
  7. ggh4x/_facet_helpers.py +313 -0
  8. ggh4x/_facet_utils.py +649 -0
  9. ggh4x/_gap_grobs.py +606 -0
  10. ggh4x/_registry.py +10 -0
  11. ggh4x/_rlang.py +93 -0
  12. ggh4x/_utils.py +150 -0
  13. ggh4x/_vctrs.py +233 -0
  14. ggh4x/conveniences.py +601 -0
  15. ggh4x/coord_axes_inside.py +380 -0
  16. ggh4x/element_part_rect.py +545 -0
  17. ggh4x/facet_grid2.py +1018 -0
  18. ggh4x/facet_manual.py +901 -0
  19. ggh4x/facet_nested.py +776 -0
  20. ggh4x/facet_nested_wrap.py +193 -0
  21. ggh4x/facet_wrap2.py +896 -0
  22. ggh4x/geom_box.py +536 -0
  23. ggh4x/geom_outline_point.py +444 -0
  24. ggh4x/geom_pointpath.py +259 -0
  25. ggh4x/geom_polygonraster.py +252 -0
  26. ggh4x/geom_rectrug.py +489 -0
  27. ggh4x/geom_text_aimed.py +279 -0
  28. ggh4x/guide_stringlegend.py +354 -0
  29. ggh4x/help_secondary.py +549 -0
  30. ggh4x/multiscale/__init__.py +51 -0
  31. ggh4x/multiscale/_multiscale_add.py +207 -0
  32. ggh4x/multiscale/scale_listed.py +167 -0
  33. ggh4x/multiscale/scale_manual.py +478 -0
  34. ggh4x/multiscale/scale_multi.py +393 -0
  35. ggh4x/panel_scales/__init__.py +58 -0
  36. ggh4x/panel_scales/at_panel.py +115 -0
  37. ggh4x/panel_scales/facetted_pos_scales.py +647 -0
  38. ggh4x/panel_scales/force_panelsize.py +411 -0
  39. ggh4x/panel_scales/scale_facet.py +222 -0
  40. ggh4x/position_disjoint_ranges.py +229 -0
  41. ggh4x/position_lineartrans.py +242 -0
  42. ggh4x/py.typed +0 -0
  43. ggh4x/resources/faithful.csv +273 -0
  44. ggh4x/resources/iris.csv +151 -0
  45. ggh4x/resources/mtcars.csv +33 -0
  46. ggh4x/resources/pressure.csv +20 -0
  47. ggh4x/resources/volcano.csv +87 -0
  48. ggh4x/save.py +255 -0
  49. ggh4x/stat_difference.py +388 -0
  50. ggh4x/stat_funxy.py +436 -0
  51. ggh4x/stat_rle.py +290 -0
  52. ggh4x/stat_rollingkernel.py +369 -0
  53. ggh4x/stat_theodensity.py +681 -0
  54. ggh4x/strip_nested.py +448 -0
  55. ggh4x/strip_split.py +687 -0
  56. ggh4x/strip_tag.py +636 -0
  57. ggh4x/strip_themed.py +232 -0
  58. ggh4x/strip_vanilla.py +1464 -0
  59. ggh4x/themes.py +31 -0
  60. ggh4x/themes_ggh4x.py +67 -0
  61. ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
  62. ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
  63. ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
  64. ggh4x_python-0.3.1.9000.dist-info/licenses/LICENSE +3 -0
@@ -0,0 +1,411 @@
1
+ """Force facet panel sizes (port of ggh4x ``R/force_panelsize.R``).
2
+
3
+ :func:`force_panelsizes` returns a :class:`ForcedSize` add-on object. When added
4
+ to a plot with ``+``, its handler (registered on
5
+ :func:`ggplot2_py.plot.update_ggplot`) clones the plot's live facet into a
6
+ dynamic ``Forced<FacetClass>`` subclass whose ``draw_panels`` calls the original
7
+ facet's ``draw_panels`` and then overwrites the resulting gtable's panel row
8
+ heights / column widths with the user-forced ``"null"`` units (and sets
9
+ ``respect``).
10
+
11
+ This mirrors R, where ``ggplot_add.forcedsize`` wraps ``old.facet$draw_panels``
12
+ in a new function that mutates the panel gtable's ``widths`` / ``heights`` after
13
+ the parent produced it. Because the rewrite happens *inside* ``draw_panels``
14
+ (post-parent), it overrules the theme ``panel.widths`` / ``panel.heights``
15
+ applied by ``Facet.set_panel_size`` afterwards, exactly as the R ``space``
16
+ argument is overruled.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from ggplot2_py import ggproto
24
+ from ggplot2_py.ggproto import ggproto_parent
25
+ from ggplot2_py.plot import update_ggplot
26
+ from grid_py import Unit, convert_height, convert_width, is_unit, unit_type
27
+
28
+ from ggh4x._facet_utils import panel_cols, panel_rows
29
+
30
+ __all__ = [
31
+ "force_panelsizes",
32
+ "ForcedSize",
33
+ "is_null_unit",
34
+ ]
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # is_null_unit (R force_panelsize.R:197-202)
39
+ # ---------------------------------------------------------------------------
40
+ def is_null_unit(x: Any) -> bool:
41
+ """Return whether *x* is a unit composed entirely of ``"null"`` units.
42
+
43
+ Faithful port of ggh4x's ``is_null_unit`` (``R/force_panelsize.R:197-202``):
44
+ a non-unit returns ``False``; otherwise every element's :func:`unit_type`
45
+ must equal ``"null"``.
46
+
47
+ Parameters
48
+ ----------
49
+ x : Any
50
+ Candidate object.
51
+
52
+ Returns
53
+ -------
54
+ bool
55
+ ``True`` when *x* is a unit whose every element is ``"null"``.
56
+ """
57
+ if not is_unit(x):
58
+ return False
59
+ types = unit_type(x)
60
+ if isinstance(types, str):
61
+ types = [types]
62
+ return all(t == "null" for t in types)
63
+
64
+
65
+ _ABSOLUTE_TOTAL_UNITS = ("cm", "mm", "inches", "points", "bigpts")
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # ForcedSize container (R: structure(list(...), class = "forcedsize"))
70
+ # ---------------------------------------------------------------------------
71
+ class ForcedSize:
72
+ """Deferred container of forced panel sizes (R ``forcedsize`` S3 object).
73
+
74
+ Holds the five fields produced by :func:`force_panelsizes`. It is *not* a
75
+ facet; it is consumed by :func:`_update_forcedsize` at ``+``-time, which
76
+ rewrites the plot's facet.
77
+
78
+ Attributes
79
+ ----------
80
+ rows, cols : grid_py.Unit or None
81
+ Forced panel heights (rows) / widths (cols), as ``"null"`` units (or
82
+ absolute when supplied directly).
83
+ respect : bool or None
84
+ Forced ``respect`` flag for the panel gtable, or ``None`` to inherit.
85
+ total_width, total_height : grid_py.Unit or None
86
+ Absolute total width / height of all panels plus inter-panel decoration.
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ rows: Optional[Unit] = None,
92
+ cols: Optional[Unit] = None,
93
+ respect: Optional[bool] = None,
94
+ total_width: Optional[Unit] = None,
95
+ total_height: Optional[Unit] = None,
96
+ ) -> None:
97
+ self.rows = rows
98
+ self.cols = cols
99
+ self.respect = respect
100
+ self.total_width = total_width
101
+ self.total_height = total_height
102
+
103
+ def _lengths_sum(self) -> int:
104
+ """Return ``sum(lengths(object))`` (R ``force_panelsize.R:96``)."""
105
+ total = 0
106
+ for field in (self.rows, self.cols, self.respect,
107
+ self.total_width, self.total_height):
108
+ if field is None:
109
+ continue
110
+ if is_unit(field):
111
+ total += len(field)
112
+ else:
113
+ total += 1
114
+ return total
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # force_panelsizes constructor (R force_panelsize.R:51-85)
119
+ # ---------------------------------------------------------------------------
120
+ def force_panelsizes(
121
+ rows: Any = None,
122
+ cols: Any = None,
123
+ respect: Optional[bool] = None,
124
+ total_width: Any = None,
125
+ total_height: Any = None,
126
+ ) -> ForcedSize:
127
+ """Force a facetted plot to have specified panel sizes.
128
+
129
+ Faithful port of ggh4x's ``force_panelsizes`` (``R/force_panelsize.R:51-85``).
130
+ ``rows`` / ``cols`` set panel heights / widths; bare numerics become relative
131
+ ``"null"`` units (ratios), recycled / shortened to the number of panel rows /
132
+ columns. ``total_width`` / ``total_height`` set the absolute total of all
133
+ panels plus the decoration between them, and require the corresponding
134
+ ``rows`` / ``cols`` to be relative (numeric or ``"null"`` units).
135
+
136
+ Parameters
137
+ ----------
138
+ rows, cols : numeric or grid_py.Unit or None, default None
139
+ Panel heights (rows) / widths (cols). ``None`` leaves that direction
140
+ unchanged.
141
+ respect : bool or None, default None
142
+ When ``True``, ``"null"`` widths and heights are proportional. ``None``
143
+ inherits the behaviour specified elsewhere.
144
+ total_width, total_height : grid_py.Unit or None, default None
145
+ Absolute total width / height (length-1 unit) of all panels plus the
146
+ decoration between panels.
147
+
148
+ Returns
149
+ -------
150
+ ForcedSize
151
+ An add-on object that can be added to a plot with ``+``.
152
+
153
+ Raises
154
+ ------
155
+ ValueError
156
+ When ``total_width`` is set but ``cols`` is a non-relative unit (or vice
157
+ versa for ``total_height`` / ``rows``), or when a ``total_*`` argument is
158
+ not an absolute unit of the allowed types.
159
+ """
160
+ if rows is not None and not is_unit(rows):
161
+ rows = Unit(rows, "null")
162
+ if cols is not None and not is_unit(cols):
163
+ cols = Unit(cols, "null")
164
+
165
+ if total_width is not None:
166
+ if is_unit(cols) and not is_null_unit(cols):
167
+ raise ValueError(
168
+ "Cannot set `total_width` when `cols` is not relative."
169
+ )
170
+ if not is_unit(total_width):
171
+ raise ValueError("`total_width` must be a unit object.")
172
+ _arg_match_unit(total_width, "total_width")
173
+ if total_height is not None:
174
+ if is_unit(rows) and not is_null_unit(rows):
175
+ raise ValueError(
176
+ "Cannot set `total_height` when `rows` is not relative."
177
+ )
178
+ if not is_unit(total_height):
179
+ raise ValueError("`total_height` must be a unit object.")
180
+ _arg_match_unit(total_height, "total_height")
181
+
182
+ return ForcedSize(
183
+ rows=rows,
184
+ cols=cols,
185
+ respect=respect,
186
+ total_width=total_width,
187
+ total_height=total_height,
188
+ )
189
+
190
+
191
+ def _arg_match_unit(u: Unit, nm: str) -> None:
192
+ """Validate that *u*'s unit type is one of the allowed absolute units.
193
+
194
+ Mirrors R's ``arg_match0(unitType(total_width), c("cm","mm",...))``.
195
+ """
196
+ t = unit_type(u)
197
+ if isinstance(t, (list, tuple)):
198
+ t = t[0] if t else ""
199
+ if t not in _ABSOLUTE_TOTAL_UNITS:
200
+ allowed = ", ".join(_ABSOLUTE_TOTAL_UNITS)
201
+ raise ValueError(
202
+ f"`{nm}` unit type {t!r} must be one of: {allowed}."
203
+ )
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # draw_panels override builder
208
+ # ---------------------------------------------------------------------------
209
+ def _seq_range_int(values: List[int]) -> List[int]:
210
+ """Return the contiguous integer range ``min:max`` over *values* (R ``seq_range``)."""
211
+ if not values:
212
+ return []
213
+ lo = min(values)
214
+ hi = max(values)
215
+ return list(range(lo, hi + 1))
216
+
217
+
218
+ def _make_forced_draw_panels(parent_cls: Any) -> Any:
219
+ """Build the ``draw_panels`` override for a ``Forced<FacetClass>`` clone.
220
+
221
+ Closes over the *concrete parent class* so the override can dispatch to the
222
+ original facet's ``draw_panels`` via :func:`ggproto_parent`. Faithful port
223
+ of R's ``new.fun`` (``R/force_panelsize.R:107-179``).
224
+
225
+ Parameters
226
+ ----------
227
+ parent_cls : type
228
+ The concrete facet class the forced facet was cloned from.
229
+
230
+ Returns
231
+ -------
232
+ callable
233
+ A ``draw_panels(self, ...)`` method.
234
+ """
235
+
236
+ def draw_panels(
237
+ self: Any,
238
+ panels: list,
239
+ layout: Any,
240
+ x_scales: list,
241
+ y_scales: list,
242
+ ranges: list,
243
+ coord: Any,
244
+ data: Any,
245
+ theme: Any,
246
+ params: Dict[str, Any],
247
+ ) -> Any:
248
+ # Call the original facet's draw_panels to build the panel gtable.
249
+ panel_table = ggproto_parent(parent_cls, self).draw_panels(
250
+ panels, layout, x_scales, y_scales, ranges, coord, data, theme, params
251
+ )
252
+
253
+ force = self.params
254
+ prows = panel_rows(panel_table)
255
+ pcols = panel_cols(panel_table)
256
+ t_pos = [int(v) for v in prows["t"]]
257
+ l_pos = [int(v) for v in pcols["l"]]
258
+ n_rows = len(t_pos)
259
+ n_cols = len(l_pos)
260
+ # seq_range over ALL pcols / prows values (l & r, t & b) per R's
261
+ # `seq_range(pcols)` / `seq_range(prows)`.
262
+ all_col = [int(v) for v in pcols["l"]] + [int(v) for v in pcols["r"]]
263
+ all_row = [int(v) for v in prows["t"]] + [int(v) for v in prows["b"]]
264
+
265
+ force_rows = force.get("force_rows")
266
+ force_cols = force.get("force_cols")
267
+ force_respect = force.get("force_respect")
268
+ total_width = force.get("force_total_width")
269
+ total_height = force.get("force_total_height")
270
+
271
+ # --- total_width branch (R:122-141) -------------------------------
272
+ if total_width is not None:
273
+ if force_cols is None:
274
+ colwidths = [
275
+ float(panel_table.widths[p - 1]._values[0])
276
+ for p in l_pos
277
+ ]
278
+ else:
279
+ colwidths = [
280
+ float(_recycle(force_cols, i)) for i in range(n_cols)
281
+ ]
282
+ # extra_width = columns between panel cells (the decoration):
283
+ # setdiff(seq_range(pcols), unique(unlist(pcols))).
284
+ uniq_cols = set(all_col)
285
+ extra_idx = [i for i in _seq_range_int(all_col) if i not in uniq_cols]
286
+ if len(extra_idx) > 1:
287
+ extra_width = sum(
288
+ _to_scalar(convert_width(panel_table.widths[i - 1], "cm", valueOnly=True))
289
+ for i in extra_idx
290
+ )
291
+ else:
292
+ extra_width = 0.0
293
+ tw = _to_scalar(convert_width(total_width, "cm", valueOnly=True))
294
+ avail = tw - extra_width
295
+ denom = sum(colwidths)
296
+ new_widths = [avail * c / denom for c in colwidths]
297
+ for pos, w in zip(l_pos, new_widths):
298
+ panel_table.widths[pos - 1] = Unit(w, "cm")
299
+ force_cols = None
300
+
301
+ # --- total_height branch (R:143-161) ------------------------------
302
+ if total_height is not None:
303
+ if force_rows is None:
304
+ rowheights = [
305
+ float(panel_table.heights[p - 1]._values[0])
306
+ for p in t_pos
307
+ ]
308
+ else:
309
+ rowheights = [
310
+ float(_recycle(force_rows, i)) for i in range(n_rows)
311
+ ]
312
+ uniq_rows = set(all_row)
313
+ extra_idx = [i for i in _seq_range_int(all_row) if i not in uniq_rows]
314
+ if len(extra_idx) > 1:
315
+ extra_height = sum(
316
+ _to_scalar(convert_height(panel_table.heights[i - 1], "cm", valueOnly=True))
317
+ for i in extra_idx
318
+ )
319
+ else:
320
+ extra_height = 0.0
321
+ th = _to_scalar(convert_height(total_height, "cm", valueOnly=True))
322
+ avail = th - extra_height
323
+ denom = sum(rowheights)
324
+ new_heights = [avail * r / denom for r in rowheights]
325
+ for pos, h in zip(t_pos, new_heights):
326
+ panel_table.heights[pos - 1] = Unit(h, "cm")
327
+ force_rows = None
328
+
329
+ # --- plain override (R:163-176) -----------------------------------
330
+ if force_rows is not None:
331
+ for i, pos in enumerate(t_pos):
332
+ panel_table.heights[pos - 1] = _recycle_unit(force_rows, i)
333
+ if force_cols is not None:
334
+ for i, pos in enumerate(l_pos):
335
+ panel_table.widths[pos - 1] = _recycle_unit(force_cols, i)
336
+ if force_respect is not None:
337
+ panel_table.respect = force_respect
338
+
339
+ return panel_table
340
+
341
+ return draw_panels
342
+
343
+
344
+ def _to_scalar(x: Any) -> float:
345
+ """Coerce a ``convert_*`` result (scalar or length-1 array) to a float."""
346
+ import numpy as np
347
+
348
+ arr = np.asarray(x).ravel()
349
+ return float(arr[0]) if arr.size else 0.0
350
+
351
+
352
+ def _recycle(u: Unit, i: int) -> float:
353
+ """Return the numeric value of element ``i mod len`` of unit *u*."""
354
+ return float(u._values[i % len(u)])
355
+
356
+
357
+ def _recycle_unit(u: Unit, i: int) -> Unit:
358
+ """Return element ``i mod len`` of unit *u* as a length-1 Unit (R ``rep(..., length.out)``)."""
359
+ return u[i % len(u)]
360
+
361
+
362
+ # ---------------------------------------------------------------------------
363
+ # ggplot_add.forcedsize (R force_panelsize.R:94-195)
364
+ # ---------------------------------------------------------------------------
365
+ @update_ggplot.register(ForcedSize)
366
+ def _update_forcedsize(obj: ForcedSize, plot: Any, object_name: str = "") -> Any:
367
+ """Add a :class:`ForcedSize` to *plot* (R ``ggplot_add.forcedsize``).
368
+
369
+ Clones the plot's current facet into a dynamic ``Forced<FacetClass>``
370
+ subclass whose ``draw_panels`` mutates the panel gtable's panel-row heights /
371
+ panel-column widths to the forced sizes. The forced parameters are merged
372
+ into a *copied* params dict under ``force_rows`` / ``force_cols`` /
373
+ ``force_respect`` / ``force_total_width`` / ``force_total_height``.
374
+
375
+ Parameters
376
+ ----------
377
+ obj : ForcedSize
378
+ The container produced by :func:`force_panelsizes`.
379
+ plot : ggplot2_py.plot.GGPlot
380
+ The plot to mutate.
381
+ object_name : str, optional
382
+ Unused (kept for the ``update_ggplot`` dispatch signature).
383
+
384
+ Returns
385
+ -------
386
+ ggplot2_py.plot.GGPlot
387
+ The mutated plot (returned unchanged when *obj* carries no sizes).
388
+ """
389
+ if obj._lengths_sum() < 1:
390
+ return plot
391
+
392
+ old_facet = plot.facet
393
+ parent_cls = type(old_facet)
394
+
395
+ # Merge force params into a copy of the facet's params (R:184).
396
+ old_params = dict(old_facet.params) if old_facet.params else {}
397
+ old_params["force_rows"] = obj.rows
398
+ old_params["force_cols"] = obj.cols
399
+ old_params["force_respect"] = obj.respect
400
+ old_params["force_total_width"] = obj.total_width
401
+ old_params["force_total_height"] = obj.total_height
402
+
403
+ new_facet = ggproto(
404
+ f"Forced{parent_cls.__name__}",
405
+ old_facet,
406
+ draw_panels=_make_forced_draw_panels(parent_cls),
407
+ params=old_params,
408
+ )
409
+
410
+ plot.facet = new_facet
411
+ return plot
@@ -0,0 +1,222 @@
1
+ """Single per-panel position scales (port of ggh4x ``R/scale_facet.R``).
2
+
3
+ :func:`scale_x_facet` / :func:`scale_y_facet` build a :class:`ScaleFacet` add-on
4
+ carrying a panel *predicate* and a position scale. When added to a plot, the
5
+ handler either appends to an existing ``FreeScaled<...>`` facet's scale lists or
6
+ lowers to a single-entry :class:`~ggh4x.panel_scales.facetted_pos_scales.FacettedPosScales`,
7
+ reusing that machinery.
8
+
9
+ NSE deviation
10
+ -------------
11
+ R captures ``expr`` via ``enquo`` and tidy-evaluates it against the plot layout.
12
+ Python has no NSE: ``expr`` is a *predicate* -- either a callable
13
+ ``layout_df -> bool-array`` or a string evaluated with
14
+ :meth:`pandas.DataFrame.eval` over the layout columns (``PANEL`` / ``ROW`` /
15
+ ``COL`` / ``SCALE_*`` + facet variables).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ import ggplot2_py as _gg
23
+ from ggplot2_py import ggproto
24
+ from ggplot2_py.plot import update_ggplot
25
+
26
+ from ggh4x._cli import cli_abort
27
+ from ggh4x._rlang import arg_match0
28
+
29
+ from .facetted_pos_scales import FacettedPosScales, _ScaleList
30
+
31
+ __all__ = [
32
+ "scale_facet",
33
+ "scale_x_facet",
34
+ "scale_y_facet",
35
+ "ScaleFacet",
36
+ ]
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # ScaleFacet container (R: structure(list(lhs=, rhs=), class = "scale_facet"))
41
+ # ---------------------------------------------------------------------------
42
+ class ScaleFacet:
43
+ """Deferred container of one per-panel position scale + its predicate.
44
+
45
+ Port of R's ``structure(list(lhs =, rhs =), class = "scale_facet")``.
46
+ Consumed by :func:`_update_scale_facet` at ``+``-time.
47
+
48
+ Attributes
49
+ ----------
50
+ lhs : callable or str
51
+ The panel predicate (formula LHS equivalent).
52
+ rhs : Scale
53
+ The position scale to apply (formula RHS equivalent).
54
+ """
55
+
56
+ def __init__(self, lhs: Any, rhs: Any) -> None:
57
+ self.lhs = lhs
58
+ self.rhs = rhs
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # scale_facet (R scale_facet.R:76-116)
63
+ # ---------------------------------------------------------------------------
64
+ def scale_facet(expr: Any, aes: str, *args: Any, type: str = "continuous", **kwargs: Any) -> ScaleFacet:
65
+ """Build a per-panel position scale (generic over aesthetic).
66
+
67
+ Faithful port of ggh4x's ``scale_facet`` (``R/scale_facet.R:76-116``).
68
+ Resolves the constructor ``scale_<aes>_<type>`` from the :mod:`ggplot2_py`
69
+ namespace (R's ``find_global``), instantiates it with the extra arguments,
70
+ and pairs it with the panel predicate *expr*.
71
+
72
+ Parameters
73
+ ----------
74
+ expr : callable or str
75
+ Panel predicate evaluated against the plot layout (see module docstring).
76
+ aes : {"x", "y"}
77
+ The position aesthetic.
78
+ *args, **kwargs
79
+ Extra arguments forwarded to the resolved scale constructor.
80
+ type : str, default "continuous"
81
+ Scale type, such that ``scale_<aes>_<type>`` names a constructor.
82
+
83
+ Returns
84
+ -------
85
+ ScaleFacet
86
+ An add-on object that can be added to a plot with ``+``.
87
+
88
+ Raises
89
+ ------
90
+ ValueError
91
+ When ``type == "facet"`` (circular), the constructor cannot be found, or
92
+ *expr* is missing.
93
+ """
94
+ candidate = f"scale_{aes}_{type}"
95
+ if type == "facet":
96
+ cli_abort(
97
+ f"Cannot circularly define `{candidate}` as template for `{candidate}`."
98
+ )
99
+
100
+ fun = getattr(_gg, candidate, None)
101
+ scale = None
102
+ if fun is not None:
103
+ scale = fun(*args, **kwargs)
104
+
105
+ if scale is None or not hasattr(scale, "aesthetics"):
106
+ cli_abort(
107
+ f"Cannot find a `{candidate}` function. "
108
+ "Did you misspell the `type` argument?"
109
+ )
110
+
111
+ if expr is None:
112
+ cli_abort("`expr` must be a valid expression.")
113
+
114
+ return ScaleFacet(lhs=expr, rhs=scale)
115
+
116
+
117
+ def scale_x_facet(expr: Any, *args: Any, type: str = "continuous", **kwargs: Any) -> ScaleFacet:
118
+ """Per-panel x position scale (thin wrapper, R ``scale_x_facet``).
119
+
120
+ Parameters
121
+ ----------
122
+ expr : callable or str
123
+ Panel predicate (see :func:`scale_facet`).
124
+ *args, **kwargs
125
+ Forwarded to the resolved ``scale_x_<type>`` constructor.
126
+ type : str, default "continuous"
127
+
128
+ Returns
129
+ -------
130
+ ScaleFacet
131
+ """
132
+ return scale_facet(expr, "x", *args, type=type, **kwargs)
133
+
134
+
135
+ def scale_y_facet(expr: Any, *args: Any, type: str = "continuous", **kwargs: Any) -> ScaleFacet:
136
+ """Per-panel y position scale (thin wrapper, R ``scale_y_facet``).
137
+
138
+ Parameters
139
+ ----------
140
+ expr : callable or str
141
+ Panel predicate (see :func:`scale_facet`).
142
+ *args, **kwargs
143
+ Forwarded to the resolved ``scale_y_<type>`` constructor.
144
+ type : str, default "continuous"
145
+
146
+ Returns
147
+ -------
148
+ ScaleFacet
149
+ """
150
+ return scale_facet(expr, "y", *args, type=type, **kwargs)
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # ggplot_add.scale_facet (R scale_facet.R:133-184)
155
+ # ---------------------------------------------------------------------------
156
+ @update_ggplot.register(ScaleFacet)
157
+ def _update_scale_facet(obj: ScaleFacet, plot: Any, object_name: str = "") -> Any:
158
+ """Add a :class:`ScaleFacet` to *plot* (R ``ggplot_add.scale_facet``).
159
+
160
+ Appends to an existing ``FreeScaled<...>`` facet's per-panel scale lists (and
161
+ its parallel predicate list), or lowers to a single-entry
162
+ :class:`FacettedPosScales` otherwise.
163
+
164
+ Parameters
165
+ ----------
166
+ obj : ScaleFacet
167
+ plot : ggplot2_py.plot.GGPlot
168
+ object_name : str, optional
169
+
170
+ Returns
171
+ -------
172
+ ggplot2_py.plot.GGPlot
173
+
174
+ Raises
175
+ ------
176
+ ValueError
177
+ When the plot has no facets (``FacetNull``).
178
+ """
179
+ aes = obj.rhs.aesthetics[0]
180
+ aes = arg_match0(aes, ["x", "y"], arg_name="scale$aesthetics[1]")
181
+
182
+ facet = plot.facet
183
+ cls_name = type(facet).__name__
184
+
185
+ if cls_name.startswith("FacetNull") or cls_name == "FacetNull":
186
+ nm = f"scale_{aes}_facet"
187
+ cli_abort(
188
+ f"`{nm}` cannot be added to a plot without facets. "
189
+ f"Try adding facets before adding `{nm}`."
190
+ )
191
+
192
+ if cls_name.startswith("FreeScaled"):
193
+ if aes == "x":
194
+ old = facet.new_x_scales
195
+ old_lhs = getattr(old, "lhs", None) or []
196
+ new = _ScaleList(
197
+ list(old) + [obj.rhs],
198
+ lhs=list(old_lhs) + [obj.lhs],
199
+ )
200
+ plot.facet = ggproto(None, facet, new_x_scales=new)
201
+ else:
202
+ old = facet.new_y_scales
203
+ old_lhs = getattr(old, "lhs", None) or []
204
+ new = _ScaleList(
205
+ list(old) + [obj.rhs],
206
+ lhs=list(old_lhs) + [obj.lhs],
207
+ )
208
+ plot.facet = ggproto(None, facet, new_y_scales=new)
209
+ else:
210
+ if aes == "x":
211
+ fps = FacettedPosScales(
212
+ x=_ScaleList([obj.rhs], lhs=[obj.lhs]),
213
+ y=_ScaleList([None], lhs=None),
214
+ )
215
+ else:
216
+ fps = FacettedPosScales(
217
+ x=_ScaleList([None], lhs=None),
218
+ y=_ScaleList([obj.rhs], lhs=[obj.lhs]),
219
+ )
220
+ plot = plot + fps
221
+
222
+ return plot