figrecipe 0.6.0__py3-none-any.whl → 0.9.0__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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -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/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -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 +61 -13
- 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 +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -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 +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -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/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -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 +42 -480
- 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 +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -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/METADATA +0 -394
- 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.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
figrecipe/__init__.py
CHANGED
|
@@ -56,6 +56,34 @@ 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
|
+
# Panel label
|
|
63
|
+
from ._api._panel import panel_label
|
|
64
|
+
|
|
65
|
+
# Seaborn proxy
|
|
66
|
+
from ._api._seaborn_proxy import sns
|
|
67
|
+
|
|
68
|
+
# Composition API
|
|
69
|
+
from ._composition import (
|
|
70
|
+
AlignmentMode,
|
|
71
|
+
align_panels,
|
|
72
|
+
compose,
|
|
73
|
+
distribute_panels,
|
|
74
|
+
hide_panel,
|
|
75
|
+
import_axes,
|
|
76
|
+
show_panel,
|
|
77
|
+
smart_align,
|
|
78
|
+
toggle_panel,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# scitex.stats integration
|
|
82
|
+
from ._integrations import (
|
|
83
|
+
SCITEX_STATS_AVAILABLE,
|
|
84
|
+
annotate_from_stats,
|
|
85
|
+
from_scitex_stats,
|
|
86
|
+
)
|
|
59
87
|
from ._recorder import CallRecord, FigureRecord
|
|
60
88
|
from ._reproducer import get_recipe_info
|
|
61
89
|
from ._reproducer import reproduce as _reproduce
|
|
@@ -71,93 +99,22 @@ from ._utils._units import (
|
|
|
71
99
|
)
|
|
72
100
|
from ._validator import ValidationResult
|
|
73
101
|
from ._wrappers import RecordingAxes, RecordingFigure
|
|
74
|
-
from ._wrappers._figure import create_recording_subplots
|
|
75
102
|
from .styles._style_applier import check_font, list_available_fonts
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
104
|
+
try:
|
|
105
|
+
from importlib.metadata import version as _get_version
|
|
149
106
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
__version__ = "0.4.0"
|
|
107
|
+
__version__ = _get_version("figrecipe")
|
|
108
|
+
except Exception:
|
|
109
|
+
__version__ = "0.0.0" # Fallback for development
|
|
154
110
|
__all__ = [
|
|
155
111
|
# Main API
|
|
156
112
|
"subplots",
|
|
157
113
|
"save",
|
|
158
114
|
"reproduce",
|
|
115
|
+
"load", # Alias for reproduce
|
|
159
116
|
"info",
|
|
160
|
-
"
|
|
117
|
+
"load_record",
|
|
161
118
|
"extract_data",
|
|
162
119
|
"validate",
|
|
163
120
|
# GUI Editor
|
|
@@ -192,151 +149,34 @@ __all__ = [
|
|
|
192
149
|
"crop",
|
|
193
150
|
# Panel labels
|
|
194
151
|
"panel_label",
|
|
152
|
+
# Composition
|
|
153
|
+
"compose",
|
|
154
|
+
"import_axes",
|
|
155
|
+
"hide_panel",
|
|
156
|
+
"show_panel",
|
|
157
|
+
"toggle_panel",
|
|
158
|
+
# Alignment
|
|
159
|
+
"AlignmentMode",
|
|
160
|
+
"align_panels",
|
|
161
|
+
"distribute_panels",
|
|
162
|
+
"smart_align",
|
|
163
|
+
# scitex.stats integration
|
|
164
|
+
"from_scitex_stats",
|
|
165
|
+
"annotate_from_stats",
|
|
166
|
+
"SCITEX_STATS_AVAILABLE",
|
|
195
167
|
# Version
|
|
196
168
|
"__version__",
|
|
197
169
|
]
|
|
198
170
|
|
|
199
171
|
|
|
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()
|
|
172
|
+
# Style management
|
|
173
|
+
from ._api._style_manager import (
|
|
174
|
+
STYLE,
|
|
175
|
+
apply_style,
|
|
176
|
+
list_presets,
|
|
177
|
+
load_style,
|
|
178
|
+
unload_style,
|
|
179
|
+
)
|
|
340
180
|
|
|
341
181
|
|
|
342
182
|
def subplots(
|
|
@@ -354,6 +194,8 @@ def subplots(
|
|
|
354
194
|
# Style parameters
|
|
355
195
|
style: Optional[Dict[str, Any]] = None,
|
|
356
196
|
apply_style_mm: bool = True,
|
|
197
|
+
# Panel labels (None = use style default, True/False = explicit)
|
|
198
|
+
panel_labels: Optional[bool] = None,
|
|
357
199
|
**kwargs,
|
|
358
200
|
) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
|
|
359
201
|
"""Create a figure with recording-enabled axes.
|
|
@@ -365,281 +207,51 @@ def subplots(
|
|
|
365
207
|
|
|
366
208
|
Parameters
|
|
367
209
|
----------
|
|
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
|
-
----------------
|
|
210
|
+
nrows, ncols : int
|
|
211
|
+
Number of rows and columns of subplots.
|
|
212
|
+
axes_width_mm, axes_height_mm : float, optional
|
|
213
|
+
Axes dimensions in mm.
|
|
214
|
+
margin_left_mm, margin_right_mm : float, optional
|
|
215
|
+
Left/right margins in mm.
|
|
216
|
+
margin_bottom_mm, margin_top_mm : float, optional
|
|
217
|
+
Bottom/top margins in mm.
|
|
218
|
+
space_w_mm, space_h_mm : float, optional
|
|
219
|
+
Horizontal/vertical spacing between axes in mm.
|
|
394
220
|
style : dict, optional
|
|
395
|
-
Style configuration dictionary
|
|
221
|
+
Style configuration dictionary.
|
|
396
222
|
apply_style_mm : bool
|
|
397
|
-
If True (default), apply loaded style to axes
|
|
398
|
-
|
|
399
|
-
|
|
223
|
+
If True (default), apply loaded style to axes.
|
|
224
|
+
panel_labels : bool or None
|
|
225
|
+
If True, add panel labels (A, B, C, ...).
|
|
400
226
|
**kwargs
|
|
401
|
-
Additional arguments passed to plt.subplots()
|
|
227
|
+
Additional arguments passed to plt.subplots().
|
|
402
228
|
|
|
403
229
|
Returns
|
|
404
230
|
-------
|
|
405
231
|
fig : RecordingFigure
|
|
406
232
|
Wrapped figure object.
|
|
407
233
|
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
|
|
234
|
+
Wrapped axes.
|
|
432
235
|
"""
|
|
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
|
-
]
|
|
236
|
+
from ._api._subplots import create_subplots
|
|
237
|
+
|
|
238
|
+
return create_subplots(
|
|
239
|
+
nrows=nrows,
|
|
240
|
+
ncols=ncols,
|
|
241
|
+
axes_width_mm=axes_width_mm,
|
|
242
|
+
axes_height_mm=axes_height_mm,
|
|
243
|
+
margin_left_mm=margin_left_mm,
|
|
244
|
+
margin_right_mm=margin_right_mm,
|
|
245
|
+
margin_bottom_mm=margin_bottom_mm,
|
|
246
|
+
margin_top_mm=margin_top_mm,
|
|
247
|
+
space_w_mm=space_w_mm,
|
|
248
|
+
space_h_mm=space_h_mm,
|
|
249
|
+
style=style,
|
|
250
|
+
apply_style_mm=apply_style_mm,
|
|
251
|
+
panel_labels=panel_labels,
|
|
252
|
+
**kwargs,
|
|
471
253
|
)
|
|
472
254
|
|
|
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
255
|
|
|
644
256
|
def save(
|
|
645
257
|
fig: Union[RecordingFigure, Figure],
|
|
@@ -662,188 +274,46 @@ def save(
|
|
|
662
274
|
Parameters
|
|
663
275
|
----------
|
|
664
276
|
fig : RecordingFigure or Figure
|
|
665
|
-
The figure to save.
|
|
277
|
+
The figure to save.
|
|
666
278
|
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
|
|
279
|
+
Output path (.png, .pdf, .svg, .yaml, etc.)
|
|
670
280
|
include_data : bool
|
|
671
281
|
If True, save large arrays to separate files.
|
|
672
282
|
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
|
|
283
|
+
Format for data files: 'csv', 'npz', or 'inline'.
|
|
677
284
|
validate : bool
|
|
678
|
-
If True (default), validate reproducibility after saving
|
|
679
|
-
reproducing the figure and comparing it to the original.
|
|
285
|
+
If True (default), validate reproducibility after saving.
|
|
680
286
|
validate_mse_threshold : float
|
|
681
287
|
Maximum acceptable MSE for validation (default: 100).
|
|
682
288
|
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)
|
|
289
|
+
How to handle validation failures: 'error', 'warning', or 'debug'.
|
|
687
290
|
verbose : bool
|
|
688
|
-
If True (default), print save status.
|
|
291
|
+
If True (default), print save status.
|
|
689
292
|
dpi : int, optional
|
|
690
|
-
DPI for image output.
|
|
293
|
+
DPI for image output.
|
|
691
294
|
image_format : str, optional
|
|
692
|
-
Image format when path is YAML
|
|
693
|
-
Uses style's output.format or 'png' if not specified.
|
|
295
|
+
Image format when path is YAML.
|
|
694
296
|
|
|
695
297
|
Returns
|
|
696
298
|
-------
|
|
697
299
|
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
|
|
300
|
+
(image_path, yaml_path, ValidationResult or None)
|
|
722
301
|
"""
|
|
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
|
|
302
|
+
from ._api._save import save_figure
|
|
303
|
+
|
|
304
|
+
return save_figure(
|
|
305
|
+
fig=fig,
|
|
306
|
+
path=path,
|
|
307
|
+
include_data=include_data,
|
|
308
|
+
data_format=data_format,
|
|
309
|
+
validate=validate,
|
|
310
|
+
validate_mse_threshold=validate_mse_threshold,
|
|
311
|
+
validate_error_level=validate_error_level,
|
|
312
|
+
verbose=verbose,
|
|
313
|
+
dpi=dpi,
|
|
314
|
+
image_format=image_format,
|
|
820
315
|
)
|
|
821
316
|
|
|
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
317
|
|
|
848
318
|
def reproduce(
|
|
849
319
|
path: Union[str, Path],
|
|
@@ -859,7 +329,7 @@ def reproduce(
|
|
|
859
329
|
calls : list of str, optional
|
|
860
330
|
If provided, only reproduce these specific call IDs.
|
|
861
331
|
skip_decorations : bool
|
|
862
|
-
If True, skip decoration calls
|
|
332
|
+
If True, skip decoration calls.
|
|
863
333
|
|
|
864
334
|
Returns
|
|
865
335
|
-------
|
|
@@ -867,189 +337,42 @@ def reproduce(
|
|
|
867
337
|
Reproduced figure.
|
|
868
338
|
axes : Axes or list of Axes
|
|
869
339
|
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
340
|
"""
|
|
880
341
|
return _reproduce(path, calls=calls, skip_decorations=skip_decorations)
|
|
881
342
|
|
|
882
343
|
|
|
883
344
|
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
|
-
"""
|
|
345
|
+
"""Get information about a recipe without reproducing."""
|
|
904
346
|
return get_recipe_info(path)
|
|
905
347
|
|
|
906
348
|
|
|
907
|
-
def
|
|
908
|
-
"""Load a recipe as a FigureRecord object.
|
|
349
|
+
def load_record(path: Union[str, Path]) -> FigureRecord:
|
|
350
|
+
"""Load a recipe as a FigureRecord object (advanced use)."""
|
|
351
|
+
return load_recipe(path)
|
|
909
352
|
|
|
910
|
-
Parameters
|
|
911
|
-
----------
|
|
912
|
-
path : str or Path
|
|
913
|
-
Path to .yaml recipe file.
|
|
914
353
|
|
|
915
|
-
|
|
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
|
-
"""
|
|
929
|
-
return load_recipe(path)
|
|
354
|
+
# Alias for intuitive save/load symmetry
|
|
355
|
+
load = reproduce
|
|
930
356
|
|
|
931
357
|
|
|
932
358
|
def extract_data(path: Union[str, Path]) -> Dict[str, Dict[str, Any]]:
|
|
933
359
|
"""Extract data arrays from a saved recipe.
|
|
934
360
|
|
|
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
361
|
Returns
|
|
944
362
|
-------
|
|
945
363
|
dict
|
|
946
364
|
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
365
|
"""
|
|
976
|
-
import
|
|
366
|
+
from ._api._extract import DECORATION_FUNCS, extract_call_data
|
|
977
367
|
|
|
978
368
|
record = load_recipe(path)
|
|
979
369
|
result = {}
|
|
980
370
|
|
|
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
371
|
for ax_key, ax_record in record.axes.items():
|
|
997
372
|
for call in ax_record.calls:
|
|
998
|
-
|
|
999
|
-
if call.function in decoration_funcs:
|
|
373
|
+
if call.function in DECORATION_FUNCS:
|
|
1000
374
|
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
|
-
|
|
375
|
+
call_data = extract_call_data(call)
|
|
1053
376
|
if call_data:
|
|
1054
377
|
result[call.id] = call_data
|
|
1055
378
|
|
|
@@ -1062,9 +385,6 @@ def validate(
|
|
|
1062
385
|
) -> ValidationResult:
|
|
1063
386
|
"""Validate that a saved recipe can reproduce its original figure.
|
|
1064
387
|
|
|
1065
|
-
This is a standalone validation function for existing recipes.
|
|
1066
|
-
For validation during save, use `ps.save(..., validate=True)`.
|
|
1067
|
-
|
|
1068
388
|
Parameters
|
|
1069
389
|
----------
|
|
1070
390
|
path : str or Path
|
|
@@ -1075,73 +395,11 @@ def validate(
|
|
|
1075
395
|
Returns
|
|
1076
396
|
-------
|
|
1077
397
|
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.
|
|
398
|
+
Detailed comparison results.
|
|
1094
399
|
"""
|
|
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
|
-
)
|
|
400
|
+
from ._api._validate import validate_recipe
|
|
401
|
+
|
|
402
|
+
return validate_recipe(path, mse_threshold)
|
|
1145
403
|
|
|
1146
404
|
|
|
1147
405
|
def crop(
|
|
@@ -1154,19 +412,14 @@ def crop(
|
|
|
1154
412
|
):
|
|
1155
413
|
"""Crop a figure image to its content area with a specified margin.
|
|
1156
414
|
|
|
1157
|
-
Automatically detects background color (from corners) and crops to
|
|
1158
|
-
content, leaving only the specified margin around it.
|
|
1159
|
-
|
|
1160
415
|
Parameters
|
|
1161
416
|
----------
|
|
1162
417
|
input_path : str or Path
|
|
1163
|
-
Path to the input image
|
|
418
|
+
Path to the input image.
|
|
1164
419
|
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.
|
|
420
|
+
Path to save the cropped image.
|
|
1167
421
|
margin_mm : float, optional
|
|
1168
|
-
Margin in millimeters
|
|
1169
|
-
Converted to pixels using image DPI (or 300 DPI if not available).
|
|
422
|
+
Margin in millimeters (default: 1.0mm).
|
|
1170
423
|
margin_px : int, optional
|
|
1171
424
|
Margin in pixels (overrides margin_mm if provided).
|
|
1172
425
|
overwrite : bool, optional
|
|
@@ -1178,15 +431,6 @@ def crop(
|
|
|
1178
431
|
-------
|
|
1179
432
|
Path
|
|
1180
433
|
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
434
|
"""
|
|
1191
435
|
from ._utils._crop import crop as _crop
|
|
1192
436
|
|
|
@@ -1194,165 +438,52 @@ def crop(
|
|
|
1194
438
|
|
|
1195
439
|
|
|
1196
440
|
def edit(
|
|
1197
|
-
source,
|
|
441
|
+
source=None,
|
|
1198
442
|
style=None,
|
|
1199
443
|
port: int = 5050,
|
|
444
|
+
host: str = "127.0.0.1",
|
|
1200
445
|
open_browser: bool = True,
|
|
446
|
+
hot_reload: bool = False,
|
|
447
|
+
working_dir=None,
|
|
448
|
+
desktop: bool = False,
|
|
1201
449
|
):
|
|
1202
450
|
"""Launch interactive GUI editor for figure styling.
|
|
1203
451
|
|
|
1204
|
-
Opens a browser-based editor that allows interactive adjustment of
|
|
1205
|
-
figure styles using hitmap-based element selection.
|
|
1206
|
-
|
|
1207
452
|
Parameters
|
|
1208
453
|
----------
|
|
1209
|
-
source : RecordingFigure, str, or
|
|
1210
|
-
Either a live RecordingFigure object
|
|
454
|
+
source : RecordingFigure, str, Path, or None
|
|
455
|
+
Either a live RecordingFigure object, path to a .yaml recipe file,
|
|
456
|
+
or None to create a new blank figure.
|
|
1211
457
|
style : str or dict, optional
|
|
1212
|
-
Style preset name
|
|
1213
|
-
If None, uses the currently loaded global style.
|
|
458
|
+
Style preset name or style dict.
|
|
1214
459
|
port : int, optional
|
|
1215
|
-
Flask server port (default: 5050).
|
|
460
|
+
Flask server port (default: 5050).
|
|
461
|
+
host : str, optional
|
|
462
|
+
Host to bind Flask server (default: "127.0.0.1", use "0.0.0.0" for Docker).
|
|
1216
463
|
open_browser : bool, optional
|
|
1217
464
|
Whether to open browser automatically (default: True).
|
|
465
|
+
hot_reload : bool, optional
|
|
466
|
+
Enable hot reload (default: False).
|
|
467
|
+
working_dir : str or Path, optional
|
|
468
|
+
Working directory for file browser (default: directory containing source).
|
|
469
|
+
desktop : bool, optional
|
|
470
|
+
Launch as native desktop window using pywebview (default: False).
|
|
471
|
+
Requires: pip install figrecipe[desktop]
|
|
1218
472
|
|
|
1219
473
|
Returns
|
|
1220
474
|
-------
|
|
1221
475
|
dict
|
|
1222
476
|
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
477
|
"""
|
|
1248
478
|
from ._editor import edit as _edit
|
|
1249
479
|
|
|
1250
|
-
return _edit(
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
**kwargs,
|
|
1261
|
-
):
|
|
1262
|
-
"""Add a panel label (A, B, C, ...) to an axes.
|
|
1263
|
-
|
|
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
|
-
Parameters
|
|
1269
|
-
----------
|
|
1270
|
-
ax : Axes or RecordingAxes
|
|
1271
|
-
The axes to label.
|
|
1272
|
-
label : str
|
|
1273
|
-
The label text (e.g., 'A', 'B', 'a)', '(1)').
|
|
1274
|
-
loc : str, optional
|
|
1275
|
-
Label location: 'upper left' (default), 'upper right',
|
|
1276
|
-
'lower left', 'lower right', or 'outside'.
|
|
1277
|
-
fontsize : float, optional
|
|
1278
|
-
Font size in points. If None, uses title font size from style or 10.
|
|
1279
|
-
fontweight : str, optional
|
|
1280
|
-
Font weight: 'bold' (default), 'normal', etc.
|
|
1281
|
-
offset : tuple of float, optional
|
|
1282
|
-
(x, y) offset in axes coordinates. Default (-0.1, 1.05) places
|
|
1283
|
-
label slightly outside top-left corner.
|
|
1284
|
-
**kwargs
|
|
1285
|
-
Additional arguments passed to ax.text().
|
|
1286
|
-
|
|
1287
|
-
Returns
|
|
1288
|
-
-------
|
|
1289
|
-
Text
|
|
1290
|
-
The matplotlib Text object.
|
|
1291
|
-
|
|
1292
|
-
Examples
|
|
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
|
|
1298
|
-
|
|
1299
|
-
>>> # Custom styling
|
|
1300
|
-
>>> fr.panel_label(ax, 'a)', fontsize=12, fontweight='normal')
|
|
1301
|
-
|
|
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
|
-
text_kwargs = {
|
|
1333
|
-
"fontsize": fontsize,
|
|
1334
|
-
"fontweight": fontweight,
|
|
1335
|
-
"transform": "axes", # Special string marker for axes coordinates
|
|
1336
|
-
"va": "bottom",
|
|
1337
|
-
"ha": "right" if "right" in loc else "left",
|
|
1338
|
-
}
|
|
1339
|
-
text_kwargs.update(kwargs)
|
|
1340
|
-
|
|
1341
|
-
# Get the underlying matplotlib axes
|
|
1342
|
-
mpl_ax = ax._ax if hasattr(ax, "_ax") else ax
|
|
1343
|
-
|
|
1344
|
-
# For actual rendering, use the real transform
|
|
1345
|
-
render_kwargs = text_kwargs.copy()
|
|
1346
|
-
render_kwargs["transform"] = mpl_ax.transAxes
|
|
1347
|
-
|
|
1348
|
-
# Record the call using recorder's method (handles args/kwargs processing)
|
|
1349
|
-
if hasattr(ax, "_recorder") and hasattr(ax, "_position"):
|
|
1350
|
-
ax._recorder.record_call(
|
|
1351
|
-
ax_position=ax._position,
|
|
1352
|
-
method_name="text",
|
|
1353
|
-
args=(x, y, label),
|
|
1354
|
-
kwargs=text_kwargs, # Contains transform: "axes"
|
|
1355
|
-
)
|
|
1356
|
-
|
|
1357
|
-
# Render directly on matplotlib axes with actual transform
|
|
1358
|
-
return mpl_ax.text(x, y, label, **render_kwargs)
|
|
480
|
+
return _edit(
|
|
481
|
+
source,
|
|
482
|
+
style=style,
|
|
483
|
+
port=port,
|
|
484
|
+
host=host,
|
|
485
|
+
open_browser=open_browser,
|
|
486
|
+
hot_reload=hot_reload,
|
|
487
|
+
working_dir=working_dir,
|
|
488
|
+
desktop=desktop,
|
|
489
|
+
)
|