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,218 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Caption generation utilities for scientific figures."""
4
+
5
+ from typing import Any, Dict, List, Literal, Optional
6
+
7
+ from ._stat_annotation import p_to_stars
8
+
9
+
10
+ def format_stats_value(value: float, precision: int = 2) -> str:
11
+ """Format a statistical value for display."""
12
+ if abs(value) >= 1000 or (abs(value) < 0.01 and value != 0):
13
+ return f"{value:.{precision}e}"
14
+ return f"{value:.{precision}f}"
15
+
16
+
17
+ def format_comparison(comp: Dict[str, Any], style: str = "publication") -> str:
18
+ """Format a single comparison result.
19
+
20
+ Parameters
21
+ ----------
22
+ comp : dict
23
+ Comparison dict with keys like: name, p_value, stars, effect_size, method.
24
+ style : str
25
+ "publication", "brief", or "detailed".
26
+
27
+ Returns
28
+ -------
29
+ str
30
+ Formatted comparison string.
31
+ """
32
+ name = comp.get("name", "comparison")
33
+ p_value = comp.get("p_value")
34
+ stars = comp.get("stars") or (p_to_stars(p_value) if p_value else "")
35
+ method = comp.get("method", "")
36
+ effect_size = comp.get("effect_size")
37
+
38
+ if style == "brief":
39
+ if p_value is not None:
40
+ return f"{name}: {stars}"
41
+ return name
42
+
43
+ elif style == "detailed":
44
+ parts = [name]
45
+ if method:
46
+ parts.append(f"({method})")
47
+ if p_value is not None:
48
+ if p_value < 0.001:
49
+ parts.append("p<0.001")
50
+ else:
51
+ parts.append(f"p={p_value:.3f}")
52
+ if effect_size:
53
+ if isinstance(effect_size, dict):
54
+ es_name = effect_size.get("name", "d")
55
+ es_val = effect_size.get("value", 0)
56
+ parts.append(f"{es_name}={es_val:.2f}")
57
+ else:
58
+ parts.append(f"d={effect_size:.2f}")
59
+ return " ".join(parts)
60
+
61
+ else: # publication
62
+ if p_value is not None:
63
+ if p_value < 0.001:
64
+ p_str = "p<0.001"
65
+ else:
66
+ p_str = f"p={p_value:.3f}"
67
+ if effect_size:
68
+ if isinstance(effect_size, dict):
69
+ es_val = effect_size.get("value", 0)
70
+ else:
71
+ es_val = effect_size
72
+ return f"{name} ({p_str}, d={es_val:.2f})"
73
+ return f"{name} ({p_str})"
74
+ return name
75
+
76
+
77
+ def format_panel_stats(stats: Dict[str, Any], style: str = "publication") -> str:
78
+ """Format panel-level statistics.
79
+
80
+ Parameters
81
+ ----------
82
+ stats : dict
83
+ Panel stats dict with keys like: n, mean, std, sem, group.
84
+ style : str
85
+ "publication", "brief", or "detailed".
86
+
87
+ Returns
88
+ -------
89
+ str
90
+ Formatted stats string.
91
+ """
92
+ parts = []
93
+
94
+ group = stats.get("group")
95
+ if group:
96
+ parts.append(group)
97
+
98
+ n = stats.get("n")
99
+ if n is not None:
100
+ parts.append(f"n={n}")
101
+
102
+ mean = stats.get("mean")
103
+ std = stats.get("std")
104
+ sem = stats.get("sem")
105
+
106
+ if mean is not None:
107
+ if std is not None:
108
+ parts.append(f"mean={format_stats_value(mean)}±{format_stats_value(std)}")
109
+ elif sem is not None:
110
+ parts.append(
111
+ f"mean={format_stats_value(mean)}±{format_stats_value(sem)} SEM"
112
+ )
113
+ else:
114
+ parts.append(f"mean={format_stats_value(mean)}")
115
+
116
+ return ", ".join(parts) if parts else ""
117
+
118
+
119
+ def generate_figure_caption(
120
+ title: Optional[str] = None,
121
+ panel_captions: Optional[List[str]] = None,
122
+ stats: Optional[Dict[str, Any]] = None,
123
+ style: Literal["publication", "brief", "detailed"] = "publication",
124
+ template: Optional[str] = None,
125
+ ) -> str:
126
+ """Generate a figure caption from components.
127
+
128
+ Parameters
129
+ ----------
130
+ title : str, optional
131
+ Figure title.
132
+ panel_captions : list of str, optional
133
+ List of panel captions.
134
+ stats : dict, optional
135
+ Figure-level stats with comparisons.
136
+ style : str
137
+ Caption style.
138
+ template : str, optional
139
+ Custom template with placeholders: {title}, {panels}, {stats}.
140
+
141
+ Returns
142
+ -------
143
+ str
144
+ Generated caption.
145
+ """
146
+ # Build components
147
+ title_str = title or ""
148
+
149
+ # Panel descriptions
150
+ panels_str = ""
151
+ if panel_captions:
152
+ panels_str = " ".join(p for p in panel_captions if p)
153
+
154
+ # Stats summary
155
+ stats_str = ""
156
+ if stats:
157
+ comparisons = stats.get("comparisons", [])
158
+ if comparisons:
159
+ formatted = [format_comparison(c, style) for c in comparisons]
160
+ stats_str = "; ".join(formatted)
161
+
162
+ # Apply template
163
+ if template:
164
+ return template.format(
165
+ title=title_str,
166
+ panels=panels_str,
167
+ stats=stats_str,
168
+ ).strip()
169
+
170
+ # Default formatting based on style
171
+ parts = []
172
+ if title_str:
173
+ parts.append(title_str + ".")
174
+
175
+ if panels_str:
176
+ parts.append(panels_str)
177
+
178
+ if stats_str:
179
+ parts.append(stats_str + ".")
180
+
181
+ return " ".join(parts).strip()
182
+
183
+
184
+ def generate_panel_caption(
185
+ label: Optional[str] = None,
186
+ stats: Optional[Dict[str, Any]] = None,
187
+ style: Literal["publication", "brief", "detailed"] = "publication",
188
+ ) -> str:
189
+ """Generate a panel caption from stats.
190
+
191
+ Parameters
192
+ ----------
193
+ label : str, optional
194
+ Panel label like "A" or "(A)".
195
+ stats : dict, optional
196
+ Panel-level stats.
197
+ style : str
198
+ Caption style.
199
+
200
+ Returns
201
+ -------
202
+ str
203
+ Generated panel caption.
204
+ """
205
+ parts = []
206
+
207
+ if label:
208
+ # Ensure label is in parentheses
209
+ if not label.startswith("("):
210
+ label = f"({label})"
211
+ parts.append(label)
212
+
213
+ if stats:
214
+ stats_str = format_panel_stats(stats, style)
215
+ if stats_str:
216
+ parts.append(stats_str)
217
+
218
+ return " ".join(parts)
@@ -3,7 +3,7 @@
3
3
  """Wrapped Figure that manages recording."""
4
4
 
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, Union
6
+ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union
7
7
 
8
8
  import matplotlib.pyplot as plt
9
9
  import numpy as np
@@ -93,6 +93,22 @@ class RecordingFigure:
93
93
  pass
94
94
  return default
95
95
 
96
+ def _get_theme_text_color(self, default: str = "black") -> str:
97
+ """Get text color from loaded style's theme settings."""
98
+ try:
99
+ from ..styles._style_loader import _STYLE_CACHE
100
+
101
+ if _STYLE_CACHE is not None:
102
+ theme = getattr(_STYLE_CACHE, "theme", None)
103
+ if theme is not None:
104
+ mode = getattr(theme, "mode", "light")
105
+ theme_colors = getattr(theme, mode, None)
106
+ if theme_colors is not None:
107
+ return getattr(theme_colors, "text", default)
108
+ except Exception:
109
+ pass
110
+ return default
111
+
96
112
  def suptitle(self, t: str, **kwargs) -> Any:
97
113
  """Set super title for the figure and record it.
98
114
 
@@ -162,6 +178,156 @@ class RecordingFigure:
162
178
  # Call the underlying figure's supylabel
163
179
  return self._fig.supylabel(t, **kwargs)
164
180
 
181
+ def add_panel_labels(
182
+ self,
183
+ labels: Optional[List[str]] = None,
184
+ loc: str = "upper left",
185
+ offset: Tuple[float, float] = (-0.1, 1.05),
186
+ fontsize: Optional[float] = None,
187
+ fontweight: str = "bold",
188
+ **kwargs,
189
+ ) -> List[Any]:
190
+ """Add panel labels (A, B, C, D, etc.) to multi-panel figures.
191
+
192
+ Parameters
193
+ ----------
194
+ labels : list of str, optional
195
+ Custom labels. If None, uses uppercase letters (A, B, C, ...).
196
+ loc : str
197
+ Location hint: 'upper left' (default), 'upper right', 'lower left', 'lower right'.
198
+ offset : tuple of float
199
+ (x, y) offset in axes coordinates from the corner.
200
+ Default is (-0.1, 1.05) for upper left positioning.
201
+ fontsize : float, optional
202
+ Font size in points. If None, uses style's title_pt or 10.
203
+ fontweight : str
204
+ Font weight (default: 'bold').
205
+ **kwargs
206
+ Additional arguments passed to ax.text().
207
+
208
+ Returns
209
+ -------
210
+ list of Text
211
+ The matplotlib Text objects created.
212
+
213
+ Examples
214
+ --------
215
+ >>> fig, axes = fr.subplots(2, 2)
216
+ >>> fig.add_panel_labels() # Adds A, B, C, D
217
+ >>> fig.add_panel_labels(['i', 'ii', 'iii', 'iv']) # Custom labels
218
+ >>> fig.add_panel_labels(loc='upper right', offset=(1.05, 1.05))
219
+ """
220
+ from ._panel_labels import add_panel_labels as _add_panel_labels
221
+
222
+ # Get fontsize from style if not specified
223
+ if fontsize is None:
224
+ fontsize = self._get_style_fontsize("title_pt", 10)
225
+
226
+ # Get theme text color (unless user provided 'color' in kwargs)
227
+ if "color" not in kwargs:
228
+ text_color = self._get_theme_text_color()
229
+ else:
230
+ text_color = kwargs.pop("color")
231
+
232
+ def record_callback(info):
233
+ self._recorder.figure_record.panel_labels = info
234
+
235
+ return _add_panel_labels(
236
+ all_axes=self.flat,
237
+ labels=labels,
238
+ loc=loc,
239
+ offset=offset,
240
+ fontsize=fontsize,
241
+ fontweight=fontweight,
242
+ text_color=text_color,
243
+ record_callback=record_callback,
244
+ **kwargs,
245
+ )
246
+
247
+ def set_title_metadata(self, title: str) -> "RecordingFigure":
248
+ """Set figure title metadata (not rendered, stored in recipe).
249
+
250
+ This is for storing a publication/reference title for the figure,
251
+ separate from suptitle which is rendered on the figure.
252
+
253
+ Parameters
254
+ ----------
255
+ title : str
256
+ The figure title for publication/reference.
257
+
258
+ Returns
259
+ -------
260
+ RecordingFigure
261
+ Self for method chaining.
262
+
263
+ Examples
264
+ --------
265
+ >>> fig, ax = fr.subplots()
266
+ >>> fig.set_title_metadata("Effect of temperature on reaction rate")
267
+ >>> fig.set_caption("Figure 1. Reaction rates measured at various temperatures.")
268
+ """
269
+ self._recorder.figure_record.title_metadata = title
270
+ return self
271
+
272
+ def set_caption(self, caption: str) -> "RecordingFigure":
273
+ """Set figure caption metadata (not rendered, stored in recipe).
274
+
275
+ This is for storing a publication caption for the figure,
276
+ typically used in scientific papers (e.g., "Fig. 1. Description...").
277
+
278
+ Parameters
279
+ ----------
280
+ caption : str
281
+ The figure caption text.
282
+
283
+ Returns
284
+ -------
285
+ RecordingFigure
286
+ Self for method chaining.
287
+
288
+ Examples
289
+ --------
290
+ >>> fig, ax = fr.subplots()
291
+ >>> fig.set_caption("Figure 1. Temperature dependence of reaction rates.")
292
+ """
293
+ self._recorder.figure_record.caption = caption
294
+ return self
295
+
296
+ @property
297
+ def title_metadata(self) -> Optional[str]:
298
+ """Get the figure title metadata."""
299
+ return self._recorder.figure_record.title_metadata
300
+
301
+ @property
302
+ def caption(self) -> Optional[str]:
303
+ """Get the figure caption metadata."""
304
+ return self._recorder.figure_record.caption
305
+
306
+ def set_stats(self, stats: Dict[str, Any]) -> "RecordingFigure":
307
+ """Set figure-level statistics metadata (not rendered, stored in recipe).
308
+
309
+ Parameters
310
+ ----------
311
+ stats : dict
312
+ Statistics dictionary (comparisons, summary, correction_method, alpha).
313
+ """
314
+ self._recorder.figure_record.stats = stats
315
+ return self
316
+
317
+ @property
318
+ def stats(self) -> Optional[Dict[str, Any]]:
319
+ """Get the figure-level statistics metadata."""
320
+ return self._recorder.figure_record.stats
321
+
322
+ def generate_caption(self, style: str = "publication", template: str = None) -> str:
323
+ """Generate caption from stored stats. Styles: publication, brief, detailed."""
324
+ from ._caption_generator import generate_figure_caption
325
+
326
+ panels = [ax.caption for ax in self.flat if ax.caption]
327
+ return generate_figure_caption(
328
+ self.title_metadata, panels, self.stats, style, template
329
+ )
330
+
165
331
  def __getattr__(self, name: str) -> Any:
166
332
  """Delegate attribute access to underlying figure."""
167
333
  return getattr(self._fig, name)
@@ -203,6 +369,15 @@ class RecordingFigure:
203
369
  >>> fig.savefig('figure.png') # Saves both figure.png and figure.yaml
204
370
  >>> fig.savefig('figure.png', save_recipe=False) # Image only
205
371
  """
372
+ # Finalize ticks and special plots before saving
373
+ from ..styles._style_applier import finalize_special_plots, finalize_ticks
374
+ from ..styles._style_loader import get_current_style_dict
375
+
376
+ style_dict = get_current_style_dict()
377
+ for ax in self._fig.get_axes():
378
+ finalize_ticks(ax)
379
+ finalize_special_plots(ax, style_dict)
380
+
206
381
  # Handle file-like objects (BytesIO, etc.) - just pass through
207
382
  if hasattr(fname, "write"):
208
383
  self._fig.savefig(fname, **kwargs)
@@ -255,6 +430,7 @@ def create_recording_subplots(
255
430
  nrows: int = 1,
256
431
  ncols: int = 1,
257
432
  recorder: Optional["Recorder"] = None,
433
+ panel_labels: bool = False,
258
434
  **kwargs,
259
435
  ) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
260
436
  """Create a figure with recording-enabled axes.
@@ -267,6 +443,9 @@ def create_recording_subplots(
267
443
  Number of columns.
268
444
  recorder : Recorder, optional
269
445
  Recorder instance. Created if not provided.
446
+ panel_labels : bool
447
+ If True and figure has multiple panels, automatically add
448
+ panel labels (A, B, C, D, ...). Default is False.
270
449
  **kwargs
271
450
  Passed to plt.subplots().
272
451
 
@@ -276,6 +455,10 @@ def create_recording_subplots(
276
455
  Wrapped figure.
277
456
  axes : RecordingAxes or ndarray
278
457
  Wrapped axes (single if 1x1, otherwise numpy array matching matplotlib).
458
+
459
+ Examples
460
+ --------
461
+ >>> fig, axes = fr.subplots(2, 2, panel_labels=True) # Auto-adds A, B, C, D
279
462
  """
280
463
  from .._recorder import Recorder
281
464
 
@@ -312,6 +495,10 @@ def create_recording_subplots(
312
495
 
313
496
  wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes)
314
497
 
498
+ # Add panel labels if requested (multi-panel figures only)
499
+ if panel_labels:
500
+ wrapped_fig.add_panel_labels()
501
+
315
502
  # Return in same shape as matplotlib (numpy arrays for consistency)
316
503
  if nrows == 1:
317
504
  # 1xN -> 1D array of shape (N,)
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel label utilities for multi-panel figures."""
4
+
5
+ import string
6
+ from typing import TYPE_CHECKING, Any, List, Optional, Tuple
7
+
8
+ if TYPE_CHECKING:
9
+ from ._axes import RecordingAxes
10
+
11
+
12
+ def add_panel_labels(
13
+ all_axes: List["RecordingAxes"],
14
+ labels: Optional[List[str]],
15
+ loc: str,
16
+ offset: Tuple[float, float],
17
+ fontsize: float,
18
+ fontweight: str,
19
+ text_color: str,
20
+ record_callback: Any,
21
+ **kwargs,
22
+ ) -> List[Any]:
23
+ """Add panel labels (A, B, C, D, etc.) to axes.
24
+
25
+ Parameters
26
+ ----------
27
+ all_axes : list of RecordingAxes
28
+ Flattened list of all axes.
29
+ labels : list of str or None
30
+ Custom labels. If None, uses uppercase letters.
31
+ loc : str
32
+ Location hint: 'upper left', 'upper right', 'lower left', 'lower right'.
33
+ offset : tuple of float
34
+ (x, y) offset in axes coordinates.
35
+ fontsize : float
36
+ Font size in points.
37
+ fontweight : str
38
+ Font weight.
39
+ text_color : str
40
+ Text color.
41
+ record_callback : callable
42
+ Callback to record panel labels info.
43
+ **kwargs
44
+ Additional arguments passed to ax.text().
45
+
46
+ Returns
47
+ -------
48
+ list of Text
49
+ The matplotlib Text objects created.
50
+ """
51
+ n_axes = len(all_axes)
52
+
53
+ # Generate default labels if not provided
54
+ if labels is None:
55
+ labels = list(string.ascii_uppercase[:n_axes])
56
+ elif len(labels) < n_axes:
57
+ # Extend with letters if not enough labels provided
58
+ labels = list(labels) + list(string.ascii_uppercase[len(labels) : n_axes])
59
+
60
+ # Calculate position based on loc
61
+ x, y, ha, va = _calculate_position(loc, offset)
62
+
63
+ # Record panel labels
64
+ record_callback(
65
+ {
66
+ "labels": labels[:n_axes],
67
+ "loc": loc,
68
+ "offset": offset,
69
+ "fontsize": fontsize,
70
+ "fontweight": fontweight,
71
+ "color": text_color,
72
+ "kwargs": kwargs,
73
+ }
74
+ )
75
+
76
+ # Add labels to each axes
77
+ text_objects = []
78
+ for ax, label in zip(all_axes, labels[:n_axes]):
79
+ text = ax.ax.text(
80
+ x,
81
+ y,
82
+ label,
83
+ transform=ax.ax.transAxes,
84
+ fontsize=fontsize,
85
+ fontweight=fontweight,
86
+ color=text_color,
87
+ ha=ha,
88
+ va=va,
89
+ **kwargs,
90
+ )
91
+ text_objects.append(text)
92
+
93
+ return text_objects
94
+
95
+
96
+ def _calculate_position(
97
+ loc: str, offset: Tuple[float, float]
98
+ ) -> Tuple[float, float, str, str]:
99
+ """Calculate text position and alignment based on location.
100
+
101
+ Returns
102
+ -------
103
+ tuple
104
+ (x, y, ha, va) where ha/va are horizontal/vertical alignment.
105
+ """
106
+ if loc == "upper left":
107
+ x, y = offset
108
+ ha, va = "right", "bottom"
109
+ elif loc == "upper right":
110
+ x, y = offset
111
+ ha, va = "left", "bottom"
112
+ elif loc == "lower left":
113
+ x, y = offset[0], -offset[1] + 1.0
114
+ ha, va = "right", "top"
115
+ elif loc == "lower right":
116
+ x, y = offset
117
+ ha, va = "left", "top"
118
+ else:
119
+ x, y = offset
120
+ ha, va = "right", "bottom"
121
+
122
+ return x, y, ha, va
123
+
124
+
125
+ __all__ = ["add_panel_labels"]
126
+
127
+ # EOF