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
@@ -3,16 +3,17 @@
3
3
  """Wrapped Figure that manages recording."""
4
4
 
5
5
  from pathlib import Path
6
- from typing import Any, Dict, List, Literal, Optional, Tuple, Union, TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, Union
7
7
 
8
8
  import matplotlib.pyplot as plt
9
+ import numpy as np
9
10
  from matplotlib.figure import Figure
11
+ from numpy.typing import NDArray
10
12
 
11
13
  from ._axes import RecordingAxes
12
14
 
13
15
  if TYPE_CHECKING:
14
- from .._recorder import Recorder, FigureRecord
15
- from .._utils._numpy_io import DataFormat
16
+ from .._recorder import FigureRecord, Recorder
16
17
 
17
18
 
18
19
  class RecordingFigure:
@@ -79,6 +80,229 @@ class RecordingFigure:
79
80
  """Get the figure record."""
80
81
  return self._recorder.figure_record
81
82
 
83
+ def _get_style_fontsize(self, key: str, default: float) -> float:
84
+ """Get fontsize from loaded style."""
85
+ try:
86
+ from ..styles._style_loader import _STYLE_CACHE
87
+
88
+ if _STYLE_CACHE is not None:
89
+ fonts = getattr(_STYLE_CACHE, "fonts", None)
90
+ if fonts is not None:
91
+ return getattr(fonts, key, default)
92
+ except Exception:
93
+ pass
94
+ return default
95
+
96
+ def _get_theme_text_color(self, default: str = "black") -> str:
97
+ """Get text color from loaded style's theme settings."""
98
+ try:
99
+ from ..styles._style_loader import _STYLE_CACHE
100
+
101
+ if _STYLE_CACHE is not None:
102
+ theme = getattr(_STYLE_CACHE, "theme", None)
103
+ if theme is not None:
104
+ mode = getattr(theme, "mode", "light")
105
+ theme_colors = getattr(theme, mode, None)
106
+ if theme_colors is not None:
107
+ return getattr(theme_colors, "text", default)
108
+ except Exception:
109
+ pass
110
+ return default
111
+
112
+ def suptitle(self, t: str, **kwargs) -> Any:
113
+ """Set super title for the figure and record it.
114
+
115
+ Parameters
116
+ ----------
117
+ t : str
118
+ The super title text.
119
+ **kwargs
120
+ Additional arguments passed to matplotlib's suptitle().
121
+
122
+ Returns
123
+ -------
124
+ Text
125
+ The matplotlib Text object.
126
+ """
127
+ # Auto-apply fontsize from style if not specified
128
+ if "fontsize" not in kwargs:
129
+ kwargs["fontsize"] = self._get_style_fontsize("suptitle_pt", 10)
130
+ # Record the suptitle call
131
+ self._recorder.figure_record.suptitle = {"text": t, "kwargs": kwargs}
132
+ # Call the underlying figure's suptitle
133
+ return self._fig.suptitle(t, **kwargs)
134
+
135
+ def supxlabel(self, t: str, **kwargs) -> Any:
136
+ """Set super x-label for the figure and record it.
137
+
138
+ Parameters
139
+ ----------
140
+ t : str
141
+ The super x-label text.
142
+ **kwargs
143
+ Additional arguments passed to matplotlib's supxlabel().
144
+
145
+ Returns
146
+ -------
147
+ Text
148
+ The matplotlib Text object.
149
+ """
150
+ # Auto-apply fontsize from style if not specified
151
+ if "fontsize" not in kwargs:
152
+ kwargs["fontsize"] = self._get_style_fontsize("supxlabel_pt", 8)
153
+ # Record the supxlabel call
154
+ self._recorder.figure_record.supxlabel = {"text": t, "kwargs": kwargs}
155
+ # Call the underlying figure's supxlabel
156
+ return self._fig.supxlabel(t, **kwargs)
157
+
158
+ def supylabel(self, t: str, **kwargs) -> Any:
159
+ """Set super y-label for the figure and record it.
160
+
161
+ Parameters
162
+ ----------
163
+ t : str
164
+ The super y-label text.
165
+ **kwargs
166
+ Additional arguments passed to matplotlib's supylabel().
167
+
168
+ Returns
169
+ -------
170
+ Text
171
+ The matplotlib Text object.
172
+ """
173
+ # Auto-apply fontsize from style if not specified
174
+ if "fontsize" not in kwargs:
175
+ kwargs["fontsize"] = self._get_style_fontsize("supylabel_pt", 8)
176
+ # Record the supylabel call
177
+ self._recorder.figure_record.supylabel = {"text": t, "kwargs": kwargs}
178
+ # Call the underlying figure's supylabel
179
+ return self._fig.supylabel(t, **kwargs)
180
+
181
+ def add_panel_labels(
182
+ self,
183
+ labels: Optional[List[str]] = None,
184
+ loc: str = "upper left",
185
+ offset: Tuple[float, float] = (-0.1, 1.05),
186
+ fontsize: Optional[float] = None,
187
+ fontweight: str = "bold",
188
+ **kwargs,
189
+ ) -> List[Any]:
190
+ """Add panel labels (A, B, C, D, etc.) to multi-panel figures.
191
+
192
+ Parameters
193
+ ----------
194
+ labels : list of str, optional
195
+ Custom labels. If None, uses uppercase letters (A, B, C, ...).
196
+ loc : str
197
+ Location hint: 'upper left' (default), 'upper right', 'lower left', 'lower right'.
198
+ offset : tuple of float
199
+ (x, y) offset in axes coordinates from the corner.
200
+ Default is (-0.1, 1.05) for upper left positioning.
201
+ fontsize : float, optional
202
+ Font size in points. If None, uses style's title_pt or 10.
203
+ fontweight : str
204
+ Font weight (default: 'bold').
205
+ **kwargs
206
+ Additional arguments passed to ax.text().
207
+
208
+ Returns
209
+ -------
210
+ list of Text
211
+ The matplotlib Text objects created.
212
+
213
+ Examples
214
+ --------
215
+ >>> fig, axes = fr.subplots(2, 2)
216
+ >>> fig.add_panel_labels() # Adds A, B, C, D
217
+ >>> fig.add_panel_labels(['i', 'ii', 'iii', 'iv']) # Custom labels
218
+ >>> fig.add_panel_labels(loc='upper right', offset=(1.05, 1.05))
219
+ """
220
+ from ._panel_labels import add_panel_labels as _add_panel_labels
221
+
222
+ # Get fontsize from style if not specified
223
+ if fontsize is None:
224
+ fontsize = self._get_style_fontsize("title_pt", 10)
225
+
226
+ # Get theme text color (unless user provided 'color' in kwargs)
227
+ if "color" not in kwargs:
228
+ text_color = self._get_theme_text_color()
229
+ else:
230
+ text_color = kwargs.pop("color")
231
+
232
+ def record_callback(info):
233
+ self._recorder.figure_record.panel_labels = info
234
+
235
+ return _add_panel_labels(
236
+ all_axes=self.flat,
237
+ labels=labels,
238
+ loc=loc,
239
+ offset=offset,
240
+ fontsize=fontsize,
241
+ fontweight=fontweight,
242
+ text_color=text_color,
243
+ record_callback=record_callback,
244
+ **kwargs,
245
+ )
246
+
247
+ def set_title_metadata(self, title: str) -> "RecordingFigure":
248
+ """Set figure title metadata (not rendered, stored in recipe).
249
+
250
+ This is for storing a publication/reference title for the figure,
251
+ separate from suptitle which is rendered on the figure.
252
+
253
+ Parameters
254
+ ----------
255
+ title : str
256
+ The figure title for publication/reference.
257
+
258
+ Returns
259
+ -------
260
+ RecordingFigure
261
+ Self for method chaining.
262
+
263
+ Examples
264
+ --------
265
+ >>> fig, ax = fr.subplots()
266
+ >>> fig.set_title_metadata("Effect of temperature on reaction rate")
267
+ >>> fig.set_caption("Figure 1. Reaction rates measured at various temperatures.")
268
+ """
269
+ self._recorder.figure_record.title_metadata = title
270
+ return self
271
+
272
+ def set_caption(self, caption: str) -> "RecordingFigure":
273
+ """Set figure caption metadata (not rendered, stored in recipe).
274
+
275
+ This is for storing a publication caption for the figure,
276
+ typically used in scientific papers (e.g., "Fig. 1. Description...").
277
+
278
+ Parameters
279
+ ----------
280
+ caption : str
281
+ The figure caption text.
282
+
283
+ Returns
284
+ -------
285
+ RecordingFigure
286
+ Self for method chaining.
287
+
288
+ Examples
289
+ --------
290
+ >>> fig, ax = fr.subplots()
291
+ >>> fig.set_caption("Figure 1. Temperature dependence of reaction rates.")
292
+ """
293
+ self._recorder.figure_record.caption = caption
294
+ return self
295
+
296
+ @property
297
+ def title_metadata(self) -> Optional[str]:
298
+ """Get the figure title metadata."""
299
+ return self._recorder.figure_record.title_metadata
300
+
301
+ @property
302
+ def caption(self) -> Optional[str]:
303
+ """Get the figure caption metadata."""
304
+ return self._recorder.figure_record.caption
305
+
82
306
  def __getattr__(self, name: str) -> Any:
83
307
  """Delegate attribute access to underlying figure."""
84
308
  return getattr(self._fig, name)
@@ -88,6 +312,7 @@ class RecordingFigure:
88
312
  fname,
89
313
  save_recipe: bool = True,
90
314
  recipe_format: Literal["csv", "npz", "inline"] = "csv",
315
+ verbose: bool = True,
91
316
  **kwargs,
92
317
  ):
93
318
  """Save the figure image and optionally the recipe.
@@ -101,6 +326,8 @@ class RecordingFigure:
101
326
  Recipe will be saved with same name but .yaml extension.
102
327
  recipe_format : str
103
328
  Format for data in recipe: 'csv' (default), 'npz', or 'inline'.
329
+ verbose : bool
330
+ If True (default), print save status.
104
331
  **kwargs
105
332
  Passed to matplotlib's savefig().
106
333
 
@@ -117,8 +344,17 @@ class RecordingFigure:
117
344
  >>> fig.savefig('figure.png') # Saves both figure.png and figure.yaml
118
345
  >>> fig.savefig('figure.png', save_recipe=False) # Image only
119
346
  """
347
+ # Finalize ticks and special plots before saving
348
+ from ..styles._style_applier import finalize_special_plots, finalize_ticks
349
+ from ..styles._style_loader import get_current_style_dict
350
+
351
+ style_dict = get_current_style_dict()
352
+ for ax in self._fig.get_axes():
353
+ finalize_ticks(ax)
354
+ finalize_special_plots(ax, style_dict)
355
+
120
356
  # Handle file-like objects (BytesIO, etc.) - just pass through
121
- if hasattr(fname, 'write'):
357
+ if hasattr(fname, "write"):
122
358
  self._fig.savefig(fname, **kwargs)
123
359
  return fname
124
360
 
@@ -128,8 +364,12 @@ class RecordingFigure:
128
364
  if save_recipe:
129
365
  recipe_path = fname.with_suffix(".yaml")
130
366
  self.save_recipe(recipe_path, include_data=True, data_format=recipe_format)
367
+ if verbose:
368
+ print(f"Saved: {fname} + {recipe_path}")
131
369
  return fname, recipe_path
132
370
 
371
+ if verbose:
372
+ print(f"Saved: {fname}")
133
373
  return fname
134
374
 
135
375
  def save_recipe(
@@ -155,15 +395,19 @@ class RecordingFigure:
155
395
  Path to saved recipe file.
156
396
  """
157
397
  from .._serializer import save_recipe
158
- return save_recipe(self._recorder.figure_record, path, include_data, data_format)
398
+
399
+ return save_recipe(
400
+ self._recorder.figure_record, path, include_data, data_format
401
+ )
159
402
 
160
403
 
161
404
  def create_recording_subplots(
162
405
  nrows: int = 1,
163
406
  ncols: int = 1,
164
407
  recorder: Optional["Recorder"] = None,
408
+ panel_labels: bool = False,
165
409
  **kwargs,
166
- ) -> Tuple[RecordingFigure, Union[RecordingAxes, List[RecordingAxes]]]:
410
+ ) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
167
411
  """Create a figure with recording-enabled axes.
168
412
 
169
413
  Parameters
@@ -174,6 +418,9 @@ def create_recording_subplots(
174
418
  Number of columns.
175
419
  recorder : Recorder, optional
176
420
  Recorder instance. Created if not provided.
421
+ panel_labels : bool
422
+ If True and figure has multiple panels, automatically add
423
+ panel labels (A, B, C, D, ...). Default is False.
177
424
  **kwargs
178
425
  Passed to plt.subplots().
179
426
 
@@ -181,8 +428,12 @@ def create_recording_subplots(
181
428
  -------
182
429
  fig : RecordingFigure
183
430
  Wrapped figure.
184
- axes : RecordingAxes or list
185
- Wrapped axes (single if 1x1, otherwise 2D array).
431
+ axes : RecordingAxes or ndarray
432
+ Wrapped axes (single if 1x1, otherwise numpy array matching matplotlib).
433
+
434
+ Examples
435
+ --------
436
+ >>> fig, axes = fr.subplots(2, 2, panel_labels=True) # Auto-adds A, B, C, D
186
437
  """
187
438
  from .._recorder import Recorder
188
439
 
@@ -205,23 +456,31 @@ def create_recording_subplots(
205
456
  wrapped_fig = RecordingFigure(fig, recorder, wrapped_ax)
206
457
  return wrapped_fig, wrapped_ax
207
458
 
208
- # Handle 1D or 2D arrays
209
- import numpy as np
210
- mpl_axes = np.atleast_2d(mpl_axes)
459
+ # Handle 1D or 2D arrays - reshape to (nrows, ncols) for uniform processing
460
+ mpl_axes_arr = np.asarray(mpl_axes)
461
+ if mpl_axes_arr.ndim == 1:
462
+ mpl_axes_arr = mpl_axes_arr.reshape(nrows, ncols)
211
463
 
212
464
  wrapped_axes = []
213
- for i in range(mpl_axes.shape[0]):
465
+ for i in range(nrows):
214
466
  row = []
215
- for j in range(mpl_axes.shape[1]):
216
- row.append(RecordingAxes(mpl_axes[i, j], recorder, position=(i, j)))
467
+ for j in range(ncols):
468
+ row.append(RecordingAxes(mpl_axes_arr[i, j], recorder, position=(i, j)))
217
469
  wrapped_axes.append(row)
218
470
 
219
471
  wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes)
220
472
 
221
- # Return in same shape as matplotlib
473
+ # Add panel labels if requested (multi-panel figures only)
474
+ if panel_labels:
475
+ wrapped_fig.add_panel_labels()
476
+
477
+ # Return in same shape as matplotlib (numpy arrays for consistency)
222
478
  if nrows == 1:
223
- return wrapped_fig, wrapped_axes[0]
479
+ # 1xN -> 1D array of shape (N,)
480
+ return wrapped_fig, np.array(wrapped_axes[0], dtype=object)
224
481
  elif ncols == 1:
225
- return wrapped_fig, [row[0] for row in wrapped_axes]
482
+ # Nx1 -> 1D array of shape (N,)
483
+ return wrapped_fig, np.array([row[0] for row in wrapped_axes], dtype=object)
226
484
  else:
227
- return wrapped_fig, wrapped_axes
485
+ # NxM -> 2D array
486
+ return wrapped_fig, np.array(wrapped_axes, dtype=object)
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel label utilities for multi-panel figures."""
4
+
5
+ import string
6
+ from typing import TYPE_CHECKING, Any, List, Optional, Tuple
7
+
8
+ if TYPE_CHECKING:
9
+ from ._axes import RecordingAxes
10
+
11
+
12
+ def add_panel_labels(
13
+ all_axes: List["RecordingAxes"],
14
+ labels: Optional[List[str]],
15
+ loc: str,
16
+ offset: Tuple[float, float],
17
+ fontsize: float,
18
+ fontweight: str,
19
+ text_color: str,
20
+ record_callback: Any,
21
+ **kwargs,
22
+ ) -> List[Any]:
23
+ """Add panel labels (A, B, C, D, etc.) to axes.
24
+
25
+ Parameters
26
+ ----------
27
+ all_axes : list of RecordingAxes
28
+ Flattened list of all axes.
29
+ labels : list of str or None
30
+ Custom labels. If None, uses uppercase letters.
31
+ loc : str
32
+ Location hint: 'upper left', 'upper right', 'lower left', 'lower right'.
33
+ offset : tuple of float
34
+ (x, y) offset in axes coordinates.
35
+ fontsize : float
36
+ Font size in points.
37
+ fontweight : str
38
+ Font weight.
39
+ text_color : str
40
+ Text color.
41
+ record_callback : callable
42
+ Callback to record panel labels info.
43
+ **kwargs
44
+ Additional arguments passed to ax.text().
45
+
46
+ Returns
47
+ -------
48
+ list of Text
49
+ The matplotlib Text objects created.
50
+ """
51
+ n_axes = len(all_axes)
52
+
53
+ # Generate default labels if not provided
54
+ if labels is None:
55
+ labels = list(string.ascii_uppercase[:n_axes])
56
+ elif len(labels) < n_axes:
57
+ # Extend with letters if not enough labels provided
58
+ labels = list(labels) + list(string.ascii_uppercase[len(labels) : n_axes])
59
+
60
+ # Calculate position based on loc
61
+ x, y, ha, va = _calculate_position(loc, offset)
62
+
63
+ # Record panel labels
64
+ record_callback(
65
+ {
66
+ "labels": labels[:n_axes],
67
+ "loc": loc,
68
+ "offset": offset,
69
+ "fontsize": fontsize,
70
+ "fontweight": fontweight,
71
+ "color": text_color,
72
+ "kwargs": kwargs,
73
+ }
74
+ )
75
+
76
+ # Add labels to each axes
77
+ text_objects = []
78
+ for ax, label in zip(all_axes, labels[:n_axes]):
79
+ text = ax.ax.text(
80
+ x,
81
+ y,
82
+ label,
83
+ transform=ax.ax.transAxes,
84
+ fontsize=fontsize,
85
+ fontweight=fontweight,
86
+ color=text_color,
87
+ ha=ha,
88
+ va=va,
89
+ **kwargs,
90
+ )
91
+ text_objects.append(text)
92
+
93
+ return text_objects
94
+
95
+
96
+ def _calculate_position(
97
+ loc: str, offset: Tuple[float, float]
98
+ ) -> Tuple[float, float, str, str]:
99
+ """Calculate text position and alignment based on location.
100
+
101
+ Returns
102
+ -------
103
+ tuple
104
+ (x, y, ha, va) where ha/va are horizontal/vertical alignment.
105
+ """
106
+ if loc == "upper left":
107
+ x, y = offset
108
+ ha, va = "right", "bottom"
109
+ elif loc == "upper right":
110
+ x, y = offset
111
+ ha, va = "left", "bottom"
112
+ elif loc == "lower left":
113
+ x, y = offset[0], -offset[1] + 1.0
114
+ ha, va = "right", "top"
115
+ elif loc == "lower right":
116
+ x, y = offset
117
+ ha, va = "left", "top"
118
+ else:
119
+ x, y = offset
120
+ ha, va = "right", "bottom"
121
+
122
+ return x, y, ha, va
123
+
124
+
125
+ __all__ = ["add_panel_labels"]
126
+
127
+ # EOF
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Helper functions for custom plot methods in RecordingAxes."""
4
+
5
+ from typing import List, Tuple
6
+
7
+ import numpy as np
8
+
9
+
10
+ def get_colors_from_style(n_colors: int, explicit_colors=None) -> List:
11
+ """Get colors from style or matplotlib defaults.
12
+
13
+ Parameters
14
+ ----------
15
+ n_colors : int
16
+ Number of colors needed.
17
+ explicit_colors : list or color, optional
18
+ Explicitly provided colors.
19
+
20
+ Returns
21
+ -------
22
+ list
23
+ List of colors.
24
+ """
25
+ if explicit_colors is not None:
26
+ if isinstance(explicit_colors, list):
27
+ return explicit_colors
28
+ return [explicit_colors] * n_colors
29
+
30
+ from ..styles import get_style
31
+
32
+ style = get_style()
33
+ if style and "colors" in style and "palette" in style.colors:
34
+ palette = list(style.colors.palette)
35
+ colors = []
36
+ for c in palette:
37
+ if isinstance(c, (list, tuple)) and len(c) >= 3:
38
+ if all(v <= 1.0 for v in c):
39
+ colors.append(tuple(c))
40
+ else:
41
+ colors.append(tuple(v / 255.0 for v in c))
42
+ else:
43
+ colors.append(c)
44
+ return colors
45
+
46
+ # Matplotlib default color cycle
47
+ import matplotlib.pyplot as plt
48
+
49
+ return [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
50
+
51
+
52
+ def beeswarm_positions(
53
+ data: np.ndarray,
54
+ width: float,
55
+ rng: np.random.Generator,
56
+ ) -> np.ndarray:
57
+ """Calculate beeswarm-style x positions to minimize overlap.
58
+
59
+ This is a simplified beeswarm that uses binning and jittering.
60
+
61
+ Parameters
62
+ ----------
63
+ data : array
64
+ Y values of points.
65
+ width : float
66
+ Maximum jitter width.
67
+ rng : Generator
68
+ Random number generator.
69
+
70
+ Returns
71
+ -------
72
+ array
73
+ X offsets for each point.
74
+ """
75
+ n = len(data)
76
+ if n == 0:
77
+ return np.array([])
78
+
79
+ # Sort data and get order
80
+ order = np.argsort(data)
81
+ sorted_data = data[order]
82
+
83
+ # Group nearby points and offset them
84
+ x_offsets = np.zeros(n)
85
+
86
+ # Simple approach: bin by quantiles and spread within each bin
87
+ n_bins = max(1, int(np.sqrt(n)))
88
+ bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
89
+
90
+ for i in range(n_bins):
91
+ mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
92
+ n_in_bin = mask.sum()
93
+ if n_in_bin > 0:
94
+ # Spread points evenly within bin width
95
+ offsets = np.linspace(-width / 2, width / 2, n_in_bin)
96
+ # Add small random noise
97
+ offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
98
+ x_offsets[mask] = offsets
99
+
100
+ # Restore original order
101
+ result = np.zeros(n)
102
+ result[order] = x_offsets
103
+ return result
104
+
105
+
106
+ def compute_joyplot_kdes(arrays: List[np.ndarray], x: np.ndarray) -> Tuple[List, float]:
107
+ """Compute KDEs for joyplot ridges.
108
+
109
+ Parameters
110
+ ----------
111
+ arrays : list
112
+ List of data arrays.
113
+ x : array
114
+ X values for KDE evaluation.
115
+
116
+ Returns
117
+ -------
118
+ tuple
119
+ (kdes, max_density)
120
+ """
121
+ from scipy import stats
122
+
123
+ kdes = []
124
+ max_density = 0
125
+ for arr in arrays:
126
+ arr = np.asarray(arr)
127
+ if len(arr) > 1:
128
+ kde = stats.gaussian_kde(arr)
129
+ density = kde(x)
130
+ kdes.append(density)
131
+ max_density = max(max_density, np.max(density))
132
+ else:
133
+ kdes.append(np.zeros_like(x))
134
+ return kdes, max_density
135
+
136
+
137
+ __all__ = [
138
+ "get_colors_from_style",
139
+ "beeswarm_positions",
140
+ "compute_joyplot_kdes",
141
+ ]
142
+
143
+ # EOF