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,18 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Figure reproduction module.
4
+
5
+ This module provides functionality to reproduce figures from recipe files.
6
+ The public API exposes three main functions:
7
+ - reproduce: Reproduce a figure from a recipe file
8
+ - reproduce_from_record: Reproduce a figure from a FigureRecord object
9
+ - get_recipe_info: Get information about a recipe without reproducing it
10
+ """
11
+
12
+ from ._core import get_recipe_info, reproduce, reproduce_from_record
13
+
14
+ __all__ = [
15
+ "reproduce",
16
+ "reproduce_from_record",
17
+ "get_recipe_info",
18
+ ]
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Core reproduction logic for figure reproduction."""
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Union
8
+
9
+ import matplotlib.pyplot as plt
10
+ import numpy as np
11
+ from matplotlib.axes import Axes
12
+
13
+ from .._recorder import CallRecord, FigureRecord
14
+ from .._serializer import load_recipe
15
+ from .._utils._bundle import resolve_recipe_path
16
+
17
+
18
+ def reproduce(
19
+ path: Union[str, Path],
20
+ calls: Optional[List[str]] = None,
21
+ skip_decorations: bool = False,
22
+ apply_overrides: bool = True,
23
+ ):
24
+ """Reproduce a figure from a recipe file.
25
+
26
+ Parameters
27
+ ----------
28
+ path : str or Path
29
+ Path to recipe. Supports multiple formats:
30
+ - .yaml/.yml file: Direct recipe file
31
+ - .png/.jpg/etc: Image with associated .yaml
32
+ - Directory: Bundle containing recipe.yaml
33
+ - .zip: ZIP archive containing recipe.yaml
34
+ calls : list of str, optional
35
+ If provided, only reproduce these specific call IDs.
36
+ skip_decorations : bool
37
+ If True, skip decoration calls (labels, legends, etc.).
38
+ apply_overrides : bool
39
+ If True (default), apply .overrides.json if it exists.
40
+ This preserves manual GUI editor changes.
41
+
42
+ Returns
43
+ -------
44
+ fig : RecordingFigure
45
+ Reproduced figure (same type as subplots() returns).
46
+ axes : RecordingAxes or ndarray of RecordingAxes
47
+ Reproduced axes (single if 1x1, otherwise numpy array).
48
+
49
+ Examples
50
+ --------
51
+ >>> import figrecipe as fr
52
+ >>> fig, ax = fr.reproduce("experiment_001.yaml")
53
+ >>> fig, ax = fr.reproduce("experiment_001.png") # Also works
54
+ >>> fig, ax = fr.reproduce("figure_bundle/") # Directory bundle
55
+ >>> fig, ax = fr.reproduce("figure.zip") # ZIP bundle
56
+ >>> plt.show()
57
+ """
58
+ # Resolve path to actual recipe YAML (handles directories, ZIPs, images)
59
+ path, temp_dir = resolve_recipe_path(path)
60
+
61
+ try:
62
+ record = load_recipe(path)
63
+
64
+ # Check for override file and merge if exists
65
+ if apply_overrides:
66
+ overrides_path = path.with_suffix(".overrides.json")
67
+ if overrides_path.exists():
68
+ import json
69
+
70
+ with open(overrides_path) as f:
71
+ data = json.load(f)
72
+
73
+ # Apply style overrides
74
+ manual_overrides = data.get("manual_overrides", {})
75
+ if manual_overrides:
76
+ if record.style is None:
77
+ record.style = {}
78
+ record.style.update(manual_overrides)
79
+
80
+ # Apply call overrides (kwargs changes from editor)
81
+ call_overrides = data.get("call_overrides", {})
82
+ if call_overrides:
83
+ for ax_key, ax_record in record.axes.items():
84
+ for call in ax_record.calls:
85
+ if call.id in call_overrides:
86
+ call.kwargs.update(call_overrides[call.id])
87
+
88
+ return reproduce_from_record(
89
+ record, calls=calls, skip_decorations=skip_decorations
90
+ )
91
+ finally:
92
+ if temp_dir is not None and temp_dir.exists():
93
+ shutil.rmtree(temp_dir, ignore_errors=True)
94
+
95
+
96
+ def reproduce_from_record(
97
+ record: FigureRecord,
98
+ calls: Optional[List[str]] = None,
99
+ skip_decorations: bool = False,
100
+ ):
101
+ """Reproduce a figure from a FigureRecord.
102
+
103
+ Parameters
104
+ ----------
105
+ record : FigureRecord
106
+ The figure record to reproduce.
107
+ calls : list of str, optional
108
+ If provided, only reproduce these specific call IDs.
109
+ skip_decorations : bool
110
+ If True, skip decoration calls.
111
+
112
+ Returns
113
+ -------
114
+ fig : RecordingFigure
115
+ Reproduced figure (wrapped).
116
+ axes : RecordingAxes or ndarray of RecordingAxes
117
+ Reproduced axes (wrapped, numpy array for multi-axes).
118
+ """
119
+ from .._recorder import Recorder
120
+ from .._wrappers import RecordingAxes, RecordingFigure
121
+
122
+ # Determine grid size from axes positions
123
+ max_row = 0
124
+ max_col = 0
125
+ for ax_key in record.axes.keys():
126
+ parts = ax_key.split("_")
127
+ if len(parts) >= 3:
128
+ max_row = max(max_row, int(parts[1]))
129
+ max_col = max(max_col, int(parts[2]))
130
+
131
+ nrows = max_row + 1
132
+ ncols = max_col + 1
133
+
134
+ # Create figure
135
+ fig, mpl_axes = plt.subplots(
136
+ nrows,
137
+ ncols,
138
+ figsize=record.figsize,
139
+ dpi=record.dpi,
140
+ constrained_layout=record.constrained_layout,
141
+ )
142
+
143
+ # Apply layout if recorded (skip if constrained_layout is used)
144
+ if record.layout is not None and not record.constrained_layout:
145
+ fig.subplots_adjust(**record.layout)
146
+
147
+ # Ensure axes is 2D array
148
+ if nrows == 1 and ncols == 1:
149
+ axes_2d = np.array([[mpl_axes]])
150
+ else:
151
+ axes_2d = np.atleast_2d(mpl_axes)
152
+ if nrows == 1:
153
+ axes_2d = axes_2d.reshape(1, -1)
154
+ elif ncols == 1:
155
+ axes_2d = axes_2d.reshape(-1, 1)
156
+
157
+ # Apply style BEFORE replaying calls (to match original order:
158
+ # style is applied during subplots(), then user creates plots/decorations)
159
+ if record.style is not None:
160
+ from ..styles import apply_style_mm
161
+
162
+ for row in range(nrows):
163
+ for col in range(ncols):
164
+ apply_style_mm(axes_2d[row, col], record.style)
165
+
166
+ # Result cache for resolving references (e.g., clabel needs ContourSet from contour)
167
+ result_cache: Dict[str, Any] = {}
168
+
169
+ # Replay calls on each axes
170
+ for ax_key, ax_record in record.axes.items():
171
+ parts = ax_key.split("_")
172
+ if len(parts) >= 3:
173
+ row, col = int(parts[1]), int(parts[2])
174
+ else:
175
+ row, col = 0, 0
176
+
177
+ ax = axes_2d[row, col]
178
+
179
+ # Replay plotting calls
180
+ for call in ax_record.calls:
181
+ if calls is not None and call.id not in calls:
182
+ continue
183
+ result = _replay_call(ax, call, result_cache)
184
+ if result is not None:
185
+ result_cache[call.id] = result
186
+
187
+ # Replay decorations
188
+ if not skip_decorations:
189
+ for call in ax_record.decorations:
190
+ if calls is not None and call.id not in calls:
191
+ continue
192
+ result = _replay_call(ax, call, result_cache)
193
+ if result is not None:
194
+ result_cache[call.id] = result
195
+
196
+ # Apply panel visibility
197
+ if not getattr(ax_record, "visible", True):
198
+ ax.set_visible(False)
199
+
200
+ # Finalize tick configuration and special plot types (avoids categorical axis interference)
201
+ from ..styles._style_applier import finalize_special_plots, finalize_ticks
202
+
203
+ for row in range(nrows):
204
+ for col in range(ncols):
205
+ finalize_ticks(axes_2d[row, col])
206
+ finalize_special_plots(axes_2d[row, col], record.style or {})
207
+
208
+ # Apply figure-level labels if recorded
209
+ if record.suptitle is not None:
210
+ text = record.suptitle.get("text", "")
211
+ kwargs = record.suptitle.get("kwargs", {}).copy()
212
+ # Only add y=1.02 if not using constrained_layout (which handles positioning)
213
+ if "y" not in kwargs and not record.constrained_layout:
214
+ kwargs["y"] = 1.02
215
+ fig.suptitle(text, **kwargs)
216
+
217
+ if record.supxlabel is not None:
218
+ text = record.supxlabel.get("text", "")
219
+ kwargs = record.supxlabel.get("kwargs", {})
220
+ fig.supxlabel(text, **kwargs)
221
+
222
+ if record.supylabel is not None:
223
+ text = record.supylabel.get("text", "")
224
+ kwargs = record.supylabel.get("kwargs", {})
225
+ fig.supylabel(text, **kwargs)
226
+
227
+ # Wrap in Recording types (same as subplots() returns)
228
+ recorder = Recorder()
229
+ recorder._figure_record = record
230
+
231
+ # Wrap axes in RecordingAxes
232
+ wrapped_axes = np.empty((nrows, ncols), dtype=object)
233
+ for i in range(nrows):
234
+ for j in range(ncols):
235
+ wrapped_axes[i, j] = RecordingAxes(axes_2d[i, j], recorder, position=(i, j))
236
+
237
+ # Create RecordingFigure
238
+ wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes.tolist())
239
+
240
+ # Reproduce panel labels if recorded
241
+ if record.panel_labels is not None:
242
+ labels = record.panel_labels.get("labels")
243
+ loc = record.panel_labels.get("loc", "upper left")
244
+ offset = tuple(record.panel_labels.get("offset", (-0.1, 1.05)))
245
+ fontsize = record.panel_labels.get("fontsize")
246
+ fontweight = record.panel_labels.get("fontweight", "bold")
247
+ color = record.panel_labels.get("color")
248
+ extra_kwargs = record.panel_labels.get("kwargs", {})
249
+ if color is not None:
250
+ extra_kwargs["color"] = color
251
+ wrapped_fig.add_panel_labels(
252
+ labels=labels,
253
+ loc=loc,
254
+ offset=offset,
255
+ fontsize=fontsize,
256
+ fontweight=fontweight,
257
+ **extra_kwargs,
258
+ )
259
+
260
+ # Return in appropriate format (matching subplots() behavior)
261
+ if nrows == 1 and ncols == 1:
262
+ return wrapped_fig, wrapped_axes[0, 0]
263
+ elif nrows == 1:
264
+ return wrapped_fig, np.array(wrapped_axes[0], dtype=object)
265
+ elif ncols == 1:
266
+ return wrapped_fig, np.array(wrapped_axes[:, 0], dtype=object)
267
+ else:
268
+ return wrapped_fig, wrapped_axes
269
+
270
+
271
+ def _replay_call(
272
+ ax: Axes, call: CallRecord, result_cache: Optional[Dict[str, Any]] = None
273
+ ) -> Any:
274
+ """Replay a single call on an axes.
275
+
276
+ Parameters
277
+ ----------
278
+ ax : Axes
279
+ The matplotlib axes.
280
+ call : CallRecord
281
+ The call to replay.
282
+ result_cache : dict, optional
283
+ Cache mapping call_id -> result for resolving references.
284
+
285
+ Returns
286
+ -------
287
+ Any
288
+ Result of the matplotlib call.
289
+ """
290
+ if result_cache is None:
291
+ result_cache = {}
292
+
293
+ method_name = call.function
294
+
295
+ # Check if it's a seaborn call
296
+ if method_name.startswith("sns."):
297
+ from ._seaborn import replay_seaborn_call
298
+
299
+ return replay_seaborn_call(ax, call)
300
+
301
+ # Handle violinplot with inner option specially
302
+ if method_name == "violinplot":
303
+ from ._violin import replay_violinplot_call
304
+
305
+ return replay_violinplot_call(ax, call)
306
+
307
+ # Handle joyplot specially (custom method)
308
+ if method_name == "joyplot":
309
+ from ._custom_plots import replay_joyplot_call
310
+
311
+ return replay_joyplot_call(ax, call)
312
+
313
+ # Handle swarmplot specially (custom method)
314
+ if method_name == "swarmplot":
315
+ from ._custom_plots import replay_swarmplot_call
316
+
317
+ return replay_swarmplot_call(ax, call)
318
+
319
+ # Handle stat_annotation specially (custom method)
320
+ if method_name == "stat_annotation":
321
+ from .._wrappers._stat_annotation import draw_stat_annotation
322
+
323
+ kwargs = call.kwargs.copy()
324
+ x1 = kwargs.pop("x1", 0)
325
+ x2 = kwargs.pop("x2", 1)
326
+ return draw_stat_annotation(ax, x1, x2, **kwargs)
327
+
328
+ method = getattr(ax, method_name, None)
329
+
330
+ if method is None:
331
+ # Method not found, skip
332
+ return None
333
+
334
+ # Reconstruct args
335
+ args = []
336
+ for arg_data in call.args:
337
+ value = _reconstruct_value(arg_data, result_cache)
338
+ args.append(value)
339
+
340
+ # Get kwargs and reconstruct arrays
341
+ kwargs = _reconstruct_kwargs(call.kwargs)
342
+
343
+ # Handle special transform markers
344
+ if "transform" in kwargs:
345
+ transform_val = kwargs["transform"]
346
+ if transform_val == "axes":
347
+ kwargs["transform"] = ax.transAxes
348
+ elif transform_val == "data":
349
+ kwargs["transform"] = ax.transData
350
+ elif transform_val == "figure":
351
+ kwargs["transform"] = ax.figure.transFigure
352
+ # If it's already a Transform object or something else, leave it
353
+
354
+ # Call the method
355
+ try:
356
+ return method(*args, **kwargs)
357
+ except Exception as e:
358
+ # Log warning but continue
359
+ import warnings
360
+
361
+ warnings.warn(f"Failed to replay {method_name}: {e}")
362
+ return None
363
+
364
+
365
+ def _reconstruct_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]:
366
+ """Reconstruct kwargs, converting 2D lists back to numpy arrays.
367
+
368
+ Parameters
369
+ ----------
370
+ kwargs : dict
371
+ Raw kwargs from call record.
372
+
373
+ Returns
374
+ -------
375
+ dict
376
+ Kwargs with arrays properly reconstructed.
377
+ """
378
+ result = {}
379
+ for key, value in kwargs.items():
380
+ # Handle 'colors' parameter specially - must be a list for pie/bar/etc.
381
+ # A single color string like 'red' would be interpreted as ['r','e','d']
382
+ if key == "colors" and isinstance(value, str):
383
+ result[key] = [value]
384
+ elif isinstance(value, list) and len(value) > 0:
385
+ # Check if it's a 2D list (list of lists) - should be numpy array
386
+ if isinstance(value[0], list):
387
+ result[key] = np.array(value)
388
+ else:
389
+ # 1D list - could be array or just list, try to preserve
390
+ result[key] = value
391
+ else:
392
+ result[key] = value
393
+ return result
394
+
395
+
396
+ def _reconstruct_value(
397
+ arg_data: Dict[str, Any], result_cache: Optional[Dict[str, Any]] = None
398
+ ) -> Any:
399
+ """Reconstruct a value from serialized arg data.
400
+
401
+ Parameters
402
+ ----------
403
+ arg_data : dict
404
+ Serialized argument data.
405
+ result_cache : dict, optional
406
+ Cache mapping call_id -> result for resolving references.
407
+
408
+ Returns
409
+ -------
410
+ Any
411
+ Reconstructed value.
412
+ """
413
+ if result_cache is None:
414
+ result_cache = {}
415
+
416
+ # Check if we have a pre-loaded array
417
+ if "_loaded_array" in arg_data:
418
+ return arg_data["_loaded_array"]
419
+
420
+ data = arg_data.get("data")
421
+
422
+ # Check if it's a reference to another call's result (e.g., ContourSet for clabel)
423
+ if isinstance(data, dict) and "__ref__" in data:
424
+ ref_id = data["__ref__"]
425
+ if ref_id in result_cache:
426
+ return result_cache[ref_id]
427
+ else:
428
+ import warnings
429
+
430
+ warnings.warn(f"Could not resolve reference to {ref_id}")
431
+ return None
432
+
433
+ # Check if it's a list of arrays (e.g., boxplot, violinplot)
434
+ if arg_data.get("_is_array_list") and isinstance(data, list):
435
+ dtype = arg_data.get("dtype")
436
+ # Convert each inner list to numpy array
437
+ return [
438
+ np.array(arr_data, dtype=dtype if isinstance(dtype, str) else None)
439
+ for arr_data in data
440
+ ]
441
+
442
+ # If data is a list, convert to numpy array
443
+ if isinstance(data, list):
444
+ dtype = arg_data.get("dtype")
445
+ try:
446
+ return np.array(data, dtype=dtype if dtype else None)
447
+ except (TypeError, ValueError):
448
+ return np.array(data)
449
+
450
+ return data
451
+
452
+
453
+ def get_recipe_info(path: Union[str, Path]) -> Dict[str, Any]:
454
+ """Get information about a recipe without reproducing.
455
+
456
+ Parameters
457
+ ----------
458
+ path : str or Path
459
+ Path to .yaml recipe file.
460
+
461
+ Returns
462
+ -------
463
+ dict
464
+ Recipe information including:
465
+ - id: Figure ID
466
+ - created: Creation timestamp
467
+ - matplotlib_version: Version used
468
+ - figsize: Figure size
469
+ - n_axes: Number of axes
470
+ - calls: List of call IDs
471
+ """
472
+ record = load_recipe(path)
473
+
474
+ all_calls = []
475
+ for ax_record in record.axes.values():
476
+ for call in ax_record.calls:
477
+ all_calls.append(
478
+ {
479
+ "id": call.id,
480
+ "function": call.function,
481
+ "n_args": len(call.args),
482
+ "kwargs": list(call.kwargs.keys()),
483
+ }
484
+ )
485
+ for call in ax_record.decorations:
486
+ all_calls.append(
487
+ {
488
+ "id": call.id,
489
+ "function": call.function,
490
+ "type": "decoration",
491
+ }
492
+ )
493
+
494
+ return {
495
+ "id": record.id,
496
+ "created": record.created,
497
+ "matplotlib_version": record.matplotlib_version,
498
+ "figsize": record.figsize,
499
+ "dpi": record.dpi,
500
+ "n_axes": len(record.axes),
501
+ "calls": all_calls,
502
+ }
503
+
504
+
505
+ __all__ = [
506
+ "reproduce",
507
+ "reproduce_from_record",
508
+ "get_recipe_info",
509
+ ]