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,478 @@
1
+ """Manual hybrid discrete/continuous position scales (R source: ``scale_manual.R``).
2
+
3
+ Ports ggh4x's :func:`scale_x_manual` / :func:`scale_y_manual` and the
4
+ :class:`ScaleManualPosition` ggproto. These behave like discrete position scales
5
+ (accepting discrete data and limits) but place each discrete level at an arbitrary
6
+ *continuous* coordinate, which needn't be equally spaced.
7
+
8
+ The load-bearing trick (per the R comment at ``scale_manual.R:169``) is that the
9
+ scale's continuous range (:attr:`range_c`) is trained with the scale expansion
10
+ **already baked in** at :meth:`ScaleManualPosition.train` time — this is what lets
11
+ discrete labels land at the requested continuous positions.
12
+
13
+ The :func:`sep_discrete` helper (a function factory that maps separator-delimited
14
+ grouped labels to numeric positions) lives in :mod:`ggh4x.conveniences` and is
15
+ re-exported here for convenience.
16
+
17
+ All semantics were verified against a live R ``ggh4x`` session
18
+ (``ScaleManualPosition$train``/``$map`` ``range_c`` and mapped values for plain,
19
+ named, ``c_limits`` and ``sep_discrete`` value vectors).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, List, Optional, Sequence
25
+
26
+ import numpy as np
27
+
28
+ import scales as _scales
29
+
30
+ from ggplot2_py._compat import waiver, is_waiver
31
+ from ggplot2_py.scale import (
32
+ ScaleDiscretePosition,
33
+ discrete_scale,
34
+ expansion,
35
+ mapped_discrete,
36
+ _is_discrete,
37
+ )
38
+
39
+ from .._cli import cli_abort, cli_warn
40
+ from ..conveniences import sep_discrete
41
+
42
+ __all__ = [
43
+ "scale_x_manual",
44
+ "scale_y_manual",
45
+ "ScaleManualPosition",
46
+ "sep_discrete",
47
+ ]
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # helpers
52
+ # ---------------------------------------------------------------------------
53
+ def _is_discrete_input(x: Any) -> bool:
54
+ """Return ``True`` for discrete data (R ``is.discrete``: factor/char/logical).
55
+
56
+ Port of ``scale_manual.R:259-261``. Delegates to ggplot2_py's
57
+ :func:`_is_discrete` which covers pandas Categoricals/object Series, string and
58
+ boolean ``ndarray``\\ s, and ``str``/``bool`` scalars and sequences.
59
+
60
+ Parameters
61
+ ----------
62
+ x : Any
63
+ Candidate data.
64
+
65
+ Returns
66
+ -------
67
+ bool
68
+ """
69
+ return _is_discrete(x)
70
+
71
+
72
+ def _values_names(values: Any) -> Optional[List[str]]:
73
+ """Return the names of a named ``values`` mapping, or ``None`` if unnamed.
74
+
75
+ A dict-like ``values`` (the Python idiom for R's named numeric vector) carries
76
+ names as its keys; anything else is unnamed.
77
+
78
+ Parameters
79
+ ----------
80
+ values : Any
81
+
82
+ Returns
83
+ -------
84
+ list of str or None
85
+ """
86
+ if isinstance(values, dict):
87
+ return [str(k) for k in values.keys()]
88
+ return None
89
+
90
+
91
+ def _values_numeric(values: Any) -> np.ndarray:
92
+ """Return the numeric magnitudes of ``values`` (dict values or the vector)."""
93
+ if isinstance(values, dict):
94
+ return np.asarray(list(values.values()), dtype=float)
95
+ return np.asarray(values, dtype=float)
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # ScaleManualPosition ggproto class (scale_manual.R:155-206)
100
+ # ---------------------------------------------------------------------------
101
+ class ScaleManualPosition(ScaleDiscretePosition):
102
+ """Position scale placing discrete levels at arbitrary continuous coordinates.
103
+
104
+ Subclass of :class:`ggplot2_py.scale.ScaleDiscretePosition` ported from R
105
+ ``ScaleManualPosition`` (``scale_manual.R:155-206``). Overrides
106
+ :meth:`train` and :meth:`map` entirely.
107
+
108
+ Attributes
109
+ ----------
110
+ c_limits : numpy.ndarray or None
111
+ Optional length-2 continuous-limit override (``NaN`` entries fall back to
112
+ the data-derived range), set by :func:`_scale_position_manual`.
113
+ range_c : scales.ContinuousRange
114
+ The continuous range, trained with the expansion already applied at
115
+ :meth:`train` time.
116
+ """
117
+
118
+ c_limits: Any = None
119
+
120
+ def train(self, x: Any) -> None:
121
+ """Train the continuous range, baking in the scale expansion.
122
+
123
+ Port of R ``ScaleManualPosition$train`` (``scale_manual.R:158-174``).
124
+ For discrete *x* the discrete range is trained and the value range is the
125
+ range of the palette over the limits; for continuous *x* the value range
126
+ is ``range(x)``. Any non-``NaN`` :attr:`c_limits` overrides the
127
+ corresponding bound. The expansion (``self.expand`` or
128
+ ``expansion(add=0.6)``) is then applied and the expanded bounds trained
129
+ into :attr:`range_c`.
130
+
131
+ Parameters
132
+ ----------
133
+ x : array-like
134
+ Layer data for this position aesthetic.
135
+ """
136
+ if _is_discrete_input(x):
137
+ self.range.train(
138
+ x, drop=self.drop, na_rm=not self.na_translate
139
+ )
140
+ pal = self.palette(self.get_limits())
141
+ # A named palette (R named numeric vector) is a dict here; range() in R
142
+ # operates on the magnitudes regardless of names.
143
+ pal_arr = _values_numeric(pal)
144
+ rng = np.array([np.nanmin(pal_arr), np.nanmax(pal_arr)], dtype=float)
145
+ else:
146
+ x_arr = np.asarray(x, dtype=float)
147
+ rng = np.array([np.nanmin(x_arr), np.nanmax(x_arr)], dtype=float)
148
+
149
+ # c_limits override (scale_manual.R:166-168): NA-aware ifelse.
150
+ if self.c_limits is not None:
151
+ c_lim = np.asarray(self.c_limits, dtype=float)
152
+ rng = np.where(np.isnan(c_lim), rng, c_lim)
153
+
154
+ # Hack for scale expansion (scale_manual.R:169-173). expansion() returns
155
+ # [mul_lo, add_lo, mul_hi, add_hi]; expand_range(range, mul, add).
156
+ expand = self.expand if not is_waiver(self.expand) else expansion(add=0.6)
157
+ expand = np.asarray(expand, dtype=float)
158
+ lower = _scales.expand_range(rng, expand[0], expand[1])[0]
159
+ upper = _scales.expand_range(rng, expand[2], expand[3])[1]
160
+ self.range_c.train(np.array([lower, upper], dtype=float))
161
+
162
+ def map(self, x: Any, limits: Optional[Any] = None) -> Any:
163
+ """Map discrete *x* to continuous positions via the manual palette.
164
+
165
+ Port of R ``ScaleManualPosition$map`` (``scale_manual.R:176-205``). For
166
+ discrete *x* the palette is resolved (with the
167
+ ``n_breaks_cache``/``palette_cache`` cache), honouring a *named* palette
168
+ (match by name, blanking names absent from *limits*) or a positional match
169
+ against *limits*; missing values are filled with :attr:`na_value` when
170
+ :attr:`na_translate`. Continuous *x* passes straight through. The result
171
+ is always wrapped as a :func:`mapped_discrete` sentinel.
172
+
173
+ Parameters
174
+ ----------
175
+ x : array-like
176
+ Data to map.
177
+ limits : array-like, optional
178
+ Scale limits. Defaults to :meth:`get_limits`.
179
+
180
+ Returns
181
+ -------
182
+ ggplot2_py.scale._MappedDiscrete
183
+ The mapped continuous positions.
184
+ """
185
+ if limits is None:
186
+ limits = self.get_limits()
187
+
188
+ if _is_discrete_input(x):
189
+ limits_list = list(limits) if limits is not None else []
190
+ n = sum(
191
+ 1
192
+ for v in limits_list
193
+ if not (v is None or (isinstance(v, float) and np.isnan(v)))
194
+ )
195
+
196
+ if self.n_breaks_cache is not None and self.n_breaks_cache == n:
197
+ pal = self.palette_cache
198
+ else:
199
+ if self.n_breaks_cache is not None:
200
+ cli_warn("Cached palette does not match requested.")
201
+ pal = self.palette(limits)
202
+ self.palette_cache = pal
203
+ self.n_breaks_cache = n
204
+
205
+ pal_names = _values_names(pal)
206
+ limits_str = [str(v) for v in limits_list]
207
+ x_str = [str(v) for v in np.asarray(x)]
208
+
209
+ if pal_names is not None:
210
+ # Named-palette branch (scale_manual.R:190-194).
211
+ pal_vals = list(_values_numeric(pal))
212
+ # Blank entries whose names are not in the limits.
213
+ pal_vals = [
214
+ (np.nan if pal_names[i] not in limits_str else pal_vals[i])
215
+ for i in range(len(pal_names))
216
+ ]
217
+ name_to_val = {pal_names[i]: pal_vals[i] for i in range(len(pal_names))}
218
+ pal_match = np.array(
219
+ [name_to_val.get(v, np.nan) for v in x_str], dtype=float
220
+ )
221
+ else:
222
+ # Positional branch (scale_manual.R:196): pal[match(x, limits)].
223
+ pal_vals = np.asarray(pal, dtype=float)
224
+ idx = {v: i for i, v in enumerate(limits_str)}
225
+ pal_match = np.array(
226
+ [
227
+ pal_vals[idx[v]] if v in idx and idx[v] < len(pal_vals) else np.nan
228
+ for v in x_str
229
+ ],
230
+ dtype=float,
231
+ )
232
+
233
+ if self.na_translate:
234
+ x_is_na = np.array(
235
+ [
236
+ v is None or (isinstance(v, float) and np.isnan(v)) or v == "nan"
237
+ for v in x_str
238
+ ]
239
+ )
240
+ na_fill = float(self.na_value) if _is_number(self.na_value) else np.nan
241
+ fill_mask = x_is_na | np.isnan(pal_match)
242
+ pal_match = np.where(fill_mask, na_fill, pal_match)
243
+
244
+ x = pal_match
245
+
246
+ return mapped_discrete(np.asarray(x, dtype=float))
247
+
248
+
249
+ def _is_number(v: Any) -> bool:
250
+ """Return ``True`` for a real numeric scalar (used for ``na_value`` coercion)."""
251
+ return isinstance(v, (int, float, np.integer, np.floating)) and not isinstance(v, bool)
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # internal constructor (scale_manual.R:82-151)
256
+ # ---------------------------------------------------------------------------
257
+ def _scale_position_manual(
258
+ aesthetics: Sequence[str],
259
+ values: Any = None,
260
+ *,
261
+ limits: Any = None,
262
+ c_limits: Any = None,
263
+ breaks: Any = None,
264
+ expand: Any = None,
265
+ guide: Any = None,
266
+ position: str = "bottom",
267
+ **kwargs: Any,
268
+ ) -> ScaleManualPosition:
269
+ """Build a :class:`ScaleManualPosition` (R ``scale_position_manual``).
270
+
271
+ Port of ``scale_manual.R:82-151``.
272
+
273
+ Parameters
274
+ ----------
275
+ aesthetics : sequence of str
276
+ The position aesthetics (e.g. ``["x", "xmin", "xmax", "xend"]``).
277
+ values : callable or numeric or dict
278
+ A palette function ``limits -> numeric``, or a numeric vector (optionally
279
+ named via a ``dict``) of positions parallel to the unique values.
280
+ limits : array-like or callable, optional
281
+ Scale limits. Defaults to intersecting the data with the ``values`` names
282
+ when *values* is named.
283
+ c_limits : array-like or None, optional
284
+ ``None`` to use the value range, or a length-2 numeric (``NaN`` entries
285
+ fall back to the value range) for custom continuous limits.
286
+ breaks : array-like, optional
287
+ Breaks; when *values* is an unnamed vector these also name it.
288
+ expand : array-like or Waiver, optional
289
+ Scale expansion.
290
+ guide : Any, optional
291
+ Guide spec.
292
+ position : str, default ``"bottom"``
293
+ Axis position.
294
+ **kwargs : Any
295
+ Extra arguments forwarded to :func:`ggplot2_py.discrete_scale`.
296
+
297
+ Returns
298
+ -------
299
+ ScaleManualPosition
300
+
301
+ Raises
302
+ ------
303
+ ValueError
304
+ If *values* is neither a function nor numeric, or if *c_limits* is not
305
+ ``None`` or a length-2 numeric vector.
306
+ """
307
+ if breaks is None:
308
+ breaks = waiver()
309
+ if expand is None:
310
+ expand = waiver()
311
+ if guide is None:
312
+ guide = waiver()
313
+
314
+ if not callable(values):
315
+ # Validate is.numeric before extracting magnitudes (scale_manual.R:96-101).
316
+ if not (isinstance(values, dict) or _is_numeric_vector(values)):
317
+ cli_abort("The `values` argument must be `numeric`.", TypeError)
318
+
319
+ names = _values_names(values)
320
+ # If limits is None and values has names -> intersect-with-names limits.
321
+ if limits is None and names is not None:
322
+ names_set = list(names)
323
+
324
+ def _limits_fn(x: Any, _names: List[str] = names_set) -> List[str]:
325
+ # intersect(x, names(values)) %||% character()
326
+ xs = [str(v) for v in x]
327
+ return [v for v in xs if v in _names]
328
+
329
+ limits = _limits_fn
330
+
331
+ # Unnamed vector + breaks given -> name the values by breaks.
332
+ if (
333
+ names is None
334
+ and not is_waiver(breaks)
335
+ and breaks is not None
336
+ and not callable(breaks)
337
+ ):
338
+ brks = list(breaks)
339
+ vals = list(_values_numeric(values))
340
+ if len(brks) <= len(vals):
341
+ values = {str(brks[i]): vals[i] for i in range(len(brks))}
342
+ else:
343
+ values = {str(brks[i]): vals[i] for i in range(len(vals))}
344
+
345
+ # Build the palette (scale_manual.R:118-126).
346
+ def _pal(lims: Any, _values: Any = values) -> np.ndarray:
347
+ vals = _values_numeric(_values)
348
+ lims_list = list(lims) if lims is not None else []
349
+ if len(lims_list) > len(vals):
350
+ cli_abort(
351
+ f"Insufficient values in manual scale. {len(lims_list)} needed "
352
+ f"but {len(vals)} provided."
353
+ )
354
+ out = vals[: len(lims_list)]
355
+ names2 = _values_names(_values)
356
+ if names2 is not None:
357
+ # Carry the names so map()'s named-palette branch fires.
358
+ return {names2[i]: out[i] for i in range(len(out))}
359
+ return out
360
+
361
+ pal = _pal
362
+ else:
363
+ pal = values
364
+
365
+ # Validate c_limits (scale_manual.R:131-136).
366
+ if c_limits is not None:
367
+ c_arr = np.asarray(c_limits, dtype=float)
368
+ if c_arr.ndim != 1 or c_arr.size != 2:
369
+ cli_abort(
370
+ "The `c_limits` argument must either be `None` or a `numeric` "
371
+ "vector of length 2."
372
+ )
373
+
374
+ sc = discrete_scale(
375
+ list(aesthetics),
376
+ pal,
377
+ limits=limits,
378
+ expand=expand,
379
+ guide=guide,
380
+ position=position,
381
+ super_class=ScaleManualPosition,
382
+ **kwargs,
383
+ )
384
+ sc.range_c = _scales.ContinuousRange()
385
+ sc.c_limits = c_limits
386
+ return sc
387
+
388
+
389
+ def _is_numeric_vector(values: Any) -> bool:
390
+ """Return ``True`` when *values* is a numeric vector (R ``is.numeric``)."""
391
+ if isinstance(values, (int, float, np.integer, np.floating)) and not isinstance(values, bool):
392
+ return True
393
+ if isinstance(values, (list, tuple, np.ndarray)):
394
+ if len(values) == 0:
395
+ return True
396
+ try:
397
+ np.asarray(values, dtype=float)
398
+ return True
399
+ except (TypeError, ValueError):
400
+ return False
401
+ return False
402
+
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # external constructors (scale_manual.R:48-78)
406
+ # ---------------------------------------------------------------------------
407
+ def scale_x_manual(
408
+ values: Any,
409
+ c_limits: Any = None,
410
+ position: str = "bottom",
411
+ **kwargs: Any,
412
+ ) -> ScaleManualPosition:
413
+ """A hybrid discrete/continuous manual position scale for ``x``.
414
+
415
+ Port of R ``scale_x_manual`` (``scale_manual.R:48-61``). Accepts discrete
416
+ input like a discrete scale but maps each level to an arbitrary continuous
417
+ coordinate.
418
+
419
+ Parameters
420
+ ----------
421
+ values : callable or numeric or dict
422
+ A numeric vector with the same length as the unique values (optionally
423
+ named via a ``dict``), or a function accepting the limits and returning a
424
+ parallel numeric vector (see :func:`sep_discrete`).
425
+ c_limits : array-like or None, default ``None``
426
+ ``None`` to use the value range as the continuous limits, or a length-2
427
+ numeric (``NaN`` entries fall back to the value range) for custom limits.
428
+ position : str, default ``"bottom"``
429
+ Axis position.
430
+ **kwargs : Any
431
+ Extra arguments forwarded to :func:`ggplot2_py.discrete_scale`.
432
+
433
+ Returns
434
+ -------
435
+ ScaleManualPosition
436
+ """
437
+ return _scale_position_manual(
438
+ ["x", "xmin", "xmax", "xend"],
439
+ values=values,
440
+ c_limits=c_limits,
441
+ position=position,
442
+ **kwargs,
443
+ )
444
+
445
+
446
+ def scale_y_manual(
447
+ values: Any,
448
+ c_limits: Any = None,
449
+ position: str = "left",
450
+ **kwargs: Any,
451
+ ) -> ScaleManualPosition:
452
+ """A hybrid discrete/continuous manual position scale for ``y``.
453
+
454
+ Port of R ``scale_y_manual`` (``scale_manual.R:65-78``). Behaves like
455
+ :func:`scale_x_manual` but for the ``y`` aesthetic.
456
+
457
+ Parameters
458
+ ----------
459
+ values : callable or numeric or dict
460
+ Positions for the unique values, or a function mapping limits to positions.
461
+ c_limits : array-like or None, default ``None``
462
+ Continuous-limit override (see :func:`scale_x_manual`).
463
+ position : str, default ``"left"``
464
+ Axis position.
465
+ **kwargs : Any
466
+ Extra arguments forwarded to :func:`ggplot2_py.discrete_scale`.
467
+
468
+ Returns
469
+ -------
470
+ ScaleManualPosition
471
+ """
472
+ return _scale_position_manual(
473
+ ["y", "ymin", "ymax", "yend"],
474
+ values=values,
475
+ c_limits=c_limits,
476
+ position=position,
477
+ **kwargs,
478
+ )