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
figrecipe/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """figrecipe CLI entry point.
4
+
5
+ Allows running figrecipe as a module:
6
+ python -m figrecipe <command>
7
+ """
8
+
9
+ from ._cli import main
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """API helper modules for figrecipe.
4
+
5
+ This package contains helper functions extracted from the main __init__.py
6
+ to reduce file size and improve maintainability.
7
+ """
8
+
9
+ from ._extract import DECORATION_FUNCS, extract_call_data, to_array
10
+ from ._panel import calculate_panel_position, get_panel_label_fontsize
11
+ from ._save import (
12
+ IMAGE_EXTENSIONS,
13
+ YAML_EXTENSIONS,
14
+ get_save_dpi,
15
+ get_save_transparency,
16
+ resolve_save_paths,
17
+ )
18
+ from ._subplots import (
19
+ _apply_mm_layout_to_figure,
20
+ _apply_style_to_axes,
21
+ _calculate_mm_layout,
22
+ _check_mm_layout,
23
+ _get_mm_value,
24
+ )
25
+
26
+ __all__ = [
27
+ # Subplots helpers
28
+ "_get_mm_value",
29
+ "_check_mm_layout",
30
+ "_calculate_mm_layout",
31
+ "_apply_mm_layout_to_figure",
32
+ "_apply_style_to_axes",
33
+ # Save helpers
34
+ "IMAGE_EXTENSIONS",
35
+ "YAML_EXTENSIONS",
36
+ "resolve_save_paths",
37
+ "get_save_dpi",
38
+ "get_save_transparency",
39
+ # Extract helpers
40
+ "DECORATION_FUNCS",
41
+ "to_array",
42
+ "extract_call_data",
43
+ # Panel helpers
44
+ "get_panel_label_fontsize",
45
+ "calculate_panel_position",
46
+ ]
47
+
48
+ # EOF
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Data extraction helpers for the public API."""
4
+
5
+ from typing import Any, Dict, Set
6
+
7
+ import numpy as np
8
+
9
+ # Decoration functions to skip when extracting data
10
+ DECORATION_FUNCS: Set[str] = {
11
+ "set_xlabel",
12
+ "set_ylabel",
13
+ "set_title",
14
+ "set_xlim",
15
+ "set_ylim",
16
+ "legend",
17
+ "grid",
18
+ "axhline",
19
+ "axvline",
20
+ "text",
21
+ "annotate",
22
+ }
23
+
24
+
25
+ def to_array(data: Any) -> np.ndarray:
26
+ """Convert data to numpy array, handling YAML types.
27
+
28
+ Parameters
29
+ ----------
30
+ data : any
31
+ Data to convert.
32
+
33
+ Returns
34
+ -------
35
+ np.ndarray
36
+ Converted numpy array.
37
+ """
38
+ # Handle dict with 'data' key (serialized array format)
39
+ if isinstance(data, dict) or (hasattr(data, "keys") and "data" in data):
40
+ return np.array(data["data"])
41
+ if hasattr(data, "tolist"): # Already array-like
42
+ return np.array(data)
43
+ return np.array(
44
+ list(data) if hasattr(data, "__iter__") and not isinstance(data, str) else data
45
+ )
46
+
47
+
48
+ def extract_call_data(call) -> Dict[str, Any]:
49
+ """Extract data arrays from a single call record.
50
+
51
+ Parameters
52
+ ----------
53
+ call : CallRecord
54
+ The call to extract data from.
55
+
56
+ Returns
57
+ -------
58
+ dict
59
+ Dictionary with extracted data arrays.
60
+ """
61
+ call_data = {}
62
+
63
+ # Extract positional arguments based on function type
64
+ if call.function in ("plot", "scatter", "fill_between"):
65
+ if len(call.args) >= 1:
66
+ call_data["x"] = to_array(call.args[0])
67
+ if len(call.args) >= 2:
68
+ call_data["y"] = to_array(call.args[1])
69
+
70
+ elif call.function == "bar":
71
+ if len(call.args) >= 1:
72
+ call_data["x"] = to_array(call.args[0])
73
+ if len(call.args) >= 2:
74
+ call_data["height"] = to_array(call.args[1])
75
+
76
+ elif call.function == "hist":
77
+ if len(call.args) >= 1:
78
+ call_data["x"] = to_array(call.args[0])
79
+
80
+ elif call.function == "errorbar":
81
+ if len(call.args) >= 1:
82
+ call_data["x"] = to_array(call.args[0])
83
+ if len(call.args) >= 2:
84
+ call_data["y"] = to_array(call.args[1])
85
+
86
+ # Extract relevant kwargs
87
+ for key in ("c", "s", "yerr", "xerr", "weights", "bins"):
88
+ if key in call.kwargs:
89
+ val = call.kwargs[key]
90
+ if (
91
+ isinstance(val, (list, tuple))
92
+ or hasattr(val, "__iter__")
93
+ and not isinstance(val, str)
94
+ ):
95
+ call_data[key] = to_array(val)
96
+ else:
97
+ call_data[key] = val
98
+
99
+ return call_data
100
+
101
+
102
+ __all__ = [
103
+ "DECORATION_FUNCS",
104
+ "to_array",
105
+ "extract_call_data",
106
+ ]
107
+
108
+ # EOF
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Notebook utilities for figrecipe.
4
+
5
+ Provides SVG rendering for Jupyter notebooks.
6
+ """
7
+
8
+ __all__ = [
9
+ "enable_svg",
10
+ ]
11
+
12
+ # Notebook display format flag (set once per session)
13
+ _notebook_format_set = False
14
+
15
+
16
+ def _enable_notebook_svg():
17
+ """Enable SVG format for Jupyter notebook display.
18
+
19
+ This provides crisp vector graphics at any zoom level.
20
+ Called automatically when load_style() or subplots() is used.
21
+ """
22
+ global _notebook_format_set
23
+ if _notebook_format_set:
24
+ return
25
+
26
+ try:
27
+ # Method 1: matplotlib_inline (IPython 7.0+, JupyterLab)
28
+ from matplotlib_inline.backend_inline import set_matplotlib_formats
29
+
30
+ set_matplotlib_formats("svg")
31
+ _notebook_format_set = True
32
+ except (ImportError, Exception):
33
+ try:
34
+ # Method 2: IPython config (older IPython)
35
+ from IPython import get_ipython
36
+
37
+ ipython = get_ipython()
38
+ if ipython is not None and hasattr(ipython, "kernel"):
39
+ # Only run in actual Jupyter kernel, not IPython console
40
+ ipython.run_line_magic(
41
+ "config", "InlineBackend.figure_formats = ['svg']"
42
+ )
43
+ _notebook_format_set = True
44
+ except Exception:
45
+ pass # Not in Jupyter environment or method not available
46
+
47
+
48
+ def enable_svg():
49
+ """Manually enable SVG format for Jupyter notebook display.
50
+
51
+ Call this if figures appear pixelated in notebooks.
52
+
53
+ Examples
54
+ --------
55
+ >>> import figrecipe as fr
56
+ >>> fr.enable_svg() # Enable SVG rendering
57
+ >>> fig, ax = fr.subplots() # Now renders as crisp SVG
58
+ """
59
+ global _notebook_format_set
60
+ _notebook_format_set = False # Force re-application
61
+ _enable_notebook_svg()
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel label helper for the public API."""
4
+
5
+ from typing import Optional, Tuple
6
+
7
+
8
+ def get_panel_label_fontsize(explicit_fontsize: Optional[float] = None) -> float:
9
+ """Get fontsize for panel labels from style or default."""
10
+ if explicit_fontsize is not None:
11
+ return explicit_fontsize
12
+
13
+ try:
14
+ from ..styles._style_loader import _STYLE_CACHE
15
+
16
+ if _STYLE_CACHE is not None:
17
+ return getattr(getattr(_STYLE_CACHE, "fonts", None), "panel_label_pt", 10)
18
+ except Exception:
19
+ pass
20
+ return 10
21
+
22
+
23
+ def calculate_panel_position(
24
+ loc: str,
25
+ offset: Tuple[float, float],
26
+ ) -> Tuple[float, float]:
27
+ """Calculate x, y position based on location and offset."""
28
+ if loc == "upper left":
29
+ x, y = offset
30
+ elif loc == "upper right":
31
+ x, y = 1.0 + abs(offset[0]), offset[1]
32
+ elif loc == "lower left":
33
+ x, y = offset[0], -abs(offset[1]) + 1.0
34
+ elif loc == "lower right":
35
+ x, y = 1.0 + abs(offset[0]), -abs(offset[1]) + 1.0
36
+ else:
37
+ x, y = offset
38
+ return x, y
39
+
40
+
41
+ def panel_label(
42
+ ax,
43
+ label: str,
44
+ loc: str = "upper left",
45
+ fontsize: Optional[float] = None,
46
+ fontweight: str = "bold",
47
+ offset: Tuple[float, float] = (-0.1, 1.05),
48
+ **kwargs,
49
+ ):
50
+ """Add a panel label (A, B, C, ...) to an axes.
51
+
52
+ Parameters
53
+ ----------
54
+ ax : Axes or RecordingAxes
55
+ The axes to label.
56
+ label : str
57
+ The label text (e.g., 'A', 'B', 'a)', '(1)').
58
+ loc : str, optional
59
+ Label location: 'upper left', 'upper right', etc.
60
+ fontsize : float, optional
61
+ Font size in points.
62
+ fontweight : str, optional
63
+ Font weight (default: 'bold').
64
+ offset : tuple of float, optional
65
+ (x, y) offset in axes coordinates.
66
+ **kwargs
67
+ Additional arguments passed to ax.text().
68
+
69
+ Returns
70
+ -------
71
+ Text
72
+ The matplotlib Text object.
73
+ """
74
+ import matplotlib.pyplot as mpl_plt
75
+
76
+ fontsize = get_panel_label_fontsize(fontsize)
77
+ x, y = calculate_panel_position(loc, offset)
78
+
79
+ default_color = mpl_plt.rcParams.get("text.color", "black")
80
+
81
+ text_kwargs = {
82
+ "fontsize": fontsize,
83
+ "fontweight": fontweight,
84
+ "color": default_color,
85
+ "transform": "axes",
86
+ "va": "bottom",
87
+ "ha": "right" if "right" in loc else "left",
88
+ }
89
+ text_kwargs.update(kwargs)
90
+
91
+ mpl_ax = ax._ax if hasattr(ax, "_ax") else ax
92
+
93
+ render_kwargs = text_kwargs.copy()
94
+ render_kwargs["transform"] = mpl_ax.transAxes
95
+
96
+ if hasattr(ax, "_recorder") and hasattr(ax, "_position"):
97
+ ax._recorder.record_call(
98
+ ax_position=ax._position,
99
+ method_name="text",
100
+ args=(x, y, label),
101
+ kwargs=text_kwargs,
102
+ )
103
+
104
+ return mpl_ax.text(x, y, label, **render_kwargs)
105
+
106
+
107
+ __all__ = [
108
+ "get_panel_label_fontsize",
109
+ "calculate_panel_position",
110
+ "panel_label",
111
+ ]
112
+
113
+ # EOF
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Save function helpers for the public API."""
4
+
5
+ import shutil
6
+ import tempfile
7
+ import zipfile
8
+ from pathlib import Path
9
+ from typing import Optional, Tuple
10
+
11
+ # Image extensions supported for saving
12
+ IMAGE_EXTENSIONS = {
13
+ ".png",
14
+ ".pdf",
15
+ ".svg",
16
+ ".jpg",
17
+ ".jpeg",
18
+ ".eps",
19
+ ".tiff",
20
+ ".tif",
21
+ }
22
+ YAML_EXTENSIONS = {".yaml", ".yml"}
23
+ BUNDLE_RECIPE_NAME = "recipe.yaml"
24
+
25
+
26
+ def resolve_save_paths(
27
+ path: Path,
28
+ image_format: Optional[str] = None,
29
+ ) -> Tuple[Path, Path, str]:
30
+ """Resolve image and YAML paths from the provided path.
31
+
32
+ Parameters
33
+ ----------
34
+ path : Path
35
+ User-provided output path.
36
+ image_format : str, optional
37
+ Explicit image format when path is YAML.
38
+
39
+ Returns
40
+ -------
41
+ tuple
42
+ (image_path, yaml_path, img_format)
43
+ """
44
+ suffix_lower = path.suffix.lower()
45
+
46
+ if suffix_lower in IMAGE_EXTENSIONS:
47
+ # User provided image path
48
+ image_path = path
49
+ yaml_path = path.with_suffix(".yaml")
50
+ img_format = suffix_lower[1:] # Remove leading dot
51
+ elif suffix_lower in YAML_EXTENSIONS:
52
+ # User provided YAML path
53
+ yaml_path = path
54
+ img_format = _get_default_image_format(image_format)
55
+ image_path = path.with_suffix(f".{img_format}")
56
+ else:
57
+ # Unknown extension - treat as base name, add both extensions
58
+ yaml_path = path.with_suffix(".yaml")
59
+ img_format = _get_default_image_format(image_format)
60
+ image_path = path.with_suffix(f".{img_format}")
61
+
62
+ return image_path, yaml_path, img_format
63
+
64
+
65
+ def _get_default_image_format(explicit_format: Optional[str] = None) -> str:
66
+ """Get default image format from style or fallback to png."""
67
+ if explicit_format is not None:
68
+ return explicit_format.lower().lstrip(".")
69
+
70
+ # Check global style for preferred format
71
+ from ..styles._style_loader import _STYLE_CACHE
72
+
73
+ if _STYLE_CACHE is not None:
74
+ try:
75
+ return _STYLE_CACHE.output.format.lower()
76
+ except (KeyError, AttributeError):
77
+ pass
78
+ return "png"
79
+
80
+
81
+ def get_save_dpi(explicit_dpi: Optional[int] = None) -> int:
82
+ """Get DPI for saving, using style default if not specified."""
83
+ if explicit_dpi is not None:
84
+ return explicit_dpi
85
+
86
+ from ..styles._style_loader import _STYLE_CACHE
87
+
88
+ if _STYLE_CACHE is not None:
89
+ try:
90
+ return _STYLE_CACHE.output.dpi
91
+ except (KeyError, AttributeError):
92
+ pass
93
+ return 300
94
+
95
+
96
+ def get_save_transparency() -> bool:
97
+ """Get transparency setting from style."""
98
+ from ..styles._style_loader import _STYLE_CACHE
99
+
100
+ if _STYLE_CACHE is not None:
101
+ try:
102
+ return _STYLE_CACHE.output.transparent
103
+ except (KeyError, AttributeError):
104
+ pass
105
+ return False
106
+
107
+
108
+ def _is_bundle_path(path: Path) -> bool:
109
+ """Check if path represents a bundle (directory or ZIP)."""
110
+ suffix = path.suffix.lower()
111
+ # ZIP file
112
+ if suffix == ".zip":
113
+ return True
114
+ # Existing directory
115
+ if path.is_dir():
116
+ return True
117
+ # Path ending with / (explicit directory)
118
+ if str(path).endswith("/"):
119
+ return True
120
+ # No extension and doesn't look like a file
121
+ if not suffix and not path.exists():
122
+ return True
123
+ return False
124
+
125
+
126
+ def _save_as_bundle(
127
+ fig,
128
+ path: Path,
129
+ include_data: bool,
130
+ data_format: str,
131
+ dpi: int,
132
+ transparent: bool,
133
+ image_format: str,
134
+ verbose: bool,
135
+ ) -> Tuple[Path, Path]:
136
+ """Save figure as a bundle (directory or ZIP)."""
137
+ suffix = path.suffix.lower()
138
+ is_zip = suffix == ".zip"
139
+
140
+ # Create temporary directory for bundle contents
141
+ with tempfile.TemporaryDirectory() as tmpdir:
142
+ tmpdir = Path(tmpdir)
143
+
144
+ # Determine image format
145
+ img_format = image_format or _get_default_image_format()
146
+ image_name = f"figure.{img_format}"
147
+
148
+ # Save image
149
+ image_path = tmpdir / image_name
150
+ fig.fig.savefig(
151
+ image_path, dpi=dpi, bbox_inches="tight", transparent=transparent
152
+ )
153
+
154
+ # Save recipe
155
+ yaml_path = tmpdir / BUNDLE_RECIPE_NAME
156
+ fig.save_recipe(yaml_path, include_data=include_data, data_format=data_format)
157
+
158
+ if is_zip:
159
+ # Create ZIP bundle
160
+ zip_path = path.with_suffix(".zip")
161
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
162
+ zf.write(yaml_path, BUNDLE_RECIPE_NAME)
163
+ zf.write(image_path, image_name)
164
+ if verbose:
165
+ print(f"Saved: {zip_path} (ZIP bundle)")
166
+ return zip_path, zip_path
167
+ else:
168
+ # Create directory bundle
169
+ bundle_dir = Path(str(path).rstrip("/"))
170
+ bundle_dir.mkdir(parents=True, exist_ok=True)
171
+ shutil.copy2(yaml_path, bundle_dir / BUNDLE_RECIPE_NAME)
172
+ shutil.copy2(image_path, bundle_dir / image_name)
173
+ if verbose:
174
+ print(f"Saved: {bundle_dir}/ (directory bundle)")
175
+ return bundle_dir, bundle_dir / BUNDLE_RECIPE_NAME
176
+
177
+
178
+ def save_figure(
179
+ fig,
180
+ path,
181
+ include_data: bool = True,
182
+ data_format: str = "csv",
183
+ validate: bool = True,
184
+ validate_mse_threshold: float = 100.0,
185
+ validate_error_level: str = "error",
186
+ verbose: bool = True,
187
+ dpi: Optional[int] = None,
188
+ image_format: Optional[str] = None,
189
+ ):
190
+ """Core save implementation.
191
+
192
+ Supports multiple output formats:
193
+ - Image file (.png, .pdf, etc.): Saves image + .yaml recipe
194
+ - YAML file (.yaml): Saves recipe + image
195
+ - Directory (path/ or no extension): Saves as bundle directory
196
+ - ZIP file (.zip): Saves as ZIP bundle
197
+ """
198
+ from .._wrappers import RecordingFigure
199
+
200
+ path = Path(path)
201
+
202
+ if not isinstance(fig, RecordingFigure):
203
+ raise TypeError(
204
+ "Expected RecordingFigure. Use fr.subplots() to create "
205
+ "a recording-enabled figure."
206
+ )
207
+
208
+ # Get DPI and transparency from style if not specified
209
+ dpi = get_save_dpi(dpi)
210
+ transparent = get_save_transparency()
211
+
212
+ # Finalize tick configuration and special plot types for all axes
213
+ from ..styles._style_applier import finalize_special_plots, finalize_ticks
214
+
215
+ # Get style for special plot finalization
216
+ style_dict = {}
217
+ if hasattr(fig, "style") and fig.style:
218
+ from ..styles import get_style
219
+
220
+ style_dict = get_style(fig.style)
221
+
222
+ for ax in fig.fig.get_axes():
223
+ finalize_ticks(ax)
224
+ finalize_special_plots(ax, style_dict)
225
+
226
+ # Check if saving as bundle
227
+ if _is_bundle_path(path):
228
+ bundle_path, yaml_path = _save_as_bundle(
229
+ fig,
230
+ path,
231
+ include_data,
232
+ data_format,
233
+ dpi,
234
+ transparent,
235
+ image_format or _get_default_image_format(),
236
+ verbose,
237
+ )
238
+ # No validation for bundles (yet)
239
+ return bundle_path, yaml_path, None
240
+
241
+ # Resolve paths for standard save
242
+ image_path, yaml_path, _ = resolve_save_paths(path, image_format)
243
+
244
+ # Save the image
245
+ fig.fig.savefig(image_path, dpi=dpi, bbox_inches="tight", transparent=transparent)
246
+
247
+ # Save the recipe
248
+ saved_yaml = fig.save_recipe(
249
+ yaml_path, include_data=include_data, data_format=data_format
250
+ )
251
+
252
+ # Validate if requested
253
+ if validate:
254
+ from .._validator import validate_on_save
255
+
256
+ result = validate_on_save(fig, saved_yaml, mse_threshold=validate_mse_threshold)
257
+ status = "PASSED" if result.valid else "FAILED"
258
+ if verbose:
259
+ print(
260
+ f"Saved: {image_path} + {yaml_path} (Reproducible Validation: {status})"
261
+ )
262
+ if not result.valid:
263
+ msg = f"Reproducibility validation failed (MSE={result.mse:.1f}): {result.message}"
264
+ if validate_error_level == "error":
265
+ raise ValueError(msg)
266
+ elif validate_error_level == "warning":
267
+ import warnings
268
+
269
+ warnings.warn(msg, UserWarning)
270
+ # "debug" level: silent, just return the result
271
+ return image_path, yaml_path, result
272
+
273
+ if verbose:
274
+ print(f"Saved: {image_path} + {yaml_path}")
275
+ return image_path, yaml_path, None
276
+
277
+
278
+ __all__ = [
279
+ "IMAGE_EXTENSIONS",
280
+ "YAML_EXTENSIONS",
281
+ "resolve_save_paths",
282
+ "get_save_dpi",
283
+ "get_save_transparency",
284
+ "save_figure",
285
+ ]
286
+
287
+ # EOF
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Seaborn proxy for figrecipe.
4
+
5
+ Provides lazy seaborn integration via ps.sns.
6
+ """
7
+
8
+ __all__ = [
9
+ "sns",
10
+ ]
11
+
12
+ # Lazy import for seaborn to avoid hard dependency
13
+ _sns_recorder = None
14
+
15
+
16
+ def _get_sns():
17
+ """Get the seaborn recorder (lazy initialization)."""
18
+ global _sns_recorder
19
+ if _sns_recorder is None:
20
+ from .._seaborn import get_seaborn_recorder
21
+
22
+ _sns_recorder = get_seaborn_recorder()
23
+ return _sns_recorder
24
+
25
+
26
+ class _SeabornProxy:
27
+ """Proxy object for seaborn access via ps.sns."""
28
+
29
+ def __getattr__(self, name: str):
30
+ return getattr(_get_sns(), name)
31
+
32
+
33
+ # Create seaborn proxy
34
+ sns = _SeabornProxy()