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
@@ -79,179 +79,113 @@ class RecordingAxes:
79
79
  return attr
80
80
 
81
81
  def _create_recording_wrapper(self, method_name: str, method: callable):
82
- """Create a wrapper function that records the call.
83
-
84
- Parameters
85
- ----------
86
- method_name : str
87
- Name of the method.
88
- method : callable
89
- The original method.
90
-
91
- Returns
92
- -------
93
- callable
94
- Wrapped method that records calls.
95
- """
96
-
97
- def wrapper(*args, id: Optional[str] = None, track: bool = True, **kwargs):
98
- # Call the original method first (without our custom kwargs)
82
+ """Create a wrapper function that records the call."""
83
+ from ._axes_helpers import record_call_with_color_capture
84
+
85
+ def wrapper(
86
+ *args,
87
+ id: Optional[str] = None,
88
+ track: bool = True,
89
+ stats: Optional[Dict[str, Any]] = None,
90
+ **kwargs,
91
+ ):
92
+ # Call matplotlib method (without stats - it's metadata only)
99
93
  result = method(*args, **kwargs)
100
-
101
- # Record the call if tracking is enabled
102
94
  if self._track and track:
103
- # Capture actual colors from result for plotting methods
104
- # that use matplotlib's color cycle
105
- recorded_kwargs = kwargs.copy()
106
- if method_name in (
107
- "plot",
108
- "scatter",
109
- "bar",
110
- "barh",
111
- "step",
112
- "fill_between",
113
- ):
114
- # Check if fmt string already specifies color (e.g., "b-", "r--")
115
- has_fmt_color = self._args_have_fmt_color(args)
116
- if (
117
- "color" not in recorded_kwargs
118
- and "c" not in recorded_kwargs
119
- and not has_fmt_color
120
- ):
121
- actual_color = self._extract_color_from_result(
122
- method_name, result
123
- )
124
- if actual_color is not None:
125
- recorded_kwargs["color"] = actual_color
126
-
127
- # Process args to detect result references (e.g., clabel's ContourSet)
128
- processed_args = self._process_result_refs_in_args(args, method_name)
129
-
130
- call_record = self._recorder.record_call(
131
- ax_position=self._position,
132
- method_name=method_name,
133
- args=processed_args,
134
- kwargs=recorded_kwargs,
135
- call_id=id,
95
+ # Re-add stats to kwargs for recording
96
+ record_kwargs = kwargs.copy()
97
+ if stats is not None:
98
+ record_kwargs["stats"] = stats
99
+ record_call_with_color_capture(
100
+ self._recorder,
101
+ self._position,
102
+ method_name,
103
+ args,
104
+ record_kwargs,
105
+ result,
106
+ id,
107
+ self._result_refs,
108
+ self.RESULT_REFERENCING_METHODS,
109
+ self.RESULT_REFERENCEABLE_METHODS,
136
110
  )
137
-
138
- # Store result reference for methods whose results can be used later
139
- if method_name in self.RESULT_REFERENCEABLE_METHODS:
140
- import builtins
141
-
142
- self._result_refs[builtins.id(result)] = call_record.id
143
-
144
111
  return result
145
112
 
146
113
  return wrapper
147
114
 
148
- def _process_result_refs_in_args(self, args: tuple, method_name: str) -> tuple:
149
- """Process args to replace matplotlib objects with references.
115
+ def set_caption(self, caption: str) -> "RecordingAxes":
116
+ """Set panel caption metadata (not rendered, stored in recipe).
150
117
 
151
- For methods like clabel that take a ContourSet as argument,
152
- replace the object with a reference to the original call_id.
118
+ This is for storing a description for this panel/axis,
119
+ typically used in multi-panel scientific figures
120
+ (e.g., "(A) Control group measurements").
153
121
 
154
122
  Parameters
155
123
  ----------
156
- args : tuple
157
- Original arguments.
158
- method_name : str
159
- Name of the method.
124
+ caption : str
125
+ The panel caption text.
160
126
 
161
127
  Returns
162
128
  -------
163
- tuple
164
- Processed args with references.
165
- """
166
- if method_name not in self.RESULT_REFERENCING_METHODS:
167
- return args
129
+ RecordingAxes
130
+ Self for method chaining.
168
131
 
169
- import builtins
132
+ Examples
133
+ --------
134
+ >>> fig, axes = fr.subplots(1, 2)
135
+ >>> axes[0].set_caption("(A) Control group")
136
+ >>> axes[1].set_caption("(B) Treatment group")
137
+ """
138
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
139
+ ax_record.caption = caption
140
+ return self
170
141
 
171
- processed = []
172
- for i, arg in enumerate(args):
173
- obj_id = builtins.id(arg)
174
- if obj_id in self._result_refs:
175
- # This arg is a reference to a previous call's result
176
- processed.append({"__ref__": self._result_refs[obj_id]})
177
- else:
178
- processed.append(arg)
179
- return tuple(processed)
142
+ @property
143
+ def panel_caption(self) -> Optional[str]:
144
+ """Get the panel caption metadata."""
145
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
146
+ return ax_record.caption
180
147
 
181
- def _args_have_fmt_color(self, args: tuple) -> bool:
182
- """Check if args contain a matplotlib fmt string with color specifier.
148
+ def set_stats(self, stats: Dict[str, Any]) -> "RecordingAxes":
149
+ """Set panel-level statistics metadata (not rendered, stored in recipe).
183
150
 
184
- Fmt strings like "b-", "r--", "go" contain color codes (b,g,r,c,m,y,k,w).
151
+ This is for storing statistical summary or comparison results
152
+ for this panel/axis, such as group means, sample sizes, or
153
+ comparison p-values.
185
154
 
186
155
  Parameters
187
156
  ----------
188
- args : tuple
189
- Arguments passed to plot method.
157
+ stats : dict
158
+ Statistics dictionary. Common keys include:
159
+ - n: sample size
160
+ - mean: mean value
161
+ - std: standard deviation
162
+ - sem: standard error of the mean
163
+ - comparisons: list of comparison results
190
164
 
191
165
  Returns
192
166
  -------
193
- bool
194
- True if a fmt string with color is found.
195
- """
196
- color_codes = set("bgrcmykw")
197
- for arg in args:
198
- if isinstance(arg, str) and len(arg) >= 1 and len(arg) <= 4:
199
- # Fmt strings are short (e.g., "b-", "r--", "go", "k:")
200
- if arg[0] in color_codes:
201
- return True
202
- return False
203
-
204
- def _extract_color_from_result(self, method_name: str, result) -> Optional[str]:
205
- """Extract actual color used from plot result.
206
-
207
- Parameters
208
- ----------
209
- method_name : str
210
- Name of the plotting method.
211
- result : Any
212
- Return value from the plotting method.
167
+ RecordingAxes
168
+ Self for method chaining.
213
169
 
214
- Returns
215
- -------
216
- str or None
217
- The color used, or None if not extractable.
170
+ Examples
171
+ --------
172
+ >>> fig, axes = fr.subplots(1, 2)
173
+ >>> axes[0].set_stats({"n": 50, "mean": 3.2, "std": 1.1})
174
+ >>> axes[1].set_stats({
175
+ ... "n": 48,
176
+ ... "mean": 5.1,
177
+ ... "comparisons": [{"vs": "control", "p_value": 0.003}]
178
+ ... })
218
179
  """
219
- try:
220
- if method_name == "plot":
221
- # plot() returns list of Line2D
222
- if result and hasattr(result[0], "get_color"):
223
- return result[0].get_color()
224
- elif method_name == "scatter":
225
- # scatter() returns PathCollection
226
- if hasattr(result, "get_facecolor"):
227
- fc = result.get_facecolor()
228
- if len(fc) > 0:
229
- # Convert RGBA to hex
230
- import matplotlib.colors as mcolors
231
-
232
- return mcolors.to_hex(fc[0])
233
- elif method_name in ("bar", "barh"):
234
- # bar() returns BarContainer
235
- if hasattr(result, "patches") and result.patches:
236
- fc = result.patches[0].get_facecolor()
237
- import matplotlib.colors as mcolors
238
-
239
- return mcolors.to_hex(fc)
240
- elif method_name == "step":
241
- # step() returns list of Line2D
242
- if result and hasattr(result[0], "get_color"):
243
- return result[0].get_color()
244
- elif method_name == "fill_between":
245
- # fill_between() returns PolyCollection
246
- if hasattr(result, "get_facecolor"):
247
- fc = result.get_facecolor()
248
- if len(fc) > 0:
249
- import matplotlib.colors as mcolors
250
-
251
- return mcolors.to_hex(fc[0])
252
- except Exception:
253
- pass
254
- return None
180
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
181
+ ax_record.stats = stats
182
+ return self
183
+
184
+ @property
185
+ def stats(self) -> Optional[Dict[str, Any]]:
186
+ """Get the panel-level statistics metadata."""
187
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
188
+ return ax_record.stats
255
189
 
256
190
  def no_record(self):
257
191
  """Context manager to temporarily disable recording.
@@ -271,116 +205,22 @@ class RecordingAxes:
271
205
  data_arrays: Dict[str, np.ndarray],
272
206
  call_id: Optional[str] = None,
273
207
  ) -> None:
274
- """Record a seaborn plotting call.
275
-
276
- Parameters
277
- ----------
278
- func_name : str
279
- Name of the seaborn function (e.g., 'scatterplot').
280
- args : tuple
281
- Processed positional arguments.
282
- kwargs : dict
283
- Processed keyword arguments.
284
- data_arrays : dict
285
- Dictionary of array data extracted from DataFrame/arrays.
286
- call_id : str, optional
287
- Custom ID for this call.
288
- """
208
+ """Record a seaborn plotting call."""
289
209
  if not self._track:
290
210
  return
291
211
 
292
- from .._utils._numpy_io import should_store_inline, to_serializable
293
-
294
- # Generate call ID if not provided
295
- if call_id is None:
296
- call_id = self._recorder._generate_call_id(f"sns_{func_name}")
297
-
298
- # Process data arrays into args format
299
- processed_args = []
300
- for i, arg in enumerate(args):
301
- if arg == "__ARRAY__":
302
- key = f"_arg_{i}"
303
- if key in data_arrays:
304
- arr = data_arrays[key]
305
- if should_store_inline(arr):
306
- processed_args.append(
307
- {
308
- "name": f"arg{i}",
309
- "data": to_serializable(arr),
310
- "dtype": str(arr.dtype),
311
- }
312
- )
313
- else:
314
- processed_args.append(
315
- {
316
- "name": f"arg{i}",
317
- "data": "__FILE__",
318
- "dtype": str(arr.dtype),
319
- "_array": arr,
320
- }
321
- )
322
- else:
323
- processed_args.append(
324
- {
325
- "name": f"arg{i}",
326
- "data": arg,
327
- }
328
- )
212
+ from ._axes_seaborn import record_seaborn_call
329
213
 
330
- # Process DataFrame column data
331
- for key, arr in data_arrays.items():
332
- if key.startswith("_col_"):
333
- param_name = key[5:] # Remove "_col_" prefix
334
- col_name = data_arrays.get(f"_colname_{param_name}", param_name)
335
- if should_store_inline(arr):
336
- processed_args.append(
337
- {
338
- "name": col_name,
339
- "param": param_name,
340
- "data": to_serializable(arr),
341
- "dtype": str(arr.dtype),
342
- }
343
- )
344
- else:
345
- processed_args.append(
346
- {
347
- "name": col_name,
348
- "param": param_name,
349
- "data": "__FILE__",
350
- "dtype": str(arr.dtype),
351
- "_array": arr,
352
- }
353
- )
354
-
355
- # Process kwarg arrays
356
- processed_kwargs = dict(kwargs)
357
- for key, value in kwargs.items():
358
- if value == "__ARRAY__":
359
- arr_key = f"_kwarg_{key}"
360
- if arr_key in data_arrays:
361
- arr = data_arrays[arr_key]
362
- if should_store_inline(arr):
363
- processed_kwargs[key] = to_serializable(arr)
364
- else:
365
- # Mark for file storage
366
- processed_kwargs[key] = "__FILE__"
367
- processed_kwargs[f"_array_{key}"] = arr
368
-
369
- # Create call record
370
- from .._recorder import CallRecord
371
-
372
- record = CallRecord(
373
- id=call_id,
374
- function=f"sns.{func_name}",
375
- args=processed_args,
376
- kwargs=processed_kwargs,
377
- ax_position=self._position,
214
+ record_seaborn_call(
215
+ self._recorder,
216
+ self._position,
217
+ func_name,
218
+ args,
219
+ kwargs,
220
+ data_arrays,
221
+ call_id,
378
222
  )
379
223
 
380
- # Add to axes record
381
- ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
382
- ax_record.add_call(record)
383
-
384
224
  # Expose common properties directly
385
225
  @property
386
226
  def figure(self):
@@ -402,64 +242,18 @@ class RecordingAxes:
402
242
  track: bool = True,
403
243
  **kwargs,
404
244
  ):
405
- """Pie chart with automatic SCITEX styling.
406
-
407
- Parameters
408
- ----------
409
- x : array-like
410
- Wedge sizes.
411
- id : str, optional
412
- Custom ID for this call.
413
- track : bool, optional
414
- Whether to record this call (default: True).
415
- **kwargs
416
- Additional arguments passed to matplotlib's pie.
417
-
418
- Returns
419
- -------
420
- tuple
421
- (patches, texts) or (patches, texts, autotexts) if autopct is set.
422
- """
423
- from ..styles import get_style
424
- from ..styles._style_applier import check_font
425
-
426
- # Call matplotlib's pie
427
- result = self._ax.pie(x, **kwargs)
428
-
429
- # Get style settings
430
- style = get_style()
431
- if style:
432
- pie_style = style.get("pie", {})
433
- text_pt = pie_style.get("text_pt", 6)
434
- show_axes = pie_style.get("show_axes", False)
435
- font_family = check_font(style.get("fonts", {}).get("family", "Arial"))
436
-
437
- # Apply text size to all pie text elements (labels and percentages)
438
- for text in self._ax.texts:
439
- text.set_fontsize(text_pt)
440
- text.set_fontfamily(font_family)
441
-
442
- # Hide axes if configured (default: hide for pie charts)
443
- if not show_axes:
444
- self._ax.set_xticks([])
445
- self._ax.set_yticks([])
446
- self._ax.set_xticklabels([])
447
- self._ax.set_yticklabels([])
448
- # Hide spines
449
- for spine in self._ax.spines.values():
450
- spine.set_visible(False)
451
-
452
- # Record the call if tracking is enabled
453
- if self._track and track:
454
- self._recorder.record_call(
455
- ax_position=self._position,
456
- method_name="pie",
457
- args=(x,),
458
- kwargs=kwargs,
459
- call_id=id,
460
- )
461
-
462
- return result
245
+ """Pie chart with automatic SCITEX styling."""
246
+ from ._axes_plots import pie_plot
247
+
248
+ return pie_plot(
249
+ self._ax,
250
+ x,
251
+ self._recorder,
252
+ self._position,
253
+ self._track and track,
254
+ id,
255
+ **kwargs,
256
+ )
463
257
 
464
258
  def imshow(
465
259
  self,
@@ -469,61 +263,18 @@ class RecordingAxes:
469
263
  track: bool = True,
470
264
  **kwargs,
471
265
  ):
472
- """Display image with automatic SCITEX styling.
473
-
474
- Parameters
475
- ----------
476
- X : array-like
477
- Image data.
478
- id : str, optional
479
- Custom ID for this call.
480
- track : bool, optional
481
- Whether to record this call (default: True).
482
- **kwargs
483
- Additional arguments passed to matplotlib's imshow.
484
-
485
- Returns
486
- -------
487
- AxesImage
488
- The created image.
489
- """
490
- from ..styles import get_style
491
-
492
- # Call matplotlib's imshow
493
- result = self._ax.imshow(X, **kwargs)
494
-
495
- # Get style settings
496
- style = get_style()
497
- if style:
498
- imshow_style = style.get("imshow", {})
499
- show_axes = imshow_style.get("show_axes", True)
500
- show_labels = imshow_style.get("show_labels", True)
501
-
502
- # Hide axes if configured
503
- if not show_axes:
504
- self._ax.set_xticks([])
505
- self._ax.set_yticks([])
506
- self._ax.set_xticklabels([])
507
- self._ax.set_yticklabels([])
508
- # Hide spines
509
- for spine in self._ax.spines.values():
510
- spine.set_visible(False)
511
-
512
- if not show_labels:
513
- self._ax.set_xlabel("")
514
- self._ax.set_ylabel("")
515
-
516
- # Record the call if tracking is enabled
517
- if self._track and track:
518
- self._recorder.record_call(
519
- ax_position=self._position,
520
- method_name="imshow",
521
- args=(X,),
522
- kwargs=kwargs,
523
- call_id=id,
524
- )
525
-
526
- return result
266
+ """Display image with automatic SCITEX styling."""
267
+ from ._axes_plots import imshow_plot
268
+
269
+ return imshow_plot(
270
+ self._ax,
271
+ X,
272
+ self._recorder,
273
+ self._position,
274
+ self._track and track,
275
+ id,
276
+ **kwargs,
277
+ )
527
278
 
528
279
  def violinplot(
529
280
  self,
@@ -535,238 +286,21 @@ class RecordingAxes:
535
286
  inner: Optional[str] = None,
536
287
  **kwargs,
537
288
  ):
538
- """Violin plot with support for inner display options.
539
-
540
- Parameters
541
- ----------
542
- dataset : array-like
543
- Data to plot.
544
- positions : array-like, optional
545
- Position of each violin on x-axis.
546
- id : str, optional
547
- Custom ID for this call.
548
- track : bool, optional
549
- Whether to record this call (default: True).
550
- inner : str, optional
551
- Inner display type: "box", "quartile", "stick", "point", "swarm", or None.
552
- Default is from style config (SCITEX default: "box").
553
- **kwargs
554
- Additional arguments passed to matplotlib's violinplot.
555
-
556
- Returns
557
- -------
558
- dict
559
- Dictionary with violin parts (bodies, cbars, cmins, cmaxes, cmeans, cmedians).
560
- """
561
- from ..styles import get_style
562
-
563
- # Get style settings
564
- style = get_style()
565
- violin_style = style.get("violinplot", {}) if style else {}
566
-
567
- # Determine inner type (user kwarg > style config > default)
568
- if inner is None:
569
- inner = violin_style.get("inner", "box")
289
+ """Violin plot with support for inner display options."""
290
+ from ._axes_plots import violinplot_plot
570
291
 
571
- # Get violin display options from style
572
- showmeans = kwargs.pop("showmeans", violin_style.get("showmeans", False))
573
- showmedians = kwargs.pop("showmedians", violin_style.get("showmedians", True))
574
- showextrema = kwargs.pop("showextrema", violin_style.get("showextrema", False))
575
-
576
- # Call matplotlib's violinplot
577
- result = self._ax.violinplot(
292
+ return violinplot_plot(
293
+ self._ax,
578
294
  dataset,
579
- positions=positions,
580
- showmeans=showmeans,
581
- showmedians=showmedians if inner not in ("box", "swarm") else False,
582
- showextrema=showextrema if inner not in ("box", "swarm") else False,
295
+ positions,
296
+ self._recorder,
297
+ self._position,
298
+ self._track and track,
299
+ id,
300
+ inner,
583
301
  **kwargs,
584
302
  )
585
303
 
586
- # Apply alpha from style to violin bodies
587
- alpha = violin_style.get("alpha", 0.7)
588
- if "bodies" in result:
589
- for body in result["bodies"]:
590
- body.set_alpha(alpha)
591
-
592
- # Overlay inner elements based on inner type
593
- if positions is None:
594
- positions = list(range(1, len(dataset) + 1))
595
-
596
- if inner == "box":
597
- self._add_violin_inner_box(dataset, positions, violin_style)
598
- elif inner == "swarm":
599
- self._add_violin_inner_swarm(dataset, positions, violin_style)
600
- elif inner == "quartile":
601
- # quartile lines are handled by showmedians + showextrema
602
- pass
603
- elif inner == "stick":
604
- self._add_violin_inner_stick(dataset, positions, violin_style)
605
- elif inner == "point":
606
- self._add_violin_inner_point(dataset, positions, violin_style)
607
-
608
- # Record the call if tracking is enabled
609
- if self._track and track:
610
- recorded_kwargs = kwargs.copy()
611
- recorded_kwargs["inner"] = inner
612
- recorded_kwargs["showmeans"] = showmeans
613
- recorded_kwargs["showmedians"] = showmedians
614
- recorded_kwargs["showextrema"] = showextrema
615
-
616
- self._recorder.record_call(
617
- ax_position=self._position,
618
- method_name="violinplot",
619
- args=(dataset,),
620
- kwargs=recorded_kwargs,
621
- call_id=id,
622
- )
623
-
624
- return result
625
-
626
- def _add_violin_inner_box(self, dataset, positions, style: Dict[str, Any]) -> None:
627
- """Add box plot inside violin.
628
-
629
- Parameters
630
- ----------
631
- dataset : array-like
632
- Data arrays for each violin.
633
- positions : array-like
634
- X positions of violins.
635
- style : dict
636
- Violin style configuration.
637
- """
638
- from ..styles._style_applier import mm_to_pt
639
-
640
- whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
641
- median_size = mm_to_pt(style.get("median_mm", 0.8))
642
-
643
- for i, (data, pos) in enumerate(zip(dataset, positions)):
644
- data = np.asarray(data)
645
- q1, median, q3 = np.percentile(data, [25, 50, 75])
646
- iqr = q3 - q1
647
- whisker_low = max(data.min(), q1 - 1.5 * iqr)
648
- whisker_high = min(data.max(), q3 + 1.5 * iqr)
649
-
650
- # Draw box (Q1 to Q3)
651
- self._ax.vlines(
652
- pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3
653
- )
654
- # Draw whiskers
655
- self._ax.vlines(
656
- pos,
657
- whisker_low,
658
- q1,
659
- colors="black",
660
- linewidths=whisker_lw * 0.5,
661
- zorder=3,
662
- )
663
- self._ax.vlines(
664
- pos,
665
- q3,
666
- whisker_high,
667
- colors="black",
668
- linewidths=whisker_lw * 0.5,
669
- zorder=3,
670
- )
671
- # Draw median as a white dot with black edge
672
- self._ax.scatter(
673
- [pos],
674
- [median],
675
- s=median_size**2,
676
- c="white",
677
- edgecolors="black",
678
- linewidths=whisker_lw,
679
- zorder=4,
680
- )
681
-
682
- def _add_violin_inner_swarm(
683
- self, dataset, positions, style: Dict[str, Any]
684
- ) -> None:
685
- """Add swarm points inside violin.
686
-
687
- Parameters
688
- ----------
689
- dataset : array-like
690
- Data arrays for each violin.
691
- positions : array-like
692
- X positions of violins.
693
- style : dict
694
- Violin style configuration.
695
- """
696
- from ..styles._style_applier import mm_to_pt
697
-
698
- point_size = mm_to_pt(style.get("median_mm", 0.8))
699
-
700
- for data, pos in zip(dataset, positions):
701
- data = np.asarray(data)
702
- n = len(data)
703
-
704
- # Simple swarm: jitter x positions
705
- # More sophisticated swarm would avoid overlaps
706
- jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
707
- x_positions = pos + jitter
708
-
709
- self._ax.scatter(
710
- x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3
711
- )
712
-
713
- def _add_violin_inner_stick(
714
- self, dataset, positions, style: Dict[str, Any]
715
- ) -> None:
716
- """Add stick (line) markers inside violin for each data point.
717
-
718
- Parameters
719
- ----------
720
- dataset : array-like
721
- Data arrays for each violin.
722
- positions : array-like
723
- X positions of violins.
724
- style : dict
725
- Violin style configuration.
726
- """
727
- from ..styles._style_applier import mm_to_pt
728
-
729
- lw = mm_to_pt(style.get("whisker_mm", 0.2))
730
-
731
- for data, pos in zip(dataset, positions):
732
- data = np.asarray(data)
733
- # Draw short horizontal lines at each data point
734
- for val in data:
735
- self._ax.hlines(
736
- val,
737
- pos - 0.05,
738
- pos + 0.05,
739
- colors="black",
740
- linewidths=lw * 0.5,
741
- alpha=0.3,
742
- zorder=3,
743
- )
744
-
745
- def _add_violin_inner_point(
746
- self, dataset, positions, style: Dict[str, Any]
747
- ) -> None:
748
- """Add point markers inside violin for each data point.
749
-
750
- Parameters
751
- ----------
752
- dataset : array-like
753
- Data arrays for each violin.
754
- positions : array-like
755
- X positions of violins.
756
- style : dict
757
- Violin style configuration.
758
- """
759
- from ..styles._style_applier import mm_to_pt
760
-
761
- point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
762
-
763
- for data, pos in zip(dataset, positions):
764
- data = np.asarray(data)
765
- x_positions = np.full_like(data, pos)
766
- self._ax.scatter(
767
- x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3
768
- )
769
-
770
304
  # Methods that should not be recorded
771
305
  def get_xlim(self):
772
306
  return self._ax.get_xlim()
@@ -796,158 +330,24 @@ class RecordingAxes:
796
330
  track: bool = True,
797
331
  **kwargs,
798
332
  ):
799
- """Create a joyplot (ridgeline plot) for distribution comparison.
800
-
801
- Parameters
802
- ----------
803
- arrays : list of array-like or dict
804
- List of 1D arrays for each ridge. If dict, uses values.
805
- overlap : float, default 0.5
806
- Amount of overlap between ridges (0 = no overlap, 1 = full overlap).
807
- fill_alpha : float, default 0.7
808
- Alpha for the filled KDE area.
809
- line_alpha : float, default 1.0
810
- Alpha for the KDE line.
811
- colors : list, optional
812
- Colors for each ridge. If None, uses color cycle.
813
- labels : list of str, optional
814
- Labels for each ridge (for y-axis).
815
- id : str, optional
816
- Custom ID for this call.
817
- track : bool, optional
818
- Whether to record this call (default: True).
819
- **kwargs
820
- Additional arguments.
821
-
822
- Returns
823
- -------
824
- RecordingAxes
825
- Self for method chaining.
826
-
827
- Examples
828
- --------
829
- >>> ax.joyplot([data1, data2, data3], overlap=0.5)
830
- >>> ax.joyplot({"A": arr_a, "B": arr_b}, labels=["A", "B"])
831
- """
832
- from scipy import stats
833
-
834
- from .._utils._units import mm_to_pt
835
- from ..styles import get_style
836
-
837
- # Convert dict to list of arrays
838
- if isinstance(arrays, dict):
839
- if labels is None:
840
- labels = list(arrays.keys())
841
- arrays = list(arrays.values())
842
-
843
- n_ridges = len(arrays)
844
-
845
- # Get colors from style or use default cycle
846
- if colors is None:
847
- style = get_style()
848
- if style and "colors" in style and "palette" in style.colors:
849
- palette = list(style.colors.palette)
850
- # Normalize RGB 0-255 to 0-1
851
- colors = []
852
- for c in palette:
853
- if isinstance(c, (list, tuple)) and len(c) >= 3:
854
- if all(v <= 1.0 for v in c):
855
- colors.append(tuple(c))
856
- else:
857
- colors.append(tuple(v / 255.0 for v in c))
858
- else:
859
- colors.append(c)
860
- else:
861
- # Matplotlib default color cycle
862
- import matplotlib.pyplot as plt
863
-
864
- colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
865
-
866
- # Calculate global x range
867
- all_data = np.concatenate([np.asarray(arr) for arr in arrays])
868
- x_min, x_max = np.min(all_data), np.max(all_data)
869
- x_range = x_max - x_min
870
- x_padding = x_range * 0.1
871
- x = np.linspace(x_min - x_padding, x_max + x_padding, 200)
872
-
873
- # Calculate KDEs and find max density for scaling
874
- kdes = []
875
- max_density = 0
876
- for arr in arrays:
877
- arr = np.asarray(arr)
878
- if len(arr) > 1:
879
- kde = stats.gaussian_kde(arr)
880
- density = kde(x)
881
- kdes.append(density)
882
- max_density = max(max_density, np.max(density))
883
- else:
884
- kdes.append(np.zeros_like(x))
885
-
886
- # Scale factor for ridge height
887
- ridge_height = 1.0 / (1.0 - overlap * 0.5) if overlap < 1 else 2.0
888
-
889
- # Get line width from style
890
- style = get_style()
891
- lw = mm_to_pt(0.2) # Default
892
- if style and "lines" in style:
893
- lw = mm_to_pt(style.lines.get("trace_mm", 0.2))
894
-
895
- # Plot each ridge from back to front
896
- for i in range(n_ridges - 1, -1, -1):
897
- color = colors[i % len(colors)]
898
- baseline = i * (1.0 - overlap)
899
-
900
- # Scale density to fit nicely
901
- scaled_density = (
902
- kdes[i] / max_density * ridge_height if max_density > 0 else kdes[i]
903
- )
904
-
905
- # Fill
906
- self._ax.fill_between(
907
- x,
908
- baseline,
909
- baseline + scaled_density,
910
- facecolor=color,
911
- edgecolor="none",
912
- alpha=fill_alpha,
913
- )
914
- # Line on top
915
- self._ax.plot(
916
- x,
917
- baseline + scaled_density,
918
- color=color,
919
- alpha=line_alpha,
920
- linewidth=lw,
921
- )
922
-
923
- # Set y limits
924
- self._ax.set_ylim(-0.1, n_ridges * (1.0 - overlap) + ridge_height)
925
-
926
- # Set y-axis labels if provided
927
- if labels:
928
- y_positions = [(i * (1.0 - overlap)) + 0.3 for i in range(n_ridges)]
929
- self._ax.set_yticks(y_positions)
930
- self._ax.set_yticklabels(labels)
931
- else:
932
- # Hide y-axis ticks for cleaner look
933
- self._ax.set_yticks([])
934
-
935
- # Record the call if tracking is enabled
936
- if self._track and track:
937
- self._recorder.record_call(
938
- ax_position=self._position,
939
- method_name="joyplot",
940
- args=(arrays,),
941
- kwargs={
942
- "overlap": overlap,
943
- "fill_alpha": fill_alpha,
944
- "line_alpha": line_alpha,
945
- "labels": labels,
946
- },
947
- call_id=id,
948
- )
949
-
950
- return self
333
+ """Create a joyplot (ridgeline plot) for distribution comparison."""
334
+ from ._axes_plots import joyplot_plot
335
+
336
+ return joyplot_plot(
337
+ self._ax,
338
+ self,
339
+ arrays,
340
+ self._recorder,
341
+ self._position,
342
+ self._track and track,
343
+ id,
344
+ overlap,
345
+ fill_alpha,
346
+ line_alpha,
347
+ colors,
348
+ labels,
349
+ **kwargs,
350
+ )
951
351
 
952
352
  def swarmplot(
953
353
  self,
@@ -962,167 +362,124 @@ class RecordingAxes:
962
362
  track: bool = True,
963
363
  **kwargs,
964
364
  ):
965
- """Create a swarm plot (beeswarm plot) showing individual data points.
966
-
967
- Parameters
968
- ----------
969
- data : list of array-like
970
- List of 1D arrays to plot.
971
- positions : array-like, optional
972
- X positions for each swarm. Default is 1, 2, 3, ...
973
- size : float, optional
974
- Marker size in mm. Default from style config.
975
- color : color or list of colors, optional
976
- Colors for each swarm.
977
- alpha : float, default 0.7
978
- Transparency of markers.
979
- jitter : float, default 0.3
980
- Width of jitter spread (in data units).
981
- id : str, optional
982
- Custom ID for this call.
983
- track : bool, optional
984
- Whether to record this call (default: True).
985
- **kwargs
986
- Additional arguments passed to scatter.
987
-
988
- Returns
989
- -------
990
- list
991
- List of PathCollection objects.
365
+ """Create a swarm plot (beeswarm plot) showing individual data points."""
366
+ from ._axes_plots import swarmplot_plot
367
+
368
+ return swarmplot_plot(
369
+ self._ax,
370
+ data,
371
+ positions,
372
+ self._recorder,
373
+ self._position,
374
+ self._track and track,
375
+ id,
376
+ size,
377
+ color,
378
+ alpha,
379
+ jitter,
380
+ **kwargs,
381
+ )
992
382
 
993
- Examples
994
- --------
995
- >>> ax.swarmplot([data1, data2, data3])
996
- >>> ax.swarmplot([arr1, arr2], positions=[0, 1], color=['red', 'blue'])
997
- """
998
- from .._utils._units import mm_to_pt
999
- from ..styles import get_style
1000
-
1001
- # Get style
1002
- style = get_style()
1003
-
1004
- # Default marker size from style
1005
- if size is None:
1006
- if style and "markers" in style:
1007
- size = style.markers.get("scatter_mm", 0.8)
1008
- else:
1009
- size = 0.8
1010
- size_pt = mm_to_pt(size) ** 2 # matplotlib uses area
1011
-
1012
- # Get colors
1013
- if color is None:
1014
- if style and "colors" in style and "palette" in style.colors:
1015
- palette = list(style.colors.palette)
1016
- colors = []
1017
- for c in palette:
1018
- if isinstance(c, (list, tuple)) and len(c) >= 3:
1019
- if all(v <= 1.0 for v in c):
1020
- colors.append(tuple(c))
1021
- else:
1022
- colors.append(tuple(v / 255.0 for v in c))
1023
- else:
1024
- colors.append(c)
1025
- else:
1026
- import matplotlib.pyplot as plt
1027
-
1028
- colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
1029
- elif isinstance(color, list):
1030
- colors = color
1031
- else:
1032
- colors = [color] * len(data)
1033
-
1034
- # Default positions
1035
- if positions is None:
1036
- positions = list(range(1, len(data) + 1))
1037
-
1038
- # Random generator for reproducible jitter
1039
- rng = np.random.default_rng(42)
1040
-
1041
- results = []
1042
- for i, (arr, pos) in enumerate(zip(data, positions)):
1043
- arr = np.asarray(arr)
1044
-
1045
- # Create jittered x positions using beeswarm algorithm (simplified)
1046
- x_jitter = self._beeswarm_positions(arr, jitter, rng)
1047
- x_positions = pos + x_jitter
1048
-
1049
- c = colors[i % len(colors)]
1050
- result = self._ax.scatter(
1051
- x_positions, arr, s=size_pt, c=[c], alpha=alpha, **kwargs
1052
- )
1053
- results.append(result)
383
+ @property
384
+ def caption(self) -> Optional[str]:
385
+ """Get the panel caption metadata."""
386
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
387
+ return ax_record.caption
1054
388
 
1055
- # Record the call if tracking is enabled
1056
- if self._track and track:
1057
- self._recorder.record_call(
1058
- ax_position=self._position,
1059
- method_name="swarmplot",
1060
- args=(data,),
1061
- kwargs={
1062
- "positions": positions,
1063
- "size": size,
1064
- "alpha": alpha,
1065
- "jitter": jitter,
1066
- },
1067
- call_id=id,
1068
- )
389
+ def generate_panel_caption(
390
+ self, label: Optional[str] = None, style: str = "publication"
391
+ ) -> str:
392
+ """Generate a caption for this panel from stats metadata."""
393
+ from ._caption_generator import generate_panel_caption
1069
394
 
1070
- return results
395
+ return generate_panel_caption(label=label, stats=self.stats, style=style)
1071
396
 
1072
- def _beeswarm_positions(
397
+ def add_stat_annotation(
1073
398
  self,
1074
- data: np.ndarray,
1075
- width: float,
1076
- rng: np.random.Generator,
1077
- ) -> np.ndarray:
1078
- """Calculate beeswarm-style x positions to minimize overlap.
1079
-
1080
- This is a simplified beeswarm that uses binning and jittering.
1081
- For a true beeswarm, we'd need to iteratively place points.
399
+ x1: float,
400
+ x2: float,
401
+ p_value: Optional[float] = None,
402
+ text: Optional[str] = None,
403
+ y: Optional[float] = None,
404
+ style: str = "stars",
405
+ bracket_height: Optional[float] = None,
406
+ text_offset: Optional[float] = None,
407
+ color: Optional[str] = None,
408
+ linewidth: Optional[float] = None,
409
+ fontsize: Optional[float] = None,
410
+ fontweight: Optional[str] = None,
411
+ id: Optional[str] = None,
412
+ track: bool = True,
413
+ **kwargs,
414
+ ):
415
+ """Add a statistical comparison annotation (bracket with stars/p-value).
1082
416
 
1083
417
  Parameters
1084
418
  ----------
1085
- data : array
1086
- Y values of points.
1087
- width : float
1088
- Maximum jitter width.
1089
- rng : Generator
1090
- Random number generator.
1091
-
1092
- Returns
1093
- -------
1094
- array
1095
- X offsets for each point.
419
+ x1, x2 : float
420
+ X positions of the two groups being compared.
421
+ p_value : float, optional
422
+ P-value for automatic star conversion.
423
+ text : str, optional
424
+ Custom text (overrides p_value formatting).
425
+ y : float, optional
426
+ Y position for bracket (auto-calculated if None).
427
+ style : str
428
+ "stars", "p_value", "both", or "bracket_only".
1096
429
  """
1097
- n = len(data)
1098
- if n == 0:
1099
- return np.array([])
1100
-
1101
- # Sort data and get order
1102
- order = np.argsort(data)
1103
- sorted_data = data[order]
1104
-
1105
- # Group nearby points and offset them
1106
- x_offsets = np.zeros(n)
1107
-
1108
- # Simple approach: bin by quantiles and spread within each bin
1109
- n_bins = max(1, int(np.sqrt(n)))
1110
- bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
1111
-
1112
- for i in range(n_bins):
1113
- mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
1114
- n_in_bin = mask.sum()
1115
- if n_in_bin > 0:
1116
- # Spread points evenly within bin width
1117
- offsets = np.linspace(-width / 2, width / 2, n_in_bin)
1118
- # Add small random noise
1119
- offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
1120
- x_offsets[mask] = offsets
1121
-
1122
- # Restore original order
1123
- result = np.zeros(n)
1124
- result[order] = x_offsets
1125
- return result
430
+ from ._stat_annotation import draw_stat_annotation
431
+
432
+ # Draw the annotation
433
+ artists = draw_stat_annotation(
434
+ self._ax,
435
+ x1,
436
+ x2,
437
+ y=y,
438
+ text=text,
439
+ p_value=p_value,
440
+ style=style,
441
+ bracket_height=bracket_height,
442
+ text_offset=text_offset,
443
+ color=color,
444
+ linewidth=linewidth,
445
+ fontsize=fontsize,
446
+ fontweight=fontweight,
447
+ **kwargs,
448
+ )
449
+
450
+ # Record if tracking
451
+ if self._track and track:
452
+ call_id = id if id else self._recorder._generate_call_id("stat_annotation")
453
+ record_kwargs = {
454
+ "x1": x1,
455
+ "x2": x2,
456
+ "p_value": p_value,
457
+ "text": text,
458
+ "y": y,
459
+ "style": style,
460
+ "bracket_height": bracket_height,
461
+ "text_offset": text_offset,
462
+ "color": color,
463
+ "linewidth": linewidth,
464
+ "fontsize": fontsize,
465
+ }
466
+ record_kwargs.update(kwargs)
467
+ # Remove None values
468
+ record_kwargs = {k: v for k, v in record_kwargs.items() if v is not None}
469
+
470
+ from .._recorder import CallRecord
471
+
472
+ record = CallRecord(
473
+ id=call_id,
474
+ function="stat_annotation",
475
+ args=[],
476
+ kwargs=record_kwargs,
477
+ ax_position=self._position,
478
+ )
479
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
480
+ ax_record.add_decoration(record)
481
+
482
+ return artists
1126
483
 
1127
484
 
1128
485
  class _NoRecordContext: