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,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""convert command - Convert between formats."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
13
|
+
@click.option(
|
|
14
|
+
"-f",
|
|
15
|
+
"--format",
|
|
16
|
+
"fmt",
|
|
17
|
+
type=click.Choice(["png", "pdf", "svg", "yaml"]),
|
|
18
|
+
required=True,
|
|
19
|
+
help="Target format.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"-o",
|
|
23
|
+
"--output",
|
|
24
|
+
type=click.Path(),
|
|
25
|
+
help="Output path.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--dpi",
|
|
29
|
+
type=int,
|
|
30
|
+
default=300,
|
|
31
|
+
help="DPI for raster output (default: 300).",
|
|
32
|
+
)
|
|
33
|
+
def convert(
|
|
34
|
+
source: str,
|
|
35
|
+
fmt: str,
|
|
36
|
+
output: Optional[str],
|
|
37
|
+
dpi: int,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Convert between figure formats.
|
|
40
|
+
|
|
41
|
+
SOURCE is a .yaml recipe or image file.
|
|
42
|
+
"""
|
|
43
|
+
source_path = Path(source)
|
|
44
|
+
|
|
45
|
+
# Determine output path
|
|
46
|
+
if output:
|
|
47
|
+
output_path = Path(output)
|
|
48
|
+
else:
|
|
49
|
+
output_path = source_path.with_suffix(f".{fmt}")
|
|
50
|
+
|
|
51
|
+
# Handle different source types
|
|
52
|
+
if source_path.suffix in [".yaml", ".yml"]:
|
|
53
|
+
_convert_from_recipe(source_path, output_path, fmt, dpi)
|
|
54
|
+
elif source_path.suffix in [".png", ".pdf", ".svg"]:
|
|
55
|
+
_convert_image(source_path, output_path, fmt, dpi)
|
|
56
|
+
else:
|
|
57
|
+
raise click.ClickException(f"Unsupported source format: {source_path.suffix}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _convert_from_recipe(source: Path, output: Path, fmt: str, dpi: int) -> None:
|
|
61
|
+
"""Convert from YAML recipe to image format."""
|
|
62
|
+
import matplotlib.pyplot as plt
|
|
63
|
+
|
|
64
|
+
from .. import reproduce
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
fig, _ = reproduce(source)
|
|
68
|
+
|
|
69
|
+
if fmt == "yaml":
|
|
70
|
+
# Already have YAML, just copy
|
|
71
|
+
import shutil
|
|
72
|
+
|
|
73
|
+
shutil.copy(source, output)
|
|
74
|
+
else:
|
|
75
|
+
fig.savefig(output, dpi=dpi, format=fmt)
|
|
76
|
+
|
|
77
|
+
# Close the figure (handle both regular and Recording figures)
|
|
78
|
+
try:
|
|
79
|
+
plt.close(fig)
|
|
80
|
+
except TypeError:
|
|
81
|
+
plt.close("all")
|
|
82
|
+
|
|
83
|
+
click.echo(f"Converted: {output}")
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise click.ClickException(f"Conversion failed: {e}") from e
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _convert_image(source: Path, output: Path, fmt: str, dpi: int) -> None:
|
|
90
|
+
"""Convert between image formats."""
|
|
91
|
+
if fmt == "yaml":
|
|
92
|
+
raise click.ClickException(
|
|
93
|
+
"Cannot convert image to YAML. Use a recipe file instead."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from PIL import Image
|
|
98
|
+
|
|
99
|
+
img = Image.open(source)
|
|
100
|
+
|
|
101
|
+
if fmt == "pdf":
|
|
102
|
+
img.save(output, "PDF", resolution=dpi)
|
|
103
|
+
elif fmt == "svg":
|
|
104
|
+
raise click.ClickException(
|
|
105
|
+
"Cannot convert raster image to SVG. Use a recipe file instead."
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
img.save(output, fmt.upper())
|
|
109
|
+
|
|
110
|
+
click.echo(f"Converted: {output}")
|
|
111
|
+
|
|
112
|
+
except ImportError:
|
|
113
|
+
raise click.ClickException(
|
|
114
|
+
"Image conversion requires Pillow. Install with: pip install figrecipe[imaging]"
|
|
115
|
+
) from None
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise click.ClickException(f"Conversion failed: {e}") from e
|
figrecipe/_cli/_crop.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""crop command - Crop image to content."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("image", type=click.Path(exists=True))
|
|
13
|
+
@click.option(
|
|
14
|
+
"-o",
|
|
15
|
+
"--output",
|
|
16
|
+
type=click.Path(),
|
|
17
|
+
help="Output path for cropped image.",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"--margin",
|
|
21
|
+
type=str,
|
|
22
|
+
default="1mm",
|
|
23
|
+
help="Margin around content (e.g., '2mm' or '10px'). Default: 1mm.",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--overwrite",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
help="Overwrite the input file.",
|
|
29
|
+
)
|
|
30
|
+
def crop(
|
|
31
|
+
image: str,
|
|
32
|
+
output: Optional[str],
|
|
33
|
+
margin: str,
|
|
34
|
+
overwrite: bool,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Crop an image to its content area.
|
|
37
|
+
|
|
38
|
+
IMAGE is the path to the image file (PNG, PDF, etc.).
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
from .. import crop as fr_crop
|
|
42
|
+
except ImportError:
|
|
43
|
+
raise click.ClickException(
|
|
44
|
+
"Crop requires Pillow. Install with: pip install figrecipe[imaging]"
|
|
45
|
+
) from None
|
|
46
|
+
|
|
47
|
+
image_path = Path(image)
|
|
48
|
+
|
|
49
|
+
# Parse margin
|
|
50
|
+
margin_mm = None
|
|
51
|
+
margin_px = None
|
|
52
|
+
|
|
53
|
+
if margin.endswith("mm"):
|
|
54
|
+
margin_mm = float(margin[:-2])
|
|
55
|
+
elif margin.endswith("px"):
|
|
56
|
+
margin_px = int(margin[:-2])
|
|
57
|
+
else:
|
|
58
|
+
# Default to mm
|
|
59
|
+
try:
|
|
60
|
+
margin_mm = float(margin)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise click.ClickException(f"Invalid margin format: {margin}") from None
|
|
63
|
+
|
|
64
|
+
# Determine output path
|
|
65
|
+
if output:
|
|
66
|
+
output_path = Path(output)
|
|
67
|
+
elif overwrite:
|
|
68
|
+
output_path = None # Will overwrite in place
|
|
69
|
+
else:
|
|
70
|
+
output_path = image_path.with_stem(f"{image_path.stem}_cropped")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
result = fr_crop(
|
|
74
|
+
image_path,
|
|
75
|
+
output_path=output_path,
|
|
76
|
+
margin_mm=margin_mm,
|
|
77
|
+
margin_px=margin_px,
|
|
78
|
+
overwrite=overwrite,
|
|
79
|
+
)
|
|
80
|
+
click.echo(f"Cropped: {result}")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise click.ClickException(f"Crop failed: {e}") from e
|
figrecipe/_cli/_edit.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""edit command - Launch GUI editor."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("source", type=click.Path(exists=True), required=False)
|
|
13
|
+
@click.option(
|
|
14
|
+
"--port",
|
|
15
|
+
type=int,
|
|
16
|
+
default=5050,
|
|
17
|
+
help="Server port (default: 5050).",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"--host",
|
|
21
|
+
type=str,
|
|
22
|
+
default="127.0.0.1",
|
|
23
|
+
help="Host to bind (default: 127.0.0.1).",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--no-browser",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
help="Don't auto-open browser.",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--desktop",
|
|
32
|
+
is_flag=True,
|
|
33
|
+
help="Launch as native desktop window (requires pywebview).",
|
|
34
|
+
)
|
|
35
|
+
def edit(
|
|
36
|
+
source: Optional[str],
|
|
37
|
+
port: int,
|
|
38
|
+
host: str,
|
|
39
|
+
no_browser: bool,
|
|
40
|
+
desktop: bool,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Launch interactive GUI editor.
|
|
43
|
+
|
|
44
|
+
SOURCE is the optional path to a .yaml recipe file or bundle.
|
|
45
|
+
If not provided, creates a new blank figure.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
from .. import edit as fr_edit
|
|
49
|
+
except ImportError:
|
|
50
|
+
raise click.ClickException(
|
|
51
|
+
"Editor requires Flask. Install with: pip install figrecipe[editor]"
|
|
52
|
+
) from None
|
|
53
|
+
|
|
54
|
+
source_path = Path(source) if source else None
|
|
55
|
+
|
|
56
|
+
if desktop:
|
|
57
|
+
click.echo("Starting editor in desktop mode...")
|
|
58
|
+
else:
|
|
59
|
+
click.echo(f"Starting editor on http://{host}:{port}")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
fr_edit(
|
|
63
|
+
source_path,
|
|
64
|
+
port=port,
|
|
65
|
+
host=host,
|
|
66
|
+
open_browser=not no_browser,
|
|
67
|
+
desktop=desktop,
|
|
68
|
+
)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise click.ClickException(f"Editor failed: {e}") from e
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""extract command - Extract plotted data from recipes."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
14
|
+
@click.option(
|
|
15
|
+
"-o",
|
|
16
|
+
"--output",
|
|
17
|
+
type=click.Path(),
|
|
18
|
+
help="Output directory for extracted data.",
|
|
19
|
+
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"-f",
|
|
22
|
+
"--format",
|
|
23
|
+
"fmt",
|
|
24
|
+
type=click.Choice(["csv", "npz", "json"]),
|
|
25
|
+
default="csv",
|
|
26
|
+
help="Data format (default: csv).",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--axes",
|
|
30
|
+
type=str,
|
|
31
|
+
help="Specific axes to extract (e.g., ax_0_0).",
|
|
32
|
+
)
|
|
33
|
+
def extract(
|
|
34
|
+
source: str,
|
|
35
|
+
output: Optional[str],
|
|
36
|
+
fmt: str,
|
|
37
|
+
axes: Optional[str],
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Extract plotted data arrays from a recipe.
|
|
40
|
+
|
|
41
|
+
SOURCE is the path to a .yaml recipe file.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from .. import extract_data
|
|
45
|
+
|
|
46
|
+
source_path = Path(source)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
data = extract_data(source_path)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise click.ClickException(f"Failed to extract data: {e}") from e
|
|
52
|
+
|
|
53
|
+
if not data:
|
|
54
|
+
click.echo("No data found in recipe.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Determine output directory
|
|
58
|
+
if output:
|
|
59
|
+
output_dir = Path(output)
|
|
60
|
+
else:
|
|
61
|
+
output_dir = source_path.parent / f"{source_path.stem}_data"
|
|
62
|
+
|
|
63
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
# Export data
|
|
66
|
+
for call_id, call_data in data.items():
|
|
67
|
+
if axes and not call_id.startswith(axes):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if fmt == "json":
|
|
71
|
+
_save_json(output_dir / f"{call_id}.json", call_data)
|
|
72
|
+
elif fmt == "npz":
|
|
73
|
+
_save_npz(output_dir / f"{call_id}.npz", call_data)
|
|
74
|
+
else: # csv
|
|
75
|
+
_save_csv(output_dir / f"{call_id}.csv", call_data)
|
|
76
|
+
|
|
77
|
+
click.echo(f"Extracted: {call_id}")
|
|
78
|
+
|
|
79
|
+
click.echo(f"\nData saved to: {output_dir}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _save_json(path: Path, data: dict) -> None:
|
|
83
|
+
"""Save data as JSON."""
|
|
84
|
+
import numpy as np
|
|
85
|
+
|
|
86
|
+
def convert(obj):
|
|
87
|
+
if isinstance(obj, np.ndarray):
|
|
88
|
+
return obj.tolist()
|
|
89
|
+
return obj
|
|
90
|
+
|
|
91
|
+
with open(path, "w") as f:
|
|
92
|
+
json.dump({k: convert(v) for k, v in data.items()}, f, indent=2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _save_npz(path: Path, data: dict) -> None:
|
|
96
|
+
"""Save data as NPZ."""
|
|
97
|
+
import numpy as np
|
|
98
|
+
|
|
99
|
+
np.savez(path, **data)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _save_csv(path: Path, data: dict) -> None:
|
|
103
|
+
"""Save data as CSV."""
|
|
104
|
+
import numpy as np
|
|
105
|
+
|
|
106
|
+
# Try to create a table from the data
|
|
107
|
+
arrays = {k: np.asarray(v) for k, v in data.items() if hasattr(v, "__len__")}
|
|
108
|
+
|
|
109
|
+
if not arrays:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Find max length
|
|
113
|
+
max_len = max(len(a.flatten()) for a in arrays.values())
|
|
114
|
+
|
|
115
|
+
with open(path, "w") as f:
|
|
116
|
+
# Header
|
|
117
|
+
f.write(",".join(arrays.keys()) + "\n")
|
|
118
|
+
|
|
119
|
+
# Data rows
|
|
120
|
+
for i in range(max_len):
|
|
121
|
+
row = []
|
|
122
|
+
for arr in arrays.values():
|
|
123
|
+
flat = arr.flatten()
|
|
124
|
+
if i < len(flat):
|
|
125
|
+
row.append(str(flat[i]))
|
|
126
|
+
else:
|
|
127
|
+
row.append("")
|
|
128
|
+
f.write(",".join(row) + "\n")
|
figrecipe/_cli/_fonts.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""fonts command - Font management."""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.option(
|
|
12
|
+
"--check",
|
|
13
|
+
type=str,
|
|
14
|
+
help="Check if a specific font is available.",
|
|
15
|
+
)
|
|
16
|
+
@click.option(
|
|
17
|
+
"--search",
|
|
18
|
+
type=str,
|
|
19
|
+
help="Search for fonts matching a pattern.",
|
|
20
|
+
)
|
|
21
|
+
def fonts(check: Optional[str], search: Optional[str]) -> None:
|
|
22
|
+
"""List or check available fonts."""
|
|
23
|
+
from .. import check_font, list_available_fonts
|
|
24
|
+
|
|
25
|
+
if check:
|
|
26
|
+
available = check_font(check)
|
|
27
|
+
if available:
|
|
28
|
+
click.echo(f"Font '{check}' is available.")
|
|
29
|
+
else:
|
|
30
|
+
click.echo(f"Font '{check}' is NOT available.")
|
|
31
|
+
raise SystemExit(1)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
all_fonts = list_available_fonts()
|
|
35
|
+
|
|
36
|
+
if search:
|
|
37
|
+
pattern = search.lower()
|
|
38
|
+
matching = [f for f in all_fonts if pattern in f.lower()]
|
|
39
|
+
click.echo(f"Fonts matching '{search}':")
|
|
40
|
+
for font in sorted(matching):
|
|
41
|
+
click.echo(f" {font}")
|
|
42
|
+
click.echo(f"\nFound {len(matching)} matching fonts.")
|
|
43
|
+
else:
|
|
44
|
+
click.echo("Available fonts:")
|
|
45
|
+
for font in sorted(all_fonts):
|
|
46
|
+
click.echo(f" {font}")
|
|
47
|
+
click.echo(f"\nTotal: {len(all_fonts)} fonts.")
|
figrecipe/_cli/_info.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""info command - Inspect recipe metadata."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
13
|
+
@click.option(
|
|
14
|
+
"--json",
|
|
15
|
+
"as_json",
|
|
16
|
+
is_flag=True,
|
|
17
|
+
help="Output as JSON.",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"-v",
|
|
21
|
+
"--verbose",
|
|
22
|
+
is_flag=True,
|
|
23
|
+
help="Show detailed information.",
|
|
24
|
+
)
|
|
25
|
+
def info(source: str, as_json: bool, verbose: bool) -> None:
|
|
26
|
+
"""Show information about a recipe.
|
|
27
|
+
|
|
28
|
+
SOURCE is the path to a .yaml recipe file.
|
|
29
|
+
"""
|
|
30
|
+
from .. import info as fr_info
|
|
31
|
+
|
|
32
|
+
source_path = Path(source)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
recipe_info = fr_info(source_path)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise click.ClickException(f"Failed to load recipe: {e}") from e
|
|
38
|
+
|
|
39
|
+
if as_json:
|
|
40
|
+
click.echo(json.dumps(recipe_info, indent=2, default=str))
|
|
41
|
+
else:
|
|
42
|
+
_print_info(recipe_info, verbose)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _print_info(info: dict, verbose: bool) -> None:
|
|
46
|
+
"""Print recipe info in human-readable format."""
|
|
47
|
+
click.echo(f"Recipe Version: {info.get('figrecipe_version', 'unknown')}")
|
|
48
|
+
click.echo(f"Figure ID: {info.get('id', 'unknown')}")
|
|
49
|
+
click.echo(f"Created: {info.get('created', 'unknown')}")
|
|
50
|
+
click.echo(f"Matplotlib: {info.get('matplotlib_version', 'unknown')}")
|
|
51
|
+
|
|
52
|
+
if "figure" in info:
|
|
53
|
+
fig = info["figure"]
|
|
54
|
+
click.echo(f"Figure Size: {fig.get('figsize', 'unknown')}")
|
|
55
|
+
click.echo(f"DPI: {fig.get('dpi', 'unknown')}")
|
|
56
|
+
|
|
57
|
+
if "axes" in info:
|
|
58
|
+
click.echo(f"Axes Count: {len(info['axes'])}")
|
|
59
|
+
|
|
60
|
+
if verbose:
|
|
61
|
+
for ax_key, ax_info in info["axes"].items():
|
|
62
|
+
click.echo(f"\n {ax_key}:")
|
|
63
|
+
if "calls" in ax_info:
|
|
64
|
+
for call in ax_info["calls"]:
|
|
65
|
+
func = call.get("function", "unknown")
|
|
66
|
+
call_id = call.get("id", "")
|
|
67
|
+
click.echo(f" - {func} ({call_id})")
|
figrecipe/_cli/_main.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Main CLI entry point for figrecipe."""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .. import __version__
|
|
8
|
+
from ._compose import compose
|
|
9
|
+
from ._convert import convert
|
|
10
|
+
from ._crop import crop
|
|
11
|
+
from ._edit import edit
|
|
12
|
+
from ._extract import extract
|
|
13
|
+
from ._fonts import fonts
|
|
14
|
+
from ._info import info
|
|
15
|
+
from ._reproduce import reproduce
|
|
16
|
+
from ._style import style
|
|
17
|
+
from ._validate import validate
|
|
18
|
+
from ._version import version as version_cmd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group(
|
|
22
|
+
invoke_without_command=True,
|
|
23
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
24
|
+
)
|
|
25
|
+
@click.option("--version", "-V", is_flag=True, help="Show version and exit.")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def main(ctx: click.Context, version: bool) -> None:
|
|
28
|
+
"""figrecipe - Reproducible matplotlib figures.
|
|
29
|
+
|
|
30
|
+
A command-line interface for creating, reproducing, and editing
|
|
31
|
+
matplotlib figures using YAML recipes.
|
|
32
|
+
|
|
33
|
+
When run without a subcommand, launches the GUI editor.
|
|
34
|
+
"""
|
|
35
|
+
if version:
|
|
36
|
+
click.echo(f"figrecipe {__version__}")
|
|
37
|
+
ctx.exit(0)
|
|
38
|
+
|
|
39
|
+
if ctx.invoked_subcommand is None:
|
|
40
|
+
ctx.invoke(edit)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Register commands
|
|
44
|
+
main.add_command(reproduce)
|
|
45
|
+
main.add_command(info)
|
|
46
|
+
main.add_command(extract)
|
|
47
|
+
main.add_command(validate)
|
|
48
|
+
main.add_command(edit)
|
|
49
|
+
main.add_command(crop)
|
|
50
|
+
main.add_command(compose)
|
|
51
|
+
main.add_command(style)
|
|
52
|
+
main.add_command(convert)
|
|
53
|
+
main.add_command(fonts)
|
|
54
|
+
main.add_command(version_cmd)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""reproduce command - Recreate figure from recipe."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
13
|
+
@click.option(
|
|
14
|
+
"-o",
|
|
15
|
+
"--output",
|
|
16
|
+
type=click.Path(),
|
|
17
|
+
help="Output path for the reproduced figure.",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"-f",
|
|
21
|
+
"--format",
|
|
22
|
+
"fmt",
|
|
23
|
+
type=click.Choice(["png", "pdf", "svg"]),
|
|
24
|
+
default="png",
|
|
25
|
+
help="Output format (default: png).",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--dpi",
|
|
29
|
+
type=int,
|
|
30
|
+
default=300,
|
|
31
|
+
help="DPI for raster output (default: 300).",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--show",
|
|
35
|
+
is_flag=True,
|
|
36
|
+
help="Display the figure interactively.",
|
|
37
|
+
)
|
|
38
|
+
def reproduce(
|
|
39
|
+
source: str,
|
|
40
|
+
output: Optional[str],
|
|
41
|
+
fmt: str,
|
|
42
|
+
dpi: int,
|
|
43
|
+
show: bool,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Reproduce a figure from a YAML recipe.
|
|
46
|
+
|
|
47
|
+
SOURCE is the path to a .yaml recipe file or bundle directory.
|
|
48
|
+
"""
|
|
49
|
+
import matplotlib.pyplot as plt
|
|
50
|
+
|
|
51
|
+
from .. import reproduce as fr_reproduce
|
|
52
|
+
|
|
53
|
+
source_path = Path(source)
|
|
54
|
+
|
|
55
|
+
# Reproduce the figure
|
|
56
|
+
try:
|
|
57
|
+
fig, axes = fr_reproduce(source_path)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise click.ClickException(f"Failed to reproduce: {e}") from e
|
|
60
|
+
|
|
61
|
+
# Determine output path
|
|
62
|
+
if output:
|
|
63
|
+
output_path = Path(output)
|
|
64
|
+
else:
|
|
65
|
+
output_path = source_path.with_suffix(f".reproduced.{fmt}")
|
|
66
|
+
|
|
67
|
+
# Save or show
|
|
68
|
+
if show:
|
|
69
|
+
plt.show()
|
|
70
|
+
else:
|
|
71
|
+
fig.savefig(output_path, dpi=dpi, format=fmt)
|
|
72
|
+
click.echo(f"Saved: {output_path}")
|
|
73
|
+
|
|
74
|
+
# Close the figure (handle both regular and Recording figures)
|
|
75
|
+
try:
|
|
76
|
+
plt.close(fig)
|
|
77
|
+
except TypeError:
|
|
78
|
+
# RecordingFigure wrapper - close all instead
|
|
79
|
+
plt.close("all")
|