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/__main__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""API helper modules for figrecipe.
|
|
4
|
+
|
|
5
|
+
This package contains helper functions extracted from the main __init__.py
|
|
6
|
+
to reduce file size and improve maintainability.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._extract import DECORATION_FUNCS, extract_call_data, to_array
|
|
10
|
+
from ._panel import calculate_panel_position, get_panel_label_fontsize
|
|
11
|
+
from ._save import (
|
|
12
|
+
IMAGE_EXTENSIONS,
|
|
13
|
+
YAML_EXTENSIONS,
|
|
14
|
+
get_save_dpi,
|
|
15
|
+
get_save_transparency,
|
|
16
|
+
resolve_save_paths,
|
|
17
|
+
)
|
|
18
|
+
from ._subplots import (
|
|
19
|
+
_apply_mm_layout_to_figure,
|
|
20
|
+
_apply_style_to_axes,
|
|
21
|
+
_calculate_mm_layout,
|
|
22
|
+
_check_mm_layout,
|
|
23
|
+
_get_mm_value,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Subplots helpers
|
|
28
|
+
"_get_mm_value",
|
|
29
|
+
"_check_mm_layout",
|
|
30
|
+
"_calculate_mm_layout",
|
|
31
|
+
"_apply_mm_layout_to_figure",
|
|
32
|
+
"_apply_style_to_axes",
|
|
33
|
+
# Save helpers
|
|
34
|
+
"IMAGE_EXTENSIONS",
|
|
35
|
+
"YAML_EXTENSIONS",
|
|
36
|
+
"resolve_save_paths",
|
|
37
|
+
"get_save_dpi",
|
|
38
|
+
"get_save_transparency",
|
|
39
|
+
# Extract helpers
|
|
40
|
+
"DECORATION_FUNCS",
|
|
41
|
+
"to_array",
|
|
42
|
+
"extract_call_data",
|
|
43
|
+
# Panel helpers
|
|
44
|
+
"get_panel_label_fontsize",
|
|
45
|
+
"calculate_panel_position",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# EOF
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Data extraction helpers for the public API."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Set
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
# Decoration functions to skip when extracting data
|
|
10
|
+
DECORATION_FUNCS: Set[str] = {
|
|
11
|
+
"set_xlabel",
|
|
12
|
+
"set_ylabel",
|
|
13
|
+
"set_title",
|
|
14
|
+
"set_xlim",
|
|
15
|
+
"set_ylim",
|
|
16
|
+
"legend",
|
|
17
|
+
"grid",
|
|
18
|
+
"axhline",
|
|
19
|
+
"axvline",
|
|
20
|
+
"text",
|
|
21
|
+
"annotate",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_array(data: Any) -> np.ndarray:
|
|
26
|
+
"""Convert data to numpy array, handling YAML types.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
data : any
|
|
31
|
+
Data to convert.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
np.ndarray
|
|
36
|
+
Converted numpy array.
|
|
37
|
+
"""
|
|
38
|
+
# Handle dict with 'data' key (serialized array format)
|
|
39
|
+
if isinstance(data, dict) or (hasattr(data, "keys") and "data" in data):
|
|
40
|
+
return np.array(data["data"])
|
|
41
|
+
if hasattr(data, "tolist"): # Already array-like
|
|
42
|
+
return np.array(data)
|
|
43
|
+
return np.array(
|
|
44
|
+
list(data) if hasattr(data, "__iter__") and not isinstance(data, str) else data
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_call_data(call) -> Dict[str, Any]:
|
|
49
|
+
"""Extract data arrays from a single call record.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
call : CallRecord
|
|
54
|
+
The call to extract data from.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
dict
|
|
59
|
+
Dictionary with extracted data arrays.
|
|
60
|
+
"""
|
|
61
|
+
call_data = {}
|
|
62
|
+
|
|
63
|
+
# Extract positional arguments based on function type
|
|
64
|
+
if call.function in ("plot", "scatter", "fill_between"):
|
|
65
|
+
if len(call.args) >= 1:
|
|
66
|
+
call_data["x"] = to_array(call.args[0])
|
|
67
|
+
if len(call.args) >= 2:
|
|
68
|
+
call_data["y"] = to_array(call.args[1])
|
|
69
|
+
|
|
70
|
+
elif call.function == "bar":
|
|
71
|
+
if len(call.args) >= 1:
|
|
72
|
+
call_data["x"] = to_array(call.args[0])
|
|
73
|
+
if len(call.args) >= 2:
|
|
74
|
+
call_data["height"] = to_array(call.args[1])
|
|
75
|
+
|
|
76
|
+
elif call.function == "hist":
|
|
77
|
+
if len(call.args) >= 1:
|
|
78
|
+
call_data["x"] = to_array(call.args[0])
|
|
79
|
+
|
|
80
|
+
elif call.function == "errorbar":
|
|
81
|
+
if len(call.args) >= 1:
|
|
82
|
+
call_data["x"] = to_array(call.args[0])
|
|
83
|
+
if len(call.args) >= 2:
|
|
84
|
+
call_data["y"] = to_array(call.args[1])
|
|
85
|
+
|
|
86
|
+
# Extract relevant kwargs
|
|
87
|
+
for key in ("c", "s", "yerr", "xerr", "weights", "bins"):
|
|
88
|
+
if key in call.kwargs:
|
|
89
|
+
val = call.kwargs[key]
|
|
90
|
+
if (
|
|
91
|
+
isinstance(val, (list, tuple))
|
|
92
|
+
or hasattr(val, "__iter__")
|
|
93
|
+
and not isinstance(val, str)
|
|
94
|
+
):
|
|
95
|
+
call_data[key] = to_array(val)
|
|
96
|
+
else:
|
|
97
|
+
call_data[key] = val
|
|
98
|
+
|
|
99
|
+
return call_data
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"DECORATION_FUNCS",
|
|
104
|
+
"to_array",
|
|
105
|
+
"extract_call_data",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# EOF
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Notebook utilities for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides SVG rendering for Jupyter notebooks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"enable_svg",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Notebook display format flag (set once per session)
|
|
13
|
+
_notebook_format_set = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _enable_notebook_svg():
|
|
17
|
+
"""Enable SVG format for Jupyter notebook display.
|
|
18
|
+
|
|
19
|
+
This provides crisp vector graphics at any zoom level.
|
|
20
|
+
Called automatically when load_style() or subplots() is used.
|
|
21
|
+
"""
|
|
22
|
+
global _notebook_format_set
|
|
23
|
+
if _notebook_format_set:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Method 1: matplotlib_inline (IPython 7.0+, JupyterLab)
|
|
28
|
+
from matplotlib_inline.backend_inline import set_matplotlib_formats
|
|
29
|
+
|
|
30
|
+
set_matplotlib_formats("svg")
|
|
31
|
+
_notebook_format_set = True
|
|
32
|
+
except (ImportError, Exception):
|
|
33
|
+
try:
|
|
34
|
+
# Method 2: IPython config (older IPython)
|
|
35
|
+
from IPython import get_ipython
|
|
36
|
+
|
|
37
|
+
ipython = get_ipython()
|
|
38
|
+
if ipython is not None and hasattr(ipython, "kernel"):
|
|
39
|
+
# Only run in actual Jupyter kernel, not IPython console
|
|
40
|
+
ipython.run_line_magic(
|
|
41
|
+
"config", "InlineBackend.figure_formats = ['svg']"
|
|
42
|
+
)
|
|
43
|
+
_notebook_format_set = True
|
|
44
|
+
except Exception:
|
|
45
|
+
pass # Not in Jupyter environment or method not available
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def enable_svg():
|
|
49
|
+
"""Manually enable SVG format for Jupyter notebook display.
|
|
50
|
+
|
|
51
|
+
Call this if figures appear pixelated in notebooks.
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> import figrecipe as fr
|
|
56
|
+
>>> fr.enable_svg() # Enable SVG rendering
|
|
57
|
+
>>> fig, ax = fr.subplots() # Now renders as crisp SVG
|
|
58
|
+
"""
|
|
59
|
+
global _notebook_format_set
|
|
60
|
+
_notebook_format_set = False # Force re-application
|
|
61
|
+
_enable_notebook_svg()
|
figrecipe/_api/_panel.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Panel label helper for the public API."""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_panel_label_fontsize(explicit_fontsize: Optional[float] = None) -> float:
|
|
9
|
+
"""Get fontsize for panel labels from style or default."""
|
|
10
|
+
if explicit_fontsize is not None:
|
|
11
|
+
return explicit_fontsize
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
15
|
+
|
|
16
|
+
if _STYLE_CACHE is not None:
|
|
17
|
+
return getattr(getattr(_STYLE_CACHE, "fonts", None), "panel_label_pt", 10)
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
return 10
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def calculate_panel_position(
|
|
24
|
+
loc: str,
|
|
25
|
+
offset: Tuple[float, float],
|
|
26
|
+
) -> Tuple[float, float]:
|
|
27
|
+
"""Calculate x, y position based on location and offset."""
|
|
28
|
+
if loc == "upper left":
|
|
29
|
+
x, y = offset
|
|
30
|
+
elif loc == "upper right":
|
|
31
|
+
x, y = 1.0 + abs(offset[0]), offset[1]
|
|
32
|
+
elif loc == "lower left":
|
|
33
|
+
x, y = offset[0], -abs(offset[1]) + 1.0
|
|
34
|
+
elif loc == "lower right":
|
|
35
|
+
x, y = 1.0 + abs(offset[0]), -abs(offset[1]) + 1.0
|
|
36
|
+
else:
|
|
37
|
+
x, y = offset
|
|
38
|
+
return x, y
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def panel_label(
|
|
42
|
+
ax,
|
|
43
|
+
label: str,
|
|
44
|
+
loc: str = "upper left",
|
|
45
|
+
fontsize: Optional[float] = None,
|
|
46
|
+
fontweight: str = "bold",
|
|
47
|
+
offset: Tuple[float, float] = (-0.1, 1.05),
|
|
48
|
+
**kwargs,
|
|
49
|
+
):
|
|
50
|
+
"""Add a panel label (A, B, C, ...) to an axes.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
ax : Axes or RecordingAxes
|
|
55
|
+
The axes to label.
|
|
56
|
+
label : str
|
|
57
|
+
The label text (e.g., 'A', 'B', 'a)', '(1)').
|
|
58
|
+
loc : str, optional
|
|
59
|
+
Label location: 'upper left', 'upper right', etc.
|
|
60
|
+
fontsize : float, optional
|
|
61
|
+
Font size in points.
|
|
62
|
+
fontweight : str, optional
|
|
63
|
+
Font weight (default: 'bold').
|
|
64
|
+
offset : tuple of float, optional
|
|
65
|
+
(x, y) offset in axes coordinates.
|
|
66
|
+
**kwargs
|
|
67
|
+
Additional arguments passed to ax.text().
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
Text
|
|
72
|
+
The matplotlib Text object.
|
|
73
|
+
"""
|
|
74
|
+
import matplotlib.pyplot as mpl_plt
|
|
75
|
+
|
|
76
|
+
fontsize = get_panel_label_fontsize(fontsize)
|
|
77
|
+
x, y = calculate_panel_position(loc, offset)
|
|
78
|
+
|
|
79
|
+
default_color = mpl_plt.rcParams.get("text.color", "black")
|
|
80
|
+
|
|
81
|
+
text_kwargs = {
|
|
82
|
+
"fontsize": fontsize,
|
|
83
|
+
"fontweight": fontweight,
|
|
84
|
+
"color": default_color,
|
|
85
|
+
"transform": "axes",
|
|
86
|
+
"va": "bottom",
|
|
87
|
+
"ha": "right" if "right" in loc else "left",
|
|
88
|
+
}
|
|
89
|
+
text_kwargs.update(kwargs)
|
|
90
|
+
|
|
91
|
+
mpl_ax = ax._ax if hasattr(ax, "_ax") else ax
|
|
92
|
+
|
|
93
|
+
render_kwargs = text_kwargs.copy()
|
|
94
|
+
render_kwargs["transform"] = mpl_ax.transAxes
|
|
95
|
+
|
|
96
|
+
if hasattr(ax, "_recorder") and hasattr(ax, "_position"):
|
|
97
|
+
ax._recorder.record_call(
|
|
98
|
+
ax_position=ax._position,
|
|
99
|
+
method_name="text",
|
|
100
|
+
args=(x, y, label),
|
|
101
|
+
kwargs=text_kwargs,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return mpl_ax.text(x, y, label, **render_kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
"get_panel_label_fontsize",
|
|
109
|
+
"calculate_panel_position",
|
|
110
|
+
"panel_label",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# EOF
|
figrecipe/_api/_save.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Save function helpers for the public API."""
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
|
|
11
|
+
# Image extensions supported for saving
|
|
12
|
+
IMAGE_EXTENSIONS = {
|
|
13
|
+
".png",
|
|
14
|
+
".pdf",
|
|
15
|
+
".svg",
|
|
16
|
+
".jpg",
|
|
17
|
+
".jpeg",
|
|
18
|
+
".eps",
|
|
19
|
+
".tiff",
|
|
20
|
+
".tif",
|
|
21
|
+
}
|
|
22
|
+
YAML_EXTENSIONS = {".yaml", ".yml"}
|
|
23
|
+
BUNDLE_RECIPE_NAME = "recipe.yaml"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_save_paths(
|
|
27
|
+
path: Path,
|
|
28
|
+
image_format: Optional[str] = None,
|
|
29
|
+
) -> Tuple[Path, Path, str]:
|
|
30
|
+
"""Resolve image and YAML paths from the provided path.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
path : Path
|
|
35
|
+
User-provided output path.
|
|
36
|
+
image_format : str, optional
|
|
37
|
+
Explicit image format when path is YAML.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
tuple
|
|
42
|
+
(image_path, yaml_path, img_format)
|
|
43
|
+
"""
|
|
44
|
+
suffix_lower = path.suffix.lower()
|
|
45
|
+
|
|
46
|
+
if suffix_lower in IMAGE_EXTENSIONS:
|
|
47
|
+
# User provided image path
|
|
48
|
+
image_path = path
|
|
49
|
+
yaml_path = path.with_suffix(".yaml")
|
|
50
|
+
img_format = suffix_lower[1:] # Remove leading dot
|
|
51
|
+
elif suffix_lower in YAML_EXTENSIONS:
|
|
52
|
+
# User provided YAML path
|
|
53
|
+
yaml_path = path
|
|
54
|
+
img_format = _get_default_image_format(image_format)
|
|
55
|
+
image_path = path.with_suffix(f".{img_format}")
|
|
56
|
+
else:
|
|
57
|
+
# Unknown extension - treat as base name, add both extensions
|
|
58
|
+
yaml_path = path.with_suffix(".yaml")
|
|
59
|
+
img_format = _get_default_image_format(image_format)
|
|
60
|
+
image_path = path.with_suffix(f".{img_format}")
|
|
61
|
+
|
|
62
|
+
return image_path, yaml_path, img_format
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_default_image_format(explicit_format: Optional[str] = None) -> str:
|
|
66
|
+
"""Get default image format from style or fallback to png."""
|
|
67
|
+
if explicit_format is not None:
|
|
68
|
+
return explicit_format.lower().lstrip(".")
|
|
69
|
+
|
|
70
|
+
# Check global style for preferred format
|
|
71
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
72
|
+
|
|
73
|
+
if _STYLE_CACHE is not None:
|
|
74
|
+
try:
|
|
75
|
+
return _STYLE_CACHE.output.format.lower()
|
|
76
|
+
except (KeyError, AttributeError):
|
|
77
|
+
pass
|
|
78
|
+
return "png"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_save_dpi(explicit_dpi: Optional[int] = None) -> int:
|
|
82
|
+
"""Get DPI for saving, using style default if not specified."""
|
|
83
|
+
if explicit_dpi is not None:
|
|
84
|
+
return explicit_dpi
|
|
85
|
+
|
|
86
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
87
|
+
|
|
88
|
+
if _STYLE_CACHE is not None:
|
|
89
|
+
try:
|
|
90
|
+
return _STYLE_CACHE.output.dpi
|
|
91
|
+
except (KeyError, AttributeError):
|
|
92
|
+
pass
|
|
93
|
+
return 300
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_save_transparency() -> bool:
|
|
97
|
+
"""Get transparency setting from style."""
|
|
98
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
99
|
+
|
|
100
|
+
if _STYLE_CACHE is not None:
|
|
101
|
+
try:
|
|
102
|
+
return _STYLE_CACHE.output.transparent
|
|
103
|
+
except (KeyError, AttributeError):
|
|
104
|
+
pass
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_bundle_path(path: Path) -> bool:
|
|
109
|
+
"""Check if path represents a bundle (directory or ZIP)."""
|
|
110
|
+
suffix = path.suffix.lower()
|
|
111
|
+
# ZIP file
|
|
112
|
+
if suffix == ".zip":
|
|
113
|
+
return True
|
|
114
|
+
# Existing directory
|
|
115
|
+
if path.is_dir():
|
|
116
|
+
return True
|
|
117
|
+
# Path ending with / (explicit directory)
|
|
118
|
+
if str(path).endswith("/"):
|
|
119
|
+
return True
|
|
120
|
+
# No extension and doesn't look like a file
|
|
121
|
+
if not suffix and not path.exists():
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _save_as_bundle(
|
|
127
|
+
fig,
|
|
128
|
+
path: Path,
|
|
129
|
+
include_data: bool,
|
|
130
|
+
data_format: str,
|
|
131
|
+
dpi: int,
|
|
132
|
+
transparent: bool,
|
|
133
|
+
image_format: str,
|
|
134
|
+
verbose: bool,
|
|
135
|
+
) -> Tuple[Path, Path]:
|
|
136
|
+
"""Save figure as a bundle (directory or ZIP)."""
|
|
137
|
+
suffix = path.suffix.lower()
|
|
138
|
+
is_zip = suffix == ".zip"
|
|
139
|
+
|
|
140
|
+
# Create temporary directory for bundle contents
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
142
|
+
tmpdir = Path(tmpdir)
|
|
143
|
+
|
|
144
|
+
# Determine image format
|
|
145
|
+
img_format = image_format or _get_default_image_format()
|
|
146
|
+
image_name = f"figure.{img_format}"
|
|
147
|
+
|
|
148
|
+
# Save image
|
|
149
|
+
image_path = tmpdir / image_name
|
|
150
|
+
fig.fig.savefig(
|
|
151
|
+
image_path, dpi=dpi, bbox_inches="tight", transparent=transparent
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Save recipe
|
|
155
|
+
yaml_path = tmpdir / BUNDLE_RECIPE_NAME
|
|
156
|
+
fig.save_recipe(yaml_path, include_data=include_data, data_format=data_format)
|
|
157
|
+
|
|
158
|
+
if is_zip:
|
|
159
|
+
# Create ZIP bundle
|
|
160
|
+
zip_path = path.with_suffix(".zip")
|
|
161
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
162
|
+
zf.write(yaml_path, BUNDLE_RECIPE_NAME)
|
|
163
|
+
zf.write(image_path, image_name)
|
|
164
|
+
if verbose:
|
|
165
|
+
print(f"Saved: {zip_path} (ZIP bundle)")
|
|
166
|
+
return zip_path, zip_path
|
|
167
|
+
else:
|
|
168
|
+
# Create directory bundle
|
|
169
|
+
bundle_dir = Path(str(path).rstrip("/"))
|
|
170
|
+
bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
shutil.copy2(yaml_path, bundle_dir / BUNDLE_RECIPE_NAME)
|
|
172
|
+
shutil.copy2(image_path, bundle_dir / image_name)
|
|
173
|
+
if verbose:
|
|
174
|
+
print(f"Saved: {bundle_dir}/ (directory bundle)")
|
|
175
|
+
return bundle_dir, bundle_dir / BUNDLE_RECIPE_NAME
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def save_figure(
|
|
179
|
+
fig,
|
|
180
|
+
path,
|
|
181
|
+
include_data: bool = True,
|
|
182
|
+
data_format: str = "csv",
|
|
183
|
+
validate: bool = True,
|
|
184
|
+
validate_mse_threshold: float = 100.0,
|
|
185
|
+
validate_error_level: str = "error",
|
|
186
|
+
verbose: bool = True,
|
|
187
|
+
dpi: Optional[int] = None,
|
|
188
|
+
image_format: Optional[str] = None,
|
|
189
|
+
):
|
|
190
|
+
"""Core save implementation.
|
|
191
|
+
|
|
192
|
+
Supports multiple output formats:
|
|
193
|
+
- Image file (.png, .pdf, etc.): Saves image + .yaml recipe
|
|
194
|
+
- YAML file (.yaml): Saves recipe + image
|
|
195
|
+
- Directory (path/ or no extension): Saves as bundle directory
|
|
196
|
+
- ZIP file (.zip): Saves as ZIP bundle
|
|
197
|
+
"""
|
|
198
|
+
from .._wrappers import RecordingFigure
|
|
199
|
+
|
|
200
|
+
path = Path(path)
|
|
201
|
+
|
|
202
|
+
if not isinstance(fig, RecordingFigure):
|
|
203
|
+
raise TypeError(
|
|
204
|
+
"Expected RecordingFigure. Use fr.subplots() to create "
|
|
205
|
+
"a recording-enabled figure."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Get DPI and transparency from style if not specified
|
|
209
|
+
dpi = get_save_dpi(dpi)
|
|
210
|
+
transparent = get_save_transparency()
|
|
211
|
+
|
|
212
|
+
# Finalize tick configuration and special plot types for all axes
|
|
213
|
+
from ..styles._style_applier import finalize_special_plots, finalize_ticks
|
|
214
|
+
|
|
215
|
+
# Get style for special plot finalization
|
|
216
|
+
style_dict = {}
|
|
217
|
+
if hasattr(fig, "style") and fig.style:
|
|
218
|
+
from ..styles import get_style
|
|
219
|
+
|
|
220
|
+
style_dict = get_style(fig.style)
|
|
221
|
+
|
|
222
|
+
for ax in fig.fig.get_axes():
|
|
223
|
+
finalize_ticks(ax)
|
|
224
|
+
finalize_special_plots(ax, style_dict)
|
|
225
|
+
|
|
226
|
+
# Check if saving as bundle
|
|
227
|
+
if _is_bundle_path(path):
|
|
228
|
+
bundle_path, yaml_path = _save_as_bundle(
|
|
229
|
+
fig,
|
|
230
|
+
path,
|
|
231
|
+
include_data,
|
|
232
|
+
data_format,
|
|
233
|
+
dpi,
|
|
234
|
+
transparent,
|
|
235
|
+
image_format or _get_default_image_format(),
|
|
236
|
+
verbose,
|
|
237
|
+
)
|
|
238
|
+
# No validation for bundles (yet)
|
|
239
|
+
return bundle_path, yaml_path, None
|
|
240
|
+
|
|
241
|
+
# Resolve paths for standard save
|
|
242
|
+
image_path, yaml_path, _ = resolve_save_paths(path, image_format)
|
|
243
|
+
|
|
244
|
+
# Save the image
|
|
245
|
+
fig.fig.savefig(image_path, dpi=dpi, bbox_inches="tight", transparent=transparent)
|
|
246
|
+
|
|
247
|
+
# Save the recipe
|
|
248
|
+
saved_yaml = fig.save_recipe(
|
|
249
|
+
yaml_path, include_data=include_data, data_format=data_format
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Validate if requested
|
|
253
|
+
if validate:
|
|
254
|
+
from .._validator import validate_on_save
|
|
255
|
+
|
|
256
|
+
result = validate_on_save(fig, saved_yaml, mse_threshold=validate_mse_threshold)
|
|
257
|
+
status = "PASSED" if result.valid else "FAILED"
|
|
258
|
+
if verbose:
|
|
259
|
+
print(
|
|
260
|
+
f"Saved: {image_path} + {yaml_path} (Reproducible Validation: {status})"
|
|
261
|
+
)
|
|
262
|
+
if not result.valid:
|
|
263
|
+
msg = f"Reproducibility validation failed (MSE={result.mse:.1f}): {result.message}"
|
|
264
|
+
if validate_error_level == "error":
|
|
265
|
+
raise ValueError(msg)
|
|
266
|
+
elif validate_error_level == "warning":
|
|
267
|
+
import warnings
|
|
268
|
+
|
|
269
|
+
warnings.warn(msg, UserWarning)
|
|
270
|
+
# "debug" level: silent, just return the result
|
|
271
|
+
return image_path, yaml_path, result
|
|
272
|
+
|
|
273
|
+
if verbose:
|
|
274
|
+
print(f"Saved: {image_path} + {yaml_path}")
|
|
275
|
+
return image_path, yaml_path, None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
__all__ = [
|
|
279
|
+
"IMAGE_EXTENSIONS",
|
|
280
|
+
"YAML_EXTENSIONS",
|
|
281
|
+
"resolve_save_paths",
|
|
282
|
+
"get_save_dpi",
|
|
283
|
+
"get_save_transparency",
|
|
284
|
+
"save_figure",
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
# EOF
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Seaborn proxy for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides lazy seaborn integration via ps.sns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"sns",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Lazy import for seaborn to avoid hard dependency
|
|
13
|
+
_sns_recorder = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_sns():
|
|
17
|
+
"""Get the seaborn recorder (lazy initialization)."""
|
|
18
|
+
global _sns_recorder
|
|
19
|
+
if _sns_recorder is None:
|
|
20
|
+
from .._seaborn import get_seaborn_recorder
|
|
21
|
+
|
|
22
|
+
_sns_recorder = get_seaborn_recorder()
|
|
23
|
+
return _sns_recorder
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _SeabornProxy:
|
|
27
|
+
"""Proxy object for seaborn access via ps.sns."""
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str):
|
|
30
|
+
return getattr(_get_sns(), name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Create seaborn proxy
|
|
34
|
+
sns = _SeabornProxy()
|