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,77 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """style command - Style management subcommands."""
4
+
5
+
6
+ import click
7
+
8
+
9
+ @click.group()
10
+ def style() -> None:
11
+ """Manage figure styles and presets."""
12
+ pass
13
+
14
+
15
+ @style.command("list")
16
+ def list_styles() -> None:
17
+ """List available style presets."""
18
+ from .. import list_presets
19
+
20
+ presets = list_presets()
21
+
22
+ click.echo("Available style presets:")
23
+ for preset in presets:
24
+ click.echo(f" - {preset}")
25
+
26
+
27
+ @style.command("show")
28
+ @click.argument("name")
29
+ def show_style(name: str) -> None:
30
+ """Show details of a style preset.
31
+
32
+ NAME is the preset name (e.g., SCITEX, MATPLOTLIB).
33
+ """
34
+ from ruamel.yaml import YAML
35
+
36
+ from ..styles._style_loader import load_preset
37
+
38
+ try:
39
+ style_dict = load_preset(name)
40
+ except Exception as e:
41
+ raise click.ClickException(f"Failed to load preset '{name}': {e}") from e
42
+
43
+ yaml = YAML()
44
+ yaml.default_flow_style = False
45
+
46
+ click.echo(f"Style preset: {name}\n")
47
+
48
+ import io
49
+
50
+ stream = io.StringIO()
51
+ yaml.dump(style_dict, stream)
52
+ click.echo(stream.getvalue())
53
+
54
+
55
+ @style.command("apply")
56
+ @click.argument("name")
57
+ def apply_style_cmd(name: str) -> None:
58
+ """Apply a style preset globally.
59
+
60
+ NAME is the preset name (e.g., SCITEX, MATPLOTLIB).
61
+ """
62
+ from .. import load_style
63
+
64
+ try:
65
+ load_style(name)
66
+ click.echo(f"Applied style: {name}")
67
+ except Exception as e:
68
+ raise click.ClickException(f"Failed to apply style: {e}") from e
69
+
70
+
71
+ @style.command("reset")
72
+ def reset_style() -> None:
73
+ """Reset to default matplotlib style."""
74
+ from .. import unload_style
75
+
76
+ unload_style()
77
+ click.echo("Style reset to defaults.")
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """validate command - Verify recipe reproducibility."""
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+
10
+ @click.command()
11
+ @click.argument("source", type=click.Path(exists=True))
12
+ @click.option(
13
+ "--threshold",
14
+ type=float,
15
+ default=100.0,
16
+ help="MSE threshold for validation (default: 100).",
17
+ )
18
+ @click.option(
19
+ "--strict",
20
+ is_flag=True,
21
+ help="Fail on any difference.",
22
+ )
23
+ @click.option(
24
+ "-q",
25
+ "--quiet",
26
+ is_flag=True,
27
+ help="Only output pass/fail status.",
28
+ )
29
+ def validate(
30
+ source: str,
31
+ threshold: float,
32
+ strict: bool,
33
+ quiet: bool,
34
+ ) -> None:
35
+ """Validate that a recipe reproduces its original figure.
36
+
37
+ SOURCE is the path to a .yaml recipe file.
38
+ """
39
+ from .. import validate as fr_validate
40
+
41
+ source_path = Path(source)
42
+
43
+ if strict:
44
+ threshold = 0.0
45
+
46
+ try:
47
+ result = fr_validate(source_path, mse_threshold=threshold)
48
+ except Exception as e:
49
+ raise click.ClickException(f"Validation failed: {e}") from e
50
+
51
+ if quiet:
52
+ if result.valid:
53
+ click.echo("PASS")
54
+ else:
55
+ click.echo("FAIL")
56
+ raise SystemExit(1)
57
+ else:
58
+ click.echo(f"Validation: {'PASS' if result.valid else 'FAIL'}")
59
+ click.echo(f"MSE: {result.mse:.6f}")
60
+ click.echo(f"Threshold: {threshold}")
61
+
62
+ if hasattr(result, "message") and result.message:
63
+ click.echo(f"Message: {result.message}")
64
+
65
+ if not result.valid:
66
+ raise SystemExit(1)
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """version command - Show version information."""
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.option(
10
+ "--full",
11
+ is_flag=True,
12
+ help="Show full version info with dependencies.",
13
+ )
14
+ def version(full: bool) -> None:
15
+ """Show version information."""
16
+ from .. import __version__
17
+
18
+ click.echo(f"figrecipe {__version__}")
19
+
20
+ if full:
21
+ click.echo()
22
+ _show_dependency_versions()
23
+
24
+
25
+ def _show_dependency_versions() -> None:
26
+ """Show versions of key dependencies."""
27
+ deps = [
28
+ ("matplotlib", "matplotlib"),
29
+ ("numpy", "numpy"),
30
+ ("ruamel.yaml", "ruamel.yaml"),
31
+ ("scipy", "scipy"),
32
+ ("Pillow", "PIL"),
33
+ ("seaborn", "seaborn"),
34
+ ("pandas", "pandas"),
35
+ ("flask", "flask"),
36
+ ]
37
+
38
+ click.echo("Dependencies:")
39
+ for name, module in deps:
40
+ try:
41
+ mod = __import__(module)
42
+ ver = getattr(mod, "__version__", "unknown")
43
+ click.echo(f" {name}: {ver}")
44
+ except ImportError:
45
+ click.echo(f" {name}: not installed")
46
+
47
+ # Python version
48
+ import sys
49
+
50
+ click.echo(f"\nPython: {sys.version}")
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Composition module for combining multiple figures.
4
+
5
+ This module provides functionality to:
6
+ - Compose new figures from multiple recipe sources
7
+ - Import axes from external recipes into existing figures
8
+ - Hide/show panels for visual composition
9
+ - Align and distribute panels
10
+
11
+ Phase 1-3 of the composition feature.
12
+ """
13
+
14
+ from ._alignment import AlignmentMode, align_panels, distribute_panels, smart_align
15
+ from ._compose import compose
16
+ from ._import_axes import import_axes
17
+ from ._visibility import hide_panel, show_panel, toggle_panel
18
+
19
+ __all__ = [
20
+ # Phase 1: Composition
21
+ "compose",
22
+ "import_axes",
23
+ # Phase 2: Visibility
24
+ "hide_panel",
25
+ "show_panel",
26
+ "toggle_panel",
27
+ # Phase 3: Alignment
28
+ "AlignmentMode",
29
+ "align_panels",
30
+ "distribute_panels",
31
+ "smart_align",
32
+ ]
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel alignment tools for composition feature.
4
+
5
+ Provides alignment and distribution functions for multi-panel figures.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import List, Optional, Tuple, Union
10
+
11
+ from matplotlib.transforms import Bbox
12
+
13
+ from .._wrappers import RecordingFigure
14
+
15
+
16
+ class AlignmentMode(Enum):
17
+ """Alignment modes for panel positioning."""
18
+
19
+ LEFT = "left"
20
+ RIGHT = "right"
21
+ TOP = "top"
22
+ BOTTOM = "bottom"
23
+ CENTER_H = "center_h" # Horizontal center
24
+ CENTER_V = "center_v" # Vertical center
25
+ AXIS_X = "axis_x" # Align x-axes
26
+ AXIS_Y = "axis_y" # Align y-axes
27
+
28
+
29
+ def align_panels(
30
+ fig: RecordingFigure,
31
+ panels: List[Tuple[int, int]],
32
+ mode: Union[str, AlignmentMode],
33
+ reference: Optional[Tuple[int, int]] = None,
34
+ ) -> None:
35
+ """Align multiple panels to a reference panel.
36
+
37
+ Parameters
38
+ ----------
39
+ fig : RecordingFigure
40
+ The figure containing the panels.
41
+ panels : list of tuple
42
+ List of (row, col) positions to align.
43
+ mode : str or AlignmentMode
44
+ Alignment mode: 'left', 'right', 'top', 'bottom',
45
+ 'center_h', 'center_v', 'axis_x', 'axis_y'.
46
+ reference : tuple, optional
47
+ Reference panel position. If None, uses first panel.
48
+
49
+ Examples
50
+ --------
51
+ >>> import figrecipe as fr
52
+ >>> fig, axes = fr.subplots(2, 2)
53
+ >>> # Align left column panels to left edge
54
+ >>> fr.align_panels(fig, [(0, 0), (1, 0)], mode="left")
55
+ """
56
+ mode = AlignmentMode(mode) if isinstance(mode, str) else mode
57
+
58
+ if not panels:
59
+ return
60
+
61
+ ref_pos = reference or panels[0]
62
+ ref_ax = _get_mpl_axes(fig, ref_pos)
63
+ if ref_ax is None:
64
+ return
65
+ ref_bbox = ref_ax.get_position()
66
+
67
+ for pos in panels:
68
+ if pos == ref_pos:
69
+ continue
70
+
71
+ ax = _get_mpl_axes(fig, pos)
72
+ if ax is None:
73
+ continue
74
+ bbox = ax.get_position()
75
+ new_bbox = _calculate_aligned_bbox(bbox, ref_bbox, mode)
76
+ ax.set_position(new_bbox)
77
+
78
+
79
+ def distribute_panels(
80
+ fig: RecordingFigure,
81
+ panels: List[Tuple[int, int]],
82
+ direction: str = "horizontal",
83
+ spacing_mm: Optional[float] = None,
84
+ ) -> None:
85
+ """Distribute panels evenly with optional fixed spacing.
86
+
87
+ Parameters
88
+ ----------
89
+ fig : RecordingFigure
90
+ The figure containing the panels.
91
+ panels : list of tuple
92
+ List of (row, col) positions to distribute.
93
+ direction : str
94
+ 'horizontal' or 'vertical'.
95
+ spacing_mm : float, optional
96
+ Fixed spacing in mm. If None, distribute evenly within
97
+ current bounds.
98
+
99
+ Examples
100
+ --------
101
+ >>> import figrecipe as fr
102
+ >>> fig, axes = fr.subplots(1, 3)
103
+ >>> # Distribute evenly
104
+ >>> fr.distribute_panels(fig, [(0, 0), (0, 1), (0, 2)])
105
+ >>> # With fixed 5mm spacing
106
+ >>> fr.distribute_panels(fig, [(0, 0), (0, 1), (0, 2)], spacing_mm=5)
107
+ """
108
+ if len(panels) < 2:
109
+ return
110
+
111
+ # Sort panels by position
112
+ if direction == "horizontal":
113
+ sorted_panels = sorted(panels, key=lambda p: p[1])
114
+ else:
115
+ sorted_panels = sorted(panels, key=lambda p: p[0])
116
+
117
+ # Get bounding boxes
118
+ bboxes = []
119
+ valid_panels = []
120
+ for p in sorted_panels:
121
+ ax = _get_mpl_axes(fig, p)
122
+ if ax is not None:
123
+ bboxes.append(ax.get_position())
124
+ valid_panels.append(p)
125
+
126
+ if len(valid_panels) < 2:
127
+ return
128
+
129
+ # Calculate even distribution
130
+ if direction == "horizontal":
131
+ _distribute_horizontal(fig, valid_panels, bboxes, spacing_mm)
132
+ else:
133
+ _distribute_vertical(fig, valid_panels, bboxes, spacing_mm)
134
+
135
+
136
+ def smart_align(
137
+ fig: RecordingFigure,
138
+ panels: Optional[List[Tuple[int, int]]] = None,
139
+ ) -> None:
140
+ """Automatically align panels in a compact grid layout.
141
+
142
+ Works like human behavior:
143
+ 1. Detect grid structure (nrows, ncols)
144
+ 2. Place panels from top-left to bottom-right
145
+ 3. Calculate minimum rectangle to cover all content in each row/column
146
+ 4. Unify row heights and column widths
147
+ 5. Use space effectively with theme margins and spacing
148
+
149
+ Uses margin and spacing values from the loaded SCITEX theme:
150
+ - margins.left_mm, margins.right_mm, margins.top_mm, margins.bottom_mm
151
+ - spacing.horizontal_mm, spacing.vertical_mm
152
+
153
+ Parameters
154
+ ----------
155
+ fig : RecordingFigure
156
+ The figure containing the panels.
157
+ panels : list of tuple, optional
158
+ Specific panels to align. If None, aligns all panels.
159
+
160
+ Examples
161
+ --------
162
+ >>> import figrecipe as fr
163
+ >>> fig, axes = fr.subplots(2, 2)
164
+ >>> # ... add plots ...
165
+ >>> fr.smart_align(fig) # Align all panels using theme settings
166
+ """
167
+ from .._utils._units import mm_to_inch
168
+
169
+ if panels is None:
170
+ panels = [tuple(map(int, ax_key.split("_")[1:3])) for ax_key in fig.record.axes]
171
+
172
+ if not panels:
173
+ return
174
+
175
+ # Get matplotlib figure
176
+ mpl_fig = fig.fig if hasattr(fig, "fig") else fig
177
+
178
+ # Get style from loaded theme
179
+ try:
180
+ from ..styles._style_loader import _STYLE_CACHE
181
+
182
+ style = _STYLE_CACHE
183
+ except (ImportError, AttributeError):
184
+ style = None
185
+
186
+ # Extract margin values from theme (with defaults)
187
+ if style and hasattr(style, "margins"):
188
+ margin_left = style.margins.get("left_mm", 6)
189
+ margin_right = style.margins.get("right_mm", 1)
190
+ margin_top = style.margins.get("top_mm", 5)
191
+ margin_bottom = style.margins.get("bottom_mm", 5)
192
+ else:
193
+ margin_left = margin_right = margin_top = margin_bottom = 5
194
+
195
+ # Extract spacing values from theme (with defaults)
196
+ if style and hasattr(style, "spacing"):
197
+ spacing_h_mm = style.spacing.get("horizontal_mm", 10)
198
+ spacing_v_mm = style.spacing.get("vertical_mm", 15)
199
+ else:
200
+ spacing_h_mm = 10
201
+ spacing_v_mm = 15
202
+
203
+ # Determine grid dimensions
204
+ max_row = max(p[0] for p in panels)
205
+ max_col = max(p[1] for p in panels)
206
+ nrows = max_row + 1
207
+ ncols = max_col + 1
208
+
209
+ # Get figure size in inches
210
+ fig_width, fig_height = mpl_fig.get_size_inches()
211
+
212
+ # Convert margins/spacing to figure fraction
213
+ margin_left_frac = mm_to_inch(margin_left) / fig_width
214
+ margin_right_frac = mm_to_inch(margin_right) / fig_width
215
+ margin_top_frac = mm_to_inch(margin_top) / fig_height
216
+ margin_bottom_frac = mm_to_inch(margin_bottom) / fig_height
217
+ spacing_frac_w = mm_to_inch(spacing_h_mm) / fig_width
218
+ spacing_frac_h = mm_to_inch(spacing_v_mm) / fig_height
219
+
220
+ # Build grid of axes
221
+ grid = {}
222
+ for pos in panels:
223
+ ax = _get_mpl_axes(fig, pos)
224
+ if ax is not None:
225
+ grid[pos] = ax
226
+
227
+ # Calculate content-based widths for each column
228
+ col_widths = []
229
+ for c in range(ncols):
230
+ max_width = 0
231
+ for r in range(nrows):
232
+ if (r, c) in grid:
233
+ bbox = grid[(r, c)].get_position()
234
+ max_width = max(max_width, bbox.width)
235
+ col_widths.append(max_width if max_width > 0 else 0.2)
236
+
237
+ # Calculate content-based heights for each row
238
+ row_heights = []
239
+ for r in range(nrows):
240
+ max_height = 0
241
+ for c in range(ncols):
242
+ if (r, c) in grid:
243
+ bbox = grid[(r, c)].get_position()
244
+ max_height = max(max_height, bbox.height)
245
+ row_heights.append(max_height if max_height > 0 else 0.15)
246
+
247
+ # Calculate total content size
248
+ total_content_w = sum(col_widths) + spacing_frac_w * (ncols - 1)
249
+ total_content_h = sum(row_heights) + spacing_frac_h * (nrows - 1)
250
+
251
+ # Available space after asymmetric margins
252
+ avail_w = 1.0 - margin_left_frac - margin_right_frac
253
+ avail_h = 1.0 - margin_top_frac - margin_bottom_frac
254
+
255
+ # Scale factor to fit content in available space
256
+ scale_w = avail_w / total_content_w if total_content_w > 0 else 1.0
257
+ scale_h = avail_h / total_content_h if total_content_h > 0 else 1.0
258
+ scale = min(scale_w, scale_h, 1.0) # Don't enlarge, only shrink if needed
259
+
260
+ # Apply scaling
261
+ col_widths = [w * scale for w in col_widths]
262
+ row_heights = [h * scale for h in row_heights]
263
+ spacing_w = spacing_frac_w * scale
264
+ spacing_h = spacing_frac_h * scale
265
+
266
+ # Recalculate total after scaling
267
+ total_w = sum(col_widths) + spacing_w * (ncols - 1)
268
+ total_h = sum(row_heights) + spacing_h * (nrows - 1)
269
+
270
+ # Position grid: left-aligned with left margin, centered vertically
271
+ start_x = margin_left_frac + (avail_w - total_w) / 2
272
+
273
+ # Position panels from top-left to bottom-right
274
+ # Matplotlib y=0 is bottom, so we work from top down
275
+ y = 1.0 - margin_top_frac - (avail_h - total_h) / 2
276
+ for r in range(nrows):
277
+ y -= row_heights[r]
278
+ x = start_x
279
+ for c in range(ncols):
280
+ if (r, c) in grid:
281
+ ax = grid[(r, c)]
282
+ new_bbox = Bbox.from_bounds(x, y, col_widths[c], row_heights[r])
283
+ ax.set_position(new_bbox)
284
+ x += col_widths[c] + spacing_w
285
+ y -= spacing_h
286
+
287
+
288
+ def _get_mpl_axes(fig: RecordingFigure, position: Tuple[int, int]):
289
+ """Get matplotlib axes at position.
290
+
291
+ Parameters
292
+ ----------
293
+ fig : RecordingFigure
294
+ The figure.
295
+ position : tuple
296
+ (row, col) position.
297
+
298
+ Returns
299
+ -------
300
+ matplotlib.axes.Axes or None
301
+ The matplotlib axes, or None if not found.
302
+ """
303
+ row, col = position
304
+ try:
305
+ axes = fig._axes
306
+ if isinstance(axes, list):
307
+ if isinstance(axes[0], list):
308
+ ax = axes[row][col]
309
+ else:
310
+ # 1D list for single row/column
311
+ ax = axes[max(row, col)]
312
+ else:
313
+ # Numpy array
314
+ ax = axes[row, col]
315
+
316
+ return ax._ax if hasattr(ax, "_ax") else ax
317
+ except (IndexError, AttributeError, KeyError, TypeError):
318
+ return None
319
+
320
+
321
+ def _calculate_aligned_bbox(
322
+ bbox: Bbox,
323
+ ref_bbox: Bbox,
324
+ mode: AlignmentMode,
325
+ ) -> Bbox:
326
+ """Calculate new bbox aligned to reference.
327
+
328
+ Parameters
329
+ ----------
330
+ bbox : Bbox
331
+ Current bounding box.
332
+ ref_bbox : Bbox
333
+ Reference bounding box.
334
+ mode : AlignmentMode
335
+ Alignment mode.
336
+
337
+ Returns
338
+ -------
339
+ Bbox
340
+ New aligned bounding box.
341
+ """
342
+ x0, y0 = bbox.x0, bbox.y0
343
+ width, height = bbox.width, bbox.height
344
+
345
+ if mode == AlignmentMode.LEFT:
346
+ x0 = ref_bbox.x0
347
+ elif mode == AlignmentMode.RIGHT:
348
+ x0 = ref_bbox.x1 - width
349
+ elif mode == AlignmentMode.TOP:
350
+ y0 = ref_bbox.y1 - height
351
+ elif mode == AlignmentMode.BOTTOM:
352
+ y0 = ref_bbox.y0
353
+ elif mode == AlignmentMode.CENTER_H:
354
+ x0 = ref_bbox.x0 + (ref_bbox.width - width) / 2
355
+ elif mode == AlignmentMode.CENTER_V:
356
+ y0 = ref_bbox.y0 + (ref_bbox.height - height) / 2
357
+ elif mode == AlignmentMode.AXIS_X:
358
+ # Align bottom edges (x-axis position)
359
+ y0 = ref_bbox.y0
360
+ elif mode == AlignmentMode.AXIS_Y:
361
+ # Align left edges (y-axis position)
362
+ x0 = ref_bbox.x0
363
+
364
+ return Bbox.from_bounds(x0, y0, width, height)
365
+
366
+
367
+ def _distribute_horizontal(
368
+ fig: RecordingFigure,
369
+ panels: List[Tuple[int, int]],
370
+ bboxes: List[Bbox],
371
+ spacing_mm: Optional[float],
372
+ ) -> None:
373
+ """Distribute panels horizontally.
374
+
375
+ Parameters
376
+ ----------
377
+ fig : RecordingFigure
378
+ The figure.
379
+ panels : list of tuple
380
+ Panel positions (sorted).
381
+ bboxes : list of Bbox
382
+ Current bounding boxes.
383
+ spacing_mm : float or None
384
+ Fixed spacing in mm, or None for even distribution.
385
+ """
386
+ if spacing_mm is not None:
387
+ from .._utils._units import mm_to_inch
388
+
389
+ fig_width = fig.fig.get_figwidth()
390
+ spacing = mm_to_inch(spacing_mm) / fig_width
391
+ else:
392
+ total_width = sum(b.width for b in bboxes)
393
+ available = bboxes[-1].x1 - bboxes[0].x0
394
+ spacing = (
395
+ (available - total_width) / (len(panels) - 1) if len(panels) > 1 else 0
396
+ )
397
+
398
+ x = bboxes[0].x0
399
+ for panel, bbox in zip(panels, bboxes):
400
+ ax = _get_mpl_axes(fig, panel)
401
+ if ax is not None:
402
+ new_bbox = Bbox.from_bounds(x, bbox.y0, bbox.width, bbox.height)
403
+ ax.set_position(new_bbox)
404
+ x += bbox.width + spacing
405
+
406
+
407
+ def _distribute_vertical(
408
+ fig: RecordingFigure,
409
+ panels: List[Tuple[int, int]],
410
+ bboxes: List[Bbox],
411
+ spacing_mm: Optional[float],
412
+ ) -> None:
413
+ """Distribute panels vertically.
414
+
415
+ Parameters
416
+ ----------
417
+ fig : RecordingFigure
418
+ The figure.
419
+ panels : list of tuple
420
+ Panel positions (sorted).
421
+ bboxes : list of Bbox
422
+ Current bounding boxes.
423
+ spacing_mm : float or None
424
+ Fixed spacing in mm, or None for even distribution.
425
+ """
426
+ if spacing_mm is not None:
427
+ from .._utils._units import mm_to_inch
428
+
429
+ fig_height = fig.fig.get_figheight()
430
+ spacing = mm_to_inch(spacing_mm) / fig_height
431
+ else:
432
+ total_height = sum(b.height for b in bboxes)
433
+ available = bboxes[-1].y1 - bboxes[0].y0
434
+ spacing = (
435
+ (available - total_height) / (len(panels) - 1) if len(panels) > 1 else 0
436
+ )
437
+
438
+ y = bboxes[0].y0
439
+ for panel, bbox in zip(panels, bboxes):
440
+ ax = _get_mpl_axes(fig, panel)
441
+ if ax is not None:
442
+ new_bbox = Bbox.from_bounds(bbox.x0, y, bbox.width, bbox.height)
443
+ ax.set_position(new_bbox)
444
+ y += bbox.height + spacing
445
+
446
+
447
+ __all__ = [
448
+ "AlignmentMode",
449
+ "align_panels",
450
+ "distribute_panels",
451
+ "smart_align",
452
+ ]