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/aes.py ADDED
@@ -0,0 +1,586 @@
1
+ """
2
+ Aesthetic mapping system for ggplot2_py.
3
+
4
+ This module implements the core aesthetic-mapping machinery that translates
5
+ user-specified column references (and computed-variable references) into a
6
+ structured :class:`Mapping` object consumed by layers, scales, and stats.
7
+
8
+ In R's ggplot2, ``aes()`` uses non-standard evaluation (quosures). In this
9
+ Python port we use plain strings for column names and the helper classes
10
+ :class:`AfterStat`, :class:`AfterScale`, and :class:`Stage` for deferred
11
+ references.
12
+
13
+ Examples
14
+ --------
15
+ >>> from ggplot2_py.aes import aes, after_stat, after_scale, stage
16
+ >>> aes(x="displ", y="hwy", colour="class")
17
+ >>> aes(x="displ", y=after_stat("count"))
18
+ >>> aes(colour=stage(start="class", after_scale="fill"))
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import (
24
+ Any,
25
+ Callable,
26
+ Dict,
27
+ Iterable,
28
+ List,
29
+ Optional,
30
+ Sequence,
31
+ Union,
32
+ )
33
+
34
+ __all__ = [
35
+ "aes",
36
+ "after_stat",
37
+ "after_scale",
38
+ "stage",
39
+ "vars",
40
+ "is_mapping",
41
+ "standardise_aes_names",
42
+ "rename_aes",
43
+ "Mapping",
44
+ "AfterStat",
45
+ "AfterScale",
46
+ "Stage",
47
+ "AESTHETIC_ALIASES",
48
+ "eval_aes_value",
49
+ ]
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Aesthetic-alias lookup (R name -> canonical ggplot2 name)
53
+ # ---------------------------------------------------------------------------
54
+
55
+ AESTHETIC_ALIASES: Dict[str, str] = {
56
+ "color": "colour",
57
+ "pch": "shape",
58
+ "cex": "size",
59
+ "lwd": "linewidth",
60
+ "lty": "linetype",
61
+ "bg": "fill",
62
+ }
63
+ """Mapping of common R aesthetic aliases to their canonical ggplot2 names."""
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Helper classes for deferred / staged aesthetics
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ class AfterStat:
71
+ """Reference to a variable computed by the stat layer.
72
+
73
+ Parameters
74
+ ----------
75
+ x : str or callable
76
+ Name of the computed-stat variable (e.g. ``"count"``, ``"density"``),
77
+ or a callable ``f(data: DataFrame) -> array`` evaluated after stat
78
+ computation.
79
+
80
+ Examples
81
+ --------
82
+ >>> AfterStat("count")
83
+ AfterStat('count')
84
+ >>> AfterStat(lambda d: d["count"] / d["count"].max())
85
+ AfterStat(<lambda>)
86
+ """
87
+
88
+ __slots__ = ("x",)
89
+
90
+ def __init__(self, x: Union[str, Callable[..., Any]]) -> None:
91
+ if not isinstance(x, str) and not callable(x):
92
+ raise TypeError(
93
+ f"AfterStat expects a str or callable, got {type(x).__name__}"
94
+ )
95
+ self.x = x
96
+
97
+ # -- repr / eq / hash ---------------------------------------------------
98
+
99
+ def __repr__(self) -> str:
100
+ if callable(self.x) and not isinstance(self.x, str):
101
+ name = getattr(self.x, "__name__", "<callable>")
102
+ return f"AfterStat({name})"
103
+ return f"AfterStat({self.x!r})"
104
+
105
+ def __eq__(self, other: object) -> bool:
106
+ if isinstance(other, AfterStat):
107
+ return self.x == other.x
108
+ return NotImplemented
109
+
110
+ def __hash__(self) -> int:
111
+ if callable(self.x) and not isinstance(self.x, str):
112
+ return hash(("AfterStat", id(self.x)))
113
+ return hash(("AfterStat", self.x))
114
+
115
+
116
+ class AfterScale:
117
+ """Reference to a variable available after scale transformation.
118
+
119
+ Parameters
120
+ ----------
121
+ x : str or callable
122
+ Name of the post-scale variable (e.g. ``"fill"``), or a callable
123
+ ``f(data: DataFrame) -> array`` evaluated after scale mapping.
124
+
125
+ Examples
126
+ --------
127
+ >>> AfterScale("fill")
128
+ AfterScale('fill')
129
+ >>> AfterScale(lambda d: alpha(d["colour"], 0.3))
130
+ AfterScale(<lambda>)
131
+ """
132
+
133
+ __slots__ = ("x",)
134
+
135
+ def __init__(self, x: Union[str, Callable[..., Any]]) -> None:
136
+ if not isinstance(x, str) and not callable(x):
137
+ raise TypeError(
138
+ f"AfterScale expects a str or callable, got {type(x).__name__}"
139
+ )
140
+ self.x = x
141
+
142
+ def __repr__(self) -> str:
143
+ if callable(self.x) and not isinstance(self.x, str):
144
+ name = getattr(self.x, "__name__", "<callable>")
145
+ return f"AfterScale({name})"
146
+ return f"AfterScale({self.x!r})"
147
+
148
+ def __eq__(self, other: object) -> bool:
149
+ if isinstance(other, AfterScale):
150
+ return self.x == other.x
151
+ return NotImplemented
152
+
153
+ def __hash__(self) -> int:
154
+ if callable(self.x) and not isinstance(self.x, str):
155
+ return hash(("AfterScale", id(self.x)))
156
+ return hash(("AfterScale", self.x))
157
+
158
+
159
+ class Stage:
160
+ """Staged aesthetic with potentially different values at each pipeline stage.
161
+
162
+ At most one of *start*, *after_stat*, or *after_scale* should be the
163
+ "primary" mapping; the others provide overrides for later stages.
164
+
165
+ Each slot accepts a column-name string, a callable
166
+ ``f(data: DataFrame) -> array``, or a wrapper object.
167
+
168
+ Parameters
169
+ ----------
170
+ start : str, callable, or None, optional
171
+ Column name or callable used at the initial data stage.
172
+ after_stat : str, callable, AfterStat, or None, optional
173
+ Reference used after the stat computation.
174
+ after_scale : str, callable, AfterScale, or None, optional
175
+ Reference used after scale transformation.
176
+
177
+ Examples
178
+ --------
179
+ >>> Stage(start="class", after_scale="fill")
180
+ Stage(start='class', after_stat=None, after_scale='fill')
181
+ >>> Stage(after_stat=lambda d: d["count"] / d["count"].max())
182
+ Stage(start=None, after_stat=AfterStat(<lambda>), after_scale=None)
183
+ """
184
+
185
+ __slots__ = ("start", "after_stat", "after_scale")
186
+
187
+ def __init__(
188
+ self,
189
+ start: Optional[Union[str, Callable[..., Any]]] = None,
190
+ after_stat: Optional[Union[str, Callable[..., Any], AfterStat]] = None,
191
+ after_scale: Optional[Union[str, Callable[..., Any], AfterScale]] = None,
192
+ ) -> None:
193
+ self.start = start
194
+
195
+ # Normalise bare strings and callables to wrapper objects.
196
+ if isinstance(after_stat, str) or (
197
+ callable(after_stat) and not isinstance(after_stat, AfterStat)
198
+ ):
199
+ after_stat = AfterStat(after_stat)
200
+ self.after_stat: Optional[AfterStat] = after_stat # type: ignore[assignment]
201
+
202
+ if isinstance(after_scale, str) or (
203
+ callable(after_scale) and not isinstance(after_scale, AfterScale)
204
+ ):
205
+ after_scale = AfterScale(after_scale)
206
+ self.after_scale: Optional[AfterScale] = after_scale # type: ignore[assignment]
207
+
208
+ def __repr__(self) -> str:
209
+ return (
210
+ f"Stage(start={self.start!r}, after_stat={self.after_stat!r}, "
211
+ f"after_scale={self.after_scale!r})"
212
+ )
213
+
214
+ def __eq__(self, other: object) -> bool:
215
+ if isinstance(other, Stage):
216
+ return (
217
+ self.start == other.start
218
+ and self.after_stat == other.after_stat
219
+ and self.after_scale == other.after_scale
220
+ )
221
+ return NotImplemented
222
+
223
+ def __hash__(self) -> int:
224
+ return hash(("Stage", self.start, self.after_stat, self.after_scale))
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Mapping class (dict subclass)
229
+ # ---------------------------------------------------------------------------
230
+
231
+ #: Type alias for a single aesthetic value.
232
+ AesValue = Union[str, AfterStat, AfterScale, Stage, Callable[..., Any], int, float]
233
+
234
+
235
+ class Mapping(dict):
236
+ """Aesthetic mapping object, analogous to the output of R's ``aes()``.
237
+
238
+ A thin :class:`dict` subclass whose keys are canonical aesthetic names
239
+ (e.g. ``"x"``, ``"colour"``) and whose values describe how to map data
240
+ to that aesthetic.
241
+
242
+ Values may be:
243
+
244
+ * **str** — column-name reference (most common).
245
+ * **AfterStat** — computed variable from a stat layer.
246
+ * **AfterScale** — variable available after scale transformation.
247
+ * **Stage** — staged aesthetic with per-stage overrides.
248
+ * **callable** — a lambda or function applied to the data.
249
+ * **scalar** (int, float, str) — constant aesthetic value.
250
+
251
+ Examples
252
+ --------
253
+ >>> m = Mapping(x="displ", y="hwy", colour="class")
254
+ >>> m["x"]
255
+ 'displ'
256
+ """
257
+
258
+ def __repr__(self) -> str:
259
+ inner = ", ".join(f"{k}={v!r}" for k, v in self.items())
260
+ return f"aes({inner})"
261
+
262
+ # Convenience: attribute access mirrors key access (read-only).
263
+ def __getattr__(self, name: str) -> Any:
264
+ try:
265
+ return self[name]
266
+ except KeyError:
267
+ raise AttributeError(
268
+ f"{type(self).__name__!r} object has no attribute {name!r}"
269
+ ) from None
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Centralised aesthetic-value evaluator
274
+ # ---------------------------------------------------------------------------
275
+
276
+ import numpy as np
277
+ import pandas as pd
278
+
279
+
280
+ def eval_aes_value(
281
+ val: Any,
282
+ data: "pd.DataFrame",
283
+ ) -> Any:
284
+ """Evaluate a single aesthetic value against a DataFrame.
285
+
286
+ This is the Python equivalent of R's ``eval_tidy(quo, data)`` pattern.
287
+ It handles the three kinds of aesthetic values that appear in
288
+ :class:`Mapping` objects:
289
+
290
+ * **str** — looked up as a column name in *data*.
291
+ * **callable** — called with *data* as the single argument.
292
+ * **scalar / other** — returned unchanged (will be broadcast by the
293
+ caller).
294
+
295
+ Parameters
296
+ ----------
297
+ val : str, callable, or scalar
298
+ The aesthetic value to evaluate.
299
+ data : DataFrame
300
+ The layer data to evaluate against.
301
+
302
+ Returns
303
+ -------
304
+ numpy array, scalar, or ``None``
305
+ ``None`` is returned when *val* is a string that does not match any
306
+ column in *data* (the column may appear in a later pipeline stage).
307
+ """
308
+ if callable(val) and not isinstance(val, (str, type)):
309
+ result = val(data)
310
+ if isinstance(result, pd.Series):
311
+ return result.values
312
+ return result
313
+ elif isinstance(val, str):
314
+ if val in data.columns:
315
+ return data[val].values
316
+ return None # column not yet available
317
+ else:
318
+ return val # scalar — broadcast by caller
319
+
320
+
321
+ # ---------------------------------------------------------------------------
322
+ # Public convenience constructors
323
+ # ---------------------------------------------------------------------------
324
+
325
+
326
+ def after_stat(x: Union[str, Callable[..., Any]]) -> AfterStat:
327
+ """Create an :class:`AfterStat` reference.
328
+
329
+ Parameters
330
+ ----------
331
+ x : str or callable
332
+ Name of a stat-computed variable (e.g. ``"count"``), or a callable
333
+ ``f(data) -> array`` to be evaluated after stat computation.
334
+
335
+ Returns
336
+ -------
337
+ AfterStat
338
+
339
+ Examples
340
+ --------
341
+ >>> after_stat("density")
342
+ AfterStat('density')
343
+ >>> after_stat(lambda d: d["count"] / d["count"].max())
344
+ AfterStat(<lambda>)
345
+ """
346
+ return AfterStat(x)
347
+
348
+
349
+ def after_scale(x: Union[str, Callable[..., Any]]) -> AfterScale:
350
+ """Create an :class:`AfterScale` reference.
351
+
352
+ Parameters
353
+ ----------
354
+ x : str or callable
355
+ Name of a post-scale variable (e.g. ``"fill"``), or a callable
356
+ ``f(data) -> array`` to be evaluated after scale mapping.
357
+
358
+ Returns
359
+ -------
360
+ AfterScale
361
+
362
+ Examples
363
+ --------
364
+ >>> after_scale("fill")
365
+ AfterScale('fill')
366
+ >>> after_scale(lambda d: d["colour"].str.replace("FF", "80"))
367
+ AfterScale(<lambda>)
368
+ """
369
+ return AfterScale(x)
370
+
371
+
372
+ def stage(
373
+ start: Optional[Union[str, Callable[..., Any]]] = None,
374
+ after_stat: Optional[Union[str, Callable[..., Any], AfterStat]] = None,
375
+ after_scale: Optional[Union[str, Callable[..., Any], AfterScale]] = None,
376
+ ) -> Stage:
377
+ """Create a :class:`Stage` aesthetic with per-stage overrides.
378
+
379
+ Parameters
380
+ ----------
381
+ start : str, callable, or None, optional
382
+ Column name or callable used at the initial data stage.
383
+ after_stat : str, callable, AfterStat, or None, optional
384
+ Reference used after the stat computation.
385
+ after_scale : str, callable, AfterScale, or None, optional
386
+ Reference used after scale transformation.
387
+
388
+ Returns
389
+ -------
390
+ Stage
391
+
392
+ Examples
393
+ --------
394
+ >>> stage(start="class", after_scale="fill")
395
+ Stage(start='class', after_stat=None, after_scale='fill')
396
+ """
397
+ return Stage(start=start, after_stat=after_stat, after_scale=after_scale)
398
+
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # aes() — main entry point
402
+ # ---------------------------------------------------------------------------
403
+
404
+
405
+ def aes(
406
+ x: Optional[AesValue] = None,
407
+ y: Optional[AesValue] = None,
408
+ **kwargs: AesValue,
409
+ ) -> Mapping:
410
+ """Create an aesthetic mapping.
411
+
412
+ This is the Python equivalent of R's ``aes()`` function. Column
413
+ references are passed as plain strings; deferred references use
414
+ :func:`after_stat`, :func:`after_scale`, or :func:`stage`.
415
+
416
+ Parameters
417
+ ----------
418
+ x : str, callable, or scalar, optional
419
+ Mapping for the *x* aesthetic.
420
+ y : str, callable, or scalar, optional
421
+ Mapping for the *y* aesthetic.
422
+ **kwargs
423
+ Additional aesthetic mappings. Names are automatically
424
+ standardised (e.g. ``color`` becomes ``colour``).
425
+
426
+ Returns
427
+ -------
428
+ Mapping
429
+ An aesthetic-mapping dictionary.
430
+
431
+ Examples
432
+ --------
433
+ >>> aes(x="displ", y="hwy")
434
+ aes(x='displ', y='hwy')
435
+
436
+ >>> aes(x="displ", y="hwy", color="class")
437
+ aes(x='displ', y='hwy', colour='class')
438
+
439
+ >>> aes(x="displ", y=after_stat("count"))
440
+ aes(x='displ', y=AfterStat('count'))
441
+ """
442
+ raw: Dict[str, AesValue] = {}
443
+ if x is not None:
444
+ raw["x"] = x
445
+ if y is not None:
446
+ raw["y"] = y
447
+ raw.update(kwargs)
448
+
449
+ # Standardise names (e.g. "color" -> "colour").
450
+ result = Mapping()
451
+ for key, value in raw.items():
452
+ canonical = _standardise_single(key)
453
+ result[canonical] = value
454
+
455
+ return result
456
+
457
+
458
+ # ---------------------------------------------------------------------------
459
+ # Name standardisation
460
+ # ---------------------------------------------------------------------------
461
+
462
+
463
+ def _standardise_single(name: str) -> str:
464
+ """Canonicalise a single aesthetic name.
465
+
466
+ Parameters
467
+ ----------
468
+ name : str
469
+ Raw aesthetic name.
470
+
471
+ Returns
472
+ -------
473
+ str
474
+ Canonical aesthetic name.
475
+ """
476
+ return AESTHETIC_ALIASES.get(name, name)
477
+
478
+
479
+ def standardise_aes_names(aes_names: Iterable[str]) -> List[str]:
480
+ """Standardise a sequence of aesthetic names to their canonical forms.
481
+
482
+ Parameters
483
+ ----------
484
+ aes_names : Iterable[str]
485
+ Aesthetic names, possibly containing aliases.
486
+
487
+ Returns
488
+ -------
489
+ list of str
490
+ Canonical aesthetic names in the same order.
491
+
492
+ Examples
493
+ --------
494
+ >>> standardise_aes_names(["color", "lwd", "x"])
495
+ ['colour', 'linewidth', 'x']
496
+ """
497
+ return [_standardise_single(n) for n in aes_names]
498
+
499
+
500
+ def rename_aes(mapping: Mapping) -> Mapping:
501
+ """Return a copy of *mapping* with all keys standardised.
502
+
503
+ Parameters
504
+ ----------
505
+ mapping : Mapping
506
+ An aesthetic mapping (or plain dict).
507
+
508
+ Returns
509
+ -------
510
+ Mapping
511
+ New mapping with canonical aesthetic names as keys.
512
+
513
+ Examples
514
+ --------
515
+ >>> rename_aes(Mapping(color="class"))
516
+ aes(colour='class')
517
+ """
518
+ return Mapping(
519
+ {_standardise_single(k): v for k, v in mapping.items()}
520
+ )
521
+
522
+
523
+ # ---------------------------------------------------------------------------
524
+ # vars() helper (for facets)
525
+ # ---------------------------------------------------------------------------
526
+
527
+
528
+ def vars(*args: str, **kwargs: str) -> List[str]:
529
+ """Specify variables for faceting.
530
+
531
+ A thin helper that collects positional and keyword variable names into a
532
+ flat list of strings, suitable for passing to ``facet_wrap`` or
533
+ ``facet_grid``.
534
+
535
+ Parameters
536
+ ----------
537
+ *args : str
538
+ Variable names given positionally.
539
+ **kwargs : str
540
+ Variable names given as keyword arguments (values are used, keys
541
+ are ignored).
542
+
543
+ Returns
544
+ -------
545
+ list of str
546
+ Flat list of variable-name strings.
547
+
548
+ Examples
549
+ --------
550
+ >>> vars("cyl", "drv")
551
+ ['cyl', 'drv']
552
+
553
+ >>> vars(rows="cyl")
554
+ ['cyl']
555
+ """
556
+ result: List[str] = list(args)
557
+ result.extend(kwargs.values())
558
+ return result
559
+
560
+
561
+ # ---------------------------------------------------------------------------
562
+ # Type guard
563
+ # ---------------------------------------------------------------------------
564
+
565
+
566
+ def is_mapping(x: Any) -> bool:
567
+ """Test whether *x* is an aesthetic :class:`Mapping`.
568
+
569
+ Parameters
570
+ ----------
571
+ x : object
572
+ Object to test.
573
+
574
+ Returns
575
+ -------
576
+ bool
577
+ ``True`` if *x* is a :class:`Mapping` instance.
578
+
579
+ Examples
580
+ --------
581
+ >>> is_mapping(aes(x="a"))
582
+ True
583
+ >>> is_mapping({"x": "a"})
584
+ False
585
+ """
586
+ return isinstance(x, Mapping)