figrecipe 0.6.0__py3-none-any.whl → 0.7.4__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 +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,206 +10,26 @@ __all__ = [
|
|
|
10
10
|
"apply_theme_colors",
|
|
11
11
|
"check_font",
|
|
12
12
|
"finalize_ticks",
|
|
13
|
+
"finalize_special_plots",
|
|
13
14
|
"list_available_fonts",
|
|
14
15
|
]
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
+
from typing import Any, Dict
|
|
18
18
|
|
|
19
19
|
from matplotlib.axes import Axes
|
|
20
20
|
|
|
21
21
|
from .._utils._units import mm_to_pt
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
--------
|
|
34
|
-
>>> fonts = ps.list_available_fonts()
|
|
35
|
-
>>> print(fonts[:5])
|
|
36
|
-
['Arial', 'Courier New', 'DejaVu Sans', ...]
|
|
37
|
-
"""
|
|
38
|
-
import matplotlib.font_manager as fm
|
|
39
|
-
|
|
40
|
-
fonts = set()
|
|
41
|
-
for font in fm.fontManager.ttflist:
|
|
42
|
-
fonts.add(font.name)
|
|
43
|
-
return sorted(fonts)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
|
|
47
|
-
"""Check if font is available, with fallback and helpful error message.
|
|
48
|
-
|
|
49
|
-
Parameters
|
|
50
|
-
----------
|
|
51
|
-
font_family : str
|
|
52
|
-
Requested font family name.
|
|
53
|
-
fallback : str
|
|
54
|
-
Fallback font if requested font is not available.
|
|
55
|
-
|
|
56
|
-
Returns
|
|
57
|
-
-------
|
|
58
|
-
str
|
|
59
|
-
The font to use (original if available, fallback otherwise).
|
|
60
|
-
|
|
61
|
-
Examples
|
|
62
|
-
--------
|
|
63
|
-
>>> font = check_font("Arial") # Returns "Arial" if available
|
|
64
|
-
>>> font = check_font("NonExistentFont") # Returns fallback with warning
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
available = list_available_fonts()
|
|
68
|
-
|
|
69
|
-
if font_family in available:
|
|
70
|
-
return font_family
|
|
71
|
-
|
|
72
|
-
# Font not found - show helpful message
|
|
73
|
-
similar = [f for f in available if font_family.lower() in f.lower()]
|
|
74
|
-
|
|
75
|
-
msg = f"Font '{font_family}' not found.\n"
|
|
76
|
-
if similar:
|
|
77
|
-
msg += f" Similar fonts available: {similar[:5]}\n"
|
|
78
|
-
msg += f" Using fallback: '{fallback}'\n"
|
|
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"
|
|
81
|
-
|
|
82
|
-
warnings.warn(msg, UserWarning)
|
|
83
|
-
|
|
84
|
-
return fallback if fallback in available else "DejaVu Sans"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# Default theme color palettes (Monaco/VS Code style for dark)
|
|
88
|
-
THEME_COLORS = {
|
|
89
|
-
"dark": {
|
|
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
|
|
97
|
-
},
|
|
98
|
-
"light": {
|
|
99
|
-
"figure_bg": "none", # Transparent
|
|
100
|
-
"axes_bg": "none", # Transparent
|
|
101
|
-
"legend_bg": "none", # Transparent
|
|
102
|
-
"text": "black",
|
|
103
|
-
"spine": "black",
|
|
104
|
-
"tick": "black",
|
|
105
|
-
"grid": "#cccccc",
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def apply_theme_colors(
|
|
111
|
-
ax: Axes,
|
|
112
|
-
theme: str = "light",
|
|
113
|
-
custom_colors: Optional[Dict[str, str]] = None,
|
|
114
|
-
) -> None:
|
|
115
|
-
"""Apply theme colors to axes for dark/light mode support.
|
|
116
|
-
|
|
117
|
-
Parameters
|
|
118
|
-
----------
|
|
119
|
-
ax : matplotlib.axes.Axes
|
|
120
|
-
Target axes to apply theme to
|
|
121
|
-
theme : str or dict
|
|
122
|
-
Color theme: "light" or "dark" (default: "light")
|
|
123
|
-
If dict, extracts 'mode' key (for YAML-style theme dicts)
|
|
124
|
-
custom_colors : dict, optional
|
|
125
|
-
Custom color overrides. Keys: figure_bg, axes_bg, legend_bg, text, spine, tick, grid
|
|
126
|
-
|
|
127
|
-
Examples
|
|
128
|
-
--------
|
|
129
|
-
>>> fig, ax = plt.subplots()
|
|
130
|
-
>>> apply_theme_colors(ax, theme="dark") # Eye-friendly dark mode
|
|
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
|
-
|
|
140
|
-
# Get base theme colors
|
|
141
|
-
colors = THEME_COLORS.get(theme, THEME_COLORS["light"]).copy()
|
|
142
|
-
|
|
143
|
-
# Apply custom overrides
|
|
144
|
-
if custom_colors:
|
|
145
|
-
# Handle legacy key name (background -> figure_bg)
|
|
146
|
-
if "background" in custom_colors and "figure_bg" not in custom_colors:
|
|
147
|
-
custom_colors["figure_bg"] = custom_colors.pop("background")
|
|
148
|
-
colors.update(custom_colors)
|
|
149
|
-
|
|
150
|
-
# Helper to check for transparent/none
|
|
151
|
-
def is_transparent(color):
|
|
152
|
-
if color is None:
|
|
153
|
-
return False
|
|
154
|
-
return str(color).lower() in ("none", "transparent")
|
|
155
|
-
|
|
156
|
-
# Apply axes background (handle "none"/"transparent" for transparency)
|
|
157
|
-
axes_bg = colors.get("axes_bg", "none")
|
|
158
|
-
if is_transparent(axes_bg):
|
|
159
|
-
ax.set_facecolor("none")
|
|
160
|
-
ax.patch.set_alpha(0)
|
|
161
|
-
else:
|
|
162
|
-
ax.set_facecolor(axes_bg)
|
|
163
|
-
|
|
164
|
-
# Apply figure background if accessible
|
|
165
|
-
fig = ax.get_figure()
|
|
166
|
-
if fig is not None:
|
|
167
|
-
fig_bg = colors.get("figure_bg", "none")
|
|
168
|
-
if is_transparent(fig_bg):
|
|
169
|
-
fig.patch.set_facecolor("none")
|
|
170
|
-
fig.patch.set_alpha(0)
|
|
171
|
-
else:
|
|
172
|
-
fig.patch.set_facecolor(fig_bg)
|
|
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
|
-
|
|
182
|
-
# Apply text colors (labels, titles)
|
|
183
|
-
ax.xaxis.label.set_color(colors["text"])
|
|
184
|
-
ax.yaxis.label.set_color(colors["text"])
|
|
185
|
-
ax.title.set_color(colors["text"])
|
|
186
|
-
|
|
187
|
-
# Apply spine colors
|
|
188
|
-
for spine in ax.spines.values():
|
|
189
|
-
spine.set_color(colors["spine"])
|
|
190
|
-
|
|
191
|
-
# Apply tick colors (both marks and labels)
|
|
192
|
-
ax.tick_params(colors=colors["tick"], which="both")
|
|
193
|
-
for label in ax.get_xticklabels() + ax.get_yticklabels():
|
|
194
|
-
label.set_color(colors["tick"])
|
|
195
|
-
|
|
196
|
-
# Apply legend colors if legend exists
|
|
197
|
-
legend = ax.get_legend()
|
|
198
|
-
if legend is not None:
|
|
199
|
-
for text in legend.get_texts():
|
|
200
|
-
text.set_color(colors["text"])
|
|
201
|
-
title = legend.get_title()
|
|
202
|
-
if title:
|
|
203
|
-
title.set_color(colors["text"])
|
|
204
|
-
frame = legend.get_frame()
|
|
205
|
-
if frame:
|
|
206
|
-
legend_bg = colors.get("legend_bg", colors.get("axes_bg", "none"))
|
|
207
|
-
if is_transparent(legend_bg):
|
|
208
|
-
frame.set_facecolor("none")
|
|
209
|
-
frame.set_alpha(0)
|
|
210
|
-
else:
|
|
211
|
-
frame.set_facecolor(legend_bg)
|
|
212
|
-
frame.set_edgecolor(colors["spine"])
|
|
22
|
+
from ._finalize import finalize_special_plots, finalize_ticks
|
|
23
|
+
from ._fonts import check_font, list_available_fonts
|
|
24
|
+
from ._plot_styles import (
|
|
25
|
+
apply_barplot_style,
|
|
26
|
+
apply_boxplot_style,
|
|
27
|
+
apply_histogram_style,
|
|
28
|
+
apply_matrix_style,
|
|
29
|
+
apply_pie_style,
|
|
30
|
+
apply_violinplot_style,
|
|
31
|
+
)
|
|
32
|
+
from ._themes import THEME_COLORS, apply_theme_colors
|
|
213
33
|
|
|
214
34
|
|
|
215
35
|
def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
@@ -262,6 +82,8 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
262
82
|
>>> trace_lw = apply_style_mm(ax, style)
|
|
263
83
|
>>> ax.plot(x, y, lw=trace_lw)
|
|
264
84
|
"""
|
|
85
|
+
import matplotlib as mpl
|
|
86
|
+
|
|
265
87
|
# Apply theme colors (dark/light mode)
|
|
266
88
|
theme = style.get("theme", "light")
|
|
267
89
|
theme_colors = style.get("theme_colors", None)
|
|
@@ -284,43 +106,27 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
284
106
|
# Convert marker size from mm to points
|
|
285
107
|
marker_size_mm = style.get("marker_size_mm")
|
|
286
108
|
if marker_size_mm is not None:
|
|
287
|
-
import matplotlib as mpl
|
|
288
|
-
|
|
289
109
|
marker_size_pt = mm_to_pt(marker_size_mm)
|
|
290
110
|
mpl.rcParams["lines.markersize"] = marker_size_pt
|
|
291
111
|
|
|
292
112
|
# Set boxplot flier (outlier) marker size
|
|
293
113
|
flier_mm = style.get("markers_flier_mm", style.get("flier_mm"))
|
|
294
114
|
if flier_mm is not None:
|
|
295
|
-
import matplotlib as mpl
|
|
296
|
-
|
|
297
115
|
flier_size_pt = mm_to_pt(flier_mm)
|
|
298
116
|
mpl.rcParams["boxplot.flierprops.markersize"] = flier_size_pt
|
|
299
117
|
|
|
300
118
|
# Set boxplot median color
|
|
301
119
|
median_color = style.get("boxplot_median_color")
|
|
302
120
|
if median_color is not None:
|
|
303
|
-
import matplotlib as mpl
|
|
304
|
-
|
|
305
121
|
mpl.rcParams["boxplot.medianprops.color"] = median_color
|
|
306
122
|
|
|
307
|
-
# Apply
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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)
|
|
123
|
+
# Apply plot-specific styles
|
|
124
|
+
apply_boxplot_style(ax, style)
|
|
125
|
+
apply_violinplot_style(ax, style)
|
|
126
|
+
apply_barplot_style(ax, style)
|
|
127
|
+
apply_histogram_style(ax, style)
|
|
128
|
+
apply_pie_style(ax, style)
|
|
129
|
+
apply_matrix_style(ax, style)
|
|
324
130
|
|
|
325
131
|
# Configure tick parameters
|
|
326
132
|
tick_pad_pt = style.get("tick_pad_pt", 2.0)
|
|
@@ -334,7 +140,7 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
334
140
|
right=False,
|
|
335
141
|
)
|
|
336
142
|
|
|
337
|
-
# Apply font sizes and family
|
|
143
|
+
# Apply font sizes and family
|
|
338
144
|
axis_fs = style.get("axis_font_size_pt", 8)
|
|
339
145
|
tick_fs = style.get("tick_font_size_pt", 7)
|
|
340
146
|
title_fs = style.get("title_font_size_pt", 9)
|
|
@@ -360,9 +166,7 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
360
166
|
title_pad_pt = style.get("title_pad_pt", 4.0)
|
|
361
167
|
ax.set_title(ax.get_title(), pad=title_pad_pt)
|
|
362
168
|
|
|
363
|
-
# Set legend font size and background via rcParams
|
|
364
|
-
import matplotlib as mpl
|
|
365
|
-
|
|
169
|
+
# Set legend font size and background via rcParams
|
|
366
170
|
mpl.rcParams["legend.fontsize"] = legend_fs
|
|
367
171
|
mpl.rcParams["legend.title_fontsize"] = legend_fs
|
|
368
172
|
|
|
@@ -403,34 +207,26 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
403
207
|
else:
|
|
404
208
|
ax.grid(False)
|
|
405
209
|
|
|
406
|
-
# Configure number of ticks (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# This will be applied in _finalize_ticks() before saving
|
|
413
|
-
ax._figrecipe_n_ticks = n_ticks
|
|
210
|
+
# Configure number of ticks (deferred to finalize_ticks)
|
|
211
|
+
n_ticks_min = style.get("n_ticks_min")
|
|
212
|
+
n_ticks_max = style.get("n_ticks_max")
|
|
213
|
+
if n_ticks_min is not None or n_ticks_max is not None:
|
|
214
|
+
ax._figrecipe_n_ticks_min = n_ticks_min or 3
|
|
215
|
+
ax._figrecipe_n_ticks_max = n_ticks_max or 4
|
|
414
216
|
|
|
415
217
|
# Apply color palette to both rcParams and this specific axes
|
|
416
218
|
color_palette = style.get("color_palette")
|
|
417
219
|
if color_palette is not None:
|
|
418
|
-
import matplotlib as mpl
|
|
419
|
-
|
|
420
|
-
# Normalize colors (RGB 0-255 to 0-1)
|
|
421
220
|
normalized_palette = []
|
|
422
221
|
for c in color_palette:
|
|
423
222
|
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
424
|
-
# Check if already normalized
|
|
425
223
|
if all(v <= 1.0 for v in c):
|
|
426
224
|
normalized_palette.append(tuple(c))
|
|
427
225
|
else:
|
|
428
226
|
normalized_palette.append(tuple(v / 255.0 for v in c))
|
|
429
227
|
else:
|
|
430
228
|
normalized_palette.append(c)
|
|
431
|
-
# Set rcParams for future axes
|
|
432
229
|
mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=normalized_palette)
|
|
433
|
-
# Also set the color cycle on this specific axes (axes cache cycler at creation)
|
|
434
230
|
ax.set_prop_cycle(color=normalized_palette)
|
|
435
231
|
|
|
436
232
|
# Store style in axes for reference
|
|
@@ -441,254 +237,10 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
441
237
|
return trace_lw_pt
|
|
442
238
|
|
|
443
239
|
|
|
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
|
-
|
|
687
240
|
if __name__ == "__main__":
|
|
688
241
|
import matplotlib.pyplot as plt
|
|
689
242
|
import numpy as np
|
|
690
243
|
|
|
691
|
-
# Test styling
|
|
692
244
|
print("Testing style application...")
|
|
693
245
|
|
|
694
246
|
fig, ax = plt.subplots(figsize=(4, 3))
|
|
@@ -714,3 +266,5 @@ if __name__ == "__main__":
|
|
|
714
266
|
plt.savefig("/tmp/test_style.png", dpi=300, bbox_inches="tight")
|
|
715
267
|
print("Saved to /tmp/test_style.png")
|
|
716
268
|
plt.close()
|
|
269
|
+
|
|
270
|
+
# EOF
|