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.
Files changed (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {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
@@ -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
@@ -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")
@@ -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.")
@@ -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})")
@@ -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")