figrecipe 0.6.0__py3-none-any.whl → 0.7.4__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 (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -79,179 +79,60 @@ 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
- """
82
+ """Create a wrapper function that records the call."""
83
+ from ._axes_helpers import record_call_with_color_capture
96
84
 
97
85
  def wrapper(*args, id: Optional[str] = None, track: bool = True, **kwargs):
98
- # Call the original method first (without our custom kwargs)
99
86
  result = method(*args, **kwargs)
100
-
101
- # Record the call if tracking is enabled
102
87
  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,
88
+ record_call_with_color_capture(
89
+ self._recorder,
90
+ self._position,
91
+ method_name,
92
+ args,
93
+ kwargs,
94
+ result,
95
+ id,
96
+ self._result_refs,
97
+ self.RESULT_REFERENCING_METHODS,
98
+ self.RESULT_REFERENCEABLE_METHODS,
136
99
  )
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
100
  return result
145
101
 
146
102
  return wrapper
147
103
 
148
- def _process_result_refs_in_args(self, args: tuple, method_name: str) -> tuple:
149
- """Process args to replace matplotlib objects with references.
104
+ def set_caption(self, caption: str) -> "RecordingAxes":
105
+ """Set panel caption metadata (not rendered, stored in recipe).
150
106
 
151
- For methods like clabel that take a ContourSet as argument,
152
- replace the object with a reference to the original call_id.
107
+ This is for storing a description for this panel/axis,
108
+ typically used in multi-panel scientific figures
109
+ (e.g., "(A) Control group measurements").
153
110
 
154
111
  Parameters
155
112
  ----------
156
- args : tuple
157
- Original arguments.
158
- method_name : str
159
- Name of the method.
113
+ caption : str
114
+ The panel caption text.
160
115
 
161
116
  Returns
162
117
  -------
163
- tuple
164
- Processed args with references.
165
- """
166
- if method_name not in self.RESULT_REFERENCING_METHODS:
167
- return args
168
-
169
- import builtins
170
-
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)
180
-
181
- def _args_have_fmt_color(self, args: tuple) -> bool:
182
- """Check if args contain a matplotlib fmt string with color specifier.
183
-
184
- Fmt strings like "b-", "r--", "go" contain color codes (b,g,r,c,m,y,k,w).
185
-
186
- Parameters
187
- ----------
188
- args : tuple
189
- Arguments passed to plot method.
190
-
191
- Returns
192
- -------
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.
118
+ RecordingAxes
119
+ Self for method chaining.
213
120
 
214
- Returns
215
- -------
216
- str or None
217
- The color used, or None if not extractable.
121
+ Examples
122
+ --------
123
+ >>> fig, axes = fr.subplots(1, 2)
124
+ >>> axes[0].set_caption("(A) Control group")
125
+ >>> axes[1].set_caption("(B) Treatment group")
218
126
  """
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
127
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
128
+ ax_record.caption = caption
129
+ return self
250
130
 
251
- return mcolors.to_hex(fc[0])
252
- except Exception:
253
- pass
254
- return None
131
+ @property
132
+ def panel_caption(self) -> Optional[str]:
133
+ """Get the panel caption metadata."""
134
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
135
+ return ax_record.caption
255
136
 
256
137
  def no_record(self):
257
138
  """Context manager to temporarily disable recording.
@@ -271,116 +152,22 @@ class RecordingAxes:
271
152
  data_arrays: Dict[str, np.ndarray],
272
153
  call_id: Optional[str] = None,
273
154
  ) -> 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
- """
155
+ """Record a seaborn plotting call."""
289
156
  if not self._track:
290
157
  return
291
158
 
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
- )
329
-
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
159
+ from ._axes_seaborn import record_seaborn_call
371
160
 
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,
161
+ record_seaborn_call(
162
+ self._recorder,
163
+ self._position,
164
+ func_name,
165
+ args,
166
+ kwargs,
167
+ data_arrays,
168
+ call_id,
378
169
  )
379
170
 
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
171
  # Expose common properties directly
385
172
  @property
386
173
  def figure(self):
@@ -402,64 +189,18 @@ class RecordingAxes:
402
189
  track: bool = True,
403
190
  **kwargs,
404
191
  ):
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
192
+ """Pie chart with automatic SCITEX styling."""
193
+ from ._axes_plots import pie_plot
194
+
195
+ return pie_plot(
196
+ self._ax,
197
+ x,
198
+ self._recorder,
199
+ self._position,
200
+ self._track and track,
201
+ id,
202
+ **kwargs,
203
+ )
463
204
 
464
205
  def imshow(
465
206
  self,
@@ -469,61 +210,18 @@ class RecordingAxes:
469
210
  track: bool = True,
470
211
  **kwargs,
471
212
  ):
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
213
+ """Display image with automatic SCITEX styling."""
214
+ from ._axes_plots import imshow_plot
215
+
216
+ return imshow_plot(
217
+ self._ax,
218
+ X,
219
+ self._recorder,
220
+ self._position,
221
+ self._track and track,
222
+ id,
223
+ **kwargs,
224
+ )
527
225
 
528
226
  def violinplot(
529
227
  self,
@@ -535,238 +233,21 @@ class RecordingAxes:
535
233
  inner: Optional[str] = None,
536
234
  **kwargs,
537
235
  ):
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
236
+ """Violin plot with support for inner display options."""
237
+ from ._axes_plots import violinplot_plot
562
238
 
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")
570
-
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(
239
+ return violinplot_plot(
240
+ self._ax,
578
241
  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,
242
+ positions,
243
+ self._recorder,
244
+ self._position,
245
+ self._track and track,
246
+ id,
247
+ inner,
583
248
  **kwargs,
584
249
  )
585
250
 
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
251
  # Methods that should not be recorded
771
252
  def get_xlim(self):
772
253
  return self._ax.get_xlim()
@@ -796,158 +277,24 @@ class RecordingAxes:
796
277
  track: bool = True,
797
278
  **kwargs,
798
279
  ):
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
280
+ """Create a joyplot (ridgeline plot) for distribution comparison."""
281
+ from ._axes_plots import joyplot_plot
282
+
283
+ return joyplot_plot(
284
+ self._ax,
285
+ self,
286
+ arrays,
287
+ self._recorder,
288
+ self._position,
289
+ self._track and track,
290
+ id,
291
+ overlap,
292
+ fill_alpha,
293
+ line_alpha,
294
+ colors,
295
+ labels,
296
+ **kwargs,
297
+ )
951
298
 
952
299
  def swarmplot(
953
300
  self,
@@ -962,167 +309,29 @@ class RecordingAxes:
962
309
  track: bool = True,
963
310
  **kwargs,
964
311
  ):
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.
992
-
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)
1054
-
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
- )
1069
-
1070
- return results
1071
-
1072
- def _beeswarm_positions(
1073
- 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.
1082
-
1083
- Parameters
1084
- ----------
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.
1096
- """
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
312
+ """Create a swarm plot (beeswarm plot) showing individual data points."""
313
+ from ._axes_plots import swarmplot_plot
314
+
315
+ return swarmplot_plot(
316
+ self._ax,
317
+ data,
318
+ positions,
319
+ self._recorder,
320
+ self._position,
321
+ self._track and track,
322
+ id,
323
+ size,
324
+ color,
325
+ alpha,
326
+ jitter,
327
+ **kwargs,
328
+ )
1121
329
 
1122
- # Restore original order
1123
- result = np.zeros(n)
1124
- result[order] = x_offsets
1125
- return result
330
+ @property
331
+ def caption(self) -> Optional[str]:
332
+ """Get the panel caption metadata."""
333
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
334
+ return ax_record.caption
1126
335
 
1127
336
 
1128
337
  class _NoRecordContext: