figrecipe 0.6.0__py3-none-any.whl → 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
figrecipe/__init__.py
CHANGED
|
@@ -56,6 +56,11 @@ from matplotlib.axes import Axes
|
|
|
56
56
|
from matplotlib.figure import Figure
|
|
57
57
|
from numpy.typing import NDArray
|
|
58
58
|
|
|
59
|
+
# Notebook utilities
|
|
60
|
+
from ._api._notebook import enable_svg
|
|
61
|
+
|
|
62
|
+
# Seaborn proxy
|
|
63
|
+
from ._api._seaborn_proxy import sns
|
|
59
64
|
from ._recorder import CallRecord, FigureRecord
|
|
60
65
|
from ._reproducer import get_recipe_info
|
|
61
66
|
from ._reproducer import reproduce as _reproduce
|
|
@@ -71,86 +76,9 @@ from ._utils._units import (
|
|
|
71
76
|
)
|
|
72
77
|
from ._validator import ValidationResult
|
|
73
78
|
from ._wrappers import RecordingAxes, RecordingFigure
|
|
74
|
-
from ._wrappers._figure import create_recording_subplots
|
|
75
79
|
from .styles._style_applier import check_font, list_available_fonts
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
_notebook_format_set = False
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _enable_notebook_svg():
|
|
82
|
-
"""Enable SVG format for Jupyter notebook display.
|
|
83
|
-
|
|
84
|
-
This provides crisp vector graphics at any zoom level.
|
|
85
|
-
Called automatically when load_style() or subplots() is used.
|
|
86
|
-
"""
|
|
87
|
-
global _notebook_format_set
|
|
88
|
-
if _notebook_format_set:
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
# Method 1: matplotlib_inline (IPython 7.0+, JupyterLab)
|
|
93
|
-
from matplotlib_inline.backend_inline import set_matplotlib_formats
|
|
94
|
-
|
|
95
|
-
set_matplotlib_formats("svg")
|
|
96
|
-
_notebook_format_set = True
|
|
97
|
-
except (ImportError, Exception):
|
|
98
|
-
try:
|
|
99
|
-
# Method 2: IPython config (older IPython)
|
|
100
|
-
from IPython import get_ipython
|
|
101
|
-
|
|
102
|
-
ipython = get_ipython()
|
|
103
|
-
if ipython is not None and hasattr(ipython, "kernel"):
|
|
104
|
-
# Only run in actual Jupyter kernel, not IPython console
|
|
105
|
-
ipython.run_line_magic(
|
|
106
|
-
"config", "InlineBackend.figure_formats = ['svg']"
|
|
107
|
-
)
|
|
108
|
-
_notebook_format_set = True
|
|
109
|
-
except Exception:
|
|
110
|
-
pass # Not in Jupyter environment or method not available
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def enable_svg():
|
|
114
|
-
"""Manually enable SVG format for Jupyter notebook display.
|
|
115
|
-
|
|
116
|
-
Call this if figures appear pixelated in notebooks.
|
|
117
|
-
|
|
118
|
-
Examples
|
|
119
|
-
--------
|
|
120
|
-
>>> import figrecipe as fr
|
|
121
|
-
>>> fr.enable_svg() # Enable SVG rendering
|
|
122
|
-
>>> fig, ax = fr.subplots() # Now renders as crisp SVG
|
|
123
|
-
"""
|
|
124
|
-
global _notebook_format_set
|
|
125
|
-
_notebook_format_set = False # Force re-application
|
|
126
|
-
_enable_notebook_svg()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# Lazy import for seaborn to avoid hard dependency
|
|
130
|
-
_sns_recorder = None
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _get_sns():
|
|
134
|
-
"""Get the seaborn recorder (lazy initialization)."""
|
|
135
|
-
global _sns_recorder
|
|
136
|
-
if _sns_recorder is None:
|
|
137
|
-
from ._seaborn import get_seaborn_recorder
|
|
138
|
-
|
|
139
|
-
_sns_recorder = get_seaborn_recorder()
|
|
140
|
-
return _sns_recorder
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
class _SeabornProxy:
|
|
144
|
-
"""Proxy object for seaborn access via ps.sns."""
|
|
145
|
-
|
|
146
|
-
def __getattr__(self, name: str):
|
|
147
|
-
return getattr(_get_sns(), name)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# Create seaborn proxy
|
|
151
|
-
sns = _SeabornProxy()
|
|
152
|
-
|
|
153
|
-
__version__ = "0.4.0"
|
|
81
|
+
__version__ = "0.7.2"
|
|
154
82
|
__all__ = [
|
|
155
83
|
# Main API
|
|
156
84
|
"subplots",
|
|
@@ -197,146 +125,14 @@ __all__ = [
|
|
|
197
125
|
]
|
|
198
126
|
|
|
199
127
|
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
automatically use the loaded style (fonts, colors, theme, etc.).
|
|
209
|
-
|
|
210
|
-
Parameters
|
|
211
|
-
----------
|
|
212
|
-
style : str, Path, bool, or None
|
|
213
|
-
One of:
|
|
214
|
-
- "SCITEX" / "FIGRECIPE": Scientific publication style (default)
|
|
215
|
-
- "MATPLOTLIB": Vanilla matplotlib defaults
|
|
216
|
-
- Path to custom YAML file: "/path/to/my_style.yaml"
|
|
217
|
-
- None or False: Unload style (reset to matplotlib defaults)
|
|
218
|
-
dark : bool, optional
|
|
219
|
-
If True, apply dark theme transformation (default: False).
|
|
220
|
-
Equivalent to appending "_DARK" to preset name.
|
|
221
|
-
|
|
222
|
-
Returns
|
|
223
|
-
-------
|
|
224
|
-
DotDict or None
|
|
225
|
-
Style configuration with dot-notation access.
|
|
226
|
-
Returns None if style is unloaded.
|
|
227
|
-
|
|
228
|
-
Examples
|
|
229
|
-
--------
|
|
230
|
-
>>> import figrecipe as fr
|
|
231
|
-
|
|
232
|
-
>>> # Load scientific style (default)
|
|
233
|
-
>>> fr.load_style()
|
|
234
|
-
>>> fr.load_style("SCITEX") # explicit
|
|
235
|
-
|
|
236
|
-
>>> # Load dark theme
|
|
237
|
-
>>> fr.load_style("SCITEX_DARK")
|
|
238
|
-
>>> fr.load_style("SCITEX", dark=True) # equivalent
|
|
239
|
-
|
|
240
|
-
>>> # Reset to vanilla matplotlib
|
|
241
|
-
>>> fr.load_style(None) # unload
|
|
242
|
-
>>> fr.load_style(False) # unload
|
|
243
|
-
>>> fr.load_style("MATPLOTLIB") # explicit vanilla
|
|
244
|
-
|
|
245
|
-
>>> # Access style values
|
|
246
|
-
>>> style = fr.load_style("SCITEX")
|
|
247
|
-
>>> style.axes.width_mm
|
|
248
|
-
40
|
|
249
|
-
"""
|
|
250
|
-
from .styles import load_style as _load_style
|
|
251
|
-
|
|
252
|
-
return _load_style(style, dark=dark)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def unload_style():
|
|
256
|
-
"""Unload the current style and reset to matplotlib defaults.
|
|
257
|
-
|
|
258
|
-
After calling this, subsequent `subplots()` calls will use vanilla
|
|
259
|
-
matplotlib behavior without FigRecipe styling.
|
|
260
|
-
|
|
261
|
-
Examples
|
|
262
|
-
--------
|
|
263
|
-
>>> import figrecipe as fr
|
|
264
|
-
>>> fr.load_style("SCITEX") # Apply scientific style
|
|
265
|
-
>>> fig, ax = fr.subplots() # Styled
|
|
266
|
-
>>> fr.unload_style() # Reset to matplotlib defaults
|
|
267
|
-
>>> fig, ax = fr.subplots() # Vanilla matplotlib
|
|
268
|
-
"""
|
|
269
|
-
from .styles import unload_style as _unload_style
|
|
270
|
-
|
|
271
|
-
_unload_style()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def list_presets():
|
|
275
|
-
"""List available style presets.
|
|
276
|
-
|
|
277
|
-
Returns
|
|
278
|
-
-------
|
|
279
|
-
list of str
|
|
280
|
-
Names of available presets.
|
|
281
|
-
|
|
282
|
-
Examples
|
|
283
|
-
--------
|
|
284
|
-
>>> import figrecipe as ps
|
|
285
|
-
>>> ps.list_presets()
|
|
286
|
-
['MINIMAL', 'PRESENTATION', 'SCIENTIFIC']
|
|
287
|
-
"""
|
|
288
|
-
from .styles import list_presets as _list_presets
|
|
289
|
-
|
|
290
|
-
return _list_presets()
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def apply_style(ax, style=None):
|
|
294
|
-
"""Apply mm-based styling to an axes.
|
|
295
|
-
|
|
296
|
-
Parameters
|
|
297
|
-
----------
|
|
298
|
-
ax : matplotlib.axes.Axes
|
|
299
|
-
Target axes to apply styling to.
|
|
300
|
-
style : dict or DotDict, optional
|
|
301
|
-
Style configuration. If None, uses default FIGRECIPE_STYLE.
|
|
302
|
-
|
|
303
|
-
Returns
|
|
304
|
-
-------
|
|
305
|
-
float
|
|
306
|
-
Trace line width in points.
|
|
307
|
-
|
|
308
|
-
Examples
|
|
309
|
-
--------
|
|
310
|
-
>>> import figrecipe as ps
|
|
311
|
-
>>> import matplotlib.pyplot as plt
|
|
312
|
-
>>> fig, ax = plt.subplots()
|
|
313
|
-
>>> trace_lw = ps.apply_style(ax)
|
|
314
|
-
>>> ax.plot(x, y, lw=trace_lw)
|
|
315
|
-
"""
|
|
316
|
-
from .styles import apply_style_mm, get_style, to_subplots_kwargs
|
|
317
|
-
|
|
318
|
-
if style is None:
|
|
319
|
-
style = to_subplots_kwargs(get_style())
|
|
320
|
-
elif hasattr(style, "to_subplots_kwargs"):
|
|
321
|
-
style = style.to_subplots_kwargs()
|
|
322
|
-
return apply_style_mm(ax, style)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
class _StyleProxy:
|
|
326
|
-
"""Proxy object for lazy style loading."""
|
|
327
|
-
|
|
328
|
-
def __getattr__(self, name):
|
|
329
|
-
from .styles import STYLE
|
|
330
|
-
|
|
331
|
-
return getattr(STYLE, name)
|
|
332
|
-
|
|
333
|
-
def to_subplots_kwargs(self):
|
|
334
|
-
from .styles import to_subplots_kwargs
|
|
335
|
-
|
|
336
|
-
return to_subplots_kwargs()
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
STYLE = _StyleProxy()
|
|
128
|
+
# Style management
|
|
129
|
+
from ._api._style_manager import (
|
|
130
|
+
STYLE,
|
|
131
|
+
apply_style,
|
|
132
|
+
list_presets,
|
|
133
|
+
load_style,
|
|
134
|
+
unload_style,
|
|
135
|
+
)
|
|
340
136
|
|
|
341
137
|
|
|
342
138
|
def subplots(
|
|
@@ -354,6 +150,8 @@ def subplots(
|
|
|
354
150
|
# Style parameters
|
|
355
151
|
style: Optional[Dict[str, Any]] = None,
|
|
356
152
|
apply_style_mm: bool = True,
|
|
153
|
+
# Panel labels (None = use style default, True/False = explicit)
|
|
154
|
+
panel_labels: Optional[bool] = None,
|
|
357
155
|
**kwargs,
|
|
358
156
|
) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
|
|
359
157
|
"""Create a figure with recording-enabled axes.
|
|
@@ -365,281 +163,51 @@ def subplots(
|
|
|
365
163
|
|
|
366
164
|
Parameters
|
|
367
165
|
----------
|
|
368
|
-
nrows : int
|
|
369
|
-
Number of rows of subplots.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
Axes height in mm.
|
|
379
|
-
margin_left_mm : float, optional
|
|
380
|
-
Left margin in mm (default: 15).
|
|
381
|
-
margin_right_mm : float, optional
|
|
382
|
-
Right margin in mm (default: 5).
|
|
383
|
-
margin_bottom_mm : float, optional
|
|
384
|
-
Bottom margin in mm (default: 12).
|
|
385
|
-
margin_top_mm : float, optional
|
|
386
|
-
Top margin in mm (default: 8).
|
|
387
|
-
space_w_mm : float, optional
|
|
388
|
-
Horizontal spacing between axes in mm (default: 8).
|
|
389
|
-
space_h_mm : float, optional
|
|
390
|
-
Vertical spacing between axes in mm (default: 10).
|
|
391
|
-
|
|
392
|
-
Style Parameters
|
|
393
|
-
----------------
|
|
166
|
+
nrows, ncols : int
|
|
167
|
+
Number of rows and columns of subplots.
|
|
168
|
+
axes_width_mm, axes_height_mm : float, optional
|
|
169
|
+
Axes dimensions in mm.
|
|
170
|
+
margin_left_mm, margin_right_mm : float, optional
|
|
171
|
+
Left/right margins in mm.
|
|
172
|
+
margin_bottom_mm, margin_top_mm : float, optional
|
|
173
|
+
Bottom/top margins in mm.
|
|
174
|
+
space_w_mm, space_h_mm : float, optional
|
|
175
|
+
Horizontal/vertical spacing between axes in mm.
|
|
394
176
|
style : dict, optional
|
|
395
|
-
Style configuration dictionary
|
|
177
|
+
Style configuration dictionary.
|
|
396
178
|
apply_style_mm : bool
|
|
397
|
-
If True (default), apply loaded style to axes
|
|
398
|
-
|
|
399
|
-
|
|
179
|
+
If True (default), apply loaded style to axes.
|
|
180
|
+
panel_labels : bool or None
|
|
181
|
+
If True, add panel labels (A, B, C, ...).
|
|
400
182
|
**kwargs
|
|
401
|
-
Additional arguments passed to plt.subplots()
|
|
183
|
+
Additional arguments passed to plt.subplots().
|
|
402
184
|
|
|
403
185
|
Returns
|
|
404
186
|
-------
|
|
405
187
|
fig : RecordingFigure
|
|
406
188
|
Wrapped figure object.
|
|
407
189
|
axes : RecordingAxes or ndarray
|
|
408
|
-
Wrapped axes
|
|
409
|
-
|
|
410
|
-
Examples
|
|
411
|
-
--------
|
|
412
|
-
Basic usage:
|
|
413
|
-
|
|
414
|
-
>>> import figrecipe as ps
|
|
415
|
-
>>> fig, ax = ps.subplots()
|
|
416
|
-
>>> ax.plot([1, 2, 3], [4, 5, 6], color='blue')
|
|
417
|
-
>>> ps.save(fig, 'simple.yaml')
|
|
418
|
-
|
|
419
|
-
MM-based layout:
|
|
420
|
-
|
|
421
|
-
>>> fig, ax = ps.subplots(
|
|
422
|
-
... axes_width_mm=40,
|
|
423
|
-
... axes_height_mm=28,
|
|
424
|
-
... margin_left_mm=15,
|
|
425
|
-
... margin_bottom_mm=12,
|
|
426
|
-
... )
|
|
427
|
-
|
|
428
|
-
With style (automatically applied):
|
|
429
|
-
|
|
430
|
-
>>> ps.load_style("FIGRECIPE_DARK") # Load dark theme
|
|
431
|
-
>>> fig, ax = ps.subplots() # Style applied automatically
|
|
190
|
+
Wrapped axes.
|
|
432
191
|
"""
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
)
|
|
451
|
-
if val is None:
|
|
452
|
-
break
|
|
453
|
-
if val is not None:
|
|
454
|
-
return val
|
|
455
|
-
except (KeyError, AttributeError):
|
|
456
|
-
pass
|
|
457
|
-
return default
|
|
458
|
-
|
|
459
|
-
# Check if mm-based layout is requested (explicit OR from global style)
|
|
460
|
-
has_explicit_mm = any(
|
|
461
|
-
[
|
|
462
|
-
axes_width_mm is not None,
|
|
463
|
-
axes_height_mm is not None,
|
|
464
|
-
margin_left_mm is not None,
|
|
465
|
-
margin_right_mm is not None,
|
|
466
|
-
margin_bottom_mm is not None,
|
|
467
|
-
margin_top_mm is not None,
|
|
468
|
-
space_w_mm is not None,
|
|
469
|
-
space_h_mm is not None,
|
|
470
|
-
]
|
|
192
|
+
from ._api._subplots import create_subplots
|
|
193
|
+
|
|
194
|
+
return create_subplots(
|
|
195
|
+
nrows=nrows,
|
|
196
|
+
ncols=ncols,
|
|
197
|
+
axes_width_mm=axes_width_mm,
|
|
198
|
+
axes_height_mm=axes_height_mm,
|
|
199
|
+
margin_left_mm=margin_left_mm,
|
|
200
|
+
margin_right_mm=margin_right_mm,
|
|
201
|
+
margin_bottom_mm=margin_bottom_mm,
|
|
202
|
+
margin_top_mm=margin_top_mm,
|
|
203
|
+
space_w_mm=space_w_mm,
|
|
204
|
+
space_h_mm=space_h_mm,
|
|
205
|
+
style=style,
|
|
206
|
+
apply_style_mm=apply_style_mm,
|
|
207
|
+
panel_labels=panel_labels,
|
|
208
|
+
**kwargs,
|
|
471
209
|
)
|
|
472
210
|
|
|
473
|
-
# Also use mm layout if global style has mm values
|
|
474
|
-
has_style_mm = False
|
|
475
|
-
if global_style is not None:
|
|
476
|
-
try:
|
|
477
|
-
has_style_mm = (
|
|
478
|
-
global_style.get("axes", {}).get("width_mm") is not None
|
|
479
|
-
or getattr(getattr(global_style, "axes", None), "width_mm", None)
|
|
480
|
-
is not None
|
|
481
|
-
)
|
|
482
|
-
except (KeyError, AttributeError):
|
|
483
|
-
pass
|
|
484
|
-
|
|
485
|
-
use_mm_layout = has_explicit_mm or has_style_mm
|
|
486
|
-
|
|
487
|
-
if use_mm_layout and "figsize" not in kwargs:
|
|
488
|
-
# Get mm values: explicit params > global style > hardcoded defaults
|
|
489
|
-
aw = _get_mm(axes_width_mm, ["axes", "width_mm"], 40)
|
|
490
|
-
ah = _get_mm(axes_height_mm, ["axes", "height_mm"], 28)
|
|
491
|
-
ml = _get_mm(margin_left_mm, ["margins", "left_mm"], 15)
|
|
492
|
-
mr = _get_mm(margin_right_mm, ["margins", "right_mm"], 5)
|
|
493
|
-
mb = _get_mm(margin_bottom_mm, ["margins", "bottom_mm"], 12)
|
|
494
|
-
mt = _get_mm(margin_top_mm, ["margins", "top_mm"], 8)
|
|
495
|
-
sw = _get_mm(space_w_mm, ["spacing", "horizontal_mm"], 8)
|
|
496
|
-
sh = _get_mm(space_h_mm, ["spacing", "vertical_mm"], 10)
|
|
497
|
-
|
|
498
|
-
# Calculate total figure size
|
|
499
|
-
total_width_mm = ml + (ncols * aw) + ((ncols - 1) * sw) + mr
|
|
500
|
-
total_height_mm = mb + (nrows * ah) + ((nrows - 1) * sh) + mt
|
|
501
|
-
|
|
502
|
-
# Convert to inches and set figsize
|
|
503
|
-
kwargs["figsize"] = (mm_to_inch(total_width_mm), mm_to_inch(total_height_mm))
|
|
504
|
-
|
|
505
|
-
# Store mm metadata for recording (will be extracted by create_recording_subplots)
|
|
506
|
-
mm_layout = {
|
|
507
|
-
"axes_width_mm": aw,
|
|
508
|
-
"axes_height_mm": ah,
|
|
509
|
-
"margin_left_mm": ml,
|
|
510
|
-
"margin_right_mm": mr,
|
|
511
|
-
"margin_bottom_mm": mb,
|
|
512
|
-
"margin_top_mm": mt,
|
|
513
|
-
"space_w_mm": sw,
|
|
514
|
-
"space_h_mm": sh,
|
|
515
|
-
}
|
|
516
|
-
else:
|
|
517
|
-
mm_layout = None
|
|
518
|
-
|
|
519
|
-
# Apply DPI from global style if not explicitly provided
|
|
520
|
-
if "dpi" not in kwargs and global_style is not None:
|
|
521
|
-
# Try figure.dpi first, then output.dpi
|
|
522
|
-
style_dpi = None
|
|
523
|
-
try:
|
|
524
|
-
if hasattr(global_style, "figure") and hasattr(global_style.figure, "dpi"):
|
|
525
|
-
style_dpi = global_style.figure.dpi
|
|
526
|
-
elif hasattr(global_style, "output") and hasattr(
|
|
527
|
-
global_style.output, "dpi"
|
|
528
|
-
):
|
|
529
|
-
style_dpi = global_style.output.dpi
|
|
530
|
-
except (KeyError, AttributeError):
|
|
531
|
-
pass
|
|
532
|
-
if style_dpi is not None:
|
|
533
|
-
kwargs["dpi"] = style_dpi
|
|
534
|
-
|
|
535
|
-
# Handle style parameter
|
|
536
|
-
if style is not None:
|
|
537
|
-
if hasattr(style, "to_subplots_kwargs"):
|
|
538
|
-
# Merge style kwargs (style values are overridden by explicit params)
|
|
539
|
-
style_kwargs = style.to_subplots_kwargs()
|
|
540
|
-
for key, value in style_kwargs.items():
|
|
541
|
-
if key not in kwargs:
|
|
542
|
-
kwargs[key] = value
|
|
543
|
-
|
|
544
|
-
# Check if style specifies constrained_layout
|
|
545
|
-
style_constrained = False
|
|
546
|
-
if global_style is not None:
|
|
547
|
-
from .styles._style_loader import to_subplots_kwargs
|
|
548
|
-
|
|
549
|
-
style_dict_check = to_subplots_kwargs(global_style)
|
|
550
|
-
style_constrained = style_dict_check.get("constrained_layout", False)
|
|
551
|
-
|
|
552
|
-
# Use constrained_layout if: style specifies it, or non-mm layout (better auto-spacing)
|
|
553
|
-
if "constrained_layout" not in kwargs:
|
|
554
|
-
if style_constrained:
|
|
555
|
-
kwargs["constrained_layout"] = True
|
|
556
|
-
elif not use_mm_layout:
|
|
557
|
-
kwargs["constrained_layout"] = True
|
|
558
|
-
|
|
559
|
-
# Create the recording subplots
|
|
560
|
-
fig, axes = create_recording_subplots(nrows, ncols, **kwargs)
|
|
561
|
-
|
|
562
|
-
# Record constrained_layout setting for reproduction
|
|
563
|
-
fig.record.constrained_layout = kwargs.get("constrained_layout", False)
|
|
564
|
-
|
|
565
|
-
# Store mm_layout metadata on figure for serialization
|
|
566
|
-
# Skip mm-based layout if constrained_layout is True (they're incompatible)
|
|
567
|
-
use_constrained = kwargs.get("constrained_layout", False)
|
|
568
|
-
if mm_layout is not None and not use_constrained:
|
|
569
|
-
fig._mm_layout = mm_layout
|
|
570
|
-
|
|
571
|
-
# Apply subplots_adjust to position axes correctly
|
|
572
|
-
total_width_mm = ml + (ncols * aw) + ((ncols - 1) * sw) + mr
|
|
573
|
-
total_height_mm = mb + (nrows * ah) + ((nrows - 1) * sh) + mt
|
|
574
|
-
|
|
575
|
-
# Calculate relative positions (0-1 range)
|
|
576
|
-
left = ml / total_width_mm
|
|
577
|
-
right = 1 - (mr / total_width_mm)
|
|
578
|
-
bottom = mb / total_height_mm
|
|
579
|
-
top = 1 - (mt / total_height_mm)
|
|
580
|
-
|
|
581
|
-
# Calculate spacing as fraction of figure size
|
|
582
|
-
wspace = sw / aw if ncols > 1 else 0
|
|
583
|
-
hspace = sh / ah if nrows > 1 else 0
|
|
584
|
-
|
|
585
|
-
fig.fig.subplots_adjust(
|
|
586
|
-
left=left,
|
|
587
|
-
right=right,
|
|
588
|
-
bottom=bottom,
|
|
589
|
-
top=top,
|
|
590
|
-
wspace=wspace,
|
|
591
|
-
hspace=hspace,
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
# Record layout in figure record for reproduction
|
|
595
|
-
fig.record.layout = {
|
|
596
|
-
"left": left,
|
|
597
|
-
"right": right,
|
|
598
|
-
"bottom": bottom,
|
|
599
|
-
"top": top,
|
|
600
|
-
"wspace": wspace,
|
|
601
|
-
"hspace": hspace,
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
# Apply styling if requested and a style is actually loaded
|
|
605
|
-
style_dict = None
|
|
606
|
-
should_apply_style = False
|
|
607
|
-
|
|
608
|
-
if style is not None:
|
|
609
|
-
# Explicit style parameter provided
|
|
610
|
-
should_apply_style = True
|
|
611
|
-
style_dict = (
|
|
612
|
-
style.to_subplots_kwargs()
|
|
613
|
-
if hasattr(style, "to_subplots_kwargs")
|
|
614
|
-
else style
|
|
615
|
-
)
|
|
616
|
-
elif apply_style_mm and global_style is not None:
|
|
617
|
-
# Use global style if loaded and has meaningful values (not MATPLOTLIB)
|
|
618
|
-
from .styles import to_subplots_kwargs
|
|
619
|
-
|
|
620
|
-
style_dict = to_subplots_kwargs(global_style)
|
|
621
|
-
# Only apply if style has essential mm values (skip MATPLOTLIB which has all None)
|
|
622
|
-
if style_dict and style_dict.get("axes_thickness_mm") is not None:
|
|
623
|
-
should_apply_style = True
|
|
624
|
-
|
|
625
|
-
if should_apply_style and style_dict:
|
|
626
|
-
from .styles import apply_style_mm as _apply_style
|
|
627
|
-
|
|
628
|
-
if nrows == 1 and ncols == 1:
|
|
629
|
-
_apply_style(axes._ax, style_dict)
|
|
630
|
-
else:
|
|
631
|
-
# Handle 2D array of axes
|
|
632
|
-
import numpy as np
|
|
633
|
-
|
|
634
|
-
axes_array = np.array(axes)
|
|
635
|
-
for ax in axes_array.flat:
|
|
636
|
-
_apply_style(ax._ax if hasattr(ax, "_ax") else ax, style_dict)
|
|
637
|
-
|
|
638
|
-
# Record style in figure record for reproduction
|
|
639
|
-
fig.record.style = style_dict
|
|
640
|
-
|
|
641
|
-
return fig, axes
|
|
642
|
-
|
|
643
211
|
|
|
644
212
|
def save(
|
|
645
213
|
fig: Union[RecordingFigure, Figure],
|
|
@@ -662,188 +230,46 @@ def save(
|
|
|
662
230
|
Parameters
|
|
663
231
|
----------
|
|
664
232
|
fig : RecordingFigure or Figure
|
|
665
|
-
The figure to save.
|
|
233
|
+
The figure to save.
|
|
666
234
|
path : str or Path
|
|
667
|
-
Output path.
|
|
668
|
-
- Image path (.png, .pdf, .svg, .jpg): Saves image + YAML recipe
|
|
669
|
-
- YAML path (.yaml, .yml): Saves recipe + image
|
|
235
|
+
Output path (.png, .pdf, .svg, .yaml, etc.)
|
|
670
236
|
include_data : bool
|
|
671
237
|
If True, save large arrays to separate files.
|
|
672
238
|
data_format : str
|
|
673
|
-
Format for data files: 'csv'
|
|
674
|
-
- 'csv': Human-readable CSV files with dtype header
|
|
675
|
-
- 'npz': Compressed numpy binary format (efficient)
|
|
676
|
-
- 'inline': Store all data directly in YAML
|
|
239
|
+
Format for data files: 'csv', 'npz', or 'inline'.
|
|
677
240
|
validate : bool
|
|
678
|
-
If True (default), validate reproducibility after saving
|
|
679
|
-
reproducing the figure and comparing it to the original.
|
|
241
|
+
If True (default), validate reproducibility after saving.
|
|
680
242
|
validate_mse_threshold : float
|
|
681
243
|
Maximum acceptable MSE for validation (default: 100).
|
|
682
244
|
validate_error_level : str
|
|
683
|
-
How to handle validation failures: 'error'
|
|
684
|
-
- 'error': Raise ValueError on failure
|
|
685
|
-
- 'warning': Emit UserWarning on failure
|
|
686
|
-
- 'debug': Silent (check result.valid manually)
|
|
245
|
+
How to handle validation failures: 'error', 'warning', or 'debug'.
|
|
687
246
|
verbose : bool
|
|
688
|
-
If True (default), print save status.
|
|
247
|
+
If True (default), print save status.
|
|
689
248
|
dpi : int, optional
|
|
690
|
-
DPI for image output.
|
|
249
|
+
DPI for image output.
|
|
691
250
|
image_format : str, optional
|
|
692
|
-
Image format when path is YAML
|
|
693
|
-
Uses style's output.format or 'png' if not specified.
|
|
251
|
+
Image format when path is YAML.
|
|
694
252
|
|
|
695
253
|
Returns
|
|
696
254
|
-------
|
|
697
255
|
tuple
|
|
698
|
-
(image_path, yaml_path, ValidationResult or None)
|
|
699
|
-
ValidationResult is None when validate=False.
|
|
700
|
-
|
|
701
|
-
Examples
|
|
702
|
-
--------
|
|
703
|
-
>>> import figrecipe as fr
|
|
704
|
-
>>> fig, ax = fr.subplots()
|
|
705
|
-
>>> ax.plot(x, y, color='red', id='my_data')
|
|
706
|
-
>>>
|
|
707
|
-
>>> # Save as PNG (also creates experiment.yaml)
|
|
708
|
-
>>> img_path, yaml_path, result = fr.save(fig, 'experiment.png')
|
|
709
|
-
>>>
|
|
710
|
-
>>> # Save as YAML (also creates experiment.png)
|
|
711
|
-
>>> img_path, yaml_path, result = fr.save(fig, 'experiment.yaml')
|
|
712
|
-
>>>
|
|
713
|
-
>>> # Save as PDF with custom DPI
|
|
714
|
-
>>> fr.save(fig, 'experiment.pdf', dpi=600)
|
|
715
|
-
|
|
716
|
-
Notes
|
|
717
|
-
-----
|
|
718
|
-
The recipe file contains:
|
|
719
|
-
- Figure metadata (size, DPI, matplotlib version)
|
|
720
|
-
- All plotting calls with their arguments
|
|
721
|
-
- References to data files for large arrays
|
|
256
|
+
(image_path, yaml_path, ValidationResult or None)
|
|
722
257
|
"""
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
".jpg",
|
|
737
|
-
".jpeg",
|
|
738
|
-
".eps",
|
|
739
|
-
".tiff",
|
|
740
|
-
".tif",
|
|
741
|
-
}
|
|
742
|
-
YAML_EXTENSIONS = {".yaml", ".yml"}
|
|
743
|
-
|
|
744
|
-
suffix_lower = path.suffix.lower()
|
|
745
|
-
|
|
746
|
-
if suffix_lower in IMAGE_EXTENSIONS:
|
|
747
|
-
# User provided image path
|
|
748
|
-
image_path = path
|
|
749
|
-
yaml_path = path.with_suffix(".yaml")
|
|
750
|
-
img_format = suffix_lower[1:] # Remove leading dot
|
|
751
|
-
elif suffix_lower in YAML_EXTENSIONS:
|
|
752
|
-
# User provided YAML path
|
|
753
|
-
yaml_path = path
|
|
754
|
-
# Determine image format from style or default
|
|
755
|
-
if image_format is not None:
|
|
756
|
-
img_format = image_format.lower().lstrip(".")
|
|
757
|
-
else:
|
|
758
|
-
# Check global style for preferred format
|
|
759
|
-
from .styles._style_loader import _STYLE_CACHE
|
|
760
|
-
|
|
761
|
-
if _STYLE_CACHE is not None:
|
|
762
|
-
try:
|
|
763
|
-
img_format = _STYLE_CACHE.output.format.lower()
|
|
764
|
-
except (KeyError, AttributeError):
|
|
765
|
-
img_format = "png"
|
|
766
|
-
else:
|
|
767
|
-
img_format = "png"
|
|
768
|
-
image_path = path.with_suffix(f".{img_format}")
|
|
769
|
-
else:
|
|
770
|
-
# Unknown extension - treat as base name, add both extensions
|
|
771
|
-
yaml_path = path.with_suffix(".yaml")
|
|
772
|
-
if image_format is not None:
|
|
773
|
-
img_format = image_format.lower().lstrip(".")
|
|
774
|
-
else:
|
|
775
|
-
from .styles._style_loader import _STYLE_CACHE
|
|
776
|
-
|
|
777
|
-
if _STYLE_CACHE is not None:
|
|
778
|
-
try:
|
|
779
|
-
img_format = _STYLE_CACHE.output.format.lower()
|
|
780
|
-
except (KeyError, AttributeError):
|
|
781
|
-
img_format = "png"
|
|
782
|
-
else:
|
|
783
|
-
img_format = "png"
|
|
784
|
-
image_path = path.with_suffix(f".{img_format}")
|
|
785
|
-
|
|
786
|
-
# Get DPI from style if not specified
|
|
787
|
-
if dpi is None:
|
|
788
|
-
from .styles._style_loader import _STYLE_CACHE
|
|
789
|
-
|
|
790
|
-
if _STYLE_CACHE is not None:
|
|
791
|
-
try:
|
|
792
|
-
dpi = _STYLE_CACHE.output.dpi
|
|
793
|
-
except (KeyError, AttributeError):
|
|
794
|
-
dpi = 300
|
|
795
|
-
else:
|
|
796
|
-
dpi = 300
|
|
797
|
-
|
|
798
|
-
# Get transparency setting from style
|
|
799
|
-
transparent = False
|
|
800
|
-
from .styles._style_loader import _STYLE_CACHE
|
|
801
|
-
|
|
802
|
-
if _STYLE_CACHE is not None:
|
|
803
|
-
try:
|
|
804
|
-
transparent = _STYLE_CACHE.output.transparent
|
|
805
|
-
except (KeyError, AttributeError):
|
|
806
|
-
pass
|
|
807
|
-
|
|
808
|
-
# Finalize tick configuration for all axes (avoids categorical axis interference)
|
|
809
|
-
from .styles._style_applier import finalize_ticks
|
|
810
|
-
|
|
811
|
-
for ax in fig.fig.get_axes():
|
|
812
|
-
finalize_ticks(ax)
|
|
813
|
-
|
|
814
|
-
# Save the image
|
|
815
|
-
fig.fig.savefig(image_path, dpi=dpi, bbox_inches="tight", transparent=transparent)
|
|
816
|
-
|
|
817
|
-
# Save the recipe
|
|
818
|
-
saved_yaml = fig.save_recipe(
|
|
819
|
-
yaml_path, include_data=include_data, data_format=data_format
|
|
258
|
+
from ._api._save import save_figure
|
|
259
|
+
|
|
260
|
+
return save_figure(
|
|
261
|
+
fig=fig,
|
|
262
|
+
path=path,
|
|
263
|
+
include_data=include_data,
|
|
264
|
+
data_format=data_format,
|
|
265
|
+
validate=validate,
|
|
266
|
+
validate_mse_threshold=validate_mse_threshold,
|
|
267
|
+
validate_error_level=validate_error_level,
|
|
268
|
+
verbose=verbose,
|
|
269
|
+
dpi=dpi,
|
|
270
|
+
image_format=image_format,
|
|
820
271
|
)
|
|
821
272
|
|
|
822
|
-
# Validate if requested
|
|
823
|
-
if validate:
|
|
824
|
-
from ._validator import validate_on_save
|
|
825
|
-
|
|
826
|
-
result = validate_on_save(fig, saved_yaml, mse_threshold=validate_mse_threshold)
|
|
827
|
-
status = "PASSED" if result.valid else "FAILED"
|
|
828
|
-
if verbose:
|
|
829
|
-
print(
|
|
830
|
-
f"Saved: {image_path} + {yaml_path} (Reproducible Validation: {status})"
|
|
831
|
-
)
|
|
832
|
-
if not result.valid:
|
|
833
|
-
msg = f"Reproducibility validation failed (MSE={result.mse:.1f}): {result.message}"
|
|
834
|
-
if validate_error_level == "error":
|
|
835
|
-
raise ValueError(msg)
|
|
836
|
-
elif validate_error_level == "warning":
|
|
837
|
-
import warnings
|
|
838
|
-
|
|
839
|
-
warnings.warn(msg, UserWarning)
|
|
840
|
-
# "debug" level: silent, just return the result
|
|
841
|
-
return image_path, yaml_path, result
|
|
842
|
-
|
|
843
|
-
if verbose:
|
|
844
|
-
print(f"Saved: {image_path} + {yaml_path}")
|
|
845
|
-
return image_path, yaml_path, None
|
|
846
|
-
|
|
847
273
|
|
|
848
274
|
def reproduce(
|
|
849
275
|
path: Union[str, Path],
|
|
@@ -859,7 +285,7 @@ def reproduce(
|
|
|
859
285
|
calls : list of str, optional
|
|
860
286
|
If provided, only reproduce these specific call IDs.
|
|
861
287
|
skip_decorations : bool
|
|
862
|
-
If True, skip decoration calls
|
|
288
|
+
If True, skip decoration calls.
|
|
863
289
|
|
|
864
290
|
Returns
|
|
865
291
|
-------
|
|
@@ -867,189 +293,38 @@ def reproduce(
|
|
|
867
293
|
Reproduced figure.
|
|
868
294
|
axes : Axes or list of Axes
|
|
869
295
|
Reproduced axes.
|
|
870
|
-
|
|
871
|
-
Examples
|
|
872
|
-
--------
|
|
873
|
-
>>> import figrecipe as ps
|
|
874
|
-
>>> fig, ax = ps.reproduce('experiment.yaml')
|
|
875
|
-
>>> plt.show()
|
|
876
|
-
|
|
877
|
-
>>> # Reproduce only specific plots
|
|
878
|
-
>>> fig, ax = ps.reproduce('experiment.yaml', calls=['scatter_001'])
|
|
879
296
|
"""
|
|
880
297
|
return _reproduce(path, calls=calls, skip_decorations=skip_decorations)
|
|
881
298
|
|
|
882
299
|
|
|
883
300
|
def info(path: Union[str, Path]) -> Dict[str, Any]:
|
|
884
|
-
"""Get information about a recipe without reproducing.
|
|
885
|
-
|
|
886
|
-
Parameters
|
|
887
|
-
----------
|
|
888
|
-
path : str or Path
|
|
889
|
-
Path to .yaml recipe file.
|
|
890
|
-
|
|
891
|
-
Returns
|
|
892
|
-
-------
|
|
893
|
-
dict
|
|
894
|
-
Recipe information including figure ID, creation time,
|
|
895
|
-
matplotlib version, size, and list of calls.
|
|
896
|
-
|
|
897
|
-
Examples
|
|
898
|
-
--------
|
|
899
|
-
>>> import figrecipe as ps
|
|
900
|
-
>>> recipe_info = ps.info('experiment.yaml')
|
|
901
|
-
>>> print(f"Created: {recipe_info['created']}")
|
|
902
|
-
>>> print(f"Calls: {len(recipe_info['calls'])}")
|
|
903
|
-
"""
|
|
301
|
+
"""Get information about a recipe without reproducing."""
|
|
904
302
|
return get_recipe_info(path)
|
|
905
303
|
|
|
906
304
|
|
|
907
305
|
def load(path: Union[str, Path]) -> FigureRecord:
|
|
908
|
-
"""Load a recipe as a FigureRecord object.
|
|
909
|
-
|
|
910
|
-
Parameters
|
|
911
|
-
----------
|
|
912
|
-
path : str or Path
|
|
913
|
-
Path to .yaml recipe file.
|
|
914
|
-
|
|
915
|
-
Returns
|
|
916
|
-
-------
|
|
917
|
-
FigureRecord
|
|
918
|
-
The loaded figure record.
|
|
919
|
-
|
|
920
|
-
Examples
|
|
921
|
-
--------
|
|
922
|
-
>>> import figrecipe as ps
|
|
923
|
-
>>> record = ps.load('experiment.yaml')
|
|
924
|
-
>>> # Modify the record
|
|
925
|
-
>>> record.axes['ax_0_0'].calls[0].kwargs['color'] = 'blue'
|
|
926
|
-
>>> # Reproduce with modifications
|
|
927
|
-
>>> fig, ax = ps.reproduce_from_record(record)
|
|
928
|
-
"""
|
|
306
|
+
"""Load a recipe as a FigureRecord object."""
|
|
929
307
|
return load_recipe(path)
|
|
930
308
|
|
|
931
309
|
|
|
932
310
|
def extract_data(path: Union[str, Path]) -> Dict[str, Dict[str, Any]]:
|
|
933
311
|
"""Extract data arrays from a saved recipe.
|
|
934
312
|
|
|
935
|
-
This function allows you to import/recover the data that was
|
|
936
|
-
plotted in a figure from its recipe file.
|
|
937
|
-
|
|
938
|
-
Parameters
|
|
939
|
-
----------
|
|
940
|
-
path : str or Path
|
|
941
|
-
Path to .yaml recipe file.
|
|
942
|
-
|
|
943
313
|
Returns
|
|
944
314
|
-------
|
|
945
315
|
dict
|
|
946
316
|
Nested dictionary: {call_id: {'x': array, 'y': array, ...}}
|
|
947
|
-
Each call's data is stored under its ID with keys for each argument.
|
|
948
|
-
|
|
949
|
-
Examples
|
|
950
|
-
--------
|
|
951
|
-
>>> import figrecipe as ps
|
|
952
|
-
>>> import numpy as np
|
|
953
|
-
>>>
|
|
954
|
-
>>> # Create and save a figure
|
|
955
|
-
>>> x = np.linspace(0, 10, 100)
|
|
956
|
-
>>> y = np.sin(x)
|
|
957
|
-
>>> fig, ax = ps.subplots()
|
|
958
|
-
>>> ax.plot(x, y, id='sine_wave')
|
|
959
|
-
>>> ps.save(fig, 'figure.yaml')
|
|
960
|
-
>>>
|
|
961
|
-
>>> # Later, extract the data
|
|
962
|
-
>>> data = ps.extract_data('figure.yaml')
|
|
963
|
-
>>> x_recovered = data['sine_wave']['x']
|
|
964
|
-
>>> y_recovered = data['sine_wave']['y']
|
|
965
|
-
>>> np.allclose(x, x_recovered)
|
|
966
|
-
True
|
|
967
|
-
|
|
968
|
-
Notes
|
|
969
|
-
-----
|
|
970
|
-
- Data is extracted from all plot calls (plot, scatter, bar, etc.)
|
|
971
|
-
- For plot() calls: 'x' and 'y' contain the coordinates
|
|
972
|
-
- For scatter(): 'x', 'y', and optionally 'c' (colors), 's' (sizes)
|
|
973
|
-
- For bar(): 'x' (categories) and 'height' (values)
|
|
974
|
-
- For hist(): 'x' (data array)
|
|
975
317
|
"""
|
|
976
|
-
import
|
|
318
|
+
from ._api._extract import DECORATION_FUNCS, extract_call_data
|
|
977
319
|
|
|
978
320
|
record = load_recipe(path)
|
|
979
321
|
result = {}
|
|
980
322
|
|
|
981
|
-
# Decoration functions to skip
|
|
982
|
-
decoration_funcs = {
|
|
983
|
-
"set_xlabel",
|
|
984
|
-
"set_ylabel",
|
|
985
|
-
"set_title",
|
|
986
|
-
"set_xlim",
|
|
987
|
-
"set_ylim",
|
|
988
|
-
"legend",
|
|
989
|
-
"grid",
|
|
990
|
-
"axhline",
|
|
991
|
-
"axvline",
|
|
992
|
-
"text",
|
|
993
|
-
"annotate",
|
|
994
|
-
}
|
|
995
|
-
|
|
996
323
|
for ax_key, ax_record in record.axes.items():
|
|
997
324
|
for call in ax_record.calls:
|
|
998
|
-
|
|
999
|
-
if call.function in decoration_funcs:
|
|
325
|
+
if call.function in DECORATION_FUNCS:
|
|
1000
326
|
continue
|
|
1001
|
-
|
|
1002
|
-
call_data = {}
|
|
1003
|
-
|
|
1004
|
-
def to_array(data):
|
|
1005
|
-
"""Convert data to numpy array, handling YAML types."""
|
|
1006
|
-
# Handle dict with 'data' key (serialized array format)
|
|
1007
|
-
if isinstance(data, dict) or (hasattr(data, "keys") and "data" in data):
|
|
1008
|
-
return np.array(data["data"])
|
|
1009
|
-
if hasattr(data, "tolist"): # Already array-like
|
|
1010
|
-
return np.array(data)
|
|
1011
|
-
return np.array(
|
|
1012
|
-
list(data)
|
|
1013
|
-
if hasattr(data, "__iter__") and not isinstance(data, str)
|
|
1014
|
-
else data
|
|
1015
|
-
)
|
|
1016
|
-
|
|
1017
|
-
# Extract positional arguments based on function type
|
|
1018
|
-
if call.function in ("plot", "scatter", "fill_between"):
|
|
1019
|
-
if len(call.args) >= 1:
|
|
1020
|
-
call_data["x"] = to_array(call.args[0])
|
|
1021
|
-
if len(call.args) >= 2:
|
|
1022
|
-
call_data["y"] = to_array(call.args[1])
|
|
1023
|
-
|
|
1024
|
-
elif call.function == "bar":
|
|
1025
|
-
if len(call.args) >= 1:
|
|
1026
|
-
call_data["x"] = to_array(call.args[0])
|
|
1027
|
-
if len(call.args) >= 2:
|
|
1028
|
-
call_data["height"] = to_array(call.args[1])
|
|
1029
|
-
|
|
1030
|
-
elif call.function == "hist":
|
|
1031
|
-
if len(call.args) >= 1:
|
|
1032
|
-
call_data["x"] = to_array(call.args[0])
|
|
1033
|
-
|
|
1034
|
-
elif call.function == "errorbar":
|
|
1035
|
-
if len(call.args) >= 1:
|
|
1036
|
-
call_data["x"] = to_array(call.args[0])
|
|
1037
|
-
if len(call.args) >= 2:
|
|
1038
|
-
call_data["y"] = to_array(call.args[1])
|
|
1039
|
-
|
|
1040
|
-
# Extract relevant kwargs
|
|
1041
|
-
for key in ("c", "s", "yerr", "xerr", "weights", "bins"):
|
|
1042
|
-
if key in call.kwargs:
|
|
1043
|
-
val = call.kwargs[key]
|
|
1044
|
-
if (
|
|
1045
|
-
isinstance(val, (list, tuple))
|
|
1046
|
-
or hasattr(val, "__iter__")
|
|
1047
|
-
and not isinstance(val, str)
|
|
1048
|
-
):
|
|
1049
|
-
call_data[key] = to_array(val)
|
|
1050
|
-
else:
|
|
1051
|
-
call_data[key] = val
|
|
1052
|
-
|
|
327
|
+
call_data = extract_call_data(call)
|
|
1053
328
|
if call_data:
|
|
1054
329
|
result[call.id] = call_data
|
|
1055
330
|
|
|
@@ -1062,9 +337,6 @@ def validate(
|
|
|
1062
337
|
) -> ValidationResult:
|
|
1063
338
|
"""Validate that a saved recipe can reproduce its original figure.
|
|
1064
339
|
|
|
1065
|
-
This is a standalone validation function for existing recipes.
|
|
1066
|
-
For validation during save, use `ps.save(..., validate=True)`.
|
|
1067
|
-
|
|
1068
340
|
Parameters
|
|
1069
341
|
----------
|
|
1070
342
|
path : str or Path
|
|
@@ -1075,73 +347,11 @@ def validate(
|
|
|
1075
347
|
Returns
|
|
1076
348
|
-------
|
|
1077
349
|
ValidationResult
|
|
1078
|
-
Detailed comparison results
|
|
1079
|
-
|
|
1080
|
-
Examples
|
|
1081
|
-
--------
|
|
1082
|
-
>>> import figrecipe as ps
|
|
1083
|
-
>>> result = ps.validate('experiment.yaml')
|
|
1084
|
-
>>> print(result.summary())
|
|
1085
|
-
>>> if result.valid:
|
|
1086
|
-
... print("Recipe is reproducible!")
|
|
1087
|
-
|
|
1088
|
-
Notes
|
|
1089
|
-
-----
|
|
1090
|
-
This function reproduces the figure from the recipe and compares
|
|
1091
|
-
the result to re-rendering the recipe. It cannot compare to the
|
|
1092
|
-
original figure unless you use `ps.save(..., validate=True)` which
|
|
1093
|
-
performs validation before closing the original figure.
|
|
350
|
+
Detailed comparison results.
|
|
1094
351
|
"""
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
import numpy as np
|
|
1100
|
-
|
|
1101
|
-
from ._reproducer import reproduce
|
|
1102
|
-
from ._utils._image_diff import compare_images
|
|
1103
|
-
|
|
1104
|
-
path = Path(path)
|
|
1105
|
-
|
|
1106
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1107
|
-
tmpdir = Path(tmpdir)
|
|
1108
|
-
|
|
1109
|
-
# Reproduce twice
|
|
1110
|
-
fig1, _ = reproduce(path)
|
|
1111
|
-
img1_path = tmpdir / "render1.png"
|
|
1112
|
-
fig1.savefig(img1_path, dpi=150)
|
|
1113
|
-
|
|
1114
|
-
fig2, _ = reproduce(path)
|
|
1115
|
-
img2_path = tmpdir / "render2.png"
|
|
1116
|
-
fig2.savefig(img2_path, dpi=150)
|
|
1117
|
-
|
|
1118
|
-
# Compare
|
|
1119
|
-
diff = compare_images(img1_path, img2_path)
|
|
1120
|
-
|
|
1121
|
-
mse = diff["mse"]
|
|
1122
|
-
if np.isnan(mse):
|
|
1123
|
-
valid = False
|
|
1124
|
-
message = f"Image dimensions differ: {diff['size1']} vs {diff['size2']}"
|
|
1125
|
-
elif mse > mse_threshold:
|
|
1126
|
-
valid = False
|
|
1127
|
-
message = f"MSE ({mse:.2f}) exceeds threshold ({mse_threshold})"
|
|
1128
|
-
else:
|
|
1129
|
-
valid = True
|
|
1130
|
-
message = "Recipe produces consistent output"
|
|
1131
|
-
|
|
1132
|
-
return ValidationResult(
|
|
1133
|
-
valid=valid,
|
|
1134
|
-
mse=mse if not np.isnan(mse) else float("inf"),
|
|
1135
|
-
psnr=diff["psnr"],
|
|
1136
|
-
max_diff=diff["max_diff"]
|
|
1137
|
-
if not np.isnan(diff["max_diff"])
|
|
1138
|
-
else float("inf"),
|
|
1139
|
-
size_original=diff["size1"],
|
|
1140
|
-
size_reproduced=diff["size2"],
|
|
1141
|
-
same_size=diff["same_size"],
|
|
1142
|
-
file_size_diff=diff["file_size2"] - diff["file_size1"],
|
|
1143
|
-
message=message,
|
|
1144
|
-
)
|
|
352
|
+
from ._api._validate import validate_recipe
|
|
353
|
+
|
|
354
|
+
return validate_recipe(path, mse_threshold)
|
|
1145
355
|
|
|
1146
356
|
|
|
1147
357
|
def crop(
|
|
@@ -1154,19 +364,14 @@ def crop(
|
|
|
1154
364
|
):
|
|
1155
365
|
"""Crop a figure image to its content area with a specified margin.
|
|
1156
366
|
|
|
1157
|
-
Automatically detects background color (from corners) and crops to
|
|
1158
|
-
content, leaving only the specified margin around it.
|
|
1159
|
-
|
|
1160
367
|
Parameters
|
|
1161
368
|
----------
|
|
1162
369
|
input_path : str or Path
|
|
1163
|
-
Path to the input image
|
|
370
|
+
Path to the input image.
|
|
1164
371
|
output_path : str or Path, optional
|
|
1165
|
-
Path to save the cropped image.
|
|
1166
|
-
overwrites the input. If None and overwrite=False, adds '_cropped' suffix.
|
|
372
|
+
Path to save the cropped image.
|
|
1167
373
|
margin_mm : float, optional
|
|
1168
|
-
Margin in millimeters
|
|
1169
|
-
Converted to pixels using image DPI (or 300 DPI if not available).
|
|
374
|
+
Margin in millimeters (default: 1.0mm).
|
|
1170
375
|
margin_px : int, optional
|
|
1171
376
|
Margin in pixels (overrides margin_mm if provided).
|
|
1172
377
|
overwrite : bool, optional
|
|
@@ -1178,15 +383,6 @@ def crop(
|
|
|
1178
383
|
-------
|
|
1179
384
|
Path
|
|
1180
385
|
Path to the saved cropped image.
|
|
1181
|
-
|
|
1182
|
-
Examples
|
|
1183
|
-
--------
|
|
1184
|
-
>>> import figrecipe as fr
|
|
1185
|
-
>>> fig, ax = fr.subplots(axes_width_mm=60, axes_height_mm=40)
|
|
1186
|
-
>>> ax.plot([1, 2, 3], [1, 2, 3], id='line')
|
|
1187
|
-
>>> fig.savefig("figure.png", dpi=300)
|
|
1188
|
-
>>> fr.crop("figure.png", overwrite=True) # 1mm margin
|
|
1189
|
-
>>> fr.crop("figure.png", margin_mm=2.0) # 2mm margin
|
|
1190
386
|
"""
|
|
1191
387
|
from ._utils._crop import crop as _crop
|
|
1192
388
|
|
|
@@ -1198,56 +394,33 @@ def edit(
|
|
|
1198
394
|
style=None,
|
|
1199
395
|
port: int = 5050,
|
|
1200
396
|
open_browser: bool = True,
|
|
397
|
+
hot_reload: bool = False,
|
|
1201
398
|
):
|
|
1202
399
|
"""Launch interactive GUI editor for figure styling.
|
|
1203
400
|
|
|
1204
|
-
Opens a browser-based editor that allows interactive adjustment of
|
|
1205
|
-
figure styles using hitmap-based element selection.
|
|
1206
|
-
|
|
1207
401
|
Parameters
|
|
1208
402
|
----------
|
|
1209
403
|
source : RecordingFigure, str, or Path
|
|
1210
404
|
Either a live RecordingFigure object or path to a .yaml recipe file.
|
|
1211
405
|
style : str or dict, optional
|
|
1212
|
-
Style preset name
|
|
1213
|
-
If None, uses the currently loaded global style.
|
|
406
|
+
Style preset name or style dict.
|
|
1214
407
|
port : int, optional
|
|
1215
|
-
Flask server port (default: 5050).
|
|
408
|
+
Flask server port (default: 5050).
|
|
1216
409
|
open_browser : bool, optional
|
|
1217
410
|
Whether to open browser automatically (default: True).
|
|
411
|
+
hot_reload : bool, optional
|
|
412
|
+
Enable hot reload (default: False).
|
|
1218
413
|
|
|
1219
414
|
Returns
|
|
1220
415
|
-------
|
|
1221
416
|
dict
|
|
1222
417
|
Final style overrides after editing session.
|
|
1223
|
-
|
|
1224
|
-
Examples
|
|
1225
|
-
--------
|
|
1226
|
-
Edit a live figure:
|
|
1227
|
-
|
|
1228
|
-
>>> import figrecipe as fr
|
|
1229
|
-
>>> fig, ax = fr.subplots()
|
|
1230
|
-
>>> ax.plot([1, 2, 3], [1, 4, 9], id='quadratic')
|
|
1231
|
-
>>> overrides = fr.edit(fig)
|
|
1232
|
-
|
|
1233
|
-
Edit a saved recipe:
|
|
1234
|
-
|
|
1235
|
-
>>> overrides = fr.edit('my_figure.yaml')
|
|
1236
|
-
|
|
1237
|
-
With explicit style:
|
|
1238
|
-
|
|
1239
|
-
>>> overrides = fr.edit(fig, style='SCITEX_DARK')
|
|
1240
|
-
|
|
1241
|
-
Notes
|
|
1242
|
-
-----
|
|
1243
|
-
Requires Flask to be installed. Install with:
|
|
1244
|
-
pip install figrecipe[editor]
|
|
1245
|
-
or:
|
|
1246
|
-
pip install flask pillow
|
|
1247
418
|
"""
|
|
1248
419
|
from ._editor import edit as _edit
|
|
1249
420
|
|
|
1250
|
-
return _edit(
|
|
421
|
+
return _edit(
|
|
422
|
+
source, style=style, port=port, open_browser=open_browser, hot_reload=hot_reload
|
|
423
|
+
)
|
|
1251
424
|
|
|
1252
425
|
|
|
1253
426
|
def panel_label(
|
|
@@ -1261,10 +434,6 @@ def panel_label(
|
|
|
1261
434
|
):
|
|
1262
435
|
"""Add a panel label (A, B, C, ...) to an axes.
|
|
1263
436
|
|
|
1264
|
-
Panel labels are commonly used in multi-panel scientific figures to
|
|
1265
|
-
identify individual subplots. This function places a label at the
|
|
1266
|
-
specified location relative to the axes.
|
|
1267
|
-
|
|
1268
437
|
Parameters
|
|
1269
438
|
----------
|
|
1270
439
|
ax : Axes or RecordingAxes
|
|
@@ -1272,15 +441,13 @@ def panel_label(
|
|
|
1272
441
|
label : str
|
|
1273
442
|
The label text (e.g., 'A', 'B', 'a)', '(1)').
|
|
1274
443
|
loc : str, optional
|
|
1275
|
-
Label location: 'upper left'
|
|
1276
|
-
'lower left', 'lower right', or 'outside'.
|
|
444
|
+
Label location: 'upper left', 'upper right', etc.
|
|
1277
445
|
fontsize : float, optional
|
|
1278
|
-
Font size in points.
|
|
446
|
+
Font size in points.
|
|
1279
447
|
fontweight : str, optional
|
|
1280
|
-
Font weight: 'bold'
|
|
448
|
+
Font weight (default: 'bold').
|
|
1281
449
|
offset : tuple of float, optional
|
|
1282
|
-
(x, y) offset in axes coordinates.
|
|
1283
|
-
label slightly outside top-left corner.
|
|
450
|
+
(x, y) offset in axes coordinates.
|
|
1284
451
|
**kwargs
|
|
1285
452
|
Additional arguments passed to ax.text().
|
|
1286
453
|
|
|
@@ -1288,71 +455,37 @@ def panel_label(
|
|
|
1288
455
|
-------
|
|
1289
456
|
Text
|
|
1290
457
|
The matplotlib Text object.
|
|
458
|
+
"""
|
|
459
|
+
import matplotlib.pyplot as mpl_plt
|
|
1291
460
|
|
|
1292
|
-
|
|
1293
|
-
--------
|
|
1294
|
-
>>> import figrecipe as fr
|
|
1295
|
-
>>> fig, axes = fr.subplots(nrows=2, ncols=2)
|
|
1296
|
-
>>> for i, ax in enumerate(axes.flat):
|
|
1297
|
-
... fr.panel_label(ax, chr(65 + i)) # A, B, C, D
|
|
461
|
+
from ._api._panel import calculate_panel_position, get_panel_label_fontsize
|
|
1298
462
|
|
|
1299
|
-
|
|
1300
|
-
|
|
463
|
+
fontsize = get_panel_label_fontsize(fontsize)
|
|
464
|
+
x, y = calculate_panel_position(loc, offset)
|
|
465
|
+
|
|
466
|
+
default_color = mpl_plt.rcParams.get("text.color", "black")
|
|
1301
467
|
|
|
1302
|
-
>>> # Outside position (default)
|
|
1303
|
-
>>> fr.panel_label(ax, 'A', loc='upper left')
|
|
1304
|
-
"""
|
|
1305
|
-
# Get fontsize from style if available, otherwise default to 10pt
|
|
1306
|
-
if fontsize is None:
|
|
1307
|
-
try:
|
|
1308
|
-
from .styles._style_loader import _STYLE_CACHE
|
|
1309
|
-
|
|
1310
|
-
if _STYLE_CACHE is not None:
|
|
1311
|
-
fontsize = getattr(
|
|
1312
|
-
getattr(_STYLE_CACHE, "fonts", None), "panel_label_pt", 10
|
|
1313
|
-
)
|
|
1314
|
-
else:
|
|
1315
|
-
fontsize = 10
|
|
1316
|
-
except Exception:
|
|
1317
|
-
fontsize = 10
|
|
1318
|
-
|
|
1319
|
-
# Calculate position based on loc
|
|
1320
|
-
if loc == "upper left":
|
|
1321
|
-
x, y = offset
|
|
1322
|
-
elif loc == "upper right":
|
|
1323
|
-
x, y = 1.0 + abs(offset[0]), offset[1]
|
|
1324
|
-
elif loc == "lower left":
|
|
1325
|
-
x, y = offset[0], -abs(offset[1]) + 1.0
|
|
1326
|
-
elif loc == "lower right":
|
|
1327
|
-
x, y = 1.0 + abs(offset[0]), -abs(offset[1]) + 1.0
|
|
1328
|
-
else:
|
|
1329
|
-
x, y = offset
|
|
1330
|
-
|
|
1331
|
-
# Default kwargs - use 'axes' as transform string (handled by reproducer)
|
|
1332
468
|
text_kwargs = {
|
|
1333
469
|
"fontsize": fontsize,
|
|
1334
470
|
"fontweight": fontweight,
|
|
1335
|
-
"
|
|
471
|
+
"color": default_color,
|
|
472
|
+
"transform": "axes",
|
|
1336
473
|
"va": "bottom",
|
|
1337
474
|
"ha": "right" if "right" in loc else "left",
|
|
1338
475
|
}
|
|
1339
476
|
text_kwargs.update(kwargs)
|
|
1340
477
|
|
|
1341
|
-
# Get the underlying matplotlib axes
|
|
1342
478
|
mpl_ax = ax._ax if hasattr(ax, "_ax") else ax
|
|
1343
479
|
|
|
1344
|
-
# For actual rendering, use the real transform
|
|
1345
480
|
render_kwargs = text_kwargs.copy()
|
|
1346
481
|
render_kwargs["transform"] = mpl_ax.transAxes
|
|
1347
482
|
|
|
1348
|
-
# Record the call using recorder's method (handles args/kwargs processing)
|
|
1349
483
|
if hasattr(ax, "_recorder") and hasattr(ax, "_position"):
|
|
1350
484
|
ax._recorder.record_call(
|
|
1351
485
|
ax_position=ax._position,
|
|
1352
486
|
method_name="text",
|
|
1353
487
|
args=(x, y, label),
|
|
1354
|
-
kwargs=text_kwargs,
|
|
488
|
+
kwargs=text_kwargs,
|
|
1355
489
|
)
|
|
1356
490
|
|
|
1357
|
-
# Render directly on matplotlib axes with actual transform
|
|
1358
491
|
return mpl_ax.text(x, y, label, **render_kwargs)
|