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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Style management API for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides style loading, unloading, and application functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"load_style",
|
|
10
|
+
"unload_style",
|
|
11
|
+
"list_presets",
|
|
12
|
+
"apply_style",
|
|
13
|
+
"STYLE",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_style(style="SCITEX", dark=False):
|
|
18
|
+
"""Load style configuration and apply it globally.
|
|
19
|
+
|
|
20
|
+
After calling this function, subsequent `subplots()` calls will
|
|
21
|
+
automatically use the loaded style (fonts, colors, theme, etc.).
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
style : str, Path, bool, or None
|
|
26
|
+
One of:
|
|
27
|
+
- "SCITEX" / "FIGRECIPE": Scientific publication style (default)
|
|
28
|
+
- "MATPLOTLIB": Vanilla matplotlib defaults
|
|
29
|
+
- Path to custom YAML file: "/path/to/my_style.yaml"
|
|
30
|
+
- None or False: Unload style (reset to matplotlib defaults)
|
|
31
|
+
dark : bool, optional
|
|
32
|
+
If True, apply dark theme transformation (default: False).
|
|
33
|
+
Equivalent to appending "_DARK" to preset name.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
DotDict or None
|
|
38
|
+
Style configuration with dot-notation access.
|
|
39
|
+
Returns None if style is unloaded.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> import figrecipe as fr
|
|
44
|
+
|
|
45
|
+
>>> # Load scientific style (default)
|
|
46
|
+
>>> fr.load_style()
|
|
47
|
+
>>> fr.load_style("SCITEX") # explicit
|
|
48
|
+
|
|
49
|
+
>>> # Load dark theme
|
|
50
|
+
>>> fr.load_style("SCITEX_DARK")
|
|
51
|
+
>>> fr.load_style("SCITEX", dark=True) # equivalent
|
|
52
|
+
|
|
53
|
+
>>> # Reset to vanilla matplotlib
|
|
54
|
+
>>> fr.load_style(None) # unload
|
|
55
|
+
>>> fr.load_style(False) # unload
|
|
56
|
+
>>> fr.load_style("MATPLOTLIB") # explicit vanilla
|
|
57
|
+
|
|
58
|
+
>>> # Access style values
|
|
59
|
+
>>> style = fr.load_style("SCITEX")
|
|
60
|
+
>>> style.axes.width_mm
|
|
61
|
+
40
|
|
62
|
+
"""
|
|
63
|
+
from ..styles import load_style as _load_style
|
|
64
|
+
|
|
65
|
+
return _load_style(style, dark=dark)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def unload_style():
|
|
69
|
+
"""Unload the current style and reset to matplotlib defaults.
|
|
70
|
+
|
|
71
|
+
After calling this, subsequent `subplots()` calls will use vanilla
|
|
72
|
+
matplotlib behavior without FigRecipe styling.
|
|
73
|
+
|
|
74
|
+
Examples
|
|
75
|
+
--------
|
|
76
|
+
>>> import figrecipe as fr
|
|
77
|
+
>>> fr.load_style("SCITEX") # Apply scientific style
|
|
78
|
+
>>> fig, ax = fr.subplots() # Styled
|
|
79
|
+
>>> fr.unload_style() # Reset to matplotlib defaults
|
|
80
|
+
>>> fig, ax = fr.subplots() # Vanilla matplotlib
|
|
81
|
+
"""
|
|
82
|
+
from ..styles import unload_style as _unload_style
|
|
83
|
+
|
|
84
|
+
_unload_style()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def list_presets():
|
|
88
|
+
"""List available style presets.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
list of str
|
|
93
|
+
Names of available presets.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
>>> import figrecipe as ps
|
|
98
|
+
>>> ps.list_presets()
|
|
99
|
+
['MINIMAL', 'PRESENTATION', 'SCIENTIFIC']
|
|
100
|
+
"""
|
|
101
|
+
from ..styles import list_presets as _list_presets
|
|
102
|
+
|
|
103
|
+
return _list_presets()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def apply_style(ax, style=None):
|
|
107
|
+
"""Apply mm-based styling to an axes.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
ax : matplotlib.axes.Axes
|
|
112
|
+
Target axes to apply styling to.
|
|
113
|
+
style : dict or DotDict, optional
|
|
114
|
+
Style configuration. If None, uses default FIGRECIPE_STYLE.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
float
|
|
119
|
+
Trace line width in points.
|
|
120
|
+
|
|
121
|
+
Examples
|
|
122
|
+
--------
|
|
123
|
+
>>> import figrecipe as ps
|
|
124
|
+
>>> import matplotlib.pyplot as plt
|
|
125
|
+
>>> fig, ax = plt.subplots()
|
|
126
|
+
>>> trace_lw = ps.apply_style(ax)
|
|
127
|
+
>>> ax.plot(x, y, lw=trace_lw)
|
|
128
|
+
"""
|
|
129
|
+
from ..styles import apply_style_mm, get_style, to_subplots_kwargs
|
|
130
|
+
|
|
131
|
+
if style is None:
|
|
132
|
+
style = to_subplots_kwargs(get_style())
|
|
133
|
+
elif hasattr(style, "to_subplots_kwargs"):
|
|
134
|
+
style = style.to_subplots_kwargs()
|
|
135
|
+
return apply_style_mm(ax, style)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _StyleProxy:
|
|
139
|
+
"""Proxy object for lazy style loading."""
|
|
140
|
+
|
|
141
|
+
def __getattr__(self, name):
|
|
142
|
+
from ..styles import STYLE
|
|
143
|
+
|
|
144
|
+
return getattr(STYLE, name)
|
|
145
|
+
|
|
146
|
+
def to_subplots_kwargs(self):
|
|
147
|
+
from ..styles import to_subplots_kwargs
|
|
148
|
+
|
|
149
|
+
return to_subplots_kwargs()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Create style proxy
|
|
153
|
+
STYLE = _StyleProxy()
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Subplots helper implementation for the public API."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from .._utils._units import mm_to_inch
|
|
10
|
+
from .._wrappers import RecordingAxes, RecordingFigure
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_mm_value(explicit, global_style, style_path, default):
|
|
14
|
+
"""Get mm value with priority: explicit > global style > default."""
|
|
15
|
+
if explicit is not None:
|
|
16
|
+
return explicit
|
|
17
|
+
if global_style is not None:
|
|
18
|
+
try:
|
|
19
|
+
val = global_style
|
|
20
|
+
for key in style_path:
|
|
21
|
+
val = val.get(key) if isinstance(val, dict) else getattr(val, key, None)
|
|
22
|
+
if val is None:
|
|
23
|
+
break
|
|
24
|
+
if val is not None:
|
|
25
|
+
return val
|
|
26
|
+
except (KeyError, AttributeError):
|
|
27
|
+
pass
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _check_mm_layout(
|
|
32
|
+
axes_width_mm,
|
|
33
|
+
axes_height_mm,
|
|
34
|
+
margin_left_mm,
|
|
35
|
+
margin_right_mm,
|
|
36
|
+
margin_bottom_mm,
|
|
37
|
+
margin_top_mm,
|
|
38
|
+
space_w_mm,
|
|
39
|
+
space_h_mm,
|
|
40
|
+
global_style,
|
|
41
|
+
):
|
|
42
|
+
"""Check if mm-based layout is requested."""
|
|
43
|
+
has_explicit_mm = any(
|
|
44
|
+
[
|
|
45
|
+
axes_width_mm is not None,
|
|
46
|
+
axes_height_mm is not None,
|
|
47
|
+
margin_left_mm is not None,
|
|
48
|
+
margin_right_mm is not None,
|
|
49
|
+
margin_bottom_mm is not None,
|
|
50
|
+
margin_top_mm is not None,
|
|
51
|
+
space_w_mm is not None,
|
|
52
|
+
space_h_mm is not None,
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
has_style_mm = False
|
|
57
|
+
if global_style is not None:
|
|
58
|
+
try:
|
|
59
|
+
has_style_mm = (
|
|
60
|
+
global_style.get("axes", {}).get("width_mm") is not None
|
|
61
|
+
or getattr(getattr(global_style, "axes", None), "width_mm", None)
|
|
62
|
+
is not None
|
|
63
|
+
)
|
|
64
|
+
except (KeyError, AttributeError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return has_explicit_mm or has_style_mm
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _calculate_mm_layout(
|
|
71
|
+
nrows: int,
|
|
72
|
+
ncols: int,
|
|
73
|
+
axes_width_mm: Optional[float],
|
|
74
|
+
axes_height_mm: Optional[float],
|
|
75
|
+
margin_left_mm: Optional[float],
|
|
76
|
+
margin_right_mm: Optional[float],
|
|
77
|
+
margin_bottom_mm: Optional[float],
|
|
78
|
+
margin_top_mm: Optional[float],
|
|
79
|
+
space_w_mm: Optional[float],
|
|
80
|
+
space_h_mm: Optional[float],
|
|
81
|
+
global_style,
|
|
82
|
+
kwargs: Dict[str, Any],
|
|
83
|
+
) -> Tuple[Optional[Dict[str, float]], Dict[str, Any]]:
|
|
84
|
+
"""Calculate mm-based layout and update kwargs with figsize."""
|
|
85
|
+
aw = _get_mm_value(axes_width_mm, global_style, ["axes", "width_mm"], 40)
|
|
86
|
+
ah = _get_mm_value(axes_height_mm, global_style, ["axes", "height_mm"], 28)
|
|
87
|
+
ml = _get_mm_value(margin_left_mm, global_style, ["margins", "left_mm"], 15)
|
|
88
|
+
mr = _get_mm_value(margin_right_mm, global_style, ["margins", "right_mm"], 5)
|
|
89
|
+
mb = _get_mm_value(margin_bottom_mm, global_style, ["margins", "bottom_mm"], 12)
|
|
90
|
+
mt = _get_mm_value(margin_top_mm, global_style, ["margins", "top_mm"], 8)
|
|
91
|
+
sw = _get_mm_value(space_w_mm, global_style, ["spacing", "horizontal_mm"], 8)
|
|
92
|
+
sh = _get_mm_value(space_h_mm, global_style, ["spacing", "vertical_mm"], 10)
|
|
93
|
+
|
|
94
|
+
# Calculate total figure size
|
|
95
|
+
total_width_mm = ml + (ncols * aw) + ((ncols - 1) * sw) + mr
|
|
96
|
+
total_height_mm = mb + (nrows * ah) + ((nrows - 1) * sh) + mt
|
|
97
|
+
|
|
98
|
+
# Convert to inches and set figsize
|
|
99
|
+
kwargs["figsize"] = (mm_to_inch(total_width_mm), mm_to_inch(total_height_mm))
|
|
100
|
+
|
|
101
|
+
mm_layout = {
|
|
102
|
+
"axes_width_mm": aw,
|
|
103
|
+
"axes_height_mm": ah,
|
|
104
|
+
"margin_left_mm": ml,
|
|
105
|
+
"margin_right_mm": mr,
|
|
106
|
+
"margin_bottom_mm": mb,
|
|
107
|
+
"margin_top_mm": mt,
|
|
108
|
+
"space_w_mm": sw,
|
|
109
|
+
"space_h_mm": sh,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return mm_layout, kwargs
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _apply_mm_layout_to_figure(
|
|
116
|
+
fig: RecordingFigure,
|
|
117
|
+
mm_layout: Dict[str, float],
|
|
118
|
+
nrows: int,
|
|
119
|
+
ncols: int,
|
|
120
|
+
):
|
|
121
|
+
"""Apply mm-based layout adjustments to figure."""
|
|
122
|
+
ml = mm_layout["margin_left_mm"]
|
|
123
|
+
mr = mm_layout["margin_right_mm"]
|
|
124
|
+
mb = mm_layout["margin_bottom_mm"]
|
|
125
|
+
mt = mm_layout["margin_top_mm"]
|
|
126
|
+
aw = mm_layout["axes_width_mm"]
|
|
127
|
+
ah = mm_layout["axes_height_mm"]
|
|
128
|
+
sw = mm_layout["space_w_mm"]
|
|
129
|
+
sh = mm_layout["space_h_mm"]
|
|
130
|
+
|
|
131
|
+
total_width_mm = ml + (ncols * aw) + ((ncols - 1) * sw) + mr
|
|
132
|
+
total_height_mm = mb + (nrows * ah) + ((nrows - 1) * sh) + mt
|
|
133
|
+
|
|
134
|
+
# Calculate relative positions (0-1 range)
|
|
135
|
+
left = ml / total_width_mm
|
|
136
|
+
right = 1 - (mr / total_width_mm)
|
|
137
|
+
bottom = mb / total_height_mm
|
|
138
|
+
top = 1 - (mt / total_height_mm)
|
|
139
|
+
|
|
140
|
+
# Calculate spacing as fraction of figure size
|
|
141
|
+
wspace = sw / aw if ncols > 1 else 0
|
|
142
|
+
hspace = sh / ah if nrows > 1 else 0
|
|
143
|
+
|
|
144
|
+
fig.fig.subplots_adjust(
|
|
145
|
+
left=left,
|
|
146
|
+
right=right,
|
|
147
|
+
bottom=bottom,
|
|
148
|
+
top=top,
|
|
149
|
+
wspace=wspace,
|
|
150
|
+
hspace=hspace,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Record layout in figure record for reproduction
|
|
154
|
+
fig.record.layout = {
|
|
155
|
+
"left": left,
|
|
156
|
+
"right": right,
|
|
157
|
+
"bottom": bottom,
|
|
158
|
+
"top": top,
|
|
159
|
+
"wspace": wspace,
|
|
160
|
+
"hspace": hspace,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _apply_style_to_axes(
|
|
165
|
+
fig: RecordingFigure,
|
|
166
|
+
axes: Union[RecordingAxes, NDArray],
|
|
167
|
+
nrows: int,
|
|
168
|
+
ncols: int,
|
|
169
|
+
style: Optional[Dict[str, Any]],
|
|
170
|
+
apply_style_mm: bool,
|
|
171
|
+
global_style,
|
|
172
|
+
) -> Optional[Dict[str, Any]]:
|
|
173
|
+
"""Apply style to axes and return style dict if applied."""
|
|
174
|
+
import numpy as np
|
|
175
|
+
|
|
176
|
+
from ..styles import apply_style_mm as _apply_style
|
|
177
|
+
from ..styles import to_subplots_kwargs
|
|
178
|
+
|
|
179
|
+
style_dict = None
|
|
180
|
+
should_apply_style = False
|
|
181
|
+
|
|
182
|
+
if style is not None:
|
|
183
|
+
should_apply_style = True
|
|
184
|
+
style_dict = (
|
|
185
|
+
style.to_subplots_kwargs()
|
|
186
|
+
if hasattr(style, "to_subplots_kwargs")
|
|
187
|
+
else style
|
|
188
|
+
)
|
|
189
|
+
elif apply_style_mm and global_style is not None:
|
|
190
|
+
style_dict = to_subplots_kwargs(global_style)
|
|
191
|
+
if style_dict and style_dict.get("axes_thickness_mm") is not None:
|
|
192
|
+
should_apply_style = True
|
|
193
|
+
|
|
194
|
+
if should_apply_style and style_dict:
|
|
195
|
+
if nrows == 1 and ncols == 1:
|
|
196
|
+
_apply_style(axes._ax, style_dict)
|
|
197
|
+
else:
|
|
198
|
+
axes_array = np.array(axes)
|
|
199
|
+
for ax in axes_array.flat:
|
|
200
|
+
_apply_style(ax._ax if hasattr(ax, "_ax") else ax, style_dict)
|
|
201
|
+
|
|
202
|
+
fig.record.style = style_dict
|
|
203
|
+
|
|
204
|
+
return style_dict
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def create_subplots(
|
|
208
|
+
nrows: int = 1,
|
|
209
|
+
ncols: int = 1,
|
|
210
|
+
axes_width_mm: Optional[float] = None,
|
|
211
|
+
axes_height_mm: Optional[float] = None,
|
|
212
|
+
margin_left_mm: Optional[float] = None,
|
|
213
|
+
margin_right_mm: Optional[float] = None,
|
|
214
|
+
margin_bottom_mm: Optional[float] = None,
|
|
215
|
+
margin_top_mm: Optional[float] = None,
|
|
216
|
+
space_w_mm: Optional[float] = None,
|
|
217
|
+
space_h_mm: Optional[float] = None,
|
|
218
|
+
style: Optional[Dict[str, Any]] = None,
|
|
219
|
+
apply_style_mm: bool = True,
|
|
220
|
+
panel_labels: Optional[bool] = None,
|
|
221
|
+
**kwargs,
|
|
222
|
+
) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
|
|
223
|
+
"""Core subplots implementation."""
|
|
224
|
+
from .._wrappers._figure import create_recording_subplots
|
|
225
|
+
from ..styles._style_loader import _STYLE_CACHE, to_subplots_kwargs
|
|
226
|
+
|
|
227
|
+
global_style = _STYLE_CACHE
|
|
228
|
+
|
|
229
|
+
# Check if mm-based layout is requested
|
|
230
|
+
use_mm_layout = _check_mm_layout(
|
|
231
|
+
axes_width_mm,
|
|
232
|
+
axes_height_mm,
|
|
233
|
+
margin_left_mm,
|
|
234
|
+
margin_right_mm,
|
|
235
|
+
margin_bottom_mm,
|
|
236
|
+
margin_top_mm,
|
|
237
|
+
space_w_mm,
|
|
238
|
+
space_h_mm,
|
|
239
|
+
global_style,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if use_mm_layout and "figsize" not in kwargs:
|
|
243
|
+
mm_layout, kwargs = _calculate_mm_layout(
|
|
244
|
+
nrows,
|
|
245
|
+
ncols,
|
|
246
|
+
axes_width_mm,
|
|
247
|
+
axes_height_mm,
|
|
248
|
+
margin_left_mm,
|
|
249
|
+
margin_right_mm,
|
|
250
|
+
margin_bottom_mm,
|
|
251
|
+
margin_top_mm,
|
|
252
|
+
space_w_mm,
|
|
253
|
+
space_h_mm,
|
|
254
|
+
global_style,
|
|
255
|
+
kwargs,
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
mm_layout = None
|
|
259
|
+
|
|
260
|
+
# Apply DPI from global style if not explicitly provided
|
|
261
|
+
if "dpi" not in kwargs and global_style is not None:
|
|
262
|
+
style_dpi = None
|
|
263
|
+
try:
|
|
264
|
+
if hasattr(global_style, "figure") and hasattr(global_style.figure, "dpi"):
|
|
265
|
+
style_dpi = global_style.figure.dpi
|
|
266
|
+
elif hasattr(global_style, "output") and hasattr(
|
|
267
|
+
global_style.output, "dpi"
|
|
268
|
+
):
|
|
269
|
+
style_dpi = global_style.output.dpi
|
|
270
|
+
except (KeyError, AttributeError):
|
|
271
|
+
pass
|
|
272
|
+
if style_dpi is not None:
|
|
273
|
+
kwargs["dpi"] = style_dpi
|
|
274
|
+
|
|
275
|
+
# Handle style parameter
|
|
276
|
+
if style is not None:
|
|
277
|
+
if hasattr(style, "to_subplots_kwargs"):
|
|
278
|
+
style_kwargs = style.to_subplots_kwargs()
|
|
279
|
+
for key, value in style_kwargs.items():
|
|
280
|
+
if key not in kwargs:
|
|
281
|
+
kwargs[key] = value
|
|
282
|
+
|
|
283
|
+
# Check if style specifies constrained_layout
|
|
284
|
+
style_constrained = False
|
|
285
|
+
if global_style is not None:
|
|
286
|
+
style_dict_check = to_subplots_kwargs(global_style)
|
|
287
|
+
style_constrained = style_dict_check.get("constrained_layout", False)
|
|
288
|
+
|
|
289
|
+
# Use constrained_layout if: style specifies it, or non-mm layout
|
|
290
|
+
if "constrained_layout" not in kwargs:
|
|
291
|
+
if style_constrained:
|
|
292
|
+
kwargs["constrained_layout"] = True
|
|
293
|
+
elif not use_mm_layout:
|
|
294
|
+
kwargs["constrained_layout"] = True
|
|
295
|
+
|
|
296
|
+
# Create the recording subplots
|
|
297
|
+
fig, axes = create_recording_subplots(nrows, ncols, **kwargs)
|
|
298
|
+
|
|
299
|
+
# Record constrained_layout setting for reproduction
|
|
300
|
+
fig.record.constrained_layout = kwargs.get("constrained_layout", False)
|
|
301
|
+
|
|
302
|
+
# Store mm_layout metadata on figure for serialization
|
|
303
|
+
use_constrained = kwargs.get("constrained_layout", False)
|
|
304
|
+
if mm_layout is not None and not use_constrained:
|
|
305
|
+
fig._mm_layout = mm_layout
|
|
306
|
+
_apply_mm_layout_to_figure(fig, mm_layout, nrows, ncols)
|
|
307
|
+
|
|
308
|
+
# Apply styling using helper
|
|
309
|
+
_apply_style_to_axes(fig, axes, nrows, ncols, style, apply_style_mm, global_style)
|
|
310
|
+
|
|
311
|
+
# Determine panel_labels setting
|
|
312
|
+
use_panel_labels = panel_labels
|
|
313
|
+
if use_panel_labels is None and global_style is not None:
|
|
314
|
+
behavior = global_style.get("behavior", {})
|
|
315
|
+
use_panel_labels = behavior.get("panel_labels", False)
|
|
316
|
+
|
|
317
|
+
# Add panel labels if enabled (for multi-panel figures)
|
|
318
|
+
if use_panel_labels and (nrows > 1 or ncols > 1):
|
|
319
|
+
fig.add_panel_labels()
|
|
320
|
+
|
|
321
|
+
return fig, axes
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
__all__ = [
|
|
325
|
+
"_get_mm_value",
|
|
326
|
+
"_check_mm_layout",
|
|
327
|
+
"_calculate_mm_layout",
|
|
328
|
+
"_apply_mm_layout_to_figure",
|
|
329
|
+
"_apply_style_to_axes",
|
|
330
|
+
"create_subplots",
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
# EOF
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Standalone validation implementation."""
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .._reproducer import reproduce
|
|
12
|
+
from .._utils._image_diff import compare_images
|
|
13
|
+
from .._validator import ValidationResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_recipe(
|
|
17
|
+
path: Union[str, Path],
|
|
18
|
+
mse_threshold: float = 100.0,
|
|
19
|
+
) -> ValidationResult:
|
|
20
|
+
"""Validate that a saved recipe can reproduce its original figure.
|
|
21
|
+
|
|
22
|
+
For standalone validation, we reproduce twice and compare
|
|
23
|
+
(This validates the recipe is self-consistent).
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
path : str or Path
|
|
28
|
+
Path to .yaml recipe file.
|
|
29
|
+
mse_threshold : float
|
|
30
|
+
Maximum acceptable MSE for validation to pass (default: 100).
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
ValidationResult
|
|
35
|
+
Detailed comparison results.
|
|
36
|
+
"""
|
|
37
|
+
path = Path(path)
|
|
38
|
+
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
40
|
+
tmpdir = Path(tmpdir)
|
|
41
|
+
|
|
42
|
+
# Reproduce twice
|
|
43
|
+
fig1, _ = reproduce(path)
|
|
44
|
+
img1_path = tmpdir / "render1.png"
|
|
45
|
+
fig1.savefig(img1_path, dpi=150)
|
|
46
|
+
|
|
47
|
+
fig2, _ = reproduce(path)
|
|
48
|
+
img2_path = tmpdir / "render2.png"
|
|
49
|
+
fig2.savefig(img2_path, dpi=150)
|
|
50
|
+
|
|
51
|
+
# Compare
|
|
52
|
+
diff = compare_images(img1_path, img2_path)
|
|
53
|
+
|
|
54
|
+
mse = diff["mse"]
|
|
55
|
+
if np.isnan(mse):
|
|
56
|
+
valid = False
|
|
57
|
+
message = f"Image dimensions differ: {diff['size1']} vs {diff['size2']}"
|
|
58
|
+
elif mse > mse_threshold:
|
|
59
|
+
valid = False
|
|
60
|
+
message = f"MSE ({mse:.2f}) exceeds threshold ({mse_threshold})"
|
|
61
|
+
else:
|
|
62
|
+
valid = True
|
|
63
|
+
message = "Recipe produces consistent output"
|
|
64
|
+
|
|
65
|
+
return ValidationResult(
|
|
66
|
+
valid=valid,
|
|
67
|
+
mse=mse if not np.isnan(mse) else float("inf"),
|
|
68
|
+
psnr=diff["psnr"],
|
|
69
|
+
max_diff=diff["max_diff"]
|
|
70
|
+
if not np.isnan(diff["max_diff"])
|
|
71
|
+
else float("inf"),
|
|
72
|
+
size_original=diff["size1"],
|
|
73
|
+
size_reproduced=diff["size2"],
|
|
74
|
+
same_size=diff["same_size"],
|
|
75
|
+
file_size_diff=diff["file_size2"] - diff["file_size1"],
|
|
76
|
+
message=message,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["validate_recipe"]
|
|
81
|
+
|
|
82
|
+
# EOF
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""compose command - Combine multiple figures."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("sources", nargs=-1, type=click.Path(exists=True), required=True)
|
|
13
|
+
@click.option(
|
|
14
|
+
"-o",
|
|
15
|
+
"--output",
|
|
16
|
+
type=click.Path(),
|
|
17
|
+
required=True,
|
|
18
|
+
help="Output path for composed figure.",
|
|
19
|
+
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--layout",
|
|
22
|
+
type=click.Choice(["horizontal", "vertical", "grid"]),
|
|
23
|
+
default="horizontal",
|
|
24
|
+
help="Layout arrangement (default: horizontal).",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--cols",
|
|
28
|
+
type=int,
|
|
29
|
+
help="Number of columns for grid layout.",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--dpi",
|
|
33
|
+
type=int,
|
|
34
|
+
default=300,
|
|
35
|
+
help="DPI for output (default: 300).",
|
|
36
|
+
)
|
|
37
|
+
def compose(
|
|
38
|
+
sources: Tuple[str, ...],
|
|
39
|
+
output: str,
|
|
40
|
+
layout: str,
|
|
41
|
+
cols: Optional[int],
|
|
42
|
+
dpi: int,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Compose multiple figures into one.
|
|
45
|
+
|
|
46
|
+
SOURCES are paths to .yaml recipe files or bundle directories.
|
|
47
|
+
"""
|
|
48
|
+
from .. import compose as fr_compose
|
|
49
|
+
from .. import reproduce, save
|
|
50
|
+
|
|
51
|
+
if len(sources) < 2:
|
|
52
|
+
raise click.ClickException("At least 2 source figures required.")
|
|
53
|
+
|
|
54
|
+
source_paths = [Path(s) for s in sources]
|
|
55
|
+
output_path = Path(output)
|
|
56
|
+
|
|
57
|
+
# Determine grid dimensions
|
|
58
|
+
n = len(sources)
|
|
59
|
+
if layout == "horizontal":
|
|
60
|
+
nrows, ncols = 1, n
|
|
61
|
+
elif layout == "vertical":
|
|
62
|
+
nrows, ncols = n, 1
|
|
63
|
+
else: # grid
|
|
64
|
+
if cols:
|
|
65
|
+
ncols = cols
|
|
66
|
+
nrows = (n + cols - 1) // cols
|
|
67
|
+
else:
|
|
68
|
+
# Auto-determine roughly square grid
|
|
69
|
+
import math
|
|
70
|
+
|
|
71
|
+
ncols = math.ceil(math.sqrt(n))
|
|
72
|
+
nrows = math.ceil(n / ncols)
|
|
73
|
+
|
|
74
|
+
# Reproduce and compose figures
|
|
75
|
+
try:
|
|
76
|
+
figures = []
|
|
77
|
+
for src in source_paths:
|
|
78
|
+
fig, _ = reproduce(src)
|
|
79
|
+
figures.append(fig)
|
|
80
|
+
|
|
81
|
+
composed = fr_compose(*figures, nrows=nrows, ncols=ncols)
|
|
82
|
+
save(composed, output_path, dpi=dpi)
|
|
83
|
+
|
|
84
|
+
click.echo(f"Composed {len(figures)} figures: {output_path}")
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise click.ClickException(f"Composition failed: {e}") from e
|