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,279 @@
1
+ """Aimed text (port of ggh4x ``geom_text_aimed.R``).
2
+
3
+ Like :func:`ggplot2_py.geom_text`, ``geom_text_aimed()`` draws text, but it
4
+ rotates each label so it appears *aimed* towards a point defined by the
5
+ ``xend``/``yend`` aesthetics. The computed angle is added to the ``angle``
6
+ aesthetic and is evaluated in absolute coordinates, so resizing the plot keeps
7
+ the same appearance.
8
+
9
+ R source: ``ggh4x/R/geom_text_aimed.R``.
10
+
11
+ Notes
12
+ -----
13
+ * :meth:`GeomTextAimed.draw_panel` builds a second data frame ``aim`` from the
14
+ ``xend``/``yend`` aesthetics (renamed to ``x``/``y``) and coord-transforms it
15
+ separately so the aim point lands in the same transformed space as the text
16
+ anchor. Character ``hjust``/``vjust`` are resolved with :func:`compute_just`.
17
+ * ``xend``/``yend`` default to ``-Inf`` (the lower-left corner), so unaimed text
18
+ points to the lower-left. These are real, mappable default aesthetics.
19
+ * ``parse=True`` (R plotmath) has no engine in :mod:`grid_py`; this port falls
20
+ back to plain-string labels (documented deviation).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Optional
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+ from ggplot2_py.geom import (
31
+ GeomText,
32
+ FromTheme,
33
+ Gpar,
34
+ Mapping,
35
+ PT,
36
+ _coord_transform,
37
+ scales_alpha,
38
+ )
39
+
40
+ from ._aimed_text_grob import aimed_text_grob, compute_just
41
+
42
+ __all__ = [
43
+ "geom_text_aimed",
44
+ "GeomTextAimed",
45
+ ]
46
+
47
+
48
+ class GeomTextAimed(GeomText):
49
+ """Text geom that aims each label towards an ``(xend, yend)`` point.
50
+
51
+ Subclass of :class:`ggplot2_py.GeomText` ported from R ``GeomTextAimed``
52
+ (``geom_text_aimed.R:93-156``).
53
+ """
54
+
55
+ # R geom_text_aimed.R:95-107. GeomText-like defaults plus the aim-target
56
+ # aesthetics ``xend``/``yend`` (default -Inf) and ``angle``.
57
+ default_aes: Mapping = Mapping(
58
+ colour=FromTheme("colour", fallback="ink"),
59
+ size=FromTheme("fontsize"),
60
+ family=FromTheme("family", fallback=""),
61
+ angle=0,
62
+ xend=-np.inf,
63
+ yend=-np.inf,
64
+ hjust=0.5,
65
+ vjust=0.5,
66
+ alpha=None,
67
+ fontface=1,
68
+ lineheight=1.2,
69
+ )
70
+
71
+ extra_params = ("na_rm", "flip_upsidedown")
72
+
73
+ def draw_panel(
74
+ self,
75
+ data: pd.DataFrame,
76
+ panel_params: Any,
77
+ coord: Any,
78
+ parse: bool = False,
79
+ na_rm: bool = False,
80
+ check_overlap: bool = False,
81
+ flip_upsidedown: bool = True,
82
+ **params: Any,
83
+ ) -> Any:
84
+ """Build an :class:`~ggh4x._aimed_text_grob.AimedTextGrob` for one panel.
85
+
86
+ Port of R ``GeomTextAimed$draw_panel`` (``geom_text_aimed.R:108-154``).
87
+
88
+ Parameters
89
+ ----------
90
+ data : pandas.DataFrame
91
+ Layer data for one panel (carries ``xend``/``yend``).
92
+ panel_params : Any
93
+ Panel scales / ranges.
94
+ coord : Any
95
+ Active coordinate system.
96
+ parse : bool, default ``False``
97
+ R plotmath parsing. Not supported; treated as a plain-label
98
+ passthrough (documented deviation).
99
+ na_rm : bool, default ``False``
100
+ Whether missing values are silently removed.
101
+ check_overlap : bool, default ``False``
102
+ Whether overlapping labels are suppressed.
103
+ flip_upsidedown : bool, default ``True``
104
+ Whether labels rotated into ``(90, 270)`` are flipped for
105
+ readability.
106
+ **params : Any
107
+ Ignored extra parameters.
108
+
109
+ Returns
110
+ -------
111
+ grid_py.Grob
112
+ An :class:`~ggh4x._aimed_text_grob.AimedTextGrob`.
113
+ """
114
+ data = data.copy()
115
+ lab = data["label"].to_numpy() if "label" in data.columns else np.array([])
116
+
117
+ # parse=True would build R plotmath expressions; no engine exists in
118
+ # grid_py, so fall back to plain string labels.
119
+ # (R raises if label is not character; we simply pass through.)
120
+
121
+ # Build the aim frame from xend/yend and coord-transform separately so
122
+ # the aim point ends up in the same transformed space.
123
+ aim = pd.DataFrame(
124
+ {
125
+ "x": data["xend"].to_numpy(dtype="float64"),
126
+ "y": data["yend"].to_numpy(dtype="float64"),
127
+ }
128
+ )
129
+ data = _coord_transform(coord, data, panel_params)
130
+ aim = _coord_transform(coord, aim, panel_params)
131
+
132
+ # Resolve character justifications.
133
+ hjust = data["hjust"].to_numpy() if "hjust" in data.columns else np.full(len(data), 0.5)
134
+ vjust = data["vjust"].to_numpy() if "vjust" in data.columns else np.full(len(data), 0.5)
135
+ if _is_character(vjust):
136
+ vjust = compute_just(vjust, data["y"].to_numpy(dtype="float64"))
137
+ else:
138
+ vjust = np.asarray(vjust, dtype="float64")
139
+ if _is_character(hjust):
140
+ hjust = compute_just(hjust, data["x"].to_numpy(dtype="float64"))
141
+ else:
142
+ hjust = np.asarray(hjust, dtype="float64")
143
+
144
+ size = data["size"].to_numpy(dtype="float64") if "size" in data.columns else np.full(len(data), 3.88)
145
+ colour = data["colour"].to_numpy() if "colour" in data.columns else "black"
146
+ alpha = data["alpha"].to_numpy() if "alpha" in data.columns else None
147
+ family = data["family"].to_numpy() if "family" in data.columns else ""
148
+ fontface = data["fontface"].to_numpy() if "fontface" in data.columns else 1
149
+ lineheight = data["lineheight"].to_numpy() if "lineheight" in data.columns else 1.2
150
+ angle = data["angle"].to_numpy(dtype="float64") if "angle" in data.columns else np.zeros(len(data))
151
+
152
+ gp = Gpar(
153
+ col=scales_alpha(colour, alpha),
154
+ fontsize=size * PT,
155
+ fontfamily=family,
156
+ fontface=fontface,
157
+ lineheight=lineheight,
158
+ )
159
+
160
+ return aimed_text_grob(
161
+ label=lab,
162
+ x=data["x"].to_numpy(dtype="float64"),
163
+ y=data["y"].to_numpy(dtype="float64"),
164
+ x0=aim["x"].to_numpy(dtype="float64"),
165
+ y0=aim["y"].to_numpy(dtype="float64"),
166
+ default_units="native",
167
+ hjust=hjust,
168
+ vjust=vjust,
169
+ rot=angle,
170
+ gp=gp,
171
+ flip_upsidedown=flip_upsidedown,
172
+ check_overlap=check_overlap,
173
+ )
174
+
175
+
176
+ def _is_character(values: Any) -> bool:
177
+ """Return ``True`` when *values* contains (any) string entries.
178
+
179
+ Parameters
180
+ ----------
181
+ values : Any
182
+ A scalar or array of justification values.
183
+
184
+ Returns
185
+ -------
186
+ bool
187
+ Whether the input is character-like (R ``is.character``).
188
+ """
189
+ arr = np.atleast_1d(np.asarray(values, dtype=object))
190
+ return any(isinstance(v, str) for v in arr)
191
+
192
+
193
+ def geom_text_aimed(
194
+ mapping: Optional[Mapping] = None,
195
+ data: Any = None,
196
+ stat: str = "identity",
197
+ position: str = "identity",
198
+ parse: bool = False,
199
+ nudge_x: float = 0,
200
+ nudge_y: float = 0,
201
+ flip_upsidedown: bool = True,
202
+ check_overlap: bool = False,
203
+ na_rm: bool = False,
204
+ show_legend: Any = None,
205
+ inherit_aes: bool = True,
206
+ **kwargs: Any,
207
+ ) -> Any:
208
+ """Create an aimed-text layer.
209
+
210
+ Port of R ``geom_text_aimed()`` (``geom_text_aimed.R:45-85``). Draws text
211
+ rotated to point at the ``xend``/``yend`` aim target.
212
+
213
+ Parameters
214
+ ----------
215
+ mapping : Mapping, optional
216
+ Aesthetic mapping created by :func:`ggplot2_py.aes`.
217
+ data : Any, optional
218
+ Layer data.
219
+ stat : str, default ``"identity"``
220
+ Statistical transformation.
221
+ position : str, default ``"identity"``
222
+ Position adjustment. Cannot be combined with ``nudge_x``/``nudge_y``.
223
+ parse : bool, default ``False``
224
+ R plotmath parsing. Not supported (plain-label fallback).
225
+ nudge_x, nudge_y : float, default ``0``
226
+ Horizontal / vertical nudge offsets (translated to a
227
+ :func:`ggplot2_py.position_nudge`).
228
+ flip_upsidedown : bool, default ``True``
229
+ Whether labels rotated into ``(90, 270)`` are flipped for readability.
230
+ check_overlap : bool, default ``False``
231
+ Whether overlapping labels are suppressed.
232
+ na_rm : bool, default ``False``
233
+ If ``True``, silently remove missing values.
234
+ show_legend : bool or None, default ``None``
235
+ Whether to show a legend for this layer.
236
+ inherit_aes : bool, default ``True``
237
+ Whether to inherit the plot's default aesthetics.
238
+ **kwargs : Any
239
+ Additional aesthetic parameters passed to the layer.
240
+
241
+ Returns
242
+ -------
243
+ ggplot2_py.Layer
244
+ A layer object that can be added to a plot.
245
+
246
+ Raises
247
+ ------
248
+ ValueError
249
+ If both ``position`` and ``nudge_x``/``nudge_y`` are specified.
250
+ """
251
+ from ggplot2_py.layer import layer
252
+
253
+ if nudge_x != 0 or nudge_y != 0:
254
+ if position != "identity":
255
+ from ggh4x._cli import cli_abort
256
+
257
+ cli_abort(
258
+ "Specify either `position` or `nudge_x`/`nudge_y`, not both."
259
+ )
260
+ from ggplot2_py.position import position_nudge
261
+
262
+ position = position_nudge(nudge_x, nudge_y)
263
+
264
+ return layer(
265
+ data=data,
266
+ mapping=mapping,
267
+ stat=stat,
268
+ geom=GeomTextAimed,
269
+ position=position,
270
+ show_legend=show_legend,
271
+ inherit_aes=inherit_aes,
272
+ params={
273
+ "parse": parse,
274
+ "check_overlap": check_overlap,
275
+ "na_rm": na_rm,
276
+ "flip_upsidedown": flip_upsidedown,
277
+ **kwargs,
278
+ },
279
+ )
@@ -0,0 +1,354 @@
1
+ """String legend guide (R source: ``guide_stringlegend.R``).
2
+
3
+ Ports ggh4x's :func:`guide_stringlegend` constructor and the
4
+ :class:`GuideStringlegend` ggproto, which renders colour/fill (and optionally
5
+ ``family``/``fontface``) mappings as **coloured text strings** rather than as the
6
+ geom key swatches drawn by :func:`ggplot2_py.guide_legend`.
7
+
8
+ This is live, non-deprecated ggh4x code. :class:`GuideStringlegend` extends
9
+ :class:`ggplot2_py.guide.GuideLegend`, inheriting the full legend draw
10
+ orchestration (:meth:`ggplot2_py.guide.Guide.draw`) and overriding exactly the
11
+ five points where a string legend differs from a key legend:
12
+
13
+ * :meth:`GuideStringlegend.get_layer_key` -- identity passthrough (no geom keys);
14
+ * :meth:`GuideStringlegend.setup_params` -- parent params then zero key cell sizes;
15
+ * :meth:`GuideStringlegend.setup_elements` -- pull ``legend.text`` margin onto the
16
+ resolved text element and zero the key width/height (the load-bearing
17
+ "text only, no swatch" mechanism);
18
+ * :meth:`GuideStringlegend.build_labels` -- a coloured text grob per key row;
19
+ * :meth:`GuideStringlegend.build_decor` -- a single empty grob (no swatches).
20
+
21
+ Parent dispatch follows the fixed strategy: ``GuideLegend.setup_params`` is a
22
+ ``@staticmethod(params)`` while ``GuideLegend.setup_elements`` is an instance
23
+ method; each is invoked through :func:`ggplot2_py.ggproto_parent` with its correct
24
+ positional arity.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import Any, Dict, List, Optional
30
+
31
+ from ggplot2_py import ggproto_parent
32
+ from ggplot2_py._compat import waiver
33
+ from ggplot2_py.guide import GuideLegend, new_guide
34
+ from ggplot2_py.theme_elements import (
35
+ ElementText,
36
+ calc_element,
37
+ element_grob,
38
+ )
39
+ from grid_py import Unit, null_grob
40
+
41
+ __all__ = [
42
+ "guide_stringlegend",
43
+ "GuideStringlegend",
44
+ ]
45
+
46
+
47
+ def _column(key: Any, name: str) -> Any:
48
+ """Return key column *name* (a sequence) or ``None`` when absent.
49
+
50
+ Mirrors R's ``key$<name>`` which yields ``NULL`` for a missing column. *key*
51
+ is a :class:`pandas.DataFrame`; ``df.get(name)`` returns ``None`` when the
52
+ column is not present.
53
+
54
+ Parameters
55
+ ----------
56
+ key : pandas.DataFrame
57
+ name : str
58
+
59
+ Returns
60
+ -------
61
+ pandas.Series or None
62
+ """
63
+ if key is None:
64
+ return None
65
+ getter = getattr(key, "get", None)
66
+ if callable(getter):
67
+ return getter(name)
68
+ return None
69
+
70
+
71
+ def _at(seq: Any, i: int) -> Any:
72
+ """Return the ``i``-th element of *seq* (``Series`` or list), or ``None``."""
73
+ if seq is None:
74
+ return None
75
+ iloc = getattr(seq, "iloc", None)
76
+ if iloc is not None:
77
+ return iloc[i]
78
+ return seq[i]
79
+
80
+
81
+ class GuideStringlegend(GuideLegend):
82
+ """Legend that renders colour/fill mappings as coloured text, not key swatches.
83
+
84
+ Subclass of :class:`ggplot2_py.guide.GuideLegend` ported from R
85
+ ``GuideStringlegend`` (``guide_stringlegend.R:52-98``).
86
+
87
+ Notes
88
+ -----
89
+ The ``available_aes`` (``["colour", "fill", "family", "fontface"]``) and
90
+ ``name`` (``"stringlegend"``) are injected by :func:`guide_stringlegend` at
91
+ instance-build time via :func:`ggplot2_py.guide.new_guide`.
92
+ """
93
+
94
+ _class_name = "GuideStringlegend"
95
+
96
+ def get_layer_key(
97
+ self,
98
+ params: Dict[str, Any],
99
+ layers: Optional[List[Any]] = None,
100
+ data: Optional[List[Any]] = None,
101
+ ) -> Dict[str, Any]:
102
+ """Return *params* unchanged (R ``get_layer_key``: identity passthrough).
103
+
104
+ Port of ``guide_stringlegend.R:55-57``. A string legend has no geom keys,
105
+ so it bypasses the base machinery that resolves per-key ``draw_key`` decor.
106
+ The arity ``(self, params, layers, data=None)`` is kept so the inherited
107
+ ``process_layers`` still dispatches correctly.
108
+
109
+ Parameters
110
+ ----------
111
+ params : dict
112
+ The guide parameters.
113
+ layers, data : optional
114
+ Accepted and ignored (R uses ``...``).
115
+
116
+ Returns
117
+ -------
118
+ dict
119
+ *params* unchanged.
120
+ """
121
+ return params
122
+
123
+ @staticmethod
124
+ def setup_params(params: Dict[str, Any]) -> Dict[str, Any]:
125
+ """Compute legend params, then zero the key cell sizes.
126
+
127
+ Port of ``guide_stringlegend.R:59-63``. Delegates to the parent
128
+ ``GuideLegend.setup_params`` (a ``@staticmethod`` computing
129
+ ``nrow``/``ncol``/``n_breaks`` and validating ``direction``), then sets
130
+ ``params['sizes'] = {'widths': 0, 'heights': 0}`` so the key cells take no
131
+ space. The load-bearing zeroing is also enforced via the key unit
132
+ elements in :meth:`setup_elements`.
133
+
134
+ Parameters
135
+ ----------
136
+ params : dict
137
+
138
+ Returns
139
+ -------
140
+ dict
141
+ """
142
+ params = GuideLegend.setup_params(params)
143
+ params = dict(params)
144
+ params["sizes"] = {"widths": 0, "heights": 0}
145
+ return params
146
+
147
+ def setup_elements(
148
+ self,
149
+ params: Dict[str, Any],
150
+ elements: Optional[Dict[str, Any]] = None,
151
+ theme: Any = None,
152
+ ) -> Dict[str, Any]:
153
+ """Resolve elements, injecting the text margin and zeroing the key size.
154
+
155
+ Port of ``guide_stringlegend.R:65-73``. Merges ``params['theme']`` into
156
+ *theme* (then clears it to avoid a double-add), delegates to the parent
157
+ ``GuideLegend.setup_elements``, pulls the ``legend.text`` margin onto the
158
+ resolved text element (so :meth:`build_labels`' titleGrob margins control
159
+ inter-key spacing), sets ``spacing_y`` from ``legend.key.spacing.y`` and
160
+ zeroes the key width/height (so only the text shows).
161
+
162
+ Parameters
163
+ ----------
164
+ params : dict
165
+ elements : dict, optional
166
+ Defaults to a copy of ``self.elements``.
167
+ theme : Theme, optional
168
+
169
+ Returns
170
+ -------
171
+ dict
172
+ """
173
+ if elements is None:
174
+ elements = dict(self.elements)
175
+ if theme is not None:
176
+ # Theme.__add__ tolerates a None operand (params['theme'] may be None).
177
+ theme = theme + params.get("theme")
178
+ params = dict(params)
179
+ params["theme"] = None
180
+
181
+ elements = ggproto_parent(GuideLegend, self).setup_elements(
182
+ params, elements, theme
183
+ )
184
+
185
+ elements["spacing_y"] = calc_element("legend.key.spacing.y", theme)
186
+
187
+ # Pull the legend.text margin onto the resolved text element. Build a
188
+ # fresh ElementText (copying every field) rather than mutating .margin in
189
+ # place, since element objects may be shared/cached.
190
+ text_el = elements.get("text")
191
+ text_margin = getattr(calc_element("legend.text", theme), "margin", None)
192
+ if text_el is not None:
193
+ elements["text"] = ElementText(
194
+ family=getattr(text_el, "family", None),
195
+ face=getattr(text_el, "face", None),
196
+ colour=getattr(text_el, "colour", None),
197
+ size=getattr(text_el, "size", None),
198
+ hjust=getattr(text_el, "hjust", None),
199
+ vjust=getattr(text_el, "vjust", None),
200
+ angle=getattr(text_el, "angle", None),
201
+ lineheight=getattr(text_el, "lineheight", None),
202
+ margin=text_margin,
203
+ )
204
+
205
+ elements["key_height"] = Unit(0, "cm")
206
+ elements["key_width"] = Unit(0, "cm")
207
+ return elements
208
+
209
+ @staticmethod
210
+ def build_labels(
211
+ key: Any, elements: Dict[str, Any], params: Dict[str, Any]
212
+ ) -> List[Any]:
213
+ """Build one coloured text grob per key row.
214
+
215
+ Port of ``guide_stringlegend.R:75-95`` -- the core override. When there
216
+ are no labels, returns one :func:`grid_py.null_grob` per key row.
217
+ Otherwise the per-row colour is ``key$colour`` falling back (whole-column)
218
+ to ``key$fill``, and each label is drawn via :func:`element_grob` on the
219
+ resolved text element with ``margin_x``/``margin_y`` so the
220
+ ``legend.text`` margin (set in :meth:`setup_elements`) drives layout.
221
+ ``family``/``fontface`` columns are passed per-row when present, else
222
+ ``None`` so the text element's defaults apply.
223
+
224
+ Parameters
225
+ ----------
226
+ key : pandas.DataFrame
227
+ The guide key (``.label``, ``colour``/``fill``, optionally
228
+ ``family``/``fontface``).
229
+ elements : dict
230
+ Resolved guide elements (``text`` carries the injected margin).
231
+ params : dict
232
+
233
+ Returns
234
+ -------
235
+ list of grob
236
+ One grob per key row, coloured by the colour/fill aesthetic.
237
+ """
238
+ n_key = len(key) if key is not None else 0
239
+ labels = _column(key, ".label")
240
+ n_labels = 0 if labels is None else len(labels)
241
+ if n_labels < 1:
242
+ return [null_grob() for _ in range(n_key)]
243
+
244
+ # colour <- key$colour %||% key$fill (whole-column coalesce).
245
+ colour = _column(key, "colour")
246
+ if colour is None:
247
+ colour = _column(key, "fill")
248
+ family = _column(key, "family")
249
+ fontface = _column(key, "fontface")
250
+ text_el = elements.get("text")
251
+
252
+ out: List[Any] = []
253
+ for i in range(n_labels):
254
+ out.append(
255
+ element_grob(
256
+ text_el,
257
+ label=str(_at(labels, i)),
258
+ colour=_at(colour, i),
259
+ family=_at(family, i),
260
+ face=_at(fontface, i),
261
+ margin_x=True,
262
+ margin_y=True,
263
+ )
264
+ )
265
+ return out
266
+
267
+ @staticmethod
268
+ def build_decor(
269
+ decor: Any = None,
270
+ grobs: Any = None,
271
+ elements: Optional[Dict[str, Any]] = None,
272
+ params: Optional[Dict[str, Any]] = None,
273
+ **kwargs: Any,
274
+ ) -> Any:
275
+ """Return empty grobs (R ``build_decor``: no key swatches).
276
+
277
+ Port of ``guide_stringlegend.R:97`` -- ``function(...) zeroGrob()``. The
278
+ colour is shown in the label text instead of in a swatch, so the decor is
279
+ suppressed.
280
+
281
+ R returns a *single* ``zeroGrob`` because its downstream legend assembly
282
+ treats a single empty grob as "no decor". The ggplot2_py procedural
283
+ assembly (``_guide_legend.measure_legend_grobs``) instead iterates ``decor``
284
+ by index (``decor[i]._width``), so a scalar grob raises ``len(decor)``;
285
+ the slice-test confirmed this. We therefore return one
286
+ :func:`grid_py.null_grob` per key row to stay shape-compatible. Each grob
287
+ carries no ``_width``/``_height``, so combined with the zeroed
288
+ ``key_width``/``key_height`` units (see :meth:`setup_elements`) the key
289
+ cells collapse to zero -- the same visual result as R.
290
+
291
+ Returns
292
+ -------
293
+ list of grid_py.Grob
294
+ One :func:`grid_py.null_grob` per key row.
295
+ """
296
+ n = 0
297
+ if params is not None:
298
+ key = params.get("key")
299
+ n = params.get("n_breaks", len(key) if key is not None else 0)
300
+ return [null_grob() for _ in range(int(n))]
301
+
302
+
303
+ def guide_stringlegend(
304
+ title: Any = waiver(),
305
+ theme: Any = None,
306
+ position: Optional[str] = None,
307
+ direction: Optional[str] = None,
308
+ nrow: Optional[int] = None,
309
+ ncol: Optional[int] = None,
310
+ reverse: bool = False,
311
+ order: int = 0,
312
+ ) -> GuideStringlegend:
313
+ """Construct a string legend guide showing colour/fill mappings as text.
314
+
315
+ Port of R ``guide_stringlegend`` (``guide_stringlegend.R:22-44``). Builds a
316
+ :class:`GuideStringlegend` via :func:`ggplot2_py.guide.new_guide` with
317
+ ``available_aes = ["colour", "fill", "family", "fontface"]`` and
318
+ ``name = "stringlegend"``. It can be supplied to
319
+ :func:`ggplot2_py.guides` or as a scale's ``guide`` argument.
320
+
321
+ Parameters
322
+ ----------
323
+ title : str or Waiver, optional
324
+ Legend title. Defaults to the scale's name.
325
+ theme : Theme, optional
326
+ Guide-local theme overrides.
327
+ position : str, optional
328
+ Legend position.
329
+ direction : str, optional
330
+ ``"horizontal"`` or ``"vertical"``.
331
+ nrow, ncol : int, optional
332
+ Legend grid dimensions.
333
+ reverse : bool, default ``False``
334
+ Reverse the order of the legend keys.
335
+ order : int, default ``0``
336
+ Ordering relative to other guides.
337
+
338
+ Returns
339
+ -------
340
+ GuideStringlegend
341
+ """
342
+ return new_guide(
343
+ title=title,
344
+ theme=theme,
345
+ direction=direction,
346
+ nrow=nrow,
347
+ ncol=ncol,
348
+ reverse=reverse,
349
+ order=order,
350
+ position=position,
351
+ available_aes=["colour", "fill", "family", "fontface"],
352
+ name="stringlegend",
353
+ super=GuideStringlegend,
354
+ )