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,298 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Integration with scitex.stats for statistical results."""
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ # Check if scitex.stats is available
9
+ try:
10
+ from scitex import stats as scitex_stats
11
+
12
+ SCITEX_STATS_AVAILABLE = True
13
+ except ImportError:
14
+ scitex_stats = None
15
+ SCITEX_STATS_AVAILABLE = False
16
+
17
+
18
+ def from_scitex_stats(
19
+ stats_result: Union[Dict[str, Any], List[Dict[str, Any]]],
20
+ ) -> Dict[str, Any]:
21
+ """Convert scitex.stats result(s) to figrecipe stats format.
22
+
23
+ Parameters
24
+ ----------
25
+ stats_result : dict or list of dict
26
+ Statistical result(s) from scitex.stats. Supports:
27
+ - Single comparison dict
28
+ - List of comparison dicts
29
+ - scitex.stats flat format: {name, method, p_value, effect_size, ci95}
30
+ - scitex.stats nested format: {method: {name}, results: {p_value}}
31
+
32
+ Returns
33
+ -------
34
+ dict
35
+ Figrecipe-compatible stats dict with 'comparisons' list.
36
+
37
+ Examples
38
+ --------
39
+ >>> from scitex import stats
40
+ >>> result = stats.ttest_ind(x, y)
41
+ >>> fr_stats = from_scitex_stats(result)
42
+ >>> fig.set_stats(fr_stats)
43
+
44
+ >>> # Or for multiple comparisons
45
+ >>> results = [stats.ttest_ind(a, b), stats.ttest_ind(a, c)]
46
+ >>> fr_stats = from_scitex_stats(results)
47
+ """
48
+ # Normalize to list
49
+ if isinstance(stats_result, dict):
50
+ results = [stats_result]
51
+ else:
52
+ results = list(stats_result)
53
+
54
+ comparisons = []
55
+ for result in results:
56
+ comp = _convert_single_result(result)
57
+ if comp:
58
+ comparisons.append(comp)
59
+
60
+ return {"comparisons": comparisons}
61
+
62
+
63
+ def _convert_single_result(result: Dict[str, Any]) -> Dict[str, Any]:
64
+ """Convert a single scitex.stats result to figrecipe format."""
65
+ # Handle flat format (legacy or from load_statsz)
66
+ if "p_value" in result and "results" not in result:
67
+ return _convert_flat_format(result)
68
+
69
+ # Handle nested format (from test functions)
70
+ if "results" in result:
71
+ return _convert_nested_format(result)
72
+
73
+ # Handle already-converted format
74
+ if "comparisons" in result:
75
+ # Already in figrecipe format, return first comparison
76
+ comps = result.get("comparisons", [])
77
+ return comps[0] if comps else {}
78
+
79
+ return {}
80
+
81
+
82
+ def _convert_flat_format(result: Dict[str, Any]) -> Dict[str, Any]:
83
+ """Convert flat scitex.stats format."""
84
+ p_value = result.get("p_value")
85
+ stars = result.get("formatted") or result.get("stars")
86
+ if not stars and p_value is not None:
87
+ stars = _p_to_stars(p_value)
88
+
89
+ comp = {
90
+ "name": result.get("name", "comparison"),
91
+ "p_value": p_value,
92
+ "stars": stars,
93
+ "method": result.get("method", ""),
94
+ }
95
+
96
+ # Handle effect size
97
+ es = result.get("effect_size")
98
+ if es is not None:
99
+ ci = result.get("ci95", [])
100
+ if isinstance(es, (int, float)):
101
+ comp["effect_size"] = {
102
+ "name": "d",
103
+ "value": float(es),
104
+ }
105
+ if len(ci) >= 2:
106
+ comp["effect_size"]["ci_lower"] = ci[0]
107
+ comp["effect_size"]["ci_upper"] = ci[1]
108
+ elif isinstance(es, dict):
109
+ comp["effect_size"] = es
110
+
111
+ return comp
112
+
113
+
114
+ def _convert_nested_format(result: Dict[str, Any]) -> Dict[str, Any]:
115
+ """Convert nested scitex.stats format."""
116
+ method_data = result.get("method", {})
117
+ results_data = result.get("results", {})
118
+
119
+ method_name = (
120
+ method_data.get("name", "")
121
+ if isinstance(method_data, dict)
122
+ else str(method_data)
123
+ )
124
+ p_value = results_data.get("p_value")
125
+ stars = _p_to_stars(p_value) if p_value is not None else ""
126
+
127
+ comp = {
128
+ "name": result.get("name", "comparison"),
129
+ "p_value": p_value,
130
+ "stars": stars,
131
+ "method": method_name,
132
+ }
133
+
134
+ # Handle effect size from results
135
+ es_data = results_data.get("effect_size")
136
+ if es_data:
137
+ if isinstance(es_data, dict):
138
+ comp["effect_size"] = es_data
139
+ else:
140
+ comp["effect_size"] = {"name": "d", "value": float(es_data)}
141
+
142
+ return comp
143
+
144
+
145
+ def _p_to_stars(p_value: float, ns_symbol: bool = True) -> str:
146
+ """Convert p-value to significance stars."""
147
+ if p_value < 0.001:
148
+ return "***"
149
+ elif p_value < 0.01:
150
+ return "**"
151
+ elif p_value < 0.05:
152
+ return "*"
153
+ return "ns" if ns_symbol else ""
154
+
155
+
156
+ def load_stats_bundle(path: Union[str, Path]) -> Dict[str, Any]:
157
+ """Load stats from a scitex.stats bundle file.
158
+
159
+ Parameters
160
+ ----------
161
+ path : str or Path
162
+ Path to .statsz or .zip bundle file.
163
+
164
+ Returns
165
+ -------
166
+ dict
167
+ Figrecipe-compatible stats dict.
168
+
169
+ Raises
170
+ ------
171
+ ImportError
172
+ If scitex.stats is not installed.
173
+ """
174
+ if not SCITEX_STATS_AVAILABLE:
175
+ raise ImportError(
176
+ "scitex.stats is required for bundle loading. "
177
+ "Install with: pip install scitex[stats]"
178
+ )
179
+
180
+ data = scitex_stats.load_statsz(path)
181
+ comparisons = data.get("comparisons", [])
182
+
183
+ # Convert each comparison
184
+ return from_scitex_stats(comparisons)
185
+
186
+
187
+ def annotate_from_stats(
188
+ ax,
189
+ stats: Dict[str, Any],
190
+ positions: Optional[Dict[str, float]] = None,
191
+ style: str = "stars",
192
+ **kwargs,
193
+ ) -> List[Any]:
194
+ """Add stat annotations to axes from stats dict.
195
+
196
+ Parameters
197
+ ----------
198
+ ax : RecordingAxes
199
+ The axes to annotate.
200
+ stats : dict
201
+ Stats dict with 'comparisons' list. Each comparison should have:
202
+ - name: "Group A vs Group B" (parsed for group names)
203
+ - p_value: float
204
+ - Optional: groups: ["Group A", "Group B"]
205
+ positions : dict, optional
206
+ Mapping of group names to x positions. If None, uses 0, 1, 2, ...
207
+ style : str
208
+ Annotation style: "stars", "p_value", "both".
209
+ **kwargs
210
+ Additional arguments passed to add_stat_annotation().
211
+
212
+ Returns
213
+ -------
214
+ list
215
+ List of artist objects created.
216
+
217
+ Examples
218
+ --------
219
+ >>> stats = from_scitex_stats(result)
220
+ >>> annotate_from_stats(ax, stats, positions={"Control": 0, "Treatment": 1})
221
+ """
222
+ comparisons = stats.get("comparisons", [])
223
+ if not comparisons:
224
+ return []
225
+
226
+ artists = []
227
+ y_offset = 0
228
+
229
+ for comp in comparisons:
230
+ # Get positions for this comparison
231
+ x1, x2 = _get_comparison_positions(comp, positions)
232
+ if x1 is None or x2 is None:
233
+ continue
234
+
235
+ # Calculate y position (stack annotations)
236
+ y = kwargs.pop("y", None)
237
+ if y is None:
238
+ # Auto-calculate based on data
239
+ ylim = ax.get_ylim() if hasattr(ax, "get_ylim") else (0, 1)
240
+ y = ylim[1] + (ylim[1] - ylim[0]) * 0.05 * (1 + y_offset)
241
+ y_offset += 1
242
+
243
+ # Add annotation
244
+ result = ax.add_stat_annotation(
245
+ x1,
246
+ x2,
247
+ p_value=comp.get("p_value"),
248
+ text=comp.get("stars") if style == "stars" else None,
249
+ y=y,
250
+ style=style,
251
+ id=comp.get("name", "").replace(" ", "_"),
252
+ **kwargs,
253
+ )
254
+ artists.extend(result if isinstance(result, list) else [result])
255
+
256
+ return artists
257
+
258
+
259
+ def _get_comparison_positions(
260
+ comp: Dict[str, Any],
261
+ positions: Optional[Dict[str, float]],
262
+ ) -> tuple:
263
+ """Extract x positions for a comparison."""
264
+ # Try explicit groups
265
+ groups = comp.get("groups", [])
266
+ if len(groups) >= 2 and positions:
267
+ x1 = positions.get(groups[0])
268
+ x2 = positions.get(groups[1])
269
+ if x1 is not None and x2 is not None:
270
+ return x1, x2
271
+
272
+ # Try parsing from name (e.g., "Control vs Treatment")
273
+ name = comp.get("name", "")
274
+ if " vs " in name:
275
+ parts = name.split(" vs ")
276
+ if len(parts) >= 2 and positions:
277
+ x1 = positions.get(parts[0].strip())
278
+ x2 = positions.get(parts[1].strip())
279
+ if x1 is not None and x2 is not None:
280
+ return x1, x2
281
+
282
+ # Try x1, x2 directly in comparison
283
+ if "x1" in comp and "x2" in comp:
284
+ return comp["x1"], comp["x2"]
285
+
286
+ # Default to sequential positions
287
+ if positions is None:
288
+ return 0, 1
289
+
290
+ return None, None
291
+
292
+
293
+ __all__ = [
294
+ "SCITEX_STATS_AVAILABLE",
295
+ "from_scitex_stats",
296
+ "load_stats_bundle",
297
+ "annotate_from_stats",
298
+ ]
@@ -22,6 +22,14 @@ DECORATION_METHODS = {
22
22
  "text",
23
23
  "annotate",
24
24
  "clabel",
25
+ "axis", # For axis('off'), axis('on'), axis('equal'), etc.
26
+ "set_xticks",
27
+ "set_yticks",
28
+ "set_xticklabels",
29
+ "set_yticklabels",
30
+ "tick_params",
31
+ # Statistical annotations
32
+ "stat_annotation", # Comparison brackets with stars/p-values
25
33
  }
26
34
 
27
35
  # EOF
figrecipe/_recorder.py CHANGED
@@ -25,16 +25,21 @@ class CallRecord:
25
25
  kwargs: Dict[str, Any]
26
26
  timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
27
27
  ax_position: Tuple[int, int] = (0, 0)
28
+ # Statistics associated with this plot call (e.g., n, mean, sem)
29
+ stats: Optional[Dict[str, Any]] = None
28
30
 
29
31
  def to_dict(self) -> Dict[str, Any]:
30
32
  """Convert to dictionary for serialization."""
31
- return {
33
+ result = {
32
34
  "id": self.id,
33
35
  "function": self.function,
34
36
  "args": self.args,
35
37
  "kwargs": self.kwargs,
36
38
  "timestamp": self.timestamp,
37
39
  }
40
+ if self.stats is not None:
41
+ result["stats"] = self.stats
42
+ return result
38
43
 
39
44
  @classmethod
40
45
  def from_dict(
@@ -48,6 +53,7 @@ class CallRecord:
48
53
  kwargs=data["kwargs"],
49
54
  timestamp=data.get("timestamp", ""),
50
55
  ax_position=ax_position,
56
+ stats=data.get("stats"),
51
57
  )
52
58
 
53
59
 
@@ -58,6 +64,12 @@ class AxesRecord:
58
64
  position: Tuple[int, int]
59
65
  calls: List[CallRecord] = field(default_factory=list)
60
66
  decorations: List[CallRecord] = field(default_factory=list)
67
+ # Panel-level caption (e.g., "(A) Description of this panel")
68
+ caption: Optional[str] = None
69
+ # Panel-level statistics (e.g., summary stats, comparison results)
70
+ stats: Optional[Dict[str, Any]] = None
71
+ # Panel visibility (for composition)
72
+ visible: bool = True
61
73
 
62
74
  def add_call(self, record: CallRecord) -> None:
63
75
  """Add a plotting call record."""
@@ -69,10 +81,17 @@ class AxesRecord:
69
81
 
70
82
  def to_dict(self) -> Dict[str, Any]:
71
83
  """Convert to dictionary for serialization."""
72
- return {
84
+ result = {
73
85
  "calls": [c.to_dict() for c in self.calls],
74
86
  "decorations": [d.to_dict() for d in self.decorations],
75
87
  }
88
+ if self.caption is not None:
89
+ result["caption"] = self.caption
90
+ if self.stats is not None:
91
+ result["stats"] = self.stats
92
+ if not self.visible: # Only serialize if hidden (default is True)
93
+ result["visible"] = False
94
+ return result
76
95
 
77
96
 
78
97
  @dataclass
@@ -95,6 +114,13 @@ class FigureRecord:
95
114
  suptitle: Optional[Dict[str, Any]] = None
96
115
  supxlabel: Optional[Dict[str, Any]] = None
97
116
  supylabel: Optional[Dict[str, Any]] = None
117
+ # Panel labels (A, B, C, D for multi-panel figures)
118
+ panel_labels: Optional[Dict[str, Any]] = None
119
+ # Metadata for scientific figures (not rendered, stored in recipe)
120
+ title_metadata: Optional[str] = None # Figure title for publication/reference
121
+ caption: Optional[str] = None # Figure caption (e.g., "Fig. 1. Description...")
122
+ # Figure-level statistics (e.g., comparisons across panels, summary)
123
+ stats: Optional[Dict[str, Any]] = None
98
124
 
99
125
  def get_axes_key(self, row: int, col: int) -> str:
100
126
  """Get dictionary key for axes at position."""
@@ -138,12 +164,26 @@ class FigureRecord:
138
164
  # Add supylabel if set
139
165
  if self.supylabel is not None:
140
166
  result["figure"]["supylabel"] = self.supylabel
167
+ # Add panel_labels if set
168
+ if self.panel_labels is not None:
169
+ result["figure"]["panel_labels"] = self.panel_labels
170
+ # Add metadata section for scientific figures
171
+ metadata = {}
172
+ if self.title_metadata is not None:
173
+ metadata["title"] = self.title_metadata
174
+ if self.caption is not None:
175
+ metadata["caption"] = self.caption
176
+ if self.stats is not None:
177
+ metadata["stats"] = self.stats
178
+ if metadata:
179
+ result["metadata"] = metadata
141
180
  return result
142
181
 
143
182
  @classmethod
144
183
  def from_dict(cls, data: Dict[str, Any]) -> "FigureRecord":
145
184
  """Create from dictionary."""
146
185
  fig_data = data.get("figure", {})
186
+ metadata = data.get("metadata", {})
147
187
  record = cls(
148
188
  id=data.get("id", f"fig_{uuid.uuid4().hex[:8]}"),
149
189
  created=data.get("created", ""),
@@ -156,6 +196,10 @@ class FigureRecord:
156
196
  suptitle=fig_data.get("suptitle"),
157
197
  supxlabel=fig_data.get("supxlabel"),
158
198
  supylabel=fig_data.get("supylabel"),
199
+ panel_labels=fig_data.get("panel_labels"),
200
+ title_metadata=metadata.get("title"),
201
+ caption=metadata.get("caption"),
202
+ stats=metadata.get("stats"),
159
203
  )
160
204
 
161
205
  # Reconstruct axes
@@ -167,7 +211,12 @@ class FigureRecord:
167
211
  else:
168
212
  row, col = 0, 0
169
213
 
170
- ax_record = AxesRecord(position=(row, col))
214
+ ax_record = AxesRecord(
215
+ position=(row, col),
216
+ caption=ax_data.get("caption"),
217
+ stats=ax_data.get("stats"),
218
+ visible=ax_data.get("visible", True),
219
+ )
171
220
  for call_data in ax_data.get("calls", []):
172
221
  ax_record.calls.append(CallRecord.from_dict(call_data, (row, col)))
173
222
  for dec_data in ax_data.get("decorations", []):
@@ -243,6 +292,9 @@ class Recorder:
243
292
  if call_id is None:
244
293
  call_id = self._generate_call_id(method_name)
245
294
 
295
+ # Extract stats from kwargs before processing (stats is metadata, not matplotlib arg)
296
+ call_stats = kwargs.pop("stats", None) if "stats" in kwargs else None
297
+
246
298
  # Process args into serializable format
247
299
  processed_args = self._process_args(args, method_name)
248
300
 
@@ -255,6 +307,7 @@ class Recorder:
255
307
  args=processed_args,
256
308
  kwargs=processed_kwargs,
257
309
  ax_position=ax_position,
310
+ stats=call_stats,
258
311
  )
259
312
 
260
313
  # Add to appropriate axes
@@ -272,111 +325,12 @@ class Recorder:
272
325
  args: tuple,
273
326
  method_name: str,
274
327
  ) -> List[Dict[str, Any]]:
275
- """Process positional arguments for storage.
276
-
277
- Parameters
278
- ----------
279
- args : tuple
280
- Raw positional arguments.
281
- method_name : str
282
- Name of the method.
328
+ """Process positional arguments for storage."""
329
+ from ._recorder_utils import process_args
283
330
 
284
- Returns
285
- -------
286
- list
287
- Processed args with name and data.
288
- """
289
- from ._utils._numpy_io import should_store_inline, to_serializable
290
-
291
- processed = []
292
- # Simple arg names based on common patterns
293
- arg_names = self._get_arg_names(method_name, len(args))
294
-
295
- for i, (name, value) in enumerate(zip(arg_names, args)):
296
- # Handle result references (e.g., ContourSet for clabel)
297
- if isinstance(value, dict) and "__ref__" in value:
298
- processed.append(
299
- {
300
- "name": name,
301
- "data": {"__ref__": value["__ref__"]},
302
- }
303
- )
304
- continue
305
-
306
- if isinstance(value, np.ndarray):
307
- if should_store_inline(value):
308
- processed.append(
309
- {
310
- "name": name,
311
- "data": to_serializable(value),
312
- "dtype": str(value.dtype),
313
- }
314
- )
315
- else:
316
- # Mark for file storage (will be handled by serializer)
317
- processed.append(
318
- {
319
- "name": name,
320
- "data": "__FILE__",
321
- "dtype": str(value.dtype),
322
- "_array": value, # Temporary, removed during serialization
323
- }
324
- )
325
- elif hasattr(value, "values"): # pandas
326
- arr = np.asarray(value)
327
- if should_store_inline(arr):
328
- processed.append(
329
- {
330
- "name": name,
331
- "data": to_serializable(arr),
332
- "dtype": str(arr.dtype),
333
- }
334
- )
335
- else:
336
- processed.append(
337
- {
338
- "name": name,
339
- "data": "__FILE__",
340
- "dtype": str(arr.dtype),
341
- "_array": arr,
342
- }
343
- )
344
- elif (
345
- isinstance(value, (list, tuple))
346
- and len(value) > 0
347
- and isinstance(value[0], np.ndarray)
348
- ):
349
- # List of arrays (e.g., boxplot, violinplot data)
350
- arrays_data = [to_serializable(arr) for arr in value]
351
- dtypes = [str(arr.dtype) for arr in value]
352
- processed.append(
353
- {
354
- "name": name,
355
- "data": arrays_data,
356
- "dtype": (dtypes[0] if len(set(dtypes)) == 1 else dtypes),
357
- "_is_array_list": True,
358
- }
359
- )
360
- else:
361
- # Scalar or other serializable value
362
- try:
363
- processed.append(
364
- {
365
- "name": name,
366
- "data": (
367
- value if self._is_serializable(value) else str(value)
368
- ),
369
- }
370
- )
371
- except (TypeError, ValueError):
372
- processed.append(
373
- {
374
- "name": name,
375
- "data": str(value),
376
- }
377
- )
378
-
379
- return processed
331
+ return process_args(
332
+ args, method_name, self._get_arg_names, self._is_serializable
333
+ )
380
334
 
381
335
  def _get_arg_names(self, method_name: str, n_args: int) -> List[str]:
382
336
  """Get argument names for a method from signatures.
@@ -436,8 +390,8 @@ class Recorder:
436
390
  except Exception:
437
391
  pass
438
392
 
439
- # Remove internal keys
440
- skip_keys = {"id", "track", "_array"}
393
+ # Remove internal keys (stats is handled separately as metadata)
394
+ skip_keys = {"id", "track", "_array", "stats"}
441
395
  processed = {}
442
396
 
443
397
  for key, value in kwargs.items():
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Utilities for recorder argument processing."""
4
+
5
+ from typing import Any, Dict, List
6
+
7
+ import numpy as np
8
+
9
+
10
+ def process_args(
11
+ args: tuple,
12
+ method_name: str,
13
+ get_arg_names_func,
14
+ is_serializable_func,
15
+ ) -> List[Dict[str, Any]]:
16
+ """Process positional arguments for storage.
17
+
18
+ Parameters
19
+ ----------
20
+ args : tuple
21
+ Raw positional arguments.
22
+ method_name : str
23
+ Name of the method.
24
+ get_arg_names_func : callable
25
+ Function to get argument names.
26
+ is_serializable_func : callable
27
+ Function to check serializability.
28
+
29
+ Returns
30
+ -------
31
+ list
32
+ Processed args with name and data.
33
+ """
34
+ from ._utils._numpy_io import should_store_inline, to_serializable
35
+
36
+ processed = []
37
+ arg_names = get_arg_names_func(method_name, len(args))
38
+
39
+ for name, value in zip(arg_names, args):
40
+ processed_arg = _process_single_arg(
41
+ name, value, should_store_inline, to_serializable, is_serializable_func
42
+ )
43
+ processed.append(processed_arg)
44
+
45
+ return processed
46
+
47
+
48
+ def _process_single_arg(
49
+ name: str,
50
+ value: Any,
51
+ should_store_inline,
52
+ to_serializable,
53
+ is_serializable_func,
54
+ ) -> Dict[str, Any]:
55
+ """Process a single argument value."""
56
+ # Handle result references (e.g., ContourSet for clabel)
57
+ if isinstance(value, dict) and "__ref__" in value:
58
+ return {"name": name, "data": {"__ref__": value["__ref__"]}}
59
+
60
+ if isinstance(value, np.ndarray):
61
+ return _process_ndarray(name, value, should_store_inline, to_serializable)
62
+
63
+ if hasattr(value, "values"): # pandas
64
+ arr = np.asarray(value)
65
+ return _process_ndarray(name, arr, should_store_inline, to_serializable)
66
+
67
+ if (
68
+ isinstance(value, (list, tuple))
69
+ and len(value) > 0
70
+ and isinstance(value[0], np.ndarray)
71
+ ):
72
+ # List of arrays (e.g., boxplot, violinplot data)
73
+ return _process_array_list(name, value, to_serializable)
74
+
75
+ # Scalar or other serializable value
76
+ return _process_scalar(name, value, is_serializable_func)
77
+
78
+
79
+ def _process_ndarray(
80
+ name: str, value: np.ndarray, should_store_inline, to_serializable
81
+ ) -> Dict[str, Any]:
82
+ """Process numpy array argument."""
83
+ if should_store_inline(value):
84
+ return {
85
+ "name": name,
86
+ "data": to_serializable(value),
87
+ "dtype": str(value.dtype),
88
+ }
89
+ else:
90
+ # Mark for file storage (will be handled by serializer)
91
+ return {
92
+ "name": name,
93
+ "data": "__FILE__",
94
+ "dtype": str(value.dtype),
95
+ "_array": value, # Temporary, removed during serialization
96
+ }
97
+
98
+
99
+ def _process_array_list(name: str, value: list, to_serializable) -> Dict[str, Any]:
100
+ """Process list of arrays argument."""
101
+ arrays_data = [to_serializable(arr) for arr in value]
102
+ dtypes = [str(arr.dtype) for arr in value]
103
+ return {
104
+ "name": name,
105
+ "data": arrays_data,
106
+ "dtype": (dtypes[0] if len(set(dtypes)) == 1 else dtypes),
107
+ "_is_array_list": True,
108
+ }
109
+
110
+
111
+ def _process_scalar(name: str, value: Any, is_serializable_func) -> Dict[str, Any]:
112
+ """Process scalar or other value."""
113
+ try:
114
+ return {
115
+ "name": name,
116
+ "data": value if is_serializable_func(value) else str(value),
117
+ }
118
+ except (TypeError, ValueError):
119
+ return {"name": name, "data": str(value)}
120
+
121
+
122
+ __all__ = ["process_args"]
123
+
124
+ # EOF