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/geom_text_aimed.py
ADDED
|
@@ -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
|
+
)
|