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
ggh4x/strip_split.py ADDED
@@ -0,0 +1,687 @@
1
+ """Split strips for ggh4x facets (port of ggh4x ``strip_split.R``).
2
+
3
+ This module ports :class:`StripSplit` and the :func:`strip_split` constructor.
4
+ Split strips let each faceting variable be placed on a different side of the
5
+ panel (``"top"`` / ``"bottom"`` / ``"left"`` / ``"right"``), overruling
6
+ ``strip.position`` / ``switch``. A single-variable strip is spanned across the
7
+ panels that share its value.
8
+
9
+ ``StripSplit`` extends :class:`ggh4x.strip_nested.StripNested` (so it inherits
10
+ the RLE-merge ``assemble_strip`` / ``finish_strip``). It overrides:
11
+
12
+ * :meth:`StripSplit.setup` -- does *not* separate cols from rows; builds one
13
+ ``vars`` frame from ``union(rows, cols)`` (grid) or ``facets`` (wrap), de-dups
14
+ the layout, and calls its own single-``vars`` :meth:`get_strips` signature.
15
+ * :meth:`StripSplit.get_strips` -- per-side strip construction driven by
16
+ ``params["position"]``; builds an :func:`ggh4x._borrowed_ggplot2.id`
17
+ composite-key hierarchy and spans single-variable strips.
18
+ * :meth:`StripSplit.incorporate_grid` -- four independent side-blocks placing
19
+ each side's strips into the panel gtable.
20
+ * :meth:`StripSplit.incorporate_wrap` -- trivial delegation to
21
+ :meth:`incorporate_grid`.
22
+
23
+ R source: ``ggh4x/R/strip_split.R``.
24
+
25
+ Notes
26
+ -----
27
+ * **id() composite keys.** R uses the ggplot2-internal ``id()`` to build a
28
+ per-variable integer key table; this port reuses
29
+ :func:`ggh4x._borrowed_ggplot2.id` (verified to match R's lexicographic,
30
+ level-aware codes). The ids drive both the ``!duplicated(ids)`` selection and
31
+ the ``split(layout$ROW/COL, ids)`` span extension.
32
+ * **Span logic.** When a side has a single variable and every strip is a single
33
+ panel (``all(strp.t == strp.b)`` for x, ``all(strp.l == strp.r)`` for y), the
34
+ bottom/right edge is extended to the panel at the maximum ROW/COL within each
35
+ id group (``vapply(split(...), max)`` then ``match`` back to a ``PANEL``).
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import warnings
41
+ from typing import Any, Dict, List, Optional, Sequence
42
+
43
+ import numpy as np
44
+ import pandas as pd
45
+
46
+ from ggplot2_py.ggproto import ggproto
47
+ from gtable_py import gtable_add_cols, gtable_add_grob, gtable_add_rows
48
+
49
+ from ggh4x._borrowed_ggplot2 import empty, id
50
+ from ggh4x._cli import cli_abort
51
+ from ggh4x._facet_utils import split_heights_cm, split_widths_cm
52
+ from ggh4x._rlang import arg_match0
53
+ from ggh4x.strip_nested import StripNested
54
+ from ggh4x.strip_vanilla import (
55
+ _format_labels,
56
+ _panel_layout,
57
+ validate_element_list,
58
+ )
59
+
60
+ __all__ = ["StripSplit", "strip_split"]
61
+
62
+
63
+ def _arg_match_multiple(
64
+ arg: Sequence[str],
65
+ values: Sequence[str],
66
+ arg_name: str = "arg",
67
+ ) -> List[str]:
68
+ """Port of R ``rlang::arg_match(arg, values, multiple = TRUE)``.
69
+
70
+ Validates every element of *arg* against the allowed *values*, aborting on
71
+ the first invalid element.
72
+
73
+ Parameters
74
+ ----------
75
+ arg : sequence of str
76
+ The supplied vector of choices.
77
+ values : sequence of str
78
+ The allowed values.
79
+ arg_name : str, default ``"arg"``
80
+ Argument name (for the error message).
81
+
82
+ Returns
83
+ -------
84
+ list of str
85
+ *arg* as a list when every element is valid.
86
+
87
+ Raises
88
+ ------
89
+ ValueError
90
+ When any element of *arg* is not among *values*.
91
+ """
92
+ out = list(arg)
93
+ for x in out:
94
+ if x not in values:
95
+ choices = ", ".join(repr(v) for v in values)
96
+ cli_abort(f"`{arg_name}` must be one of {choices}, not {x!r}.")
97
+ return out
98
+
99
+
100
+ def _match_first(values: Sequence[Any], table: Sequence[Any]) -> List[int]:
101
+ """Port of R ``match(values, table)`` (1-based first-occurrence index).
102
+
103
+ Parameters
104
+ ----------
105
+ values : sequence
106
+ Values to look up.
107
+ table : sequence
108
+ The lookup table.
109
+
110
+ Returns
111
+ -------
112
+ list of int
113
+ For each value, the 1-based index of its first occurrence in *table*
114
+ (0 when absent, mirroring how the result is only ever used as a valid
115
+ index here).
116
+ """
117
+ lookup: Dict[Any, int] = {}
118
+ for i, t in enumerate(table):
119
+ if t not in lookup:
120
+ lookup[t] = i + 1
121
+ return [lookup.get(v, 0) for v in values]
122
+
123
+
124
+ def _split_max(values: Sequence[Any], keys: Sequence[Any]) -> List[Any]:
125
+ """Port of R ``vapply(split(values, keys), max, integer(1))``.
126
+
127
+ Groups *values* by *keys* (sorted key levels, as R ``split`` does) and
128
+ returns the per-group maximum.
129
+
130
+ Parameters
131
+ ----------
132
+ values : sequence
133
+ Values to group and reduce.
134
+ keys : sequence
135
+ Grouping key per value.
136
+
137
+ Returns
138
+ -------
139
+ list
140
+ The maximum of each group, in sorted-key order.
141
+ """
142
+ order = sorted(set(keys))
143
+ groups: Dict[Any, List[Any]] = {k: [] for k in order}
144
+ for v, k in zip(values, keys):
145
+ groups[k].append(v)
146
+ return [max(groups[k]) for k in order]
147
+
148
+
149
+ class StripSplit(StripNested):
150
+ """Strip that places different faceting variables on different sides.
151
+
152
+ Subclass of :class:`ggh4x.strip_nested.StripNested`. See the module
153
+ docstring for the per-side / id-span algorithm.
154
+
155
+ Attributes
156
+ ----------
157
+ params : dict
158
+ Adds ``position`` (a list of side names) to the base params.
159
+ """
160
+
161
+ _class_name = "StripSplit"
162
+
163
+ def setup(
164
+ self,
165
+ layout: pd.DataFrame,
166
+ params: Dict[str, Any],
167
+ theme: Any,
168
+ type: str,
169
+ ) -> None:
170
+ """Build a single ``vars`` frame (cols+rows) and delegate to get_strips.
171
+
172
+ Port of R ``StripSplit$setup`` (``strip_split.R:110-135``). Unlike the
173
+ base, split strips do not separate column from row variables.
174
+
175
+ Parameters
176
+ ----------
177
+ layout : pandas.DataFrame
178
+ The facet layout.
179
+ params : dict
180
+ Facet params (``facets`` for wrap; ``rows`` / ``cols`` for grid; plus
181
+ ``labeller``).
182
+ theme : Theme
183
+ The active theme.
184
+ type : str
185
+ ``"wrap"`` or ``"grid"`` (kept as ``type`` for facet compatibility).
186
+ """
187
+ self._set(elements=self.setup_elements(theme, type))
188
+
189
+ if type == "wrap":
190
+ facets = params.get("facets") or {}
191
+ facet_names = list(facets.keys()) if hasattr(facets, "keys") else list(facets)
192
+ if len(facet_names) == 0:
193
+ vars_frame = pd.DataFrame({"(all)": ["(all)"]})
194
+ layout_sel = layout
195
+ else:
196
+ vars_frame = layout[facet_names].reset_index(drop=True)
197
+ layout_sel = layout
198
+ else:
199
+ row_names = _names(params.get("rows"))
200
+ col_names = _names(params.get("cols"))
201
+ # R: union(names(rows), names(cols)) -- order preserved, dedup.
202
+ var_names: List[str] = []
203
+ for nm in row_names + col_names:
204
+ if nm not in var_names:
205
+ var_names.append(nm)
206
+ mask = _not_duplicated(layout, var_names)
207
+ layout_sel = layout.loc[mask]
208
+ vars_frame = layout_sel[var_names] if var_names else _empty_frame(layout_sel)
209
+
210
+ self.get_strips(
211
+ vars=vars_frame,
212
+ labeller=params.get("labeller"),
213
+ theme=theme,
214
+ params=self.params,
215
+ layout=layout_sel,
216
+ )
217
+
218
+ def get_strips( # type: ignore[override]
219
+ self,
220
+ vars: Any = None,
221
+ labeller: Any = None,
222
+ theme: Any = None,
223
+ params: Optional[Dict[str, Any]] = None,
224
+ layout: Optional[pd.DataFrame] = None,
225
+ ) -> None:
226
+ """Construct the per-side strips and span single-variable strips.
227
+
228
+ Port of R ``StripSplit$get_strips`` (``strip_split.R:137-213``). Note
229
+ the signature differs from the base ``get_strips`` (a single ``vars``
230
+ frame and ``layout``, not separate x / y).
231
+
232
+ Parameters
233
+ ----------
234
+ vars : pandas.DataFrame
235
+ The de-duplicated variable frame (columns = faceting variables).
236
+ labeller : callable or str
237
+ Labeller spec.
238
+ theme : Theme
239
+ Active theme.
240
+ params : dict
241
+ Strip params (carries ``position``).
242
+ layout : pandas.DataFrame
243
+ The de-duplicated layout (carries ``PANEL`` / ``ROW`` / ``COL``).
244
+ """
245
+ if empty(vars):
246
+ self._set(
247
+ strips={
248
+ "x": {"top": None, "bottom": None},
249
+ "y": {"left": None, "right": None},
250
+ }
251
+ )
252
+ return
253
+
254
+ positions = list(params["position"])
255
+ elem = self.elements
256
+ ncol_vars = vars.shape[1]
257
+ var_cols = list(vars.columns)
258
+
259
+ # Recycle position to the number of facet variables (with a warning).
260
+ if len(positions) != ncol_vars:
261
+ warnings.warn(
262
+ "The `position` argument in `strip_split()` is being recycled "
263
+ "to match the length of the facetting variables, as provided in "
264
+ "the `facets`, `rows`, or `cols` arguments in the facet function.",
265
+ stacklevel=2,
266
+ )
267
+ positions = [positions[i % len(positions)] for i in range(ncol_vars)]
268
+
269
+ # id() composite-key table controlling the strip hierarchy. R
270
+ # (strip_split.R:167-168): ids[[k]] <- id(vars[, 1:k]) — a CUMULATIVE
271
+ # composite of columns 1..k, not just column k. Using the single
272
+ # column k mis-merges strips whenever a non-first facet variable sits
273
+ # on its own side.
274
+ ids = pd.DataFrame(
275
+ {
276
+ var_cols[i]: np.asarray(id(vars[var_cols[: i + 1]]), dtype=int)
277
+ for i in range(ncol_vars)
278
+ }
279
+ )
280
+
281
+ layout = layout.reset_index(drop=True)
282
+ ids = ids.reset_index(drop=True)
283
+ vars = vars.reset_index(drop=True)
284
+
285
+ result: Dict[str, Any] = {}
286
+ for pos in ("top", "bottom", "left", "right"):
287
+ if pos not in positions:
288
+ result[pos] = None
289
+ continue
290
+
291
+ # Variables assigned to this side.
292
+ cn = [var_cols[i] for i in range(ncol_vars) if positions[i] == pos]
293
+ side_id_cols = [var_cols[i] for i in range(ncol_vars) if positions[i] == pos]
294
+
295
+ # De-duplicate by the composite id of this side's variables.
296
+ keep = _not_duplicated(ids[side_id_cols], side_id_cols)
297
+ lay_sel = layout.loc[keep].reset_index(drop=True)
298
+
299
+ # Format labels for the selected variables.
300
+ sub = lay_sel[cn].reset_index(drop=True)
301
+ lab = _format_labels(sub, labeller)
302
+ if pos == "right":
303
+ lab = lab[:, ::-1]
304
+
305
+ strp = self.assemble_strip(lab, pos, elem, params, lay_sel)
306
+
307
+ # Span single-variable strips across panels.
308
+ if len(cn) == 1:
309
+ col = cn[0]
310
+ key = list(ids[col])
311
+ t = [int(v) for v in strp["t"]]
312
+ b = [int(v) for v in strp["b"]]
313
+ l = [int(v) for v in strp["l"]]
314
+ r = [int(v) for v in strp["r"]]
315
+ if all(tt == bb for tt, bb in zip(t, b)):
316
+ max_row = _split_max(list(layout["ROW"]), key)
317
+ panel_at = _match_first(max_row, list(layout["ROW"]))
318
+ panel_vals = list(layout["PANEL"])
319
+ strp = strp.copy()
320
+ strp["b"] = [int(panel_vals[p - 1]) for p in panel_at]
321
+ if all(ll == rr for ll, rr in zip(l, r)):
322
+ max_col = _split_max(list(layout["COL"]), key)
323
+ panel_at = _match_first(max_col, list(layout["COL"]))
324
+ panel_vals = list(layout["PANEL"])
325
+ strp = strp.copy()
326
+ strp["r"] = [int(panel_vals[p - 1]) for p in panel_at]
327
+ result[pos] = strp
328
+
329
+ self._set(
330
+ strips={
331
+ "x": {"top": result["top"], "bottom": result["bottom"]},
332
+ "y": {"left": result["left"], "right": result["right"]},
333
+ }
334
+ )
335
+
336
+ def incorporate_grid(self, panels: Any, switch: Any) -> Any:
337
+ """Place all four sides independently into the panel gtable.
338
+
339
+ Port of R ``StripSplit$incorporate_grid`` (``strip_split.R:215-331``).
340
+ The four side-blocks re-query the panel-cell layout between insertions
341
+ because ``gtable_add_rows`` / ``gtable_add_cols`` shift indices.
342
+
343
+ Parameters
344
+ ----------
345
+ panels : Gtable
346
+ The assembled panel gtable.
347
+ switch : Any
348
+ Unused (split strips overrule ``switch``).
349
+
350
+ Returns
351
+ -------
352
+ Gtable
353
+ The panel gtable with all sides' strips inserted.
354
+ """
355
+ inside = self.elements["inside"]
356
+ padding = self.elements["padding"]
357
+ strips = self.strips
358
+
359
+ # --- top ----------------------------------------------------------
360
+ pos_cols = _panel_layout(panels)
361
+ side = strips["x"]["top"]
362
+ if side is not None:
363
+ strip = list(side["grobs"])
364
+ tbl = _tlbr(side)
365
+ names = ["strip-t-" + str(i + 1) for i in range(len(strip))]
366
+ stripheight = split_heights_cm(strip, tbl["t"])
367
+ where = [pos_cols["t"][ti] - 1 for ti in tbl["t"]]
368
+ if not inside["x"]:
369
+ where = [w - 1 for w in where]
370
+ for w in sorted(set(where), reverse=True):
371
+ panels = gtable_add_rows(panels, padding, w)
372
+ uniq = _unique(where)
373
+ where = [w + _match_first([w], uniq)[0] - 1 for w in where]
374
+ for w in sorted(set(where), reverse=True):
375
+ idx = [i for i, ww in enumerate(where) if ww == w]
376
+ panels = gtable_add_rows(panels, _unit_at(stripheight, idx[0]), w)
377
+ panels = gtable_add_grob(
378
+ panels,
379
+ [strip[i] for i in idx],
380
+ name=[names[i] for i in idx],
381
+ t=[where[i] + 1 for i in idx],
382
+ l=[pos_cols["l"][tbl["l"][i]] for i in idx],
383
+ r=[pos_cols["r"][tbl["r"][i]] for i in idx],
384
+ clip="on",
385
+ z=2,
386
+ )
387
+
388
+ # --- bottom -------------------------------------------------------
389
+ pos_cols = _panel_layout(panels)
390
+ side = strips["x"]["bottom"]
391
+ if side is not None:
392
+ strip = list(side["grobs"])
393
+ tbl = _tlbr(side)
394
+ names = ["strip-b-" + str(i + 1) for i in range(len(strip))]
395
+ stripheight = split_heights_cm(strip, tbl["t"])
396
+ where = [pos_cols["b"][bi] for bi in tbl["b"]]
397
+ if not inside["x"]:
398
+ where = [w + 1 for w in where]
399
+ for w in sorted(set(where), reverse=True):
400
+ panels = gtable_add_rows(panels, padding, w)
401
+ uniq = _unique(where)
402
+ where = [w + _match_first([w], uniq)[0] for w in where]
403
+ for w in sorted(set(where), reverse=True):
404
+ idx = [i for i, ww in enumerate(where) if ww == w]
405
+ panels = gtable_add_rows(panels, _unit_at(stripheight, idx[0]), w)
406
+ panels = gtable_add_grob(
407
+ panels,
408
+ [strip[i] for i in idx],
409
+ name=[names[i] for i in idx],
410
+ t=[where[i] + 1 for i in idx],
411
+ l=[pos_cols["l"][tbl["l"][i]] for i in idx],
412
+ r=[pos_cols["r"][tbl["r"][i]] for i in idx],
413
+ clip="on",
414
+ z=2,
415
+ )
416
+
417
+ # --- left ---------------------------------------------------------
418
+ pos_rows = _panel_layout(panels)
419
+ side = strips["y"]["left"]
420
+ if side is not None:
421
+ strip = list(side["grobs"])
422
+ tbl = _tlbr(side)
423
+ names = ["strip-l-" + str(i + 1) for i in range(len(strip))]
424
+ stripwidth = split_widths_cm(strip, tbl["l"])
425
+ where = [pos_rows["l"][li] - 2 for li in tbl["l"]]
426
+ if not inside["y"]:
427
+ for w in _unique(where):
428
+ panels = gtable_add_cols(panels, padding, w)
429
+ uniq = _unique(where)
430
+ where = [w + _match_first([w], uniq)[0] - 1 for w in where]
431
+ for w in sorted(set(where), reverse=True):
432
+ idx = [i for i, ww in enumerate(where) if ww == w]
433
+ panels = gtable_add_cols(panels, _unit_at(stripwidth, idx[0]), w)
434
+ panels = gtable_add_grob(
435
+ panels,
436
+ [strip[i] for i in idx],
437
+ name=[names[i] for i in idx],
438
+ t=[pos_rows["t"][tbl["t"][i]] for i in idx],
439
+ b=[pos_rows["b"][tbl["b"][i]] for i in idx],
440
+ l=[where[i] + 1 for i in idx],
441
+ clip="on",
442
+ z=2,
443
+ )
444
+
445
+ # --- right --------------------------------------------------------
446
+ pos_rows = _panel_layout(panels)
447
+ side = strips["y"]["right"]
448
+ if side is not None:
449
+ strip = list(side["grobs"])
450
+ tbl = _tlbr(side)
451
+ names = ["strip-r-" + str(i + 1) for i in range(len(strip))]
452
+ stripwidth = split_widths_cm(strip, tbl["r"])
453
+ where = [pos_rows["r"][ri] for ri in tbl["r"]]
454
+ if not inside["y"]:
455
+ where = [w + 1 for w in where]
456
+ for w in sorted(set(where), reverse=True):
457
+ panels = gtable_add_cols(panels, padding, w)
458
+ uniq = _unique(where)
459
+ where = [w + _match_first([w], uniq)[0] for w in where]
460
+ for w in sorted(set(where), reverse=True):
461
+ idx = [i for i, ww in enumerate(where) if ww == w]
462
+ panels = gtable_add_cols(panels, _unit_at(stripwidth, idx[0]), w)
463
+ panels = gtable_add_grob(
464
+ panels,
465
+ [strip[i] for i in idx],
466
+ name=[names[i] for i in idx],
467
+ t=[pos_rows["t"][tbl["t"][i]] for i in idx],
468
+ b=[pos_rows["b"][tbl["b"][i]] for i in idx],
469
+ l=[where[i] + 1 for i in idx],
470
+ clip="on",
471
+ z=2,
472
+ )
473
+
474
+ return panels
475
+
476
+ def incorporate_wrap(
477
+ self,
478
+ panels: Any,
479
+ position: str,
480
+ clip: str = "off",
481
+ sizes: Optional[Dict[str, Any]] = None,
482
+ ) -> Any:
483
+ """Reuse the grid placement algorithm for wrapped facets.
484
+
485
+ Port of R ``StripSplit$incorporate_wrap`` (``strip_split.R:333-337``).
486
+ The ``clip`` / ``sizes`` arguments are accepted for facet-call
487
+ compatibility but ignored (only ``panels`` is forwarded).
488
+
489
+ Parameters
490
+ ----------
491
+ panels : Gtable
492
+ The assembled panel gtable.
493
+ position : str
494
+ Unused.
495
+ clip : str, default ``"off"``
496
+ Unused.
497
+ sizes : dict, optional
498
+ Unused.
499
+
500
+ Returns
501
+ -------
502
+ Gtable
503
+ The panel gtable with all sides' strips inserted.
504
+ """
505
+ return self.incorporate_grid(panels, False)
506
+
507
+
508
+ # ---------------------------------------------------------------------------
509
+ # Helpers
510
+ # ---------------------------------------------------------------------------
511
+ def _names(param: Any) -> List[str]:
512
+ """Return the variable names from a facet ``rows`` / ``cols`` param.
513
+
514
+ Parameters
515
+ ----------
516
+ param : Any
517
+ A ``rows`` / ``cols`` spec (mapping, list, or ``None``).
518
+
519
+ Returns
520
+ -------
521
+ list of str
522
+ """
523
+ if param is None:
524
+ return []
525
+ if hasattr(param, "keys"):
526
+ return list(param.keys())
527
+ if isinstance(param, (list, tuple)):
528
+ return [str(p) for p in param]
529
+ return []
530
+
531
+
532
+ def _empty_frame(df: pd.DataFrame) -> pd.DataFrame:
533
+ """Return a 0-column frame with the same row count as *df*."""
534
+ return pd.DataFrame(index=df.index)
535
+
536
+
537
+ def _not_duplicated(frame: pd.DataFrame, names: Sequence[str]) -> np.ndarray:
538
+ """Port of R ``!duplicated(frame[names])`` (boolean keep-mask).
539
+
540
+ Parameters
541
+ ----------
542
+ frame : pandas.DataFrame
543
+ The frame to de-duplicate.
544
+ names : sequence of str
545
+ Columns to de-duplicate on.
546
+
547
+ Returns
548
+ -------
549
+ numpy.ndarray of bool
550
+ ``True`` for the first occurrence of each unique combination.
551
+ """
552
+ n = frame.shape[0]
553
+ names = list(names)
554
+ if not names:
555
+ mask = np.zeros(n, dtype=bool)
556
+ if n > 0:
557
+ mask[0] = True
558
+ return mask
559
+ return ~frame[names].duplicated().to_numpy()
560
+
561
+
562
+ def _tlbr(side: pd.DataFrame) -> Dict[str, List[int]]:
563
+ """Return ``{t,l,b,r}`` panel-id lists from a built strip side.
564
+
565
+ Parameters
566
+ ----------
567
+ side : pandas.DataFrame
568
+ A built strip placement frame.
569
+
570
+ Returns
571
+ -------
572
+ dict
573
+ """
574
+ return {
575
+ "t": [int(v) for v in side["t"]],
576
+ "l": [int(v) for v in side["l"]],
577
+ "b": [int(v) for v in side["b"]],
578
+ "r": [int(v) for v in side["r"]],
579
+ }
580
+
581
+
582
+ def _unique(seq: Sequence[Any]) -> List[Any]:
583
+ """Port of R ``unique()`` -- first-occurrence order preserved.
584
+
585
+ Parameters
586
+ ----------
587
+ seq : sequence
588
+
589
+ Returns
590
+ -------
591
+ list
592
+ """
593
+ seen: Dict[Any, None] = {}
594
+ for v in seq:
595
+ if v not in seen:
596
+ seen[v] = None
597
+ return list(seen.keys())
598
+
599
+
600
+ def _unit_at(u: Any, i: int) -> Any:
601
+ """Return the single unit at 0-based position *i* (R ``u[i+1]``).
602
+
603
+ Parameters
604
+ ----------
605
+ u : grid_py.Unit
606
+ Source unit vector.
607
+ i : int
608
+ 0-based index.
609
+
610
+ Returns
611
+ -------
612
+ grid_py.Unit
613
+ """
614
+ n = len(u)
615
+ if n <= 1:
616
+ return u
617
+ return u[i % n]
618
+
619
+
620
+ # R's ``StripSplit`` ggproto instance used as the parent of every clone.
621
+ _STRIP_SPLIT_SINGLETON: "StripSplit" = StripSplit()
622
+
623
+
624
+ def strip_split(
625
+ position: Any = ("top", "left"),
626
+ clip: str = "inherit",
627
+ size: str = "constant",
628
+ bleed: bool = False,
629
+ text_x: Any = None,
630
+ text_y: Any = None,
631
+ background_x: Any = None,
632
+ background_y: Any = None,
633
+ by_layer_x: bool = False,
634
+ by_layer_y: bool = False,
635
+ ) -> StripSplit:
636
+ """Create a split strip (per-variable side placement).
637
+
638
+ Port of R ``strip_split()`` (``strip_split.R:65-99``).
639
+
640
+ Parameters
641
+ ----------
642
+ position : sequence of str, default ``("top", "left")``
643
+ Where each faceting variable's strip is placed; each of ``"top"`` /
644
+ ``"bottom"`` / ``"left"`` / ``"right"``. Recycled to the number of
645
+ variables (with a warning) when the lengths differ.
646
+ clip : str, default ``"inherit"``
647
+ Whether labels are clipped to background boxes.
648
+ size : str, default ``"constant"``
649
+ Whether strip margins across layers are ``"constant"`` or ``"variable"``.
650
+ bleed : bool, default ``False``
651
+ Whether lower-layer strips may merge across higher-layer boundaries.
652
+ text_x, text_y, background_x, background_y : list or element or None
653
+ Per-strip themed elements (see :func:`ggh4x.strip_themed.strip_themed`).
654
+ by_layer_x, by_layer_y : bool, default ``False``
655
+ Map elements to layers (``True``) or strips (``False``).
656
+
657
+ Returns
658
+ -------
659
+ StripSplit
660
+ A ``StripSplit`` ggproto instance usable in ggh4x facets.
661
+ """
662
+ if isinstance(position, str):
663
+ position = [position]
664
+ params = {
665
+ "clip": arg_match0(clip, ["on", "off", "inherit"], arg_name="clip"),
666
+ "size": arg_match0(size, ["constant", "variable"], arg_name="size"),
667
+ "bleed": bool(bleed),
668
+ "position": _arg_match_multiple(
669
+ list(position),
670
+ ["top", "bottom", "left", "right"],
671
+ arg_name="position",
672
+ ),
673
+ }
674
+ given_elements = {
675
+ "text_x": validate_element_list(text_x, "element_text"),
676
+ "text_y": validate_element_list(text_y, "element_text"),
677
+ "background_x": validate_element_list(background_x, "element_rect"),
678
+ "background_y": validate_element_list(background_y, "element_rect"),
679
+ "by_layer_x": bool(by_layer_x),
680
+ "by_layer_y": bool(by_layer_y),
681
+ }
682
+ return ggproto(
683
+ None,
684
+ _STRIP_SPLIT_SINGLETON,
685
+ params=params,
686
+ given_elements=given_elements,
687
+ )