figrecipe 0.5.0__py3-none-any.whl → 0.6.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 +361 -93
- figrecipe/_dev/__init__.py +120 -0
- figrecipe/_dev/demo_plotters/__init__.py +195 -0
- figrecipe/_dev/demo_plotters/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_pie.py +23 -0
- figrecipe/_dev/demo_plotters/plot_plot.py +27 -0
- figrecipe/_dev/demo_plotters/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/plot_streamplot.py +30 -0
- figrecipe/_dev/demo_plotters/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_xcorr.py +25 -0
- figrecipe/_editor/__init__.py +230 -0
- figrecipe/_editor/_bbox.py +978 -0
- figrecipe/_editor/_flask_app.py +1229 -0
- figrecipe/_editor/_hitmap.py +937 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_renderer.py +349 -0
- figrecipe/_editor/_templates/__init__.py +75 -0
- figrecipe/_editor/_templates/_html.py +406 -0
- figrecipe/_editor/_templates/_scripts.py +2778 -0
- figrecipe/_editor/_templates/_styles.py +1326 -0
- figrecipe/_params/_DECORATION_METHODS.py +27 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +126 -73
- figrecipe/_reproducer.py +658 -41
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_loader.py +515 -56
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +860 -46
- figrecipe/_wrappers/_figure.py +115 -18
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +9 -10
- figrecipe/styles/_style_applier.py +332 -28
- figrecipe/styles/_style_loader.py +172 -44
- figrecipe/styles/presets/MATPLOTLIB.yaml +94 -0
- figrecipe/styles/presets/SCITEX.yaml +176 -0
- figrecipe-0.6.0.dist-info/METADATA +394 -0
- figrecipe-0.6.0.dist-info/RECORD +90 -0
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
Applies mm-based styling to matplotlib axes for publication-quality figures.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"apply_style_mm",
|
|
10
|
+
"apply_theme_colors",
|
|
11
|
+
"check_font",
|
|
12
|
+
"finalize_ticks",
|
|
13
|
+
"list_available_fonts",
|
|
14
|
+
]
|
|
9
15
|
|
|
10
16
|
import warnings
|
|
11
17
|
from typing import Any, Dict, List, Optional
|
|
@@ -30,6 +36,7 @@ def list_available_fonts() -> List[str]:
|
|
|
30
36
|
['Arial', 'Courier New', 'DejaVu Sans', ...]
|
|
31
37
|
"""
|
|
32
38
|
import matplotlib.font_manager as fm
|
|
39
|
+
|
|
33
40
|
fonts = set()
|
|
34
41
|
for font in fm.fontManager.ttflist:
|
|
35
42
|
fonts.add(font.name)
|
|
@@ -56,7 +63,6 @@ def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
|
|
|
56
63
|
>>> font = check_font("Arial") # Returns "Arial" if available
|
|
57
64
|
>>> font = check_font("NonExistentFont") # Returns fallback with warning
|
|
58
65
|
"""
|
|
59
|
-
import matplotlib.font_manager as fm
|
|
60
66
|
|
|
61
67
|
available = list_available_fonts()
|
|
62
68
|
|
|
@@ -70,8 +76,8 @@ def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
|
|
|
70
76
|
if similar:
|
|
71
77
|
msg += f" Similar fonts available: {similar[:5]}\n"
|
|
72
78
|
msg += f" Using fallback: '{fallback}'\n"
|
|
73
|
-
msg +=
|
|
74
|
-
msg +=
|
|
79
|
+
msg += " To see all available fonts: ps.list_available_fonts()\n"
|
|
80
|
+
msg += " To install Arial on Linux: sudo apt install ttf-mscorefonts-installer"
|
|
75
81
|
|
|
76
82
|
warnings.warn(msg, UserWarning)
|
|
77
83
|
|
|
@@ -81,18 +87,18 @@ def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
|
|
|
81
87
|
# Default theme color palettes (Monaco/VS Code style for dark)
|
|
82
88
|
THEME_COLORS = {
|
|
83
89
|
"dark": {
|
|
84
|
-
"figure_bg": "#1e1e1e",
|
|
85
|
-
"axes_bg": "#252526",
|
|
86
|
-
"legend_bg": "#252526",
|
|
87
|
-
"text": "#d4d4d4",
|
|
88
|
-
"spine": "#3c3c3c",
|
|
89
|
-
"tick": "#d4d4d4",
|
|
90
|
-
"grid": "#3a3a3a",
|
|
90
|
+
"figure_bg": "#1e1e1e", # VS Code main background
|
|
91
|
+
"axes_bg": "#252526", # VS Code panel background
|
|
92
|
+
"legend_bg": "#252526", # Same as axes
|
|
93
|
+
"text": "#d4d4d4", # VS Code default text
|
|
94
|
+
"spine": "#3c3c3c", # Subtle border color
|
|
95
|
+
"tick": "#d4d4d4", # Match text
|
|
96
|
+
"grid": "#3a3a3a", # Subtle grid
|
|
91
97
|
},
|
|
92
98
|
"light": {
|
|
93
|
-
"figure_bg": "none",
|
|
94
|
-
"axes_bg": "none",
|
|
95
|
-
"legend_bg": "none",
|
|
99
|
+
"figure_bg": "none", # Transparent
|
|
100
|
+
"axes_bg": "none", # Transparent
|
|
101
|
+
"legend_bg": "none", # Transparent
|
|
96
102
|
"text": "black",
|
|
97
103
|
"spine": "black",
|
|
98
104
|
"tick": "black",
|
|
@@ -112,8 +118,9 @@ def apply_theme_colors(
|
|
|
112
118
|
----------
|
|
113
119
|
ax : matplotlib.axes.Axes
|
|
114
120
|
Target axes to apply theme to
|
|
115
|
-
theme : str
|
|
121
|
+
theme : str or dict
|
|
116
122
|
Color theme: "light" or "dark" (default: "light")
|
|
123
|
+
If dict, extracts 'mode' key (for YAML-style theme dicts)
|
|
117
124
|
custom_colors : dict, optional
|
|
118
125
|
Custom color overrides. Keys: figure_bg, axes_bg, legend_bg, text, spine, tick, grid
|
|
119
126
|
|
|
@@ -122,6 +129,14 @@ def apply_theme_colors(
|
|
|
122
129
|
>>> fig, ax = plt.subplots()
|
|
123
130
|
>>> apply_theme_colors(ax, theme="dark") # Eye-friendly dark mode
|
|
124
131
|
"""
|
|
132
|
+
# Handle dict-style theme (from YAML: {mode: "light", dark: {...}})
|
|
133
|
+
if isinstance(theme, dict):
|
|
134
|
+
theme = theme.get("mode", "light")
|
|
135
|
+
|
|
136
|
+
# Ensure theme is a string
|
|
137
|
+
if not isinstance(theme, str):
|
|
138
|
+
theme = "light"
|
|
139
|
+
|
|
125
140
|
# Get base theme colors
|
|
126
141
|
colors = THEME_COLORS.get(theme, THEME_COLORS["light"]).copy()
|
|
127
142
|
|
|
@@ -156,6 +171,14 @@ def apply_theme_colors(
|
|
|
156
171
|
else:
|
|
157
172
|
fig.patch.set_facecolor(fig_bg)
|
|
158
173
|
|
|
174
|
+
# Apply text colors to figure-level text elements (suptitle, supxlabel, supylabel)
|
|
175
|
+
if hasattr(fig, "_suptitle") and fig._suptitle is not None:
|
|
176
|
+
fig._suptitle.set_color(colors["text"])
|
|
177
|
+
if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
|
|
178
|
+
fig._supxlabel.set_color(colors["text"])
|
|
179
|
+
if hasattr(fig, "_supylabel") and fig._supylabel is not None:
|
|
180
|
+
fig._supylabel.set_color(colors["text"])
|
|
181
|
+
|
|
159
182
|
# Apply text colors (labels, titles)
|
|
160
183
|
ax.xaxis.label.set_color(colors["text"])
|
|
161
184
|
ax.yaxis.label.set_color(colors["text"])
|
|
@@ -262,9 +285,43 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
262
285
|
marker_size_mm = style.get("marker_size_mm")
|
|
263
286
|
if marker_size_mm is not None:
|
|
264
287
|
import matplotlib as mpl
|
|
288
|
+
|
|
265
289
|
marker_size_pt = mm_to_pt(marker_size_mm)
|
|
266
290
|
mpl.rcParams["lines.markersize"] = marker_size_pt
|
|
267
291
|
|
|
292
|
+
# Set boxplot flier (outlier) marker size
|
|
293
|
+
flier_mm = style.get("markers_flier_mm", style.get("flier_mm"))
|
|
294
|
+
if flier_mm is not None:
|
|
295
|
+
import matplotlib as mpl
|
|
296
|
+
|
|
297
|
+
flier_size_pt = mm_to_pt(flier_mm)
|
|
298
|
+
mpl.rcParams["boxplot.flierprops.markersize"] = flier_size_pt
|
|
299
|
+
|
|
300
|
+
# Set boxplot median color
|
|
301
|
+
median_color = style.get("boxplot_median_color")
|
|
302
|
+
if median_color is not None:
|
|
303
|
+
import matplotlib as mpl
|
|
304
|
+
|
|
305
|
+
mpl.rcParams["boxplot.medianprops.color"] = median_color
|
|
306
|
+
|
|
307
|
+
# Apply boxplot line widths to existing boxplot elements
|
|
308
|
+
_apply_boxplot_style(ax, style)
|
|
309
|
+
|
|
310
|
+
# Apply violinplot line widths to existing violinplot elements
|
|
311
|
+
_apply_violinplot_style(ax, style)
|
|
312
|
+
|
|
313
|
+
# Apply barplot edge widths to existing bar elements
|
|
314
|
+
_apply_barplot_style(ax, style)
|
|
315
|
+
|
|
316
|
+
# Apply histogram edge widths to existing histogram elements
|
|
317
|
+
_apply_histogram_style(ax, style)
|
|
318
|
+
|
|
319
|
+
# Apply pie chart styling
|
|
320
|
+
_apply_pie_style(ax, style)
|
|
321
|
+
|
|
322
|
+
# Apply imshow/matshow/spy styling (hide axes if configured)
|
|
323
|
+
_apply_matrix_style(ax, style)
|
|
324
|
+
|
|
268
325
|
# Configure tick parameters
|
|
269
326
|
tick_pad_pt = style.get("tick_pad_pt", 2.0)
|
|
270
327
|
tick_direction = style.get("tick_direction", "out")
|
|
@@ -305,8 +362,9 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
305
362
|
|
|
306
363
|
# Set legend font size and background via rcParams (for future legends)
|
|
307
364
|
import matplotlib as mpl
|
|
308
|
-
|
|
309
|
-
mpl.rcParams[
|
|
365
|
+
|
|
366
|
+
mpl.rcParams["legend.fontsize"] = legend_fs
|
|
367
|
+
mpl.rcParams["legend.title_fontsize"] = legend_fs
|
|
310
368
|
|
|
311
369
|
# Set legend colors from theme
|
|
312
370
|
theme = style.get("theme", "light")
|
|
@@ -323,15 +381,15 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
323
381
|
|
|
324
382
|
# Handle transparent backgrounds
|
|
325
383
|
if str(legend_bg).lower() in ("none", "transparent"):
|
|
326
|
-
mpl.rcParams[
|
|
327
|
-
mpl.rcParams[
|
|
384
|
+
mpl.rcParams["legend.facecolor"] = "none"
|
|
385
|
+
mpl.rcParams["legend.framealpha"] = 0
|
|
328
386
|
else:
|
|
329
|
-
mpl.rcParams[
|
|
330
|
-
mpl.rcParams[
|
|
387
|
+
mpl.rcParams["legend.facecolor"] = legend_bg
|
|
388
|
+
mpl.rcParams["legend.framealpha"] = 1.0
|
|
331
389
|
|
|
332
390
|
# Set legend text and edge colors
|
|
333
|
-
mpl.rcParams[
|
|
334
|
-
mpl.rcParams[
|
|
391
|
+
mpl.rcParams["legend.edgecolor"] = spine_color
|
|
392
|
+
mpl.rcParams["legend.labelcolor"] = text_color
|
|
335
393
|
|
|
336
394
|
legend = ax.get_legend()
|
|
337
395
|
if legend is not None:
|
|
@@ -345,17 +403,20 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
345
403
|
else:
|
|
346
404
|
ax.grid(False)
|
|
347
405
|
|
|
348
|
-
# Configure number of ticks
|
|
406
|
+
# Configure number of ticks (only for numeric axes, not categorical)
|
|
407
|
+
# We defer tick configuration to avoid interfering with categorical axes
|
|
408
|
+
# that get set up later by bar(), boxplot(), etc.
|
|
349
409
|
n_ticks = style.get("n_ticks")
|
|
350
410
|
if n_ticks is not None:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
ax.
|
|
411
|
+
# Store n_ticks preference on the axes for later application
|
|
412
|
+
# This will be applied in _finalize_ticks() before saving
|
|
413
|
+
ax._figrecipe_n_ticks = n_ticks
|
|
354
414
|
|
|
355
415
|
# Apply color palette to both rcParams and this specific axes
|
|
356
416
|
color_palette = style.get("color_palette")
|
|
357
417
|
if color_palette is not None:
|
|
358
418
|
import matplotlib as mpl
|
|
419
|
+
|
|
359
420
|
# Normalize colors (RGB 0-255 to 0-1)
|
|
360
421
|
normalized_palette = []
|
|
361
422
|
for c in color_palette:
|
|
@@ -368,7 +429,7 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
368
429
|
else:
|
|
369
430
|
normalized_palette.append(c)
|
|
370
431
|
# Set rcParams for future axes
|
|
371
|
-
mpl.rcParams[
|
|
432
|
+
mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=normalized_palette)
|
|
372
433
|
# Also set the color cycle on this specific axes (axes cache cycler at creation)
|
|
373
434
|
ax.set_prop_cycle(color=normalized_palette)
|
|
374
435
|
|
|
@@ -380,6 +441,249 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
380
441
|
return trace_lw_pt
|
|
381
442
|
|
|
382
443
|
|
|
444
|
+
def _apply_boxplot_style(ax: Axes, style: Dict[str, Any]) -> None:
|
|
445
|
+
"""Apply boxplot line width styling to existing boxplot elements.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
ax : matplotlib.axes.Axes
|
|
450
|
+
Target axes containing boxplot elements.
|
|
451
|
+
style : dict
|
|
452
|
+
Style dictionary with boxplot_* keys.
|
|
453
|
+
"""
|
|
454
|
+
from matplotlib.lines import Line2D
|
|
455
|
+
from matplotlib.patches import PathPatch
|
|
456
|
+
|
|
457
|
+
# Get line widths from style
|
|
458
|
+
box_lw = mm_to_pt(style.get("boxplot_line_mm", 0.2))
|
|
459
|
+
whisker_lw = mm_to_pt(style.get("boxplot_whisker_mm", 0.2))
|
|
460
|
+
cap_lw = mm_to_pt(style.get("boxplot_cap_mm", 0.2))
|
|
461
|
+
median_lw = mm_to_pt(style.get("boxplot_median_mm", 0.2))
|
|
462
|
+
median_color = style.get("boxplot_median_color", "black")
|
|
463
|
+
flier_edge_lw = mm_to_pt(style.get("boxplot_flier_edge_mm", 0.2))
|
|
464
|
+
|
|
465
|
+
# Boxplot creates Line2D objects for whiskers, caps, medians, fliers
|
|
466
|
+
# and PathPatch objects for boxes
|
|
467
|
+
for child in ax.get_children():
|
|
468
|
+
# Check if it's a boxplot box (PathPatch with specific properties)
|
|
469
|
+
if isinstance(child, PathPatch):
|
|
470
|
+
# Boxes are typically PathPatch with edgecolor
|
|
471
|
+
if child.get_edgecolor() is not None:
|
|
472
|
+
child.set_linewidth(box_lw)
|
|
473
|
+
|
|
474
|
+
# Check for Line2D objects (whiskers, caps, medians, fliers)
|
|
475
|
+
elif isinstance(child, Line2D):
|
|
476
|
+
xdata = child.get_xdata()
|
|
477
|
+
ydata = child.get_ydata()
|
|
478
|
+
|
|
479
|
+
# Fliers are markers with no line (linestyle='None' or '')
|
|
480
|
+
# and typically have varying number of points (outliers)
|
|
481
|
+
marker = child.get_marker()
|
|
482
|
+
linestyle = child.get_linestyle()
|
|
483
|
+
if marker and marker != "None" and linestyle in ("None", "", " "):
|
|
484
|
+
# This is likely a flier (outlier marker)
|
|
485
|
+
child.set_markeredgewidth(flier_edge_lw)
|
|
486
|
+
elif len(xdata) == 2 and len(ydata) == 2:
|
|
487
|
+
# Horizontal line (could be median or cap)
|
|
488
|
+
if ydata[0] == ydata[1]:
|
|
489
|
+
# Check if it's likely a median (middle of box) or cap
|
|
490
|
+
# Medians are usually solid, caps are at extremes
|
|
491
|
+
if linestyle == "-":
|
|
492
|
+
# Could be median - apply median style
|
|
493
|
+
child.set_linewidth(median_lw)
|
|
494
|
+
if median_color:
|
|
495
|
+
child.set_color(median_color)
|
|
496
|
+
else:
|
|
497
|
+
child.set_linewidth(cap_lw)
|
|
498
|
+
# Vertical line (whisker)
|
|
499
|
+
elif xdata[0] == xdata[1]:
|
|
500
|
+
child.set_linewidth(whisker_lw)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _apply_violinplot_style(ax: Axes, style: Dict[str, Any]) -> None:
|
|
504
|
+
"""Apply violinplot line width styling to existing violinplot elements.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
ax : matplotlib.axes.Axes
|
|
509
|
+
Target axes containing violinplot elements.
|
|
510
|
+
style : dict
|
|
511
|
+
Style dictionary with violinplot_* keys.
|
|
512
|
+
"""
|
|
513
|
+
from matplotlib.collections import LineCollection, PolyCollection
|
|
514
|
+
|
|
515
|
+
# Get line widths from style
|
|
516
|
+
body_lw = mm_to_pt(style.get("violinplot_line_mm", 0.2))
|
|
517
|
+
whisker_lw = mm_to_pt(style.get("violinplot_whisker_mm", 0.2))
|
|
518
|
+
|
|
519
|
+
for child in ax.get_children():
|
|
520
|
+
# Violin bodies are PolyCollection
|
|
521
|
+
if isinstance(child, PolyCollection):
|
|
522
|
+
child.set_linewidth(body_lw)
|
|
523
|
+
|
|
524
|
+
# Violin inner elements (cbars, cmins, cmaxes) are LineCollection
|
|
525
|
+
elif isinstance(child, LineCollection):
|
|
526
|
+
child.set_linewidth(whisker_lw)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _apply_barplot_style(ax: Axes, style: Dict[str, Any]) -> None:
|
|
530
|
+
"""Apply barplot edge styling to existing bar elements.
|
|
531
|
+
|
|
532
|
+
Parameters
|
|
533
|
+
----------
|
|
534
|
+
ax : matplotlib.axes.Axes
|
|
535
|
+
Target axes containing bar elements.
|
|
536
|
+
style : dict
|
|
537
|
+
Style dictionary with barplot_* keys.
|
|
538
|
+
"""
|
|
539
|
+
from matplotlib.patches import Rectangle
|
|
540
|
+
|
|
541
|
+
# Get edge width from style
|
|
542
|
+
edge_lw = mm_to_pt(style.get("barplot_edge_mm", 0.2))
|
|
543
|
+
|
|
544
|
+
# Bar plots create Rectangle patches
|
|
545
|
+
for patch in ax.patches:
|
|
546
|
+
if isinstance(patch, Rectangle):
|
|
547
|
+
patch.set_linewidth(edge_lw)
|
|
548
|
+
# Set edge color to black for clean scientific look
|
|
549
|
+
patch.set_edgecolor("black")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _apply_histogram_style(ax: Axes, style: Dict[str, Any]) -> None:
|
|
553
|
+
"""Apply histogram edge styling to existing histogram elements.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
ax : matplotlib.axes.Axes
|
|
558
|
+
Target axes containing histogram elements.
|
|
559
|
+
style : dict
|
|
560
|
+
Style dictionary with histogram_* keys.
|
|
561
|
+
"""
|
|
562
|
+
from matplotlib.patches import Rectangle
|
|
563
|
+
|
|
564
|
+
# Get edge width from style
|
|
565
|
+
edge_lw = mm_to_pt(style.get("histogram_edge_mm", 0.2))
|
|
566
|
+
|
|
567
|
+
# Histograms also create Rectangle patches
|
|
568
|
+
for patch in ax.patches:
|
|
569
|
+
if isinstance(patch, Rectangle):
|
|
570
|
+
patch.set_linewidth(edge_lw)
|
|
571
|
+
# Set edge color to black for clean scientific look
|
|
572
|
+
patch.set_edgecolor("black")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _apply_pie_style(ax: Axes, style: Dict[str, Any]) -> None:
|
|
576
|
+
"""Apply pie chart styling to existing pie elements.
|
|
577
|
+
|
|
578
|
+
Parameters
|
|
579
|
+
----------
|
|
580
|
+
ax : matplotlib.axes.Axes
|
|
581
|
+
Target axes containing pie chart elements.
|
|
582
|
+
style : dict
|
|
583
|
+
Style dictionary with pie_* keys.
|
|
584
|
+
"""
|
|
585
|
+
from matplotlib.patches import Wedge
|
|
586
|
+
|
|
587
|
+
# Check if axes contains pie chart (wedge patches)
|
|
588
|
+
has_pie = any(isinstance(p, Wedge) for p in ax.patches)
|
|
589
|
+
if not has_pie:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
# Get pie text size from style (default 6pt for scientific publications)
|
|
593
|
+
text_pt = style.get("pie_text_pt", 6)
|
|
594
|
+
show_axes = style.get("pie_show_axes", False)
|
|
595
|
+
font_family = check_font(style.get("font_family", "Arial"))
|
|
596
|
+
|
|
597
|
+
# Apply text size to all pie text elements (labels and percentages)
|
|
598
|
+
for text in ax.texts:
|
|
599
|
+
text.set_fontsize(text_pt)
|
|
600
|
+
text.set_fontfamily(font_family)
|
|
601
|
+
|
|
602
|
+
# Hide axes if configured (default: hide for pie charts)
|
|
603
|
+
if not show_axes:
|
|
604
|
+
ax.set_xticks([])
|
|
605
|
+
ax.set_yticks([])
|
|
606
|
+
ax.set_xticklabels([])
|
|
607
|
+
ax.set_yticklabels([])
|
|
608
|
+
# Hide spines
|
|
609
|
+
for spine in ax.spines.values():
|
|
610
|
+
spine.set_visible(False)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _apply_matrix_style(ax: Axes, style: Dict[str, Any]) -> None:
|
|
614
|
+
"""Apply imshow/matshow/spy styling (hide axes if configured).
|
|
615
|
+
|
|
616
|
+
Parameters
|
|
617
|
+
----------
|
|
618
|
+
ax : matplotlib.axes.Axes
|
|
619
|
+
Target axes containing matrix plot elements.
|
|
620
|
+
style : dict
|
|
621
|
+
Style dictionary with imshow_*, matshow_*, spy_* keys.
|
|
622
|
+
"""
|
|
623
|
+
from matplotlib.image import AxesImage
|
|
624
|
+
|
|
625
|
+
# Check if axes contains an image (imshow/matshow)
|
|
626
|
+
has_image = any(isinstance(c, AxesImage) for c in ax.get_children())
|
|
627
|
+
if not has_image:
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
# Check if imshow_show_axes is False
|
|
631
|
+
show_axes = style.get("imshow_show_axes", True)
|
|
632
|
+
show_labels = style.get("imshow_show_labels", True)
|
|
633
|
+
|
|
634
|
+
if not show_axes:
|
|
635
|
+
ax.set_xticks([])
|
|
636
|
+
ax.set_yticks([])
|
|
637
|
+
ax.set_xticklabels([])
|
|
638
|
+
ax.set_yticklabels([])
|
|
639
|
+
# Hide spines
|
|
640
|
+
for spine in ax.spines.values():
|
|
641
|
+
spine.set_visible(False)
|
|
642
|
+
|
|
643
|
+
if not show_labels:
|
|
644
|
+
ax.set_xlabel("")
|
|
645
|
+
ax.set_ylabel("")
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def finalize_ticks(ax: Axes) -> None:
|
|
649
|
+
"""
|
|
650
|
+
Apply deferred tick configuration after all plotting is done.
|
|
651
|
+
|
|
652
|
+
This function applies the n_ticks setting stored by apply_style_mm(),
|
|
653
|
+
but only to numeric axes (not categorical).
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
ax : matplotlib.axes.Axes
|
|
658
|
+
The axes to finalize.
|
|
659
|
+
"""
|
|
660
|
+
from matplotlib.ticker import MaxNLocator
|
|
661
|
+
|
|
662
|
+
n_ticks = getattr(ax, "_figrecipe_n_ticks", None)
|
|
663
|
+
if n_ticks is None:
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
# Check if x-axis is categorical (has string tick labels)
|
|
667
|
+
x_labels = [t.get_text() for t in ax.get_xticklabels()]
|
|
668
|
+
x_is_categorical = any(
|
|
669
|
+
lbl and not lbl.replace(".", "").replace("-", "").replace("+", "").isdigit()
|
|
670
|
+
for lbl in x_labels
|
|
671
|
+
if lbl
|
|
672
|
+
)
|
|
673
|
+
if not x_is_categorical:
|
|
674
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
|
|
675
|
+
|
|
676
|
+
# Check if y-axis is categorical
|
|
677
|
+
y_labels = [t.get_text() for t in ax.get_yticklabels()]
|
|
678
|
+
y_is_categorical = any(
|
|
679
|
+
lbl and not lbl.replace(".", "").replace("-", "").replace("+", "").isdigit()
|
|
680
|
+
for lbl in y_labels
|
|
681
|
+
if lbl
|
|
682
|
+
)
|
|
683
|
+
if not y_is_categorical:
|
|
684
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
|
|
685
|
+
|
|
686
|
+
|
|
383
687
|
if __name__ == "__main__":
|
|
384
688
|
import matplotlib.pyplot as plt
|
|
385
689
|
import numpy as np
|