figrecipe 0.5.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 +220 -819
- 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 +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- 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/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- 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 +258 -0
- 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/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- 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 +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- 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 +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- 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/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- 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 +193 -170
- 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 +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- 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 +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- 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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,188 +5,31 @@
|
|
|
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
|
+
"finalize_special_plots",
|
|
14
|
+
"list_available_fonts",
|
|
15
|
+
]
|
|
9
16
|
|
|
10
|
-
import
|
|
11
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
+
from typing import Any, Dict
|
|
12
18
|
|
|
13
19
|
from matplotlib.axes import Axes
|
|
14
20
|
|
|
15
21
|
from .._utils._units import mm_to_pt
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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"])
|
|
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
|
|
190
33
|
|
|
191
34
|
|
|
192
35
|
def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
@@ -239,6 +82,8 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
239
82
|
>>> trace_lw = apply_style_mm(ax, style)
|
|
240
83
|
>>> ax.plot(x, y, lw=trace_lw)
|
|
241
84
|
"""
|
|
85
|
+
import matplotlib as mpl
|
|
86
|
+
|
|
242
87
|
# Apply theme colors (dark/light mode)
|
|
243
88
|
theme = style.get("theme", "light")
|
|
244
89
|
theme_colors = style.get("theme_colors", None)
|
|
@@ -261,10 +106,28 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
261
106
|
# Convert marker size from mm to points
|
|
262
107
|
marker_size_mm = style.get("marker_size_mm")
|
|
263
108
|
if marker_size_mm is not None:
|
|
264
|
-
import matplotlib as mpl
|
|
265
109
|
marker_size_pt = mm_to_pt(marker_size_mm)
|
|
266
110
|
mpl.rcParams["lines.markersize"] = marker_size_pt
|
|
267
111
|
|
|
112
|
+
# Set boxplot flier (outlier) marker size
|
|
113
|
+
flier_mm = style.get("markers_flier_mm", style.get("flier_mm"))
|
|
114
|
+
if flier_mm is not None:
|
|
115
|
+
flier_size_pt = mm_to_pt(flier_mm)
|
|
116
|
+
mpl.rcParams["boxplot.flierprops.markersize"] = flier_size_pt
|
|
117
|
+
|
|
118
|
+
# Set boxplot median color
|
|
119
|
+
median_color = style.get("boxplot_median_color")
|
|
120
|
+
if median_color is not None:
|
|
121
|
+
mpl.rcParams["boxplot.medianprops.color"] = median_color
|
|
122
|
+
|
|
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)
|
|
130
|
+
|
|
268
131
|
# Configure tick parameters
|
|
269
132
|
tick_pad_pt = style.get("tick_pad_pt", 2.0)
|
|
270
133
|
tick_direction = style.get("tick_direction", "out")
|
|
@@ -277,7 +140,7 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
277
140
|
right=False,
|
|
278
141
|
)
|
|
279
142
|
|
|
280
|
-
# Apply font sizes and family
|
|
143
|
+
# Apply font sizes and family
|
|
281
144
|
axis_fs = style.get("axis_font_size_pt", 8)
|
|
282
145
|
tick_fs = style.get("tick_font_size_pt", 7)
|
|
283
146
|
title_fs = style.get("title_font_size_pt", 9)
|
|
@@ -303,10 +166,9 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
303
166
|
title_pad_pt = style.get("title_pad_pt", 4.0)
|
|
304
167
|
ax.set_title(ax.get_title(), pad=title_pad_pt)
|
|
305
168
|
|
|
306
|
-
# Set legend font size and background via rcParams
|
|
307
|
-
|
|
308
|
-
mpl.rcParams[
|
|
309
|
-
mpl.rcParams['legend.title_fontsize'] = legend_fs
|
|
169
|
+
# Set legend font size and background via rcParams
|
|
170
|
+
mpl.rcParams["legend.fontsize"] = legend_fs
|
|
171
|
+
mpl.rcParams["legend.title_fontsize"] = legend_fs
|
|
310
172
|
|
|
311
173
|
# Set legend colors from theme
|
|
312
174
|
theme = style.get("theme", "light")
|
|
@@ -323,15 +185,15 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
323
185
|
|
|
324
186
|
# Handle transparent backgrounds
|
|
325
187
|
if str(legend_bg).lower() in ("none", "transparent"):
|
|
326
|
-
mpl.rcParams[
|
|
327
|
-
mpl.rcParams[
|
|
188
|
+
mpl.rcParams["legend.facecolor"] = "none"
|
|
189
|
+
mpl.rcParams["legend.framealpha"] = 0
|
|
328
190
|
else:
|
|
329
|
-
mpl.rcParams[
|
|
330
|
-
mpl.rcParams[
|
|
191
|
+
mpl.rcParams["legend.facecolor"] = legend_bg
|
|
192
|
+
mpl.rcParams["legend.framealpha"] = 1.0
|
|
331
193
|
|
|
332
194
|
# Set legend text and edge colors
|
|
333
|
-
mpl.rcParams[
|
|
334
|
-
mpl.rcParams[
|
|
195
|
+
mpl.rcParams["legend.edgecolor"] = spine_color
|
|
196
|
+
mpl.rcParams["legend.labelcolor"] = text_color
|
|
335
197
|
|
|
336
198
|
legend = ax.get_legend()
|
|
337
199
|
if legend is not None:
|
|
@@ -345,31 +207,26 @@ def apply_style_mm(ax: Axes, style: Dict[str, Any]) -> float:
|
|
|
345
207
|
else:
|
|
346
208
|
ax.grid(False)
|
|
347
209
|
|
|
348
|
-
# Configure number of ticks
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
ax.
|
|
353
|
-
ax.
|
|
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
|
|
354
216
|
|
|
355
217
|
# Apply color palette to both rcParams and this specific axes
|
|
356
218
|
color_palette = style.get("color_palette")
|
|
357
219
|
if color_palette is not None:
|
|
358
|
-
import matplotlib as mpl
|
|
359
|
-
# Normalize colors (RGB 0-255 to 0-1)
|
|
360
220
|
normalized_palette = []
|
|
361
221
|
for c in color_palette:
|
|
362
222
|
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
363
|
-
# Check if already normalized
|
|
364
223
|
if all(v <= 1.0 for v in c):
|
|
365
224
|
normalized_palette.append(tuple(c))
|
|
366
225
|
else:
|
|
367
226
|
normalized_palette.append(tuple(v / 255.0 for v in c))
|
|
368
227
|
else:
|
|
369
228
|
normalized_palette.append(c)
|
|
370
|
-
|
|
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)
|
|
229
|
+
mpl.rcParams["axes.prop_cycle"] = mpl.cycler(color=normalized_palette)
|
|
373
230
|
ax.set_prop_cycle(color=normalized_palette)
|
|
374
231
|
|
|
375
232
|
# Store style in axes for reference
|
|
@@ -384,7 +241,6 @@ if __name__ == "__main__":
|
|
|
384
241
|
import matplotlib.pyplot as plt
|
|
385
242
|
import numpy as np
|
|
386
243
|
|
|
387
|
-
# Test styling
|
|
388
244
|
print("Testing style application...")
|
|
389
245
|
|
|
390
246
|
fig, ax = plt.subplots(figsize=(4, 3))
|
|
@@ -410,3 +266,5 @@ if __name__ == "__main__":
|
|
|
410
266
|
plt.savefig("/tmp/test_style.png", dpi=300, bbox_inches="tight")
|
|
411
267
|
print("Saved to /tmp/test_style.png")
|
|
412
268
|
plt.close()
|
|
269
|
+
|
|
270
|
+
# EOF
|
|
@@ -18,12 +18,15 @@ Usage:
|
|
|
18
18
|
|
|
19
19
|
__all__ = [
|
|
20
20
|
"load_style",
|
|
21
|
+
"load_preset",
|
|
21
22
|
"unload_style",
|
|
22
23
|
"get_style",
|
|
24
|
+
"get_current_style_dict",
|
|
23
25
|
"reload_style",
|
|
24
26
|
"list_presets",
|
|
25
27
|
"STYLE",
|
|
26
28
|
"to_subplots_kwargs",
|
|
29
|
+
"DotDict",
|
|
27
30
|
]
|
|
28
31
|
|
|
29
32
|
from pathlib import Path
|
|
@@ -31,6 +34,8 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
31
34
|
|
|
32
35
|
from ruamel.yaml import YAML
|
|
33
36
|
|
|
37
|
+
from ._dotdict import DotDict
|
|
38
|
+
from ._kwargs_converter import to_subplots_kwargs
|
|
34
39
|
|
|
35
40
|
# Path to presets directory
|
|
36
41
|
_PRESETS_DIR = Path(__file__).parent / "presets"
|
|
@@ -44,7 +49,7 @@ _PRESET_ALIASES = {
|
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
# Global style cache
|
|
47
|
-
_STYLE_CACHE: Optional[
|
|
52
|
+
_STYLE_CACHE: Optional[DotDict] = None
|
|
48
53
|
_CURRENT_STYLE_NAME: Optional[str] = None
|
|
49
54
|
|
|
50
55
|
|
|
@@ -70,52 +75,6 @@ def list_presets() -> List[str]:
|
|
|
70
75
|
return ["MATPLOTLIB", "SCITEX"]
|
|
71
76
|
|
|
72
77
|
|
|
73
|
-
class DotDict(dict):
|
|
74
|
-
"""Dictionary with dot-notation access to nested keys.
|
|
75
|
-
|
|
76
|
-
Examples
|
|
77
|
-
--------
|
|
78
|
-
>>> d = DotDict({"axes": {"width_mm": 40}})
|
|
79
|
-
>>> d.axes.width_mm
|
|
80
|
-
40
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
def __getattr__(self, key: str) -> Any:
|
|
84
|
-
# Handle special methods first
|
|
85
|
-
if key == 'to_subplots_kwargs':
|
|
86
|
-
return lambda: to_subplots_kwargs(self)
|
|
87
|
-
try:
|
|
88
|
-
value = self[key]
|
|
89
|
-
if isinstance(value, dict) and not isinstance(value, DotDict):
|
|
90
|
-
value = DotDict(value)
|
|
91
|
-
self[key] = value
|
|
92
|
-
return value
|
|
93
|
-
except KeyError:
|
|
94
|
-
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
95
|
-
|
|
96
|
-
def __setattr__(self, key: str, value: Any) -> None:
|
|
97
|
-
self[key] = value
|
|
98
|
-
|
|
99
|
-
def __delattr__(self, key: str) -> None:
|
|
100
|
-
try:
|
|
101
|
-
del self[key]
|
|
102
|
-
except KeyError:
|
|
103
|
-
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
104
|
-
|
|
105
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
106
|
-
"""Get value with default, supporting nested keys with dots."""
|
|
107
|
-
if "." in key:
|
|
108
|
-
parts = key.split(".")
|
|
109
|
-
value = self
|
|
110
|
-
for part in parts:
|
|
111
|
-
if isinstance(value, dict) and part in value:
|
|
112
|
-
value = value[part]
|
|
113
|
-
else:
|
|
114
|
-
return default
|
|
115
|
-
return value
|
|
116
|
-
return super().get(key, default)
|
|
117
|
-
|
|
118
|
-
|
|
119
78
|
def _deep_merge(base: Dict, override: Dict) -> Dict:
|
|
120
79
|
"""Deep merge two dictionaries, with override taking precedence."""
|
|
121
80
|
result = base.copy()
|
|
@@ -172,17 +131,18 @@ def _apply_dark_theme(style_dict: Dict) -> Dict:
|
|
|
172
131
|
Style dictionary with dark theme applied
|
|
173
132
|
"""
|
|
174
133
|
import copy
|
|
134
|
+
|
|
175
135
|
result = copy.deepcopy(style_dict)
|
|
176
136
|
|
|
177
137
|
# Monaco/VS Code dark theme colors (from scitex-cloud UIUX.md)
|
|
178
138
|
dark_colors = {
|
|
179
|
-
"figure_bg": "#1e1e1e",
|
|
180
|
-
"axes_bg": "#1e1e1e",
|
|
181
|
-
"legend_bg": "#1e1e1e",
|
|
182
|
-
"text": "#d4d4d4",
|
|
183
|
-
"spine": "#3c3c3c",
|
|
184
|
-
"tick": "#d4d4d4",
|
|
185
|
-
"grid": "#3a3a3a",
|
|
139
|
+
"figure_bg": "#1e1e1e", # VS Code main background
|
|
140
|
+
"axes_bg": "#1e1e1e", # Same as figure background
|
|
141
|
+
"legend_bg": "#1e1e1e", # Same as figure background
|
|
142
|
+
"text": "#d4d4d4", # VS Code default text
|
|
143
|
+
"spine": "#3c3c3c", # Subtle border color
|
|
144
|
+
"tick": "#d4d4d4", # Match text
|
|
145
|
+
"grid": "#3a3a3a", # Subtle grid
|
|
186
146
|
}
|
|
187
147
|
|
|
188
148
|
# Update theme section
|
|
@@ -198,6 +158,43 @@ def _apply_dark_theme(style_dict: Dict) -> Dict:
|
|
|
198
158
|
return result
|
|
199
159
|
|
|
200
160
|
|
|
161
|
+
def load_preset(name: str, dark: bool = False) -> Optional[DotDict]:
|
|
162
|
+
"""Load a style preset without affecting global cache.
|
|
163
|
+
|
|
164
|
+
This is useful for GUI editors that need to switch themes
|
|
165
|
+
without affecting the global state.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
name : str
|
|
170
|
+
Preset name (e.g., "SCITEX", "MATPLOTLIB")
|
|
171
|
+
dark : bool, optional
|
|
172
|
+
If True, apply dark theme transformation
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
DotDict or None
|
|
177
|
+
Style configuration as DotDict, or None if not found
|
|
178
|
+
"""
|
|
179
|
+
# Resolve aliases
|
|
180
|
+
resolved_name = _PRESET_ALIASES.get(name.upper(), name.upper())
|
|
181
|
+
|
|
182
|
+
# Find the preset file
|
|
183
|
+
style_path = _PRESETS_DIR / f"{resolved_name}.yaml"
|
|
184
|
+
|
|
185
|
+
if not style_path.exists():
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
style_dict = _load_yaml(style_path)
|
|
189
|
+
style_dict["_name"] = name.upper()
|
|
190
|
+
|
|
191
|
+
# Apply dark theme if requested
|
|
192
|
+
if dark:
|
|
193
|
+
style_dict = _apply_dark_theme(style_dict)
|
|
194
|
+
|
|
195
|
+
return DotDict(style_dict)
|
|
196
|
+
|
|
197
|
+
|
|
201
198
|
def unload_style() -> None:
|
|
202
199
|
"""Unload the current style and reset to matplotlib defaults.
|
|
203
200
|
|
|
@@ -218,10 +215,13 @@ def unload_style() -> None:
|
|
|
218
215
|
|
|
219
216
|
# Reset matplotlib rcParams to defaults
|
|
220
217
|
import matplotlib as mpl
|
|
218
|
+
|
|
221
219
|
mpl.rcParams.update(mpl.rcParamsDefault)
|
|
222
220
|
|
|
223
221
|
|
|
224
|
-
def load_style(
|
|
222
|
+
def load_style(
|
|
223
|
+
style: Optional[Union[str, Path, bool]] = "SCITEX", dark: bool = False
|
|
224
|
+
) -> Optional[DotDict]:
|
|
225
225
|
"""Load style configuration from preset or YAML file.
|
|
226
226
|
|
|
227
227
|
Parameters
|
|
@@ -294,7 +294,14 @@ def load_style(style: Optional[Union[str, Path, bool]] = "SCITEX", dark: bool =
|
|
|
294
294
|
resolved_style = base_style
|
|
295
295
|
|
|
296
296
|
# Determine the style path
|
|
297
|
-
if isinstance(resolved_style, Path) or (
|
|
297
|
+
if isinstance(resolved_style, Path) or (
|
|
298
|
+
isinstance(resolved_style, str)
|
|
299
|
+
and (
|
|
300
|
+
"/" in resolved_style
|
|
301
|
+
or "\\" in resolved_style
|
|
302
|
+
or resolved_style.endswith(".yaml")
|
|
303
|
+
)
|
|
304
|
+
):
|
|
298
305
|
# Explicit file path
|
|
299
306
|
style_path = Path(resolved_style)
|
|
300
307
|
style_name = str(resolved_style)
|
|
@@ -343,76 +350,19 @@ def get_style() -> DotDict:
|
|
|
343
350
|
return _STYLE_CACHE
|
|
344
351
|
|
|
345
352
|
|
|
346
|
-
def
|
|
347
|
-
"""
|
|
348
|
-
|
|
349
|
-
Parameters
|
|
350
|
-
----------
|
|
351
|
-
style : DotDict, optional
|
|
352
|
-
Style configuration. If None, uses current loaded style.
|
|
353
|
+
def get_current_style_dict() -> Dict[str, Any]:
|
|
354
|
+
"""Get current style as a flat dictionary for style applier functions.
|
|
353
355
|
|
|
354
356
|
Returns
|
|
355
357
|
-------
|
|
356
358
|
dict
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
Examples
|
|
360
|
-
--------
|
|
361
|
-
>>> style = load_style()
|
|
362
|
-
>>> kwargs = to_subplots_kwargs(style)
|
|
363
|
-
>>> fig, ax = ps.subplots(**kwargs)
|
|
359
|
+
Flattened style dictionary with keys like 'pie_show_axes', 'imshow_show_axes'.
|
|
360
|
+
Returns empty dict if no style is loaded.
|
|
364
361
|
"""
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# Axes dimensions
|
|
370
|
-
"axes_width_mm": style.axes.width_mm,
|
|
371
|
-
"axes_height_mm": style.axes.height_mm,
|
|
372
|
-
"axes_thickness_mm": style.axes.thickness_mm,
|
|
373
|
-
# Margins
|
|
374
|
-
"margin_left_mm": style.margins.left_mm,
|
|
375
|
-
"margin_right_mm": style.margins.right_mm,
|
|
376
|
-
"margin_bottom_mm": style.margins.bottom_mm,
|
|
377
|
-
"margin_top_mm": style.margins.top_mm,
|
|
378
|
-
# Spacing
|
|
379
|
-
"space_w_mm": style.spacing.horizontal_mm,
|
|
380
|
-
"space_h_mm": style.spacing.vertical_mm,
|
|
381
|
-
# Ticks
|
|
382
|
-
"tick_length_mm": style.ticks.length_mm,
|
|
383
|
-
"tick_thickness_mm": style.ticks.thickness_mm,
|
|
384
|
-
"n_ticks": style.ticks.n_ticks,
|
|
385
|
-
# Lines
|
|
386
|
-
"trace_thickness_mm": style.lines.trace_mm,
|
|
387
|
-
# Markers
|
|
388
|
-
"marker_size_mm": style.markers.size_mm,
|
|
389
|
-
# Fonts
|
|
390
|
-
"font_family": style.fonts.family,
|
|
391
|
-
"axis_font_size_pt": style.fonts.axis_label_pt,
|
|
392
|
-
"tick_font_size_pt": style.fonts.tick_label_pt,
|
|
393
|
-
"title_font_size_pt": style.fonts.title_pt,
|
|
394
|
-
"suptitle_font_size_pt": style.fonts.suptitle_pt,
|
|
395
|
-
"legend_font_size_pt": style.fonts.legend_pt,
|
|
396
|
-
# Padding
|
|
397
|
-
"label_pad_pt": style.padding.label_pt,
|
|
398
|
-
"tick_pad_pt": style.padding.tick_pt,
|
|
399
|
-
"title_pad_pt": style.padding.title_pt,
|
|
400
|
-
# Output
|
|
401
|
-
"dpi": style.output.dpi,
|
|
402
|
-
# Theme
|
|
403
|
-
"theme": style.theme.mode,
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
# Add theme colors from preset if available
|
|
407
|
-
theme_mode = style.theme.mode
|
|
408
|
-
if "theme" in style and theme_mode in style.theme:
|
|
409
|
-
result["theme_colors"] = dict(style.theme[theme_mode])
|
|
410
|
-
|
|
411
|
-
# Add color palette if available
|
|
412
|
-
if "colors" in style and "palette" in style.colors:
|
|
413
|
-
result["color_palette"] = list(style.colors.palette)
|
|
414
|
-
|
|
415
|
-
return result
|
|
362
|
+
global _STYLE_CACHE
|
|
363
|
+
if _STYLE_CACHE is None:
|
|
364
|
+
return {}
|
|
365
|
+
return to_subplots_kwargs(_STYLE_CACHE)
|
|
416
366
|
|
|
417
367
|
|
|
418
368
|
# Lazy-loaded global STYLE object
|
|
@@ -448,3 +398,5 @@ if __name__ == "__main__":
|
|
|
448
398
|
|
|
449
399
|
print("\nUsing STYLE proxy...")
|
|
450
400
|
print(f" STYLE.fonts.family: {STYLE.fonts.family}")
|
|
401
|
+
|
|
402
|
+
# EOF
|