ggh4x-python 0.3.1.9000__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. ggh4x/__init__.py +140 -0
  2. ggh4x/_aimed_text_grob.py +432 -0
  3. ggh4x/_borrowed_ggplot2.py +273 -0
  4. ggh4x/_cli.py +84 -0
  5. ggh4x/_datasets.py +106 -0
  6. ggh4x/_download.py +111 -0
  7. ggh4x/_facet_helpers.py +313 -0
  8. ggh4x/_facet_utils.py +649 -0
  9. ggh4x/_gap_grobs.py +606 -0
  10. ggh4x/_registry.py +10 -0
  11. ggh4x/_rlang.py +93 -0
  12. ggh4x/_utils.py +150 -0
  13. ggh4x/_vctrs.py +233 -0
  14. ggh4x/conveniences.py +601 -0
  15. ggh4x/coord_axes_inside.py +380 -0
  16. ggh4x/element_part_rect.py +545 -0
  17. ggh4x/facet_grid2.py +1018 -0
  18. ggh4x/facet_manual.py +901 -0
  19. ggh4x/facet_nested.py +776 -0
  20. ggh4x/facet_nested_wrap.py +193 -0
  21. ggh4x/facet_wrap2.py +896 -0
  22. ggh4x/geom_box.py +536 -0
  23. ggh4x/geom_outline_point.py +444 -0
  24. ggh4x/geom_pointpath.py +259 -0
  25. ggh4x/geom_polygonraster.py +252 -0
  26. ggh4x/geom_rectrug.py +489 -0
  27. ggh4x/geom_text_aimed.py +279 -0
  28. ggh4x/guide_stringlegend.py +354 -0
  29. ggh4x/help_secondary.py +549 -0
  30. ggh4x/multiscale/__init__.py +51 -0
  31. ggh4x/multiscale/_multiscale_add.py +207 -0
  32. ggh4x/multiscale/scale_listed.py +167 -0
  33. ggh4x/multiscale/scale_manual.py +478 -0
  34. ggh4x/multiscale/scale_multi.py +393 -0
  35. ggh4x/panel_scales/__init__.py +58 -0
  36. ggh4x/panel_scales/at_panel.py +115 -0
  37. ggh4x/panel_scales/facetted_pos_scales.py +647 -0
  38. ggh4x/panel_scales/force_panelsize.py +411 -0
  39. ggh4x/panel_scales/scale_facet.py +222 -0
  40. ggh4x/position_disjoint_ranges.py +229 -0
  41. ggh4x/position_lineartrans.py +242 -0
  42. ggh4x/py.typed +0 -0
  43. ggh4x/resources/faithful.csv +273 -0
  44. ggh4x/resources/iris.csv +151 -0
  45. ggh4x/resources/mtcars.csv +33 -0
  46. ggh4x/resources/pressure.csv +20 -0
  47. ggh4x/resources/volcano.csv +87 -0
  48. ggh4x/save.py +255 -0
  49. ggh4x/stat_difference.py +388 -0
  50. ggh4x/stat_funxy.py +436 -0
  51. ggh4x/stat_rle.py +290 -0
  52. ggh4x/stat_rollingkernel.py +369 -0
  53. ggh4x/stat_theodensity.py +681 -0
  54. ggh4x/strip_nested.py +448 -0
  55. ggh4x/strip_split.py +687 -0
  56. ggh4x/strip_tag.py +636 -0
  57. ggh4x/strip_themed.py +232 -0
  58. ggh4x/strip_vanilla.py +1464 -0
  59. ggh4x/themes.py +31 -0
  60. ggh4x/themes_ggh4x.py +67 -0
  61. ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
  62. ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
  63. ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
  64. ggh4x_python-0.3.1.9000.dist-info/licenses/LICENSE +3 -0
@@ -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