ggplot2-python 4.0.2.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.
- ggplot2_py/__init__.py +852 -0
- ggplot2_py/_compat.py +475 -0
- ggplot2_py/_plugins.py +129 -0
- ggplot2_py/_utils.py +544 -0
- ggplot2_py/aes.py +586 -0
- ggplot2_py/annotation.py +540 -0
- ggplot2_py/coord.py +2108 -0
- ggplot2_py/coords/__init__.py +49 -0
- ggplot2_py/datasets.py +265 -0
- ggplot2_py/draw_key.py +454 -0
- ggplot2_py/facet.py +1456 -0
- ggplot2_py/fortify.py +95 -0
- ggplot2_py/geom.py +4516 -0
- ggplot2_py/geoms/__init__.py +12 -0
- ggplot2_py/ggproto.py +279 -0
- ggplot2_py/guide.py +2925 -0
- ggplot2_py/guide_axis.py +615 -0
- ggplot2_py/guide_colourbar.py +657 -0
- ggplot2_py/guide_legend.py +1061 -0
- ggplot2_py/guides/__init__.py +8 -0
- ggplot2_py/labeller.py +296 -0
- ggplot2_py/labels.py +309 -0
- ggplot2_py/layer.py +954 -0
- ggplot2_py/layout.py +754 -0
- ggplot2_py/limits.py +314 -0
- ggplot2_py/plot.py +1401 -0
- ggplot2_py/plot_render.py +866 -0
- ggplot2_py/position.py +1269 -0
- ggplot2_py/protocols.py +171 -0
- ggplot2_py/py.typed +0 -0
- ggplot2_py/qplot.py +233 -0
- ggplot2_py/resources/diamonds.csv +53941 -0
- ggplot2_py/resources/economics.csv +575 -0
- ggplot2_py/resources/economics_long.csv +2871 -0
- ggplot2_py/resources/faithfuld.csv +5626 -0
- ggplot2_py/resources/luv_colours.csv +658 -0
- ggplot2_py/resources/midwest.csv +438 -0
- ggplot2_py/resources/mpg.csv +235 -0
- ggplot2_py/resources/msleep.csv +84 -0
- ggplot2_py/resources/presidential.csv +13 -0
- ggplot2_py/resources/seals.csv +1156 -0
- ggplot2_py/resources/txhousing.csv +8603 -0
- ggplot2_py/save.py +316 -0
- ggplot2_py/scale.py +2727 -0
- ggplot2_py/scales/__init__.py +4252 -0
- ggplot2_py/stat.py +6071 -0
- ggplot2_py/stats/__init__.py +9 -0
- ggplot2_py/theme.py +490 -0
- ggplot2_py/theme_defaults.py +1350 -0
- ggplot2_py/theme_elements.py +2052 -0
- ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
- ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
- ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
- ggplot2_python-4.0.2.9000.dist-info/licenses/LICENSE +3 -0
ggplot2_py/guide.py
ADDED
|
@@ -0,0 +1,2925 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Guide system for ggplot2_py.
|
|
3
|
+
|
|
4
|
+
Ports the R guide infrastructure (``guide-.R``, ``guide-legend.R``,
|
|
5
|
+
``guide-colorbar.R``, ``guide-axis.R``, ``guide-none.R``, ``guide-bins.R``,
|
|
6
|
+
``guide-colorsteps.R``, ``guide-custom.R``, ``guide-axis-logticks.R``,
|
|
7
|
+
``guide-axis-stack.R``, ``guide-axis-theta.R``, ``guide-old.R``, and
|
|
8
|
+
``guides-.R``) into a unified Python module.
|
|
9
|
+
|
|
10
|
+
The module defines:
|
|
11
|
+
|
|
12
|
+
* **Guide** -- base GGProto class for all guides.
|
|
13
|
+
* Concrete guide classes (``GuideLegend``, ``GuideColourbar``, etc.).
|
|
14
|
+
* Constructor functions (``guide_legend()``, ``guide_colourbar()``, etc.).
|
|
15
|
+
* The **Guides** container and the ``guides()`` helper.
|
|
16
|
+
* Legacy S3-style shims (``guide_train``, ``guide_merge``, etc.).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import warnings
|
|
23
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import pandas as pd
|
|
27
|
+
|
|
28
|
+
from ggplot2_py.ggproto import GGProto, ggproto, ggproto_parent, is_ggproto
|
|
29
|
+
from ggplot2_py._compat import (
|
|
30
|
+
Waiver,
|
|
31
|
+
cli_abort,
|
|
32
|
+
cli_warn,
|
|
33
|
+
is_waiver,
|
|
34
|
+
waiver,
|
|
35
|
+
)
|
|
36
|
+
from ggplot2_py._utils import compact, modify_list, snake_class
|
|
37
|
+
from ggplot2_py.aes import standardise_aes_names, rename_aes
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Classes
|
|
41
|
+
"Guide",
|
|
42
|
+
"GuideAxis",
|
|
43
|
+
"GuideAxisLogticks",
|
|
44
|
+
"GuideAxisStack",
|
|
45
|
+
"GuideAxisTheta",
|
|
46
|
+
"GuideBins",
|
|
47
|
+
"GuideColourbar",
|
|
48
|
+
"GuideColoursteps",
|
|
49
|
+
"GuideCustom",
|
|
50
|
+
"GuideLegend",
|
|
51
|
+
"GuideNone",
|
|
52
|
+
"GuideOld",
|
|
53
|
+
# Constructors
|
|
54
|
+
"guide_axis",
|
|
55
|
+
"guide_axis_logticks",
|
|
56
|
+
"guide_axis_stack",
|
|
57
|
+
"guide_axis_theta",
|
|
58
|
+
"guide_bins",
|
|
59
|
+
"guide_colourbar",
|
|
60
|
+
"guide_colorbar",
|
|
61
|
+
"guide_coloursteps",
|
|
62
|
+
"guide_colorsteps",
|
|
63
|
+
"guide_custom",
|
|
64
|
+
"guide_legend",
|
|
65
|
+
"guide_none",
|
|
66
|
+
# Guides container
|
|
67
|
+
"guides",
|
|
68
|
+
"Guides",
|
|
69
|
+
# Helpers
|
|
70
|
+
"new_guide",
|
|
71
|
+
"old_guide",
|
|
72
|
+
"guide_gengrob",
|
|
73
|
+
"guide_geom",
|
|
74
|
+
"guide_merge",
|
|
75
|
+
"guide_train",
|
|
76
|
+
"guide_transform",
|
|
77
|
+
"is_guide",
|
|
78
|
+
"is_guides",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Positional constants
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
_TRBL: List[str] = ["top", "right", "bottom", "left"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Utility helpers
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _hash_object(obj: Any) -> str:
|
|
94
|
+
"""Return a deterministic hash string for *obj*.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
obj : Any
|
|
99
|
+
The object to hash. Converted to ``repr`` then hashed with MD5.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
str
|
|
104
|
+
Hexadecimal hash digest.
|
|
105
|
+
"""
|
|
106
|
+
return hashlib.md5(repr(obj).encode("utf-8")).hexdigest()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _defaults(target: dict, defaults: dict) -> dict:
|
|
110
|
+
"""Return a new dict with *defaults* filled in for missing keys.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
target : dict
|
|
115
|
+
Primary values.
|
|
116
|
+
defaults : dict
|
|
117
|
+
Fall-back values.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict
|
|
122
|
+
Merged dictionary.
|
|
123
|
+
"""
|
|
124
|
+
out = dict(defaults)
|
|
125
|
+
out.update(target)
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _validate_guide(guide: Any) -> Any:
|
|
130
|
+
"""Ensure *guide* is a Guide class/instance.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
guide : str or Guide
|
|
135
|
+
Either a guide shorthand name (e.g. ``"legend"``) or a Guide
|
|
136
|
+
class / instance.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
Guide
|
|
141
|
+
A validated guide object.
|
|
142
|
+
|
|
143
|
+
Raises
|
|
144
|
+
------
|
|
145
|
+
ValueError
|
|
146
|
+
If *guide* cannot be resolved.
|
|
147
|
+
"""
|
|
148
|
+
if isinstance(guide, str):
|
|
149
|
+
guide = _resolve_guide_name(guide)
|
|
150
|
+
if isinstance(guide, type) and issubclass(guide, GGProto):
|
|
151
|
+
# It is a class; instantiate with default params
|
|
152
|
+
return guide()
|
|
153
|
+
if isinstance(guide, GGProto):
|
|
154
|
+
return guide
|
|
155
|
+
cli_abort(f"Cannot resolve guide: {guide!r}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _resolve_guide_name(name: str) -> type:
|
|
159
|
+
"""Map a short string name to a Guide class.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
name : str
|
|
164
|
+
Short name, e.g. ``"legend"``, ``"colourbar"``, ``"none"``.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
type
|
|
169
|
+
The corresponding Guide class.
|
|
170
|
+
"""
|
|
171
|
+
_REGISTRY: Dict[str, type] = {
|
|
172
|
+
"none": GuideNone,
|
|
173
|
+
"legend": GuideLegend,
|
|
174
|
+
"colourbar": GuideColourbar,
|
|
175
|
+
"colorbar": GuideColourbar,
|
|
176
|
+
"coloursteps": GuideColoursteps,
|
|
177
|
+
"colorsteps": GuideColoursteps,
|
|
178
|
+
"bins": GuideBins,
|
|
179
|
+
"axis": GuideAxis,
|
|
180
|
+
"axis_logticks": GuideAxisLogticks,
|
|
181
|
+
"axis_theta": GuideAxisTheta,
|
|
182
|
+
"axis_stack": GuideAxisStack,
|
|
183
|
+
"custom": GuideCustom,
|
|
184
|
+
}
|
|
185
|
+
key = name.lower().replace("-", "_")
|
|
186
|
+
cls = _REGISTRY.get(key)
|
|
187
|
+
if cls is None:
|
|
188
|
+
cli_abort(f"Unknown guide type: {name!r}")
|
|
189
|
+
return cls
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ============================================================================
|
|
193
|
+
# Guide base class
|
|
194
|
+
# ============================================================================
|
|
195
|
+
|
|
196
|
+
class Guide(GGProto):
|
|
197
|
+
"""Base class for all ggplot2 guides.
|
|
198
|
+
|
|
199
|
+
A ``Guide`` is responsible for rendering the visual representation of a
|
|
200
|
+
scale -- axis tick marks, legends, colour bars, and so on.
|
|
201
|
+
|
|
202
|
+
Attributes
|
|
203
|
+
----------
|
|
204
|
+
params : dict
|
|
205
|
+
Default parameters. Subclasses extend this dict.
|
|
206
|
+
elements : dict
|
|
207
|
+
Theme element names used by this guide.
|
|
208
|
+
hashables : list[str]
|
|
209
|
+
Parameter keys used to compute a deduplication hash.
|
|
210
|
+
available_aes : list[str]
|
|
211
|
+
Aesthetics that this guide can represent.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
_class_name: str = "Guide"
|
|
215
|
+
|
|
216
|
+
# -- Fields --------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
params: Dict[str, Any] = {
|
|
219
|
+
"title": waiver(),
|
|
220
|
+
"theme": None,
|
|
221
|
+
"name": "",
|
|
222
|
+
"position": waiver(),
|
|
223
|
+
"direction": None,
|
|
224
|
+
"order": 0,
|
|
225
|
+
"hash": "",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
available_aes: List[str] = []
|
|
229
|
+
|
|
230
|
+
elements: Dict[str, str] = {}
|
|
231
|
+
|
|
232
|
+
hashables: List[str] = ["title", "name"]
|
|
233
|
+
|
|
234
|
+
# -- Key extraction ------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def extract_key(
|
|
238
|
+
scale: Any,
|
|
239
|
+
aesthetic: str,
|
|
240
|
+
**kwargs: Any,
|
|
241
|
+
) -> Optional[pd.DataFrame]:
|
|
242
|
+
"""Extract key (break positions / labels) from a scale.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
scale : Scale
|
|
247
|
+
The scale from which to extract breaks.
|
|
248
|
+
aesthetic : str
|
|
249
|
+
Name of the aesthetic this guide represents.
|
|
250
|
+
**kwargs : Any
|
|
251
|
+
Additional arguments forwarded by subclasses.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
pd.DataFrame or None
|
|
256
|
+
A DataFrame with columns for the aesthetic, ``.value``, and
|
|
257
|
+
``.label``; or ``None`` if the scale has no breaks.
|
|
258
|
+
"""
|
|
259
|
+
breaks = getattr(scale, "get_breaks", lambda: None)()
|
|
260
|
+
if breaks is None:
|
|
261
|
+
return None
|
|
262
|
+
mapped = getattr(scale, "map", lambda x: x)(breaks)
|
|
263
|
+
labels = getattr(scale, "get_labels", lambda x: x)(breaks)
|
|
264
|
+
|
|
265
|
+
key = pd.DataFrame({
|
|
266
|
+
aesthetic: mapped,
|
|
267
|
+
".value": breaks,
|
|
268
|
+
".label": labels if labels is not None else [str(b) for b in breaks],
|
|
269
|
+
})
|
|
270
|
+
return key
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def extract_decor(
|
|
274
|
+
scale: Any,
|
|
275
|
+
aesthetic: str,
|
|
276
|
+
**kwargs: Any,
|
|
277
|
+
) -> Optional[pd.DataFrame]:
|
|
278
|
+
"""Extract decoration data from a scale.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
scale : Scale
|
|
283
|
+
The scale.
|
|
284
|
+
aesthetic : str
|
|
285
|
+
Aesthetic name.
|
|
286
|
+
**kwargs : Any
|
|
287
|
+
Extra arguments.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
pd.DataFrame or None
|
|
292
|
+
Decoration data or ``None``.
|
|
293
|
+
"""
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def extract_params(
|
|
298
|
+
scale: Any,
|
|
299
|
+
params: Dict[str, Any],
|
|
300
|
+
**kwargs: Any,
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""Post-process guide parameters after extraction.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
scale : Scale
|
|
307
|
+
The source scale.
|
|
308
|
+
params : dict
|
|
309
|
+
Current guide parameters.
|
|
310
|
+
**kwargs : Any
|
|
311
|
+
Additional arguments.
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
dict
|
|
316
|
+
Possibly-modified parameters.
|
|
317
|
+
"""
|
|
318
|
+
title = kwargs.get("title", waiver())
|
|
319
|
+
scale_name = getattr(scale, "name", None)
|
|
320
|
+
if is_waiver(params.get("title")):
|
|
321
|
+
if not is_waiver(title):
|
|
322
|
+
params["title"] = title
|
|
323
|
+
elif scale_name is not None:
|
|
324
|
+
params["title"] = scale_name
|
|
325
|
+
return params
|
|
326
|
+
|
|
327
|
+
# -- Training / transform ------------------------------------------------
|
|
328
|
+
|
|
329
|
+
def train(
|
|
330
|
+
self,
|
|
331
|
+
params: Optional[Dict[str, Any]] = None,
|
|
332
|
+
scale: Any = None,
|
|
333
|
+
aesthetic: Optional[str] = None,
|
|
334
|
+
**kwargs: Any,
|
|
335
|
+
) -> Optional[Dict[str, Any]]:
|
|
336
|
+
"""Train the guide on a scale.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
params : dict, optional
|
|
341
|
+
Guide parameters.
|
|
342
|
+
scale : Scale, optional
|
|
343
|
+
The scale to train on.
|
|
344
|
+
aesthetic : str, optional
|
|
345
|
+
Aesthetic name.
|
|
346
|
+
**kwargs : Any
|
|
347
|
+
Extra arguments (e.g. ``title``).
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
dict or None
|
|
352
|
+
Updated parameters, or ``None`` to drop this guide.
|
|
353
|
+
"""
|
|
354
|
+
if params is None:
|
|
355
|
+
params = dict(self.params)
|
|
356
|
+
if scale is None:
|
|
357
|
+
return params
|
|
358
|
+
|
|
359
|
+
params["aesthetic"] = aesthetic or ""
|
|
360
|
+
|
|
361
|
+
# Extract key — mirrors R's inject(self$extract_key(scale, !!!params))
|
|
362
|
+
safe = {k: v for k, v in params.items() if k not in ("key", "decor")}
|
|
363
|
+
try:
|
|
364
|
+
key = self.extract_key(scale, **safe)
|
|
365
|
+
except TypeError:
|
|
366
|
+
# Fallback: pass only aesthetic
|
|
367
|
+
key = self.extract_key(scale, aesthetic=aesthetic)
|
|
368
|
+
if key is not None and hasattr(key, "empty") and key.empty:
|
|
369
|
+
return None
|
|
370
|
+
params["key"] = key
|
|
371
|
+
|
|
372
|
+
# Extract decor
|
|
373
|
+
try:
|
|
374
|
+
params["decor"] = self.extract_decor(scale, aesthetic=aesthetic)
|
|
375
|
+
except Exception:
|
|
376
|
+
params["decor"] = None
|
|
377
|
+
|
|
378
|
+
# Post-process
|
|
379
|
+
params = self.extract_params(scale, params)
|
|
380
|
+
|
|
381
|
+
# Compute hash
|
|
382
|
+
hash_vals = []
|
|
383
|
+
for h in self.hashables:
|
|
384
|
+
if h in params:
|
|
385
|
+
hash_vals.append(params[h])
|
|
386
|
+
elif isinstance(params.get("key"), pd.DataFrame) and h.startswith("key."):
|
|
387
|
+
col = h.split(".", 1)[1]
|
|
388
|
+
if col in params["key"].columns:
|
|
389
|
+
hash_vals.append(list(params["key"][col]))
|
|
390
|
+
params["hash"] = _hash_object(hash_vals)
|
|
391
|
+
|
|
392
|
+
return params
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def transform(
|
|
396
|
+
params: Dict[str, Any],
|
|
397
|
+
coord: Any,
|
|
398
|
+
panel_params: Any,
|
|
399
|
+
) -> Dict[str, Any]:
|
|
400
|
+
"""Transform guide data through coordinate system.
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
params : dict
|
|
405
|
+
Guide parameters including ``key`` and ``decor``.
|
|
406
|
+
coord : Coord
|
|
407
|
+
Coordinate system.
|
|
408
|
+
panel_params : object
|
|
409
|
+
Panel parameters from the coordinate system.
|
|
410
|
+
|
|
411
|
+
Returns
|
|
412
|
+
-------
|
|
413
|
+
dict
|
|
414
|
+
Parameters with transformed ``key`` / ``decor``.
|
|
415
|
+
"""
|
|
416
|
+
key = params.get("key")
|
|
417
|
+
if key is not None and hasattr(coord, "transform") and not key.empty:
|
|
418
|
+
params["key"] = coord.transform(key, panel_params)
|
|
419
|
+
return params
|
|
420
|
+
|
|
421
|
+
def get_layer_key(
|
|
422
|
+
self,
|
|
423
|
+
params: Dict[str, Any],
|
|
424
|
+
layers: List[Any],
|
|
425
|
+
data: Optional[List[Any]] = None,
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
"""Map layer key information into the guide parameters.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
params : dict
|
|
432
|
+
Guide parameters.
|
|
433
|
+
layers : list
|
|
434
|
+
Plot layers.
|
|
435
|
+
data : list, optional
|
|
436
|
+
Layer data.
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
dict
|
|
441
|
+
Updated parameters.
|
|
442
|
+
"""
|
|
443
|
+
return params
|
|
444
|
+
|
|
445
|
+
def process_layers(
|
|
446
|
+
self,
|
|
447
|
+
params: Dict[str, Any],
|
|
448
|
+
layers: List[Any],
|
|
449
|
+
data: Optional[List[Any]] = None,
|
|
450
|
+
theme: Any = None,
|
|
451
|
+
) -> Optional[Dict[str, Any]]:
|
|
452
|
+
"""Process layer information to generate geom info.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
params : dict
|
|
457
|
+
Guide parameters.
|
|
458
|
+
layers : list
|
|
459
|
+
Plot layers.
|
|
460
|
+
data : list, optional
|
|
461
|
+
Layer data.
|
|
462
|
+
theme : Theme, optional
|
|
463
|
+
Plot theme.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
dict or None
|
|
468
|
+
Updated parameters or ``None`` if guide should be dropped.
|
|
469
|
+
"""
|
|
470
|
+
return self.get_layer_key(params, layers, data)
|
|
471
|
+
|
|
472
|
+
# -- Setup / override ----------------------------------------------------
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def setup_params(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
476
|
+
"""Validate and set up parameters before drawing.
|
|
477
|
+
|
|
478
|
+
Parameters
|
|
479
|
+
----------
|
|
480
|
+
params : dict
|
|
481
|
+
Guide parameters.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
dict
|
|
486
|
+
Validated parameters.
|
|
487
|
+
"""
|
|
488
|
+
return params
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def override_elements(
|
|
492
|
+
params: Dict[str, Any],
|
|
493
|
+
elements: Dict[str, Any],
|
|
494
|
+
theme: Any,
|
|
495
|
+
) -> Dict[str, Any]:
|
|
496
|
+
"""Resolve theme elements for this guide.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
params : dict
|
|
501
|
+
Guide parameters.
|
|
502
|
+
elements : dict
|
|
503
|
+
Element name -> theme element name mapping.
|
|
504
|
+
theme : Theme
|
|
505
|
+
The plot theme.
|
|
506
|
+
|
|
507
|
+
Returns
|
|
508
|
+
-------
|
|
509
|
+
dict
|
|
510
|
+
Resolved element objects.
|
|
511
|
+
"""
|
|
512
|
+
return elements
|
|
513
|
+
|
|
514
|
+
def setup_elements(
|
|
515
|
+
self,
|
|
516
|
+
params: Dict[str, Any],
|
|
517
|
+
elements: Optional[Dict[str, str]] = None,
|
|
518
|
+
theme: Any = None,
|
|
519
|
+
) -> Dict[str, Any]:
|
|
520
|
+
"""Set up theme elements used by this guide.
|
|
521
|
+
|
|
522
|
+
Parameters
|
|
523
|
+
----------
|
|
524
|
+
params : dict
|
|
525
|
+
Guide parameters.
|
|
526
|
+
elements : dict, optional
|
|
527
|
+
Element specifications. Falls back to ``self.elements``.
|
|
528
|
+
theme : Theme, optional
|
|
529
|
+
Plot theme.
|
|
530
|
+
|
|
531
|
+
Returns
|
|
532
|
+
-------
|
|
533
|
+
dict
|
|
534
|
+
Resolved elements.
|
|
535
|
+
"""
|
|
536
|
+
if elements is None:
|
|
537
|
+
elements = dict(self.elements)
|
|
538
|
+
return self.override_elements(params, elements, theme)
|
|
539
|
+
|
|
540
|
+
# -- Build methods -------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def build_title(
|
|
544
|
+
label: Any,
|
|
545
|
+
elements: Dict[str, Any],
|
|
546
|
+
params: Dict[str, Any],
|
|
547
|
+
) -> Any:
|
|
548
|
+
"""Build the guide title grob.
|
|
549
|
+
|
|
550
|
+
Parameters
|
|
551
|
+
----------
|
|
552
|
+
label : str or None
|
|
553
|
+
Title text.
|
|
554
|
+
elements : dict
|
|
555
|
+
Resolved theme elements.
|
|
556
|
+
params : dict
|
|
557
|
+
Guide parameters.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
grob
|
|
562
|
+
A title grob or ``None``.
|
|
563
|
+
"""
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
@staticmethod
|
|
567
|
+
def build_labels(
|
|
568
|
+
key: pd.DataFrame,
|
|
569
|
+
elements: Dict[str, Any],
|
|
570
|
+
params: Dict[str, Any],
|
|
571
|
+
) -> Any:
|
|
572
|
+
"""Build label grobs from the key.
|
|
573
|
+
|
|
574
|
+
Parameters
|
|
575
|
+
----------
|
|
576
|
+
key : pd.DataFrame
|
|
577
|
+
The guide key.
|
|
578
|
+
elements : dict
|
|
579
|
+
Resolved theme elements.
|
|
580
|
+
params : dict
|
|
581
|
+
Guide parameters.
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
grob or list of grobs
|
|
586
|
+
Label grobs.
|
|
587
|
+
"""
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
@staticmethod
|
|
591
|
+
def build_decor(
|
|
592
|
+
decor: Any,
|
|
593
|
+
grobs: Any,
|
|
594
|
+
elements: Dict[str, Any],
|
|
595
|
+
params: Dict[str, Any],
|
|
596
|
+
) -> Any:
|
|
597
|
+
"""Build decoration grobs.
|
|
598
|
+
|
|
599
|
+
Parameters
|
|
600
|
+
----------
|
|
601
|
+
decor : pd.DataFrame or None
|
|
602
|
+
Decoration data.
|
|
603
|
+
grobs : dict
|
|
604
|
+
Previously built grobs.
|
|
605
|
+
elements : dict
|
|
606
|
+
Resolved theme elements.
|
|
607
|
+
params : dict
|
|
608
|
+
Guide parameters.
|
|
609
|
+
|
|
610
|
+
Returns
|
|
611
|
+
-------
|
|
612
|
+
grob or list of grobs
|
|
613
|
+
Decoration grobs.
|
|
614
|
+
"""
|
|
615
|
+
return None
|
|
616
|
+
|
|
617
|
+
@staticmethod
|
|
618
|
+
def build_ticks(
|
|
619
|
+
key: pd.DataFrame,
|
|
620
|
+
elements: Dict[str, Any],
|
|
621
|
+
params: Dict[str, Any],
|
|
622
|
+
) -> Any:
|
|
623
|
+
"""Build tick mark grobs.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
key : pd.DataFrame
|
|
628
|
+
The guide key.
|
|
629
|
+
elements : dict
|
|
630
|
+
Resolved theme elements.
|
|
631
|
+
params : dict
|
|
632
|
+
Guide parameters.
|
|
633
|
+
|
|
634
|
+
Returns
|
|
635
|
+
-------
|
|
636
|
+
grob
|
|
637
|
+
Tick mark grobs.
|
|
638
|
+
"""
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
@staticmethod
|
|
642
|
+
def measure_grobs(
|
|
643
|
+
grobs: Dict[str, Any],
|
|
644
|
+
params: Dict[str, Any],
|
|
645
|
+
elements: Dict[str, Any],
|
|
646
|
+
) -> Dict[str, Any]:
|
|
647
|
+
"""Measure built grobs for layout.
|
|
648
|
+
|
|
649
|
+
Parameters
|
|
650
|
+
----------
|
|
651
|
+
grobs : dict
|
|
652
|
+
Named dictionary of grobs.
|
|
653
|
+
params : dict
|
|
654
|
+
Guide parameters.
|
|
655
|
+
elements : dict
|
|
656
|
+
Resolved elements.
|
|
657
|
+
|
|
658
|
+
Returns
|
|
659
|
+
-------
|
|
660
|
+
dict
|
|
661
|
+
Dictionary with ``width`` and ``height`` keys.
|
|
662
|
+
"""
|
|
663
|
+
return {"width": None, "height": None}
|
|
664
|
+
|
|
665
|
+
@staticmethod
|
|
666
|
+
def arrange_layout(
|
|
667
|
+
key: pd.DataFrame,
|
|
668
|
+
sizes: Dict[str, Any],
|
|
669
|
+
params: Dict[str, Any],
|
|
670
|
+
elements: Dict[str, Any],
|
|
671
|
+
) -> Dict[str, Any]:
|
|
672
|
+
"""Compute the layout specification.
|
|
673
|
+
|
|
674
|
+
Parameters
|
|
675
|
+
----------
|
|
676
|
+
key : pd.DataFrame
|
|
677
|
+
The guide key.
|
|
678
|
+
sizes : dict
|
|
679
|
+
Size measurements from :meth:`measure_grobs`.
|
|
680
|
+
params : dict
|
|
681
|
+
Guide parameters.
|
|
682
|
+
elements : dict
|
|
683
|
+
Resolved elements.
|
|
684
|
+
|
|
685
|
+
Returns
|
|
686
|
+
-------
|
|
687
|
+
dict
|
|
688
|
+
Layout specification.
|
|
689
|
+
"""
|
|
690
|
+
return {}
|
|
691
|
+
|
|
692
|
+
@staticmethod
|
|
693
|
+
def assemble_drawing(
|
|
694
|
+
grobs: Dict[str, Any],
|
|
695
|
+
layout: Dict[str, Any],
|
|
696
|
+
sizes: Dict[str, Any],
|
|
697
|
+
params: Dict[str, Any],
|
|
698
|
+
elements: Dict[str, Any],
|
|
699
|
+
) -> Any:
|
|
700
|
+
"""Assemble the final guide drawing (gtable).
|
|
701
|
+
|
|
702
|
+
Parameters
|
|
703
|
+
----------
|
|
704
|
+
grobs : dict
|
|
705
|
+
Named grobs.
|
|
706
|
+
layout : dict
|
|
707
|
+
Layout specification.
|
|
708
|
+
sizes : dict
|
|
709
|
+
Size measurements.
|
|
710
|
+
params : dict
|
|
711
|
+
Guide parameters.
|
|
712
|
+
elements : dict
|
|
713
|
+
Resolved elements.
|
|
714
|
+
|
|
715
|
+
Returns
|
|
716
|
+
-------
|
|
717
|
+
gtable or grob
|
|
718
|
+
The final assembled guide graphic.
|
|
719
|
+
"""
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
# -- Merge ---------------------------------------------------------------
|
|
723
|
+
|
|
724
|
+
def merge(
|
|
725
|
+
self,
|
|
726
|
+
params: Dict[str, Any],
|
|
727
|
+
new_guide: "Guide",
|
|
728
|
+
new_params: Dict[str, Any],
|
|
729
|
+
) -> Dict[str, Any]:
|
|
730
|
+
"""Merge another guide into this one.
|
|
731
|
+
|
|
732
|
+
Parameters
|
|
733
|
+
----------
|
|
734
|
+
params : dict
|
|
735
|
+
This guide's parameters.
|
|
736
|
+
new_guide : Guide
|
|
737
|
+
The other guide.
|
|
738
|
+
new_params : dict
|
|
739
|
+
The other guide's parameters.
|
|
740
|
+
|
|
741
|
+
Returns
|
|
742
|
+
-------
|
|
743
|
+
dict
|
|
744
|
+
A dict with keys ``guide`` and ``params`` representing the
|
|
745
|
+
merged result.
|
|
746
|
+
"""
|
|
747
|
+
new_key = new_params.get("key")
|
|
748
|
+
if new_key is not None and isinstance(new_key, pd.DataFrame):
|
|
749
|
+
key = params.get("key")
|
|
750
|
+
if key is not None and isinstance(key, pd.DataFrame):
|
|
751
|
+
# Merge keys by joining on shared columns
|
|
752
|
+
common = [c for c in key.columns if c in new_key.columns
|
|
753
|
+
and c.startswith(".")]
|
|
754
|
+
if common:
|
|
755
|
+
new_cols = [c for c in new_key.columns if c not in common]
|
|
756
|
+
if new_cols:
|
|
757
|
+
params["key"] = pd.merge(
|
|
758
|
+
key, new_key[common + new_cols],
|
|
759
|
+
on=common, how="left",
|
|
760
|
+
)
|
|
761
|
+
else:
|
|
762
|
+
# Just add new aesthetic columns
|
|
763
|
+
for col in new_key.columns:
|
|
764
|
+
if col not in key.columns:
|
|
765
|
+
params["key"][col] = new_key[col].values
|
|
766
|
+
return {"guide": self, "params": params}
|
|
767
|
+
|
|
768
|
+
# -- Draw ----------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
def draw(
|
|
771
|
+
self,
|
|
772
|
+
theme: Any = None,
|
|
773
|
+
position: Optional[str] = None,
|
|
774
|
+
direction: Optional[str] = None,
|
|
775
|
+
params: Optional[Dict[str, Any]] = None,
|
|
776
|
+
) -> Any:
|
|
777
|
+
"""Draw the guide.
|
|
778
|
+
|
|
779
|
+
Parameters
|
|
780
|
+
----------
|
|
781
|
+
theme : Theme, optional
|
|
782
|
+
Plot theme.
|
|
783
|
+
position : str, optional
|
|
784
|
+
Position (``"top"``, ``"right"``, ``"bottom"``, ``"left"``,
|
|
785
|
+
``"inside"``).
|
|
786
|
+
direction : str, optional
|
|
787
|
+
``"horizontal"`` or ``"vertical"``.
|
|
788
|
+
params : dict, optional
|
|
789
|
+
Guide parameters. Defaults to ``self.params``.
|
|
790
|
+
|
|
791
|
+
Returns
|
|
792
|
+
-------
|
|
793
|
+
grob or gtable
|
|
794
|
+
The rendered guide.
|
|
795
|
+
"""
|
|
796
|
+
if params is None:
|
|
797
|
+
params = dict(self.params)
|
|
798
|
+
|
|
799
|
+
# Update position/direction if provided
|
|
800
|
+
if position is not None:
|
|
801
|
+
params["position"] = position
|
|
802
|
+
if direction is not None:
|
|
803
|
+
params["direction"] = direction
|
|
804
|
+
|
|
805
|
+
params = self.setup_params(params)
|
|
806
|
+
elems = self.setup_elements(params, dict(self.elements), theme)
|
|
807
|
+
|
|
808
|
+
# Build components
|
|
809
|
+
key = params.get("key")
|
|
810
|
+
if key is None:
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
grobs: Dict[str, Any] = {}
|
|
814
|
+
title = params.get("title")
|
|
815
|
+
if not is_waiver(title) and title is not None:
|
|
816
|
+
grobs["title"] = self.build_title(title, elems, params)
|
|
817
|
+
|
|
818
|
+
grobs["labels"] = self.build_labels(key, elems, params)
|
|
819
|
+
grobs["ticks"] = self.build_ticks(key, elems, params)
|
|
820
|
+
|
|
821
|
+
decor = params.get("decor")
|
|
822
|
+
grobs["decor"] = self.build_decor(decor, grobs, elems, params)
|
|
823
|
+
|
|
824
|
+
sizes = self.measure_grobs(grobs, params, elems)
|
|
825
|
+
layout = self.arrange_layout(key, sizes, params, elems)
|
|
826
|
+
|
|
827
|
+
return self.assemble_drawing(grobs, layout, sizes, params, elems)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# ============================================================================
|
|
831
|
+
# GuideNone -- suppresses the guide
|
|
832
|
+
# ============================================================================
|
|
833
|
+
|
|
834
|
+
class GuideNone(Guide):
|
|
835
|
+
"""A guide that draws nothing.
|
|
836
|
+
|
|
837
|
+
Attributes
|
|
838
|
+
----------
|
|
839
|
+
_class_name : str
|
|
840
|
+
``"GuideNone"``.
|
|
841
|
+
"""
|
|
842
|
+
|
|
843
|
+
_class_name: str = "GuideNone"
|
|
844
|
+
|
|
845
|
+
params: Dict[str, Any] = {
|
|
846
|
+
"title": waiver(),
|
|
847
|
+
"theme": None,
|
|
848
|
+
"name": "none",
|
|
849
|
+
"position": waiver(),
|
|
850
|
+
"direction": None,
|
|
851
|
+
"order": 0,
|
|
852
|
+
"hash": "",
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
available_aes: List[str] = ["any"]
|
|
856
|
+
|
|
857
|
+
def train(
|
|
858
|
+
self,
|
|
859
|
+
params: Optional[Dict[str, Any]] = None,
|
|
860
|
+
scale: Any = None,
|
|
861
|
+
aesthetic: Optional[str] = None,
|
|
862
|
+
**kwargs: Any,
|
|
863
|
+
) -> Dict[str, Any]:
|
|
864
|
+
"""Perform no training.
|
|
865
|
+
|
|
866
|
+
Returns
|
|
867
|
+
-------
|
|
868
|
+
dict
|
|
869
|
+
The unmodified parameters.
|
|
870
|
+
"""
|
|
871
|
+
return params if params is not None else dict(self.params)
|
|
872
|
+
|
|
873
|
+
@staticmethod
|
|
874
|
+
def transform(params: Dict[str, Any], coord: Any = None, **kwargs: Any) -> Dict[str, Any]:
|
|
875
|
+
"""Pass through without transformation.
|
|
876
|
+
|
|
877
|
+
Returns
|
|
878
|
+
-------
|
|
879
|
+
dict
|
|
880
|
+
Unmodified parameters.
|
|
881
|
+
"""
|
|
882
|
+
return params
|
|
883
|
+
|
|
884
|
+
def draw(self, **kwargs: Any) -> None:
|
|
885
|
+
"""Draw nothing.
|
|
886
|
+
|
|
887
|
+
Returns
|
|
888
|
+
-------
|
|
889
|
+
None
|
|
890
|
+
"""
|
|
891
|
+
return None
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
# ============================================================================
|
|
895
|
+
# GuideAxis -- position axis guide
|
|
896
|
+
# ============================================================================
|
|
897
|
+
|
|
898
|
+
class GuideAxis(Guide):
|
|
899
|
+
"""Guide for position axes (x / y).
|
|
900
|
+
|
|
901
|
+
Renders tick marks, labels, and axis lines for position scales.
|
|
902
|
+
|
|
903
|
+
Attributes
|
|
904
|
+
----------
|
|
905
|
+
_class_name : str
|
|
906
|
+
``"GuideAxis"``.
|
|
907
|
+
"""
|
|
908
|
+
|
|
909
|
+
_class_name: str = "GuideAxis"
|
|
910
|
+
|
|
911
|
+
params: Dict[str, Any] = {
|
|
912
|
+
"title": waiver(),
|
|
913
|
+
"theme": None,
|
|
914
|
+
"name": "axis",
|
|
915
|
+
"hash": "",
|
|
916
|
+
"position": waiver(),
|
|
917
|
+
"direction": None,
|
|
918
|
+
"angle": None,
|
|
919
|
+
"n.dodge": 1,
|
|
920
|
+
"minor.ticks": False,
|
|
921
|
+
"cap": "none",
|
|
922
|
+
"order": 0,
|
|
923
|
+
"check.overlap": False,
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
available_aes: List[str] = ["x", "y"]
|
|
927
|
+
|
|
928
|
+
hashables: List[str] = ["title", "name"]
|
|
929
|
+
|
|
930
|
+
elements: Dict[str, str] = {
|
|
931
|
+
"line": "axis.line",
|
|
932
|
+
"text": "axis.text",
|
|
933
|
+
"ticks": "axis.ticks",
|
|
934
|
+
"minor": "axis.minor.ticks",
|
|
935
|
+
"major_length": "axis.ticks.length",
|
|
936
|
+
"minor_length": "axis.minor.ticks.length",
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
@staticmethod
|
|
940
|
+
def extract_key(
|
|
941
|
+
scale: Any,
|
|
942
|
+
aesthetic: str,
|
|
943
|
+
minor_ticks: bool = False,
|
|
944
|
+
**kwargs: Any,
|
|
945
|
+
) -> Optional[pd.DataFrame]:
|
|
946
|
+
"""Extract break positions for axis guide.
|
|
947
|
+
|
|
948
|
+
Parameters
|
|
949
|
+
----------
|
|
950
|
+
scale : Scale
|
|
951
|
+
Position scale.
|
|
952
|
+
aesthetic : str
|
|
953
|
+
``"x"`` or ``"y"``.
|
|
954
|
+
minor_ticks : bool
|
|
955
|
+
Whether to include minor tick positions.
|
|
956
|
+
**kwargs : Any
|
|
957
|
+
Extra arguments.
|
|
958
|
+
|
|
959
|
+
Returns
|
|
960
|
+
-------
|
|
961
|
+
pd.DataFrame or None
|
|
962
|
+
Key with break positions.
|
|
963
|
+
"""
|
|
964
|
+
major = Guide.extract_key(scale, aesthetic)
|
|
965
|
+
if major is None:
|
|
966
|
+
major = pd.DataFrame()
|
|
967
|
+
if not minor_ticks:
|
|
968
|
+
return major
|
|
969
|
+
|
|
970
|
+
minor_breaks = getattr(scale, "get_breaks_minor", lambda: [])()
|
|
971
|
+
if minor_breaks is None:
|
|
972
|
+
minor_breaks = []
|
|
973
|
+
if major is not None and not major.empty:
|
|
974
|
+
major_vals = set(major[".value"].tolist())
|
|
975
|
+
minor_breaks = [b for b in minor_breaks
|
|
976
|
+
if b not in major_vals and np.isfinite(b)]
|
|
977
|
+
else:
|
|
978
|
+
minor_breaks = [b for b in minor_breaks if np.isfinite(b)]
|
|
979
|
+
|
|
980
|
+
if not minor_breaks:
|
|
981
|
+
return major
|
|
982
|
+
|
|
983
|
+
mapped = getattr(scale, "map", lambda x: x)(minor_breaks)
|
|
984
|
+
minor = pd.DataFrame({
|
|
985
|
+
aesthetic: mapped,
|
|
986
|
+
".value": minor_breaks,
|
|
987
|
+
".type": ["minor"] * len(minor_breaks),
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
if major is not None and not major.empty:
|
|
991
|
+
major = major.copy()
|
|
992
|
+
major[".type"] = "major"
|
|
993
|
+
return pd.concat([major, minor], ignore_index=True)
|
|
994
|
+
return minor
|
|
995
|
+
|
|
996
|
+
@staticmethod
|
|
997
|
+
def extract_params(
|
|
998
|
+
scale: Any,
|
|
999
|
+
params: Dict[str, Any],
|
|
1000
|
+
**kwargs: Any,
|
|
1001
|
+
) -> Dict[str, Any]:
|
|
1002
|
+
"""Append aesthetic name to the guide name.
|
|
1003
|
+
|
|
1004
|
+
Parameters
|
|
1005
|
+
----------
|
|
1006
|
+
scale : Scale
|
|
1007
|
+
The position scale.
|
|
1008
|
+
params : dict
|
|
1009
|
+
Guide parameters.
|
|
1010
|
+
**kwargs : Any
|
|
1011
|
+
Extra arguments.
|
|
1012
|
+
|
|
1013
|
+
Returns
|
|
1014
|
+
-------
|
|
1015
|
+
dict
|
|
1016
|
+
Updated parameters.
|
|
1017
|
+
"""
|
|
1018
|
+
aes = params.get("aesthetic", "")
|
|
1019
|
+
params["name"] = f"{params.get('name', 'axis')}_{aes}"
|
|
1020
|
+
return params
|
|
1021
|
+
|
|
1022
|
+
@staticmethod
|
|
1023
|
+
def extract_decor(
|
|
1024
|
+
scale: Any,
|
|
1025
|
+
aesthetic: str,
|
|
1026
|
+
key: Optional[pd.DataFrame] = None,
|
|
1027
|
+
cap: str = "none",
|
|
1028
|
+
**kwargs: Any,
|
|
1029
|
+
) -> pd.DataFrame:
|
|
1030
|
+
"""Build axis line decoration data.
|
|
1031
|
+
|
|
1032
|
+
Parameters
|
|
1033
|
+
----------
|
|
1034
|
+
scale : Scale
|
|
1035
|
+
The position scale.
|
|
1036
|
+
aesthetic : str
|
|
1037
|
+
``"x"`` or ``"y"``.
|
|
1038
|
+
key : pd.DataFrame, optional
|
|
1039
|
+
The guide key.
|
|
1040
|
+
cap : str
|
|
1041
|
+
One of ``"none"``, ``"both"``, ``"upper"``, ``"lower"``.
|
|
1042
|
+
**kwargs : Any
|
|
1043
|
+
Extra arguments.
|
|
1044
|
+
|
|
1045
|
+
Returns
|
|
1046
|
+
-------
|
|
1047
|
+
pd.DataFrame
|
|
1048
|
+
Axis line positions.
|
|
1049
|
+
"""
|
|
1050
|
+
value = [-np.inf, np.inf]
|
|
1051
|
+
has_key = key is not None and not key.empty
|
|
1052
|
+
if cap in ("both", "upper") and has_key:
|
|
1053
|
+
value[1] = key[aesthetic].max()
|
|
1054
|
+
if cap in ("both", "lower") and has_key:
|
|
1055
|
+
value[0] = key[aesthetic].min()
|
|
1056
|
+
return pd.DataFrame({aesthetic: value})
|
|
1057
|
+
|
|
1058
|
+
@staticmethod
|
|
1059
|
+
def transform(
|
|
1060
|
+
params: Dict[str, Any],
|
|
1061
|
+
coord: Any,
|
|
1062
|
+
panel_params: Any,
|
|
1063
|
+
) -> Dict[str, Any]:
|
|
1064
|
+
"""Transform axis data through coordinate system.
|
|
1065
|
+
|
|
1066
|
+
Parameters
|
|
1067
|
+
----------
|
|
1068
|
+
params : dict
|
|
1069
|
+
Guide parameters.
|
|
1070
|
+
coord : Coord
|
|
1071
|
+
Coordinate system.
|
|
1072
|
+
panel_params : object
|
|
1073
|
+
Panel parameters.
|
|
1074
|
+
|
|
1075
|
+
Returns
|
|
1076
|
+
-------
|
|
1077
|
+
dict
|
|
1078
|
+
Transformed parameters.
|
|
1079
|
+
"""
|
|
1080
|
+
key = params.get("key")
|
|
1081
|
+
if key is not None and hasattr(coord, "transform") and not key.empty:
|
|
1082
|
+
aesthetic = params.get("aesthetic", "x")
|
|
1083
|
+
ortho = "y" if aesthetic == "x" else "x"
|
|
1084
|
+
position = params.get("position")
|
|
1085
|
+
if position in ("bottom", "left"):
|
|
1086
|
+
override = -np.inf
|
|
1087
|
+
else:
|
|
1088
|
+
override = np.inf
|
|
1089
|
+
|
|
1090
|
+
if not key.empty:
|
|
1091
|
+
if ortho not in key.columns:
|
|
1092
|
+
key = key.copy()
|
|
1093
|
+
key[ortho] = override
|
|
1094
|
+
params["key"] = coord.transform(key, panel_params)
|
|
1095
|
+
|
|
1096
|
+
decor = params.get("decor")
|
|
1097
|
+
if decor is not None and hasattr(coord, "transform"):
|
|
1098
|
+
aesthetic = params.get("aesthetic", "x")
|
|
1099
|
+
ortho = "y" if aesthetic == "x" else "x"
|
|
1100
|
+
if ortho not in decor.columns:
|
|
1101
|
+
decor = decor.copy()
|
|
1102
|
+
position = params.get("position")
|
|
1103
|
+
decor[ortho] = -np.inf if position in ("bottom", "left") else np.inf
|
|
1104
|
+
params["decor"] = coord.transform(decor, panel_params)
|
|
1105
|
+
|
|
1106
|
+
return params
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
# ============================================================================
|
|
1110
|
+
# GuideLegend -- legend for non-position aesthetics
|
|
1111
|
+
# ============================================================================
|
|
1112
|
+
|
|
1113
|
+
class GuideLegend(Guide):
|
|
1114
|
+
"""Legend guide for non-position aesthetics.
|
|
1115
|
+
|
|
1116
|
+
Shows keys (geoms) mapped onto discrete or discretised values.
|
|
1117
|
+
|
|
1118
|
+
Attributes
|
|
1119
|
+
----------
|
|
1120
|
+
_class_name : str
|
|
1121
|
+
``"GuideLegend"``.
|
|
1122
|
+
"""
|
|
1123
|
+
|
|
1124
|
+
_class_name: str = "GuideLegend"
|
|
1125
|
+
|
|
1126
|
+
params: Dict[str, Any] = {
|
|
1127
|
+
"title": waiver(),
|
|
1128
|
+
"theme": None,
|
|
1129
|
+
"override.aes": {},
|
|
1130
|
+
"nrow": None,
|
|
1131
|
+
"ncol": None,
|
|
1132
|
+
"reverse": False,
|
|
1133
|
+
"order": 0,
|
|
1134
|
+
"name": "legend",
|
|
1135
|
+
"hash": "",
|
|
1136
|
+
"position": None,
|
|
1137
|
+
"direction": None,
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
available_aes: List[str] = ["any"]
|
|
1141
|
+
|
|
1142
|
+
hashables: List[str] = ["title", "name"]
|
|
1143
|
+
|
|
1144
|
+
elements: Dict[str, str] = {
|
|
1145
|
+
"background": "legend.background",
|
|
1146
|
+
"margin": "legend.margin",
|
|
1147
|
+
"key": "legend.key",
|
|
1148
|
+
"key_height": "legend.key.height",
|
|
1149
|
+
"key_width": "legend.key.width",
|
|
1150
|
+
"key_just": "legend.key.justification",
|
|
1151
|
+
"text": "legend.text",
|
|
1152
|
+
"theme.title": "legend.title",
|
|
1153
|
+
"spacing_x": "legend.key.spacing.x",
|
|
1154
|
+
"spacing_y": "legend.key.spacing.y",
|
|
1155
|
+
"text_position": "legend.text.position",
|
|
1156
|
+
"title_position": "legend.title.position",
|
|
1157
|
+
"byrow": "legend.byrow",
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
@staticmethod
|
|
1161
|
+
def extract_params(
|
|
1162
|
+
scale: Any,
|
|
1163
|
+
params: Dict[str, Any],
|
|
1164
|
+
title: Any = None,
|
|
1165
|
+
**kwargs: Any,
|
|
1166
|
+
) -> Dict[str, Any]:
|
|
1167
|
+
"""Extract and validate legend parameters.
|
|
1168
|
+
|
|
1169
|
+
Parameters
|
|
1170
|
+
----------
|
|
1171
|
+
scale : Scale
|
|
1172
|
+
The mapped scale.
|
|
1173
|
+
params : dict
|
|
1174
|
+
Guide parameters.
|
|
1175
|
+
title : str or Waiver, optional
|
|
1176
|
+
Title override.
|
|
1177
|
+
**kwargs : Any
|
|
1178
|
+
Extra arguments.
|
|
1179
|
+
|
|
1180
|
+
Returns
|
|
1181
|
+
-------
|
|
1182
|
+
dict
|
|
1183
|
+
Updated parameters.
|
|
1184
|
+
"""
|
|
1185
|
+
if title is None:
|
|
1186
|
+
title = waiver()
|
|
1187
|
+
# Resolve title
|
|
1188
|
+
scale_name = getattr(scale, "name", None)
|
|
1189
|
+
if is_waiver(params.get("title")):
|
|
1190
|
+
if not is_waiver(title):
|
|
1191
|
+
params["title"] = title
|
|
1192
|
+
elif scale_name is not None:
|
|
1193
|
+
params["title"] = scale_name
|
|
1194
|
+
|
|
1195
|
+
# Reverse key order if requested
|
|
1196
|
+
if params.get("reverse", False):
|
|
1197
|
+
key = params.get("key")
|
|
1198
|
+
if key is not None and isinstance(key, pd.DataFrame) and not key.empty:
|
|
1199
|
+
params["key"] = key.iloc[::-1].reset_index(drop=True)
|
|
1200
|
+
return params
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
# ============================================================================
|
|
1204
|
+
# GuideColourbar -- continuous colour bar guide
|
|
1205
|
+
# ============================================================================
|
|
1206
|
+
|
|
1207
|
+
class GuideColourbar(GuideLegend):
|
|
1208
|
+
"""Continuous colour bar guide.
|
|
1209
|
+
|
|
1210
|
+
Shows a smooth colour gradient representing continuous colour/fill
|
|
1211
|
+
scales.
|
|
1212
|
+
|
|
1213
|
+
Attributes
|
|
1214
|
+
----------
|
|
1215
|
+
_class_name : str
|
|
1216
|
+
``"GuideColourbar"``.
|
|
1217
|
+
"""
|
|
1218
|
+
|
|
1219
|
+
_class_name: str = "GuideColourbar"
|
|
1220
|
+
|
|
1221
|
+
params: Dict[str, Any] = {
|
|
1222
|
+
"title": waiver(),
|
|
1223
|
+
"theme": None,
|
|
1224
|
+
"nbin": 300,
|
|
1225
|
+
"display": "raster",
|
|
1226
|
+
"alpha": float("nan"),
|
|
1227
|
+
"draw_lim": [True, True],
|
|
1228
|
+
"angle": None,
|
|
1229
|
+
"position": None,
|
|
1230
|
+
"direction": None,
|
|
1231
|
+
"reverse": False,
|
|
1232
|
+
"order": 0,
|
|
1233
|
+
"name": "colourbar",
|
|
1234
|
+
"hash": "",
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
available_aes: List[str] = ["colour", "color", "fill"]
|
|
1238
|
+
|
|
1239
|
+
hashables: List[str] = ["title", "name"]
|
|
1240
|
+
|
|
1241
|
+
elements: Dict[str, str] = {
|
|
1242
|
+
"background": "legend.background",
|
|
1243
|
+
"margin": "legend.margin",
|
|
1244
|
+
"key": "legend.key",
|
|
1245
|
+
"key_height": "legend.key.height",
|
|
1246
|
+
"key_width": "legend.key.width",
|
|
1247
|
+
"text": "legend.text",
|
|
1248
|
+
"theme.title": "legend.title",
|
|
1249
|
+
"ticks": "legend.ticks",
|
|
1250
|
+
"ticks_length": "legend.ticks.length",
|
|
1251
|
+
"frame": "legend.frame",
|
|
1252
|
+
"text_position": "legend.text.position",
|
|
1253
|
+
"title_position": "legend.title.position",
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
# ============================================================================
|
|
1258
|
+
# GuideColoursteps -- stepped colour bar guide
|
|
1259
|
+
# ============================================================================
|
|
1260
|
+
|
|
1261
|
+
class GuideColoursteps(GuideColourbar):
|
|
1262
|
+
"""Discretised (stepped) colour bar guide.
|
|
1263
|
+
|
|
1264
|
+
Displays areas between breaks as single constant colours instead of
|
|
1265
|
+
a smooth gradient.
|
|
1266
|
+
|
|
1267
|
+
Attributes
|
|
1268
|
+
----------
|
|
1269
|
+
_class_name : str
|
|
1270
|
+
``"GuideColoursteps"``.
|
|
1271
|
+
"""
|
|
1272
|
+
|
|
1273
|
+
_class_name: str = "GuideColoursteps"
|
|
1274
|
+
|
|
1275
|
+
params: Dict[str, Any] = {
|
|
1276
|
+
**GuideColourbar.params,
|
|
1277
|
+
"even.steps": True,
|
|
1278
|
+
"show.limits": None,
|
|
1279
|
+
"name": "coloursteps",
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
available_aes: List[str] = ["colour", "color", "fill"]
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
# ============================================================================
|
|
1286
|
+
# GuideBins -- binned legend guide
|
|
1287
|
+
# ============================================================================
|
|
1288
|
+
|
|
1289
|
+
class GuideBins(GuideLegend):
|
|
1290
|
+
"""Binned legend guide.
|
|
1291
|
+
|
|
1292
|
+
A version of the legend guide for binned scales. Places ticks between
|
|
1293
|
+
keys and optionally shows a small axis.
|
|
1294
|
+
|
|
1295
|
+
Attributes
|
|
1296
|
+
----------
|
|
1297
|
+
_class_name : str
|
|
1298
|
+
``"GuideBins"``.
|
|
1299
|
+
"""
|
|
1300
|
+
|
|
1301
|
+
_class_name: str = "GuideBins"
|
|
1302
|
+
|
|
1303
|
+
params: Dict[str, Any] = {
|
|
1304
|
+
**GuideLegend.params,
|
|
1305
|
+
"angle": None,
|
|
1306
|
+
"show.limits": None,
|
|
1307
|
+
"name": "bins",
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
available_aes: List[str] = ["any"]
|
|
1311
|
+
|
|
1312
|
+
elements: Dict[str, str] = {
|
|
1313
|
+
**GuideLegend.elements,
|
|
1314
|
+
"axis_line": "legend.axis.line",
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
# ============================================================================
|
|
1319
|
+
# GuideCustom -- user-supplied grob guide
|
|
1320
|
+
# ============================================================================
|
|
1321
|
+
|
|
1322
|
+
class GuideCustom(Guide):
|
|
1323
|
+
"""Custom guide that displays a user-supplied grob.
|
|
1324
|
+
|
|
1325
|
+
Attributes
|
|
1326
|
+
----------
|
|
1327
|
+
_class_name : str
|
|
1328
|
+
``"GuideCustom"``.
|
|
1329
|
+
"""
|
|
1330
|
+
|
|
1331
|
+
_class_name: str = "GuideCustom"
|
|
1332
|
+
|
|
1333
|
+
params: Dict[str, Any] = {
|
|
1334
|
+
**Guide.params,
|
|
1335
|
+
"grob": None,
|
|
1336
|
+
"width": None,
|
|
1337
|
+
"height": None,
|
|
1338
|
+
"name": "custom",
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
available_aes: List[str] = ["any"]
|
|
1342
|
+
|
|
1343
|
+
hashables: List[str] = ["title", "grob"]
|
|
1344
|
+
|
|
1345
|
+
elements: Dict[str, str] = {
|
|
1346
|
+
"background": "legend.background",
|
|
1347
|
+
"margin": "legend.margin",
|
|
1348
|
+
"title": "legend.title",
|
|
1349
|
+
"title_position": "legend.title.position",
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
def train(
|
|
1353
|
+
self,
|
|
1354
|
+
params: Optional[Dict[str, Any]] = None,
|
|
1355
|
+
scale: Any = None,
|
|
1356
|
+
aesthetic: Optional[str] = None,
|
|
1357
|
+
**kwargs: Any,
|
|
1358
|
+
) -> Dict[str, Any]:
|
|
1359
|
+
"""Custom guides skip training.
|
|
1360
|
+
|
|
1361
|
+
Returns
|
|
1362
|
+
-------
|
|
1363
|
+
dict
|
|
1364
|
+
Unchanged parameters.
|
|
1365
|
+
"""
|
|
1366
|
+
return params if params is not None else dict(self.params)
|
|
1367
|
+
|
|
1368
|
+
@staticmethod
|
|
1369
|
+
def transform(params: Dict[str, Any], coord: Any = None, **kwargs: Any) -> Dict[str, Any]:
|
|
1370
|
+
"""Pass through without transformation.
|
|
1371
|
+
|
|
1372
|
+
Returns
|
|
1373
|
+
-------
|
|
1374
|
+
dict
|
|
1375
|
+
Unmodified parameters.
|
|
1376
|
+
"""
|
|
1377
|
+
return params
|
|
1378
|
+
|
|
1379
|
+
def draw(
|
|
1380
|
+
self,
|
|
1381
|
+
theme: Any = None,
|
|
1382
|
+
position: Optional[str] = None,
|
|
1383
|
+
direction: Optional[str] = None,
|
|
1384
|
+
params: Optional[Dict[str, Any]] = None,
|
|
1385
|
+
) -> Any:
|
|
1386
|
+
"""Draw the custom grob with optional title.
|
|
1387
|
+
|
|
1388
|
+
Parameters
|
|
1389
|
+
----------
|
|
1390
|
+
theme : Theme, optional
|
|
1391
|
+
Plot theme.
|
|
1392
|
+
position : str, optional
|
|
1393
|
+
Legend position.
|
|
1394
|
+
direction : str, optional
|
|
1395
|
+
Legend direction.
|
|
1396
|
+
params : dict, optional
|
|
1397
|
+
Guide parameters.
|
|
1398
|
+
|
|
1399
|
+
Returns
|
|
1400
|
+
-------
|
|
1401
|
+
grob
|
|
1402
|
+
The custom grob.
|
|
1403
|
+
"""
|
|
1404
|
+
if params is None:
|
|
1405
|
+
params = dict(self.params)
|
|
1406
|
+
return params.get("grob")
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
# ============================================================================
|
|
1410
|
+
# GuideAxisLogticks -- log-scale tick marks
|
|
1411
|
+
# ============================================================================
|
|
1412
|
+
|
|
1413
|
+
class GuideAxisLogticks(GuideAxis):
|
|
1414
|
+
"""Axis guide with logarithmic tick marks.
|
|
1415
|
+
|
|
1416
|
+
Replaces standard tick placement with ticks at log10-spaced intervals.
|
|
1417
|
+
|
|
1418
|
+
Attributes
|
|
1419
|
+
----------
|
|
1420
|
+
_class_name : str
|
|
1421
|
+
``"GuideAxisLogticks"``.
|
|
1422
|
+
"""
|
|
1423
|
+
|
|
1424
|
+
_class_name: str = "GuideAxisLogticks"
|
|
1425
|
+
|
|
1426
|
+
params: Dict[str, Any] = {
|
|
1427
|
+
**GuideAxis.params,
|
|
1428
|
+
"long": 2.25,
|
|
1429
|
+
"mid": 1.5,
|
|
1430
|
+
"short": 0.75,
|
|
1431
|
+
"prescale.base": None,
|
|
1432
|
+
"negative.small": None,
|
|
1433
|
+
"short.theme": None,
|
|
1434
|
+
"expanded": True,
|
|
1435
|
+
"name": "axis_logticks",
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
available_aes: List[str] = ["x", "y"]
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
# ============================================================================
|
|
1442
|
+
# GuideAxisStack -- stacked axis guides
|
|
1443
|
+
# ============================================================================
|
|
1444
|
+
|
|
1445
|
+
class GuideAxisStack(GuideAxis):
|
|
1446
|
+
"""Stacked axis guide combining multiple axis guides.
|
|
1447
|
+
|
|
1448
|
+
Attributes
|
|
1449
|
+
----------
|
|
1450
|
+
_class_name : str
|
|
1451
|
+
``"GuideAxisStack"``.
|
|
1452
|
+
"""
|
|
1453
|
+
|
|
1454
|
+
_class_name: str = "GuideAxisStack"
|
|
1455
|
+
|
|
1456
|
+
params: Dict[str, Any] = {
|
|
1457
|
+
"guides": [],
|
|
1458
|
+
"guide_params": [],
|
|
1459
|
+
"spacing": None,
|
|
1460
|
+
"name": "stacked_axis",
|
|
1461
|
+
"title": waiver(),
|
|
1462
|
+
"theme": None,
|
|
1463
|
+
"angle": waiver(),
|
|
1464
|
+
"hash": "",
|
|
1465
|
+
"position": waiver(),
|
|
1466
|
+
"direction": None,
|
|
1467
|
+
"order": 0,
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
available_aes: List[str] = ["x", "y", "theta", "r"]
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
# ============================================================================
|
|
1474
|
+
# GuideAxisTheta -- angle axis for radial coordinates
|
|
1475
|
+
# ============================================================================
|
|
1476
|
+
|
|
1477
|
+
class GuideAxisTheta(GuideAxis):
|
|
1478
|
+
"""Angle axis guide for polar / radial coordinates.
|
|
1479
|
+
|
|
1480
|
+
Attributes
|
|
1481
|
+
----------
|
|
1482
|
+
_class_name : str
|
|
1483
|
+
``"GuideAxisTheta"``.
|
|
1484
|
+
"""
|
|
1485
|
+
|
|
1486
|
+
_class_name: str = "GuideAxisTheta"
|
|
1487
|
+
|
|
1488
|
+
params: Dict[str, Any] = {
|
|
1489
|
+
**GuideAxis.params,
|
|
1490
|
+
"name": "axis_theta",
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
available_aes: List[str] = ["x", "y", "theta"]
|
|
1494
|
+
|
|
1495
|
+
@staticmethod
|
|
1496
|
+
def transform(
|
|
1497
|
+
params: Dict[str, Any],
|
|
1498
|
+
coord: Any,
|
|
1499
|
+
panel_params: Any,
|
|
1500
|
+
) -> Dict[str, Any]:
|
|
1501
|
+
"""Transform data for theta axis.
|
|
1502
|
+
|
|
1503
|
+
Delegates to :meth:`GuideAxis.transform` and then adds
|
|
1504
|
+
``theta`` column for label angle computation.
|
|
1505
|
+
|
|
1506
|
+
Parameters
|
|
1507
|
+
----------
|
|
1508
|
+
params : dict
|
|
1509
|
+
Guide parameters.
|
|
1510
|
+
coord : Coord
|
|
1511
|
+
Coordinate system.
|
|
1512
|
+
panel_params : object
|
|
1513
|
+
Panel parameters.
|
|
1514
|
+
|
|
1515
|
+
Returns
|
|
1516
|
+
-------
|
|
1517
|
+
dict
|
|
1518
|
+
Transformed parameters.
|
|
1519
|
+
"""
|
|
1520
|
+
params = GuideAxis.transform(params, coord, panel_params)
|
|
1521
|
+
key = params.get("key")
|
|
1522
|
+
if key is not None and not key.empty and "theta" not in key.columns:
|
|
1523
|
+
position = params.get("position", "bottom")
|
|
1524
|
+
theta_map = {
|
|
1525
|
+
"top": 0.0,
|
|
1526
|
+
"bottom": np.pi,
|
|
1527
|
+
"left": 1.5 * np.pi,
|
|
1528
|
+
"right": 0.5 * np.pi,
|
|
1529
|
+
}
|
|
1530
|
+
key = key.copy()
|
|
1531
|
+
key["theta"] = theta_map.get(position, 0.0)
|
|
1532
|
+
params["key"] = key
|
|
1533
|
+
return params
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
# ============================================================================
|
|
1537
|
+
# GuideOld -- legacy S3 compatibility wrapper
|
|
1538
|
+
# ============================================================================
|
|
1539
|
+
|
|
1540
|
+
class GuideOld(Guide):
|
|
1541
|
+
"""Compatibility wrapper for the previous S3-based guide system.
|
|
1542
|
+
|
|
1543
|
+
The old S3 methods (``guide_train``, ``guide_merge``, etc.) are
|
|
1544
|
+
dispatched through this class as a fallback.
|
|
1545
|
+
|
|
1546
|
+
Attributes
|
|
1547
|
+
----------
|
|
1548
|
+
_class_name : str
|
|
1549
|
+
``"GuideOld"``.
|
|
1550
|
+
"""
|
|
1551
|
+
|
|
1552
|
+
_class_name: str = "GuideOld"
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
# ============================================================================
|
|
1556
|
+
# new_guide() -- Guide constructor factory
|
|
1557
|
+
# ============================================================================
|
|
1558
|
+
|
|
1559
|
+
def new_guide(
|
|
1560
|
+
*,
|
|
1561
|
+
available_aes: Union[str, List[str]] = "any",
|
|
1562
|
+
super: type = Guide, # noqa: A002 (shadows builtin intentionally)
|
|
1563
|
+
**kwargs: Any,
|
|
1564
|
+
) -> Guide:
|
|
1565
|
+
"""Construct a Guide instance with validated parameters.
|
|
1566
|
+
|
|
1567
|
+
Parameters
|
|
1568
|
+
----------
|
|
1569
|
+
available_aes : str or list of str
|
|
1570
|
+
Aesthetics supported by this guide. ``"any"`` matches all
|
|
1571
|
+
non-position aesthetics.
|
|
1572
|
+
super : type
|
|
1573
|
+
The Guide (sub)class to instantiate.
|
|
1574
|
+
**kwargs : Any
|
|
1575
|
+
Parameter overrides. Must be a subset of ``super.params`` keys.
|
|
1576
|
+
|
|
1577
|
+
Returns
|
|
1578
|
+
-------
|
|
1579
|
+
Guide
|
|
1580
|
+
A new guide instance.
|
|
1581
|
+
|
|
1582
|
+
Raises
|
|
1583
|
+
------
|
|
1584
|
+
ValueError
|
|
1585
|
+
If required parameters are missing.
|
|
1586
|
+
"""
|
|
1587
|
+
if isinstance(available_aes, str):
|
|
1588
|
+
available_aes = [available_aes]
|
|
1589
|
+
|
|
1590
|
+
# Determine valid parameter names
|
|
1591
|
+
param_names = set(super.params.keys()) if hasattr(super, "params") else set()
|
|
1592
|
+
|
|
1593
|
+
# Split into params vs extra
|
|
1594
|
+
params: Dict[str, Any] = {}
|
|
1595
|
+
extra_args: List[str] = []
|
|
1596
|
+
for k, v in kwargs.items():
|
|
1597
|
+
if k in param_names:
|
|
1598
|
+
params[k] = v
|
|
1599
|
+
else:
|
|
1600
|
+
extra_args.append(k)
|
|
1601
|
+
|
|
1602
|
+
if extra_args:
|
|
1603
|
+
cls_name = snake_class(super) if hasattr(super, "_class_name") else str(super)
|
|
1604
|
+
cli_warn(
|
|
1605
|
+
f"Ignoring unknown argument(s) to {cls_name}: "
|
|
1606
|
+
f"{', '.join(extra_args)}."
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
# Fill defaults
|
|
1610
|
+
if hasattr(super, "params"):
|
|
1611
|
+
merged = dict(super.params)
|
|
1612
|
+
merged.update(params)
|
|
1613
|
+
params = merged
|
|
1614
|
+
|
|
1615
|
+
# Validate required base Guide params
|
|
1616
|
+
required = set(Guide.params.keys())
|
|
1617
|
+
missing = required - set(params.keys())
|
|
1618
|
+
if missing:
|
|
1619
|
+
cli_abort(
|
|
1620
|
+
f"The following parameters are required for setting up a guide "
|
|
1621
|
+
f"but are missing: {', '.join(sorted(missing))}"
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
# Validate theme
|
|
1625
|
+
theme = params.get("theme")
|
|
1626
|
+
if theme is not None:
|
|
1627
|
+
direction = params.get("direction")
|
|
1628
|
+
if direction is None and hasattr(theme, "get"):
|
|
1629
|
+
params["direction"] = theme.get("legend.direction")
|
|
1630
|
+
|
|
1631
|
+
# Ensure order is an integer
|
|
1632
|
+
params["order"] = int(params.get("order", 0))
|
|
1633
|
+
|
|
1634
|
+
# Create instance
|
|
1635
|
+
instance = super()
|
|
1636
|
+
instance.params = params
|
|
1637
|
+
instance.available_aes = list(available_aes)
|
|
1638
|
+
return instance
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
# ============================================================================
|
|
1642
|
+
# Constructor functions
|
|
1643
|
+
# ============================================================================
|
|
1644
|
+
|
|
1645
|
+
def guide_none(
|
|
1646
|
+
title: Any = waiver(),
|
|
1647
|
+
position: Any = waiver(),
|
|
1648
|
+
) -> GuideNone:
|
|
1649
|
+
"""Create a guide that draws nothing.
|
|
1650
|
+
|
|
1651
|
+
Parameters
|
|
1652
|
+
----------
|
|
1653
|
+
title : str or Waiver
|
|
1654
|
+
Guide title (unused but kept for interface consistency).
|
|
1655
|
+
position : str or Waiver
|
|
1656
|
+
Position hint.
|
|
1657
|
+
|
|
1658
|
+
Returns
|
|
1659
|
+
-------
|
|
1660
|
+
GuideNone
|
|
1661
|
+
An empty guide.
|
|
1662
|
+
"""
|
|
1663
|
+
return new_guide(
|
|
1664
|
+
title=title,
|
|
1665
|
+
position=position,
|
|
1666
|
+
available_aes="any",
|
|
1667
|
+
super=GuideNone,
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
def guide_axis(
|
|
1672
|
+
title: Any = waiver(),
|
|
1673
|
+
theme: Any = None,
|
|
1674
|
+
check_overlap: bool = False,
|
|
1675
|
+
angle: Any = waiver(),
|
|
1676
|
+
n_dodge: int = 1,
|
|
1677
|
+
minor_ticks: bool = False,
|
|
1678
|
+
cap: Union[str, bool] = "none",
|
|
1679
|
+
order: int = 0,
|
|
1680
|
+
position: Any = waiver(),
|
|
1681
|
+
) -> GuideAxis:
|
|
1682
|
+
"""Create an axis guide.
|
|
1683
|
+
|
|
1684
|
+
Parameters
|
|
1685
|
+
----------
|
|
1686
|
+
title : str or Waiver
|
|
1687
|
+
Axis title.
|
|
1688
|
+
theme : Theme, optional
|
|
1689
|
+
Theme overrides.
|
|
1690
|
+
check_overlap : bool
|
|
1691
|
+
Silently remove overlapping labels.
|
|
1692
|
+
angle : float or Waiver
|
|
1693
|
+
Text angle in degrees.
|
|
1694
|
+
n_dodge : int
|
|
1695
|
+
Number of rows/columns for dodging labels.
|
|
1696
|
+
minor_ticks : bool
|
|
1697
|
+
Whether to draw minor ticks.
|
|
1698
|
+
cap : str or bool
|
|
1699
|
+
Axis line capping: ``"none"``, ``"both"``, ``"upper"``,
|
|
1700
|
+
``"lower"``, ``True`` (="both"), or ``False`` (="none").
|
|
1701
|
+
order : int
|
|
1702
|
+
Guide ordering priority.
|
|
1703
|
+
position : str or Waiver
|
|
1704
|
+
Where the axis is drawn.
|
|
1705
|
+
|
|
1706
|
+
Returns
|
|
1707
|
+
-------
|
|
1708
|
+
GuideAxis
|
|
1709
|
+
An axis guide instance.
|
|
1710
|
+
"""
|
|
1711
|
+
if isinstance(cap, bool):
|
|
1712
|
+
cap = "both" if cap else "none"
|
|
1713
|
+
if cap not in ("none", "both", "upper", "lower"):
|
|
1714
|
+
cli_abort(f"`cap` must be one of 'none', 'both', 'upper', 'lower', got {cap!r}")
|
|
1715
|
+
|
|
1716
|
+
return new_guide(
|
|
1717
|
+
title=title,
|
|
1718
|
+
theme=theme,
|
|
1719
|
+
**{
|
|
1720
|
+
"check.overlap": check_overlap,
|
|
1721
|
+
},
|
|
1722
|
+
angle=angle,
|
|
1723
|
+
**{
|
|
1724
|
+
"n.dodge": n_dodge,
|
|
1725
|
+
"minor.ticks": minor_ticks,
|
|
1726
|
+
},
|
|
1727
|
+
cap=cap,
|
|
1728
|
+
order=order,
|
|
1729
|
+
position=position,
|
|
1730
|
+
available_aes=["x", "y", "r"],
|
|
1731
|
+
name="axis",
|
|
1732
|
+
super=GuideAxis,
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def guide_legend(
|
|
1737
|
+
title: Any = waiver(),
|
|
1738
|
+
theme: Any = None,
|
|
1739
|
+
position: Optional[str] = None,
|
|
1740
|
+
direction: Optional[str] = None,
|
|
1741
|
+
override_aes: Optional[Dict[str, Any]] = None,
|
|
1742
|
+
nrow: Optional[int] = None,
|
|
1743
|
+
ncol: Optional[int] = None,
|
|
1744
|
+
reverse: bool = False,
|
|
1745
|
+
order: int = 0,
|
|
1746
|
+
**kwargs: Any,
|
|
1747
|
+
) -> GuideLegend:
|
|
1748
|
+
"""Create a legend guide.
|
|
1749
|
+
|
|
1750
|
+
Parameters
|
|
1751
|
+
----------
|
|
1752
|
+
title : str or Waiver
|
|
1753
|
+
Legend title.
|
|
1754
|
+
theme : Theme, optional
|
|
1755
|
+
Theme overrides.
|
|
1756
|
+
position : str, optional
|
|
1757
|
+
One of ``"top"``, ``"right"``, ``"bottom"``, ``"left"``, or
|
|
1758
|
+
``"inside"``.
|
|
1759
|
+
direction : str, optional
|
|
1760
|
+
``"horizontal"`` or ``"vertical"``.
|
|
1761
|
+
override_aes : dict, optional
|
|
1762
|
+
Aesthetic parameters to override in the legend keys.
|
|
1763
|
+
nrow : int, optional
|
|
1764
|
+
Number of rows.
|
|
1765
|
+
ncol : int, optional
|
|
1766
|
+
Number of columns.
|
|
1767
|
+
reverse : bool
|
|
1768
|
+
Reverse the order of keys.
|
|
1769
|
+
order : int
|
|
1770
|
+
Guide ordering priority.
|
|
1771
|
+
**kwargs : Any
|
|
1772
|
+
Ignored (for compatibility).
|
|
1773
|
+
|
|
1774
|
+
Returns
|
|
1775
|
+
-------
|
|
1776
|
+
GuideLegend
|
|
1777
|
+
A legend guide.
|
|
1778
|
+
"""
|
|
1779
|
+
if position is not None and position not in _TRBL + ["inside"]:
|
|
1780
|
+
cli_abort(
|
|
1781
|
+
f"`position` must be one of {_TRBL + ['inside']!r}, got {position!r}"
|
|
1782
|
+
)
|
|
1783
|
+
if override_aes is None:
|
|
1784
|
+
override_aes = {}
|
|
1785
|
+
|
|
1786
|
+
return new_guide(
|
|
1787
|
+
title=title,
|
|
1788
|
+
theme=theme,
|
|
1789
|
+
direction=direction,
|
|
1790
|
+
**{"override.aes": override_aes},
|
|
1791
|
+
nrow=nrow,
|
|
1792
|
+
ncol=ncol,
|
|
1793
|
+
reverse=reverse,
|
|
1794
|
+
order=order,
|
|
1795
|
+
position=position,
|
|
1796
|
+
available_aes="any",
|
|
1797
|
+
name="legend",
|
|
1798
|
+
super=GuideLegend,
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def guide_colourbar(
|
|
1803
|
+
title: Any = waiver(),
|
|
1804
|
+
theme: Any = None,
|
|
1805
|
+
nbin: Optional[int] = None,
|
|
1806
|
+
display: str = "raster",
|
|
1807
|
+
alpha: float = float("nan"),
|
|
1808
|
+
draw_ulim: bool = True,
|
|
1809
|
+
draw_llim: bool = True,
|
|
1810
|
+
angle: Optional[float] = None,
|
|
1811
|
+
position: Optional[str] = None,
|
|
1812
|
+
direction: Optional[str] = None,
|
|
1813
|
+
reverse: bool = False,
|
|
1814
|
+
order: int = 0,
|
|
1815
|
+
available_aes: Optional[List[str]] = None,
|
|
1816
|
+
**kwargs: Any,
|
|
1817
|
+
) -> GuideColourbar:
|
|
1818
|
+
"""Create a continuous colour bar guide.
|
|
1819
|
+
|
|
1820
|
+
Parameters
|
|
1821
|
+
----------
|
|
1822
|
+
title : str or Waiver
|
|
1823
|
+
Guide title.
|
|
1824
|
+
theme : Theme, optional
|
|
1825
|
+
Theme overrides.
|
|
1826
|
+
nbin : int, optional
|
|
1827
|
+
Number of bins. Defaults to 300 for raster/rectangles, 15 for
|
|
1828
|
+
gradient.
|
|
1829
|
+
display : str
|
|
1830
|
+
``"raster"``, ``"rectangles"``, or ``"gradient"``.
|
|
1831
|
+
alpha : float
|
|
1832
|
+
Colour transparency (0--1). ``NaN`` preserves encoded alpha.
|
|
1833
|
+
draw_ulim : bool
|
|
1834
|
+
Draw upper limit tick.
|
|
1835
|
+
draw_llim : bool
|
|
1836
|
+
Draw lower limit tick.
|
|
1837
|
+
angle : float, optional
|
|
1838
|
+
Label angle.
|
|
1839
|
+
position : str, optional
|
|
1840
|
+
Legend position.
|
|
1841
|
+
direction : str, optional
|
|
1842
|
+
``"horizontal"`` or ``"vertical"``.
|
|
1843
|
+
reverse : bool
|
|
1844
|
+
Reverse colour bar direction.
|
|
1845
|
+
order : int
|
|
1846
|
+
Guide ordering priority.
|
|
1847
|
+
available_aes : list of str, optional
|
|
1848
|
+
Supported aesthetics. Defaults to colour/color/fill.
|
|
1849
|
+
**kwargs : Any
|
|
1850
|
+
Ignored.
|
|
1851
|
+
|
|
1852
|
+
Returns
|
|
1853
|
+
-------
|
|
1854
|
+
GuideColourbar
|
|
1855
|
+
A colour bar guide.
|
|
1856
|
+
"""
|
|
1857
|
+
if display not in ("raster", "rectangles", "gradient"):
|
|
1858
|
+
cli_abort(f"`display` must be 'raster', 'rectangles', or 'gradient', got {display!r}")
|
|
1859
|
+
if nbin is None:
|
|
1860
|
+
nbin = 15 if display == "gradient" else 300
|
|
1861
|
+
|
|
1862
|
+
if position is not None and position not in _TRBL + ["inside"]:
|
|
1863
|
+
cli_abort(f"`position` must be one of {_TRBL + ['inside']!r}, got {position!r}")
|
|
1864
|
+
|
|
1865
|
+
if available_aes is None:
|
|
1866
|
+
available_aes = ["colour", "color", "fill"]
|
|
1867
|
+
|
|
1868
|
+
return new_guide(
|
|
1869
|
+
title=title,
|
|
1870
|
+
theme=theme,
|
|
1871
|
+
nbin=nbin,
|
|
1872
|
+
display=display,
|
|
1873
|
+
alpha=alpha,
|
|
1874
|
+
angle=angle,
|
|
1875
|
+
draw_lim=[bool(draw_llim), bool(draw_ulim)],
|
|
1876
|
+
position=position,
|
|
1877
|
+
direction=direction,
|
|
1878
|
+
reverse=reverse,
|
|
1879
|
+
order=order,
|
|
1880
|
+
available_aes=available_aes,
|
|
1881
|
+
name="colourbar",
|
|
1882
|
+
super=GuideColourbar,
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
# Alias
|
|
1887
|
+
guide_colorbar = guide_colourbar
|
|
1888
|
+
"""Alias for :func:`guide_colourbar`."""
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def guide_coloursteps(
|
|
1892
|
+
title: Any = waiver(),
|
|
1893
|
+
theme: Any = None,
|
|
1894
|
+
alpha: float = float("nan"),
|
|
1895
|
+
angle: Optional[float] = None,
|
|
1896
|
+
even_steps: bool = True,
|
|
1897
|
+
show_limits: Optional[bool] = None,
|
|
1898
|
+
direction: Optional[str] = None,
|
|
1899
|
+
position: Optional[str] = None,
|
|
1900
|
+
reverse: bool = False,
|
|
1901
|
+
order: int = 0,
|
|
1902
|
+
available_aes: Optional[List[str]] = None,
|
|
1903
|
+
**kwargs: Any,
|
|
1904
|
+
) -> GuideColoursteps:
|
|
1905
|
+
"""Create a stepped colour bar guide.
|
|
1906
|
+
|
|
1907
|
+
Parameters
|
|
1908
|
+
----------
|
|
1909
|
+
title : str or Waiver
|
|
1910
|
+
Guide title.
|
|
1911
|
+
theme : Theme, optional
|
|
1912
|
+
Theme overrides.
|
|
1913
|
+
alpha : float
|
|
1914
|
+
Colour transparency.
|
|
1915
|
+
angle : float, optional
|
|
1916
|
+
Label angle.
|
|
1917
|
+
even_steps : bool
|
|
1918
|
+
Make all bins the same rendered size.
|
|
1919
|
+
show_limits : bool, optional
|
|
1920
|
+
Show scale limits.
|
|
1921
|
+
direction : str, optional
|
|
1922
|
+
``"horizontal"`` or ``"vertical"``.
|
|
1923
|
+
position : str, optional
|
|
1924
|
+
Legend position.
|
|
1925
|
+
reverse : bool
|
|
1926
|
+
Reverse colour bar.
|
|
1927
|
+
order : int
|
|
1928
|
+
Guide ordering priority.
|
|
1929
|
+
available_aes : list of str, optional
|
|
1930
|
+
Supported aesthetics.
|
|
1931
|
+
**kwargs : Any
|
|
1932
|
+
Ignored.
|
|
1933
|
+
|
|
1934
|
+
Returns
|
|
1935
|
+
-------
|
|
1936
|
+
GuideColoursteps
|
|
1937
|
+
A stepped colour bar guide.
|
|
1938
|
+
"""
|
|
1939
|
+
if available_aes is None:
|
|
1940
|
+
available_aes = ["colour", "color", "fill"]
|
|
1941
|
+
|
|
1942
|
+
return new_guide(
|
|
1943
|
+
title=title,
|
|
1944
|
+
theme=theme,
|
|
1945
|
+
alpha=alpha,
|
|
1946
|
+
angle=angle,
|
|
1947
|
+
**{
|
|
1948
|
+
"even.steps": even_steps,
|
|
1949
|
+
"show.limits": show_limits,
|
|
1950
|
+
},
|
|
1951
|
+
position=position,
|
|
1952
|
+
direction=direction,
|
|
1953
|
+
reverse=reverse,
|
|
1954
|
+
order=order,
|
|
1955
|
+
available_aes=available_aes,
|
|
1956
|
+
super=GuideColoursteps,
|
|
1957
|
+
)
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
# Alias
|
|
1961
|
+
guide_colorsteps = guide_coloursteps
|
|
1962
|
+
"""Alias for :func:`guide_coloursteps`."""
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def guide_bins(
|
|
1966
|
+
title: Any = waiver(),
|
|
1967
|
+
theme: Any = None,
|
|
1968
|
+
angle: Optional[float] = None,
|
|
1969
|
+
position: Optional[str] = None,
|
|
1970
|
+
direction: Optional[str] = None,
|
|
1971
|
+
override_aes: Optional[Dict[str, Any]] = None,
|
|
1972
|
+
reverse: bool = False,
|
|
1973
|
+
order: int = 0,
|
|
1974
|
+
show_limits: Optional[bool] = None,
|
|
1975
|
+
**kwargs: Any,
|
|
1976
|
+
) -> GuideBins:
|
|
1977
|
+
"""Create a binned legend guide.
|
|
1978
|
+
|
|
1979
|
+
Parameters
|
|
1980
|
+
----------
|
|
1981
|
+
title : str or Waiver
|
|
1982
|
+
Guide title.
|
|
1983
|
+
theme : Theme, optional
|
|
1984
|
+
Theme overrides.
|
|
1985
|
+
angle : float, optional
|
|
1986
|
+
Label angle.
|
|
1987
|
+
position : str, optional
|
|
1988
|
+
Legend position.
|
|
1989
|
+
direction : str, optional
|
|
1990
|
+
``"horizontal"`` or ``"vertical"``.
|
|
1991
|
+
override_aes : dict, optional
|
|
1992
|
+
Aesthetic overrides for keys.
|
|
1993
|
+
reverse : bool
|
|
1994
|
+
Reverse key order.
|
|
1995
|
+
order : int
|
|
1996
|
+
Guide ordering priority.
|
|
1997
|
+
show_limits : bool, optional
|
|
1998
|
+
Show scale limits.
|
|
1999
|
+
**kwargs : Any
|
|
2000
|
+
Ignored.
|
|
2001
|
+
|
|
2002
|
+
Returns
|
|
2003
|
+
-------
|
|
2004
|
+
GuideBins
|
|
2005
|
+
A binned legend guide.
|
|
2006
|
+
"""
|
|
2007
|
+
if position is not None and position not in _TRBL + ["inside"]:
|
|
2008
|
+
cli_abort(f"`position` must be one of {_TRBL + ['inside']!r}, got {position!r}")
|
|
2009
|
+
if override_aes is None:
|
|
2010
|
+
override_aes = {}
|
|
2011
|
+
|
|
2012
|
+
return new_guide(
|
|
2013
|
+
title=title,
|
|
2014
|
+
theme=theme,
|
|
2015
|
+
angle=angle,
|
|
2016
|
+
position=position,
|
|
2017
|
+
direction=direction,
|
|
2018
|
+
**{
|
|
2019
|
+
"override.aes": override_aes,
|
|
2020
|
+
"show.limits": show_limits,
|
|
2021
|
+
},
|
|
2022
|
+
reverse=reverse,
|
|
2023
|
+
order=order,
|
|
2024
|
+
available_aes="any",
|
|
2025
|
+
name="bins",
|
|
2026
|
+
super=GuideBins,
|
|
2027
|
+
)
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
def guide_custom(
|
|
2031
|
+
grob: Any,
|
|
2032
|
+
width: Any = None,
|
|
2033
|
+
height: Any = None,
|
|
2034
|
+
title: Optional[str] = None,
|
|
2035
|
+
theme: Any = None,
|
|
2036
|
+
position: Optional[str] = None,
|
|
2037
|
+
order: int = 0,
|
|
2038
|
+
) -> GuideCustom:
|
|
2039
|
+
"""Create a custom guide displaying a user-supplied grob.
|
|
2040
|
+
|
|
2041
|
+
Parameters
|
|
2042
|
+
----------
|
|
2043
|
+
grob : grob
|
|
2044
|
+
The graphical object to display.
|
|
2045
|
+
width : unit, optional
|
|
2046
|
+
Allocated width.
|
|
2047
|
+
height : unit, optional
|
|
2048
|
+
Allocated height.
|
|
2049
|
+
title : str, optional
|
|
2050
|
+
Guide title. ``None`` means no title.
|
|
2051
|
+
theme : Theme, optional
|
|
2052
|
+
Theme overrides.
|
|
2053
|
+
position : str, optional
|
|
2054
|
+
Legend position.
|
|
2055
|
+
order : int
|
|
2056
|
+
Guide ordering priority.
|
|
2057
|
+
|
|
2058
|
+
Returns
|
|
2059
|
+
-------
|
|
2060
|
+
GuideCustom
|
|
2061
|
+
A custom guide.
|
|
2062
|
+
"""
|
|
2063
|
+
return new_guide(
|
|
2064
|
+
grob=grob,
|
|
2065
|
+
width=width,
|
|
2066
|
+
height=height,
|
|
2067
|
+
title=title,
|
|
2068
|
+
theme=theme,
|
|
2069
|
+
hash=_hash_object([title, grob]),
|
|
2070
|
+
position=position,
|
|
2071
|
+
order=order,
|
|
2072
|
+
available_aes="any",
|
|
2073
|
+
super=GuideCustom,
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def guide_axis_logticks(
|
|
2078
|
+
long: float = 2.25,
|
|
2079
|
+
mid: float = 1.5,
|
|
2080
|
+
short: float = 0.75,
|
|
2081
|
+
prescale_base: Optional[float] = None,
|
|
2082
|
+
negative_small: Optional[float] = None,
|
|
2083
|
+
short_theme: Any = None,
|
|
2084
|
+
expanded: bool = True,
|
|
2085
|
+
cap: Union[str, bool] = "none",
|
|
2086
|
+
theme: Any = None,
|
|
2087
|
+
title: Any = waiver(),
|
|
2088
|
+
order: int = 0,
|
|
2089
|
+
position: Any = waiver(),
|
|
2090
|
+
**kwargs: Any,
|
|
2091
|
+
) -> GuideAxisLogticks:
|
|
2092
|
+
"""Create an axis guide with log-spaced tick marks.
|
|
2093
|
+
|
|
2094
|
+
Parameters
|
|
2095
|
+
----------
|
|
2096
|
+
long : float
|
|
2097
|
+
Relative length of long (decade) ticks.
|
|
2098
|
+
mid : float
|
|
2099
|
+
Relative length of mid ticks.
|
|
2100
|
+
short : float
|
|
2101
|
+
Relative length of short ticks.
|
|
2102
|
+
prescale_base : float, optional
|
|
2103
|
+
Log base for pre-transformed data.
|
|
2104
|
+
negative_small : float, optional
|
|
2105
|
+
Smallest absolute value ticked when 0 is included.
|
|
2106
|
+
short_theme : element, optional
|
|
2107
|
+
Theme element for shortest ticks.
|
|
2108
|
+
expanded : bool
|
|
2109
|
+
Cover expanded range.
|
|
2110
|
+
cap : str or bool
|
|
2111
|
+
Axis line capping.
|
|
2112
|
+
theme : Theme, optional
|
|
2113
|
+
Theme overrides.
|
|
2114
|
+
title : str or Waiver
|
|
2115
|
+
Axis title.
|
|
2116
|
+
order : int
|
|
2117
|
+
Guide ordering priority.
|
|
2118
|
+
position : str or Waiver
|
|
2119
|
+
Axis position.
|
|
2120
|
+
**kwargs : Any
|
|
2121
|
+
Forwarded to :func:`guide_axis`.
|
|
2122
|
+
|
|
2123
|
+
Returns
|
|
2124
|
+
-------
|
|
2125
|
+
GuideAxisLogticks
|
|
2126
|
+
A log-tick axis guide.
|
|
2127
|
+
"""
|
|
2128
|
+
if isinstance(cap, bool):
|
|
2129
|
+
cap = "both" if cap else "none"
|
|
2130
|
+
|
|
2131
|
+
return new_guide(
|
|
2132
|
+
title=title,
|
|
2133
|
+
theme=theme,
|
|
2134
|
+
long=long,
|
|
2135
|
+
mid=mid,
|
|
2136
|
+
short=short,
|
|
2137
|
+
**{
|
|
2138
|
+
"prescale.base": prescale_base,
|
|
2139
|
+
"negative.small": negative_small,
|
|
2140
|
+
"short.theme": short_theme,
|
|
2141
|
+
},
|
|
2142
|
+
expanded=expanded,
|
|
2143
|
+
cap=cap,
|
|
2144
|
+
order=order,
|
|
2145
|
+
position=position,
|
|
2146
|
+
available_aes=["x", "y"],
|
|
2147
|
+
name="axis_logticks",
|
|
2148
|
+
super=GuideAxisLogticks,
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2151
|
+
|
|
2152
|
+
def guide_axis_stack(
|
|
2153
|
+
first: Any = "axis",
|
|
2154
|
+
*args: Any,
|
|
2155
|
+
title: Any = waiver(),
|
|
2156
|
+
theme: Any = None,
|
|
2157
|
+
spacing: Any = None,
|
|
2158
|
+
order: int = 0,
|
|
2159
|
+
position: Any = waiver(),
|
|
2160
|
+
) -> GuideAxisStack:
|
|
2161
|
+
"""Create a stacked axis guide.
|
|
2162
|
+
|
|
2163
|
+
Parameters
|
|
2164
|
+
----------
|
|
2165
|
+
first : str or Guide
|
|
2166
|
+
The innermost axis guide.
|
|
2167
|
+
*args : str or Guide
|
|
2168
|
+
Additional axis guides to stack.
|
|
2169
|
+
title : str or Waiver
|
|
2170
|
+
Axis title.
|
|
2171
|
+
theme : Theme, optional
|
|
2172
|
+
Theme overrides.
|
|
2173
|
+
spacing : unit, optional
|
|
2174
|
+
Space between stacked guides.
|
|
2175
|
+
order : int
|
|
2176
|
+
Guide ordering priority.
|
|
2177
|
+
position : str or Waiver
|
|
2178
|
+
Axis position.
|
|
2179
|
+
|
|
2180
|
+
Returns
|
|
2181
|
+
-------
|
|
2182
|
+
GuideAxisStack
|
|
2183
|
+
A stacked axis guide.
|
|
2184
|
+
"""
|
|
2185
|
+
axes = [_validate_guide(first)] + [_validate_guide(a) for a in args]
|
|
2186
|
+
guide_params = [dict(getattr(a, "params", {})) for a in axes]
|
|
2187
|
+
|
|
2188
|
+
return new_guide(
|
|
2189
|
+
title=title,
|
|
2190
|
+
theme=theme,
|
|
2191
|
+
guides=axes,
|
|
2192
|
+
guide_params=guide_params,
|
|
2193
|
+
spacing=spacing,
|
|
2194
|
+
available_aes=["x", "y", "theta", "r"],
|
|
2195
|
+
order=order,
|
|
2196
|
+
position=position,
|
|
2197
|
+
name="stacked_axis",
|
|
2198
|
+
super=GuideAxisStack,
|
|
2199
|
+
)
|
|
2200
|
+
|
|
2201
|
+
|
|
2202
|
+
def guide_axis_theta(
|
|
2203
|
+
title: Any = waiver(),
|
|
2204
|
+
theme: Any = None,
|
|
2205
|
+
angle: Any = waiver(),
|
|
2206
|
+
minor_ticks: bool = False,
|
|
2207
|
+
cap: Union[str, bool] = "none",
|
|
2208
|
+
order: int = 0,
|
|
2209
|
+
position: Any = waiver(),
|
|
2210
|
+
) -> GuideAxisTheta:
|
|
2211
|
+
"""Create an angle axis guide for radial coordinates.
|
|
2212
|
+
|
|
2213
|
+
Parameters
|
|
2214
|
+
----------
|
|
2215
|
+
title : str or Waiver
|
|
2216
|
+
Axis title.
|
|
2217
|
+
theme : Theme, optional
|
|
2218
|
+
Theme overrides.
|
|
2219
|
+
angle : float or Waiver
|
|
2220
|
+
Text angle.
|
|
2221
|
+
minor_ticks : bool
|
|
2222
|
+
Draw minor ticks.
|
|
2223
|
+
cap : str or bool
|
|
2224
|
+
Axis line capping.
|
|
2225
|
+
order : int
|
|
2226
|
+
Guide ordering priority.
|
|
2227
|
+
position : str or Waiver
|
|
2228
|
+
Axis position.
|
|
2229
|
+
|
|
2230
|
+
Returns
|
|
2231
|
+
-------
|
|
2232
|
+
GuideAxisTheta
|
|
2233
|
+
A theta axis guide.
|
|
2234
|
+
"""
|
|
2235
|
+
if isinstance(cap, bool):
|
|
2236
|
+
cap = "both" if cap else "none"
|
|
2237
|
+
|
|
2238
|
+
return new_guide(
|
|
2239
|
+
title=title,
|
|
2240
|
+
theme=theme,
|
|
2241
|
+
angle=angle,
|
|
2242
|
+
cap=cap,
|
|
2243
|
+
**{"minor.ticks": minor_ticks},
|
|
2244
|
+
available_aes=["x", "y", "theta"],
|
|
2245
|
+
order=order,
|
|
2246
|
+
position=position,
|
|
2247
|
+
name="axis_theta",
|
|
2248
|
+
super=GuideAxisTheta,
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
# ============================================================================
|
|
2253
|
+
# Legacy S3 compatibility functions
|
|
2254
|
+
# ============================================================================
|
|
2255
|
+
|
|
2256
|
+
def old_guide(guide: Any) -> GuideOld:
|
|
2257
|
+
"""Wrap a legacy guide object.
|
|
2258
|
+
|
|
2259
|
+
Parameters
|
|
2260
|
+
----------
|
|
2261
|
+
guide : object
|
|
2262
|
+
An old-style guide.
|
|
2263
|
+
|
|
2264
|
+
Returns
|
|
2265
|
+
-------
|
|
2266
|
+
GuideOld
|
|
2267
|
+
Wrapped guide.
|
|
2268
|
+
"""
|
|
2269
|
+
instance = GuideOld()
|
|
2270
|
+
instance._legacy = guide
|
|
2271
|
+
return instance
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def guide_train(guide: Any, scale: Any, aesthetic: Optional[str] = None) -> Any:
|
|
2275
|
+
"""Legacy S3-style ``guide_train`` dispatch.
|
|
2276
|
+
|
|
2277
|
+
Parameters
|
|
2278
|
+
----------
|
|
2279
|
+
guide : Guide
|
|
2280
|
+
The guide to train.
|
|
2281
|
+
scale : Scale
|
|
2282
|
+
Scale to train on.
|
|
2283
|
+
aesthetic : str, optional
|
|
2284
|
+
Aesthetic name.
|
|
2285
|
+
|
|
2286
|
+
Returns
|
|
2287
|
+
-------
|
|
2288
|
+
Any
|
|
2289
|
+
Trained guide parameters.
|
|
2290
|
+
"""
|
|
2291
|
+
if hasattr(guide, "train"):
|
|
2292
|
+
return guide.train(params=dict(getattr(guide, "params", {})),
|
|
2293
|
+
scale=scale, aesthetic=aesthetic)
|
|
2294
|
+
cli_abort("Guide classes have been rewritten as GGProto classes. "
|
|
2295
|
+
"The old S3 guide methods have been superseded.")
|
|
2296
|
+
|
|
2297
|
+
|
|
2298
|
+
def guide_merge(guide: Any, new_guide: Any) -> Any:
|
|
2299
|
+
"""Legacy S3-style ``guide_merge`` dispatch.
|
|
2300
|
+
|
|
2301
|
+
Parameters
|
|
2302
|
+
----------
|
|
2303
|
+
guide : Guide
|
|
2304
|
+
Primary guide.
|
|
2305
|
+
new_guide : Guide
|
|
2306
|
+
Guide to merge in.
|
|
2307
|
+
|
|
2308
|
+
Returns
|
|
2309
|
+
-------
|
|
2310
|
+
Any
|
|
2311
|
+
Merged guide.
|
|
2312
|
+
"""
|
|
2313
|
+
if hasattr(guide, "merge"):
|
|
2314
|
+
return guide.merge(dict(getattr(guide, "params", {})),
|
|
2315
|
+
new_guide,
|
|
2316
|
+
dict(getattr(new_guide, "params", {})))
|
|
2317
|
+
cli_abort("Guide classes have been rewritten as GGProto classes. "
|
|
2318
|
+
"The old S3 guide methods have been superseded.")
|
|
2319
|
+
|
|
2320
|
+
|
|
2321
|
+
def guide_geom(guide: Any, layers: Any = None, default_mapping: Any = None) -> Any:
|
|
2322
|
+
"""Legacy S3-style ``guide_geom`` dispatch.
|
|
2323
|
+
|
|
2324
|
+
Parameters
|
|
2325
|
+
----------
|
|
2326
|
+
guide : Guide
|
|
2327
|
+
The guide.
|
|
2328
|
+
layers : list, optional
|
|
2329
|
+
Plot layers.
|
|
2330
|
+
default_mapping : Mapping, optional
|
|
2331
|
+
Default aesthetic mapping.
|
|
2332
|
+
|
|
2333
|
+
Returns
|
|
2334
|
+
-------
|
|
2335
|
+
Any
|
|
2336
|
+
Geom info.
|
|
2337
|
+
"""
|
|
2338
|
+
if hasattr(guide, "process_layers"):
|
|
2339
|
+
return guide.process_layers(dict(getattr(guide, "params", {})),
|
|
2340
|
+
layers or [])
|
|
2341
|
+
cli_abort("Guide classes have been rewritten as GGProto classes. "
|
|
2342
|
+
"The old S3 guide methods have been superseded.")
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
def guide_transform(guide: Any, coord: Any, panel_params: Any) -> Any:
|
|
2346
|
+
"""Legacy S3-style ``guide_transform`` dispatch.
|
|
2347
|
+
|
|
2348
|
+
Parameters
|
|
2349
|
+
----------
|
|
2350
|
+
guide : Guide
|
|
2351
|
+
The guide.
|
|
2352
|
+
coord : Coord
|
|
2353
|
+
Coordinate system.
|
|
2354
|
+
panel_params : object
|
|
2355
|
+
Panel parameters.
|
|
2356
|
+
|
|
2357
|
+
Returns
|
|
2358
|
+
-------
|
|
2359
|
+
Any
|
|
2360
|
+
Transformed parameters.
|
|
2361
|
+
"""
|
|
2362
|
+
if hasattr(guide, "transform"):
|
|
2363
|
+
return guide.transform(dict(getattr(guide, "params", {})),
|
|
2364
|
+
coord, panel_params)
|
|
2365
|
+
cli_abort("Guide classes have been rewritten as GGProto classes. "
|
|
2366
|
+
"The old S3 guide methods have been superseded.")
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
def guide_gengrob(guide: Any, theme: Any) -> Any:
|
|
2370
|
+
"""Legacy S3-style ``guide_gengrob`` dispatch.
|
|
2371
|
+
|
|
2372
|
+
Parameters
|
|
2373
|
+
----------
|
|
2374
|
+
guide : Guide
|
|
2375
|
+
The guide.
|
|
2376
|
+
theme : Theme
|
|
2377
|
+
Plot theme.
|
|
2378
|
+
|
|
2379
|
+
Returns
|
|
2380
|
+
-------
|
|
2381
|
+
Any
|
|
2382
|
+
Generated grob.
|
|
2383
|
+
"""
|
|
2384
|
+
if hasattr(guide, "draw"):
|
|
2385
|
+
return guide.draw(theme=theme)
|
|
2386
|
+
cli_abort("Guide classes have been rewritten as GGProto classes. "
|
|
2387
|
+
"The old S3 guide methods have been superseded.")
|
|
2388
|
+
|
|
2389
|
+
|
|
2390
|
+
# ============================================================================
|
|
2391
|
+
# Type-checking helpers
|
|
2392
|
+
# ============================================================================
|
|
2393
|
+
|
|
2394
|
+
def is_guide(x: Any) -> bool:
|
|
2395
|
+
"""Test whether *x* is a Guide.
|
|
2396
|
+
|
|
2397
|
+
Parameters
|
|
2398
|
+
----------
|
|
2399
|
+
x : Any
|
|
2400
|
+
Object to test.
|
|
2401
|
+
|
|
2402
|
+
Returns
|
|
2403
|
+
-------
|
|
2404
|
+
bool
|
|
2405
|
+
``True`` if *x* is a ``Guide`` instance or subclass.
|
|
2406
|
+
"""
|
|
2407
|
+
return isinstance(x, Guide) or (isinstance(x, type) and issubclass(x, Guide))
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
def is_guides(x: Any) -> bool:
|
|
2411
|
+
"""Test whether *x* is a Guides container.
|
|
2412
|
+
|
|
2413
|
+
Parameters
|
|
2414
|
+
----------
|
|
2415
|
+
x : Any
|
|
2416
|
+
Object to test.
|
|
2417
|
+
|
|
2418
|
+
Returns
|
|
2419
|
+
-------
|
|
2420
|
+
bool
|
|
2421
|
+
``True`` if *x* is a ``Guides`` instance.
|
|
2422
|
+
"""
|
|
2423
|
+
return isinstance(x, Guides)
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
# ============================================================================
|
|
2427
|
+
# Guides container class
|
|
2428
|
+
# ============================================================================
|
|
2429
|
+
|
|
2430
|
+
class Guides:
|
|
2431
|
+
"""Container for guide specifications by aesthetic.
|
|
2432
|
+
|
|
2433
|
+
A ``Guides`` object maps aesthetic names to guide objects (or strings
|
|
2434
|
+
that will be resolved to guide objects later). It manages the full
|
|
2435
|
+
lifecycle of merging, training, and assembling guides.
|
|
2436
|
+
|
|
2437
|
+
Parameters
|
|
2438
|
+
----------
|
|
2439
|
+
guide_map : dict, optional
|
|
2440
|
+
Initial mapping of aesthetic name -> guide specification.
|
|
2441
|
+
|
|
2442
|
+
Attributes
|
|
2443
|
+
----------
|
|
2444
|
+
guides : dict
|
|
2445
|
+
Aesthetic -> Guide mapping.
|
|
2446
|
+
params : list[dict]
|
|
2447
|
+
Parallel list of parameters for each guide.
|
|
2448
|
+
aesthetics : list[str]
|
|
2449
|
+
Parallel list of aesthetic names.
|
|
2450
|
+
"""
|
|
2451
|
+
|
|
2452
|
+
def __init__(self, guide_map: Optional[Dict[str, Any]] = None) -> None:
|
|
2453
|
+
self.guides: Dict[str, Any] = guide_map or {}
|
|
2454
|
+
self.params: List[Dict[str, Any]] = []
|
|
2455
|
+
self.aesthetics: List[str] = []
|
|
2456
|
+
self._missing: GuideNone = guide_none()
|
|
2457
|
+
|
|
2458
|
+
def __repr__(self) -> str:
|
|
2459
|
+
keys = list(self.guides.keys())
|
|
2460
|
+
return f"<Guides: {keys}>"
|
|
2461
|
+
|
|
2462
|
+
# -- Setters -------------------------------------------------------------
|
|
2463
|
+
|
|
2464
|
+
def add(self, guides: Any) -> None:
|
|
2465
|
+
"""Add new guides provided by the user.
|
|
2466
|
+
|
|
2467
|
+
Parameters
|
|
2468
|
+
----------
|
|
2469
|
+
guides : dict or Guides
|
|
2470
|
+
New guide specifications to incorporate. Existing entries
|
|
2471
|
+
are kept as defaults.
|
|
2472
|
+
"""
|
|
2473
|
+
if guides is None:
|
|
2474
|
+
return
|
|
2475
|
+
if isinstance(guides, Guides):
|
|
2476
|
+
guides = guides.guides
|
|
2477
|
+
self.guides = _defaults(guides, self.guides)
|
|
2478
|
+
|
|
2479
|
+
def update_params(self, params: List[Optional[Dict[str, Any]]]) -> None:
|
|
2480
|
+
"""Update guide parameters in place.
|
|
2481
|
+
|
|
2482
|
+
Parameters
|
|
2483
|
+
----------
|
|
2484
|
+
params : list of dict or None
|
|
2485
|
+
New parameter dicts. ``None`` entries replace the
|
|
2486
|
+
corresponding guide with ``guide_none()``.
|
|
2487
|
+
"""
|
|
2488
|
+
if len(params) != len(self.params):
|
|
2489
|
+
cli_abort(
|
|
2490
|
+
f"Cannot update {len(self.params)} guide(s) with a list of "
|
|
2491
|
+
f"{len(params)} parameter(s)."
|
|
2492
|
+
)
|
|
2493
|
+
for i, p in enumerate(params):
|
|
2494
|
+
if p is None:
|
|
2495
|
+
self.guides[i] = self._missing
|
|
2496
|
+
else:
|
|
2497
|
+
self.params[i] = p
|
|
2498
|
+
|
|
2499
|
+
def subset_guides(self, mask: List[bool]) -> None:
|
|
2500
|
+
"""Keep only guides where *mask* is ``True``.
|
|
2501
|
+
|
|
2502
|
+
Parameters
|
|
2503
|
+
----------
|
|
2504
|
+
mask : list of bool
|
|
2505
|
+
Boolean mask parallel to ``self.guides``.
|
|
2506
|
+
"""
|
|
2507
|
+
if isinstance(self.guides, dict):
|
|
2508
|
+
keys = list(self.guides.keys())
|
|
2509
|
+
self.guides = {k: v for k, keep in zip(keys, mask)
|
|
2510
|
+
for v in [self.guides[k]] if keep}
|
|
2511
|
+
elif isinstance(self.guides, list):
|
|
2512
|
+
self.guides = [g for g, keep in zip(self.guides, mask) if keep]
|
|
2513
|
+
self.params = [p for p, keep in zip(self.params, mask) if keep]
|
|
2514
|
+
self.aesthetics = [a for a, keep in zip(self.aesthetics, mask) if keep]
|
|
2515
|
+
|
|
2516
|
+
# -- Getters -------------------------------------------------------------
|
|
2517
|
+
|
|
2518
|
+
def get_guide(self, index: Union[int, str]) -> Optional[Any]:
|
|
2519
|
+
"""Retrieve a guide by index or aesthetic name.
|
|
2520
|
+
|
|
2521
|
+
Parameters
|
|
2522
|
+
----------
|
|
2523
|
+
index : int or str
|
|
2524
|
+
Index or aesthetic name.
|
|
2525
|
+
|
|
2526
|
+
Returns
|
|
2527
|
+
-------
|
|
2528
|
+
Guide or None
|
|
2529
|
+
The guide, or ``None`` if not found.
|
|
2530
|
+
"""
|
|
2531
|
+
if isinstance(index, str):
|
|
2532
|
+
if isinstance(self.guides, dict):
|
|
2533
|
+
return self.guides.get(index)
|
|
2534
|
+
if index in self.aesthetics:
|
|
2535
|
+
idx = self.aesthetics.index(index)
|
|
2536
|
+
guides_list = list(self.guides.values()) if isinstance(
|
|
2537
|
+
self.guides, dict) else self.guides
|
|
2538
|
+
return guides_list[idx]
|
|
2539
|
+
return None
|
|
2540
|
+
guides_list = list(self.guides.values()) if isinstance(
|
|
2541
|
+
self.guides, dict) else self.guides
|
|
2542
|
+
if 0 <= index < len(guides_list):
|
|
2543
|
+
return guides_list[index]
|
|
2544
|
+
return None
|
|
2545
|
+
|
|
2546
|
+
def get_params(self, index: Union[int, str]) -> Optional[Dict[str, Any]]:
|
|
2547
|
+
"""Retrieve parameters by index or aesthetic name.
|
|
2548
|
+
|
|
2549
|
+
Parameters
|
|
2550
|
+
----------
|
|
2551
|
+
index : int or str
|
|
2552
|
+
Index or aesthetic name.
|
|
2553
|
+
|
|
2554
|
+
Returns
|
|
2555
|
+
-------
|
|
2556
|
+
dict or None
|
|
2557
|
+
Parameters, or ``None`` if not found.
|
|
2558
|
+
"""
|
|
2559
|
+
if isinstance(index, str):
|
|
2560
|
+
if index in self.aesthetics:
|
|
2561
|
+
idx = self.aesthetics.index(index)
|
|
2562
|
+
return self.params[idx]
|
|
2563
|
+
return None
|
|
2564
|
+
if 0 <= index < len(self.params):
|
|
2565
|
+
return self.params[index]
|
|
2566
|
+
return None
|
|
2567
|
+
|
|
2568
|
+
# -- Building ------------------------------------------------------------
|
|
2569
|
+
|
|
2570
|
+
def setup(
|
|
2571
|
+
self,
|
|
2572
|
+
scales: List[Any],
|
|
2573
|
+
aesthetics: Optional[List[str]] = None,
|
|
2574
|
+
default: Any = None,
|
|
2575
|
+
missing: Any = None,
|
|
2576
|
+
) -> "Guides":
|
|
2577
|
+
"""Generate a guide for every scale-aesthetic pair.
|
|
2578
|
+
|
|
2579
|
+
Parameters
|
|
2580
|
+
----------
|
|
2581
|
+
scales : list
|
|
2582
|
+
Scale objects.
|
|
2583
|
+
aesthetics : list of str, optional
|
|
2584
|
+
Aesthetic names parallel to *scales*.
|
|
2585
|
+
default : Guide, optional
|
|
2586
|
+
Default guide when none is specified.
|
|
2587
|
+
missing : Guide, optional
|
|
2588
|
+
Guide for unresolvable entries.
|
|
2589
|
+
|
|
2590
|
+
Returns
|
|
2591
|
+
-------
|
|
2592
|
+
Guides
|
|
2593
|
+
A new ``Guides`` instance populated with resolved guides.
|
|
2594
|
+
"""
|
|
2595
|
+
if default is None:
|
|
2596
|
+
default = self._missing
|
|
2597
|
+
if missing is None:
|
|
2598
|
+
missing = self._missing
|
|
2599
|
+
if aesthetics is None:
|
|
2600
|
+
aesthetics = [getattr(s, "aesthetics", ["unknown"])[0] for s in scales]
|
|
2601
|
+
|
|
2602
|
+
new_guides: List[Any] = []
|
|
2603
|
+
for idx, scale in enumerate(scales):
|
|
2604
|
+
aes_name = aesthetics[idx]
|
|
2605
|
+
guide = self.guides.get(aes_name)
|
|
2606
|
+
|
|
2607
|
+
# Fallback hierarchy
|
|
2608
|
+
if guide is None:
|
|
2609
|
+
guide = getattr(scale, "guide", None)
|
|
2610
|
+
if guide is None or is_waiver(guide):
|
|
2611
|
+
guide = default
|
|
2612
|
+
if guide is None:
|
|
2613
|
+
guide = missing
|
|
2614
|
+
|
|
2615
|
+
# Resolve string names
|
|
2616
|
+
guide = _validate_guide(guide)
|
|
2617
|
+
|
|
2618
|
+
# Check compatibility
|
|
2619
|
+
if not isinstance(guide, GuideNone):
|
|
2620
|
+
scale_aes = getattr(scale, "aesthetics", [])
|
|
2621
|
+
if not any(a in ("x", "y") for a in scale_aes):
|
|
2622
|
+
scale_aes = list(scale_aes) + ["any"]
|
|
2623
|
+
available = getattr(guide, "available_aes", [])
|
|
2624
|
+
if not any(a in available for a in scale_aes):
|
|
2625
|
+
cli_warn(
|
|
2626
|
+
f"{snake_class(guide)} cannot be used for "
|
|
2627
|
+
f"{', '.join(scale_aes[:4])}."
|
|
2628
|
+
)
|
|
2629
|
+
guide = missing
|
|
2630
|
+
|
|
2631
|
+
new_guides.append(guide)
|
|
2632
|
+
|
|
2633
|
+
child = Guides()
|
|
2634
|
+
child.guides = new_guides
|
|
2635
|
+
child.params = [dict(getattr(g, "params", {})) for g in new_guides]
|
|
2636
|
+
child.aesthetics = list(aesthetics)
|
|
2637
|
+
return child
|
|
2638
|
+
|
|
2639
|
+
def train(self, scales: List[Any], labels: Dict[str, str]) -> None:
|
|
2640
|
+
"""Train each guide on its paired scale.
|
|
2641
|
+
|
|
2642
|
+
Parameters
|
|
2643
|
+
----------
|
|
2644
|
+
scales : list
|
|
2645
|
+
Scale objects, parallel to ``self.guides``.
|
|
2646
|
+
labels : dict
|
|
2647
|
+
Aesthetic -> label mapping.
|
|
2648
|
+
"""
|
|
2649
|
+
guides_list = list(self.guides) if isinstance(self.guides, dict) else self.guides
|
|
2650
|
+
new_params: List[Optional[Dict[str, Any]]] = []
|
|
2651
|
+
for i, (guide, scale) in enumerate(zip(guides_list, scales)):
|
|
2652
|
+
aes = self.aesthetics[i] if i < len(self.aesthetics) else ""
|
|
2653
|
+
p = guide.train(
|
|
2654
|
+
params=dict(self.params[i]) if i < len(self.params) else {},
|
|
2655
|
+
scale=scale,
|
|
2656
|
+
aesthetic=aes,
|
|
2657
|
+
title=labels.get(aes),
|
|
2658
|
+
)
|
|
2659
|
+
new_params.append(p)
|
|
2660
|
+
|
|
2661
|
+
# Filter out None (dropped guides)
|
|
2662
|
+
keep = [p is not None for p in new_params]
|
|
2663
|
+
self.params = [p for p in new_params if p is not None]
|
|
2664
|
+
self.guides = [g for g, k in zip(guides_list, keep) if k]
|
|
2665
|
+
self.aesthetics = [a for a, k in zip(self.aesthetics, keep) if k]
|
|
2666
|
+
|
|
2667
|
+
# Drop GuideNone entries
|
|
2668
|
+
keep_none = [not isinstance(g, GuideNone) for g in self.guides]
|
|
2669
|
+
self.subset_guides(keep_none)
|
|
2670
|
+
|
|
2671
|
+
def merge(self) -> None:
|
|
2672
|
+
"""Merge guides that encode the same information.
|
|
2673
|
+
|
|
2674
|
+
Groups guides by ``{order}_{hash}`` and merges groups with
|
|
2675
|
+
more than one member.
|
|
2676
|
+
"""
|
|
2677
|
+
if len(self.guides) <= 1:
|
|
2678
|
+
return
|
|
2679
|
+
|
|
2680
|
+
guides_list = list(self.guides) if isinstance(self.guides, dict) else self.guides
|
|
2681
|
+
|
|
2682
|
+
# Build hash keys
|
|
2683
|
+
orders = [p.get("order", 0) for p in self.params]
|
|
2684
|
+
orders = [99 if o == 0 else o for o in orders]
|
|
2685
|
+
hashes = [p.get("hash", "") for p in self.params]
|
|
2686
|
+
keys = [f"{o:02d}_{h}" for o, h in zip(orders, hashes)]
|
|
2687
|
+
|
|
2688
|
+
# Group by key
|
|
2689
|
+
groups: Dict[str, List[int]] = {}
|
|
2690
|
+
for i, key in enumerate(keys):
|
|
2691
|
+
groups.setdefault(key, []).append(i)
|
|
2692
|
+
|
|
2693
|
+
merged_guides: List[Any] = []
|
|
2694
|
+
merged_params: List[Dict[str, Any]] = []
|
|
2695
|
+
merged_aes: List[str] = []
|
|
2696
|
+
|
|
2697
|
+
for key in sorted(groups.keys()):
|
|
2698
|
+
indices = groups[key]
|
|
2699
|
+
if len(indices) == 1:
|
|
2700
|
+
idx = indices[0]
|
|
2701
|
+
merged_guides.append(guides_list[idx])
|
|
2702
|
+
merged_params.append(self.params[idx])
|
|
2703
|
+
merged_aes.append(self.aesthetics[idx])
|
|
2704
|
+
else:
|
|
2705
|
+
# Sequentially merge
|
|
2706
|
+
result = {
|
|
2707
|
+
"guide": guides_list[indices[0]],
|
|
2708
|
+
"params": dict(self.params[indices[0]]),
|
|
2709
|
+
}
|
|
2710
|
+
for idx in indices[1:]:
|
|
2711
|
+
result = result["guide"].merge(
|
|
2712
|
+
result["params"],
|
|
2713
|
+
guides_list[idx],
|
|
2714
|
+
dict(self.params[idx]),
|
|
2715
|
+
)
|
|
2716
|
+
merged_guides.append(result["guide"])
|
|
2717
|
+
merged_params.append(result["params"])
|
|
2718
|
+
merged_aes.append(self.aesthetics[indices[0]])
|
|
2719
|
+
|
|
2720
|
+
self.guides = merged_guides
|
|
2721
|
+
self.params = merged_params
|
|
2722
|
+
self.aesthetics = merged_aes
|
|
2723
|
+
|
|
2724
|
+
def process_layers(
|
|
2725
|
+
self,
|
|
2726
|
+
layers: List[Any],
|
|
2727
|
+
data: Optional[List[Any]] = None,
|
|
2728
|
+
theme: Any = None,
|
|
2729
|
+
) -> None:
|
|
2730
|
+
"""Let guides extract information from layers.
|
|
2731
|
+
|
|
2732
|
+
Parameters
|
|
2733
|
+
----------
|
|
2734
|
+
layers : list
|
|
2735
|
+
Plot layers.
|
|
2736
|
+
data : list, optional
|
|
2737
|
+
Layer data.
|
|
2738
|
+
theme : Theme, optional
|
|
2739
|
+
Plot theme.
|
|
2740
|
+
"""
|
|
2741
|
+
guides_list = list(self.guides) if isinstance(self.guides, dict) else self.guides
|
|
2742
|
+
new_params = []
|
|
2743
|
+
for guide, params in zip(guides_list, self.params):
|
|
2744
|
+
new_params.append(guide.process_layers(params, layers, data, theme))
|
|
2745
|
+
|
|
2746
|
+
keep = [p is not None for p in new_params]
|
|
2747
|
+
self.params = [p for p in new_params if p is not None]
|
|
2748
|
+
self.guides = [g for g, k in zip(guides_list, keep) if k]
|
|
2749
|
+
self.aesthetics = [a for a, k in zip(self.aesthetics, keep) if k]
|
|
2750
|
+
|
|
2751
|
+
def build(
|
|
2752
|
+
self,
|
|
2753
|
+
scales: Any,
|
|
2754
|
+
layers: List[Any],
|
|
2755
|
+
labels: Dict[str, str],
|
|
2756
|
+
layer_data: Optional[List[Any]] = None,
|
|
2757
|
+
theme: Any = None,
|
|
2758
|
+
) -> "Guides":
|
|
2759
|
+
"""Full guide build pipeline.
|
|
2760
|
+
|
|
2761
|
+
Parameters
|
|
2762
|
+
----------
|
|
2763
|
+
scales : ScalesList
|
|
2764
|
+
All scales from the plot.
|
|
2765
|
+
layers : list
|
|
2766
|
+
Plot layers.
|
|
2767
|
+
labels : dict
|
|
2768
|
+
Aesthetic -> label mapping.
|
|
2769
|
+
layer_data : list, optional
|
|
2770
|
+
Layer data.
|
|
2771
|
+
theme : Theme, optional
|
|
2772
|
+
Plot theme.
|
|
2773
|
+
|
|
2774
|
+
Returns
|
|
2775
|
+
-------
|
|
2776
|
+
Guides
|
|
2777
|
+
Built guides ready for assembly.
|
|
2778
|
+
"""
|
|
2779
|
+
# Extract non-position scales
|
|
2780
|
+
if hasattr(scales, "non_position_scales"):
|
|
2781
|
+
scale_list = scales.non_position_scales()
|
|
2782
|
+
if hasattr(scale_list, "scales"):
|
|
2783
|
+
scale_list = scale_list.scales
|
|
2784
|
+
else:
|
|
2785
|
+
scale_list = scales if isinstance(scales, list) else []
|
|
2786
|
+
|
|
2787
|
+
if not scale_list:
|
|
2788
|
+
return Guides()
|
|
2789
|
+
|
|
2790
|
+
# Flatten aesthetics
|
|
2791
|
+
flat_scales = []
|
|
2792
|
+
flat_aes = []
|
|
2793
|
+
for s in scale_list:
|
|
2794
|
+
aes_names = getattr(s, "aesthetics", ["unknown"])
|
|
2795
|
+
for a in aes_names:
|
|
2796
|
+
flat_scales.append(s)
|
|
2797
|
+
flat_aes.append(a)
|
|
2798
|
+
|
|
2799
|
+
guides = self.setup(flat_scales, aesthetics=flat_aes)
|
|
2800
|
+
guides.train(flat_scales, labels)
|
|
2801
|
+
|
|
2802
|
+
if not guides.guides:
|
|
2803
|
+
return Guides()
|
|
2804
|
+
|
|
2805
|
+
guides.merge()
|
|
2806
|
+
guides.process_layers(layers, layer_data, theme)
|
|
2807
|
+
return guides
|
|
2808
|
+
|
|
2809
|
+
def draw(
|
|
2810
|
+
self,
|
|
2811
|
+
theme: Any,
|
|
2812
|
+
positions: List[str],
|
|
2813
|
+
direction: Optional[str] = None,
|
|
2814
|
+
) -> List[Any]:
|
|
2815
|
+
"""Render guides into grobs.
|
|
2816
|
+
|
|
2817
|
+
Parameters
|
|
2818
|
+
----------
|
|
2819
|
+
theme : Theme
|
|
2820
|
+
Plot theme.
|
|
2821
|
+
positions : list of str
|
|
2822
|
+
Position for each guide.
|
|
2823
|
+
direction : str, optional
|
|
2824
|
+
Default direction.
|
|
2825
|
+
|
|
2826
|
+
Returns
|
|
2827
|
+
-------
|
|
2828
|
+
list
|
|
2829
|
+
Rendered grobs.
|
|
2830
|
+
"""
|
|
2831
|
+
guides_list = list(self.guides) if isinstance(self.guides, dict) else self.guides
|
|
2832
|
+
directions = [direction or "vertical"] * len(positions)
|
|
2833
|
+
for i, pos in enumerate(positions):
|
|
2834
|
+
if direction is None and pos in ("top", "bottom"):
|
|
2835
|
+
directions[i] = "horizontal"
|
|
2836
|
+
|
|
2837
|
+
grobs = []
|
|
2838
|
+
for i, guide in enumerate(guides_list):
|
|
2839
|
+
g = guide.draw(
|
|
2840
|
+
theme=theme,
|
|
2841
|
+
position=positions[i],
|
|
2842
|
+
direction=directions[i],
|
|
2843
|
+
params=self.params[i] if i < len(self.params) else None,
|
|
2844
|
+
)
|
|
2845
|
+
grobs.append(g)
|
|
2846
|
+
return grobs
|
|
2847
|
+
|
|
2848
|
+
def assemble(self, theme: Any) -> Any:
|
|
2849
|
+
"""Assemble all guides into positioned guide boxes.
|
|
2850
|
+
|
|
2851
|
+
Parameters
|
|
2852
|
+
----------
|
|
2853
|
+
theme : Theme
|
|
2854
|
+
Plot theme.
|
|
2855
|
+
|
|
2856
|
+
Returns
|
|
2857
|
+
-------
|
|
2858
|
+
dict
|
|
2859
|
+
Mapping of position -> grob/gtable.
|
|
2860
|
+
"""
|
|
2861
|
+
if not self.guides:
|
|
2862
|
+
return None
|
|
2863
|
+
|
|
2864
|
+
default_position = "right"
|
|
2865
|
+
if hasattr(theme, "__getitem__"):
|
|
2866
|
+
try:
|
|
2867
|
+
default_position = theme["legend.position"] or "right"
|
|
2868
|
+
except (KeyError, TypeError):
|
|
2869
|
+
pass
|
|
2870
|
+
elif hasattr(theme, "legend_position"):
|
|
2871
|
+
default_position = theme.legend_position or "right"
|
|
2872
|
+
|
|
2873
|
+
positions = []
|
|
2874
|
+
for p in self.params:
|
|
2875
|
+
pos = p.get("position") or default_position
|
|
2876
|
+
if is_waiver(pos):
|
|
2877
|
+
pos = default_position
|
|
2878
|
+
positions.append(pos)
|
|
2879
|
+
|
|
2880
|
+
grobs = self.draw(theme, positions)
|
|
2881
|
+
return {pos: grob for pos, grob in zip(positions, grobs)}
|
|
2882
|
+
|
|
2883
|
+
|
|
2884
|
+
# ============================================================================
|
|
2885
|
+
# guides() -- user-facing function
|
|
2886
|
+
# ============================================================================
|
|
2887
|
+
|
|
2888
|
+
def guides(**kwargs: Any) -> Optional[Guides]:
|
|
2889
|
+
"""Set guides for each scale.
|
|
2890
|
+
|
|
2891
|
+
Parameters
|
|
2892
|
+
----------
|
|
2893
|
+
**kwargs : str or Guide
|
|
2894
|
+
Mapping of aesthetic name to guide specification. Values can
|
|
2895
|
+
be guide objects, constructor calls, or strings like
|
|
2896
|
+
``"legend"`` or ``"none"``.
|
|
2897
|
+
|
|
2898
|
+
Returns
|
|
2899
|
+
-------
|
|
2900
|
+
Guides or None
|
|
2901
|
+
A ``Guides`` container, or ``None`` if no guides were given.
|
|
2902
|
+
|
|
2903
|
+
Examples
|
|
2904
|
+
--------
|
|
2905
|
+
>>> guides(colour=guide_legend(), size="none")
|
|
2906
|
+
>>> guides(colour=guide_colourbar(nbin=50), shape=guide_legend(nrow=2))
|
|
2907
|
+
"""
|
|
2908
|
+
if not kwargs:
|
|
2909
|
+
return None
|
|
2910
|
+
|
|
2911
|
+
# Standardise aesthetic names
|
|
2912
|
+
standardised: Dict[str, Any] = {}
|
|
2913
|
+
for k, v in kwargs.items():
|
|
2914
|
+
names = standardise_aes_names([k])
|
|
2915
|
+
new_key = names[0] if names else k
|
|
2916
|
+
if v is False:
|
|
2917
|
+
warnings.warn(
|
|
2918
|
+
"Setting a guide to `False` is deprecated. Use 'none' instead.",
|
|
2919
|
+
FutureWarning,
|
|
2920
|
+
stacklevel=2,
|
|
2921
|
+
)
|
|
2922
|
+
v = "none"
|
|
2923
|
+
standardised[new_key] = v
|
|
2924
|
+
|
|
2925
|
+
return Guides(standardised)
|