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