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.
Files changed (54) hide show
  1. ggplot2_py/__init__.py +852 -0
  2. ggplot2_py/_compat.py +475 -0
  3. ggplot2_py/_plugins.py +129 -0
  4. ggplot2_py/_utils.py +544 -0
  5. ggplot2_py/aes.py +586 -0
  6. ggplot2_py/annotation.py +540 -0
  7. ggplot2_py/coord.py +2108 -0
  8. ggplot2_py/coords/__init__.py +49 -0
  9. ggplot2_py/datasets.py +265 -0
  10. ggplot2_py/draw_key.py +454 -0
  11. ggplot2_py/facet.py +1456 -0
  12. ggplot2_py/fortify.py +95 -0
  13. ggplot2_py/geom.py +4516 -0
  14. ggplot2_py/geoms/__init__.py +12 -0
  15. ggplot2_py/ggproto.py +279 -0
  16. ggplot2_py/guide.py +2925 -0
  17. ggplot2_py/guide_axis.py +615 -0
  18. ggplot2_py/guide_colourbar.py +657 -0
  19. ggplot2_py/guide_legend.py +1061 -0
  20. ggplot2_py/guides/__init__.py +8 -0
  21. ggplot2_py/labeller.py +296 -0
  22. ggplot2_py/labels.py +309 -0
  23. ggplot2_py/layer.py +954 -0
  24. ggplot2_py/layout.py +754 -0
  25. ggplot2_py/limits.py +314 -0
  26. ggplot2_py/plot.py +1401 -0
  27. ggplot2_py/plot_render.py +866 -0
  28. ggplot2_py/position.py +1269 -0
  29. ggplot2_py/protocols.py +171 -0
  30. ggplot2_py/py.typed +0 -0
  31. ggplot2_py/qplot.py +233 -0
  32. ggplot2_py/resources/diamonds.csv +53941 -0
  33. ggplot2_py/resources/economics.csv +575 -0
  34. ggplot2_py/resources/economics_long.csv +2871 -0
  35. ggplot2_py/resources/faithfuld.csv +5626 -0
  36. ggplot2_py/resources/luv_colours.csv +658 -0
  37. ggplot2_py/resources/midwest.csv +438 -0
  38. ggplot2_py/resources/mpg.csv +235 -0
  39. ggplot2_py/resources/msleep.csv +84 -0
  40. ggplot2_py/resources/presidential.csv +13 -0
  41. ggplot2_py/resources/seals.csv +1156 -0
  42. ggplot2_py/resources/txhousing.csv +8603 -0
  43. ggplot2_py/save.py +316 -0
  44. ggplot2_py/scale.py +2727 -0
  45. ggplot2_py/scales/__init__.py +4252 -0
  46. ggplot2_py/stat.py +6071 -0
  47. ggplot2_py/stats/__init__.py +9 -0
  48. ggplot2_py/theme.py +490 -0
  49. ggplot2_py/theme_defaults.py +1350 -0
  50. ggplot2_py/theme_elements.py +2052 -0
  51. ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
  52. ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
  53. ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
  54. 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)