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.
- figrecipe/__init__.py +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
figrecipe/_wrappers/_axes.py
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
149
|
-
"""
|
|
104
|
+
def set_caption(self, caption: str) -> "RecordingAxes":
|
|
105
|
+
"""Set panel caption metadata (not rendered, stored in recipe).
|
|
150
106
|
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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:
|