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