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,207 @@
1
+ """The :class:`MultiScale` container and its ``ggplot_add`` handler.
2
+
3
+ R source: ``scale_listed.R:135-203`` (the shared ``ggplot_add.MultiScale`` S3
4
+ method) plus the ``structure(..., class = "MultiScale")`` tagged container built
5
+ by :func:`scale_multi.scale_fill_multi` / :func:`scale_listed.scale_listed`.
6
+
7
+ A :class:`MultiScale` is **not** a :class:`ggplot2_py.scale.Scale` or a ggproto;
8
+ it is a small deferred-mutation container. Its handler, registered on
9
+ :func:`ggplot2_py.plot.update_ggplot` via :func:`functools.singledispatch`,
10
+ runs at ``+``-time and:
11
+
12
+ 1. adds each carried scale to ``plot.scales`` (``ScalesList.add``); and
13
+ 2. rewrites every affected layer's *geom* so that the standard aesthetic
14
+ (``fill``/``colour``) it understands is exposed under the non-standard
15
+ aesthetic name (``fill1``/``spec``/...).
16
+
17
+ The geom rewrite is the **same machinery** as ggnewscale's ``bump_aes_layer``
18
+ geom branch — clone the geom, install ``handle_na``/``draw_key`` column-rename
19
+ shims (using the ``__func__``-capture + ``object.__setattr__`` install to dodge
20
+ GGProto's auto-bind arity bug) and rewrite the geom's aes-name slots — but in the
21
+ *opposite direction*: ``handle_na``/``draw_key`` rename ``new_aes -> replaced_aes``
22
+ on the data columns (so the inner geom sees the standard name), while
23
+ ``default_aes`` / ``non_missing_aes`` / ``optional_aes`` / ``required_aes`` rename
24
+ ``replaced_aes -> new_aes`` on the slot names. Unlike ggnewscale, the layer's
25
+ *stat* is left untouched.
26
+
27
+ Importing this module has the side effect of registering the handler on
28
+ ``update_ggplot``; :mod:`ggh4x.multiscale` imports it for that side effect.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from dataclasses import dataclass, field
34
+ from typing import Any, List
35
+
36
+ import ggplot2_py as _gg
37
+ from ggplot2_py.plot import update_ggplot
38
+
39
+ # Reuse ggnewscale's verified bumping primitives (clone-with-renamed-geom).
40
+ from ggnewscale._bump import (
41
+ _rename_columns,
42
+ _safe_aes_slot,
43
+ _safe_default_aes,
44
+ )
45
+ from ggnewscale._change_name import change_name
46
+
47
+ __all__ = [
48
+ "MultiScale",
49
+ ]
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # MultiScale container
54
+ # ---------------------------------------------------------------------------
55
+ @dataclass
56
+ class MultiScale:
57
+ """Deferred container of non-standard scales for one *replaced* aesthetic.
58
+
59
+ Port of R's ``structure(list(scales=, aes=, replaced_aes=), class="MultiScale")``
60
+ (``scale_multi.R:72-75`` / ``scale_listed.R:127-130``). It is intentionally a
61
+ distinct class (not a ``dict``/``list``) so :func:`functools.singledispatch`
62
+ resolves it to :func:`_add_multiscale` rather than to the list/Mapping
63
+ handlers.
64
+
65
+ Attributes
66
+ ----------
67
+ scales : list of ggplot2_py.scale.Scale
68
+ The scale objects to add to the plot.
69
+ aes : list of str
70
+ Non-standard aesthetic names (e.g. ``["fill1", "fill2"]``).
71
+ replaced_aes : str
72
+ The standard aesthetic those scales replace (``"fill"`` / ``"colour"``).
73
+ """
74
+
75
+ scales: List[Any] = field(default_factory=list)
76
+ aes: List[str] = field(default_factory=list)
77
+ replaced_aes: str = ""
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Per-layer geom rewrite
82
+ # ---------------------------------------------------------------------------
83
+ def _rewrite_layer_geom(layer: Any, new_aes: List[str], replaced_aes: str) -> Any:
84
+ """Clone *layer*'s geom so *replaced_aes* is exposed as *new_aes*.
85
+
86
+ Port of the per-layer rewrite in R ``ggplot_add.MultiScale``
87
+ (``scale_listed.R:150-201``), mirroring ggnewscale's ``bump_aes_layer`` geom
88
+ branch but renaming in the opposite direction and leaving the stat untouched.
89
+
90
+ Parameters
91
+ ----------
92
+ layer : ggplot2_py.layer.Layer
93
+ The layer whose geom is rewritten (mutated in place, matching R's
94
+ ``lay$geom <- new_geom``).
95
+ new_aes : list of str
96
+ The non-standard aesthetic name(s) used by this layer's mapping.
97
+ replaced_aes : str
98
+ The standard aesthetic name those replace.
99
+
100
+ Returns
101
+ -------
102
+ ggplot2_py.layer.Layer
103
+ The same *layer*, with its geom replaced.
104
+ """
105
+ old_geom = layer.geom
106
+
107
+ # handle_na wrapper: rename data columns new_aes -> replaced_aes, then
108
+ # delegate to the *original* geom's handle_na (scale_listed.R:157-163). R
109
+ # captures ``old_geom$handle_na`` as a closure, so the delegate runs against
110
+ # the original geom whose ``required_aes``/``non_missing_aes`` still carry the
111
+ # standard names that the renamed data now matches. ``old_geom.handle_na`` is
112
+ # a bound method of *old_geom*, so calling it with ``(data, params)`` keeps
113
+ # ``self`` pointing at the original geom (not the renamed clone).
114
+ old_handle_na = getattr(old_geom, "handle_na", None)
115
+ new_geom_kwargs: dict = {}
116
+ if old_handle_na is not None:
117
+
118
+ def _new_handle_na(self: Any, data: Any, params: Any, _fn: Any = old_handle_na) -> Any:
119
+ renamed = _rename_columns(data, {a: replaced_aes for a in new_aes})
120
+ return _fn(renamed, params)
121
+
122
+ new_geom_kwargs["handle_na"] = _new_handle_na
123
+
124
+ new_geom = _gg.ggproto(
125
+ f"New{'_'.join(new_aes)}{type(old_geom).__name__}",
126
+ old_geom,
127
+ **new_geom_kwargs,
128
+ )
129
+
130
+ # Slot name-rewrites: replaced_aes -> new_aes (scale_listed.R:179-195).
131
+ # When a layer maps several non-standard aesthetics, R's gsub replaces the
132
+ # standard slot name with *all* of them; the first match wins for downstream
133
+ # lookups, so we use new_aes[0] as ggnewscale does for the single-aes case.
134
+ target_aes = new_aes[0]
135
+ new_geom.default_aes = change_name(
136
+ _safe_default_aes(new_geom), replaced_aes, target_aes
137
+ )
138
+ new_geom.non_missing_aes = change_name(
139
+ _safe_aes_slot(new_geom, "non_missing_aes"), replaced_aes, target_aes
140
+ )
141
+ new_geom.optional_aes = change_name(
142
+ _safe_aes_slot(new_geom, "optional_aes"), replaced_aes, target_aes
143
+ )
144
+ new_geom.required_aes = change_name(
145
+ _safe_aes_slot(new_geom, "required_aes"), replaced_aes, target_aes
146
+ )
147
+
148
+ # draw_key wrapper: rename data columns new_aes -> replaced_aes, then delegate
149
+ # (scale_listed.R:165-171). Capture __func__ and install via
150
+ # object.__setattr__ to bypass GGProto auto-bind (arity dodge).
151
+ old_draw_key_attr = getattr(new_geom, "draw_key", None)
152
+ if old_draw_key_attr is not None:
153
+ old_draw_key_fn = getattr(old_draw_key_attr, "__func__", old_draw_key_attr)
154
+
155
+ def _new_draw_key(data: Any, params: Any, size: Any = None, _fn: Any = old_draw_key_fn) -> Any:
156
+ renamed = _rename_columns(data, {a: replaced_aes for a in new_aes})
157
+ return _fn(renamed, params, size)
158
+
159
+ object.__setattr__(new_geom, "draw_key", _new_draw_key)
160
+
161
+ layer.geom = new_geom
162
+ return layer
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # ggplot_add.MultiScale (scale_listed.R:142-203)
167
+ # ---------------------------------------------------------------------------
168
+ @update_ggplot.register(MultiScale)
169
+ def _add_multiscale(obj: MultiScale, plot: Any, object_name: str = "") -> Any:
170
+ """Add a :class:`MultiScale` to *plot* (R ``ggplot_add.MultiScale``).
171
+
172
+ Adds every carried scale to ``plot.scales`` then rewrites each layer that maps
173
+ one of the container's non-standard aesthetics so its geom exposes the
174
+ standard ``replaced_aes`` under that non-standard name.
175
+
176
+ Parameters
177
+ ----------
178
+ obj : MultiScale
179
+ The container to add.
180
+ plot : ggplot2_py.plot.GGPlot
181
+ The plot to mutate.
182
+ object_name : str, optional
183
+ Unused (kept for the ``update_ggplot`` dispatch signature).
184
+
185
+ Returns
186
+ -------
187
+ ggplot2_py.plot.GGPlot
188
+ The mutated plot.
189
+ """
190
+ # 1. Add scales (scale_listed.R:143-145).
191
+ for sc in obj.scales:
192
+ plot.scales.add(sc)
193
+
194
+ replaced_aes = obj.replaced_aes
195
+
196
+ # 2. Rewrite each affected layer (scale_listed.R:150-201).
197
+ new_layers: List[Any] = []
198
+ for lay in plot.layers:
199
+ mapping_keys = list(lay.mapping.keys()) if lay.mapping is not None else []
200
+ if not any(k in obj.aes for k in mapping_keys):
201
+ new_layers.append(lay)
202
+ continue
203
+ new_aes = [a for a in obj.aes if a in mapping_keys]
204
+ new_layers.append(_rewrite_layer_geom(lay, new_aes, replaced_aes))
205
+
206
+ plot.layers = new_layers
207
+ return plot
@@ -0,0 +1,167 @@
1
+ """Distribute a list of non-standard-aesthetic scales (R source: ``scale_listed.R``).
2
+
3
+ Ports ggh4x's :func:`scale_listed`, which takes a user-supplied list of ready-made
4
+ discrete scales (each bound to a single *non-standard* aesthetic) together with a
5
+ parallel ``replaces`` vector naming the *standard* aesthetic each one substitutes,
6
+ validates them, materialises their guides (setting ``available_aes`` so the legend
7
+ system matches the non-standard aesthetic) and groups them by ``replaces`` into one
8
+ :class:`~ggh4x.multiscale._multiscale_add.MultiScale` per distinct standard
9
+ aesthetic.
10
+
11
+ The grouping mirrors R's ``split(scalelist, replaces)``: groups are emitted in the
12
+ sorted order of the distinct ``replaces`` names (R orders ``split`` by factor
13
+ levels, i.e. alphabetically for a character vector). ``scale_listed`` returns the
14
+ *list* of :class:`MultiScale` objects; the per-layer geom rewrite happens later in
15
+ :func:`ggh4x.multiscale._multiscale_add._add_multiscale`.
16
+
17
+ All semantics were verified against a live R ``ggh4x`` session (validity checks,
18
+ the ``any``-union vs assign ``available_aes`` logic, and the alphabetical
19
+ ``split`` ordering).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, List
25
+
26
+ import ggplot2_py as _gg
27
+ from ggplot2_py import standardise_aes_names
28
+
29
+ from .._cli import cli_abort
30
+ from ._multiscale_add import MultiScale
31
+
32
+ __all__ = [
33
+ "scale_listed",
34
+ "ALL_AESTHETICS",
35
+ ]
36
+
37
+
38
+ # Verbatim port of R ``.all_aesthetics`` (scale_listed.R:205-211).
39
+ ALL_AESTHETICS = frozenset(
40
+ {
41
+ "adj", "alpha", "angle", "bg", "cex", "col", "color", "colour", "fg",
42
+ "fill", "group", "hjust", "label", "linetype", "lower", "lty", "lwd",
43
+ "max", "middle", "min", "pch", "radius", "sample", "shape", "size",
44
+ "srt", "upper", "vjust", "weight", "width", "x", "xend", "xmax", "xmin",
45
+ "xintercept", "y", "yend", "ymax", "ymin", "yintercept", "z",
46
+ }
47
+ )
48
+
49
+
50
+ def _is_guide_proto(obj: Any) -> bool:
51
+ """Return ``True`` when *obj* is a :class:`ggplot2_py.guide.Guide` instance."""
52
+ Guide = getattr(_gg, "Guide", None)
53
+ if Guide is None:
54
+ return False
55
+ try:
56
+ return isinstance(obj, Guide)
57
+ except TypeError:
58
+ return False
59
+
60
+
61
+ def scale_listed(scalelist: List[Any], replaces: Any = None) -> List[MultiScale]:
62
+ """Distribute a list of non-standard-aesthetic scales across the plot.
63
+
64
+ Port of R ``scale_listed`` (``scale_listed.R:56-133``). Validates the scale
65
+ list, materialises each scale's guide with the correct ``available_aes``, and
66
+ groups the scales by the standard aesthetic each replaces into one
67
+ :class:`MultiScale` per distinct ``replaces`` value.
68
+
69
+ This should only be added to a plot **after** every layer the non-standard
70
+ aesthetics affect has been added, since :class:`MultiScale` rewrites those
71
+ layers' geoms at ``+``-time.
72
+
73
+ Parameters
74
+ ----------
75
+ scalelist : list of ggplot2_py.scale.Scale
76
+ Scales each created with a single non-standard ``aesthetics`` argument.
77
+ replaces : sequence of str
78
+ Parallel to *scalelist*; the standard aesthetic (typically ``"colour"``
79
+ or ``"fill"``) each scale replaces.
80
+
81
+ Returns
82
+ -------
83
+ list of MultiScale
84
+ One :class:`MultiScale` per distinct ``replaces`` value, in the sorted
85
+ order of the distinct ``replaces`` names.
86
+
87
+ Raises
88
+ ------
89
+ ValueError
90
+ If *replaces* is not parallel to *scalelist*, contains an invalid
91
+ aesthetic, if a list element is not a :class:`ggplot2_py.scale.Scale`, or
92
+ if any scale does not have exactly one aesthetic.
93
+ """
94
+ if replaces is None:
95
+ replaces = []
96
+ replaces = list(replaces)
97
+
98
+ # Check replaces validity (scale_listed.R:58-69).
99
+ if len(scalelist) != len(replaces):
100
+ cli_abort(
101
+ "The `replaces` argument must be parallel to and of the same length "
102
+ "as the `scalelist` argument."
103
+ )
104
+ replaces = standardise_aes_names(replaces)
105
+ if not all(r in ALL_AESTHETICS for r in replaces):
106
+ cli_abort(
107
+ "The aesthetics in the `replaces` argument must be valid aesthetics."
108
+ )
109
+
110
+ # Check scalelist validity (scale_listed.R:71-82).
111
+ Scale = getattr(_gg, "Scale", None)
112
+ if not all(Scale is not None and isinstance(s, Scale) for s in scalelist):
113
+ cli_abort(
114
+ "The `scalelist` argument must have valid `Scale` objects as "
115
+ "list-elements."
116
+ )
117
+
118
+ # Check scale aesthetics (scale_listed.R:85-98).
119
+ aes_per_scale = [list(s.aesthetics or []) for s in scalelist]
120
+ if any(len(a) > 1 for a in aes_per_scale):
121
+ cli_abort("`scale_listed()` can only accept 1 aesthetic per scale.")
122
+ aesthetics: List[str] = [a for sub in aes_per_scale for a in sub]
123
+ if len(aesthetics) != len(replaces):
124
+ cli_abort(
125
+ "Every scale in the `scalelist` argument must have set valid "
126
+ "aesthetics."
127
+ )
128
+
129
+ # Interpret guides (scale_listed.R:101-121).
130
+ for scale in scalelist:
131
+ guide = scale.guide
132
+ if guide == "none" or guide is False:
133
+ continue
134
+ if not _is_guide_proto(guide):
135
+ # match.fun(paste0("guide_", guide))()
136
+ guide_fn = getattr(_gg, f"guide_{guide}", None)
137
+ if not callable(guide_fn):
138
+ cli_abort(
139
+ f"`scale_listed()` cannot find a guide constructor for "
140
+ f"`{guide}`."
141
+ )
142
+ guide = guide_fn()
143
+ if _is_guide_proto(guide):
144
+ # ggproto(NULL, old): clone so the original guide is untouched.
145
+ guide = _gg.ggproto(None, guide)
146
+ avail = list(getattr(guide, "available_aes", []) or [])
147
+ if "any" not in avail:
148
+ guide.available_aes = list(scale.aesthetics)
149
+ else:
150
+ guide.available_aes = avail + list(scale.aesthetics)
151
+ scale.guide = guide
152
+
153
+ # Split by replaced aes (scale_listed.R:123-132). R's ``split`` keys are the
154
+ # distinct ``replaces`` names ordered as factor levels (alphabetical for a
155
+ # character vector).
156
+ distinct = sorted(set(replaces))
157
+ out: List[MultiScale] = []
158
+ for aes_name in distinct:
159
+ idx = [i for i, r in enumerate(replaces) if r == aes_name]
160
+ out.append(
161
+ MultiScale(
162
+ scales=[scalelist[i] for i in idx],
163
+ aes=[aesthetics[i] for i in idx],
164
+ replaced_aes=aes_name,
165
+ )
166
+ )
167
+ return out