figrecipe 0.5.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 (189) hide show
  1. figrecipe/__init__.py +220 -819
  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 +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  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/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -2,10 +2,9 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """Wrapped Axes that records all plotting calls."""
4
4
 
5
- from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
6
6
 
7
7
  import numpy as np
8
- import matplotlib.pyplot as plt
9
8
  from matplotlib.axes import Axes
10
9
 
11
10
  if TYPE_CHECKING:
@@ -35,6 +34,11 @@ class RecordingAxes:
35
34
  >>> # The call is recorded automatically
36
35
  """
37
36
 
37
+ # Methods whose results can be referenced by other methods (e.g., clabel needs ContourSet)
38
+ RESULT_REFERENCEABLE_METHODS = {"contour", "contourf"}
39
+ # Methods that take results from other methods as arguments
40
+ RESULT_REFERENCING_METHODS = {"clabel"}
41
+
38
42
  def __init__(
39
43
  self,
40
44
  ax: Axes,
@@ -45,6 +49,8 @@ class RecordingAxes:
45
49
  self._recorder = recorder
46
50
  self._position = position
47
51
  self._track = True
52
+ # Map matplotlib result objects (by id) to their source call_id
53
+ self._result_refs: Dict[int, str] = {}
48
54
 
49
55
  @property
50
56
  def ax(self) -> Axes:
@@ -73,95 +79,60 @@ class RecordingAxes:
73
79
  return attr
74
80
 
75
81
  def _create_recording_wrapper(self, method_name: str, method: callable):
76
- """Create a wrapper function that records the call.
77
-
78
- Parameters
79
- ----------
80
- method_name : str
81
- Name of the method.
82
- method : callable
83
- The original method.
82
+ """Create a wrapper function that records the call."""
83
+ from ._axes_helpers import record_call_with_color_capture
84
84
 
85
- Returns
86
- -------
87
- callable
88
- Wrapped method that records calls.
89
- """
90
85
  def wrapper(*args, id: Optional[str] = None, track: bool = True, **kwargs):
91
- # Call the original method first (without our custom kwargs)
92
86
  result = method(*args, **kwargs)
93
-
94
- # Record the call if tracking is enabled
95
87
  if self._track and track:
96
- # Capture actual colors from result for plotting methods
97
- # that use matplotlib's color cycle
98
- recorded_kwargs = kwargs.copy()
99
- if method_name in ('plot', 'scatter', 'bar', 'barh', 'step', 'fill_between'):
100
- if 'color' not in recorded_kwargs and 'c' not in recorded_kwargs:
101
- actual_color = self._extract_color_from_result(method_name, result)
102
- if actual_color is not None:
103
- recorded_kwargs['color'] = actual_color
104
-
105
- self._recorder.record_call(
106
- ax_position=self._position,
107
- method_name=method_name,
108
- args=args,
109
- kwargs=recorded_kwargs,
110
- 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,
111
99
  )
112
-
113
100
  return result
114
101
 
115
102
  return wrapper
116
103
 
117
- def _extract_color_from_result(self, method_name: str, result) -> Optional[str]:
118
- """Extract actual color used from plot result.
104
+ def set_caption(self, caption: str) -> "RecordingAxes":
105
+ """Set panel caption metadata (not rendered, stored in recipe).
106
+
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").
119
110
 
120
111
  Parameters
121
112
  ----------
122
- method_name : str
123
- Name of the plotting method.
124
- result : Any
125
- Return value from the plotting method.
113
+ caption : str
114
+ The panel caption text.
126
115
 
127
116
  Returns
128
117
  -------
129
- str or None
130
- The color used, or None if not extractable.
118
+ RecordingAxes
119
+ Self for method chaining.
120
+
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")
131
126
  """
132
- try:
133
- if method_name == 'plot':
134
- # plot() returns list of Line2D
135
- if result and hasattr(result[0], 'get_color'):
136
- return result[0].get_color()
137
- elif method_name == 'scatter':
138
- # scatter() returns PathCollection
139
- if hasattr(result, 'get_facecolor'):
140
- fc = result.get_facecolor()
141
- if len(fc) > 0:
142
- # Convert RGBA to hex
143
- import matplotlib.colors as mcolors
144
- return mcolors.to_hex(fc[0])
145
- elif method_name in ('bar', 'barh'):
146
- # bar() returns BarContainer
147
- if hasattr(result, 'patches') and result.patches:
148
- fc = result.patches[0].get_facecolor()
149
- import matplotlib.colors as mcolors
150
- return mcolors.to_hex(fc)
151
- elif method_name == 'step':
152
- # step() returns list of Line2D
153
- if result and hasattr(result[0], 'get_color'):
154
- return result[0].get_color()
155
- elif method_name == 'fill_between':
156
- # fill_between() returns PolyCollection
157
- if hasattr(result, 'get_facecolor'):
158
- fc = result.get_facecolor()
159
- if len(fc) > 0:
160
- import matplotlib.colors as mcolors
161
- return mcolors.to_hex(fc[0])
162
- except Exception:
163
- pass
164
- return None
127
+ ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
128
+ ax_record.caption = caption
129
+ return self
130
+
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
165
136
 
166
137
  def no_record(self):
167
138
  """Context manager to temporarily disable recording.
@@ -181,105 +152,21 @@ class RecordingAxes:
181
152
  data_arrays: Dict[str, np.ndarray],
182
153
  call_id: Optional[str] = None,
183
154
  ) -> None:
184
- """Record a seaborn plotting call.
185
-
186
- Parameters
187
- ----------
188
- func_name : str
189
- Name of the seaborn function (e.g., 'scatterplot').
190
- args : tuple
191
- Processed positional arguments.
192
- kwargs : dict
193
- Processed keyword arguments.
194
- data_arrays : dict
195
- Dictionary of array data extracted from DataFrame/arrays.
196
- call_id : str, optional
197
- Custom ID for this call.
198
- """
155
+ """Record a seaborn plotting call."""
199
156
  if not self._track:
200
157
  return
201
158
 
202
- from .._utils._numpy_io import should_store_inline, to_serializable
203
-
204
- # Generate call ID if not provided
205
- if call_id is None:
206
- call_id = self._recorder._generate_call_id(f"sns_{func_name}")
207
-
208
- # Process data arrays into args format
209
- processed_args = []
210
- for i, arg in enumerate(args):
211
- if arg == "__ARRAY__":
212
- key = f"_arg_{i}"
213
- if key in data_arrays:
214
- arr = data_arrays[key]
215
- if should_store_inline(arr):
216
- processed_args.append({
217
- "name": f"arg{i}",
218
- "data": to_serializable(arr),
219
- "dtype": str(arr.dtype),
220
- })
221
- else:
222
- processed_args.append({
223
- "name": f"arg{i}",
224
- "data": "__FILE__",
225
- "dtype": str(arr.dtype),
226
- "_array": arr,
227
- })
228
- else:
229
- processed_args.append({
230
- "name": f"arg{i}",
231
- "data": arg,
232
- })
233
-
234
- # Process DataFrame column data
235
- for key, arr in data_arrays.items():
236
- if key.startswith("_col_"):
237
- param_name = key[5:] # Remove "_col_" prefix
238
- col_name = data_arrays.get(f"_colname_{param_name}", param_name)
239
- if should_store_inline(arr):
240
- processed_args.append({
241
- "name": col_name,
242
- "param": param_name,
243
- "data": to_serializable(arr),
244
- "dtype": str(arr.dtype),
245
- })
246
- else:
247
- processed_args.append({
248
- "name": col_name,
249
- "param": param_name,
250
- "data": "__FILE__",
251
- "dtype": str(arr.dtype),
252
- "_array": arr,
253
- })
254
-
255
- # Process kwarg arrays
256
- processed_kwargs = dict(kwargs)
257
- for key, value in kwargs.items():
258
- if value == "__ARRAY__":
259
- arr_key = f"_kwarg_{key}"
260
- if arr_key in data_arrays:
261
- arr = data_arrays[arr_key]
262
- if should_store_inline(arr):
263
- processed_kwargs[key] = to_serializable(arr)
264
- else:
265
- # Mark for file storage
266
- processed_kwargs[key] = "__FILE__"
267
- processed_kwargs[f"_array_{key}"] = arr
268
-
269
- # Create call record
270
- from .._recorder import CallRecord
271
-
272
- record = CallRecord(
273
- id=call_id,
274
- function=f"sns.{func_name}",
275
- args=processed_args,
276
- kwargs=processed_kwargs,
277
- ax_position=self._position,
278
- )
159
+ from ._axes_seaborn import record_seaborn_call
279
160
 
280
- # Add to axes record
281
- ax_record = self._recorder.figure_record.get_or_create_axes(*self._position)
282
- ax_record.add_call(record)
161
+ record_seaborn_call(
162
+ self._recorder,
163
+ self._position,
164
+ func_name,
165
+ args,
166
+ kwargs,
167
+ data_arrays,
168
+ call_id,
169
+ )
283
170
 
284
171
  # Expose common properties directly
285
172
  @property
@@ -294,6 +181,73 @@ class RecordingAxes:
294
181
  def yaxis(self):
295
182
  return self._ax.yaxis
296
183
 
184
+ def pie(
185
+ self,
186
+ x,
187
+ *,
188
+ id: Optional[str] = None,
189
+ track: bool = True,
190
+ **kwargs,
191
+ ):
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
+ )
204
+
205
+ def imshow(
206
+ self,
207
+ X,
208
+ *,
209
+ id: Optional[str] = None,
210
+ track: bool = True,
211
+ **kwargs,
212
+ ):
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
+ )
225
+
226
+ def violinplot(
227
+ self,
228
+ dataset,
229
+ positions=None,
230
+ *,
231
+ id: Optional[str] = None,
232
+ track: bool = True,
233
+ inner: Optional[str] = None,
234
+ **kwargs,
235
+ ):
236
+ """Violin plot with support for inner display options."""
237
+ from ._axes_plots import violinplot_plot
238
+
239
+ return violinplot_plot(
240
+ self._ax,
241
+ dataset,
242
+ positions,
243
+ self._recorder,
244
+ self._position,
245
+ self._track and track,
246
+ id,
247
+ inner,
248
+ **kwargs,
249
+ )
250
+
297
251
  # Methods that should not be recorded
298
252
  def get_xlim(self):
299
253
  return self._ax.get_xlim()
@@ -310,6 +264,75 @@ class RecordingAxes:
310
264
  def get_title(self):
311
265
  return self._ax.get_title()
312
266
 
267
+ def joyplot(
268
+ self,
269
+ arrays,
270
+ *,
271
+ overlap: float = 0.5,
272
+ fill_alpha: float = 0.7,
273
+ line_alpha: float = 1.0,
274
+ colors=None,
275
+ labels=None,
276
+ id: Optional[str] = None,
277
+ track: bool = True,
278
+ **kwargs,
279
+ ):
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
+ )
298
+
299
+ def swarmplot(
300
+ self,
301
+ data,
302
+ positions=None,
303
+ *,
304
+ size: float = None,
305
+ color=None,
306
+ alpha: float = 0.7,
307
+ jitter: float = 0.3,
308
+ id: Optional[str] = None,
309
+ track: bool = True,
310
+ **kwargs,
311
+ ):
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
+ )
329
+
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
335
+
313
336
 
314
337
  class _NoRecordContext:
315
338
  """Context manager to temporarily disable recording."""
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Helper functions for RecordingAxes."""
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ def args_have_fmt_color(args: tuple) -> bool:
9
+ """Check if args contain a matplotlib fmt string with color specifier."""
10
+ color_codes = set("bgrcmykw")
11
+ for arg in args:
12
+ if isinstance(arg, str) and len(arg) >= 1 and len(arg) <= 4:
13
+ if arg[0] in color_codes:
14
+ return True
15
+ return False
16
+
17
+
18
+ def extract_color_from_result(method_name: str, result) -> Optional[str]:
19
+ """Extract actual color used from plot result."""
20
+ try:
21
+ if method_name == "plot":
22
+ if result and hasattr(result[0], "get_color"):
23
+ return result[0].get_color()
24
+ elif method_name == "scatter":
25
+ if hasattr(result, "get_facecolor"):
26
+ fc = result.get_facecolor()
27
+ if len(fc) > 0:
28
+ import matplotlib.colors as mcolors
29
+
30
+ return mcolors.to_hex(fc[0])
31
+ elif method_name in ("bar", "barh"):
32
+ if hasattr(result, "patches") and result.patches:
33
+ fc = result.patches[0].get_facecolor()
34
+ import matplotlib.colors as mcolors
35
+
36
+ return mcolors.to_hex(fc)
37
+ elif method_name == "step":
38
+ if result and hasattr(result[0], "get_color"):
39
+ return result[0].get_color()
40
+ elif method_name == "fill_between":
41
+ if hasattr(result, "get_facecolor"):
42
+ fc = result.get_facecolor()
43
+ if len(fc) > 0:
44
+ import matplotlib.colors as mcolors
45
+
46
+ return mcolors.to_hex(fc[0])
47
+ except Exception:
48
+ pass
49
+ return None
50
+
51
+
52
+ def process_result_refs_in_args(
53
+ args: tuple,
54
+ method_name: str,
55
+ result_refs: Dict[int, str],
56
+ referencing_methods: set,
57
+ ) -> tuple:
58
+ """Process args to replace matplotlib objects with references."""
59
+ if method_name not in referencing_methods:
60
+ return args
61
+
62
+ import builtins
63
+
64
+ processed = []
65
+ for arg in args:
66
+ obj_id = builtins.id(arg)
67
+ if obj_id in result_refs:
68
+ processed.append({"__ref__": result_refs[obj_id]})
69
+ else:
70
+ processed.append(arg)
71
+ return tuple(processed)
72
+
73
+
74
+ def record_call_with_color_capture(
75
+ recorder,
76
+ position: tuple,
77
+ method_name: str,
78
+ args: tuple,
79
+ kwargs: dict,
80
+ result,
81
+ call_id: Optional[str],
82
+ result_refs: Dict[int, str],
83
+ referencing_methods: set,
84
+ referenceable_methods: set,
85
+ ) -> Any:
86
+ """Record a call with color capture and result reference handling."""
87
+ recorded_kwargs = kwargs.copy()
88
+
89
+ # Capture colors for methods using color cycle
90
+ if method_name in ("plot", "scatter", "bar", "barh", "step", "fill_between"):
91
+ has_fmt_color = args_have_fmt_color(args)
92
+ if (
93
+ "color" not in recorded_kwargs
94
+ and "c" not in recorded_kwargs
95
+ and not has_fmt_color
96
+ ):
97
+ actual_color = extract_color_from_result(method_name, result)
98
+ if actual_color is not None:
99
+ recorded_kwargs["color"] = actual_color
100
+
101
+ if method_name == "fill_between" and "edgecolor" not in recorded_kwargs:
102
+ if hasattr(result, "get_edgecolor"):
103
+ ec = result.get_edgecolor()
104
+ if len(ec) == 0:
105
+ recorded_kwargs["edgecolor"] = "none"
106
+
107
+ # Process args for result references
108
+ processed_args = process_result_refs_in_args(
109
+ args, method_name, result_refs, referencing_methods
110
+ )
111
+
112
+ call_record = recorder.record_call(
113
+ ax_position=position,
114
+ method_name=method_name,
115
+ args=processed_args,
116
+ kwargs=recorded_kwargs,
117
+ call_id=call_id,
118
+ )
119
+
120
+ # Store result reference if applicable
121
+ if method_name in referenceable_methods:
122
+ import builtins
123
+
124
+ result_refs[builtins.id(result)] = call_record.id
125
+
126
+ return call_record
127
+
128
+
129
+ __all__ = [
130
+ "args_have_fmt_color",
131
+ "extract_color_from_result",
132
+ "process_result_refs_in_args",
133
+ "record_call_with_color_capture",
134
+ ]
135
+
136
+ # EOF