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.
Files changed (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {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__ = ["apply_style_mm", "apply_theme_colors", "check_font", "list_available_fonts"]
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 warnings
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
- def list_available_fonts() -> List[str]:
19
- """List all available font families.
20
-
21
- Returns
22
- -------
23
- list of str
24
- Sorted list of available font family names.
25
-
26
- Examples
27
- --------
28
- >>> fonts = ps.list_available_fonts()
29
- >>> print(fonts[:5])
30
- ['Arial', 'Courier New', 'DejaVu Sans', ...]
31
- """
32
- import matplotlib.font_manager as fm
33
- fonts = set()
34
- for font in fm.fontManager.ttflist:
35
- fonts.add(font.name)
36
- return sorted(fonts)
37
-
38
-
39
- def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
40
- """Check if font is available, with fallback and helpful error message.
41
-
42
- Parameters
43
- ----------
44
- font_family : str
45
- Requested font family name.
46
- fallback : str
47
- Fallback font if requested font is not available.
48
-
49
- Returns
50
- -------
51
- str
52
- The font to use (original if available, fallback otherwise).
53
-
54
- Examples
55
- --------
56
- >>> font = check_font("Arial") # Returns "Arial" if available
57
- >>> font = check_font("NonExistentFont") # Returns fallback with warning
58
- """
59
- import matplotlib.font_manager as fm
60
-
61
- available = list_available_fonts()
62
-
63
- if font_family in available:
64
- return font_family
65
-
66
- # Font not found - show helpful message
67
- similar = [f for f in available if font_family.lower() in f.lower()]
68
-
69
- msg = f"Font '{font_family}' not found.\n"
70
- if similar:
71
- msg += f" Similar fonts available: {similar[:5]}\n"
72
- msg += f" Using fallback: '{fallback}'\n"
73
- msg += f" To see all available fonts: ps.list_available_fonts()\n"
74
- msg += f" To install Arial on Linux: sudo apt install ttf-mscorefonts-installer"
75
-
76
- warnings.warn(msg, UserWarning)
77
-
78
- return fallback if fallback in available else "DejaVu Sans"
79
-
80
-
81
- # Default theme color palettes (Monaco/VS Code style for dark)
82
- THEME_COLORS = {
83
- "dark": {
84
- "figure_bg": "#1e1e1e", # VS Code main background
85
- "axes_bg": "#252526", # VS Code panel background
86
- "legend_bg": "#252526", # Same as axes
87
- "text": "#d4d4d4", # VS Code default text
88
- "spine": "#3c3c3c", # Subtle border color
89
- "tick": "#d4d4d4", # Match text
90
- "grid": "#3a3a3a", # Subtle grid
91
- },
92
- "light": {
93
- "figure_bg": "none", # Transparent
94
- "axes_bg": "none", # Transparent
95
- "legend_bg": "none", # Transparent
96
- "text": "black",
97
- "spine": "black",
98
- "tick": "black",
99
- "grid": "#cccccc",
100
- },
101
- }
102
-
103
-
104
- def apply_theme_colors(
105
- ax: Axes,
106
- theme: str = "light",
107
- custom_colors: Optional[Dict[str, str]] = None,
108
- ) -> None:
109
- """Apply theme colors to axes for dark/light mode support.
110
-
111
- Parameters
112
- ----------
113
- ax : matplotlib.axes.Axes
114
- Target axes to apply theme to
115
- theme : str
116
- Color theme: "light" or "dark" (default: "light")
117
- custom_colors : dict, optional
118
- Custom color overrides. Keys: figure_bg, axes_bg, legend_bg, text, spine, tick, grid
119
-
120
- Examples
121
- --------
122
- >>> fig, ax = plt.subplots()
123
- >>> apply_theme_colors(ax, theme="dark") # Eye-friendly dark mode
124
- """
125
- # Get base theme colors
126
- colors = THEME_COLORS.get(theme, THEME_COLORS["light"]).copy()
127
-
128
- # Apply custom overrides
129
- if custom_colors:
130
- # Handle legacy key name (background -> figure_bg)
131
- if "background" in custom_colors and "figure_bg" not in custom_colors:
132
- custom_colors["figure_bg"] = custom_colors.pop("background")
133
- colors.update(custom_colors)
134
-
135
- # Helper to check for transparent/none
136
- def is_transparent(color):
137
- if color is None:
138
- return False
139
- return str(color).lower() in ("none", "transparent")
140
-
141
- # Apply axes background (handle "none"/"transparent" for transparency)
142
- axes_bg = colors.get("axes_bg", "none")
143
- if is_transparent(axes_bg):
144
- ax.set_facecolor("none")
145
- ax.patch.set_alpha(0)
146
- else:
147
- ax.set_facecolor(axes_bg)
148
-
149
- # Apply figure background if accessible
150
- fig = ax.get_figure()
151
- if fig is not None:
152
- fig_bg = colors.get("figure_bg", "none")
153
- if is_transparent(fig_bg):
154
- fig.patch.set_facecolor("none")
155
- fig.patch.set_alpha(0)
156
- else:
157
- fig.patch.set_facecolor(fig_bg)
158
-
159
- # Apply text colors (labels, titles)
160
- ax.xaxis.label.set_color(colors["text"])
161
- ax.yaxis.label.set_color(colors["text"])
162
- ax.title.set_color(colors["text"])
163
-
164
- # Apply spine colors
165
- for spine in ax.spines.values():
166
- spine.set_color(colors["spine"])
167
-
168
- # Apply tick colors (both marks and labels)
169
- ax.tick_params(colors=colors["tick"], which="both")
170
- for label in ax.get_xticklabels() + ax.get_yticklabels():
171
- label.set_color(colors["tick"])
172
-
173
- # Apply legend colors if legend exists
174
- legend = ax.get_legend()
175
- if legend is not None:
176
- for text in legend.get_texts():
177
- text.set_color(colors["text"])
178
- title = legend.get_title()
179
- if title:
180
- title.set_color(colors["text"])
181
- frame = legend.get_frame()
182
- if frame:
183
- legend_bg = colors.get("legend_bg", colors.get("axes_bg", "none"))
184
- if is_transparent(legend_bg):
185
- frame.set_facecolor("none")
186
- frame.set_alpha(0)
187
- else:
188
- frame.set_facecolor(legend_bg)
189
- frame.set_edgecolor(colors["spine"])
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 (with font availability check)
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 (for future legends)
307
- import matplotlib as mpl
308
- mpl.rcParams['legend.fontsize'] = legend_fs
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['legend.facecolor'] = 'none'
327
- mpl.rcParams['legend.framealpha'] = 0
188
+ mpl.rcParams["legend.facecolor"] = "none"
189
+ mpl.rcParams["legend.framealpha"] = 0
328
190
  else:
329
- mpl.rcParams['legend.facecolor'] = legend_bg
330
- mpl.rcParams['legend.framealpha'] = 1.0
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['legend.edgecolor'] = spine_color
334
- mpl.rcParams['legend.labelcolor'] = text_color
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
- n_ticks = style.get("n_ticks")
350
- if n_ticks is not None:
351
- from matplotlib.ticker import MaxNLocator
352
- ax.xaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
353
- ax.yaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
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
- # Set rcParams for future axes
371
- mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=normalized_palette)
372
- # Also set the color cycle on this specific axes (axes cache cycler at creation)
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["DotDict"] = None
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", # VS Code main background
180
- "axes_bg": "#1e1e1e", # Same as figure background
181
- "legend_bg": "#1e1e1e", # Same as figure background
182
- "text": "#d4d4d4", # VS Code default text
183
- "spine": "#3c3c3c", # Subtle border color
184
- "tick": "#d4d4d4", # Match text
185
- "grid": "#3a3a3a", # Subtle grid
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(style: Optional[Union[str, Path, bool]] = "SCITEX", dark: bool = False) -> Optional[DotDict]:
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 (isinstance(resolved_style, str) and ("/" in resolved_style or "\\" in resolved_style or resolved_style.endswith(".yaml"))):
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 to_subplots_kwargs(style: Optional[DotDict] = None) -> Dict[str, Any]:
347
- """Convert style DotDict to kwargs for ps.subplots().
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
- Keyword arguments for ps.subplots()
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
- if style is None:
366
- style = get_style()
367
-
368
- result = {
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