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_helpers.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Shared argument-normalisation helpers for ggh4x extended facets.
|
|
2
|
+
|
|
3
|
+
Both :mod:`ggh4x.facet_grid2` and :mod:`ggh4x.facet_wrap2` need to translate the
|
|
4
|
+
``character(1)`` / ``logical(1)`` facet arguments (``scales``, ``space``, ``axes``,
|
|
5
|
+
``remove_labels``, ``independent``) into the ``{"x": bool, "y": bool}`` dicts the
|
|
6
|
+
ggproto classes consume, and ``facet_grid2`` additionally validates the
|
|
7
|
+
``independent`` interactions. These live here (rather than in either facet module)
|
|
8
|
+
so the two facet modules can import them without importing one another.
|
|
9
|
+
|
|
10
|
+
R sources:
|
|
11
|
+
|
|
12
|
+
- ``.match_facet_arg`` — ggh4x ``R/facet_wrap2.R:418-433``.
|
|
13
|
+
- ``.validate_independent`` — ggh4x ``R/facet_grid2.R:438-485``.
|
|
14
|
+
|
|
15
|
+
Also provides the ``AspectRatio`` struct that stands in for R's
|
|
16
|
+
``attr(aspect_ratio, "respect")`` (a Python float cannot carry attributes) and the
|
|
17
|
+
identity-default ``reshape_add_margins`` used by ``FacetGrid2.compute_layout``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
24
|
+
|
|
25
|
+
import pandas as pd
|
|
26
|
+
|
|
27
|
+
from ggh4x._cli import cli_abort, cli_warn
|
|
28
|
+
from ggh4x._rlang import arg_match0
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AspectRatio",
|
|
32
|
+
"_match_facet_arg",
|
|
33
|
+
"_validate_independent",
|
|
34
|
+
"reshape_add_margins",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class AspectRatio:
|
|
40
|
+
"""A panel aspect ratio together with its ``respect`` flag.
|
|
41
|
+
|
|
42
|
+
R attaches ``attr(aspect_ratio, "respect")`` onto a bare numeric scalar in
|
|
43
|
+
``setup_aspect_ratio``; a Python ``float`` cannot carry attributes, so the
|
|
44
|
+
``(value, respect)`` pair is threaded as this small immutable struct. The
|
|
45
|
+
consumers (``setup_panel_table``) read :attr:`value` for ``abs(aspect)``
|
|
46
|
+
null-unit heights and :attr:`respect` for the gtable ``respect=`` flag.
|
|
47
|
+
|
|
48
|
+
Attributes
|
|
49
|
+
----------
|
|
50
|
+
value : float
|
|
51
|
+
The aspect ratio (R ``aspect_ratio``).
|
|
52
|
+
respect : bool
|
|
53
|
+
Whether the gtable should respect the aspect (R
|
|
54
|
+
``attr(aspect_ratio, "respect")``).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
value: float
|
|
58
|
+
respect: bool
|
|
59
|
+
|
|
60
|
+
def __float__(self) -> float: # convenience for ``abs(aspect)`` style use
|
|
61
|
+
return float(self.value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _match_facet_arg(
|
|
65
|
+
value: Any,
|
|
66
|
+
options: Sequence[str],
|
|
67
|
+
x: int = 2,
|
|
68
|
+
y: int = 3,
|
|
69
|
+
both: int = 4,
|
|
70
|
+
neither: int = 1,
|
|
71
|
+
nm: str = "value",
|
|
72
|
+
) -> Dict[str, bool]:
|
|
73
|
+
"""Normalise a facet argument to a ``{"x": bool, "y": bool}`` dict.
|
|
74
|
+
|
|
75
|
+
Faithful port of ggh4x's ``.match_facet_arg`` (``R/facet_wrap2.R:418-433``).
|
|
76
|
+
Accepts either a single non-``NA`` logical (``True`` -> the ``both`` option,
|
|
77
|
+
``False`` -> the ``neither`` option) or one of the option strings (validated
|
|
78
|
+
with :func:`ggh4x._rlang.arg_match0`). The chosen option then sets the ``x``
|
|
79
|
+
/ ``y`` booleans by membership in the ``x``/``both`` and ``y``/``both``
|
|
80
|
+
option positions.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
value : bool or str
|
|
85
|
+
The user-supplied argument. A bool selects ``both``/``neither``; a string
|
|
86
|
+
is matched against *options*.
|
|
87
|
+
options : sequence of str
|
|
88
|
+
The four allowed option strings, ordered ``[neither, x, y, both]`` by
|
|
89
|
+
default (see the ``neither``/``x``/``y``/``both`` index arguments). The R
|
|
90
|
+
defaults are e.g. ``c("fixed", "free_x", "free_y", "free")``.
|
|
91
|
+
x, y, both, neither : int, default 2, 3, 4, 1
|
|
92
|
+
1-based positions (R indices) into *options* for the ``x``-only,
|
|
93
|
+
``y``-only, both and neither cases.
|
|
94
|
+
nm : str, default ``"value"``
|
|
95
|
+
Argument name used in the ``arg_match0`` error message.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
dict
|
|
100
|
+
``{"x": bool, "y": bool}``.
|
|
101
|
+
|
|
102
|
+
Notes
|
|
103
|
+
-----
|
|
104
|
+
R source::
|
|
105
|
+
|
|
106
|
+
.match_facet_arg <- function(value, options, x = 2, y = 3, both = 4,
|
|
107
|
+
neither = 1, nm = deparse(substitute(value))) {
|
|
108
|
+
if (is.logical(value) && length(value) == 1 && !is.na(value)) {
|
|
109
|
+
if (value) value <- options[both] else value <- options[neither]
|
|
110
|
+
} else {
|
|
111
|
+
value <- rlang::arg_match0(value, options, arg_nm = nm)
|
|
112
|
+
}
|
|
113
|
+
list(x = any(value %in% options[c(x, both)]),
|
|
114
|
+
y = any(value %in% options[c(y, both)]))
|
|
115
|
+
}
|
|
116
|
+
"""
|
|
117
|
+
opts = list(options)
|
|
118
|
+
if isinstance(value, bool):
|
|
119
|
+
value = opts[both - 1] if value else opts[neither - 1]
|
|
120
|
+
else:
|
|
121
|
+
value = arg_match0(value, opts, arg_name=nm)
|
|
122
|
+
|
|
123
|
+
x_set = {opts[x - 1], opts[both - 1]}
|
|
124
|
+
y_set = {opts[y - 1], opts[both - 1]}
|
|
125
|
+
return {"x": value in x_set, "y": value in y_set}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_independent(
|
|
129
|
+
independent: Dict[str, bool],
|
|
130
|
+
free: Dict[str, bool],
|
|
131
|
+
space_free: Dict[str, bool],
|
|
132
|
+
rmlab: Dict[str, bool],
|
|
133
|
+
) -> Dict[str, Dict[str, bool]]:
|
|
134
|
+
"""Validate and reconcile the ``independent`` facet interactions.
|
|
135
|
+
|
|
136
|
+
Faithful port of ggh4x's ``.validate_independent`` (``R/facet_grid2.R:438-485``).
|
|
137
|
+
Enforces three rules per dimension when ``independent`` is set:
|
|
138
|
+
|
|
139
|
+
1. ``independent`` requires ``free`` in the same dimension (else abort).
|
|
140
|
+
2. ``independent`` cannot coexist with free ``space`` -> force ``space_free``
|
|
141
|
+
to ``False`` (with a warning).
|
|
142
|
+
3. ``independent`` axes must keep their labels -> force ``rmlab`` to ``False``
|
|
143
|
+
(with a warning).
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
independent : dict
|
|
148
|
+
``{"x": bool, "y": bool}`` -- whether scales vary within a column/row.
|
|
149
|
+
free : dict
|
|
150
|
+
``{"x": bool, "y": bool}`` -- whether scales are free.
|
|
151
|
+
space_free : dict
|
|
152
|
+
``{"x": bool, "y": bool}`` -- whether panel sizes are proportional.
|
|
153
|
+
rmlab : dict
|
|
154
|
+
``{"x": bool, "y": bool}`` -- whether inner axis labels are removed.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
dict
|
|
159
|
+
``{"independent", "free", "space_free", "rmlab"}`` with the
|
|
160
|
+
(possibly mutated) four dicts.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
ValueError
|
|
165
|
+
When a dimension is independent but its scales are not free.
|
|
166
|
+
"""
|
|
167
|
+
# Copy so we never mutate the caller's dicts.
|
|
168
|
+
independent = dict(independent)
|
|
169
|
+
free = dict(free)
|
|
170
|
+
space_free = dict(space_free)
|
|
171
|
+
rmlab = dict(rmlab)
|
|
172
|
+
|
|
173
|
+
if independent["x"]:
|
|
174
|
+
if not free["x"]:
|
|
175
|
+
cli_abort("`x` cannot be independent if scales are not free.")
|
|
176
|
+
if space_free["x"]:
|
|
177
|
+
cli_warn(
|
|
178
|
+
"`x` cannot have free space if axes are independent. "
|
|
179
|
+
"Overriding `space` for `x` to `FALSE`."
|
|
180
|
+
)
|
|
181
|
+
space_free["x"] = False
|
|
182
|
+
if rmlab["x"]:
|
|
183
|
+
cli_warn(
|
|
184
|
+
"x-axes must be labelled if they are independent. "
|
|
185
|
+
"Overriding `remove_labels` for `x` to `FALSE`."
|
|
186
|
+
)
|
|
187
|
+
rmlab["x"] = False
|
|
188
|
+
|
|
189
|
+
if independent["y"]:
|
|
190
|
+
if not free["y"]:
|
|
191
|
+
cli_abort("`y` cannot be independent if scales are not free.")
|
|
192
|
+
if space_free["y"]:
|
|
193
|
+
cli_warn(
|
|
194
|
+
"`y` cannot have free space if axes are independent. "
|
|
195
|
+
"Overriding `space` for `y` to `FALSE`."
|
|
196
|
+
)
|
|
197
|
+
space_free["y"] = False
|
|
198
|
+
if rmlab["y"]:
|
|
199
|
+
cli_warn(
|
|
200
|
+
"y-axes must be labelled if they are independent. "
|
|
201
|
+
"Overriding `remove_labels` for `y` to `FALSE`."
|
|
202
|
+
)
|
|
203
|
+
rmlab["y"] = False
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"independent": independent,
|
|
207
|
+
"free": free,
|
|
208
|
+
"space_free": space_free,
|
|
209
|
+
"rmlab": rmlab,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def reshape_add_margins(
|
|
214
|
+
df: pd.DataFrame,
|
|
215
|
+
vars_: Sequence[Sequence[str]],
|
|
216
|
+
margins: Any = False,
|
|
217
|
+
margin_nm: str = "(all)",
|
|
218
|
+
) -> pd.DataFrame:
|
|
219
|
+
"""Add facet margins to a cross-product layout frame.
|
|
220
|
+
|
|
221
|
+
Faithful port of ggplot2's ``reshape_add_margins`` (as borrowed by ggh4x).
|
|
222
|
+
Enumerates EVERY marginal variable subset via ``reshape_margins``
|
|
223
|
+
(``expand.grid`` over ``{none} ∪ {downto(margin, set)}`` per facet
|
|
224
|
+
dimension), which includes the empty set (= the original data) AND the full
|
|
225
|
+
set (= the grand-total ``(all)/(all)`` panel) — the latter was previously
|
|
226
|
+
missing. Each subset's columns are set to *margin_nm* and the frames are
|
|
227
|
+
row-bound. Marginalised columns gain *margin_nm* as their LAST factor level
|
|
228
|
+
so margin panels sort last (R ``add_all``).
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
df : pandas.DataFrame
|
|
233
|
+
The cross-product of the row and column faceting frames.
|
|
234
|
+
vars_ : sequence of sequence of str
|
|
235
|
+
``[row_var_names, col_var_names]`` -- the variables eligible for
|
|
236
|
+
marginalisation.
|
|
237
|
+
margins : bool or sequence of str, default False
|
|
238
|
+
``False`` / empty -> no margins (identity). ``True`` -> all variables
|
|
239
|
+
marginalised. A list of names -> only those variables.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
pandas.DataFrame
|
|
244
|
+
*df* with any requested marginal rows appended.
|
|
245
|
+
"""
|
|
246
|
+
if margins is False or margins is None:
|
|
247
|
+
return df
|
|
248
|
+
if isinstance(margins, (list, tuple)) and len(margins) == 0:
|
|
249
|
+
return df
|
|
250
|
+
|
|
251
|
+
from itertools import product
|
|
252
|
+
|
|
253
|
+
def _downto(a: str, b: Sequence[str]) -> List[str]:
|
|
254
|
+
# R: downto(a, b) = rev(upto(a, rev(b))) = elements of b from a to end
|
|
255
|
+
# (so marginalising an outer variable also marginalises nested ones).
|
|
256
|
+
b = list(b)
|
|
257
|
+
return b[b.index(a):] if a in b else []
|
|
258
|
+
|
|
259
|
+
# reshape_margins: every marginal variable subset (R borrowed_ggplot2.R).
|
|
260
|
+
if margins is True:
|
|
261
|
+
margins_list = [v for grp in vars_ for v in grp]
|
|
262
|
+
else:
|
|
263
|
+
margins_list = list(margins)
|
|
264
|
+
dims: List[List[List[str]]] = []
|
|
265
|
+
for set_vars in vars_:
|
|
266
|
+
sv = list(set_vars)
|
|
267
|
+
inter = [v for v in sv if v in margins_list] # R intersect: set order
|
|
268
|
+
dims.append([_downto(m, sv) for m in inter])
|
|
269
|
+
margin_sets: List[List[str]] = []
|
|
270
|
+
for combo in product(*[range(len(d) + 1) for d in dims]):
|
|
271
|
+
sel: List[str] = []
|
|
272
|
+
for set_i, choice in enumerate(combo):
|
|
273
|
+
if choice > 0: # choice 0 == "no margin on this dimension"
|
|
274
|
+
sel.extend(dims[set_i][choice - 1])
|
|
275
|
+
margin_sets.append(sel) # includes [] (original) and the full set
|
|
276
|
+
|
|
277
|
+
affected: List[str] = []
|
|
278
|
+
for s in margin_sets:
|
|
279
|
+
for v in s:
|
|
280
|
+
if v not in affected and v in df.columns:
|
|
281
|
+
affected.append(v)
|
|
282
|
+
if not affected:
|
|
283
|
+
return df
|
|
284
|
+
|
|
285
|
+
df = df.copy()
|
|
286
|
+
|
|
287
|
+
def _add_all(col: pd.Series) -> pd.Categorical:
|
|
288
|
+
# R add_all: factor with margin_nm appended as the LAST level.
|
|
289
|
+
if isinstance(col.dtype, pd.CategoricalDtype):
|
|
290
|
+
cats = list(col.cat.categories)
|
|
291
|
+
else:
|
|
292
|
+
cats = sorted(pd.unique(col.dropna()).tolist())
|
|
293
|
+
if margin_nm not in cats:
|
|
294
|
+
cats = cats + [margin_nm]
|
|
295
|
+
return pd.Categorical(col, categories=cats)
|
|
296
|
+
|
|
297
|
+
for v in affected:
|
|
298
|
+
df[v] = _add_all(df[v])
|
|
299
|
+
|
|
300
|
+
frames: List[pd.DataFrame] = []
|
|
301
|
+
for s in margin_sets:
|
|
302
|
+
block = df.copy()
|
|
303
|
+
for v in s:
|
|
304
|
+
if v in block.columns:
|
|
305
|
+
block[v] = pd.Categorical(
|
|
306
|
+
[margin_nm] * len(block),
|
|
307
|
+
categories=list(block[v].cat.categories),
|
|
308
|
+
)
|
|
309
|
+
frames.append(block)
|
|
310
|
+
out = pd.concat(frames, ignore_index=True).drop_duplicates().reset_index(
|
|
311
|
+
drop=True
|
|
312
|
+
)
|
|
313
|
+
return out
|