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.
- ggh4x/__init__.py +140 -0
- ggh4x/_aimed_text_grob.py +432 -0
- ggh4x/_borrowed_ggplot2.py +273 -0
- ggh4x/_cli.py +84 -0
- ggh4x/_datasets.py +106 -0
- ggh4x/_download.py +111 -0
- ggh4x/_facet_helpers.py +313 -0
- ggh4x/_facet_utils.py +649 -0
- ggh4x/_gap_grobs.py +606 -0
- ggh4x/_registry.py +10 -0
- ggh4x/_rlang.py +93 -0
- ggh4x/_utils.py +150 -0
- ggh4x/_vctrs.py +233 -0
- ggh4x/conveniences.py +601 -0
- ggh4x/coord_axes_inside.py +380 -0
- ggh4x/element_part_rect.py +545 -0
- ggh4x/facet_grid2.py +1018 -0
- ggh4x/facet_manual.py +901 -0
- ggh4x/facet_nested.py +776 -0
- ggh4x/facet_nested_wrap.py +193 -0
- ggh4x/facet_wrap2.py +896 -0
- ggh4x/geom_box.py +536 -0
- ggh4x/geom_outline_point.py +444 -0
- ggh4x/geom_pointpath.py +259 -0
- ggh4x/geom_polygonraster.py +252 -0
- ggh4x/geom_rectrug.py +489 -0
- ggh4x/geom_text_aimed.py +279 -0
- ggh4x/guide_stringlegend.py +354 -0
- ggh4x/help_secondary.py +549 -0
- ggh4x/multiscale/__init__.py +51 -0
- ggh4x/multiscale/_multiscale_add.py +207 -0
- ggh4x/multiscale/scale_listed.py +167 -0
- ggh4x/multiscale/scale_manual.py +478 -0
- ggh4x/multiscale/scale_multi.py +393 -0
- ggh4x/panel_scales/__init__.py +58 -0
- ggh4x/panel_scales/at_panel.py +115 -0
- ggh4x/panel_scales/facetted_pos_scales.py +647 -0
- ggh4x/panel_scales/force_panelsize.py +411 -0
- ggh4x/panel_scales/scale_facet.py +222 -0
- ggh4x/position_disjoint_ranges.py +229 -0
- ggh4x/position_lineartrans.py +242 -0
- ggh4x/py.typed +0 -0
- ggh4x/resources/faithful.csv +273 -0
- ggh4x/resources/iris.csv +151 -0
- ggh4x/resources/mtcars.csv +33 -0
- ggh4x/resources/pressure.csv +20 -0
- ggh4x/resources/volcano.csv +87 -0
- ggh4x/save.py +255 -0
- ggh4x/stat_difference.py +388 -0
- ggh4x/stat_funxy.py +436 -0
- ggh4x/stat_rle.py +290 -0
- ggh4x/stat_rollingkernel.py +369 -0
- ggh4x/stat_theodensity.py +681 -0
- ggh4x/strip_nested.py +448 -0
- ggh4x/strip_split.py +687 -0
- ggh4x/strip_tag.py +636 -0
- ggh4x/strip_themed.py +232 -0
- ggh4x/strip_vanilla.py +1464 -0
- ggh4x/themes.py +31 -0
- ggh4x/themes_ggh4x.py +67 -0
- ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
- ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
- ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
- 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)
|