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
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""Per-panel position scales (port of ggh4x ``R/facetted_pos_scales.R``).
|
|
2
|
+
|
|
3
|
+
:func:`facetted_pos_scales` returns a :class:`FacettedPosScales` add-on object.
|
|
4
|
+
When added to a plot, its handler clones the plot's live facet into a dynamic
|
|
5
|
+
``FreeScaled<FacetClass>`` subclass whose ``init_scales`` / ``train_scales`` /
|
|
6
|
+
``finish_data`` are replaced with per-panel variants: each ``SCALE_X`` /
|
|
7
|
+
``SCALE_Y`` id gets its own cloned scale, user scales (with ``oob`` forced to
|
|
8
|
+
:func:`scales.oob_keep`) substituted at matched panels, and layer data
|
|
9
|
+
transformed per panel before training.
|
|
10
|
+
|
|
11
|
+
NSE deviation
|
|
12
|
+
-------------
|
|
13
|
+
R accepts a list of two-sided formulas whose LHS is tidy-evaluated against the
|
|
14
|
+
plot layout. Python has no NSE: instead an element may be a *position scale*, a
|
|
15
|
+
``None``, or a ``(predicate, scale)`` pair where ``predicate`` is either a
|
|
16
|
+
callable ``layout_df -> bool-array`` or a string evaluated with
|
|
17
|
+
:meth:`pandas.DataFrame.eval` over the layout columns. The predicate list is
|
|
18
|
+
stored parallel to the scale list (R smuggles it via ``attr(., "lhs")``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import pandas as pd
|
|
27
|
+
from scales import oob_keep
|
|
28
|
+
|
|
29
|
+
from ggplot2_py import ggproto
|
|
30
|
+
from ggplot2_py.facet import Facet
|
|
31
|
+
from ggplot2_py.ggproto import ggproto_parent
|
|
32
|
+
from ggplot2_py.plot import update_ggplot
|
|
33
|
+
|
|
34
|
+
from ggh4x._cli import cli_abort, cli_warn
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"facetted_pos_scales",
|
|
38
|
+
"FacettedPosScales",
|
|
39
|
+
"check_facetted_scale",
|
|
40
|
+
"validate_facetted_scale",
|
|
41
|
+
"init_scale",
|
|
42
|
+
"init_scales_individual",
|
|
43
|
+
"train_scales_individual",
|
|
44
|
+
"finish_data_individual",
|
|
45
|
+
"should_transform",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Facet class-name prefixes recognised as "known" (allowlist replacing R's
|
|
49
|
+
# body-identity comparison of init/train/finish; see panel_scales.md risk 8).
|
|
50
|
+
_KNOWN_FACET_PREFIXES = (
|
|
51
|
+
"FacetGrid",
|
|
52
|
+
"FacetWrap",
|
|
53
|
+
"FacetNull",
|
|
54
|
+
"FacetManual",
|
|
55
|
+
"FreeScaled",
|
|
56
|
+
"Forced",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Predicate evaluation (NSE replacement)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
def _eval_predicate(pred: Any, layout: pd.DataFrame) -> np.ndarray:
|
|
64
|
+
"""Evaluate a panel predicate against the layout, returning a bool array.
|
|
65
|
+
|
|
66
|
+
The predicate may be a callable ``layout -> array-like`` or a string
|
|
67
|
+
expression evaluated with :meth:`pandas.DataFrame.eval`. This stands in for
|
|
68
|
+
R's tidy-evaluation of a formula LHS against the layout (``eval_tidy``).
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
pred : callable or str
|
|
73
|
+
The panel predicate.
|
|
74
|
+
layout : pandas.DataFrame
|
|
75
|
+
The plot layout (columns ``PANEL`` / ``ROW`` / ``COL`` / ``SCALE_*`` +
|
|
76
|
+
facet variables).
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
numpy.ndarray
|
|
81
|
+
A boolean array, recycled to ``len(layout)``.
|
|
82
|
+
"""
|
|
83
|
+
if callable(pred):
|
|
84
|
+
res = pred(layout)
|
|
85
|
+
elif isinstance(pred, str):
|
|
86
|
+
res = layout.eval(pred, engine="python")
|
|
87
|
+
else:
|
|
88
|
+
res = pred
|
|
89
|
+
arr = np.asarray(res)
|
|
90
|
+
if arr.dtype != bool:
|
|
91
|
+
arr = arr.astype(bool)
|
|
92
|
+
n = len(layout)
|
|
93
|
+
if arr.ndim == 0:
|
|
94
|
+
arr = np.repeat(arr, n)
|
|
95
|
+
if len(arr) != n:
|
|
96
|
+
# rep_len recycling
|
|
97
|
+
arr = np.resize(arr, n)
|
|
98
|
+
return arr
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# ScaleList: a list carrying a parallel ``lhs`` predicate list (R attr "lhs")
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
class _ScaleList(list):
|
|
105
|
+
"""A ``list`` of scales carrying an optional parallel ``lhs`` predicate list.
|
|
106
|
+
|
|
107
|
+
Stands in for R's ``structure(rhs, lhs = lhs, class = "list")``: a plain
|
|
108
|
+
list of scales (or ``None`` s) where ``self.lhs`` -- when not ``None`` --
|
|
109
|
+
holds one predicate per element (the formula LHS equivalent).
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
lhs: Optional[List[Any]] = None
|
|
113
|
+
|
|
114
|
+
def __init__(self, iterable: Sequence[Any] = (), lhs: Optional[List[Any]] = None) -> None:
|
|
115
|
+
super().__init__(iterable)
|
|
116
|
+
self.lhs = lhs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# check_facetted_scale (R facetted_pos_scales.R:115-145)
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
def _is_scale(x: Any) -> bool:
|
|
123
|
+
"""Return whether *x* looks like a ggplot2 Scale (has ``aesthetics``)."""
|
|
124
|
+
return x is not None and hasattr(x, "aesthetics") and hasattr(x, "clone")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_formula_pair(x: Any) -> bool:
|
|
128
|
+
"""Return whether *x* is a ``(predicate, scale)`` pair (formula equivalent)."""
|
|
129
|
+
return (
|
|
130
|
+
isinstance(x, (tuple, list))
|
|
131
|
+
and len(x) == 2
|
|
132
|
+
and (callable(x[0]) or isinstance(x[0], str))
|
|
133
|
+
and _is_scale(x[1])
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def check_facetted_scale(x: Optional[Sequence[Any]], aes: str = "x", allow_null: bool = True) -> bool:
|
|
138
|
+
"""Validate that *x* is a list of position scales / ``None`` s / formula pairs.
|
|
139
|
+
|
|
140
|
+
Faithful port of ggh4x's ``check_facetted_scale``
|
|
141
|
+
(``R/facetted_pos_scales.R:115-145``). Each element must be a position
|
|
142
|
+
:class:`~ggplot2_py.scale.Scale` carrying the *aes* aesthetic, a ``None``
|
|
143
|
+
(when ``allow_null``), or -- if *all* elements are formula pairs -- a
|
|
144
|
+
``(predicate, scale)`` pair.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
x : sequence or None
|
|
149
|
+
The candidate scale list.
|
|
150
|
+
aes : {"x", "y"}, default "x"
|
|
151
|
+
Required aesthetic.
|
|
152
|
+
allow_null : bool, default True
|
|
153
|
+
Whether ``None`` elements are permitted.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
bool
|
|
158
|
+
``True`` when *x* is a valid facetted-scale list.
|
|
159
|
+
"""
|
|
160
|
+
if x is None:
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
is_scale = [_is_scale(e) for e in x]
|
|
164
|
+
is_null = [e is None for e in x]
|
|
165
|
+
is_form = [_is_formula_pair(e) for e in x]
|
|
166
|
+
|
|
167
|
+
if x and all(is_form):
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
# Scales must carry the right aesthetic.
|
|
171
|
+
appropriate = [
|
|
172
|
+
(aes in list(e.aesthetics)) for e, s in zip(x, is_scale) if s
|
|
173
|
+
]
|
|
174
|
+
# is_scale[is_scale] <- is_scale[is_scale] & appropriate_aes
|
|
175
|
+
ai = 0
|
|
176
|
+
for i, s in enumerate(is_scale):
|
|
177
|
+
if s:
|
|
178
|
+
is_scale[i] = s and appropriate[ai]
|
|
179
|
+
ai += 1
|
|
180
|
+
|
|
181
|
+
if allow_null:
|
|
182
|
+
if all(s or n for s, n in zip(is_scale, is_null)):
|
|
183
|
+
return True
|
|
184
|
+
else:
|
|
185
|
+
if all(is_scale):
|
|
186
|
+
return True
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# validate_facetted_scale (R facetted_pos_scales.R:149-177)
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
def validate_facetted_scale(x: Sequence[Any], aes: str = "x") -> _ScaleList:
|
|
194
|
+
"""Split formula pairs into a scale list + parallel predicate (``lhs``) list.
|
|
195
|
+
|
|
196
|
+
Faithful port of ggh4x's ``validate_facetted_scale``
|
|
197
|
+
(``R/facetted_pos_scales.R:149-177``). When *x*'s first element is not a
|
|
198
|
+
formula pair the list is returned as-is. Otherwise each ``(predicate,
|
|
199
|
+
scale)`` pair is split: the predicate ``lhs`` is kept for later layout
|
|
200
|
+
evaluation, the scale ``rhs`` is validated for the *aes* aesthetic.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
x : sequence
|
|
205
|
+
The candidate (possibly formula-pair) list.
|
|
206
|
+
aes : {"x", "y"}, default "x"
|
|
207
|
+
Required aesthetic.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
_ScaleList
|
|
212
|
+
A scale list, with ``.lhs`` set to the parallel predicate list when *x*
|
|
213
|
+
was formula-based (else ``.lhs is None``).
|
|
214
|
+
|
|
215
|
+
Raises
|
|
216
|
+
------
|
|
217
|
+
ValueError
|
|
218
|
+
When a formula pair's right-hand side is not an appropriate scale.
|
|
219
|
+
"""
|
|
220
|
+
if not x or not _is_formula_pair(x[0]):
|
|
221
|
+
return _ScaleList(x, lhs=None)
|
|
222
|
+
|
|
223
|
+
lhs = [f[0] for f in x]
|
|
224
|
+
rhs = [f[1] for f in x]
|
|
225
|
+
|
|
226
|
+
if not check_facetted_scale(rhs, aes=aes, allow_null=False):
|
|
227
|
+
cli_abort(
|
|
228
|
+
"The right-hand side of formula does not result in an appropriate scale."
|
|
229
|
+
)
|
|
230
|
+
return _ScaleList(rhs, lhs=lhs)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# facetted_pos_scales constructor (R facetted_pos_scales.R:79-112)
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
class FacettedPosScales:
|
|
237
|
+
"""Deferred container of per-panel x / y position scales.
|
|
238
|
+
|
|
239
|
+
Port of R's ``structure(list(x =, y =), class = "facetted_pos_scales")``.
|
|
240
|
+
Consumed by :func:`_update_facetted_pos_scales` at ``+``-time.
|
|
241
|
+
|
|
242
|
+
Attributes
|
|
243
|
+
----------
|
|
244
|
+
x, y : _ScaleList
|
|
245
|
+
Per-panel x / y scale lists (each possibly carrying a ``.lhs`` predicate
|
|
246
|
+
list).
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(self, x: _ScaleList, y: _ScaleList) -> None:
|
|
250
|
+
self.x = x
|
|
251
|
+
self.y = y
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def facetted_pos_scales(
|
|
255
|
+
x: Optional[Union[Sequence[Any], Any]] = None,
|
|
256
|
+
y: Optional[Union[Sequence[Any], Any]] = None,
|
|
257
|
+
) -> FacettedPosScales:
|
|
258
|
+
"""Set individual position scales in facets.
|
|
259
|
+
|
|
260
|
+
Faithful port of ggh4x's ``facetted_pos_scales``
|
|
261
|
+
(``R/facetted_pos_scales.R:79-112``). ``x`` / ``y`` are lists whose elements
|
|
262
|
+
are position scales, ``None`` s (use the default scale at that position), or
|
|
263
|
+
``(predicate, scale)`` pairs targeting panels by predicate. The facet must
|
|
264
|
+
use free scales in the relevant direction.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
x, y : list or None, default None
|
|
269
|
+
Per-panel x / y position scales (or a single element, auto-wrapped).
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
FacettedPosScales
|
|
274
|
+
An add-on object that can be added to a plot with ``+``.
|
|
275
|
+
|
|
276
|
+
Raises
|
|
277
|
+
------
|
|
278
|
+
ValueError
|
|
279
|
+
When ``x`` or ``y`` is not a valid facetted-scale list.
|
|
280
|
+
"""
|
|
281
|
+
if not isinstance(x, list):
|
|
282
|
+
x = [x]
|
|
283
|
+
if not isinstance(y, list):
|
|
284
|
+
y = [y]
|
|
285
|
+
|
|
286
|
+
x_test = check_facetted_scale(x, "x")
|
|
287
|
+
y_test = check_facetted_scale(y, "y")
|
|
288
|
+
if not (x_test and y_test):
|
|
289
|
+
if not x_test and not y_test:
|
|
290
|
+
arg, typ = "The `x` and `y` arguments ", "appropriate"
|
|
291
|
+
elif not x_test:
|
|
292
|
+
arg, typ = "The `x` argument ", "x"
|
|
293
|
+
else:
|
|
294
|
+
arg, typ = "The `y` argument ", "y"
|
|
295
|
+
cli_abort(
|
|
296
|
+
arg
|
|
297
|
+
+ "should be `None`, or a list of formulas and/or position scales "
|
|
298
|
+
+ f"with the {typ} aesthetic."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
x = validate_facetted_scale(x, "x")
|
|
302
|
+
y = validate_facetted_scale(y, "y")
|
|
303
|
+
return FacettedPosScales(x=x, y=y)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# ggproto methods: init_scale / init_scales_individual (R:255-319)
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
def init_scale(
|
|
310
|
+
old: Any,
|
|
311
|
+
new: Optional[Sequence[Any]],
|
|
312
|
+
layout: pd.DataFrame,
|
|
313
|
+
aes: str = "x",
|
|
314
|
+
) -> Optional[List[Any]]:
|
|
315
|
+
"""Build the per-panel scale list for one aesthetic.
|
|
316
|
+
|
|
317
|
+
Faithful port of ggh4x's ``init_scale`` (``R/facetted_pos_scales.R:255-305``).
|
|
318
|
+
Clones the default *old* scale once per ``SCALE_<AES>`` id, then substitutes
|
|
319
|
+
user scales (with ``oob`` forced to :func:`scales.oob_keep`). Without
|
|
320
|
+
predicates, substitution is by list position; with predicates, panels are
|
|
321
|
+
matched by evaluating each predicate against *layout* and substitution
|
|
322
|
+
proceeds in reverse order so earlier-added scales win.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
old : Scale or None
|
|
327
|
+
The default prototype scale (``None`` -> returns ``None``).
|
|
328
|
+
new : sequence or _ScaleList or None
|
|
329
|
+
The user scale list (possibly carrying a ``.lhs`` predicate list).
|
|
330
|
+
layout : pandas.DataFrame
|
|
331
|
+
The plot layout.
|
|
332
|
+
aes : {"x", "y"}, default "x"
|
|
333
|
+
The aesthetic.
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
list or None
|
|
338
|
+
One scale per ``SCALE_<AES>`` id, or ``None`` when *old* is ``None``.
|
|
339
|
+
"""
|
|
340
|
+
if old is None:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
scalename = "SCALE_" + aes.upper()
|
|
344
|
+
n_ids = int(layout[scalename].max())
|
|
345
|
+
out: List[Any] = [old.clone() for _ in range(n_ids)]
|
|
346
|
+
|
|
347
|
+
lhs = getattr(new, "lhs", None)
|
|
348
|
+
if lhs is None:
|
|
349
|
+
# Regular: substitute at positions with a non-empty user scale.
|
|
350
|
+
for i, sc in enumerate(new or []):
|
|
351
|
+
if sc is None:
|
|
352
|
+
continue
|
|
353
|
+
clone = sc.clone()
|
|
354
|
+
clone.oob = oob_keep
|
|
355
|
+
if i < len(out):
|
|
356
|
+
out[i] = clone
|
|
357
|
+
else:
|
|
358
|
+
n = len(layout)
|
|
359
|
+
# Evaluate each predicate -> column of a logical matrix.
|
|
360
|
+
cols = [_eval_predicate(p, layout) for p in lhs]
|
|
361
|
+
for i in reversed(range(len(cols))):
|
|
362
|
+
mask = cols[i]
|
|
363
|
+
matched_rows = np.where(mask)[0]
|
|
364
|
+
# unique SCALE ids among matched layout rows
|
|
365
|
+
scale_ids = pd.unique(layout.iloc[matched_rows][scalename])
|
|
366
|
+
for sid in scale_ids:
|
|
367
|
+
clone = new[i].clone()
|
|
368
|
+
clone.oob = oob_keep
|
|
369
|
+
out[int(sid) - 1] = clone
|
|
370
|
+
return out
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def init_scales_individual(
|
|
374
|
+
self: Any,
|
|
375
|
+
layout: pd.DataFrame,
|
|
376
|
+
x_scale: Any = None,
|
|
377
|
+
y_scale: Any = None,
|
|
378
|
+
params: Optional[Dict[str, Any]] = None,
|
|
379
|
+
) -> Dict[str, list]:
|
|
380
|
+
"""Per-panel ``init_scales`` (R ``init_scales_individual``).
|
|
381
|
+
|
|
382
|
+
Faithful port of ggh4x's ``init_scales_individual``
|
|
383
|
+
(``R/facetted_pos_scales.R:308-319``). Because ggplot2_py's layout calls
|
|
384
|
+
``init_scales`` twice (x-only, then y-only), each aesthetic is guarded on
|
|
385
|
+
``is not None`` so only the populated key is returned.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
self : Facet
|
|
390
|
+
The ``FreeScaled<...>`` facet instance (carries ``new_x_scales`` /
|
|
391
|
+
``new_y_scales``).
|
|
392
|
+
layout : pandas.DataFrame
|
|
393
|
+
x_scale, y_scale : Scale or None
|
|
394
|
+
Prototype position scales (one provided per call).
|
|
395
|
+
params : dict, optional
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
dict
|
|
400
|
+
``{"x": [...]}`` or ``{"y": [...]}`` -- only the populated aesthetic.
|
|
401
|
+
"""
|
|
402
|
+
scales: Dict[str, list] = {}
|
|
403
|
+
if x_scale is not None:
|
|
404
|
+
res = init_scale(x_scale, self.new_x_scales, layout, aes="x")
|
|
405
|
+
if res is not None:
|
|
406
|
+
scales["x"] = res
|
|
407
|
+
if y_scale is not None:
|
|
408
|
+
res = init_scale(y_scale, self.new_y_scales, layout, aes="y")
|
|
409
|
+
if res is not None:
|
|
410
|
+
scales["y"] = res
|
|
411
|
+
return scales
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def train_scales_individual(
|
|
415
|
+
self: Any,
|
|
416
|
+
x_scales: list,
|
|
417
|
+
y_scales: list,
|
|
418
|
+
layout: pd.DataFrame,
|
|
419
|
+
data: List[pd.DataFrame],
|
|
420
|
+
params: Optional[Dict[str, Any]] = None,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Per-panel ``train_scales`` (R ``train_scales_individual``).
|
|
423
|
+
|
|
424
|
+
Faithful port of ggh4x's ``train_scales_individual``
|
|
425
|
+
(``R/facetted_pos_scales.R:322-332``). Transforms each layer's data through
|
|
426
|
+
:func:`finish_data_individual` *first* (so per-panel transforms precede
|
|
427
|
+
training), then delegates to the parent :class:`~ggplot2_py.facet.Facet`'s
|
|
428
|
+
``train_scales``.
|
|
429
|
+
|
|
430
|
+
Parameters
|
|
431
|
+
----------
|
|
432
|
+
self : Facet
|
|
433
|
+
x_scales, y_scales : list
|
|
434
|
+
layout : pandas.DataFrame
|
|
435
|
+
data : list of DataFrame
|
|
436
|
+
params : dict, optional
|
|
437
|
+
"""
|
|
438
|
+
data = [
|
|
439
|
+
self.finish_data(ld, layout, x_scales, y_scales, params)
|
|
440
|
+
for ld in data
|
|
441
|
+
]
|
|
442
|
+
ggproto_parent(Facet, self).train_scales(
|
|
443
|
+
x_scales, y_scales, layout, data, params
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def finish_data_individual(
|
|
448
|
+
self: Any,
|
|
449
|
+
data: pd.DataFrame,
|
|
450
|
+
layout: pd.DataFrame,
|
|
451
|
+
x_scales: list,
|
|
452
|
+
y_scales: list,
|
|
453
|
+
params: Optional[Dict[str, Any]] = None,
|
|
454
|
+
) -> pd.DataFrame:
|
|
455
|
+
"""Per-panel ``finish_data`` (R ``finish_data_individual``).
|
|
456
|
+
|
|
457
|
+
Faithful port of ggh4x's ``finish_data_individual``
|
|
458
|
+
(``R/facetted_pos_scales.R:335-368``). Splits *data* by ``PANEL`` (keeping
|
|
459
|
+
the exact input row positions), matches each chunk to its ``SCALE_X`` /
|
|
460
|
+
``SCALE_Y`` ids, transforms the appropriate columns through the panel's
|
|
461
|
+
scales, and recombines preserving the original row order.
|
|
462
|
+
|
|
463
|
+
Parameters
|
|
464
|
+
----------
|
|
465
|
+
self : Facet
|
|
466
|
+
Carries ``new_x_scales`` / ``new_y_scales``.
|
|
467
|
+
data : pandas.DataFrame
|
|
468
|
+
layout : pandas.DataFrame
|
|
469
|
+
x_scales, y_scales : list
|
|
470
|
+
params : dict, optional
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
pandas.DataFrame
|
|
475
|
+
*data* with per-panel-transformed position columns, original order.
|
|
476
|
+
"""
|
|
477
|
+
if data is None or len(data) == 0 or "PANEL" not in data.columns:
|
|
478
|
+
return data
|
|
479
|
+
|
|
480
|
+
regular_x = _scalelist_len(self.new_x_scales) == 0
|
|
481
|
+
regular_y = _scalelist_len(self.new_y_scales) == 0
|
|
482
|
+
|
|
483
|
+
# Split by PANEL preserving positional indices.
|
|
484
|
+
groups = data.groupby("PANEL", observed=True).indices # PANEL -> int positions
|
|
485
|
+
|
|
486
|
+
panel_codes = layout["PANEL"]
|
|
487
|
+
# Numeric codes of layout PANEL for matching (R: match(as.numeric(...), layout$PANEL)).
|
|
488
|
+
if isinstance(panel_codes.dtype, pd.CategoricalDtype):
|
|
489
|
+
layout_panel_num = panel_codes.cat.codes.to_numpy() + 1
|
|
490
|
+
else:
|
|
491
|
+
layout_panel_num = pd.to_numeric(panel_codes, errors="coerce").to_numpy()
|
|
492
|
+
|
|
493
|
+
out = data.copy()
|
|
494
|
+
|
|
495
|
+
for panel_val, idx in groups.items():
|
|
496
|
+
if len(idx) == 0:
|
|
497
|
+
continue
|
|
498
|
+
# numeric code of this panel
|
|
499
|
+
if isinstance(data["PANEL"].dtype, pd.CategoricalDtype):
|
|
500
|
+
cats = list(data["PANEL"].cat.categories)
|
|
501
|
+
panel_num = cats.index(panel_val) + 1 if panel_val in cats else None
|
|
502
|
+
try:
|
|
503
|
+
panel_num = int(panel_val)
|
|
504
|
+
except (TypeError, ValueError):
|
|
505
|
+
panel_num = cats.index(panel_val) + 1 if panel_val in cats else None
|
|
506
|
+
else:
|
|
507
|
+
panel_num = int(panel_val)
|
|
508
|
+
|
|
509
|
+
matches = np.where(layout_panel_num == panel_num)[0]
|
|
510
|
+
if len(matches) == 0:
|
|
511
|
+
continue
|
|
512
|
+
panel_id = matches[0]
|
|
513
|
+
xidx = int(layout.iloc[panel_id]["SCALE_X"]) - 1
|
|
514
|
+
yidx = int(layout.iloc[panel_id]["SCALE_Y"]) - 1
|
|
515
|
+
|
|
516
|
+
chunk = data.iloc[idx]
|
|
517
|
+
y_vars = should_transform(
|
|
518
|
+
y_scales[yidx] if 0 <= yidx < len(y_scales) else None,
|
|
519
|
+
list(chunk.columns),
|
|
520
|
+
)
|
|
521
|
+
x_vars = should_transform(
|
|
522
|
+
x_scales[xidx] if 0 <= xidx < len(x_scales) else None,
|
|
523
|
+
list(chunk.columns),
|
|
524
|
+
)
|
|
525
|
+
if regular_x:
|
|
526
|
+
x_vars = []
|
|
527
|
+
if regular_y:
|
|
528
|
+
y_vars = []
|
|
529
|
+
|
|
530
|
+
for j in y_vars:
|
|
531
|
+
out.iloc[idx, out.columns.get_loc(j)] = y_scales[yidx].transform(
|
|
532
|
+
chunk[j].to_numpy()
|
|
533
|
+
)
|
|
534
|
+
for j in x_vars:
|
|
535
|
+
out.iloc[idx, out.columns.get_loc(j)] = x_scales[xidx].transform(
|
|
536
|
+
chunk[j].to_numpy()
|
|
537
|
+
)
|
|
538
|
+
return out
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _scalelist_len(scales: Optional[Sequence[Any]]) -> int:
|
|
542
|
+
"""Return ``sum(lengths(scales))``: count of non-``None`` scale elements."""
|
|
543
|
+
if scales is None:
|
|
544
|
+
return 0
|
|
545
|
+
return sum(1 for s in scales if s is not None)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def should_transform(scale: Any, columns: Sequence[str]) -> List[str]:
|
|
549
|
+
"""Return the columns to transform for a panel's scale.
|
|
550
|
+
|
|
551
|
+
Faithful port of ggh4x's ``should_transform``
|
|
552
|
+
(``R/facetted_pos_scales.R:370-378``): no columns for a ``None`` scale, a
|
|
553
|
+
discrete scale, or a date/time/hms transformation; otherwise the
|
|
554
|
+
intersection of the scale's aesthetics with *columns*.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
scale : Scale or None
|
|
559
|
+
columns : sequence of str
|
|
560
|
+
|
|
561
|
+
Returns
|
|
562
|
+
-------
|
|
563
|
+
list of str
|
|
564
|
+
"""
|
|
565
|
+
if scale is None or scale.is_discrete():
|
|
566
|
+
return []
|
|
567
|
+
trans = _get_transformation(scale)
|
|
568
|
+
name = getattr(trans, "name", None)
|
|
569
|
+
if name in ("date", "time", "hms"):
|
|
570
|
+
return []
|
|
571
|
+
return [c for c in scale.aesthetics if c in columns]
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _get_transformation(scale: Any) -> Any:
|
|
575
|
+
"""Return a scale's transformation object (ggh4x ``get_transformation``)."""
|
|
576
|
+
if hasattr(scale, "get_transformation"):
|
|
577
|
+
return scale.get_transformation()
|
|
578
|
+
return getattr(scale, "trans", None)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
# ggplot_add.facetted_pos_scales (R facetted_pos_scales.R:186-250)
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
@update_ggplot.register(FacettedPosScales)
|
|
585
|
+
def _update_facetted_pos_scales(obj: FacettedPosScales, plot: Any, object_name: str = "") -> Any:
|
|
586
|
+
"""Add a :class:`FacettedPosScales` to *plot* (R ``ggplot_add.facetted_pos_scales``).
|
|
587
|
+
|
|
588
|
+
Clones the plot's facet into a ``FreeScaled<FacetClass>`` whose
|
|
589
|
+
``init_scales`` / ``train_scales`` / ``finish_data`` are the per-panel
|
|
590
|
+
variants; re-additions onto an already-``FreeScaled`` facet just update the
|
|
591
|
+
new-scale lists.
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
obj : FacettedPosScales
|
|
596
|
+
plot : ggplot2_py.plot.GGPlot
|
|
597
|
+
object_name : str, optional
|
|
598
|
+
|
|
599
|
+
Returns
|
|
600
|
+
-------
|
|
601
|
+
ggplot2_py.plot.GGPlot
|
|
602
|
+
"""
|
|
603
|
+
empty_x = [e is None for e in obj.x]
|
|
604
|
+
empty_y = [e is None for e in obj.y]
|
|
605
|
+
if all(empty_x) and all(empty_y):
|
|
606
|
+
return plot
|
|
607
|
+
|
|
608
|
+
facet = plot.facet
|
|
609
|
+
if type(facet).__name__.startswith("FreeScaled"):
|
|
610
|
+
# Already initialised; just update scale lists.
|
|
611
|
+
if not all(empty_x):
|
|
612
|
+
facet.new_x_scales = obj.x
|
|
613
|
+
if not all(empty_y):
|
|
614
|
+
facet.new_y_scales = obj.y
|
|
615
|
+
return plot
|
|
616
|
+
|
|
617
|
+
# Validity warning (allowlist replaces R body-identity check).
|
|
618
|
+
if not type(facet).__name__.startswith(_KNOWN_FACET_PREFIXES):
|
|
619
|
+
cli_warn(
|
|
620
|
+
f"Unknown facet: {type(facet).__name__}. "
|
|
621
|
+
"Overriding facetted scales may be unstable."
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
free = facet.params.get("free") if facet.params else None
|
|
625
|
+
if free is not None:
|
|
626
|
+
if free.get("x") is not None and sum(not e for e in empty_x) > 0 and not free["x"]:
|
|
627
|
+
cli_warn(
|
|
628
|
+
"Attempting to add facetted x scales, while x scales are not free. "
|
|
629
|
+
'Try adding `scales = "free_x"` to the facet.'
|
|
630
|
+
)
|
|
631
|
+
if free.get("y") is not None and sum(not e for e in empty_y) > 0 and not free["y"]:
|
|
632
|
+
cli_warn(
|
|
633
|
+
"Attempting to add facetted y scales, while y scales are not free. "
|
|
634
|
+
'Try adding `scales = "free_y"` to the facet.'
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
new_facet = ggproto(
|
|
638
|
+
f"FreeScaled{type(facet).__name__}",
|
|
639
|
+
facet,
|
|
640
|
+
new_x_scales=obj.x,
|
|
641
|
+
new_y_scales=obj.y,
|
|
642
|
+
init_scales=init_scales_individual,
|
|
643
|
+
train_scales=train_scales_individual,
|
|
644
|
+
finish_data=finish_data_individual,
|
|
645
|
+
)
|
|
646
|
+
plot.facet = new_facet
|
|
647
|
+
return plot
|