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/_compat.py ADDED
@@ -0,0 +1,475 @@
1
+ """
2
+ R infrastructure shims for ggplot2.
3
+
4
+ Replaces functionality from rlang, cli, lifecycle, withr, and vctrs that
5
+ ggplot2 relies on, adapted for idiomatic Python usage.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import warnings
12
+ from typing import Any, NoReturn, Optional
13
+
14
+ __all__ = [
15
+ "cli_abort",
16
+ "cli_warn",
17
+ "cli_inform",
18
+ "is_string",
19
+ "is_bool",
20
+ "is_character",
21
+ "is_null",
22
+ "is_bare_list",
23
+ "is_true",
24
+ "is_false",
25
+ "is_scalar_character",
26
+ "is_scalar_logical",
27
+ "is_installed",
28
+ "check_installed",
29
+ "deprecate_warn",
30
+ "deprecate_soft",
31
+ "deprecate_stop",
32
+ "Waiver",
33
+ "is_waiver",
34
+ "waiver",
35
+ "caller_arg",
36
+ ]
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # CLI messaging helpers (rlang / cli replacements)
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def cli_abort(
44
+ message: str,
45
+ *,
46
+ call: Optional[str] = None,
47
+ cls: type = ValueError,
48
+ **kwargs: Any,
49
+ ) -> NoReturn:
50
+ """Raise an exception with a formatted message.
51
+
52
+ Parameters
53
+ ----------
54
+ message : str
55
+ Error message. May contain ``{name}``-style placeholders that
56
+ are filled from *kwargs*.
57
+ call : str, optional
58
+ Name of the calling function (for context in the message).
59
+ cls : type, optional
60
+ Exception class to raise. Defaults to ``ValueError``.
61
+ **kwargs : Any
62
+ Substitution values for placeholders in *message*.
63
+
64
+ Raises
65
+ ------
66
+ Exception
67
+ An instance of *cls* with the formatted message.
68
+ """
69
+ try:
70
+ formatted = message.format(**kwargs) if kwargs else message
71
+ except (KeyError, IndexError):
72
+ formatted = message
73
+ raise cls(formatted)
74
+
75
+
76
+ def cli_warn(
77
+ message: str,
78
+ *,
79
+ call: Optional[str] = None,
80
+ **kwargs: Any,
81
+ ) -> None:
82
+ """Issue a ``UserWarning`` with a formatted message.
83
+
84
+ Parameters
85
+ ----------
86
+ message : str
87
+ Warning message. May contain ``{name}``-style placeholders.
88
+ call : str, optional
89
+ Name of the calling function.
90
+ **kwargs : Any
91
+ Substitution values for placeholders in *message*.
92
+ """
93
+ try:
94
+ formatted = message.format(**kwargs) if kwargs else message
95
+ except (KeyError, IndexError):
96
+ formatted = message
97
+ warnings.warn(formatted, UserWarning, stacklevel=2)
98
+
99
+
100
+ def cli_inform(
101
+ message: str,
102
+ *,
103
+ call: Optional[str] = None,
104
+ **kwargs: Any,
105
+ ) -> None:
106
+ """Emit an informational message (no-op by default).
107
+
108
+ In interactive sessions this could print; in batch mode it stays silent.
109
+ Override by monkey-patching if verbose output is desired.
110
+
111
+ Parameters
112
+ ----------
113
+ message : str
114
+ Informational message.
115
+ call : str, optional
116
+ Name of the calling function.
117
+ **kwargs : Any
118
+ Substitution values for placeholders in *message*.
119
+ """
120
+ # Intentionally silent – mirrors rlang::inform() in non-interactive R.
121
+ pass
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Type-checking predicates (rlang replacements)
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def is_string(x: Any) -> bool:
129
+ """Check whether *x* is a single string.
130
+
131
+ Parameters
132
+ ----------
133
+ x : Any
134
+ Object to test.
135
+
136
+ Returns
137
+ -------
138
+ bool
139
+ ``True`` if *x* is an instance of ``str``.
140
+ """
141
+ return isinstance(x, str)
142
+
143
+
144
+ def is_bool(x: Any) -> bool:
145
+ """Check whether *x* is a Python ``bool``.
146
+
147
+ Parameters
148
+ ----------
149
+ x : Any
150
+ Object to test.
151
+
152
+ Returns
153
+ -------
154
+ bool
155
+ ``True`` if *x* is an instance of ``bool``.
156
+ """
157
+ return isinstance(x, bool)
158
+
159
+
160
+ def is_character(x: Any) -> bool:
161
+ """Check whether *x* is a string or list of strings.
162
+
163
+ Parameters
164
+ ----------
165
+ x : Any
166
+ Object to test.
167
+
168
+ Returns
169
+ -------
170
+ bool
171
+ ``True`` if *x* is a ``str`` or a ``list`` whose elements are all
172
+ strings.
173
+ """
174
+ if isinstance(x, str):
175
+ return True
176
+ if isinstance(x, list):
177
+ return all(isinstance(el, str) for el in x)
178
+ return False
179
+
180
+
181
+ def is_null(x: Any) -> bool:
182
+ """Check whether *x* is ``None``.
183
+
184
+ Parameters
185
+ ----------
186
+ x : Any
187
+ Object to test.
188
+
189
+ Returns
190
+ -------
191
+ bool
192
+ ``True`` if *x* is ``None``.
193
+ """
194
+ return x is None
195
+
196
+
197
+ def is_bare_list(x: Any) -> bool:
198
+ """Check whether *x* is a plain ``list`` (not a subclass).
199
+
200
+ Parameters
201
+ ----------
202
+ x : Any
203
+ Object to test.
204
+
205
+ Returns
206
+ -------
207
+ bool
208
+ ``True`` if ``type(x)`` is exactly ``list``.
209
+ """
210
+ return type(x) is list
211
+
212
+
213
+ def is_true(x: Any) -> bool:
214
+ """Check whether *x* is literally ``True``.
215
+
216
+ Parameters
217
+ ----------
218
+ x : Any
219
+ Object to test.
220
+
221
+ Returns
222
+ -------
223
+ bool
224
+ """
225
+ return x is True
226
+
227
+
228
+ def is_false(x: Any) -> bool:
229
+ """Check whether *x* is literally ``False``.
230
+
231
+ Parameters
232
+ ----------
233
+ x : Any
234
+ Object to test.
235
+
236
+ Returns
237
+ -------
238
+ bool
239
+ """
240
+ return x is False
241
+
242
+
243
+ def is_scalar_character(x: Any) -> bool:
244
+ """Check whether *x* is a length-1 character value (a single string).
245
+
246
+ Parameters
247
+ ----------
248
+ x : Any
249
+ Object to test.
250
+
251
+ Returns
252
+ -------
253
+ bool
254
+ """
255
+ return isinstance(x, str)
256
+
257
+
258
+ def is_scalar_logical(x: Any) -> bool:
259
+ """Check whether *x* is a length-1 logical value (a single bool).
260
+
261
+ Parameters
262
+ ----------
263
+ x : Any
264
+ Object to test.
265
+
266
+ Returns
267
+ -------
268
+ bool
269
+ """
270
+ return isinstance(x, bool)
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Package availability (rlang replacements)
275
+ # ---------------------------------------------------------------------------
276
+
277
+ def is_installed(pkg: str) -> bool:
278
+ """Check whether a Python package is importable.
279
+
280
+ Parameters
281
+ ----------
282
+ pkg : str
283
+ Package name (dotted names are supported).
284
+
285
+ Returns
286
+ -------
287
+ bool
288
+ ``True`` if the package can be imported.
289
+ """
290
+ try:
291
+ importlib.import_module(pkg)
292
+ return True
293
+ except ImportError:
294
+ return False
295
+
296
+
297
+ def check_installed(
298
+ pkg: str,
299
+ reason: Optional[str] = None,
300
+ ) -> None:
301
+ """Raise ``ImportError`` if a package is not available.
302
+
303
+ Parameters
304
+ ----------
305
+ pkg : str
306
+ Package name.
307
+ reason : str, optional
308
+ Human-readable reason the package is needed (appended to the
309
+ error message).
310
+
311
+ Raises
312
+ ------
313
+ ImportError
314
+ If the package cannot be imported.
315
+ """
316
+ if not is_installed(pkg):
317
+ msg = f"The '{pkg}' package is required"
318
+ if reason:
319
+ msg += f" {reason}"
320
+ msg += "."
321
+ raise ImportError(msg)
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Deprecation helpers (lifecycle replacements)
326
+ # ---------------------------------------------------------------------------
327
+
328
+ def deprecate_warn(
329
+ when: str,
330
+ what: str,
331
+ with_: Optional[str] = None,
332
+ ) -> None:
333
+ """Emit a deprecation warning.
334
+
335
+ Parameters
336
+ ----------
337
+ when : str
338
+ Version in which the feature was deprecated (e.g. ``"3.4.0"``).
339
+ what : str
340
+ Description of the deprecated feature.
341
+ with_ : str, optional
342
+ Replacement to suggest.
343
+ """
344
+ msg = f"{what} was deprecated in version {when}."
345
+ if with_ is not None:
346
+ msg += f" Please use {with_} instead."
347
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
348
+
349
+
350
+ def deprecate_soft(
351
+ when: str,
352
+ what: str,
353
+ with_: Optional[str] = None,
354
+ ) -> None:
355
+ """Emit a soft deprecation warning.
356
+
357
+ Soft deprecations are shown only when the deprecated code is called
358
+ from outside the package. In this Python port the distinction is
359
+ ignored and a normal ``DeprecationWarning`` is emitted.
360
+
361
+ Parameters
362
+ ----------
363
+ when : str
364
+ Version string.
365
+ what : str
366
+ Deprecated feature description.
367
+ with_ : str, optional
368
+ Replacement suggestion.
369
+ """
370
+ deprecate_warn(when, what, with_=with_)
371
+
372
+
373
+ def deprecate_stop(
374
+ when: str,
375
+ what: str,
376
+ with_: Optional[str] = None,
377
+ ) -> NoReturn:
378
+ """Raise an error for a defunct (fully removed) feature.
379
+
380
+ Parameters
381
+ ----------
382
+ when : str
383
+ Version in which the feature was removed.
384
+ what : str
385
+ Description of the removed feature.
386
+ with_ : str, optional
387
+ Replacement to suggest.
388
+
389
+ Raises
390
+ ------
391
+ RuntimeError
392
+ Always raised.
393
+ """
394
+ msg = f"{what} was deprecated in version {when} and is now defunct."
395
+ if with_ is not None:
396
+ msg += f" Please use {with_} instead."
397
+ raise RuntimeError(msg)
398
+
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # Waiver sentinel (ggplot2-specific)
402
+ # ---------------------------------------------------------------------------
403
+
404
+ class Waiver:
405
+ """Sentinel class indicating "use the default value".
406
+
407
+ In R ggplot2, ``waiver()`` signals that a parameter should fall back to
408
+ the default computed by the plot. This Python equivalent works the
409
+ same way.
410
+ """
411
+
412
+ _instance: Optional["Waiver"] = None
413
+
414
+ def __new__(cls) -> "Waiver":
415
+ # Singleton so that ``Waiver() is Waiver()`` holds.
416
+ if cls._instance is None:
417
+ cls._instance = super().__new__(cls)
418
+ return cls._instance
419
+
420
+ def __repr__(self) -> str:
421
+ return "waiver()"
422
+
423
+ def __bool__(self) -> bool:
424
+ # Prevent accidental truthiness tests.
425
+ return False
426
+
427
+
428
+ def waiver() -> Waiver:
429
+ """Return a ``Waiver`` sentinel value.
430
+
431
+ Returns
432
+ -------
433
+ Waiver
434
+ The singleton waiver instance.
435
+ """
436
+ return Waiver()
437
+
438
+
439
+ def is_waiver(x: Any) -> bool:
440
+ """Check whether *x* is a ``Waiver`` sentinel.
441
+
442
+ Parameters
443
+ ----------
444
+ x : Any
445
+ Object to test.
446
+
447
+ Returns
448
+ -------
449
+ bool
450
+ ``True`` if *x* is a ``Waiver`` instance.
451
+ """
452
+ return isinstance(x, Waiver)
453
+
454
+
455
+ # ---------------------------------------------------------------------------
456
+ # Miscellaneous rlang helpers
457
+ # ---------------------------------------------------------------------------
458
+
459
+ def caller_arg(arg: str) -> str:
460
+ """Return a human-readable label for a function argument.
461
+
462
+ In R, ``rlang::caller_arg()`` inspects the call stack. Here we
463
+ simply return the argument name as-is.
464
+
465
+ Parameters
466
+ ----------
467
+ arg : str
468
+ Argument name.
469
+
470
+ Returns
471
+ -------
472
+ str
473
+ The same string, suitable for inclusion in error messages.
474
+ """
475
+ return arg
ggplot2_py/_plugins.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ Entry-point-based plugin discovery for ggplot2_py.
3
+
4
+ This module scans ``importlib.metadata.entry_points`` for packages that
5
+ declare extensions under the following groups:
6
+
7
+ - ``ggplot2_py.geoms`` — custom Geom subclasses
8
+ - ``ggplot2_py.stats`` — custom Stat subclasses
9
+ - ``ggplot2_py.positions`` — custom Position subclasses
10
+ - ``ggplot2_py.scales`` — custom Scale subclasses
11
+ - ``ggplot2_py.coords`` — custom Coord subclasses
12
+ - ``ggplot2_py.facets`` — custom Facet subclasses
13
+
14
+ Extension packages declare entry points in their ``pyproject.toml``::
15
+
16
+ [project.entry-points."ggplot2_py.geoms"]
17
+ star = "my_ext.geoms:GeomStar"
18
+
19
+ [project.entry-points."ggplot2_py.stats"]
20
+ chull = "my_ext.stats:StatChull"
21
+
22
+ At ggplot2_py import time, :func:`discover_extensions` scans all installed
23
+ packages, loads the declared classes, and registers them in the
24
+ corresponding ``_registry`` dictionaries. If a plugin fails to load
25
+ (e.g. missing dependency), a warning is emitted but import is not blocked.
26
+
27
+ This is a **Python-exclusive** extension mechanism — R has no equivalent.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import importlib
33
+ import warnings
34
+ from importlib.metadata import entry_points
35
+ from typing import Any, Dict, List, Tuple
36
+
37
+ __all__ = ["discover_extensions", "list_extensions"]
38
+
39
+ # (entry_point_group, module_path, base_class_name)
40
+ _EXTENSION_GROUPS: List[Tuple[str, str, str]] = [
41
+ ("ggplot2_py.geoms", "ggplot2_py.geom", "Geom"),
42
+ ("ggplot2_py.stats", "ggplot2_py.stat", "Stat"),
43
+ ("ggplot2_py.positions", "ggplot2_py.position", "Position"),
44
+ ("ggplot2_py.scales", "ggplot2_py.scale", "Scale"),
45
+ ("ggplot2_py.coords", "ggplot2_py.coord", "Coord"),
46
+ ("ggplot2_py.facets", "ggplot2_py.facet", "Facet"),
47
+ ]
48
+
49
+ _discovered: Dict[str, List[str]] = {} # group -> list of names
50
+
51
+
52
+ def discover_extensions() -> Dict[str, List[str]]:
53
+ """Scan installed packages for ggplot2_py entry-point extensions.
54
+
55
+ For each declared entry point, the class is loaded and registered in
56
+ the corresponding base class's ``_registry``. Classes that are
57
+ already registered (e.g. via ``__init_subclass__``) are skipped.
58
+
59
+ Returns
60
+ -------
61
+ dict
62
+ Mapping of ``{group: [name, ...]}`` for all discovered extensions.
63
+
64
+ Examples
65
+ --------
66
+ ::
67
+
68
+ from ggplot2_py._plugins import discover_extensions
69
+ found = discover_extensions()
70
+ print(found)
71
+ # {'ggplot2_py.geoms': ['star'], 'ggplot2_py.stats': ['chull'], ...}
72
+ """
73
+ global _discovered
74
+ result: Dict[str, List[str]] = {}
75
+
76
+ eps = entry_points()
77
+
78
+ for group, mod_path, base_name in _EXTENSION_GROUPS:
79
+ group_eps = list(eps.select(group=group))
80
+ if not group_eps:
81
+ continue
82
+
83
+ # Import the base module to access the registry
84
+ try:
85
+ mod = importlib.import_module(mod_path)
86
+ base_cls = getattr(mod, base_name)
87
+ registry = getattr(base_cls, "_registry", None)
88
+ except Exception:
89
+ continue
90
+
91
+ names: List[str] = []
92
+ for ep in group_eps:
93
+ try:
94
+ cls = ep.load()
95
+ # Register under both the entry-point name and CamelCase
96
+ if registry is not None:
97
+ if ep.name not in registry:
98
+ registry[ep.name] = cls
99
+ # Also register CamelCase form
100
+ camel = ep.name[0].upper() + ep.name[1:] if ep.name else ep.name
101
+ if camel not in registry:
102
+ registry[camel] = cls
103
+ names.append(ep.name)
104
+ except Exception as exc:
105
+ warnings.warn(
106
+ f"ggplot2_py: failed to load extension '{ep.name}' "
107
+ f"from group '{group}': {exc}",
108
+ stacklevel=2,
109
+ )
110
+
111
+ if names:
112
+ result[group] = names
113
+
114
+ _discovered = result
115
+ return result
116
+
117
+
118
+ def list_extensions() -> Dict[str, List[str]]:
119
+ """Return previously discovered extensions (without re-scanning).
120
+
121
+ Call :func:`discover_extensions` first, or rely on the automatic
122
+ scan at ggplot2_py import time.
123
+
124
+ Returns
125
+ -------
126
+ dict
127
+ Mapping of ``{group: [name, ...]}`` for discovered extensions.
128
+ """
129
+ return dict(_discovered)