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/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
|
+
)
|