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/_facet_utils.py ADDED
@@ -0,0 +1,649 @@
1
+ """Facet-layout helpers not present in ``gtable_py`` / ``ggplot2_py``.
2
+
3
+ R sources:
4
+
5
+ - ``panel_cols`` / ``panel_rows`` — ggplot2 internal (``R/facet-.R``). Locate panel
6
+ cells in an assembled gtable by name and return their unique column / row spans.
7
+ - ``weave_tables_row`` / ``weave_tables_col`` — ggplot2 internal, copied into ggh4x
8
+ ``R/borrowed_ggplot2.R`` (matrix-style axis-band weaving for ``facet_grid2`` /
9
+ ``facet_wrap2``).
10
+ - ``weave_panel_rows`` / ``weave_panel_cols`` — ggh4x ``R/utils_gtable.R``
11
+ (data-frame-style axis-band weaving for ``facet_manual`` / strips).
12
+ - ``split_heights_cm`` / ``split_widths_cm`` — ggh4x ``R/utils_grid.R``.
13
+ - ``df_grid`` (R ``df.grid``) — ggh4x ``R/borrowed_ggplot2.R`` (cross-product of two
14
+ data frames, used by ``FacetGrid2.compute_layout``).
15
+ - ``render_axes`` — ggplot2 internal batch axis renderer (absent from ``ggplot2_py``).
16
+
17
+ This module is the shared foundation both ``FacetGrid2`` and ``FacetWrap2`` build on.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any, Dict, List, Optional, Sequence
23
+
24
+ import numpy as np
25
+ import pandas as pd
26
+
27
+ from grid_py import Unit, null_grob
28
+ from gtable_py import gtable_add_cols, gtable_add_grob, gtable_add_rows
29
+ from gtable_py._utils import height_cm, width_cm
30
+
31
+ __all__ = [
32
+ "panel_cols",
33
+ "panel_rows",
34
+ "render_axes",
35
+ "weave_tables_row",
36
+ "weave_tables_col",
37
+ "weave_panel_rows",
38
+ "weave_panel_cols",
39
+ "split_heights_cm",
40
+ "split_widths_cm",
41
+ "df_grid",
42
+ ]
43
+
44
+
45
+ def _layout_frame(table: Any) -> pd.DataFrame:
46
+ """Return a gtable's layout as a DataFrame regardless of its native form.
47
+
48
+ ``gtable_py`` stores ``Gtable.layout`` as a ``dict`` of parallel lists
49
+ (keys ``t, l, b, r, z, clip, name``); R / some callers use a DataFrame. Normalise.
50
+ """
51
+ lay = table.layout
52
+ if isinstance(lay, pd.DataFrame):
53
+ return lay
54
+ return pd.DataFrame({k: list(v) for k, v in lay.items()})
55
+
56
+
57
+ def panel_cols(table: Any) -> pd.DataFrame:
58
+ """Return the unique ``(l, r)`` column spans of a gtable's panel cells.
59
+
60
+ Mirrors ggplot2's internal ``panel_cols``: filter ``table$layout`` to rows whose
61
+ ``name`` begins with ``"panel"``, then take unique ``l``/``r`` pairs in
62
+ first-occurrence order (R ``unique()`` semantics, not sorted).
63
+
64
+ Parameters
65
+ ----------
66
+ table : Gtable
67
+ An assembled panel gtable whose panel cells are named ``"panel-*"``.
68
+
69
+ Returns
70
+ -------
71
+ pandas.DataFrame
72
+ Columns ``l`` and ``r``; one row per distinct panel column span.
73
+ """
74
+ lay = _layout_frame(table)
75
+ mask = lay["name"].astype(str).str.match(r"^panel")
76
+ return lay.loc[mask, ["l", "r"]].drop_duplicates().reset_index(drop=True)
77
+
78
+
79
+ def panel_rows(table: Any) -> pd.DataFrame:
80
+ """Return the unique ``(t, b)`` row spans of a gtable's panel cells.
81
+
82
+ Mirrors ggplot2's internal ``panel_rows`` (see :func:`panel_cols`).
83
+
84
+ Parameters
85
+ ----------
86
+ table : Gtable
87
+ An assembled panel gtable whose panel cells are named ``"panel-*"``.
88
+
89
+ Returns
90
+ -------
91
+ pandas.DataFrame
92
+ Columns ``t`` and ``b``; one row per distinct panel row span.
93
+ """
94
+ lay = _layout_frame(table)
95
+ mask = lay["name"].astype(str).str.match(r"^panel")
96
+ return lay.loc[mask, ["t", "b"]].drop_duplicates().reset_index(drop=True)
97
+
98
+
99
+ # --- batch axis renderer (ggplot2 internal ``render_axes``) ------------------
100
+
101
+
102
+ def render_axes(
103
+ x_ranges: Optional[Sequence[Any]],
104
+ y_ranges: Optional[Sequence[Any]],
105
+ coord: Any,
106
+ theme: Any,
107
+ transpose: bool = False,
108
+ ) -> Dict[str, Any]:
109
+ """Render a batch of panel axes, mirroring ggplot2's internal ``render_axes``.
110
+
111
+ This is the ggplot2-internal helper ``ggplot2:::render_axes`` which
112
+ ``ggplot2_py`` does not expose. It loops the coord's per-panel axis
113
+ renderers over the supplied panel-parameter (range) lists.
114
+
115
+ For every element of ``x_ranges`` it calls ``coord.render_axis_h(pp, theme)``
116
+ (returning ``{"top": grob, "bottom": grob}``) and for every element of
117
+ ``y_ranges`` it calls ``coord.render_axis_v(pp, theme)`` (returning
118
+ ``{"left": grob, "right": grob}``).
119
+
120
+ Parameters
121
+ ----------
122
+ x_ranges : sequence of dict or None
123
+ Per-panel ``panel_params`` dicts used to render horizontal (x) axes. If
124
+ ``None``, the horizontal axes are not rendered (see *Notes* for how this
125
+ interacts with ``transpose``).
126
+ y_ranges : sequence of dict or None
127
+ Per-panel ``panel_params`` dicts used to render vertical (y) axes. If
128
+ ``None``, the vertical axes are not rendered.
129
+ coord : Coord
130
+ A ``ggplot2_py`` coordinate system providing ``render_axis_h`` and
131
+ ``render_axis_v`` methods.
132
+ theme : Theme
133
+ The resolved plot theme passed through to the coord's axis renderers.
134
+ transpose : bool, default False
135
+ Controls the shape of the result. When ``False`` the per-panel
136
+ ``{top, bottom}`` / ``{left, right}`` dicts are returned grouped per panel.
137
+ When ``True`` they are transposed to per-side lists, matching the shape
138
+ the ggh4x facets consume.
139
+
140
+ Returns
141
+ -------
142
+ dict
143
+ When ``transpose`` is ``False``:
144
+
145
+ ``{"x": [{"top": .., "bottom": ..}, ...], "y": [{"left": .., "right": ..}, ...]}``
146
+
147
+ — and the ``"x"`` / ``"y"`` keys are only present when the corresponding
148
+ ranges argument is not ``None``.
149
+
150
+ When ``transpose`` is ``True``:
151
+
152
+ ``{"x": {"top": [...], "bottom": [...]}, "y": {"left": [...], "right": [...]}}``
153
+
154
+ — both keys are always present (with empty per-side lists when the
155
+ corresponding ranges argument is ``None``), faithfully reproducing R's
156
+ ``lapply(NULL, ...)`` behaviour.
157
+
158
+ Notes
159
+ -----
160
+ R source (``ggplot2:::render_axes``)::
161
+
162
+ axes <- list()
163
+ if (!is.null(x)) axes$x <- lapply(x, coord$render_axis_h, theme)
164
+ if (!is.null(y)) axes$y <- lapply(y, coord$render_axis_v, theme)
165
+ if (transpose) {
166
+ axes <- list(
167
+ x = list(top = lapply(axes$x, `[[`, "top"),
168
+ bottom = lapply(axes$x, `[[`, "bottom")),
169
+ y = list(left = lapply(axes$y, `[[`, "left"),
170
+ right = lapply(axes$y, `[[`, "right")))
171
+ }
172
+ axes
173
+
174
+ The asymmetry between the two modes is deliberate and matches R: in
175
+ non-transposed mode an absent side is simply omitted from the dict, whereas
176
+ in transposed mode the side keys always exist (with empty lists) because the
177
+ transpose block unconditionally rebuilds the nested structure from a possibly
178
+ ``NULL`` ``axes$x`` / ``axes$y``.
179
+ """
180
+ axes: Dict[str, Any] = {}
181
+ if x_ranges is not None:
182
+ axes["x"] = [coord.render_axis_h(pp, theme) for pp in x_ranges]
183
+ if y_ranges is not None:
184
+ axes["y"] = [coord.render_axis_v(pp, theme) for pp in y_ranges]
185
+
186
+ if transpose:
187
+ x_list = axes.get("x", [])
188
+ y_list = axes.get("y", [])
189
+ axes = {
190
+ "x": {
191
+ "top": [a["top"] for a in x_list],
192
+ "bottom": [a["bottom"] for a in x_list],
193
+ },
194
+ "y": {
195
+ "left": [a["left"] for a in y_list],
196
+ "right": [a["right"] for a in y_list],
197
+ },
198
+ }
199
+ return axes
200
+
201
+
202
+ # --- matrix-style weaving (ggplot2 internal, ggh4x borrowed_ggplot2.R) -------
203
+
204
+
205
+ def _matrix_col(matrix: Sequence[Sequence[Any]], j: int, nrow: int) -> List[Any]:
206
+ """Extract column ``j`` (0-based) across all rows of a row-major matrix.
207
+
208
+ Mirrors R's ``matrix[, j]`` for a grob matrix stored as a list-of-lists in
209
+ row-major order (``matrix[row][col]``).
210
+ """
211
+ return [matrix[r][j] for r in range(nrow)]
212
+
213
+
214
+ def weave_tables_col(
215
+ table: Any,
216
+ table2: Optional[Sequence[Sequence[Any]]] = None,
217
+ col_shift: int = 0,
218
+ col_width: Optional[Unit] = None,
219
+ name: str = "",
220
+ z: float = 1,
221
+ clip: str = "off",
222
+ ) -> Any:
223
+ """Weave a column of axis grobs into a panel gtable, one per panel column.
224
+
225
+ Faithful port of ggplot2's internal ``weave_tables_col`` (copied into ggh4x's
226
+ ``R/borrowed_ggplot2.R``). For each panel column (right-to-left) it inserts a
227
+ new gtable column at ``panel_l + col_shift`` and, if ``table2`` is supplied,
228
+ places that column's grobs (one per panel row) into the freshly inserted
229
+ column.
230
+
231
+ Parameters
232
+ ----------
233
+ table : Gtable
234
+ Panel gtable whose panel cells are named ``"panel-*"``.
235
+ table2 : sequence of sequence of grob, optional
236
+ A grob matrix in row-major order (``table2[row][col]``) with one row per
237
+ panel row and one column per panel column. When omitted, only empty
238
+ columns are inserted (no grobs placed).
239
+ col_shift : int, default 0
240
+ Offset relative to each panel column's left index at which the new column
241
+ is inserted (R: ``-1`` for left axes, ``0`` for right axes).
242
+ col_width : Unit, optional
243
+ Widths (length = number of panel columns) for the inserted columns.
244
+ name : str, default ""
245
+ Prefix for the inserted grob names (``"{name}-{row}-{col}"``).
246
+ z : float, default 1
247
+ Drawing order of the inserted grobs (ggh4x uses ``3`` so axes sit above
248
+ panel backgrounds).
249
+ clip : str, default "off"
250
+ Clipping for the inserted grobs.
251
+
252
+ Returns
253
+ -------
254
+ Gtable
255
+ ``table`` augmented with the inserted axis columns / grobs.
256
+ """
257
+ panel_col = list(panel_cols(table)["l"])
258
+ panel_row = list(panel_rows(table)["t"])
259
+ nrow = len(panel_row)
260
+ for i in reversed(range(len(panel_col))):
261
+ col_ind = int(panel_col[i]) + col_shift
262
+ table = gtable_add_cols(table, col_width[i], pos=col_ind)
263
+ if table2 is not None:
264
+ grobs = _matrix_col(table2, i, nrow)
265
+ table = gtable_add_grob(
266
+ table,
267
+ grobs,
268
+ t=[int(x) for x in panel_row],
269
+ l=col_ind + 1,
270
+ clip=clip,
271
+ name=[f"{name}-{k + 1}-{i + 1}" for k in range(len(panel_row))],
272
+ z=z,
273
+ )
274
+ return table
275
+
276
+
277
+ def weave_tables_row(
278
+ table: Any,
279
+ table2: Optional[Sequence[Sequence[Any]]] = None,
280
+ row_shift: int = 0,
281
+ row_height: Optional[Unit] = None,
282
+ name: str = "",
283
+ z: float = 1,
284
+ clip: str = "off",
285
+ ) -> Any:
286
+ """Weave a row of axis grobs into a panel gtable, one per panel row.
287
+
288
+ Faithful port of ggplot2's internal ``weave_tables_row`` (copied into ggh4x's
289
+ ``R/borrowed_ggplot2.R``). For each panel row (bottom-to-top) it inserts a new
290
+ gtable row at ``panel_t + row_shift`` and, if ``table2`` is supplied, places
291
+ that row's grobs (one per panel column) into the freshly inserted row.
292
+
293
+ Parameters
294
+ ----------
295
+ table : Gtable
296
+ Panel gtable whose panel cells are named ``"panel-*"``.
297
+ table2 : sequence of sequence of grob, optional
298
+ A grob matrix in row-major order (``table2[row][col]``) with one row per
299
+ panel row and one column per panel column. When omitted, only empty rows
300
+ are inserted (no grobs placed).
301
+ row_shift : int, default 0
302
+ Offset relative to each panel row's top index at which the new row is
303
+ inserted (R: ``-1`` for top axes, ``0`` for bottom axes).
304
+ row_height : Unit, optional
305
+ Heights (length = number of panel rows) for the inserted rows.
306
+ name : str, default ""
307
+ Prefix for the inserted grob names (``"{name}-{col}-{row}"``).
308
+ z : float, default 1
309
+ Drawing order of the inserted grobs.
310
+ clip : str, default "off"
311
+ Clipping for the inserted grobs.
312
+
313
+ Returns
314
+ -------
315
+ Gtable
316
+ ``table`` augmented with the inserted axis rows / grobs.
317
+ """
318
+ panel_col = list(panel_cols(table)["l"])
319
+ panel_row = list(panel_rows(table)["t"])
320
+ for i in reversed(range(len(panel_row))):
321
+ row_ind = int(panel_row[i]) + row_shift
322
+ table = gtable_add_rows(table, row_height[i], pos=row_ind)
323
+ if table2 is not None:
324
+ grobs = list(table2[i])
325
+ table = gtable_add_grob(
326
+ table,
327
+ grobs,
328
+ t=row_ind + 1,
329
+ l=[int(x) for x in panel_col],
330
+ clip=clip,
331
+ name=[f"{name}-{k + 1}-{i + 1}" for k in range(len(panel_col))],
332
+ z=z,
333
+ )
334
+ return table
335
+
336
+
337
+ # --- data-frame-style weaving (ggh4x utils_gtable.R) ------------------------
338
+
339
+
340
+ def _panel_layout_frame(table: Any) -> pd.DataFrame:
341
+ """Return the ``"panel-*"`` rows of a gtable layout as a DataFrame."""
342
+ lay = _layout_frame(table)
343
+ mask = lay["name"].astype(str).str.match(r"^panel")
344
+ return lay.loc[mask].reset_index(drop=True)
345
+
346
+
347
+ def weave_panel_rows(
348
+ table: Any,
349
+ table2: Optional[pd.DataFrame] = None,
350
+ row_shift: int = 0,
351
+ row_height: Optional[Unit] = None,
352
+ name: str = "",
353
+ z: float = 1,
354
+ clip: str = "off",
355
+ pos: Optional[str] = None,
356
+ grob_var: str = "grobs",
357
+ ) -> Any:
358
+ """Insert rows into a panel table relative to the panels (ggh4x weave).
359
+
360
+ Faithful port of ggh4x's ``weave_panel_rows`` (``R/utils_gtable.R``). Unlike
361
+ :func:`weave_tables_row` (which consumes a grob *matrix*), this consumes a
362
+ *data frame* ``table2`` with integer columns ``t``, ``b``, ``l``, ``r``
363
+ indexing into the (sorted-unique) panel positions, plus a list-column named by
364
+ ``grob_var`` holding the grobs to place. This shape is used by
365
+ ``facet_manual`` and the strip subsystem.
366
+
367
+ Parameters
368
+ ----------
369
+ table : Gtable
370
+ Panel gtable whose panel cells are named ``"panel-*"``.
371
+ table2 : pandas.DataFrame, optional
372
+ Frame with integer columns ``t``, ``b``, ``l``, ``r`` (1-based indices
373
+ into the panels) and a list-column ``grob_var``. When omitted, only empty
374
+ rows are inserted.
375
+ row_shift : int, default 0
376
+ Offset relative to the panel position ``pos`` at which to insert rows.
377
+ row_height : Unit, optional
378
+ Heights for the inserted rows (length = number of unique panel rows).
379
+ name : str, default ""
380
+ Prefix for the inserted grob names.
381
+ z : float, default 1
382
+ Drawing order of the inserted grobs.
383
+ clip : str, default "off"
384
+ Clipping for the inserted grobs.
385
+ pos : {"t", "b"} or None, default None
386
+ Which panel edge to index against. ``None`` is interpreted verbatim as
387
+ ``"t"`` with the opposite edge ``"b"`` used for the grob's bottom; when
388
+ given, the same edge is used for both top and bottom of placed grobs.
389
+ grob_var : str, default "grobs"
390
+ Name of the list-column in ``table2`` holding the grobs.
391
+
392
+ Returns
393
+ -------
394
+ Gtable
395
+ ``table`` augmented with the inserted rows / grobs.
396
+ """
397
+ if pos is None:
398
+ pos = "t"
399
+ alt = "b"
400
+ else:
401
+ alt = pos
402
+
403
+ rows = panel_rows(table)
404
+ rows = sorted(set(int(v) for v in rows[pos]))
405
+
406
+ for i in reversed(range(len(rows))):
407
+ table = gtable_add_rows(table, row_height[i], pos=rows[i] + row_shift)
408
+
409
+ if table2 is not None:
410
+ if row_shift > -1:
411
+ row_shift = 1 + row_shift
412
+ panels = _panel_layout_frame(table)
413
+ panels["t"] = panels["t"] + row_shift
414
+ panels["b"] = panels["b"] + row_shift
415
+
416
+ t_idx = [int(x) for x in table2["t"]]
417
+ b_idx = [int(x) for x in table2["b"]]
418
+ l_idx = [int(x) for x in table2["l"]]
419
+ r_idx = [int(x) for x in table2["r"]]
420
+ n = len(l_idx)
421
+
422
+ table = gtable_add_grob(
423
+ table,
424
+ list(table2[grob_var]),
425
+ t=[int(panels[pos].iloc[k - 1]) for k in t_idx],
426
+ b=[int(panels[alt].iloc[k - 1]) for k in b_idx],
427
+ l=[int(panels["l"].iloc[k - 1]) for k in l_idx],
428
+ r=[int(panels["r"].iloc[k - 1]) for k in r_idx],
429
+ clip=clip,
430
+ z=z,
431
+ name=[f"{name}-{k + 1}-{k + 1}" for k in range(n)],
432
+ )
433
+ return table
434
+
435
+
436
+ def weave_panel_cols(
437
+ table: Any,
438
+ table2: Optional[pd.DataFrame] = None,
439
+ col_shift: int = 0,
440
+ col_width: Optional[Unit] = None,
441
+ name: str = "",
442
+ z: float = 1,
443
+ clip: str = "off",
444
+ pos: Optional[str] = None,
445
+ grob_var: str = "grobs",
446
+ ) -> Any:
447
+ """Insert columns into a panel table relative to the panels (ggh4x weave).
448
+
449
+ Faithful port of ggh4x's ``weave_panel_cols`` (``R/utils_gtable.R``); the
450
+ column-wise counterpart of :func:`weave_panel_rows`.
451
+
452
+ Parameters
453
+ ----------
454
+ table : Gtable
455
+ Panel gtable whose panel cells are named ``"panel-*"``.
456
+ table2 : pandas.DataFrame, optional
457
+ Frame with integer columns ``t``, ``b``, ``l``, ``r`` (1-based indices
458
+ into the panels) and a list-column ``grob_var``. When omitted, only empty
459
+ columns are inserted.
460
+ col_shift : int, default 0
461
+ Offset relative to the panel position ``pos`` at which to insert columns.
462
+ col_width : Unit, optional
463
+ Widths for the inserted columns (length = number of unique panel cols).
464
+ name : str, default ""
465
+ Prefix for the inserted grob names.
466
+ z : float, default 1
467
+ Drawing order of the inserted grobs.
468
+ clip : str, default "off"
469
+ Clipping for the inserted grobs.
470
+ pos : {"l", "r"} or None, default None
471
+ Which panel edge to index against. ``None`` is interpreted verbatim as
472
+ ``"l"`` with the opposite edge ``"r"`` used for the grob's right edge.
473
+ grob_var : str, default "grobs"
474
+ Name of the list-column in ``table2`` holding the grobs.
475
+
476
+ Returns
477
+ -------
478
+ Gtable
479
+ ``table`` augmented with the inserted columns / grobs.
480
+ """
481
+ if pos is None:
482
+ pos = "l"
483
+ alt = "r"
484
+ else:
485
+ alt = pos
486
+
487
+ cols = panel_cols(table)
488
+ cols = sorted(set(int(v) for v in cols[pos]))
489
+
490
+ for i in reversed(range(len(cols))):
491
+ table = gtable_add_cols(table, col_width[i], pos=cols[i] + col_shift)
492
+
493
+ if table2 is not None:
494
+ if col_shift > -1:
495
+ col_shift = 1 + col_shift
496
+ panels = _panel_layout_frame(table)
497
+ panels["l"] = panels["l"] + col_shift
498
+ panels["r"] = panels["r"] + col_shift
499
+
500
+ t_idx = [int(x) for x in table2["t"]]
501
+ b_idx = [int(x) for x in table2["b"]]
502
+ l_idx = [int(x) for x in table2["l"]]
503
+ r_idx = [int(x) for x in table2["r"]]
504
+ n = len(t_idx)
505
+
506
+ table = gtable_add_grob(
507
+ table,
508
+ list(table2[grob_var]),
509
+ t=[int(panels["t"].iloc[k - 1]) for k in t_idx],
510
+ b=[int(panels["b"].iloc[k - 1]) for k in b_idx],
511
+ l=[int(panels[pos].iloc[k - 1]) for k in l_idx],
512
+ r=[int(panels[alt].iloc[k - 1]) for k in r_idx],
513
+ clip=clip,
514
+ z=z,
515
+ name=[f"{name}-{k + 1}-{k + 1}" for k in range(n)],
516
+ )
517
+ return table
518
+
519
+
520
+ # --- grob-size splitting (ggh4x utils_grid.R) -------------------------------
521
+
522
+
523
+ def _split_max_cm(values: Sequence[float], split: Sequence[Any]) -> List[float]:
524
+ """Group ``values`` by ``split`` and return the max per group.
525
+
526
+ Mirrors R's ``vapply(split(values, split, drop = TRUE), max, numeric(1))``:
527
+ groups are ordered by the *sorted unique* keys of ``split`` (or by factor
528
+ level order, with unused levels dropped, when ``split`` is categorical).
529
+ """
530
+ values = list(values)
531
+ if isinstance(split, pd.Categorical):
532
+ # Factor: preserve level order, drop unused levels (drop = TRUE).
533
+ cats = pd.Categorical(split)
534
+ keys = [c for c in cats.categories if (cats == c).any()]
535
+ codes = list(cats)
536
+ elif isinstance(split, pd.Series) and isinstance(split.dtype, pd.CategoricalDtype):
537
+ cats = split.cat
538
+ present = pd.unique(split.dropna())
539
+ keys = [c for c in cats.categories if c in set(present)]
540
+ codes = list(split)
541
+ else:
542
+ codes = list(split)
543
+ # sorted unique of non-NA keys (R's split orders by sort(unique)).
544
+ keys = sorted(set(k for k in codes if not pd.isna(k)))
545
+
546
+ out: List[float] = []
547
+ for key in keys:
548
+ group = [v for v, c in zip(values, codes) if c == key]
549
+ out.append(max(group))
550
+ return out
551
+
552
+
553
+ def split_heights_cm(grobs: Sequence[Any], split: Sequence[Any]) -> Unit:
554
+ """Group grobs and report the max height (cm) per group.
555
+
556
+ Faithful port of ggh4x's ``split_heights_cm`` (``R/utils_grid.R``): measure
557
+ each grob's height in centimetres (``grobHeight`` -> ``convertHeight``), then
558
+ return, per ``split`` group, the maximum height as a ``"cm"`` :class:`Unit`.
559
+
560
+ Parameters
561
+ ----------
562
+ grobs : sequence of grob
563
+ Grobs whose heights are measured.
564
+ split : sequence
565
+ Grouping vector (same length as ``grobs``). Group ordering follows R's
566
+ ``split`` (sorted-unique keys, or factor level order with unused levels
567
+ dropped).
568
+
569
+ Returns
570
+ -------
571
+ Unit
572
+ Centimetre heights, one per group, in group order.
573
+ """
574
+ vals = [height_cm(g) for g in grobs]
575
+ out = _split_max_cm(vals, split)
576
+ return Unit(out, "cm")
577
+
578
+
579
+ def split_widths_cm(grobs: Sequence[Any], split: Sequence[Any]) -> Unit:
580
+ """Group grobs and report the max width (cm) per group.
581
+
582
+ Faithful port of ggh4x's ``split_widths_cm`` (``R/utils_grid.R``); the
583
+ width-wise counterpart of :func:`split_heights_cm`.
584
+
585
+ Parameters
586
+ ----------
587
+ grobs : sequence of grob
588
+ Grobs whose widths are measured.
589
+ split : sequence
590
+ Grouping vector (same length as ``grobs``); see :func:`split_heights_cm`.
591
+
592
+ Returns
593
+ -------
594
+ Unit
595
+ Centimetre widths, one per group, in group order.
596
+ """
597
+ vals = [width_cm(g) for g in grobs]
598
+ out = _split_max_cm(vals, split)
599
+ return Unit(out, "cm")
600
+
601
+
602
+ # --- cross-product of data frames (ggh4x df.grid) ---------------------------
603
+
604
+
605
+ def df_grid(a: Optional[pd.DataFrame], b: Optional[pd.DataFrame]) -> pd.DataFrame:
606
+ """Cross-product (Cartesian join) of two data frames.
607
+
608
+ Faithful port of ggh4x's ``df.grid`` (``R/borrowed_ggplot2.R``). When either
609
+ frame is ``None`` or empty the other is returned unchanged. Otherwise every
610
+ row of ``a`` is combined with every row of ``b``, with ``a``'s row index
611
+ varying fastest (R's ``expand.grid(i_a, i_b)`` order).
612
+
613
+ Parameters
614
+ ----------
615
+ a : pandas.DataFrame or None
616
+ Left frame.
617
+ b : pandas.DataFrame or None
618
+ Right frame.
619
+
620
+ Returns
621
+ -------
622
+ pandas.DataFrame
623
+ Column-bound cross-product (``a`` columns followed by ``b`` columns) with
624
+ ``len(a) * len(b)`` rows and a fresh ``RangeIndex``.
625
+
626
+ Notes
627
+ -----
628
+ R source::
629
+
630
+ df.grid = function(a, b) {
631
+ if (is.null(a) || nrow(a) == 0) return(b)
632
+ if (is.null(b) || nrow(b) == 0) return(a)
633
+ indexes <- expand.grid(i_a = seq_len(nrow(a)), i_b = seq_len(nrow(b)))
634
+ vec_cbind(unrowname(a[indexes$i_a, ]), unrowname(b[indexes$i_b, ]))
635
+ }
636
+ """
637
+ if a is None or len(a) == 0:
638
+ return b
639
+ if b is None or len(b) == 0:
640
+ return a
641
+
642
+ na, nb = len(a), len(b)
643
+ # expand.grid(i_a, i_b): i_a cycles fastest -> tile a, repeat b.
644
+ i_a = np.tile(np.arange(na), nb)
645
+ i_b = np.repeat(np.arange(nb), na)
646
+
647
+ left = a.iloc[i_a].reset_index(drop=True)
648
+ right = b.iloc[i_b].reset_index(drop=True)
649
+ return pd.concat([left, right], axis=1)