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.
- ggplot2_py/__init__.py +852 -0
- ggplot2_py/_compat.py +475 -0
- ggplot2_py/_plugins.py +129 -0
- ggplot2_py/_utils.py +544 -0
- ggplot2_py/aes.py +586 -0
- ggplot2_py/annotation.py +540 -0
- ggplot2_py/coord.py +2108 -0
- ggplot2_py/coords/__init__.py +49 -0
- ggplot2_py/datasets.py +265 -0
- ggplot2_py/draw_key.py +454 -0
- ggplot2_py/facet.py +1456 -0
- ggplot2_py/fortify.py +95 -0
- ggplot2_py/geom.py +4516 -0
- ggplot2_py/geoms/__init__.py +12 -0
- ggplot2_py/ggproto.py +279 -0
- ggplot2_py/guide.py +2925 -0
- ggplot2_py/guide_axis.py +615 -0
- ggplot2_py/guide_colourbar.py +657 -0
- ggplot2_py/guide_legend.py +1061 -0
- ggplot2_py/guides/__init__.py +8 -0
- ggplot2_py/labeller.py +296 -0
- ggplot2_py/labels.py +309 -0
- ggplot2_py/layer.py +954 -0
- ggplot2_py/layout.py +754 -0
- ggplot2_py/limits.py +314 -0
- ggplot2_py/plot.py +1401 -0
- ggplot2_py/plot_render.py +866 -0
- ggplot2_py/position.py +1269 -0
- ggplot2_py/protocols.py +171 -0
- ggplot2_py/py.typed +0 -0
- ggplot2_py/qplot.py +233 -0
- ggplot2_py/resources/diamonds.csv +53941 -0
- ggplot2_py/resources/economics.csv +575 -0
- ggplot2_py/resources/economics_long.csv +2871 -0
- ggplot2_py/resources/faithfuld.csv +5626 -0
- ggplot2_py/resources/luv_colours.csv +658 -0
- ggplot2_py/resources/midwest.csv +438 -0
- ggplot2_py/resources/mpg.csv +235 -0
- ggplot2_py/resources/msleep.csv +84 -0
- ggplot2_py/resources/presidential.csv +13 -0
- ggplot2_py/resources/seals.csv +1156 -0
- ggplot2_py/resources/txhousing.csv +8603 -0
- ggplot2_py/save.py +316 -0
- ggplot2_py/scale.py +2727 -0
- ggplot2_py/scales/__init__.py +4252 -0
- ggplot2_py/stat.py +6071 -0
- ggplot2_py/stats/__init__.py +9 -0
- ggplot2_py/theme.py +490 -0
- ggplot2_py/theme_defaults.py +1350 -0
- ggplot2_py/theme_elements.py +2052 -0
- ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
- ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
- ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
- 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
|