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/layer.py
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layer: the core data structure combining geom, stat, and position.
|
|
3
|
+
|
|
4
|
+
A layer holds a geom, stat, position, data, mapping, and associated
|
|
5
|
+
parameters. Layers are typically created via ``geom_*`` or ``stat_*``
|
|
6
|
+
calls, but can also be assembled directly through ``layer()``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import (
|
|
12
|
+
Any,
|
|
13
|
+
Callable,
|
|
14
|
+
Dict,
|
|
15
|
+
List,
|
|
16
|
+
Optional,
|
|
17
|
+
Sequence,
|
|
18
|
+
Type,
|
|
19
|
+
Union,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
from ggplot2_py._compat import Waiver, is_waiver, waiver, cli_abort, cli_warn
|
|
26
|
+
from ggplot2_py.ggproto import GGProto, ggproto
|
|
27
|
+
from ggplot2_py.aes import (
|
|
28
|
+
Mapping,
|
|
29
|
+
standardise_aes_names,
|
|
30
|
+
AfterStat,
|
|
31
|
+
AfterScale,
|
|
32
|
+
Stage,
|
|
33
|
+
is_mapping,
|
|
34
|
+
rename_aes,
|
|
35
|
+
eval_aes_value,
|
|
36
|
+
)
|
|
37
|
+
from ggplot2_py._utils import (
|
|
38
|
+
remove_missing,
|
|
39
|
+
snake_class,
|
|
40
|
+
compact,
|
|
41
|
+
modify_list,
|
|
42
|
+
plyr_id,
|
|
43
|
+
data_frame,
|
|
44
|
+
empty,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Layer",
|
|
49
|
+
"layer",
|
|
50
|
+
"layer_sf",
|
|
51
|
+
"is_layer",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Helpers
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def _validate_subclass(
|
|
60
|
+
x: Any,
|
|
61
|
+
subclass: str,
|
|
62
|
+
registry: Optional[Dict[str, type]] = None,
|
|
63
|
+
) -> Any:
|
|
64
|
+
"""Validate and resolve *x* to a ggproto instance of *subclass*.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
x : str or GGProto
|
|
69
|
+
Either a string name (e.g. ``"point"``) that will be looked up, or
|
|
70
|
+
an existing ggproto object.
|
|
71
|
+
subclass : str
|
|
72
|
+
Expected base class name (``"Geom"``, ``"Stat"``, ``"Position"``).
|
|
73
|
+
registry : dict, optional
|
|
74
|
+
Name -> class mapping used for string lookup. If *None*, the
|
|
75
|
+
object must already be a ggproto instance.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
GGProto
|
|
80
|
+
The resolved object.
|
|
81
|
+
|
|
82
|
+
Raises
|
|
83
|
+
------
|
|
84
|
+
TypeError
|
|
85
|
+
If *x* cannot be resolved.
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(x, GGProto) or (isinstance(x, type) and issubclass(x, GGProto)):
|
|
88
|
+
return x
|
|
89
|
+
|
|
90
|
+
if isinstance(x, str):
|
|
91
|
+
if registry is not None and x in registry:
|
|
92
|
+
return registry[x]
|
|
93
|
+
# Try CamelCase class name lookup
|
|
94
|
+
camel = subclass + x.capitalize()
|
|
95
|
+
if registry is not None and camel in registry:
|
|
96
|
+
return registry[camel]
|
|
97
|
+
cli_abort(
|
|
98
|
+
f"Cannot find {subclass.lower()} called {x!r}.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
cli_abort(
|
|
102
|
+
f"Expected a string or {subclass} object, got {type(x).__name__}.",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _camelize(x: str, first: bool = False) -> str:
|
|
107
|
+
"""Convert a snake_case string to CamelCase (R's ``camelize()``).
|
|
108
|
+
|
|
109
|
+
Unlike Python's ``str.title()``, this only capitalises the letter
|
|
110
|
+
immediately after an underscore, preserving the case of characters
|
|
111
|
+
after digits (e.g. ``"bin2d"`` → ``"Bin2d"``, not ``"Bin2D"``).
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
x : str
|
|
116
|
+
The snake_case name (e.g. ``"bin2d"``, ``"count"``, ``"qq_line"``).
|
|
117
|
+
first : bool
|
|
118
|
+
If ``True``, also capitalise the very first character.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
str
|
|
123
|
+
CamelCase result.
|
|
124
|
+
"""
|
|
125
|
+
import re
|
|
126
|
+
x = re.sub(r"_(.)", lambda m: m.group(1).upper(), x)
|
|
127
|
+
if first:
|
|
128
|
+
x = x[0].upper() + x[1:] if x else x
|
|
129
|
+
return x
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _resolve_class(name: str, prefix: str) -> Any:
|
|
133
|
+
"""Resolve a string like ``"identity"`` to a ggproto class.
|
|
134
|
+
|
|
135
|
+
Resolution order:
|
|
136
|
+
|
|
137
|
+
1. **Registry lookup** — check the auto-registration registry
|
|
138
|
+
populated by ``__init_subclass__`` on :class:`Geom`, :class:`Stat`,
|
|
139
|
+
and :class:`Position`. This allows external extension packages to
|
|
140
|
+
register their classes simply by subclassing.
|
|
141
|
+
2. **Module lookup** — ``{prefix}{CamelName}`` (e.g. ``StatIdentity``)
|
|
142
|
+
in the corresponding module.
|
|
143
|
+
3. **Fallback** — exact attribute name in the module.
|
|
144
|
+
"""
|
|
145
|
+
import importlib
|
|
146
|
+
|
|
147
|
+
# 1. Registry lookup (includes external extensions)
|
|
148
|
+
module_map = {"Stat": "ggplot2_py.stat", "Geom": "ggplot2_py.geom", "Position": "ggplot2_py.position"}
|
|
149
|
+
mod = importlib.import_module(module_map[prefix])
|
|
150
|
+
base_cls = getattr(mod, prefix) # Stat, Geom, or Position
|
|
151
|
+
registry = getattr(base_cls, "_registry", {})
|
|
152
|
+
camel_name = _camelize(name, first=True)
|
|
153
|
+
for key in (camel_name, name, name.lower()):
|
|
154
|
+
if key in registry:
|
|
155
|
+
return registry[key]
|
|
156
|
+
|
|
157
|
+
# 2. Module attribute lookup (e.g. StatIdentity, GeomPoint)
|
|
158
|
+
class_name = prefix + camel_name
|
|
159
|
+
cls = getattr(mod, class_name, None)
|
|
160
|
+
if cls is not None:
|
|
161
|
+
return cls
|
|
162
|
+
|
|
163
|
+
# 3. Fallback: exact attribute name
|
|
164
|
+
cls = getattr(mod, name, None)
|
|
165
|
+
if cls is not None:
|
|
166
|
+
return cls
|
|
167
|
+
|
|
168
|
+
cli_abort(f"Cannot find {prefix.lower()} called {name!r}.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _split_params(
|
|
172
|
+
params: Dict[str, Any],
|
|
173
|
+
geom: Any,
|
|
174
|
+
stat: Any,
|
|
175
|
+
position: Any,
|
|
176
|
+
) -> tuple:
|
|
177
|
+
"""Split *params* into ``(geom_params, stat_params, aes_params)``.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
params : dict
|
|
182
|
+
Combined parameters passed to the layer.
|
|
183
|
+
geom, stat, position
|
|
184
|
+
The ggproto objects whose parameter/aesthetic names determine the
|
|
185
|
+
split.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
tuple of (dict, dict, dict)
|
|
190
|
+
``geom_params``, ``stat_params``, ``aes_params``.
|
|
191
|
+
"""
|
|
192
|
+
# Helper: call method on class or instance (ggproto objects blur the two)
|
|
193
|
+
def _try_call(obj: Any, method: str, *args: Any) -> Optional[set]:
|
|
194
|
+
# If obj is a class, instantiate it first so instance methods work
|
|
195
|
+
if isinstance(obj, type):
|
|
196
|
+
try:
|
|
197
|
+
obj = obj()
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
fn = getattr(obj, method, None)
|
|
201
|
+
if fn is None or not callable(fn):
|
|
202
|
+
return None
|
|
203
|
+
try:
|
|
204
|
+
return set(fn(*args))
|
|
205
|
+
except Exception:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
geom_aesthetics = _try_call(geom, "aesthetics") or set()
|
|
209
|
+
stat_aesthetics = _try_call(stat, "aesthetics") or set()
|
|
210
|
+
position_aesthetics = set()
|
|
211
|
+
if hasattr(position, "required_aes"):
|
|
212
|
+
position_aesthetics = set(getattr(position, "required_aes", ()))
|
|
213
|
+
|
|
214
|
+
all_aes = geom_aesthetics | stat_aesthetics | position_aesthetics
|
|
215
|
+
|
|
216
|
+
geom_param_names = _try_call(geom, "parameters", True) or set()
|
|
217
|
+
stat_param_names = _try_call(stat, "parameters", True) or set()
|
|
218
|
+
|
|
219
|
+
params = dict(rename_aes(params)) if isinstance(params, dict) else dict(params)
|
|
220
|
+
aes_params: Dict[str, Any] = {}
|
|
221
|
+
geom_params: Dict[str, Any] = {}
|
|
222
|
+
stat_params: Dict[str, Any] = {}
|
|
223
|
+
|
|
224
|
+
for k, v in params.items():
|
|
225
|
+
if k in all_aes:
|
|
226
|
+
aes_params[k] = v
|
|
227
|
+
elif k in geom_param_names:
|
|
228
|
+
geom_params[k] = v
|
|
229
|
+
elif k in stat_param_names:
|
|
230
|
+
stat_params[k] = v
|
|
231
|
+
else:
|
|
232
|
+
# Unknown params go to geom_params by default
|
|
233
|
+
geom_params[k] = v
|
|
234
|
+
|
|
235
|
+
return geom_params, stat_params, aes_params
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Layer class
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
class Layer(GGProto):
|
|
243
|
+
"""The Layer ggproto class.
|
|
244
|
+
|
|
245
|
+
A Layer holds the Geom, Stat and Position trifecta together with data,
|
|
246
|
+
mapping, and parameter state. It is responsible for managing data flow
|
|
247
|
+
during ``ggplot_build`` and producing grobs during ``ggplot_gtable``.
|
|
248
|
+
|
|
249
|
+
Attributes
|
|
250
|
+
----------
|
|
251
|
+
geom : GGProto or None
|
|
252
|
+
Geom ggproto object.
|
|
253
|
+
stat : GGProto or None
|
|
254
|
+
Stat ggproto object.
|
|
255
|
+
position : GGProto or None
|
|
256
|
+
Position ggproto object.
|
|
257
|
+
data : pd.DataFrame, callable, Waiver, or None
|
|
258
|
+
Layer data.
|
|
259
|
+
mapping : Mapping or None
|
|
260
|
+
Aesthetic mapping for this layer.
|
|
261
|
+
computed_mapping : Mapping or None
|
|
262
|
+
Final mapping (may include inherited plot mapping).
|
|
263
|
+
aes_params : dict
|
|
264
|
+
Fixed aesthetic parameters.
|
|
265
|
+
geom_params : dict
|
|
266
|
+
Parameters for the geom.
|
|
267
|
+
stat_params : dict
|
|
268
|
+
Parameters for the stat.
|
|
269
|
+
computed_geom_params : dict or None
|
|
270
|
+
Geom parameters after ``Geom.setup_params``.
|
|
271
|
+
computed_stat_params : dict or None
|
|
272
|
+
Stat parameters after ``Stat.setup_params``.
|
|
273
|
+
inherit_aes : bool
|
|
274
|
+
Whether to inherit the plot-level mapping.
|
|
275
|
+
show_legend : bool or None
|
|
276
|
+
Whether to include this layer in the legend.
|
|
277
|
+
key_glyph : callable or None
|
|
278
|
+
Custom legend key drawing function.
|
|
279
|
+
name : str or None
|
|
280
|
+
Optional layer name.
|
|
281
|
+
layout : Any
|
|
282
|
+
Layout specification for the layer.
|
|
283
|
+
constructor : str or None
|
|
284
|
+
Name of the user-facing constructor, for error messaging.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
# Fields ----------------------------------------------------------------
|
|
288
|
+
constructor: Optional[str] = None
|
|
289
|
+
geom: Any = None
|
|
290
|
+
stat: Any = None
|
|
291
|
+
position: Any = None
|
|
292
|
+
data: Any = None
|
|
293
|
+
mapping: Optional[Mapping] = None
|
|
294
|
+
computed_mapping: Optional[Mapping] = None
|
|
295
|
+
aes_params: Dict[str, Any] = {}
|
|
296
|
+
geom_params: Dict[str, Any] = {}
|
|
297
|
+
stat_params: Dict[str, Any] = {}
|
|
298
|
+
computed_geom_params: Optional[Dict[str, Any]] = None
|
|
299
|
+
computed_stat_params: Optional[Dict[str, Any]] = None
|
|
300
|
+
inherit_aes: bool = True
|
|
301
|
+
show_legend: Optional[bool] = None
|
|
302
|
+
key_glyph: Any = None
|
|
303
|
+
name: Optional[str] = None
|
|
304
|
+
layout: Any = None
|
|
305
|
+
|
|
306
|
+
# Methods ---------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def layer_data(self, plot_data: Optional[pd.DataFrame]) -> Optional[pd.DataFrame]:
|
|
309
|
+
"""Resolve layer data against the global plot data.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
plot_data : pd.DataFrame or None
|
|
314
|
+
The ``data`` field of the ggplot object.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
pd.DataFrame or None
|
|
319
|
+
Resolved data for this layer.
|
|
320
|
+
"""
|
|
321
|
+
if is_waiver(self.data):
|
|
322
|
+
data = plot_data
|
|
323
|
+
elif callable(self.data):
|
|
324
|
+
data = self.data(plot_data)
|
|
325
|
+
if not isinstance(data, pd.DataFrame):
|
|
326
|
+
cli_abort("layer_data() must return a DataFrame.")
|
|
327
|
+
else:
|
|
328
|
+
data = self.data
|
|
329
|
+
|
|
330
|
+
if data is None or is_waiver(data):
|
|
331
|
+
return data
|
|
332
|
+
# Strip row names / reset index
|
|
333
|
+
if isinstance(data, pd.DataFrame):
|
|
334
|
+
data = data.reset_index(drop=True)
|
|
335
|
+
return data
|
|
336
|
+
|
|
337
|
+
def setup_layer(self, data: pd.DataFrame, plot: Any) -> pd.DataFrame:
|
|
338
|
+
"""Prepare layer data and finalise the mapping.
|
|
339
|
+
|
|
340
|
+
Merges the layer mapping with the global plot mapping when
|
|
341
|
+
``inherit_aes`` is True and stores the result in
|
|
342
|
+
``computed_mapping``.
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
data : pd.DataFrame
|
|
347
|
+
Layer data.
|
|
348
|
+
plot : object
|
|
349
|
+
The ggplot object (provides ``mapping``).
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
pd.DataFrame
|
|
354
|
+
Possibly-modified layer data.
|
|
355
|
+
"""
|
|
356
|
+
if self.inherit_aes:
|
|
357
|
+
plot_mapping = getattr(plot, "mapping", None) or {}
|
|
358
|
+
if self.mapping is not None:
|
|
359
|
+
# Layer mapping overrides plot mapping
|
|
360
|
+
merged = dict(plot_mapping)
|
|
361
|
+
merged.update(self.mapping)
|
|
362
|
+
self.computed_mapping = Mapping(merged) if isinstance(merged, dict) else merged
|
|
363
|
+
else:
|
|
364
|
+
self.computed_mapping = (
|
|
365
|
+
Mapping(plot_mapping) if isinstance(plot_mapping, dict) else plot_mapping
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
self.computed_mapping = self.mapping
|
|
369
|
+
return data
|
|
370
|
+
|
|
371
|
+
def compute_aesthetics(self, data: pd.DataFrame, plot: Any) -> pd.DataFrame:
|
|
372
|
+
"""Evaluate aesthetic mappings against the data.
|
|
373
|
+
|
|
374
|
+
Evaluates column references in the mapping, infers a ``group``
|
|
375
|
+
aesthetic if absent, and sets the ``PANEL`` column.
|
|
376
|
+
|
|
377
|
+
Parameters
|
|
378
|
+
----------
|
|
379
|
+
data : pd.DataFrame
|
|
380
|
+
Layer data.
|
|
381
|
+
plot : object
|
|
382
|
+
The ggplot object.
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
pd.DataFrame
|
|
387
|
+
Data with evaluated aesthetics.
|
|
388
|
+
"""
|
|
389
|
+
aesthetics = self.computed_mapping or {}
|
|
390
|
+
|
|
391
|
+
# Remove aesthetics that are set as fixed params
|
|
392
|
+
set_aes = set(self.aes_params.keys()) if self.aes_params else set()
|
|
393
|
+
aesthetics = {k: v for k, v in aesthetics.items() if k not in set_aes}
|
|
394
|
+
|
|
395
|
+
# Evaluate aesthetics: skip deferred (AfterStat/AfterScale),
|
|
396
|
+
# evaluate Stage.start at this stage, evaluate callables & strings.
|
|
397
|
+
evaluated: Dict[str, Any] = {}
|
|
398
|
+
for aes_name, aes_val in aesthetics.items():
|
|
399
|
+
if isinstance(aes_val, (AfterStat, AfterScale)):
|
|
400
|
+
# Deferred to later pipeline stages
|
|
401
|
+
continue
|
|
402
|
+
if isinstance(aes_val, Stage):
|
|
403
|
+
# Stage: evaluate .start at Stage 1, but skip if .start
|
|
404
|
+
# is itself a deferred type (AfterStat/AfterScale).
|
|
405
|
+
start_val = aes_val.start
|
|
406
|
+
if start_val is not None and not isinstance(
|
|
407
|
+
start_val, (AfterStat, AfterScale)
|
|
408
|
+
):
|
|
409
|
+
result = eval_aes_value(start_val, data)
|
|
410
|
+
if result is not None:
|
|
411
|
+
evaluated[aes_name] = result
|
|
412
|
+
continue
|
|
413
|
+
# str column ref, callable, or scalar
|
|
414
|
+
result = eval_aes_value(aes_val, data)
|
|
415
|
+
if result is not None:
|
|
416
|
+
evaluated[aes_name] = result
|
|
417
|
+
|
|
418
|
+
n = len(data)
|
|
419
|
+
if n == 0 and evaluated:
|
|
420
|
+
lengths = [
|
|
421
|
+
len(v) if hasattr(v, "__len__") and not isinstance(v, str) else 1
|
|
422
|
+
for v in evaluated.values()
|
|
423
|
+
]
|
|
424
|
+
n = max(lengths) if lengths else 0
|
|
425
|
+
|
|
426
|
+
# Build result DataFrame
|
|
427
|
+
result_dict: Dict[str, Any] = {}
|
|
428
|
+
for k, v in evaluated.items():
|
|
429
|
+
if np.isscalar(v) or isinstance(v, str):
|
|
430
|
+
result_dict[k] = np.repeat(v, n)
|
|
431
|
+
elif hasattr(v, "__len__") and len(v) == n:
|
|
432
|
+
result_dict[k] = v
|
|
433
|
+
elif hasattr(v, "__len__") and len(v) == 1:
|
|
434
|
+
result_dict[k] = np.repeat(v[0] if hasattr(v, "__getitem__") else v, n)
|
|
435
|
+
else:
|
|
436
|
+
result_dict[k] = v
|
|
437
|
+
|
|
438
|
+
# PANEL
|
|
439
|
+
if empty(data) and n > 0:
|
|
440
|
+
result_dict["PANEL"] = np.ones(n, dtype=int)
|
|
441
|
+
elif "PANEL" in data.columns:
|
|
442
|
+
result_dict["PANEL"] = data["PANEL"].values
|
|
443
|
+
|
|
444
|
+
result = pd.DataFrame(result_dict)
|
|
445
|
+
|
|
446
|
+
# Add group if missing
|
|
447
|
+
if "group" not in result.columns:
|
|
448
|
+
result = _add_group(result)
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
def compute_statistic(
|
|
453
|
+
self, data: pd.DataFrame, layout: Any
|
|
454
|
+
) -> pd.DataFrame:
|
|
455
|
+
"""Compute statistics for this layer.
|
|
456
|
+
|
|
457
|
+
Delegates to ``Stat.setup_params``, ``Stat.setup_data``,
|
|
458
|
+
and ``Stat.compute_layer``.
|
|
459
|
+
|
|
460
|
+
Parameters
|
|
461
|
+
----------
|
|
462
|
+
data : pd.DataFrame
|
|
463
|
+
Layer data.
|
|
464
|
+
layout : object
|
|
465
|
+
Layout ggproto object.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
pd.DataFrame
|
|
470
|
+
Data with computed stat columns.
|
|
471
|
+
"""
|
|
472
|
+
if empty(data):
|
|
473
|
+
return pd.DataFrame()
|
|
474
|
+
|
|
475
|
+
stat = self.stat
|
|
476
|
+
self.computed_stat_params = stat.setup_params(data, self.stat_params)
|
|
477
|
+
data = stat.setup_data(data, self.computed_stat_params)
|
|
478
|
+
data = stat.compute_layer(data, self.computed_stat_params, layout)
|
|
479
|
+
return data
|
|
480
|
+
|
|
481
|
+
def map_statistic(self, data: pd.DataFrame, plot: Any) -> pd.DataFrame:
|
|
482
|
+
"""Map computed-stat output aesthetics back to the data.
|
|
483
|
+
|
|
484
|
+
Evaluates ``after_stat()`` mappings from both the layer and the
|
|
485
|
+
stat default aesthetics.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
data : pd.DataFrame
|
|
490
|
+
Layer data after ``compute_statistic``.
|
|
491
|
+
plot : object
|
|
492
|
+
The ggplot object.
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
pd.DataFrame
|
|
497
|
+
Data with stat-mapped columns.
|
|
498
|
+
"""
|
|
499
|
+
if empty(data):
|
|
500
|
+
return pd.DataFrame()
|
|
501
|
+
|
|
502
|
+
# Merge computed_mapping with stat defaults
|
|
503
|
+
aesthetics = dict(self.computed_mapping or {})
|
|
504
|
+
stat_defaults = getattr(self.stat, "default_aes", {}) or {}
|
|
505
|
+
for k, v in stat_defaults.items():
|
|
506
|
+
if k not in aesthetics:
|
|
507
|
+
aesthetics[k] = v
|
|
508
|
+
aesthetics = compact(aesthetics)
|
|
509
|
+
|
|
510
|
+
# Evaluate AfterStat mappings (R ref: layer.R:632-668,
|
|
511
|
+
# uses eval_aesthetics with mask=list(stage=stage_calculated)).
|
|
512
|
+
# In R, stage() calls are substituted with stage_calculated() at
|
|
513
|
+
# this phase, which returns the after_stat slot.
|
|
514
|
+
new_cols: Dict[str, Any] = {}
|
|
515
|
+
for aes_name, aes_val in aesthetics.items():
|
|
516
|
+
if isinstance(aes_val, AfterStat):
|
|
517
|
+
# str or callable inside AfterStat
|
|
518
|
+
result = eval_aes_value(aes_val.x, data)
|
|
519
|
+
if result is not None:
|
|
520
|
+
new_cols[aes_name] = result
|
|
521
|
+
elif isinstance(aes_val, Stage):
|
|
522
|
+
# Stage: prefer .after_stat, then fall back to .start
|
|
523
|
+
# if .start is itself an AfterStat.
|
|
524
|
+
target_obj = aes_val.after_stat
|
|
525
|
+
if target_obj is None and isinstance(aes_val.start, AfterStat):
|
|
526
|
+
target_obj = aes_val.start
|
|
527
|
+
if target_obj is not None:
|
|
528
|
+
target = target_obj.x if isinstance(target_obj, AfterStat) else target_obj
|
|
529
|
+
result = eval_aes_value(target, data)
|
|
530
|
+
if result is not None:
|
|
531
|
+
new_cols[aes_name] = result
|
|
532
|
+
|
|
533
|
+
for k, v in new_cols.items():
|
|
534
|
+
data[k] = v
|
|
535
|
+
|
|
536
|
+
return data
|
|
537
|
+
|
|
538
|
+
def compute_geom_1(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
539
|
+
"""Prepare data for drawing (geom setup).
|
|
540
|
+
|
|
541
|
+
Checks required aesthetics and delegates to
|
|
542
|
+
``Geom.setup_params`` and ``Geom.setup_data``.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
data : pd.DataFrame
|
|
547
|
+
Layer data.
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
pd.DataFrame
|
|
552
|
+
Data after geom setup.
|
|
553
|
+
"""
|
|
554
|
+
if empty(data):
|
|
555
|
+
return pd.DataFrame()
|
|
556
|
+
|
|
557
|
+
geom = self.geom
|
|
558
|
+
# Check required aesthetics
|
|
559
|
+
required = getattr(geom, "REQUIRED_AES", None) or getattr(geom, "required_aes", ())
|
|
560
|
+
if required:
|
|
561
|
+
present = set(data.columns) | set(self.aes_params.keys())
|
|
562
|
+
for req in required:
|
|
563
|
+
alternatives = req.split("|")
|
|
564
|
+
if not any(a in present for a in alternatives):
|
|
565
|
+
cli_abort(
|
|
566
|
+
f"{snake_class(geom)} requires the following missing "
|
|
567
|
+
f"aesthetics: {req}"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
all_params = dict(self.geom_params)
|
|
571
|
+
all_params.update(self.aes_params)
|
|
572
|
+
self.computed_geom_params = geom.setup_params(data, all_params)
|
|
573
|
+
data = geom.setup_data(data, self.computed_geom_params)
|
|
574
|
+
return data
|
|
575
|
+
|
|
576
|
+
def compute_position(
|
|
577
|
+
self, data: pd.DataFrame, layout: Any
|
|
578
|
+
) -> pd.DataFrame:
|
|
579
|
+
"""Apply position adjustment.
|
|
580
|
+
|
|
581
|
+
Delegates to ``Position.use_defaults``, ``Position.setup_params``,
|
|
582
|
+
``Position.setup_data``, and ``Position.compute_layer``.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
data : pd.DataFrame
|
|
587
|
+
Layer data.
|
|
588
|
+
layout : object
|
|
589
|
+
Layout ggproto object.
|
|
590
|
+
|
|
591
|
+
Returns
|
|
592
|
+
-------
|
|
593
|
+
pd.DataFrame
|
|
594
|
+
Position-adjusted data.
|
|
595
|
+
"""
|
|
596
|
+
if empty(data):
|
|
597
|
+
return pd.DataFrame()
|
|
598
|
+
|
|
599
|
+
pos = self.position
|
|
600
|
+
if hasattr(pos, "use_defaults"):
|
|
601
|
+
data = pos.use_defaults(data, self.aes_params)
|
|
602
|
+
params = pos.setup_params(data)
|
|
603
|
+
data = pos.setup_data(data, params)
|
|
604
|
+
data = pos.compute_layer(data, params, layout)
|
|
605
|
+
return data
|
|
606
|
+
|
|
607
|
+
def compute_geom_2(
|
|
608
|
+
self,
|
|
609
|
+
data: pd.DataFrame,
|
|
610
|
+
params: Optional[Dict[str, Any]] = None,
|
|
611
|
+
theme: Any = None,
|
|
612
|
+
) -> pd.DataFrame:
|
|
613
|
+
"""Fill in default and fixed aesthetic values.
|
|
614
|
+
|
|
615
|
+
Wraps ``Geom.use_defaults``.
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
data : pd.DataFrame
|
|
620
|
+
Layer data.
|
|
621
|
+
params : dict, optional
|
|
622
|
+
Fixed aesthetic params. Defaults to ``self.aes_params``.
|
|
623
|
+
theme : object, optional
|
|
624
|
+
Theme object.
|
|
625
|
+
|
|
626
|
+
Returns
|
|
627
|
+
-------
|
|
628
|
+
pd.DataFrame
|
|
629
|
+
Data with defaults filled in.
|
|
630
|
+
"""
|
|
631
|
+
if params is None:
|
|
632
|
+
params = self.aes_params
|
|
633
|
+
if empty(data):
|
|
634
|
+
return data
|
|
635
|
+
|
|
636
|
+
geom = self.geom
|
|
637
|
+
if hasattr(geom, "use_defaults"):
|
|
638
|
+
modifiers = {}
|
|
639
|
+
if self.computed_mapping:
|
|
640
|
+
modifiers = {
|
|
641
|
+
k: v
|
|
642
|
+
for k, v in self.computed_mapping.items()
|
|
643
|
+
if isinstance(v, (AfterScale, Stage))
|
|
644
|
+
}
|
|
645
|
+
data = geom.use_defaults(data, params, modifiers, theme=theme)
|
|
646
|
+
return data
|
|
647
|
+
|
|
648
|
+
def finish_statistics(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
649
|
+
"""Apply the stat finish hook.
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
data : pd.DataFrame
|
|
654
|
+
Layer data.
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
pd.DataFrame
|
|
659
|
+
"""
|
|
660
|
+
if hasattr(self.stat, "finish_layer"):
|
|
661
|
+
return self.stat.finish_layer(data, self.computed_stat_params)
|
|
662
|
+
return data
|
|
663
|
+
|
|
664
|
+
def draw_geom(self, data: pd.DataFrame, layout: Any) -> list:
|
|
665
|
+
"""Produce grobs for every panel.
|
|
666
|
+
|
|
667
|
+
Delegates to ``Geom.handle_na`` and ``Geom.draw_layer``.
|
|
668
|
+
|
|
669
|
+
Parameters
|
|
670
|
+
----------
|
|
671
|
+
data : pd.DataFrame
|
|
672
|
+
Layer data.
|
|
673
|
+
layout : object
|
|
674
|
+
Layout ggproto object.
|
|
675
|
+
|
|
676
|
+
Returns
|
|
677
|
+
-------
|
|
678
|
+
list
|
|
679
|
+
A list of grobs, one per panel.
|
|
680
|
+
"""
|
|
681
|
+
if empty(data):
|
|
682
|
+
n = len(getattr(layout, "layout", pd.DataFrame()))
|
|
683
|
+
from grid_py import null_grob
|
|
684
|
+
return [null_grob()] * max(n, 1)
|
|
685
|
+
|
|
686
|
+
geom = self.geom
|
|
687
|
+
if hasattr(geom, "handle_na"):
|
|
688
|
+
data = geom.handle_na(data, self.computed_geom_params)
|
|
689
|
+
coord = getattr(layout, "coord", None)
|
|
690
|
+
return geom.draw_layer(data, self.computed_geom_params, layout, coord)
|
|
691
|
+
|
|
692
|
+
def __repr__(self) -> str:
|
|
693
|
+
parts = []
|
|
694
|
+
if self.mapping is not None:
|
|
695
|
+
parts.append(f"mapping: {self.mapping}")
|
|
696
|
+
if self.geom is not None:
|
|
697
|
+
parts.append(f"geom: {snake_class(self.geom)}")
|
|
698
|
+
if self.stat is not None:
|
|
699
|
+
parts.append(f"stat: {snake_class(self.stat)}")
|
|
700
|
+
if self.position is not None:
|
|
701
|
+
parts.append(f"position: {snake_class(self.position)}")
|
|
702
|
+
return "<Layer " + ", ".join(parts) + ">"
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# ---------------------------------------------------------------------------
|
|
706
|
+
# Group detection helper
|
|
707
|
+
# ---------------------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
def _add_group(data: pd.DataFrame) -> pd.DataFrame:
|
|
710
|
+
"""Infer a ``group`` column from discrete aesthetics.
|
|
711
|
+
|
|
712
|
+
Parameters
|
|
713
|
+
----------
|
|
714
|
+
data : pd.DataFrame
|
|
715
|
+
Data that may or may not contain a group column.
|
|
716
|
+
|
|
717
|
+
Returns
|
|
718
|
+
-------
|
|
719
|
+
pd.DataFrame
|
|
720
|
+
Data with a ``group`` column.
|
|
721
|
+
"""
|
|
722
|
+
if "group" in data.columns:
|
|
723
|
+
return data
|
|
724
|
+
|
|
725
|
+
# Identify discrete columns (object, category, bool)
|
|
726
|
+
disc_cols = [
|
|
727
|
+
c
|
|
728
|
+
for c in data.columns
|
|
729
|
+
if c != "PANEL"
|
|
730
|
+
and (
|
|
731
|
+
data[c].dtype == object
|
|
732
|
+
or hasattr(data[c], "cat")
|
|
733
|
+
or data[c].dtype == bool
|
|
734
|
+
)
|
|
735
|
+
]
|
|
736
|
+
if disc_cols:
|
|
737
|
+
# Create interaction of all discrete columns
|
|
738
|
+
if len(disc_cols) == 1:
|
|
739
|
+
groups = pd.Categorical(data[disc_cols[0]]).codes
|
|
740
|
+
else:
|
|
741
|
+
interaction = data[disc_cols].apply(
|
|
742
|
+
lambda row: "|".join(str(v) for v in row), axis=1
|
|
743
|
+
)
|
|
744
|
+
groups = pd.Categorical(interaction).codes
|
|
745
|
+
data = data.copy()
|
|
746
|
+
data["group"] = groups
|
|
747
|
+
else:
|
|
748
|
+
data = data.copy()
|
|
749
|
+
data["group"] = -1 # single group sentinel
|
|
750
|
+
return data
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
# ---------------------------------------------------------------------------
|
|
754
|
+
# layer() constructor
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
def layer(
|
|
758
|
+
geom: Any = None,
|
|
759
|
+
stat: Any = None,
|
|
760
|
+
data: Any = None,
|
|
761
|
+
mapping: Optional[Mapping] = None,
|
|
762
|
+
position: Any = None,
|
|
763
|
+
params: Optional[Dict[str, Any]] = None,
|
|
764
|
+
inherit_aes: bool = True,
|
|
765
|
+
check_aes: bool = True,
|
|
766
|
+
check_param: bool = True,
|
|
767
|
+
show_legend: Optional[bool] = None,
|
|
768
|
+
key_glyph: Any = None,
|
|
769
|
+
layout: Any = None,
|
|
770
|
+
layer_class: Type[Layer] = Layer,
|
|
771
|
+
**kwargs: Any,
|
|
772
|
+
) -> Layer:
|
|
773
|
+
"""Create a new layer.
|
|
774
|
+
|
|
775
|
+
Parameters
|
|
776
|
+
----------
|
|
777
|
+
geom : str or GGProto
|
|
778
|
+
Geom specification.
|
|
779
|
+
stat : str or GGProto
|
|
780
|
+
Stat specification.
|
|
781
|
+
data : DataFrame, callable, or None
|
|
782
|
+
Layer data.
|
|
783
|
+
mapping : Mapping or None
|
|
784
|
+
Aesthetic mapping.
|
|
785
|
+
position : str or GGProto
|
|
786
|
+
Position adjustment specification.
|
|
787
|
+
params : dict, optional
|
|
788
|
+
Combined geom/stat/aes parameters.
|
|
789
|
+
inherit_aes : bool
|
|
790
|
+
Whether to inherit the plot-level mapping.
|
|
791
|
+
check_aes : bool
|
|
792
|
+
Whether to check aesthetic validity.
|
|
793
|
+
check_param : bool
|
|
794
|
+
Whether to check parameter validity.
|
|
795
|
+
show_legend : bool or None
|
|
796
|
+
Whether to include in the legend.
|
|
797
|
+
key_glyph : callable or str or None
|
|
798
|
+
Legend key drawing function.
|
|
799
|
+
layout : Any
|
|
800
|
+
Layout specification.
|
|
801
|
+
layer_class : type
|
|
802
|
+
Class to instantiate. Defaults to :class:`Layer`.
|
|
803
|
+
**kwargs
|
|
804
|
+
Additional keyword arguments merged into *params*.
|
|
805
|
+
|
|
806
|
+
Returns
|
|
807
|
+
-------
|
|
808
|
+
Layer
|
|
809
|
+
A new Layer instance.
|
|
810
|
+
"""
|
|
811
|
+
if params is None:
|
|
812
|
+
params = {}
|
|
813
|
+
params.update(kwargs)
|
|
814
|
+
|
|
815
|
+
# Ensure na_rm default
|
|
816
|
+
params.setdefault("na_rm", False)
|
|
817
|
+
|
|
818
|
+
# Validate/resolve geom, stat, position
|
|
819
|
+
if geom is None:
|
|
820
|
+
geom = "blank"
|
|
821
|
+
if stat is None:
|
|
822
|
+
stat = "identity"
|
|
823
|
+
if position is None:
|
|
824
|
+
position = "identity"
|
|
825
|
+
|
|
826
|
+
# Resolve dict-form position (e.g. {"name": "jitter", "width": 0.2})
|
|
827
|
+
if isinstance(position, dict):
|
|
828
|
+
pos_name = position.pop("name", "identity")
|
|
829
|
+
pos_kwargs = position
|
|
830
|
+
position = pos_name
|
|
831
|
+
else:
|
|
832
|
+
pos_kwargs = {}
|
|
833
|
+
|
|
834
|
+
# Resolve string names to ggproto classes and ensure instances
|
|
835
|
+
if isinstance(stat, str):
|
|
836
|
+
stat = _resolve_class(stat, "Stat")
|
|
837
|
+
if isinstance(position, str):
|
|
838
|
+
position = _resolve_class(position, "Position")
|
|
839
|
+
if isinstance(geom, str):
|
|
840
|
+
geom = _resolve_class(geom, "Geom")
|
|
841
|
+
|
|
842
|
+
# Ensure we have instances, not classes (methods need bound self)
|
|
843
|
+
if isinstance(stat, type):
|
|
844
|
+
stat = stat()
|
|
845
|
+
if isinstance(position, type):
|
|
846
|
+
pos_inst = position()
|
|
847
|
+
# Apply any dict-form position kwargs
|
|
848
|
+
for k, v in pos_kwargs.items():
|
|
849
|
+
if v is not None:
|
|
850
|
+
setattr(pos_inst, k, v)
|
|
851
|
+
position = pos_inst
|
|
852
|
+
if isinstance(geom, type):
|
|
853
|
+
geom = geom()
|
|
854
|
+
|
|
855
|
+
# Split params
|
|
856
|
+
geom_params: Dict[str, Any]
|
|
857
|
+
stat_params: Dict[str, Any]
|
|
858
|
+
aes_params: Dict[str, Any]
|
|
859
|
+
|
|
860
|
+
if isinstance(geom, GGProto) or (isinstance(geom, type) and issubclass(geom, GGProto)):
|
|
861
|
+
geom_params, stat_params, aes_params = _split_params(
|
|
862
|
+
params, geom, stat, position
|
|
863
|
+
)
|
|
864
|
+
else:
|
|
865
|
+
# Deferred: put everything into geom_params for now
|
|
866
|
+
geom_params = dict(params)
|
|
867
|
+
stat_params = {}
|
|
868
|
+
aes_params = {}
|
|
869
|
+
|
|
870
|
+
# Instantiate
|
|
871
|
+
obj = object.__new__(layer_class)
|
|
872
|
+
# Copy class defaults
|
|
873
|
+
obj.constructor = None
|
|
874
|
+
obj.geom = geom
|
|
875
|
+
obj.stat = stat
|
|
876
|
+
obj.position = position
|
|
877
|
+
obj.data = waiver() if data is None else data
|
|
878
|
+
obj.mapping = mapping
|
|
879
|
+
obj.computed_mapping = None
|
|
880
|
+
obj.geom_params = geom_params
|
|
881
|
+
obj.stat_params = stat_params
|
|
882
|
+
obj.aes_params = aes_params
|
|
883
|
+
obj.computed_geom_params = None
|
|
884
|
+
obj.computed_stat_params = None
|
|
885
|
+
obj.inherit_aes = inherit_aes
|
|
886
|
+
obj.show_legend = show_legend
|
|
887
|
+
obj.key_glyph = key_glyph
|
|
888
|
+
obj.name = params.get("name")
|
|
889
|
+
obj.layout = layout or params.get("layout")
|
|
890
|
+
return obj
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def layer_sf(
|
|
894
|
+
geom: Any = None,
|
|
895
|
+
stat: Any = None,
|
|
896
|
+
data: Any = None,
|
|
897
|
+
mapping: Optional[Mapping] = None,
|
|
898
|
+
position: Any = None,
|
|
899
|
+
params: Optional[Dict[str, Any]] = None,
|
|
900
|
+
inherit_aes: bool = True,
|
|
901
|
+
check_aes: bool = True,
|
|
902
|
+
check_param: bool = True,
|
|
903
|
+
show_legend: Optional[bool] = None,
|
|
904
|
+
key_glyph: Any = None,
|
|
905
|
+
layout: Any = None,
|
|
906
|
+
**kwargs: Any,
|
|
907
|
+
) -> Layer:
|
|
908
|
+
"""Create a layer for sf (spatial) data.
|
|
909
|
+
|
|
910
|
+
This is a thin wrapper around :func:`layer` intended for use with
|
|
911
|
+
sf-type geometries.
|
|
912
|
+
|
|
913
|
+
Parameters
|
|
914
|
+
----------
|
|
915
|
+
See :func:`layer`.
|
|
916
|
+
|
|
917
|
+
Returns
|
|
918
|
+
-------
|
|
919
|
+
Layer
|
|
920
|
+
"""
|
|
921
|
+
return layer(
|
|
922
|
+
geom=geom,
|
|
923
|
+
stat=stat,
|
|
924
|
+
data=data,
|
|
925
|
+
mapping=mapping,
|
|
926
|
+
position=position,
|
|
927
|
+
params=params,
|
|
928
|
+
inherit_aes=inherit_aes,
|
|
929
|
+
check_aes=check_aes,
|
|
930
|
+
check_param=check_param,
|
|
931
|
+
show_legend=show_legend,
|
|
932
|
+
key_glyph=key_glyph,
|
|
933
|
+
layout=layout,
|
|
934
|
+
**kwargs,
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
# ---------------------------------------------------------------------------
|
|
939
|
+
# Predicate
|
|
940
|
+
# ---------------------------------------------------------------------------
|
|
941
|
+
|
|
942
|
+
def is_layer(x: Any) -> bool:
|
|
943
|
+
"""Test whether *x* is a Layer.
|
|
944
|
+
|
|
945
|
+
Parameters
|
|
946
|
+
----------
|
|
947
|
+
x : object
|
|
948
|
+
Object to test.
|
|
949
|
+
|
|
950
|
+
Returns
|
|
951
|
+
-------
|
|
952
|
+
bool
|
|
953
|
+
"""
|
|
954
|
+
return isinstance(x, Layer)
|