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/_utils.py ADDED
@@ -0,0 +1,544 @@
1
+ """
2
+ Internal utilities for ggplot2.
3
+
4
+ Replaces functionality from ``utilities.R``, ``compat-plyr.R``, ``bin.R``,
5
+ and ``grouping.R`` in the R package.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import warnings
12
+ from typing import Any, Dict, List, Optional, Sequence, TypeVar, Union
13
+
14
+ import numpy as np
15
+ import pandas as pd
16
+
17
+ __all__ = [
18
+ "remove_missing",
19
+ "resolution",
20
+ "snake_class",
21
+ "has_groups",
22
+ "empty",
23
+ "is_empty",
24
+ "try_fetch",
25
+ "compact",
26
+ "modify_list",
27
+ "data_frame",
28
+ "unique_default",
29
+ "rename",
30
+ "id_var",
31
+ "plyr_id",
32
+ "interleave",
33
+ "width_cm",
34
+ "height_cm",
35
+ "stapled_to_list",
36
+ ]
37
+
38
+ T = TypeVar("T")
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Missing-data helpers
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def remove_missing(
46
+ df: pd.DataFrame,
47
+ vars: Optional[List[str]] = None,
48
+ na_rm: bool = False,
49
+ name: str = "",
50
+ finite: bool = False,
51
+ ) -> pd.DataFrame:
52
+ """Remove rows with missing (or non-finite) values from a DataFrame.
53
+
54
+ Parameters
55
+ ----------
56
+ df : pd.DataFrame
57
+ Input data.
58
+ vars : list of str, optional
59
+ Column names to check. If ``None``, all columns are checked.
60
+ na_rm : bool, optional
61
+ If ``False``, emit a warning when rows are removed but still
62
+ remove them. If ``True``, remove silently. Default ``False``.
63
+ name : str, optional
64
+ Name of the calling layer (used in warning messages).
65
+ finite : bool, optional
66
+ If ``True``, also remove rows where checked columns contain
67
+ ``inf`` / ``-inf``. Default ``False``.
68
+
69
+ Returns
70
+ -------
71
+ pd.DataFrame
72
+ A copy of *df* with offending rows removed.
73
+ """
74
+ if df.empty:
75
+ return df
76
+
77
+ if vars is None:
78
+ check_cols = list(df.columns)
79
+ else:
80
+ check_cols = [c for c in vars if c in df.columns]
81
+
82
+ if not check_cols:
83
+ return df
84
+
85
+ if finite:
86
+ # Check for NA *and* non-finite in numeric columns.
87
+ mask = pd.Series(True, index=df.index)
88
+ for col in check_cols:
89
+ s = df[col]
90
+ if pd.api.types.is_numeric_dtype(s):
91
+ mask = mask & np.isfinite(s.to_numpy(dtype=float, na_value=np.nan))
92
+ else:
93
+ mask = mask & s.notna()
94
+ else:
95
+ mask = df[check_cols].notna().all(axis=1)
96
+
97
+ n_removed = int((~mask).sum())
98
+ if n_removed > 0 and not na_rm:
99
+ qual = "non-finite" if finite else "missing"
100
+ where = f" ({name})" if name else ""
101
+ warnings.warn(
102
+ f"Removed {n_removed} rows containing {qual} values{where}.",
103
+ UserWarning,
104
+ stacklevel=2,
105
+ )
106
+
107
+ return df.loc[mask].reset_index(drop=True)
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Numeric helpers
112
+ # ---------------------------------------------------------------------------
113
+
114
+ def resolution(x: Any, zero: bool = True, discrete: bool = False) -> float:
115
+ """Compute the resolution of a numeric vector.
116
+
117
+ The resolution is the smallest non-zero difference between adjacent
118
+ unique sorted values. This is useful for choosing default bin widths.
119
+
120
+ Parameters
121
+ ----------
122
+ x : array-like
123
+ Numeric values.
124
+ zero : bool, optional
125
+ If ``True`` (default), include zero in the set of differences so
126
+ that the result is at most 1 when there is only one unique value.
127
+
128
+ Returns
129
+ -------
130
+ float
131
+ The resolution.
132
+ """
133
+ if discrete:
134
+ return 1.0
135
+ x = np.asarray(x, dtype=float)
136
+ x = x[np.isfinite(x)]
137
+ if len(x) == 0:
138
+ return 1.0
139
+
140
+ x = np.sort(np.unique(x))
141
+ if len(x) == 1:
142
+ return 1.0 if zero else 0.0
143
+
144
+ diffs = np.diff(x)
145
+ if zero:
146
+ diffs = np.concatenate([[0.0], diffs])
147
+ diffs = diffs[diffs > 0]
148
+ if len(diffs) == 0:
149
+ return 1.0
150
+ return float(np.min(diffs))
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # String / naming helpers
155
+ # ---------------------------------------------------------------------------
156
+
157
+ def snake_class(x: Any) -> str:
158
+ """Convert an object's class name to snake_case.
159
+
160
+ Parameters
161
+ ----------
162
+ x : Any
163
+ An object (or class).
164
+
165
+ Returns
166
+ -------
167
+ str
168
+ The class name in ``snake_case``.
169
+
170
+ Examples
171
+ --------
172
+ >>> snake_class(pd.DataFrame())
173
+ 'data_frame'
174
+ """
175
+ name = type(x).__name__ if not isinstance(x, type) else x.__name__
176
+ # Insert underscore before uppercase letters that follow a lowercase
177
+ # letter or another uppercase followed by a lowercase.
178
+ s1 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
179
+ s2 = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s1)
180
+ return s2.lower()
181
+
182
+
183
+ def rename(
184
+ x: Dict[str, Any],
185
+ mapping: Optional[Dict[str, str]] = None,
186
+ **kwargs: str,
187
+ ) -> Dict[str, Any]:
188
+ """Rename keys in a dictionary.
189
+
190
+ Parameters
191
+ ----------
192
+ x : dict
193
+ Original dictionary.
194
+ mapping : dict, optional
195
+ ``{old_name: new_name}`` pairs.
196
+ **kwargs : str
197
+ Additional ``old_name=new_name`` pairs (convenience).
198
+
199
+ Returns
200
+ -------
201
+ dict
202
+ A new dictionary with renamed keys.
203
+ """
204
+ if mapping is None:
205
+ mapping = {}
206
+ mapping.update(kwargs)
207
+ return {mapping.get(k, k): v for k, v in x.items()}
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # DataFrame / grouping utilities
212
+ # ---------------------------------------------------------------------------
213
+
214
+ def has_groups(df: pd.DataFrame) -> bool:
215
+ """Check whether a DataFrame has a ``"group"`` column with >1 group.
216
+
217
+ Parameters
218
+ ----------
219
+ df : pd.DataFrame
220
+ Input data.
221
+
222
+ Returns
223
+ -------
224
+ bool
225
+ """
226
+ if "group" not in df.columns:
227
+ return False
228
+ return int(df["group"].nunique()) > 1
229
+
230
+
231
+ def empty(df: pd.DataFrame) -> bool:
232
+ """Check whether a DataFrame is conceptually empty.
233
+
234
+ A DataFrame is considered empty if it has zero rows **or** zero
235
+ columns.
236
+
237
+ Parameters
238
+ ----------
239
+ df : pd.DataFrame
240
+ Input data.
241
+
242
+ Returns
243
+ -------
244
+ bool
245
+ """
246
+ return df.shape[0] == 0 or df.shape[1] == 0
247
+
248
+
249
+ def is_empty(x: Any) -> bool:
250
+ """Check whether an object is empty.
251
+
252
+ Parameters
253
+ ----------
254
+ x : Any
255
+ Object to test. Works with DataFrames, dicts, lists, and other
256
+ sized objects.
257
+
258
+ Returns
259
+ -------
260
+ bool
261
+ """
262
+ if isinstance(x, pd.DataFrame):
263
+ return empty(x)
264
+ if x is None:
265
+ return True
266
+ try:
267
+ return len(x) == 0 # type: ignore[arg-type]
268
+ except TypeError:
269
+ return False
270
+
271
+
272
+ def data_frame(**kwargs: Any) -> pd.DataFrame:
273
+ """Create a ``pd.DataFrame`` from keyword arguments.
274
+
275
+ Each keyword becomes a column. Scalar values are broadcast to match
276
+ the length of the longest array-like argument.
277
+
278
+ Parameters
279
+ ----------
280
+ **kwargs : Any
281
+ Column name/value pairs.
282
+
283
+ Returns
284
+ -------
285
+ pd.DataFrame
286
+ """
287
+ return pd.DataFrame(kwargs)
288
+
289
+
290
+ def unique_default(x: Any) -> np.ndarray:
291
+ """Return unique values preserving the order of first occurrence.
292
+
293
+ Parameters
294
+ ----------
295
+ x : array-like
296
+ Input values.
297
+
298
+ Returns
299
+ -------
300
+ np.ndarray
301
+ """
302
+ x = np.asarray(x)
303
+ _, idx = np.unique(x, return_index=True)
304
+ return x[np.sort(idx)]
305
+
306
+
307
+ def id_var(x: Any) -> np.ndarray:
308
+ """Compute integer group IDs for a single variable.
309
+
310
+ Parameters
311
+ ----------
312
+ x : array-like
313
+ Values (may be numeric, string, or categorical).
314
+
315
+ Returns
316
+ -------
317
+ np.ndarray
318
+ Integer array of 1-based group IDs, one per element.
319
+ """
320
+ x = np.asarray(x)
321
+ uniques, inverse = np.unique(x, return_inverse=True)
322
+ return inverse + 1 # 1-based to match R convention
323
+
324
+
325
+ def plyr_id(
326
+ df: pd.DataFrame,
327
+ drop: bool = False,
328
+ ) -> np.ndarray:
329
+ """Compute interaction-style group IDs across multiple columns.
330
+
331
+ Mimics ``plyr::id()`` — each unique combination of values across all
332
+ columns of *df* receives a unique integer ID (1-based).
333
+
334
+ Parameters
335
+ ----------
336
+ df : pd.DataFrame
337
+ Data with columns to interact.
338
+ drop : bool, optional
339
+ If ``True``, re-number IDs so that unused combinations are
340
+ removed. Default ``False``.
341
+
342
+ Returns
343
+ -------
344
+ np.ndarray
345
+ 1-based integer group IDs with length ``len(df)``.
346
+ """
347
+ if df.shape[1] == 0 or df.shape[0] == 0:
348
+ return np.ones(len(df), dtype=int)
349
+
350
+ # Build a single interaction key using tuples.
351
+ cols = [df.iloc[:, i].to_numpy() for i in range(df.shape[1])]
352
+ keys = list(zip(*cols))
353
+
354
+ # Map each unique key to an integer.
355
+ seen: Dict[Any, int] = {}
356
+ ids = np.empty(len(keys), dtype=int)
357
+ counter = 0
358
+ for i, k in enumerate(keys):
359
+ if k not in seen:
360
+ counter += 1
361
+ seen[k] = counter
362
+ ids[i] = seen[k]
363
+ return ids
364
+
365
+
366
+ # ---------------------------------------------------------------------------
367
+ # Dict / list helpers
368
+ # ---------------------------------------------------------------------------
369
+
370
+ def try_fetch(expr: Any, default: Any = None) -> Any:
371
+ """Execute a callable and return *default* on any exception.
372
+
373
+ If *expr* is not callable it is returned as-is.
374
+
375
+ Parameters
376
+ ----------
377
+ expr : callable or Any
378
+ A zero-argument callable, or a plain value.
379
+ default : Any, optional
380
+ Value to return if *expr* raises.
381
+
382
+ Returns
383
+ -------
384
+ Any
385
+ """
386
+ if callable(expr):
387
+ try:
388
+ return expr()
389
+ except Exception:
390
+ return default
391
+ return expr
392
+
393
+
394
+ def compact(x: Dict[str, Any]) -> Dict[str, Any]:
395
+ """Remove ``None`` values from a dictionary.
396
+
397
+ Parameters
398
+ ----------
399
+ x : dict
400
+ Input dictionary.
401
+
402
+ Returns
403
+ -------
404
+ dict
405
+ A new dictionary with all ``None``-valued entries removed.
406
+ """
407
+ return {k: v for k, v in x.items() if v is not None}
408
+
409
+
410
+ def modify_list(
411
+ old: Dict[str, Any],
412
+ new: Dict[str, Any],
413
+ ) -> Dict[str, Any]:
414
+ """Merge two dictionaries, with *new* overriding *old*.
415
+
416
+ Parameters
417
+ ----------
418
+ old : dict
419
+ Base dictionary.
420
+ new : dict
421
+ Override dictionary. Keys whose values are ``None`` will
422
+ **remove** the corresponding key from the result (mirroring
423
+ ``utils::modifyList`` in R).
424
+
425
+ Returns
426
+ -------
427
+ dict
428
+ Merged dictionary (a fresh copy).
429
+ """
430
+ result = dict(old)
431
+ for k, v in new.items():
432
+ if v is None:
433
+ result.pop(k, None)
434
+ else:
435
+ result[k] = v
436
+ return result
437
+
438
+
439
+ # ---------------------------------------------------------------------------
440
+ # Sequence helpers
441
+ # ---------------------------------------------------------------------------
442
+
443
+ def interleave(*args: Sequence[Any]) -> list:
444
+ """Interleave elements from multiple sequences.
445
+
446
+ Parameters
447
+ ----------
448
+ *args : Sequence
449
+ Input sequences. They should all have the same length.
450
+
451
+ Returns
452
+ -------
453
+ list
454
+ A flat list with elements taken round-robin from each input.
455
+
456
+ Examples
457
+ --------
458
+ >>> interleave([1, 2, 3], [10, 20, 30])
459
+ [1, 10, 2, 20, 3, 30]
460
+ """
461
+ if not args:
462
+ return []
463
+ result: list = []
464
+ max_len = max(len(a) for a in args)
465
+ for i in range(max_len):
466
+ for a in args:
467
+ if i < len(a):
468
+ result.append(a[i])
469
+ return result
470
+
471
+
472
+ # ---------------------------------------------------------------------------
473
+ # Unit conversion helpers
474
+ # ---------------------------------------------------------------------------
475
+
476
+ def width_cm(x: Any) -> Union[float, np.ndarray]:
477
+ """Convert a grid unit to centimetres (width / x-axis).
478
+
479
+ Parameters
480
+ ----------
481
+ x : Unit or numeric
482
+ A ``grid_py.Unit`` object or a plain number (assumed to be in
483
+ centimetres already).
484
+
485
+ Returns
486
+ -------
487
+ float or np.ndarray
488
+ The width in centimetres.
489
+ """
490
+ from grid_py import Unit, convert_unit
491
+
492
+ if isinstance(x, Unit):
493
+ result = convert_unit(x, "cm", axisFrom="x", typeFrom="dimension", valueOnly=True)
494
+ return result
495
+ return float(x) if np.isscalar(x) else np.asarray(x, dtype=float)
496
+
497
+
498
+ def height_cm(x: Any) -> Union[float, np.ndarray]:
499
+ """Convert a grid unit to centimetres (height / y-axis).
500
+
501
+ Parameters
502
+ ----------
503
+ x : Unit or numeric
504
+ A ``grid_py.Unit`` object or a plain number (assumed to be in
505
+ centimetres already).
506
+
507
+ Returns
508
+ -------
509
+ float or np.ndarray
510
+ The height in centimetres.
511
+ """
512
+ from grid_py import Unit, convert_unit
513
+
514
+ if isinstance(x, Unit):
515
+ result = convert_unit(x, "cm", axisFrom="y", typeFrom="dimension", valueOnly=True)
516
+ return result
517
+ return float(x) if np.isscalar(x) else np.asarray(x, dtype=float)
518
+
519
+
520
+ # ---------------------------------------------------------------------------
521
+ # Miscellaneous
522
+ # ---------------------------------------------------------------------------
523
+
524
+ def stapled_to_list(x: Any) -> list:
525
+ """Convert a "stapled" object to a plain list.
526
+
527
+ In R, ``vctrs::vec_proxy()`` sometimes returns stapled vectors. In
528
+ Python this is a no-op — if *x* is already a list it is returned
529
+ unchanged; otherwise it is wrapped in a list.
530
+
531
+ Parameters
532
+ ----------
533
+ x : Any
534
+ Object to convert.
535
+
536
+ Returns
537
+ -------
538
+ list
539
+ """
540
+ if isinstance(x, list):
541
+ return x
542
+ if x is None:
543
+ return []
544
+ return [x]