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.
Files changed (177) hide show
  1. figrecipe/__init__.py +106 -973
  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 +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  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/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {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 warnings
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
- def list_available_fonts() -> List[str]:
25
- """List all available font families.
26
-
27
- Returns
28
- -------
29
- list of str
30
- Sorted list of available font family names.
31
-
32
- Examples
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 boxplot line widths to existing boxplot elements
308
- _apply_boxplot_style(ax, style)
309
-
310
- # Apply violinplot line widths to existing violinplot elements
311
- _apply_violinplot_style(ax, style)
312
-
313
- # Apply barplot edge widths to existing bar elements
314
- _apply_barplot_style(ax, style)
315
-
316
- # Apply histogram edge widths to existing histogram elements
317
- _apply_histogram_style(ax, style)
318
-
319
- # Apply pie chart styling
320
- _apply_pie_style(ax, style)
321
-
322
- # Apply imshow/matshow/spy styling (hide axes if configured)
323
- _apply_matrix_style(ax, style)
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 (with font availability check)
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 (for future legends)
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 (only for numeric axes, not categorical)
407
- # We defer tick configuration to avoid interfering with categorical axes
408
- # that get set up later by bar(), boxplot(), etc.
409
- n_ticks = style.get("n_ticks")
410
- if n_ticks is not None:
411
- # Store n_ticks preference on the axes for later application
412
- # This will be applied in _finalize_ticks() before saving
413
- ax._figrecipe_n_ticks = n_ticks
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