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/save.py ADDED
@@ -0,0 +1,316 @@
1
+ """
2
+ Save ggplot objects to files.
3
+
4
+ Provides :func:`ggsave` for saving plots to PNG, PDF, SVG, JPG and other
5
+ formats via Cairo.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import Any, Dict, List, Optional, Tuple, Union
12
+
13
+ from ggplot2_py._compat import cli_abort, cli_warn, cli_inform
14
+
15
+ __all__ = [
16
+ "ggsave",
17
+ "check_device",
18
+ ]
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Device mapping
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _SUPPORTED_DEVICES = frozenset({
26
+ "png", "pdf", "svg", "jpg", "jpeg", "tiff", "tif",
27
+ "bmp", "eps", "ps", "webp",
28
+ })
29
+
30
+
31
+ def check_device(device: Optional[str], filename: str) -> str:
32
+ """Determine the output device from *device* or *filename*.
33
+
34
+ Parameters
35
+ ----------
36
+ device : str or None
37
+ Explicit device name (e.g. ``"png"``).
38
+ filename : str
39
+ Output file path; the extension is used when *device* is ``None``.
40
+
41
+ Returns
42
+ -------
43
+ str
44
+ Resolved device name (lowercase extension without dot).
45
+
46
+ Raises
47
+ ------
48
+ ValueError
49
+ If the device cannot be determined or is unsupported.
50
+ """
51
+ if device is not None:
52
+ device = device.lower().strip()
53
+ if device not in _SUPPORTED_DEVICES:
54
+ cli_abort(f"Unknown graphics device: '{device}'.")
55
+ return device
56
+
57
+ _, ext = os.path.splitext(filename)
58
+ if not ext:
59
+ cli_abort(
60
+ f"Cannot determine graphics device from '{filename}'. "
61
+ "Either supply `filename` with a file extension or supply `device`."
62
+ )
63
+ ext = ext.lstrip(".").lower()
64
+ if ext not in _SUPPORTED_DEVICES:
65
+ cli_abort(f"Unknown graphics device: '{ext}'.")
66
+ return ext
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # DPI parsing
71
+ # ---------------------------------------------------------------------------
72
+
73
+ _DPI_PRESETS: Dict[str, int] = {
74
+ "screen": 72,
75
+ "print": 300,
76
+ "retina": 320,
77
+ }
78
+
79
+
80
+ def _parse_dpi(dpi: Union[int, float, str]) -> float:
81
+ """Resolve a DPI specification.
82
+
83
+ Parameters
84
+ ----------
85
+ dpi : int, float, or str
86
+ Either a number or one of ``"screen"`` (72), ``"print"`` (300),
87
+ ``"retina"`` (320).
88
+
89
+ Returns
90
+ -------
91
+ float
92
+ """
93
+ if isinstance(dpi, str):
94
+ val = _DPI_PRESETS.get(dpi.lower())
95
+ if val is None:
96
+ cli_abort(
97
+ f"Unknown DPI preset: '{dpi}'. "
98
+ f"Must be one of {list(_DPI_PRESETS)}."
99
+ )
100
+ return float(val)
101
+ return float(dpi)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Dimension helpers
106
+ # ---------------------------------------------------------------------------
107
+
108
+ _UNIT_FACTORS: Dict[str, float] = {
109
+ "in": 1.0,
110
+ "cm": 1.0 / 2.54,
111
+ "mm": 1.0 / 25.4,
112
+ }
113
+
114
+
115
+ def _to_inches(
116
+ value: Optional[float],
117
+ units: str,
118
+ dpi: float,
119
+ ) -> Optional[float]:
120
+ """Convert *value* from *units* to inches.
121
+
122
+ Parameters
123
+ ----------
124
+ value : float or None
125
+ units : str
126
+ ``"in"``, ``"cm"``, ``"mm"``, or ``"px"``.
127
+ dpi : float
128
+
129
+ Returns
130
+ -------
131
+ float or None
132
+ """
133
+ if value is None:
134
+ return None
135
+ if units == "px":
136
+ return value / dpi
137
+ factor = _UNIT_FACTORS.get(units)
138
+ if factor is None:
139
+ cli_abort(f"Unknown unit: '{units}'. Must be 'in', 'cm', 'mm', or 'px'.")
140
+ return value * factor
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # ggsave
145
+ # ---------------------------------------------------------------------------
146
+
147
+ def ggsave(
148
+ filename: str,
149
+ plot: Any = None,
150
+ device: Optional[str] = None,
151
+ path: Optional[str] = None,
152
+ width: Optional[float] = None,
153
+ height: Optional[float] = None,
154
+ units: str = "in",
155
+ dpi: Union[int, float, str] = 300,
156
+ limitsize: bool = True,
157
+ bg: Optional[str] = None,
158
+ create_dir: bool = False,
159
+ scale: float = 1.0,
160
+ **kwargs: Any,
161
+ ) -> str:
162
+ """Save a ggplot (or other grid object) to a file.
163
+
164
+ Parameters
165
+ ----------
166
+ filename : str
167
+ Output file name.
168
+ plot : GGPlot or None, optional
169
+ Plot to save. If ``None``, uses :func:`get_last_plot`.
170
+ device : str or None, optional
171
+ Graphics device (``"png"``, ``"pdf"``, ``"svg"``, etc.).
172
+ Auto-detected from *filename* extension if ``None``.
173
+ path : str or None, optional
174
+ Directory path. Combined with *filename* to form the full path.
175
+ width, height : float, optional
176
+ Plot dimensions in *units*. Defaults to 7 x 7 inches.
177
+ units : str, optional
178
+ Unit for *width* / *height*: ``"in"`` (default), ``"cm"``,
179
+ ``"mm"``, or ``"px"``.
180
+ dpi : int, float, or str, optional
181
+ Resolution (default 300). Also accepts ``"screen"`` (72),
182
+ ``"print"`` (300), ``"retina"`` (320).
183
+ limitsize : bool, optional
184
+ If ``True`` (default), refuse images > 50 x 50 inches.
185
+ bg : str or None, optional
186
+ Background colour.
187
+ create_dir : bool, optional
188
+ If ``True``, create non-existent directories.
189
+ scale : float, optional
190
+ Multiplicative scaling factor (default 1.0).
191
+ **kwargs
192
+ Passed through to the matplotlib ``savefig`` call.
193
+
194
+ Returns
195
+ -------
196
+ str
197
+ The resolved output file path.
198
+
199
+ Raises
200
+ ------
201
+ ValueError
202
+ If dimensions exceed limits or the device is unknown.
203
+ FileNotFoundError
204
+ If the target directory does not exist and *create_dir* is
205
+ ``False``.
206
+ """
207
+ from grid_py import grid_draw, grid_newpage, get_state
208
+ from grid_py.renderer import CairoRenderer
209
+
210
+ # Resolve DPI
211
+ dpi_val = _parse_dpi(dpi)
212
+
213
+ # Resolve device
214
+ dev = check_device(device, filename)
215
+
216
+ # Resolve path
217
+ if path is not None:
218
+ filename = os.path.join(path, filename)
219
+ target_dir = os.path.dirname(filename)
220
+ if target_dir and not os.path.isdir(target_dir):
221
+ if create_dir:
222
+ os.makedirs(target_dir, exist_ok=True)
223
+ else:
224
+ cli_abort(
225
+ f"Cannot find directory '{target_dir}'. "
226
+ "Supply an existing directory or use `create_dir=True`."
227
+ )
228
+
229
+ # Resolve dimensions
230
+ width_in = _to_inches(width, units, dpi_val)
231
+ height_in = _to_inches(height, units, dpi_val)
232
+ if width_in is None:
233
+ width_in = 7.0
234
+ if height_in is None:
235
+ height_in = 7.0
236
+ width_in *= scale
237
+ height_in *= scale
238
+
239
+ if limitsize and (width_in >= 50 or height_in >= 50):
240
+ cli_abort(
241
+ f"Dimensions exceed 50 inches ({width_in:.1f} x {height_in:.1f}). "
242
+ "Use `limitsize=False` if you really want a plot that big."
243
+ )
244
+
245
+ # Resolve plot
246
+ if plot is None:
247
+ from ggplot2_py.plot import get_last_plot
248
+ plot = get_last_plot()
249
+ if plot is None:
250
+ cli_abort("No plot to save. Supply `plot` or create a plot first.")
251
+
252
+ # Build the plot
253
+ from ggplot2_py.plot import ggplot_build, ggplot_gtable, is_ggplot
254
+
255
+ if is_ggplot(plot):
256
+ built = ggplot_build(plot)
257
+ gtable = ggplot_gtable(built)
258
+ else:
259
+ gtable = plot
260
+
261
+ # Resolve background
262
+ if bg is None:
263
+ bg = "white"
264
+
265
+ # Choose Cairo surface type based on output device
266
+ _VECTOR_DEVICES = {"pdf", "svg", "ps", "eps"}
267
+ if dev in _VECTOR_DEVICES:
268
+ surface_type = {"pdf": "pdf", "svg": "svg", "ps": "ps", "eps": "ps"}[dev]
269
+ renderer = CairoRenderer(
270
+ width=width_in, height=height_in, dpi=dpi_val,
271
+ surface_type=surface_type, filename=filename, bg=bg,
272
+ )
273
+ else:
274
+ # Raster output: render to ImageSurface, then write
275
+ renderer = CairoRenderer(
276
+ width=width_in, height=height_in, dpi=dpi_val,
277
+ surface_type="image", bg=bg,
278
+ )
279
+
280
+ # Bind renderer and draw
281
+ state = get_state()
282
+ state.reset()
283
+ state.init_device(renderer)
284
+
285
+ grid_draw(gtable)
286
+
287
+ # Write output
288
+ if dev in _VECTOR_DEVICES:
289
+ renderer.finish()
290
+ elif dev in ("png",):
291
+ renderer.write_to_png(filename)
292
+ elif dev in ("jpg", "jpeg", "tiff", "tif", "bmp", "webp"):
293
+ # Cairo natively writes PNG; convert via Pillow for other raster formats
294
+ from PIL import Image
295
+ import io
296
+
297
+ png_bytes = renderer.to_png_bytes()
298
+ img = Image.open(io.BytesIO(png_bytes))
299
+ fmt_map: Dict[str, str] = {
300
+ "jpg": "JPEG", "jpeg": "JPEG",
301
+ "tiff": "TIFF", "tif": "TIFF",
302
+ "bmp": "BMP", "webp": "WEBP",
303
+ }
304
+ pil_fmt = fmt_map.get(dev, dev.upper())
305
+ save_kwargs: Dict[str, Any] = {}
306
+ if pil_fmt == "JPEG":
307
+ save_kwargs["quality"] = 95
308
+ img = img.convert("RGB")
309
+ img.save(filename, format=pil_fmt, **save_kwargs)
310
+ else:
311
+ # Unknown raster format — try PNG
312
+ renderer.write_to_png(filename)
313
+
314
+ cli_inform(f"Saving {width_in:.3g} x {height_in:.3g} {units} image to {filename}")
315
+
316
+ return filename