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,393 @@
1
+ """Multiple gradient colour/fill scales (R source: ``scale_multi.R``).
2
+
3
+ Ports ggh4x's :func:`scale_colour_multi` / :func:`scale_fill_multi`, which map
4
+ several *non-standard* colour/fill aesthetics (``fill1``, ``fill2``, ...) each to
5
+ its own :func:`ggplot2_py.continuous_scale` gradient. The constructors build a
6
+ :class:`~ggh4x.multiscale._multiscale_add.MultiScale` container that defers the
7
+ plot rewrite to ``+``-time (see :mod:`ggh4x.multiscale._multiscale_add`).
8
+
9
+ The R *listed-argument* convention is reproduced exactly by :func:`_pickvalue`:
10
+ an argument that is a Python ``list`` is interpreted *per aesthetic* (its ``i``-th
11
+ element belongs to the ``i``-th aesthetic, wrapping to the first element when the
12
+ index exceeds the list length); any other value (scalar, colour vector, tuple,
13
+ ``ndarray``) is broadcast unchanged to every aesthetic.
14
+
15
+ Notes
16
+ -----
17
+ * ggplot2_py always runs the *new guide system*, so the ``trans`` extra argument
18
+ is unconditionally renamed to ``transform`` and the R ``scale_name`` argument is
19
+ never forwarded (it does not exist on :func:`ggplot2_py.continuous_scale`).
20
+ * All semantics were verified against a live R ``ggh4x`` session
21
+ (``distribute_scale_multi``, ``pickvalue``, the materialised ``GuideColourbar``
22
+ ``available_aes`` and the default ``white``/``black`` gradient palette).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Dict, List
28
+
29
+ import scales as _scales
30
+
31
+ import ggplot2_py as _gg
32
+ from ggplot2_py import standardise_aes_names
33
+ from ggplot2_py.scale import continuous_scale
34
+
35
+ from .._cli import cli_abort
36
+ from ._multiscale_add import MultiScale
37
+
38
+ __all__ = [
39
+ "scale_fill_multi",
40
+ "scale_colour_multi",
41
+ "scale_color_multi",
42
+ "MultiScale",
43
+ ]
44
+
45
+ # Sentinel marking "argument not supplied" so the R ``missing()`` cascade for
46
+ # ``colours`` / ``colors`` can be reproduced faithfully.
47
+ _MISSING = object()
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # pickvalue (scale_multi.R:174-183)
52
+ # ---------------------------------------------------------------------------
53
+ def _is_per_aesthetic_list(x: Any) -> bool:
54
+ """Return ``True`` when *x* is a Python list used as a *per-aesthetic* container.
55
+
56
+ R distinguishes ``list(c("white","red"), c("black","blue"))`` (a *list* of
57
+ vectors -- per aesthetic) from ``c("white","black")`` (a *vector* -- broadcast)
58
+ via ``class(x)[[1]] == "list"``. Python collapses both onto ``list``, so the
59
+ faithful disambiguation is: a list is per-aesthetic iff at least one of its
60
+ elements is itself a non-string sequence (a nested vector), mirroring the
61
+ documented ``colours = [["white", "red"], ...]`` idiom. A flat list of scalars
62
+ (e.g. ``["white", "black"]``) is a bare colour vector and is broadcast.
63
+
64
+ Parameters
65
+ ----------
66
+ x : Any
67
+
68
+ Returns
69
+ -------
70
+ bool
71
+ """
72
+ if type(x) is not list:
73
+ return False
74
+ return any(
75
+ isinstance(el, (list, tuple)) or hasattr(el, "__len__") and not isinstance(el, (str, bytes))
76
+ for el in x
77
+ )
78
+
79
+
80
+ def _pickvalue(x: Any, i: int) -> Any:
81
+ """Select the per-aesthetic value of *x* for aesthetic index *i*.
82
+
83
+ Port of R ``pickvalue`` (``scale_multi.R:174-183``). When *x* is a
84
+ *per-aesthetic* list (see :func:`_is_per_aesthetic_list`) the ``i``-th element
85
+ is returned, wrapping back to the first element when ``i`` exceeds the list
86
+ length. Any other value (a scalar, a bare colour vector / flat list, a
87
+ ``tuple`` or an ``ndarray``) is *broadcast* and returned unchanged.
88
+
89
+ Parameters
90
+ ----------
91
+ x : Any
92
+ The raw argument value.
93
+ i : int
94
+ Zero-based aesthetic index.
95
+
96
+ Returns
97
+ -------
98
+ Any
99
+ ``x[i]`` (with wraparound to ``x[0]``) when *x* is a per-aesthetic list;
100
+ otherwise *x* unchanged.
101
+ """
102
+ if not _is_per_aesthetic_list(x):
103
+ return x
104
+ # R: i <- if (i > length(x)) 1 else i (1-based). Here i is 0-based, so the
105
+ # out-of-range index wraps to the first element.
106
+ if i >= len(x):
107
+ i = 0
108
+ return x[i]
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # distribute_scale_multi (scale_multi.R:115-171)
113
+ # ---------------------------------------------------------------------------
114
+ def _distribute_scale_multi(
115
+ *,
116
+ aesthetics: List[str],
117
+ colours: Any,
118
+ values: Any,
119
+ na_value: Any,
120
+ guide: Any,
121
+ extra: Dict[str, Any],
122
+ ) -> List[Any]:
123
+ """Build one :func:`continuous_scale` per aesthetic, distributing arguments.
124
+
125
+ Port of R ``distribute_scale_multi`` (``scale_multi.R:115-171``). For every
126
+ aesthetic ``aesthetics[i]`` the listed arguments are picked with
127
+ :func:`_pickvalue`, the guide is materialised (so its ``available_aes`` is set
128
+ to the single non-standard aesthetic), and a gradient
129
+ :func:`continuous_scale` is constructed.
130
+
131
+ Parameters
132
+ ----------
133
+ aesthetics : list of str
134
+ Non-standard aesthetic names (e.g. ``["fill1", "fill2"]``).
135
+ colours : Any
136
+ Gradient colours (per-aesthetic ``list`` or broadcast vector).
137
+ values : Any
138
+ Gradient ``values`` positions (per-aesthetic ``list`` or broadcast).
139
+ na_value : Any
140
+ Colour for missing values (per-aesthetic ``list`` or broadcast).
141
+ guide : Any
142
+ Guide spec(s): ``"colourbar"``/``"colorbar"``/``"legend"`` strings or
143
+ :class:`ggplot2_py.guide.Guide` instances (per-aesthetic or broadcast).
144
+ extra : dict
145
+ Extra keyword arguments forwarded to :func:`continuous_scale`, each value
146
+ possibly a per-aesthetic ``list``.
147
+
148
+ Returns
149
+ -------
150
+ list of ggplot2_py.scale.ScaleContinuous
151
+ One continuous gradient scale per aesthetic.
152
+ """
153
+ n = len(aesthetics)
154
+
155
+ # Extra args: per-aesthetic dict, with the new-guide-system trans->transform
156
+ # rename always applied (scale_multi.R:119-127).
157
+ extra_args: List[Dict[str, Any]] = []
158
+ for i in range(n):
159
+ picked: Dict[str, Any] = {k: _pickvalue(v, i) for k, v in extra.items()}
160
+ if "trans" in picked:
161
+ picked["transform"] = picked.pop("trans")
162
+ extra_args.append(picked)
163
+
164
+ # Interpret guides (scale_multi.R:130-152): materialise a string/Guide into a
165
+ # Guide instance whose ``available_aes`` is the single non-standard aesthetic.
166
+ guides: List[Any] = []
167
+ for i in range(n):
168
+ this_guide = _pickvalue(guide, i)
169
+ if isinstance(this_guide, str):
170
+ # standardise_aes_names('colourbar') == standardise_aes_names('colorbar')
171
+ # i.e. colour->color normalisation; only the bar/legend distinction
172
+ # matters here.
173
+ if this_guide in ("colourbar", "colorbar"):
174
+ this_guide = _gg.guide_colourbar()
175
+ elif this_guide == "legend":
176
+ this_guide = _gg.guide_legend()
177
+ if _is_guide_proto(this_guide):
178
+ # ggproto(NULL, old, available_aes = aes): clone the instance so the
179
+ # class default is shadowed without mutating the shared original.
180
+ cloned = _gg.ggproto(None, this_guide)
181
+ cloned._set(available_aes=[aesthetics[i]])
182
+ this_guide = cloned
183
+ else:
184
+ cli_abort(
185
+ "`ggh4x`'s author hasn't programmed this path yet. "
186
+ "Choose a legend or colourbar guide."
187
+ )
188
+ guides.append(this_guide)
189
+
190
+ # Interpret scales (scale_multi.R:155-169).
191
+ out: List[Any] = []
192
+ for i in range(n):
193
+ aes_i = aesthetics[i]
194
+ palette = _scales.pal_gradient_n(
195
+ colours=_pickvalue(colours, i),
196
+ values=_pickvalue(values, i),
197
+ )
198
+ kwargs: Dict[str, Any] = dict(extra_args[i])
199
+ sc = continuous_scale(
200
+ aesthetics=aes_i,
201
+ palette=palette,
202
+ na_value=_pickvalue(na_value, i),
203
+ guide=guides[i],
204
+ **kwargs,
205
+ )
206
+ out.append(sc)
207
+ return out
208
+
209
+
210
+ def _is_guide_proto(obj: Any) -> bool:
211
+ """Return ``True`` when *obj* is a :class:`ggplot2_py.guide.Guide` instance."""
212
+ Guide = getattr(_gg, "Guide", None)
213
+ if Guide is None:
214
+ return False
215
+ try:
216
+ return isinstance(obj, Guide)
217
+ except TypeError:
218
+ return False
219
+
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # Constructors
223
+ # ---------------------------------------------------------------------------
224
+ def _resolve_colours(colours: Any, colors: Any) -> Any:
225
+ """Reproduce R's ``missing(colours)``/``missing(colors)`` default cascade.
226
+
227
+ Port of ``scale_multi.R:53-62``: prefer ``colours``; fall back to the
228
+ American ``colors``; default to ``["white", "black"]`` when neither is given.
229
+
230
+ Parameters
231
+ ----------
232
+ colours : Any
233
+ British-spelling argument, or :data:`_MISSING`.
234
+ colors : Any
235
+ American-spelling argument, or :data:`_MISSING`.
236
+
237
+ Returns
238
+ -------
239
+ Any
240
+ The resolved gradient colours.
241
+ """
242
+ if colours is _MISSING:
243
+ if colors is _MISSING:
244
+ return ["white", "black"]
245
+ return colors
246
+ return colours
247
+
248
+
249
+ def scale_fill_multi(
250
+ *,
251
+ colours: Any = _MISSING,
252
+ values: Any = None,
253
+ na_value: str = "transparent",
254
+ guide: Any = "colourbar",
255
+ aesthetics: Any = "fill",
256
+ colors: Any = _MISSING,
257
+ **kwargs: Any,
258
+ ) -> MultiScale:
259
+ """Map multiple non-standard fill aesthetics to multiple gradient scales.
260
+
261
+ Port of R ``scale_fill_multi`` (``scale_multi.R:48-76``). Distributes listed
262
+ arguments across one :func:`ggplot2_py.continuous_scale` gradient per
263
+ aesthetic and wraps the result in a :class:`MultiScale` container.
264
+
265
+ This should only be added to a plot **after** every layer it affects has been
266
+ added, since :class:`MultiScale` rewrites those layers' geoms at ``+``-time.
267
+
268
+ Parameters
269
+ ----------
270
+ colours : Any, optional
271
+ Gradient colours. A ``list`` is per-aesthetic (e.g.
272
+ ``[["white", "red"], ["black", "blue"]]``); a bare vector is broadcast.
273
+ Defaults to ``["white", "black"]``.
274
+ values : Any, optional
275
+ Gradient ``values`` positions (per-aesthetic ``list`` or broadcast).
276
+ na_value : str, default ``"transparent"``
277
+ Colour for missing values (per-aesthetic ``list`` or broadcast).
278
+ guide : Any, default ``"colourbar"``
279
+ Guide spec(s): ``"colourbar"``/``"colorbar"``/``"legend"`` or
280
+ :class:`ggplot2_py.guide.Guide` instances (per-aesthetic or broadcast).
281
+ aesthetics : str or list of str, default ``"fill"``
282
+ Non-standard aesthetic name(s) to map.
283
+ colors : Any, optional
284
+ American-spelling alias for *colours*.
285
+ **kwargs : Any
286
+ Extra arguments forwarded to :func:`continuous_scale` (each may be a
287
+ per-aesthetic ``list``).
288
+
289
+ Returns
290
+ -------
291
+ MultiScale
292
+ A deferred-mutation container of class ``MultiScale``.
293
+ """
294
+ colours = _resolve_colours(colours, colors)
295
+ aes_list = [aesthetics] if isinstance(aesthetics, str) else list(aesthetics)
296
+ scales = _distribute_scale_multi(
297
+ aesthetics=aes_list,
298
+ colours=colours,
299
+ values=values,
300
+ na_value=na_value,
301
+ guide=guide,
302
+ extra=kwargs,
303
+ )
304
+ return MultiScale(
305
+ scales=scales,
306
+ aes=aes_list,
307
+ replaced_aes=standardise_aes_names(["fill"])[0],
308
+ )
309
+
310
+
311
+ def scale_colour_multi(
312
+ *,
313
+ colours: Any = _MISSING,
314
+ values: Any = None,
315
+ na_value: str = "transparent",
316
+ guide: Any = "colourbar",
317
+ aesthetics: Any = "colour",
318
+ colors: Any = _MISSING,
319
+ **kwargs: Any,
320
+ ) -> MultiScale:
321
+ """Map multiple non-standard colour aesthetics to multiple gradient scales.
322
+
323
+ Port of R ``scale_colour_multi`` (``scale_multi.R:80-110``). Behaves exactly
324
+ like :func:`scale_fill_multi` but defaults ``aesthetics`` to ``"colour"`` and
325
+ sets ``replaced_aes`` to ``"colour"``.
326
+
327
+ Parameters
328
+ ----------
329
+ colours : Any, optional
330
+ Gradient colours (per-aesthetic ``list`` or broadcast vector). Defaults
331
+ to ``["white", "black"]``.
332
+ values : Any, optional
333
+ Gradient ``values`` positions (per-aesthetic ``list`` or broadcast).
334
+ na_value : str, default ``"transparent"``
335
+ Colour for missing values (per-aesthetic ``list`` or broadcast).
336
+ guide : Any, default ``"colourbar"``
337
+ Guide spec(s) (per-aesthetic or broadcast).
338
+ aesthetics : str or list of str, default ``"colour"``
339
+ Non-standard aesthetic name(s) to map.
340
+ colors : Any, optional
341
+ American-spelling alias for *colours*.
342
+ **kwargs : Any
343
+ Extra arguments forwarded to :func:`continuous_scale`.
344
+
345
+ Returns
346
+ -------
347
+ MultiScale
348
+ A deferred-mutation container of class ``MultiScale``.
349
+ """
350
+ colours = _resolve_colours(colours, colors)
351
+ aes_list = [aesthetics] if isinstance(aesthetics, str) else list(aesthetics)
352
+ scales = _distribute_scale_multi(
353
+ aesthetics=aes_list,
354
+ colours=colours,
355
+ values=values,
356
+ na_value=na_value,
357
+ guide=guide,
358
+ extra=kwargs,
359
+ )
360
+ return MultiScale(
361
+ scales=scales,
362
+ aes=aes_list,
363
+ replaced_aes=standardise_aes_names(["colour"])[0],
364
+ )
365
+
366
+
367
+ def scale_color_multi(
368
+ *,
369
+ colours: Any = _MISSING,
370
+ values: Any = None,
371
+ na_value: str = "transparent",
372
+ guide: Any = "colourbar",
373
+ aesthetics: Any = "colour",
374
+ colors: Any = _MISSING,
375
+ **kwargs: Any,
376
+ ) -> MultiScale:
377
+ """American-spelling alias for :func:`scale_colour_multi`.
378
+
379
+ See :func:`scale_colour_multi` for the full parameter description.
380
+
381
+ Returns
382
+ -------
383
+ MultiScale
384
+ """
385
+ return scale_colour_multi(
386
+ colours=colours,
387
+ values=values,
388
+ na_value=na_value,
389
+ guide=guide,
390
+ aesthetics=aesthetics,
391
+ colors=colors,
392
+ **kwargs,
393
+ )
@@ -0,0 +1,58 @@
1
+ """Per-panel scale and panel-size customisation for ggh4x facets.
2
+
3
+ This package ports four ggh4x R files, all implemented as **add-on objects**
4
+ (not new facet/geom bases): each is created by a constructor and consumed by an
5
+ ``ggplot_add`` handler that rewrites the plot's live facet (or a layer's geom)
6
+ at ``+``-time, producing a runtime ggproto clone.
7
+
8
+ * :mod:`force_panelsize` (``force_panelsize.R``) -- :func:`force_panelsizes`:
9
+ force the panel row heights / column widths of any facet.
10
+ * :mod:`facetted_pos_scales` (``facetted_pos_scales.R``) --
11
+ :func:`facetted_pos_scales`: per-panel position scales.
12
+ * :mod:`scale_facet` (``scale_facet.R``) -- :func:`scale_x_facet` /
13
+ :func:`scale_y_facet`: a single per-panel position scale targeting panels by
14
+ predicate.
15
+ * :mod:`at_panel` (``at_panel.R``) -- :func:`at_panel`: constrain a layer to a
16
+ subset of panels.
17
+
18
+ Importing this package registers the ``ForcedSize`` / ``FacettedPosScales`` /
19
+ ``ScaleFacet`` handlers on :func:`ggplot2_py.plot.update_ggplot` (via the
20
+ module imports), exactly as ``ggh4x.multiscale`` registers ``MultiScale``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from .at_panel import at_panel
26
+ from .facetted_pos_scales import (
27
+ FacettedPosScales,
28
+ check_facetted_scale,
29
+ facetted_pos_scales,
30
+ finish_data_individual,
31
+ init_scale,
32
+ init_scales_individual,
33
+ should_transform,
34
+ train_scales_individual,
35
+ validate_facetted_scale,
36
+ )
37
+ from .force_panelsize import ForcedSize, force_panelsizes, is_null_unit
38
+ from .scale_facet import ScaleFacet, scale_facet, scale_x_facet, scale_y_facet
39
+
40
+ __all__ = [
41
+ "force_panelsizes",
42
+ "ForcedSize",
43
+ "is_null_unit",
44
+ "facetted_pos_scales",
45
+ "FacettedPosScales",
46
+ "check_facetted_scale",
47
+ "validate_facetted_scale",
48
+ "init_scale",
49
+ "init_scales_individual",
50
+ "train_scales_individual",
51
+ "finish_data_individual",
52
+ "should_transform",
53
+ "scale_facet",
54
+ "scale_x_facet",
55
+ "scale_y_facet",
56
+ "ScaleFacet",
57
+ "at_panel",
58
+ ]
@@ -0,0 +1,115 @@
1
+ """Constrain a layer to specific panels (port of ggh4x ``R/at_panel.R``).
2
+
3
+ :func:`at_panel` clones a layer's geom so that, at draw time, the layer's data is
4
+ subset to only those panels for which a *predicate* (evaluated against the plot
5
+ layout) is ``True``. This makes panel-specific annotations possible.
6
+
7
+ NSE deviation
8
+ -------------
9
+ R captures ``expr`` via ``enquo`` and tidy-evaluates it against the plot layout.
10
+ Python has no NSE: ``expr`` is a *predicate* -- either a callable
11
+ ``layout_df -> bool-array`` or a string evaluated with
12
+ :meth:`pandas.DataFrame.eval` over the layout columns (``PANEL`` / ``ROW`` /
13
+ ``COL`` / ``SCALE_*`` + facet variables).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, List
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+
23
+ from ggplot2_py import ggproto
24
+ from ggplot2_py.ggproto import ggproto_parent
25
+ from ggplot2_py.layer import Layer
26
+
27
+ from ggh4x._cli import cli_abort
28
+
29
+ __all__ = ["at_panel"]
30
+
31
+
32
+ def _eval_keep(expr: Any, panels: pd.DataFrame) -> np.ndarray:
33
+ """Evaluate the panel predicate against the layout, returning a bool array.
34
+
35
+ Parameters
36
+ ----------
37
+ expr : callable or str
38
+ The panel predicate.
39
+ panels : pandas.DataFrame
40
+ The plot layout.
41
+
42
+ Returns
43
+ -------
44
+ numpy.ndarray
45
+ Boolean keep-mask, recycled to ``len(panels)``.
46
+ """
47
+ if callable(expr):
48
+ res = expr(panels)
49
+ elif isinstance(expr, str):
50
+ res = panels.eval(expr, engine="python")
51
+ else:
52
+ res = expr
53
+ arr = np.asarray(res)
54
+ if arr.dtype != bool:
55
+ arr = arr.astype(bool)
56
+ n = len(panels)
57
+ if arr.ndim == 0:
58
+ arr = np.repeat(arr, n)
59
+ if len(arr) != n:
60
+ arr = np.resize(arr, n)
61
+ return arr
62
+
63
+
64
+ def at_panel(layer: Any, expr: Any) -> Any:
65
+ """Constrain a layer to the panels matching *expr*.
66
+
67
+ Faithful port of ggh4x's ``at_panel`` (``R/at_panel.R:43-82``). Clones the
68
+ layer's geom with a ``draw_layer`` override that evaluates *expr* against the
69
+ plot layout, subsets the layer data to the kept panels, then delegates to the
70
+ original geom's ``draw_layer``. A bare list of layers is handled by
71
+ recursing over its layer elements.
72
+
73
+ Parameters
74
+ ----------
75
+ layer : Layer or list of Layer
76
+ The layer (or bare list of layers) to constrain.
77
+ expr : callable or str
78
+ Panel predicate evaluated against the plot layout (see module docstring).
79
+
80
+ Returns
81
+ -------
82
+ Layer or list
83
+ A layer clone whose geom only draws on matched panels (or the input list
84
+ with each layer element so cloned).
85
+
86
+ Raises
87
+ ------
88
+ ValueError
89
+ When *expr* is missing, or *layer* is neither a layer nor a bare list of
90
+ layers.
91
+ """
92
+ if expr is None:
93
+ cli_abort("`expr` must be an expression, it cannot be missing.")
94
+
95
+ if not isinstance(layer, Layer):
96
+ # Accept bare lists of layers (e.g. geom_sf()).
97
+ if isinstance(layer, list):
98
+ return [
99
+ at_panel(el, expr) if isinstance(el, Layer) else el
100
+ for el in layer
101
+ ]
102
+ cli_abort(f"`layer` must be a layer, not {type(layer).__name__}.")
103
+
104
+ old_geom = layer.geom
105
+
106
+ def draw_layer(self: Any, data: Any, params: Any, layout: Any, coord: Any) -> List[Any]:
107
+ panels = layout.layout
108
+ keep = _eval_keep(expr, panels)
109
+ kept_panels = panels.loc[keep, "PANEL"]
110
+
111
+ data = data[data["PANEL"].isin(kept_panels)]
112
+ return ggproto_parent(old_geom, self).draw_layer(data, params, layout, coord)
113
+
114
+ new_geom = ggproto(None, old_geom, draw_layer=draw_layer)
115
+ return ggproto(None, layer, geom=new_geom)