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
@@ -0,0 +1,8 @@
1
+ """
2
+ Guides sub-package for ggplot2_py.
3
+
4
+ Re-exports all public names from :mod:`ggplot2_py.guide`.
5
+ """
6
+
7
+ from ggplot2_py.guide import * # noqa: F401,F403
8
+ from ggplot2_py.guide import __all__ # noqa: F401
ggplot2_py/labeller.py ADDED
@@ -0,0 +1,296 @@
1
+ """
2
+ Facet labeller functions — faithful port of R's facet-labeller.R.
3
+
4
+ Labeller functions format the strip labels of facet grids and wraps.
5
+ They accept a dict of ``{variable_name: [values...]}`` and return
6
+ a list of formatted label strings.
7
+
8
+ R references
9
+ ------------
10
+ * ``ggplot2/R/facet-labeller.R`` — label_value, label_both, etc.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import textwrap
16
+ from typing import Any, Callable, Dict, List, Optional, Union
17
+
18
+ __all__ = [
19
+ "label_value",
20
+ "label_both",
21
+ "label_context",
22
+ "label_parsed",
23
+ "label_wrap_gen",
24
+ "as_labeller",
25
+ ]
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # label_value
30
+ # ---------------------------------------------------------------------------
31
+
32
+ def label_value(
33
+ labels: Dict[str, List[str]],
34
+ multi_line: bool = True,
35
+ ) -> List[str]:
36
+ """Return only the factor values.
37
+
38
+ This is the default labeller in ggplot2.
39
+
40
+ Mirrors ``label_value`` in R (facet-labeller.R:105-112).
41
+
42
+ Parameters
43
+ ----------
44
+ labels : dict
45
+ ``{variable_name: [value_per_panel...]}``.
46
+ multi_line : bool
47
+ If ``True`` (default), return one line per variable.
48
+ If ``False``, collapse all variables into one line.
49
+
50
+ Returns
51
+ -------
52
+ list of str
53
+ Formatted labels, one per panel.
54
+
55
+ Examples
56
+ --------
57
+ >>> label_value({"drv": ["4", "f", "r"]})
58
+ ["4", "f", "r"]
59
+ >>> label_value({"drv": ["4", "f"], "cyl": ["4", "6"]}, multi_line=False)
60
+ ["4, 4", "f, 6"]
61
+ """
62
+ var_names = list(labels.keys())
63
+ if not var_names:
64
+ return []
65
+
66
+ n_panels = len(next(iter(labels.values())))
67
+
68
+ if multi_line and len(var_names) == 1:
69
+ return [str(v) for v in labels[var_names[0]]]
70
+
71
+ if multi_line:
72
+ # Return one label per variable per panel — for multi-line strips,
73
+ # join with newline
74
+ result = []
75
+ for i in range(n_panels):
76
+ parts = [str(labels[v][i]) for v in var_names]
77
+ result.append("\n".join(parts))
78
+ return result
79
+ else:
80
+ # Single line: collapse all variables
81
+ result = []
82
+ for i in range(n_panels):
83
+ parts = [str(labels[v][i]) for v in var_names]
84
+ result.append(", ".join(parts))
85
+ return result
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # label_both
90
+ # ---------------------------------------------------------------------------
91
+
92
+ def label_both(
93
+ labels: Dict[str, List[str]],
94
+ multi_line: bool = True,
95
+ sep: str = ": ",
96
+ ) -> List[str]:
97
+ """Return ``"variable: value"`` labels.
98
+
99
+ Mirrors ``label_both`` in R (facet-labeller.R:119-142).
100
+
101
+ Parameters
102
+ ----------
103
+ labels : dict
104
+ ``{variable_name: [value_per_panel...]}``.
105
+ multi_line : bool
106
+ If ``True``, one line per variable; if ``False``, collapse.
107
+ sep : str
108
+ Separator between variable name and value.
109
+
110
+ Returns
111
+ -------
112
+ list of str
113
+ Formatted labels.
114
+
115
+ Examples
116
+ --------
117
+ >>> label_both({"drv": ["4", "f", "r"]})
118
+ ["drv: 4", "drv: f", "drv: r"]
119
+ """
120
+ var_names = list(labels.keys())
121
+ if not var_names:
122
+ return []
123
+
124
+ n_panels = len(next(iter(labels.values())))
125
+
126
+ if multi_line:
127
+ result = []
128
+ for i in range(n_panels):
129
+ parts = [f"{v}{sep}{labels[v][i]}" for v in var_names]
130
+ result.append("\n".join(parts))
131
+ return result
132
+ else:
133
+ result = []
134
+ for i in range(n_panels):
135
+ var_part = ", ".join(var_names)
136
+ val_part = ", ".join(str(labels[v][i]) for v in var_names)
137
+ result.append(f"{var_part}{sep}{val_part}")
138
+ return result
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # label_context
143
+ # ---------------------------------------------------------------------------
144
+
145
+ def label_context(
146
+ labels: Dict[str, List[str]],
147
+ multi_line: bool = True,
148
+ sep: str = ": ",
149
+ ) -> List[str]:
150
+ """Context-dependent labeller.
151
+
152
+ Uses ``label_value`` for single-variable faceting, ``label_both``
153
+ when multiple variables are involved.
154
+
155
+ Mirrors ``label_context`` in R (facet-labeller.R:147-153).
156
+
157
+ Parameters
158
+ ----------
159
+ labels : dict
160
+ ``{variable_name: [value_per_panel...]}``.
161
+ multi_line : bool
162
+ Multi-line mode.
163
+ sep : str
164
+ Separator for ``label_both``.
165
+
166
+ Returns
167
+ -------
168
+ list of str
169
+ Formatted labels.
170
+ """
171
+ if len(labels) <= 1:
172
+ return label_value(labels, multi_line)
173
+ else:
174
+ return label_both(labels, multi_line, sep)
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # label_parsed
179
+ # ---------------------------------------------------------------------------
180
+
181
+ def label_parsed(
182
+ labels: Dict[str, List[str]],
183
+ multi_line: bool = True,
184
+ ) -> List[str]:
185
+ """Interpret labels as expressions (Python: return as-is).
186
+
187
+ In R, this parses labels as plotmath expressions. In Python we
188
+ simply return the string values, since matplotlib mathtext or
189
+ LaTeX rendering is handled downstream by the text grob.
190
+
191
+ Mirrors ``label_parsed`` in R (facet-labeller.R:158-173).
192
+
193
+ Parameters
194
+ ----------
195
+ labels : dict
196
+ ``{variable_name: [value_per_panel...]}``.
197
+ multi_line : bool
198
+ Multi-line mode.
199
+
200
+ Returns
201
+ -------
202
+ list of str
203
+ Labels (unchanged).
204
+ """
205
+ return label_value(labels, multi_line)
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # label_wrap_gen
210
+ # ---------------------------------------------------------------------------
211
+
212
+ def label_wrap_gen(width: int = 25) -> Callable:
213
+ """Generate a labeller that wraps text at *width* characters.
214
+
215
+ Mirrors ``label_wrap_gen`` in R (facet-labeller.R:220-229).
216
+
217
+ Parameters
218
+ ----------
219
+ width : int
220
+ Maximum number of characters before wrapping.
221
+
222
+ Returns
223
+ -------
224
+ callable
225
+ A labeller function.
226
+
227
+ Examples
228
+ --------
229
+ >>> labeller = label_wrap_gen(10)
230
+ >>> labeller({"class": ["compact car", "midsize SUV"]})
231
+ ["compact\\ncar", "midsize\\nSUV"]
232
+ """
233
+ def _wrap_labeller(
234
+ labels: Dict[str, List[str]],
235
+ multi_line: bool = True,
236
+ ) -> List[str]:
237
+ base = label_value(labels, multi_line)
238
+ return ["\n".join(textwrap.wrap(str(lab), width)) for lab in base]
239
+
240
+ return _wrap_labeller
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # as_labeller
245
+ # ---------------------------------------------------------------------------
246
+
247
+ _LABELLER_REGISTRY: Dict[str, Callable] = {
248
+ "label_value": label_value,
249
+ "label_both": label_both,
250
+ "label_context": label_context,
251
+ "label_parsed": label_parsed,
252
+ }
253
+
254
+
255
+ def as_labeller(x: Any) -> Callable:
256
+ """Coerce *x* to a labeller function.
257
+
258
+ Mirrors ``as_labeller`` in R (facet-labeller.R:284-299).
259
+
260
+ Parameters
261
+ ----------
262
+ x : str, dict, or callable
263
+ - ``str``: lookup by name (e.g. ``"label_value"``).
264
+ - ``dict``: a lookup table mapping values to labels.
265
+ - ``callable``: returned as-is.
266
+
267
+ Returns
268
+ -------
269
+ callable
270
+ A labeller function.
271
+ """
272
+ if callable(x):
273
+ return x
274
+
275
+ if isinstance(x, str):
276
+ if x in _LABELLER_REGISTRY:
277
+ return _LABELLER_REGISTRY[x]
278
+ raise ValueError(
279
+ f"Unknown labeller: {x!r}. "
280
+ f"Available: {list(_LABELLER_REGISTRY.keys())}"
281
+ )
282
+
283
+ if isinstance(x, dict):
284
+ # Lookup-table labeller: maps values to custom labels
285
+ lookup = x
286
+
287
+ def _dict_labeller(
288
+ labels: Dict[str, List[str]],
289
+ multi_line: bool = True,
290
+ ) -> List[str]:
291
+ base = label_value(labels, multi_line)
292
+ return [lookup.get(lab, lab) for lab in base]
293
+
294
+ return _dict_labeller
295
+
296
+ raise TypeError(f"Cannot coerce {type(x).__name__} to a labeller.")
ggplot2_py/labels.py ADDED
@@ -0,0 +1,309 @@
1
+ """
2
+ Label helpers for ggplot2 plots.
3
+
4
+ Provides ``labs()``, ``xlab()``, ``ylab()``, ``ggtitle()`` for setting
5
+ axis titles, plot titles, subtitles, captions, tags, and alt-text.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List, Optional, Union
11
+
12
+ from ggplot2_py._compat import Waiver, is_waiver, waiver, cli_warn
13
+
14
+ __all__ = [
15
+ "labs",
16
+ "xlab",
17
+ "ylab",
18
+ "ggtitle",
19
+ "Labels",
20
+ "is_labels",
21
+ "get_labs",
22
+ "update_labels",
23
+ "make_labels",
24
+ ]
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Labels container
29
+ # ---------------------------------------------------------------------------
30
+
31
+ class Labels(dict):
32
+ """A thin ``dict`` subclass marking an object as a labels specification.
33
+
34
+ Behaves like a regular dictionary but carries a type tag so that the
35
+ ``+`` operator on a ``GGPlot`` object can dispatch correctly.
36
+ """
37
+
38
+ def __repr__(self) -> str:
39
+ items = ", ".join(f"{k}={v!r}" for k, v in self.items())
40
+ return f"Labels({items})"
41
+
42
+
43
+ def is_labels(x: Any) -> bool:
44
+ """Return ``True`` if *x* is a :class:`Labels` instance.
45
+
46
+ Parameters
47
+ ----------
48
+ x : object
49
+ Object to test.
50
+
51
+ Returns
52
+ -------
53
+ bool
54
+ """
55
+ return isinstance(x, Labels)
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Public API
60
+ # ---------------------------------------------------------------------------
61
+
62
+ # Canonical label keys that are NOT aesthetics.
63
+ _SPECIAL_KEYS = frozenset({
64
+ "title", "subtitle", "caption", "tag",
65
+ "alt", "alt_insight", "dictionary",
66
+ })
67
+
68
+
69
+ def labs(
70
+ *,
71
+ title: Any = None,
72
+ subtitle: Any = None,
73
+ caption: Any = None,
74
+ tag: Any = None,
75
+ alt: Any = None,
76
+ alt_insight: Any = None,
77
+ dictionary: Any = None,
78
+ **kwargs: Any,
79
+ ) -> Labels:
80
+ """Create a labels specification.
81
+
82
+ Parameters
83
+ ----------
84
+ title : str or None, optional
85
+ Plot title.
86
+ subtitle : str or None, optional
87
+ Plot subtitle (displayed below the title).
88
+ caption : str or None, optional
89
+ Plot caption (typically data source info).
90
+ tag : str or None, optional
91
+ Plot tag (e.g. ``"A"`` for sub-figures).
92
+ alt : str or callable or None, optional
93
+ Alt-text for accessibility.
94
+ alt_insight : str or None, optional
95
+ Short insight appended to auto-generated alt-text.
96
+ dictionary : dict or None, optional
97
+ Named dictionary for label look-ups.
98
+ **kwargs
99
+ Aesthetic-name = label pairs, e.g. ``x="Weight"``, ``colour="Class"``.
100
+
101
+ Returns
102
+ -------
103
+ Labels
104
+ A labels specification suitable for adding to a ggplot via ``+``.
105
+
106
+ Examples
107
+ --------
108
+ >>> labs(x="Engine displacement", y="Highway MPG", colour="Vehicle class")
109
+ """
110
+ # Collect special keyword args
111
+ args: Dict[str, Any] = {}
112
+ if title is not None:
113
+ args["title"] = title
114
+ if subtitle is not None:
115
+ args["subtitle"] = subtitle
116
+ if caption is not None:
117
+ args["caption"] = caption
118
+ if tag is not None:
119
+ args["tag"] = tag
120
+ if alt is not None:
121
+ args["alt"] = alt
122
+ if alt_insight is not None:
123
+ args["alt_insight"] = alt_insight
124
+ if dictionary is not None:
125
+ args["dictionary"] = dictionary
126
+
127
+ # Aesthetic label kwargs -- apply alias normalisation
128
+ from ggplot2_py.aes import standardise_aes_names # lazy to avoid circular
129
+
130
+ for k, v in kwargs.items():
131
+ canonical = standardise_aes_names([k])[0]
132
+ args[canonical] = v
133
+
134
+ return Labels(args)
135
+
136
+
137
+ def xlab(label: Optional[str]) -> Labels:
138
+ """Set the x-axis label.
139
+
140
+ Parameters
141
+ ----------
142
+ label : str or None
143
+ Label text. ``None`` removes the label.
144
+
145
+ Returns
146
+ -------
147
+ Labels
148
+ """
149
+ return labs(x=label)
150
+
151
+
152
+ def ylab(label: Optional[str]) -> Labels:
153
+ """Set the y-axis label.
154
+
155
+ Parameters
156
+ ----------
157
+ label : str or None
158
+ Label text. ``None`` removes the label.
159
+
160
+ Returns
161
+ -------
162
+ Labels
163
+ """
164
+ return labs(y=label)
165
+
166
+
167
+ def ggtitle(
168
+ label: Optional[str],
169
+ subtitle: Optional[str] = None,
170
+ ) -> Labels:
171
+ """Set the plot title (and optionally subtitle).
172
+
173
+ Parameters
174
+ ----------
175
+ label : str or None
176
+ Title text.
177
+ subtitle : str or None, optional
178
+ Subtitle text.
179
+
180
+ Returns
181
+ -------
182
+ Labels
183
+ """
184
+ return labs(title=label, subtitle=subtitle)
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # update_labels helper
189
+ # ---------------------------------------------------------------------------
190
+
191
+ def update_labels(plot: Any, labels: Union[Labels, Dict[str, Any]]) -> Any:
192
+ """Merge *labels* into *plot*'s existing labels.
193
+
194
+ Parameters
195
+ ----------
196
+ plot : GGPlot
197
+ The plot to update (will be cloned).
198
+ labels : Labels or dict
199
+ New labels to merge in.
200
+
201
+ Returns
202
+ -------
203
+ GGPlot
204
+ A shallow copy of *plot* with updated labels.
205
+ """
206
+ # Import here to avoid circular dependency
207
+ p = plot._clone()
208
+ # Merge: new labels override existing
209
+ merged = dict(p.labels)
210
+ merged.update(labels)
211
+ p.labels = Labels(merged)
212
+ return p
213
+
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # make_labels
217
+ # ---------------------------------------------------------------------------
218
+
219
+ def make_labels(mapping: Any) -> Dict[str, str]:
220
+ """Convert an aesthetic mapping into text labels.
221
+
222
+ Parameters
223
+ ----------
224
+ mapping : Mapping
225
+ An aesthetic mapping (from ``aes()``).
226
+
227
+ Returns
228
+ -------
229
+ dict
230
+ A dictionary of aesthetic-name -> label-string.
231
+ """
232
+ from ggplot2_py.aes import Mapping, AfterStat, AfterScale, Stage
233
+
234
+ # Accept both ``Mapping`` (from ``aes()``) and plain ``dict``
235
+ # (used by Stat.default_aes / Geom.default_aes). R's
236
+ # ``make_labels()`` treats any named list uniformly (labels.R:124).
237
+ if isinstance(mapping, Mapping):
238
+ mapping_items = mapping.items()
239
+ elif isinstance(mapping, dict):
240
+ mapping_items = mapping.items()
241
+ else:
242
+ return {}
243
+
244
+ def _label_for(val: Any) -> str:
245
+ """Generate a human-readable label for an aesthetic value."""
246
+ if val is None:
247
+ return ""
248
+ if callable(val) and not isinstance(val, str):
249
+ return getattr(val, "__name__", "<expr>")
250
+ return str(val)
251
+
252
+ result: Dict[str, str] = {}
253
+ for aes_name, val in mapping_items:
254
+ if val is None:
255
+ result[aes_name] = aes_name
256
+ elif isinstance(val, str):
257
+ result[aes_name] = val
258
+ elif isinstance(val, AfterStat):
259
+ result[aes_name] = _label_for(val.x)
260
+ elif isinstance(val, AfterScale):
261
+ result[aes_name] = _label_for(val.x)
262
+ elif isinstance(val, Stage):
263
+ # Use the start mapping if available, then after_stat, then after_scale
264
+ if val.start is not None:
265
+ result[aes_name] = _label_for(val.start)
266
+ elif val.after_stat is not None:
267
+ result[aes_name] = _label_for(val.after_stat)
268
+ elif val.after_scale is not None:
269
+ result[aes_name] = _label_for(val.after_scale)
270
+ else:
271
+ result[aes_name] = aes_name
272
+ elif callable(val):
273
+ result[aes_name] = _label_for(val)
274
+ else:
275
+ result[aes_name] = str(val)
276
+ return result
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # get_labs (requires a built plot)
281
+ # ---------------------------------------------------------------------------
282
+
283
+ def get_labs(plot: Any = None) -> Labels:
284
+ """Retrieve completed labels from a plot.
285
+
286
+ Parameters
287
+ ----------
288
+ plot : GGPlot or None
289
+ A ggplot object. If ``None``, uses ``get_last_plot()``.
290
+
291
+ Returns
292
+ -------
293
+ Labels
294
+ Resolved label dictionary.
295
+ """
296
+ if plot is None:
297
+ # lazy import to avoid circular
298
+ from ggplot2_py.plot import get_last_plot
299
+ plot = get_last_plot()
300
+
301
+ if plot is None:
302
+ return Labels()
303
+
304
+ # If already built, grab labels directly
305
+ if hasattr(plot, "plot") and hasattr(plot, "layout"):
306
+ # BuiltGGPlot
307
+ return Labels(plot.plot.labels)
308
+
309
+ return Labels(plot.labels)