figrecipe 0.5.0__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.
- figrecipe/__init__.py +1090 -0
- figrecipe/_recorder.py +435 -0
- figrecipe/_reproducer.py +358 -0
- figrecipe/_seaborn.py +305 -0
- figrecipe/_serializer.py +227 -0
- figrecipe/_signatures/__init__.py +7 -0
- figrecipe/_signatures/_loader.py +186 -0
- figrecipe/_utils/__init__.py +32 -0
- figrecipe/_utils/_crop.py +261 -0
- figrecipe/_utils/_diff.py +98 -0
- figrecipe/_utils/_image_diff.py +204 -0
- figrecipe/_utils/_numpy_io.py +204 -0
- figrecipe/_utils/_units.py +200 -0
- figrecipe/_validator.py +186 -0
- figrecipe/_wrappers/__init__.py +8 -0
- figrecipe/_wrappers/_axes.py +327 -0
- figrecipe/_wrappers/_figure.py +227 -0
- figrecipe/plt.py +12 -0
- figrecipe/pyplot.py +264 -0
- figrecipe/styles/__init__.py +50 -0
- figrecipe/styles/_style_applier.py +412 -0
- figrecipe/styles/_style_loader.py +450 -0
- figrecipe-0.5.0.dist-info/METADATA +336 -0
- figrecipe-0.5.0.dist-info/RECORD +26 -0
- figrecipe-0.5.0.dist-info/WHEEL +4 -0
- figrecipe-0.5.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Style application utilities for figrecipe.
|
|
4
|
+
|
|
5
|
+
Applies mm-based styling to matplotlib axes for publication-quality figures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ["apply_style_mm", "apply_theme_colors", "check_font", "list_available_fonts"]
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from matplotlib.axes import Axes
|
|
14
|
+
|
|
15
|
+
from .._utils._units import mm_to_pt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_available_fonts() -> List[str]:
|
|
19
|
+
"""List all available font families.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
list of str
|
|
24
|
+
Sorted list of available font family names.
|
|
25
|
+
|
|
26
|
+
Examples
|
|
27
|
+
--------
|
|
28
|
+
>>> fonts = ps.list_available_fonts()
|
|
29
|
+
>>> print(fonts[:5])
|
|
30
|
+
['Arial', 'Courier New', 'DejaVu Sans', ...]
|
|
31
|
+
"""
|
|
32
|
+
import matplotlib.font_manager as fm
|
|
33
|
+
fonts = set()
|
|
34
|
+
for font in fm.fontManager.ttflist:
|
|
35
|
+
fonts.add(font.name)
|
|
36
|
+
return sorted(fonts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
|
|
40
|
+
"""Check if font is available, with fallback and helpful error message.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
font_family : str
|
|
45
|
+
Requested font family name.
|
|
46
|
+
fallback : str
|
|
47
|
+
Fallback font if requested font is not available.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
str
|
|
52
|
+
The font to use (original if available, fallback otherwise).
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> font = check_font("Arial") # Returns "Arial" if available
|
|
57
|
+
>>> font = check_font("NonExistentFont") # Returns fallback with warning
|
|
58
|
+
"""
|
|
59
|
+
import matplotlib.font_manager as fm
|
|
60
|
+
|
|
61
|
+
available = list_available_fonts()
|
|
62
|
+
|
|
63
|
+
if font_family in available:
|
|
64
|
+
return font_family
|
|
65
|
+
|
|
66
|
+
# Font not found - show helpful message
|
|
67
|
+
similar = [f for f in available if font_family.lower() in f.lower()]
|
|
68
|
+
|
|
69
|
+
msg = f"Font '{font_family}' not found.\n"
|
|
70
|
+
if similar:
|
|
71
|
+
msg += f" Similar fonts available: {similar[:5]}\n"
|
|
72
|
+
msg += f" Using fallback: '{fallback}'\n"
|
|
73
|
+
msg += f" To see all available fonts: ps.list_available_fonts()\n"
|
|
74
|
+
msg += f" To install Arial on Linux: sudo apt install ttf-mscorefonts-installer"
|
|
75
|
+
|
|
76
|
+
warnings.warn(msg, UserWarning)
|
|
77
|
+
|
|
78
|
+
return fallback if fallback in available else "DejaVu Sans"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Default theme color palettes (Monaco/VS Code style for dark)
|
|
82
|
+
THEME_COLORS = {
|
|
83
|
+
"dark": {
|
|
84
|
+
"figure_bg": "#1e1e1e", # VS Code main background
|
|
85
|
+
"axes_bg": "#252526", # VS Code panel background
|
|
86
|
+
"legend_bg": "#252526", # Same as axes
|
|
87
|
+
"text": "#d4d4d4", # VS Code default text
|
|
88
|
+
"spine": "#3c3c3c", # Subtle border color
|
|
89
|
+
"tick": "#d4d4d4", # Match text
|
|
90
|
+
"grid": "#3a3a3a", # Subtle grid
|
|
91
|
+
},
|
|
92
|
+
"light": {
|
|
93
|
+
"figure_bg": "none", # Transparent
|
|
94
|
+
"axes_bg": "none", # Transparent
|
|
95
|
+
"legend_bg": "none", # Transparent
|
|
96
|
+
"text": "black",
|
|
97
|
+
"spine": "black",
|
|
98
|
+
"tick": "black",
|
|
99
|
+
"grid": "#cccccc",
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def apply_theme_colors(
|
|
105
|
+
ax: Axes,
|
|
106
|
+
theme: str = "light",
|
|
107
|
+
custom_colors: Optional[Dict[str, str]] = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Apply theme colors to axes for dark/light mode support.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
ax : matplotlib.axes.Axes
|
|
114
|
+
Target axes to apply theme to
|
|
115
|
+
theme : str
|
|
116
|
+
Color theme: "light" or "dark" (default: "light")
|
|
117
|
+
custom_colors : dict, optional
|
|
118
|
+
Custom color overrides. Keys: figure_bg, axes_bg, legend_bg, text, spine, tick, grid
|
|
119
|
+
|
|
120
|
+
Examples
|
|
121
|
+
--------
|
|
122
|
+
>>> fig, ax = plt.subplots()
|
|
123
|
+
>>> apply_theme_colors(ax, theme="dark") # Eye-friendly dark mode
|
|
124
|
+
"""
|
|
125
|
+
# Get base theme colors
|
|
126
|
+
colors = THEME_COLORS.get(theme, THEME_COLORS["light"]).copy()
|
|
127
|
+
|
|
128
|
+
# Apply custom overrides
|
|
129
|
+
if custom_colors:
|
|
130
|
+
# Handle legacy key name (background -> figure_bg)
|
|
131
|
+
if "background" in custom_colors and "figure_bg" not in custom_colors:
|
|
132
|
+
custom_colors["figure_bg"] = custom_colors.pop("background")
|
|
133
|
+
colors.update(custom_colors)
|
|
134
|
+
|
|
135
|
+
# Helper to check for transparent/none
|
|
136
|
+
def is_transparent(color):
|
|
137
|
+
if color is None:
|
|
138
|
+
return False
|
|
139
|
+
return str(color).lower() in ("none", "transparent")
|
|
140
|
+
|
|
141
|
+
# Apply axes background (handle "none"/"transparent" for transparency)
|
|
142
|
+
axes_bg = colors.get("axes_bg", "none")
|
|
143
|
+
if is_transparent(axes_bg):
|
|
144
|
+
ax.set_facecolor("none")
|
|
145
|
+
ax.patch.set_alpha(0)
|
|
146
|
+
else:
|
|
147
|
+
ax.set_facecolor(axes_bg)
|
|
148
|
+
|
|
149
|
+
# Apply figure background if accessible
|
|
150
|
+
fig = ax.get_figure()
|
|
151
|
+
if fig is not None:
|
|
152
|
+
fig_bg = colors.get("figure_bg", "none")
|
|
153
|
+
if is_transparent(fig_bg):
|
|
154
|
+
fig.patch.set_facecolor("none")
|
|
155
|
+
fig.patch.set_alpha(0)
|
|
156
|
+
else:
|
|
157
|
+
fig.patch.set_facecolor(fig_bg)
|
|
158
|
+
|
|
159
|
+
# Apply text colors (labels, titles)
|
|
160
|
+
ax.xaxis.label.set_color(colors["text"])
|
|
161
|
+
ax.yaxis.label.set_color(colors["text"])
|
|
162
|
+
ax.title.set_color(colors["text"])
|
|
163
|
+
|
|
164
|
+
# Apply spine colors
|
|
165
|
+
for spine in ax.spines.values():
|
|
166
|
+
spine.set_color(colors["spine"])
|
|
167
|
+
|
|
168
|
+
# Apply tick colors (both marks and labels)
|
|
169
|
+
ax.tick_params(colors=colors["tick"], which="both")
|
|
170
|
+
for label in ax.get_xticklabels() + ax.get_yticklabels():
|
|
171
|
+
label.set_color(colors["tick"])
|
|
172
|
+
|
|
173
|
+
# Apply legend colors if legend exists
|
|
174
|
+
legend = ax.get_legend()
|
|
175
|
+
if legend is not None:
|
|
176
|
+
for text in legend.get_texts():
|
|
177
|
+
text.set_color(colors["text"])
|
|
178
|
+
title = legend.get_title()
|
|
179
|
+
if title:
|
|
180
|
+
title.set_color(colors["text"])
|
|
181
|
+
frame = legend.get_frame()
|
|
182
|
+
if frame:
|
|
183
|
+
legend_bg = colors.get("legend_bg", colors.get("axes_bg", "none"))
|
|
184
|
+
if is_transparent(legend_bg):
|
|
185
|
+
frame.set_facecolor("none")
|
|
186
|
+
frame.set_alpha(0)
|
|
187
|
+
else:
|
|
188
|
+
frame.set_facecolor(legend_bg)
|
|
189
|
+
frame.set_edgecolor(colors["spine"])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
193
|
+
"""Apply publication-quality style using millimeter-based settings.
|
|
194
|
+
|
|
195
|
+
This function applies styling to matplotlib axes using millimeter and point
|
|
196
|
+
measurements for precise control over visual elements.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
ax : matplotlib.axes.Axes
|
|
201
|
+
Target axes to apply styling to
|
|
202
|
+
style : dict
|
|
203
|
+
Dictionary containing styling parameters. Supported keys:
|
|
204
|
+
- 'axes_thickness_mm' (float): Spine line width in mm (default: 0.2)
|
|
205
|
+
- 'trace_thickness_mm' (float): Plot line width in mm (default: 0.3)
|
|
206
|
+
- 'tick_length_mm' (float): Tick mark length in mm (default: 1.0)
|
|
207
|
+
- 'tick_thickness_mm' (float): Tick mark width in mm (default: 0.2)
|
|
208
|
+
- 'marker_size_mm' (float): Default marker size in mm (default: 1.0)
|
|
209
|
+
- 'axis_font_size_pt' (float): Axis label font size in points (default: 8)
|
|
210
|
+
- 'tick_font_size_pt' (float): Tick label font size in points (default: 7)
|
|
211
|
+
- 'title_font_size_pt' (float): Title font size in points (default: 9)
|
|
212
|
+
- 'legend_font_size_pt' (float): Legend font size in points (default: 7)
|
|
213
|
+
- 'label_pad_pt' (float): Axis label padding in points (default: 2.0)
|
|
214
|
+
- 'tick_pad_pt' (float): Tick label padding in points (default: 2.0)
|
|
215
|
+
- 'title_pad_pt' (float): Title padding in points (default: 4.0)
|
|
216
|
+
- 'font_family' (str): Font family (default: "Arial")
|
|
217
|
+
- 'n_ticks' (int): Number of ticks on each axis (default: 5)
|
|
218
|
+
- 'theme' (str): Color theme "light" or "dark" (default: "light")
|
|
219
|
+
- 'theme_colors' (dict): Custom theme color overrides
|
|
220
|
+
- 'hide_top_spine' (bool): Hide top spine (default: True)
|
|
221
|
+
- 'hide_right_spine' (bool): Hide right spine (default: True)
|
|
222
|
+
- 'grid' (bool): Show grid (default: False)
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
float
|
|
227
|
+
Trace line width in points, to be used with ax.plot(..., lw=trace_lw)
|
|
228
|
+
|
|
229
|
+
Examples
|
|
230
|
+
--------
|
|
231
|
+
>>> fig, ax = plt.subplots()
|
|
232
|
+
>>> style = {
|
|
233
|
+
... 'axes_thickness_mm': 0.2,
|
|
234
|
+
... 'trace_thickness_mm': 0.3,
|
|
235
|
+
... 'tick_length_mm': 1.0,
|
|
236
|
+
... 'axis_font_size_pt': 8,
|
|
237
|
+
... 'theme': 'light',
|
|
238
|
+
... }
|
|
239
|
+
>>> trace_lw = apply_style_mm(ax, style)
|
|
240
|
+
>>> ax.plot(x, y, lw=trace_lw)
|
|
241
|
+
"""
|
|
242
|
+
# Apply theme colors (dark/light mode)
|
|
243
|
+
theme = style.get("theme", "light")
|
|
244
|
+
theme_colors = style.get("theme_colors", None)
|
|
245
|
+
apply_theme_colors(ax, theme, theme_colors)
|
|
246
|
+
|
|
247
|
+
# Convert spine thickness from mm to points
|
|
248
|
+
axes_lw_pt = mm_to_pt(style.get("axes_thickness_mm", 0.2))
|
|
249
|
+
for spine in ax.spines.values():
|
|
250
|
+
spine.set_linewidth(axes_lw_pt)
|
|
251
|
+
|
|
252
|
+
# Hide spines if requested
|
|
253
|
+
if style.get("hide_top_spine", True):
|
|
254
|
+
ax.spines["top"].set_visible(False)
|
|
255
|
+
if style.get("hide_right_spine", True):
|
|
256
|
+
ax.spines["right"].set_visible(False)
|
|
257
|
+
|
|
258
|
+
# Convert trace thickness from mm to points
|
|
259
|
+
trace_lw_pt = mm_to_pt(style.get("trace_thickness_mm", 0.3))
|
|
260
|
+
|
|
261
|
+
# Convert marker size from mm to points
|
|
262
|
+
marker_size_mm = style.get("marker_size_mm")
|
|
263
|
+
if marker_size_mm is not None:
|
|
264
|
+
import matplotlib as mpl
|
|
265
|
+
marker_size_pt = mm_to_pt(marker_size_mm)
|
|
266
|
+
mpl.rcParams["lines.markersize"] = marker_size_pt
|
|
267
|
+
|
|
268
|
+
# Configure tick parameters
|
|
269
|
+
tick_pad_pt = style.get("tick_pad_pt", 2.0)
|
|
270
|
+
tick_direction = style.get("tick_direction", "out")
|
|
271
|
+
ax.tick_params(
|
|
272
|
+
direction=tick_direction,
|
|
273
|
+
length=mm_to_pt(style.get("tick_length_mm", 1.0)),
|
|
274
|
+
width=mm_to_pt(style.get("tick_thickness_mm", 0.2)),
|
|
275
|
+
pad=tick_pad_pt,
|
|
276
|
+
top=False,
|
|
277
|
+
right=False,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Apply font sizes and family (with font availability check)
|
|
281
|
+
axis_fs = style.get("axis_font_size_pt", 8)
|
|
282
|
+
tick_fs = style.get("tick_font_size_pt", 7)
|
|
283
|
+
title_fs = style.get("title_font_size_pt", 9)
|
|
284
|
+
legend_fs = style.get("legend_font_size_pt", 7)
|
|
285
|
+
label_pad_pt = style.get("label_pad_pt", 2.0)
|
|
286
|
+
requested_font = style.get("font_family", "Arial")
|
|
287
|
+
font_family = check_font(requested_font)
|
|
288
|
+
|
|
289
|
+
ax.xaxis.label.set_fontsize(axis_fs)
|
|
290
|
+
ax.xaxis.label.set_fontfamily(font_family)
|
|
291
|
+
ax.xaxis.labelpad = label_pad_pt
|
|
292
|
+
ax.yaxis.label.set_fontsize(axis_fs)
|
|
293
|
+
ax.yaxis.label.set_fontfamily(font_family)
|
|
294
|
+
ax.yaxis.labelpad = label_pad_pt
|
|
295
|
+
|
|
296
|
+
for label in ax.get_xticklabels() + ax.get_yticklabels():
|
|
297
|
+
label.set_fontsize(tick_fs)
|
|
298
|
+
label.set_fontfamily(font_family)
|
|
299
|
+
|
|
300
|
+
# Set title font, size, and padding
|
|
301
|
+
ax.title.set_fontfamily(font_family)
|
|
302
|
+
ax.title.set_fontsize(title_fs)
|
|
303
|
+
title_pad_pt = style.get("title_pad_pt", 4.0)
|
|
304
|
+
ax.set_title(ax.get_title(), pad=title_pad_pt)
|
|
305
|
+
|
|
306
|
+
# Set legend font size and background via rcParams (for future legends)
|
|
307
|
+
import matplotlib as mpl
|
|
308
|
+
mpl.rcParams['legend.fontsize'] = legend_fs
|
|
309
|
+
mpl.rcParams['legend.title_fontsize'] = legend_fs
|
|
310
|
+
|
|
311
|
+
# Set legend colors from theme
|
|
312
|
+
theme = style.get("theme", "light")
|
|
313
|
+
theme_colors = style.get("theme_colors", None)
|
|
314
|
+
if theme_colors:
|
|
315
|
+
legend_bg = theme_colors.get("legend_bg", theme_colors.get("axes_bg", "white"))
|
|
316
|
+
text_color = theme_colors.get("text", "black")
|
|
317
|
+
spine_color = theme_colors.get("spine", "black")
|
|
318
|
+
else:
|
|
319
|
+
theme_dict = THEME_COLORS.get(theme, THEME_COLORS["light"])
|
|
320
|
+
legend_bg = theme_dict.get("legend_bg", "white")
|
|
321
|
+
text_color = theme_dict.get("text", "black")
|
|
322
|
+
spine_color = theme_dict.get("spine", "black")
|
|
323
|
+
|
|
324
|
+
# Handle transparent backgrounds
|
|
325
|
+
if str(legend_bg).lower() in ("none", "transparent"):
|
|
326
|
+
mpl.rcParams['legend.facecolor'] = 'none'
|
|
327
|
+
mpl.rcParams['legend.framealpha'] = 0
|
|
328
|
+
else:
|
|
329
|
+
mpl.rcParams['legend.facecolor'] = legend_bg
|
|
330
|
+
mpl.rcParams['legend.framealpha'] = 1.0
|
|
331
|
+
|
|
332
|
+
# Set legend text and edge colors
|
|
333
|
+
mpl.rcParams['legend.edgecolor'] = spine_color
|
|
334
|
+
mpl.rcParams['legend.labelcolor'] = text_color
|
|
335
|
+
|
|
336
|
+
legend = ax.get_legend()
|
|
337
|
+
if legend is not None:
|
|
338
|
+
for text in legend.get_texts():
|
|
339
|
+
text.set_fontsize(legend_fs)
|
|
340
|
+
text.set_fontfamily(font_family)
|
|
341
|
+
|
|
342
|
+
# Configure grid
|
|
343
|
+
if style.get("grid", False):
|
|
344
|
+
ax.grid(True, alpha=0.3)
|
|
345
|
+
else:
|
|
346
|
+
ax.grid(False)
|
|
347
|
+
|
|
348
|
+
# Configure number of ticks
|
|
349
|
+
n_ticks = style.get("n_ticks")
|
|
350
|
+
if n_ticks is not None:
|
|
351
|
+
from matplotlib.ticker import MaxNLocator
|
|
352
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
|
|
353
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
|
|
354
|
+
|
|
355
|
+
# Apply color palette to both rcParams and this specific axes
|
|
356
|
+
color_palette = style.get("color_palette")
|
|
357
|
+
if color_palette is not None:
|
|
358
|
+
import matplotlib as mpl
|
|
359
|
+
# Normalize colors (RGB 0-255 to 0-1)
|
|
360
|
+
normalized_palette = []
|
|
361
|
+
for c in color_palette:
|
|
362
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
363
|
+
# Check if already normalized
|
|
364
|
+
if all(v <= 1.0 for v in c):
|
|
365
|
+
normalized_palette.append(tuple(c))
|
|
366
|
+
else:
|
|
367
|
+
normalized_palette.append(tuple(v / 255.0 for v in c))
|
|
368
|
+
else:
|
|
369
|
+
normalized_palette.append(c)
|
|
370
|
+
# Set rcParams for future axes
|
|
371
|
+
mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=normalized_palette)
|
|
372
|
+
# Also set the color cycle on this specific axes (axes cache cycler at creation)
|
|
373
|
+
ax.set_prop_cycle(color=normalized_palette)
|
|
374
|
+
|
|
375
|
+
# Store style in axes for reference
|
|
376
|
+
if not hasattr(ax, "_figrecipe_style"):
|
|
377
|
+
ax._figrecipe_style = {}
|
|
378
|
+
ax._figrecipe_style.update(style)
|
|
379
|
+
|
|
380
|
+
return trace_lw_pt
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
import matplotlib.pyplot as plt
|
|
385
|
+
import numpy as np
|
|
386
|
+
|
|
387
|
+
# Test styling
|
|
388
|
+
print("Testing style application...")
|
|
389
|
+
|
|
390
|
+
fig, ax = plt.subplots(figsize=(4, 3))
|
|
391
|
+
|
|
392
|
+
style = {
|
|
393
|
+
"axes_thickness_mm": 0.2,
|
|
394
|
+
"trace_thickness_mm": 0.3,
|
|
395
|
+
"tick_length_mm": 1.0,
|
|
396
|
+
"tick_thickness_mm": 0.2,
|
|
397
|
+
"axis_font_size_pt": 8,
|
|
398
|
+
"tick_font_size_pt": 7,
|
|
399
|
+
"theme": "light",
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
trace_lw = apply_style_mm(ax, style)
|
|
403
|
+
|
|
404
|
+
x = np.linspace(0, 2 * np.pi, 100)
|
|
405
|
+
ax.plot(x, np.sin(x), lw=trace_lw)
|
|
406
|
+
ax.set_xlabel("X axis")
|
|
407
|
+
ax.set_ylabel("Y axis")
|
|
408
|
+
ax.set_title("Test Plot")
|
|
409
|
+
|
|
410
|
+
plt.savefig("/tmp/test_style.png", dpi=300, bbox_inches="tight")
|
|
411
|
+
print("Saved to /tmp/test_style.png")
|
|
412
|
+
plt.close()
|