figrecipe 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +361 -93
- figrecipe/_dev/__init__.py +120 -0
- figrecipe/_dev/demo_plotters/__init__.py +195 -0
- figrecipe/_dev/demo_plotters/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_pie.py +23 -0
- figrecipe/_dev/demo_plotters/plot_plot.py +27 -0
- figrecipe/_dev/demo_plotters/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/plot_streamplot.py +30 -0
- figrecipe/_dev/demo_plotters/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_xcorr.py +25 -0
- figrecipe/_editor/__init__.py +230 -0
- figrecipe/_editor/_bbox.py +978 -0
- figrecipe/_editor/_flask_app.py +1229 -0
- figrecipe/_editor/_hitmap.py +937 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_renderer.py +349 -0
- figrecipe/_editor/_templates/__init__.py +75 -0
- figrecipe/_editor/_templates/_html.py +406 -0
- figrecipe/_editor/_templates/_scripts.py +2778 -0
- figrecipe/_editor/_templates/_styles.py +1326 -0
- figrecipe/_params/_DECORATION_METHODS.py +27 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +126 -73
- figrecipe/_reproducer.py +658 -41
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_loader.py +515 -56
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +860 -46
- figrecipe/_wrappers/_figure.py +115 -18
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +9 -10
- figrecipe/styles/_style_applier.py +332 -28
- figrecipe/styles/_style_loader.py +172 -44
- figrecipe/styles/presets/MATPLOTLIB.yaml +94 -0
- figrecipe/styles/presets/SCITEX.yaml +176 -0
- figrecipe-0.6.0.dist-info/METADATA +394 -0
- figrecipe-0.6.0.dist-info/RECORD +90 -0
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/licenses/LICENSE +0 -0
figrecipe/_reproducer.py
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
"""Reproduce figures from recipe files."""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Dict, List, Optional,
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
7
|
|
|
8
8
|
import matplotlib.pyplot as plt
|
|
9
9
|
import numpy as np
|
|
10
10
|
from matplotlib.axes import Axes
|
|
11
|
-
from matplotlib.figure import Figure
|
|
12
11
|
|
|
13
|
-
from ._recorder import
|
|
12
|
+
from ._recorder import CallRecord, FigureRecord
|
|
14
13
|
from ._serializer import load_recipe
|
|
15
14
|
|
|
16
15
|
|
|
@@ -18,32 +17,77 @@ def reproduce(
|
|
|
18
17
|
path: Union[str, Path],
|
|
19
18
|
calls: Optional[List[str]] = None,
|
|
20
19
|
skip_decorations: bool = False,
|
|
21
|
-
|
|
20
|
+
apply_overrides: bool = True,
|
|
21
|
+
):
|
|
22
22
|
"""Reproduce a figure from a recipe file.
|
|
23
23
|
|
|
24
24
|
Parameters
|
|
25
25
|
----------
|
|
26
26
|
path : str or Path
|
|
27
|
-
Path to .yaml recipe file.
|
|
27
|
+
Path to .yaml or .png recipe file. If .png is provided,
|
|
28
|
+
the corresponding .yaml file will be loaded.
|
|
28
29
|
calls : list of str, optional
|
|
29
30
|
If provided, only reproduce these specific call IDs.
|
|
30
31
|
skip_decorations : bool
|
|
31
32
|
If True, skip decoration calls (labels, legends, etc.).
|
|
33
|
+
apply_overrides : bool
|
|
34
|
+
If True (default), apply .overrides.json if it exists.
|
|
35
|
+
This preserves manual GUI editor changes.
|
|
32
36
|
|
|
33
37
|
Returns
|
|
34
38
|
-------
|
|
35
|
-
fig :
|
|
36
|
-
Reproduced figure.
|
|
37
|
-
axes :
|
|
38
|
-
Reproduced axes (single if 1x1, otherwise
|
|
39
|
+
fig : RecordingFigure
|
|
40
|
+
Reproduced figure (same type as subplots() returns).
|
|
41
|
+
axes : RecordingAxes or ndarray of RecordingAxes
|
|
42
|
+
Reproduced axes (single if 1x1, otherwise numpy array).
|
|
39
43
|
|
|
40
44
|
Examples
|
|
41
45
|
--------
|
|
42
46
|
>>> import figrecipe as ps
|
|
43
47
|
>>> fig, ax = ps.reproduce("experiment_001.yaml")
|
|
48
|
+
>>> fig, ax = ps.reproduce("experiment_001.png") # Also works
|
|
44
49
|
>>> plt.show()
|
|
45
50
|
"""
|
|
51
|
+
path = Path(path)
|
|
52
|
+
|
|
53
|
+
# Accept both .png and .yaml - find the yaml file
|
|
54
|
+
if path.suffix.lower() in (".png", ".jpg", ".jpeg", ".pdf", ".svg"):
|
|
55
|
+
yaml_path = path.with_suffix(".yaml")
|
|
56
|
+
if not yaml_path.exists():
|
|
57
|
+
raise FileNotFoundError(
|
|
58
|
+
f"Recipe file not found: {yaml_path}. "
|
|
59
|
+
f"Expected .yaml file alongside {path}"
|
|
60
|
+
)
|
|
61
|
+
path = yaml_path
|
|
62
|
+
|
|
46
63
|
record = load_recipe(path)
|
|
64
|
+
|
|
65
|
+
# Check for override file and merge if exists
|
|
66
|
+
if apply_overrides:
|
|
67
|
+
overrides_path = path.with_suffix(".overrides.json")
|
|
68
|
+
if overrides_path.exists():
|
|
69
|
+
import json
|
|
70
|
+
|
|
71
|
+
with open(overrides_path) as f:
|
|
72
|
+
data = json.load(f)
|
|
73
|
+
|
|
74
|
+
# Apply style overrides
|
|
75
|
+
manual_overrides = data.get("manual_overrides", {})
|
|
76
|
+
if manual_overrides:
|
|
77
|
+
# Merge overrides into record style
|
|
78
|
+
if record.style is None:
|
|
79
|
+
record.style = {}
|
|
80
|
+
record.style.update(manual_overrides)
|
|
81
|
+
|
|
82
|
+
# Apply call overrides (kwargs changes from editor)
|
|
83
|
+
call_overrides = data.get("call_overrides", {})
|
|
84
|
+
if call_overrides:
|
|
85
|
+
for ax_key, ax_record in record.axes.items():
|
|
86
|
+
for call in ax_record.calls:
|
|
87
|
+
if call.id in call_overrides:
|
|
88
|
+
# Merge call kwargs overrides
|
|
89
|
+
call.kwargs.update(call_overrides[call.id])
|
|
90
|
+
|
|
47
91
|
return reproduce_from_record(
|
|
48
92
|
record,
|
|
49
93
|
calls=calls,
|
|
@@ -55,7 +99,7 @@ def reproduce_from_record(
|
|
|
55
99
|
record: FigureRecord,
|
|
56
100
|
calls: Optional[List[str]] = None,
|
|
57
101
|
skip_decorations: bool = False,
|
|
58
|
-
)
|
|
102
|
+
):
|
|
59
103
|
"""Reproduce a figure from a FigureRecord.
|
|
60
104
|
|
|
61
105
|
Parameters
|
|
@@ -69,11 +113,14 @@ def reproduce_from_record(
|
|
|
69
113
|
|
|
70
114
|
Returns
|
|
71
115
|
-------
|
|
72
|
-
fig :
|
|
73
|
-
Reproduced figure.
|
|
74
|
-
axes :
|
|
75
|
-
Reproduced axes.
|
|
116
|
+
fig : RecordingFigure
|
|
117
|
+
Reproduced figure (wrapped).
|
|
118
|
+
axes : RecordingAxes or ndarray of RecordingAxes
|
|
119
|
+
Reproduced axes (wrapped, numpy array for multi-axes).
|
|
76
120
|
"""
|
|
121
|
+
from ._recorder import Recorder
|
|
122
|
+
from ._wrappers import RecordingAxes, RecordingFigure
|
|
123
|
+
|
|
77
124
|
# Determine grid size from axes positions
|
|
78
125
|
max_row = 0
|
|
79
126
|
max_col = 0
|
|
@@ -95,8 +142,8 @@ def reproduce_from_record(
|
|
|
95
142
|
constrained_layout=record.constrained_layout,
|
|
96
143
|
)
|
|
97
144
|
|
|
98
|
-
# Apply layout if recorded
|
|
99
|
-
if record.layout is not None:
|
|
145
|
+
# Apply layout if recorded (skip if constrained_layout is used)
|
|
146
|
+
if record.layout is not None and not record.constrained_layout:
|
|
100
147
|
fig.subplots_adjust(**record.layout)
|
|
101
148
|
|
|
102
149
|
# Ensure axes is 2D array
|
|
@@ -113,10 +160,14 @@ def reproduce_from_record(
|
|
|
113
160
|
# style is applied during subplots(), then user creates plots/decorations)
|
|
114
161
|
if record.style is not None:
|
|
115
162
|
from .styles import apply_style_mm
|
|
163
|
+
|
|
116
164
|
for row in range(nrows):
|
|
117
165
|
for col in range(ncols):
|
|
118
166
|
apply_style_mm(axes_2d[row, col], record.style)
|
|
119
167
|
|
|
168
|
+
# Result cache for resolving references (e.g., clabel needs ContourSet from contour)
|
|
169
|
+
result_cache: Dict[str, Any] = {}
|
|
170
|
+
|
|
120
171
|
# Replay calls on each axes
|
|
121
172
|
for ax_key, ax_record in record.axes.items():
|
|
122
173
|
parts = ax_key.split("_")
|
|
@@ -131,27 +182,72 @@ def reproduce_from_record(
|
|
|
131
182
|
for call in ax_record.calls:
|
|
132
183
|
if calls is not None and call.id not in calls:
|
|
133
184
|
continue
|
|
134
|
-
_replay_call(ax, call)
|
|
185
|
+
result = _replay_call(ax, call, result_cache)
|
|
186
|
+
if result is not None:
|
|
187
|
+
result_cache[call.id] = result
|
|
135
188
|
|
|
136
189
|
# Replay decorations
|
|
137
190
|
if not skip_decorations:
|
|
138
191
|
for call in ax_record.decorations:
|
|
139
192
|
if calls is not None and call.id not in calls:
|
|
140
193
|
continue
|
|
141
|
-
_replay_call(ax, call)
|
|
142
|
-
|
|
143
|
-
|
|
194
|
+
result = _replay_call(ax, call, result_cache)
|
|
195
|
+
if result is not None:
|
|
196
|
+
result_cache[call.id] = result
|
|
197
|
+
|
|
198
|
+
# Finalize tick configuration (avoids categorical axis interference)
|
|
199
|
+
from .styles._style_applier import finalize_ticks
|
|
200
|
+
|
|
201
|
+
for row in range(nrows):
|
|
202
|
+
for col in range(ncols):
|
|
203
|
+
finalize_ticks(axes_2d[row, col])
|
|
204
|
+
|
|
205
|
+
# Apply figure-level labels if recorded
|
|
206
|
+
if record.suptitle is not None:
|
|
207
|
+
text = record.suptitle.get("text", "")
|
|
208
|
+
kwargs = record.suptitle.get("kwargs", {}).copy()
|
|
209
|
+
# Only add y=1.02 if not using constrained_layout (which handles positioning)
|
|
210
|
+
if "y" not in kwargs and not record.constrained_layout:
|
|
211
|
+
kwargs["y"] = 1.02
|
|
212
|
+
fig.suptitle(text, **kwargs)
|
|
213
|
+
|
|
214
|
+
if record.supxlabel is not None:
|
|
215
|
+
text = record.supxlabel.get("text", "")
|
|
216
|
+
kwargs = record.supxlabel.get("kwargs", {})
|
|
217
|
+
fig.supxlabel(text, **kwargs)
|
|
218
|
+
|
|
219
|
+
if record.supylabel is not None:
|
|
220
|
+
text = record.supylabel.get("text", "")
|
|
221
|
+
kwargs = record.supylabel.get("kwargs", {})
|
|
222
|
+
fig.supylabel(text, **kwargs)
|
|
223
|
+
|
|
224
|
+
# Wrap in Recording types (same as subplots() returns)
|
|
225
|
+
recorder = Recorder()
|
|
226
|
+
recorder._figure_record = record
|
|
227
|
+
|
|
228
|
+
# Wrap axes in RecordingAxes
|
|
229
|
+
wrapped_axes = np.empty((nrows, ncols), dtype=object)
|
|
230
|
+
for i in range(nrows):
|
|
231
|
+
for j in range(ncols):
|
|
232
|
+
wrapped_axes[i, j] = RecordingAxes(axes_2d[i, j], recorder, position=(i, j))
|
|
233
|
+
|
|
234
|
+
# Create RecordingFigure
|
|
235
|
+
wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes.tolist())
|
|
236
|
+
|
|
237
|
+
# Return in appropriate format (matching subplots() behavior)
|
|
144
238
|
if nrows == 1 and ncols == 1:
|
|
145
|
-
return
|
|
239
|
+
return wrapped_fig, wrapped_axes[0, 0]
|
|
146
240
|
elif nrows == 1:
|
|
147
|
-
return
|
|
241
|
+
return wrapped_fig, np.array(wrapped_axes[0], dtype=object)
|
|
148
242
|
elif ncols == 1:
|
|
149
|
-
return
|
|
243
|
+
return wrapped_fig, np.array(wrapped_axes[:, 0], dtype=object)
|
|
150
244
|
else:
|
|
151
|
-
return
|
|
245
|
+
return wrapped_fig, wrapped_axes
|
|
152
246
|
|
|
153
247
|
|
|
154
|
-
def _replay_call(
|
|
248
|
+
def _replay_call(
|
|
249
|
+
ax: Axes, call: CallRecord, result_cache: Optional[Dict[str, Any]] = None
|
|
250
|
+
) -> Any:
|
|
155
251
|
"""Replay a single call on an axes.
|
|
156
252
|
|
|
157
253
|
Parameters
|
|
@@ -160,18 +256,35 @@ def _replay_call(ax: Axes, call: CallRecord) -> Any:
|
|
|
160
256
|
The matplotlib axes.
|
|
161
257
|
call : CallRecord
|
|
162
258
|
The call to replay.
|
|
259
|
+
result_cache : dict, optional
|
|
260
|
+
Cache mapping call_id -> result for resolving references.
|
|
163
261
|
|
|
164
262
|
Returns
|
|
165
263
|
-------
|
|
166
264
|
Any
|
|
167
265
|
Result of the matplotlib call.
|
|
168
266
|
"""
|
|
267
|
+
if result_cache is None:
|
|
268
|
+
result_cache = {}
|
|
269
|
+
|
|
169
270
|
method_name = call.function
|
|
170
271
|
|
|
171
272
|
# Check if it's a seaborn call
|
|
172
273
|
if method_name.startswith("sns."):
|
|
173
274
|
return _replay_seaborn_call(ax, call)
|
|
174
275
|
|
|
276
|
+
# Handle violinplot with inner option specially
|
|
277
|
+
if method_name == "violinplot":
|
|
278
|
+
return _replay_violinplot_call(ax, call)
|
|
279
|
+
|
|
280
|
+
# Handle joyplot specially (custom method)
|
|
281
|
+
if method_name == "joyplot":
|
|
282
|
+
return _replay_joyplot_call(ax, call)
|
|
283
|
+
|
|
284
|
+
# Handle swarmplot specially (custom method)
|
|
285
|
+
if method_name == "swarmplot":
|
|
286
|
+
return _replay_swarmplot_call(ax, call)
|
|
287
|
+
|
|
175
288
|
method = getattr(ax, method_name, None)
|
|
176
289
|
|
|
177
290
|
if method is None:
|
|
@@ -181,11 +294,22 @@ def _replay_call(ax: Axes, call: CallRecord) -> Any:
|
|
|
181
294
|
# Reconstruct args
|
|
182
295
|
args = []
|
|
183
296
|
for arg_data in call.args:
|
|
184
|
-
value = _reconstruct_value(arg_data)
|
|
297
|
+
value = _reconstruct_value(arg_data, result_cache)
|
|
185
298
|
args.append(value)
|
|
186
299
|
|
|
187
|
-
# Get kwargs
|
|
188
|
-
kwargs = call.kwargs
|
|
300
|
+
# Get kwargs and reconstruct arrays
|
|
301
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
302
|
+
|
|
303
|
+
# Handle special transform markers
|
|
304
|
+
if "transform" in kwargs:
|
|
305
|
+
transform_val = kwargs["transform"]
|
|
306
|
+
if transform_val == "axes":
|
|
307
|
+
kwargs["transform"] = ax.transAxes
|
|
308
|
+
elif transform_val == "data":
|
|
309
|
+
kwargs["transform"] = ax.transData
|
|
310
|
+
elif transform_val == "figure":
|
|
311
|
+
kwargs["transform"] = ax.figure.transFigure
|
|
312
|
+
# If it's already a Transform object or something else, leave it
|
|
189
313
|
|
|
190
314
|
# Call the method
|
|
191
315
|
try:
|
|
@@ -193,10 +317,208 @@ def _replay_call(ax: Axes, call: CallRecord) -> Any:
|
|
|
193
317
|
except Exception as e:
|
|
194
318
|
# Log warning but continue
|
|
195
319
|
import warnings
|
|
320
|
+
|
|
196
321
|
warnings.warn(f"Failed to replay {method_name}: {e}")
|
|
197
322
|
return None
|
|
198
323
|
|
|
199
324
|
|
|
325
|
+
def _reconstruct_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
326
|
+
"""Reconstruct kwargs, converting 2D lists back to numpy arrays.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
kwargs : dict
|
|
331
|
+
Raw kwargs from call record.
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
dict
|
|
336
|
+
Kwargs with arrays properly reconstructed.
|
|
337
|
+
"""
|
|
338
|
+
result = {}
|
|
339
|
+
for key, value in kwargs.items():
|
|
340
|
+
if isinstance(value, list) and len(value) > 0:
|
|
341
|
+
# Check if it's a 2D list (list of lists) - should be numpy array
|
|
342
|
+
if isinstance(value[0], list):
|
|
343
|
+
result[key] = np.array(value)
|
|
344
|
+
else:
|
|
345
|
+
# 1D list - could be array or just list, try to preserve
|
|
346
|
+
result[key] = value
|
|
347
|
+
else:
|
|
348
|
+
result[key] = value
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _replay_violinplot_call(ax: Axes, call: CallRecord) -> Any:
|
|
353
|
+
"""Replay a violinplot call with inner option support.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
ax : Axes
|
|
358
|
+
The matplotlib axes.
|
|
359
|
+
call : CallRecord
|
|
360
|
+
The violinplot call to replay.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
Any
|
|
365
|
+
Result of the violinplot call.
|
|
366
|
+
"""
|
|
367
|
+
# Reconstruct args
|
|
368
|
+
args = []
|
|
369
|
+
for arg_data in call.args:
|
|
370
|
+
value = _reconstruct_value(arg_data)
|
|
371
|
+
args.append(value)
|
|
372
|
+
|
|
373
|
+
# Get kwargs and reconstruct arrays
|
|
374
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
375
|
+
|
|
376
|
+
# Extract inner option (not a matplotlib kwarg)
|
|
377
|
+
inner = kwargs.pop("inner", "box")
|
|
378
|
+
|
|
379
|
+
# Get display options
|
|
380
|
+
showmeans = kwargs.pop("showmeans", False)
|
|
381
|
+
showmedians = kwargs.pop("showmedians", True)
|
|
382
|
+
showextrema = kwargs.pop("showextrema", False)
|
|
383
|
+
|
|
384
|
+
# When using inner box/swarm, suppress default median/extrema lines
|
|
385
|
+
if inner in ("box", "swarm"):
|
|
386
|
+
showmedians = False
|
|
387
|
+
showextrema = False
|
|
388
|
+
|
|
389
|
+
# Call matplotlib's violinplot
|
|
390
|
+
try:
|
|
391
|
+
result = ax.violinplot(
|
|
392
|
+
*args,
|
|
393
|
+
showmeans=showmeans,
|
|
394
|
+
showmedians=showmedians,
|
|
395
|
+
showextrema=showextrema,
|
|
396
|
+
**kwargs,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Get style settings for inner display
|
|
400
|
+
from .styles import get_style
|
|
401
|
+
|
|
402
|
+
style = get_style()
|
|
403
|
+
violin_style = style.get("violinplot", {}) if style else {}
|
|
404
|
+
|
|
405
|
+
# Apply alpha from style to violin bodies
|
|
406
|
+
alpha = violin_style.get("alpha", 0.7)
|
|
407
|
+
if "bodies" in result:
|
|
408
|
+
for body in result["bodies"]:
|
|
409
|
+
body.set_alpha(alpha)
|
|
410
|
+
|
|
411
|
+
# Determine positions
|
|
412
|
+
dataset = args[0] if args else []
|
|
413
|
+
positions = kwargs.get("positions")
|
|
414
|
+
if positions is None:
|
|
415
|
+
positions = list(range(1, len(dataset) + 1))
|
|
416
|
+
|
|
417
|
+
# Overlay inner elements based on inner type
|
|
418
|
+
if inner == "box":
|
|
419
|
+
_add_violin_inner_box(ax, dataset, positions, violin_style)
|
|
420
|
+
elif inner == "swarm":
|
|
421
|
+
_add_violin_inner_swarm(ax, dataset, positions, violin_style)
|
|
422
|
+
elif inner == "stick":
|
|
423
|
+
_add_violin_inner_stick(ax, dataset, positions, violin_style)
|
|
424
|
+
elif inner == "point":
|
|
425
|
+
_add_violin_inner_point(ax, dataset, positions, violin_style)
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
except Exception as e:
|
|
429
|
+
import warnings
|
|
430
|
+
|
|
431
|
+
warnings.warn(f"Failed to replay violinplot: {e}")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _add_violin_inner_box(ax: Axes, dataset, positions, style: Dict[str, Any]) -> None:
|
|
436
|
+
"""Add box plot inside violin for reproduction."""
|
|
437
|
+
from .styles._style_applier import mm_to_pt
|
|
438
|
+
|
|
439
|
+
whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
440
|
+
median_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
441
|
+
|
|
442
|
+
for data, pos in zip(dataset, positions):
|
|
443
|
+
data = np.asarray(data)
|
|
444
|
+
q1, median, q3 = np.percentile(data, [25, 50, 75])
|
|
445
|
+
iqr = q3 - q1
|
|
446
|
+
whisker_low = max(data.min(), q1 - 1.5 * iqr)
|
|
447
|
+
whisker_high = min(data.max(), q3 + 1.5 * iqr)
|
|
448
|
+
|
|
449
|
+
# Draw box (Q1 to Q3)
|
|
450
|
+
ax.vlines(pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3)
|
|
451
|
+
# Draw whiskers
|
|
452
|
+
ax.vlines(
|
|
453
|
+
pos, whisker_low, q1, colors="black", linewidths=whisker_lw * 0.5, zorder=3
|
|
454
|
+
)
|
|
455
|
+
ax.vlines(
|
|
456
|
+
pos, q3, whisker_high, colors="black", linewidths=whisker_lw * 0.5, zorder=3
|
|
457
|
+
)
|
|
458
|
+
# Draw median as a white dot with black edge
|
|
459
|
+
ax.scatter(
|
|
460
|
+
[pos],
|
|
461
|
+
[median],
|
|
462
|
+
s=median_size**2,
|
|
463
|
+
c="white",
|
|
464
|
+
edgecolors="black",
|
|
465
|
+
linewidths=whisker_lw,
|
|
466
|
+
zorder=4,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _add_violin_inner_swarm(
|
|
471
|
+
ax: Axes, dataset, positions, style: Dict[str, Any]
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Add swarm points inside violin for reproduction."""
|
|
474
|
+
from .styles._style_applier import mm_to_pt
|
|
475
|
+
|
|
476
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
477
|
+
|
|
478
|
+
for data, pos in zip(dataset, positions):
|
|
479
|
+
data = np.asarray(data)
|
|
480
|
+
n = len(data)
|
|
481
|
+
jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
|
|
482
|
+
x_positions = pos + jitter
|
|
483
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _add_violin_inner_stick(
|
|
487
|
+
ax: Axes, dataset, positions, style: Dict[str, Any]
|
|
488
|
+
) -> None:
|
|
489
|
+
"""Add stick markers inside violin for reproduction."""
|
|
490
|
+
from .styles._style_applier import mm_to_pt
|
|
491
|
+
|
|
492
|
+
lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
493
|
+
|
|
494
|
+
for data, pos in zip(dataset, positions):
|
|
495
|
+
data = np.asarray(data)
|
|
496
|
+
for val in data:
|
|
497
|
+
ax.hlines(
|
|
498
|
+
val,
|
|
499
|
+
pos - 0.05,
|
|
500
|
+
pos + 0.05,
|
|
501
|
+
colors="black",
|
|
502
|
+
linewidths=lw * 0.5,
|
|
503
|
+
alpha=0.3,
|
|
504
|
+
zorder=3,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _add_violin_inner_point(
|
|
509
|
+
ax: Axes, dataset, positions, style: Dict[str, Any]
|
|
510
|
+
) -> None:
|
|
511
|
+
"""Add point markers inside violin for reproduction."""
|
|
512
|
+
from .styles._style_applier import mm_to_pt
|
|
513
|
+
|
|
514
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
|
|
515
|
+
|
|
516
|
+
for data, pos in zip(dataset, positions):
|
|
517
|
+
data = np.asarray(data)
|
|
518
|
+
x_positions = np.full_like(data, pos)
|
|
519
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3)
|
|
520
|
+
|
|
521
|
+
|
|
200
522
|
def _replay_seaborn_call(ax: Axes, call: CallRecord) -> Any:
|
|
201
523
|
"""Replay a seaborn call on an axes.
|
|
202
524
|
|
|
@@ -213,10 +535,11 @@ def _replay_seaborn_call(ax: Axes, call: CallRecord) -> Any:
|
|
|
213
535
|
Result of the seaborn call.
|
|
214
536
|
"""
|
|
215
537
|
try:
|
|
216
|
-
import seaborn as sns
|
|
217
538
|
import pandas as pd
|
|
539
|
+
import seaborn as sns
|
|
218
540
|
except ImportError:
|
|
219
541
|
import warnings
|
|
542
|
+
|
|
220
543
|
warnings.warn("seaborn/pandas required to replay seaborn calls")
|
|
221
544
|
return None
|
|
222
545
|
|
|
@@ -226,6 +549,7 @@ def _replay_seaborn_call(ax: Axes, call: CallRecord) -> Any:
|
|
|
226
549
|
|
|
227
550
|
if func is None:
|
|
228
551
|
import warnings
|
|
552
|
+
|
|
229
553
|
warnings.warn(f"Seaborn function {func_name} not found")
|
|
230
554
|
return None
|
|
231
555
|
|
|
@@ -276,29 +600,318 @@ def _replay_seaborn_call(ax: Axes, call: CallRecord) -> Any:
|
|
|
276
600
|
return func(**kwargs)
|
|
277
601
|
except Exception as e:
|
|
278
602
|
import warnings
|
|
603
|
+
|
|
279
604
|
warnings.warn(f"Failed to replay sns.{func_name}: {e}")
|
|
280
605
|
return None
|
|
281
606
|
|
|
282
607
|
|
|
283
|
-
def
|
|
608
|
+
def _replay_joyplot_call(ax: Axes, call: CallRecord) -> Any:
|
|
609
|
+
"""Replay a joyplot call on an axes.
|
|
610
|
+
|
|
611
|
+
Parameters
|
|
612
|
+
----------
|
|
613
|
+
ax : Axes
|
|
614
|
+
The matplotlib axes.
|
|
615
|
+
call : CallRecord
|
|
616
|
+
The joyplot call to replay.
|
|
617
|
+
|
|
618
|
+
Returns
|
|
619
|
+
-------
|
|
620
|
+
Any
|
|
621
|
+
Result of the joyplot call.
|
|
622
|
+
"""
|
|
623
|
+
from scipy import stats
|
|
624
|
+
|
|
625
|
+
# Reconstruct args
|
|
626
|
+
arrays = []
|
|
627
|
+
for arg_data in call.args:
|
|
628
|
+
value = _reconstruct_value(arg_data)
|
|
629
|
+
if isinstance(value, list):
|
|
630
|
+
# Could be a list of arrays
|
|
631
|
+
arrays = [np.asarray(arr) for arr in value]
|
|
632
|
+
else:
|
|
633
|
+
arrays.append(np.asarray(value))
|
|
634
|
+
|
|
635
|
+
if not arrays:
|
|
636
|
+
return None
|
|
637
|
+
|
|
638
|
+
# Get kwargs
|
|
639
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
640
|
+
overlap = kwargs.get("overlap", 0.5)
|
|
641
|
+
fill_alpha = kwargs.get("fill_alpha", 0.7)
|
|
642
|
+
line_alpha = kwargs.get("line_alpha", 1.0)
|
|
643
|
+
labels = kwargs.get("labels")
|
|
644
|
+
|
|
645
|
+
n_ridges = len(arrays)
|
|
646
|
+
|
|
647
|
+
# Get colors from style
|
|
648
|
+
from .styles import get_style
|
|
649
|
+
|
|
650
|
+
style = get_style()
|
|
651
|
+
if style and "colors" in style and "palette" in style.colors:
|
|
652
|
+
palette = list(style.colors.palette)
|
|
653
|
+
colors = []
|
|
654
|
+
for c in palette:
|
|
655
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
656
|
+
if all(v <= 1.0 for v in c):
|
|
657
|
+
colors.append(tuple(c))
|
|
658
|
+
else:
|
|
659
|
+
colors.append(tuple(v / 255.0 for v in c))
|
|
660
|
+
else:
|
|
661
|
+
colors.append(c)
|
|
662
|
+
else:
|
|
663
|
+
colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
|
|
664
|
+
|
|
665
|
+
# Calculate global x range
|
|
666
|
+
all_data = np.concatenate([np.asarray(arr) for arr in arrays])
|
|
667
|
+
x_min, x_max = np.min(all_data), np.max(all_data)
|
|
668
|
+
x_range = x_max - x_min
|
|
669
|
+
x_padding = x_range * 0.1
|
|
670
|
+
x = np.linspace(x_min - x_padding, x_max + x_padding, 200)
|
|
671
|
+
|
|
672
|
+
# Calculate KDEs and find max density for scaling
|
|
673
|
+
kdes = []
|
|
674
|
+
max_density = 0
|
|
675
|
+
for arr in arrays:
|
|
676
|
+
arr = np.asarray(arr)
|
|
677
|
+
if len(arr) > 1:
|
|
678
|
+
kde = stats.gaussian_kde(arr)
|
|
679
|
+
density = kde(x)
|
|
680
|
+
kdes.append(density)
|
|
681
|
+
max_density = max(max_density, np.max(density))
|
|
682
|
+
else:
|
|
683
|
+
kdes.append(np.zeros_like(x))
|
|
684
|
+
|
|
685
|
+
# Scale factor for ridge height
|
|
686
|
+
ridge_height = 1.0 / (1.0 - overlap * 0.5) if overlap < 1 else 2.0
|
|
687
|
+
|
|
688
|
+
# Get line width from style
|
|
689
|
+
from ._utils._units import mm_to_pt
|
|
690
|
+
|
|
691
|
+
lw = mm_to_pt(0.2) # Default
|
|
692
|
+
if style and "lines" in style:
|
|
693
|
+
lw = mm_to_pt(style.lines.get("trace_mm", 0.2))
|
|
694
|
+
|
|
695
|
+
# Plot each ridge from back to front
|
|
696
|
+
for i in range(n_ridges - 1, -1, -1):
|
|
697
|
+
color = colors[i % len(colors)]
|
|
698
|
+
baseline = i * (1.0 - overlap)
|
|
699
|
+
|
|
700
|
+
# Scale density to fit nicely
|
|
701
|
+
scaled_density = (
|
|
702
|
+
kdes[i] / max_density * ridge_height if max_density > 0 else kdes[i]
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# Fill
|
|
706
|
+
ax.fill_between(
|
|
707
|
+
x,
|
|
708
|
+
baseline,
|
|
709
|
+
baseline + scaled_density,
|
|
710
|
+
facecolor=color,
|
|
711
|
+
edgecolor="none",
|
|
712
|
+
alpha=fill_alpha,
|
|
713
|
+
)
|
|
714
|
+
# Line on top
|
|
715
|
+
ax.plot(
|
|
716
|
+
x, baseline + scaled_density, color=color, alpha=line_alpha, linewidth=lw
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# Set y limits
|
|
720
|
+
ax.set_ylim(-0.1, n_ridges * (1.0 - overlap) + ridge_height)
|
|
721
|
+
|
|
722
|
+
# Set y-axis labels if provided
|
|
723
|
+
if labels:
|
|
724
|
+
y_positions = [(i * (1.0 - overlap)) + 0.3 for i in range(n_ridges)]
|
|
725
|
+
ax.set_yticks(y_positions)
|
|
726
|
+
ax.set_yticklabels(labels)
|
|
727
|
+
else:
|
|
728
|
+
ax.set_yticks([])
|
|
729
|
+
|
|
730
|
+
return ax
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _replay_swarmplot_call(ax: Axes, call: CallRecord) -> Any:
|
|
734
|
+
"""Replay a swarmplot call on an axes.
|
|
735
|
+
|
|
736
|
+
Parameters
|
|
737
|
+
----------
|
|
738
|
+
ax : Axes
|
|
739
|
+
The matplotlib axes.
|
|
740
|
+
call : CallRecord
|
|
741
|
+
The swarmplot call to replay.
|
|
742
|
+
|
|
743
|
+
Returns
|
|
744
|
+
-------
|
|
745
|
+
list
|
|
746
|
+
List of PathCollection objects.
|
|
747
|
+
"""
|
|
748
|
+
# Reconstruct args
|
|
749
|
+
data = []
|
|
750
|
+
for arg_data in call.args:
|
|
751
|
+
value = _reconstruct_value(arg_data)
|
|
752
|
+
if isinstance(value, list):
|
|
753
|
+
# Could be a list of arrays
|
|
754
|
+
data = [np.asarray(arr) for arr in value]
|
|
755
|
+
else:
|
|
756
|
+
data.append(np.asarray(value))
|
|
757
|
+
|
|
758
|
+
if not data:
|
|
759
|
+
return []
|
|
760
|
+
|
|
761
|
+
# Get kwargs
|
|
762
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
763
|
+
positions = kwargs.get("positions")
|
|
764
|
+
size = kwargs.get("size", 0.8)
|
|
765
|
+
alpha = kwargs.get("alpha", 0.7)
|
|
766
|
+
jitter = kwargs.get("jitter", 0.3)
|
|
767
|
+
|
|
768
|
+
if positions is None:
|
|
769
|
+
positions = list(range(1, len(data) + 1))
|
|
770
|
+
|
|
771
|
+
# Get style
|
|
772
|
+
from ._utils._units import mm_to_pt
|
|
773
|
+
from .styles import get_style
|
|
774
|
+
|
|
775
|
+
style = get_style()
|
|
776
|
+
size_pt = mm_to_pt(size) ** 2 # matplotlib uses area
|
|
777
|
+
|
|
778
|
+
# Get colors
|
|
779
|
+
if style and "colors" in style and "palette" in style.colors:
|
|
780
|
+
palette = list(style.colors.palette)
|
|
781
|
+
colors = []
|
|
782
|
+
for c in palette:
|
|
783
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
784
|
+
if all(v <= 1.0 for v in c):
|
|
785
|
+
colors.append(tuple(c))
|
|
786
|
+
else:
|
|
787
|
+
colors.append(tuple(v / 255.0 for v in c))
|
|
788
|
+
else:
|
|
789
|
+
colors.append(c)
|
|
790
|
+
else:
|
|
791
|
+
colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
|
|
792
|
+
|
|
793
|
+
# Random generator for reproducible jitter
|
|
794
|
+
rng = np.random.default_rng(42)
|
|
795
|
+
|
|
796
|
+
results = []
|
|
797
|
+
for i, (arr, pos) in enumerate(zip(data, positions)):
|
|
798
|
+
arr = np.asarray(arr)
|
|
799
|
+
|
|
800
|
+
# Create jittered x positions using simplified beeswarm
|
|
801
|
+
x_offsets = _beeswarm_positions(arr, jitter, rng)
|
|
802
|
+
x_positions = pos + x_offsets
|
|
803
|
+
|
|
804
|
+
c = colors[i % len(colors)]
|
|
805
|
+
result = ax.scatter(
|
|
806
|
+
x_positions,
|
|
807
|
+
arr,
|
|
808
|
+
s=size_pt,
|
|
809
|
+
c=[c],
|
|
810
|
+
alpha=alpha,
|
|
811
|
+
)
|
|
812
|
+
results.append(result)
|
|
813
|
+
|
|
814
|
+
return results
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _beeswarm_positions(
|
|
818
|
+
data: np.ndarray,
|
|
819
|
+
width: float,
|
|
820
|
+
rng: np.random.Generator,
|
|
821
|
+
) -> np.ndarray:
|
|
822
|
+
"""Calculate beeswarm-style x positions to minimize overlap.
|
|
823
|
+
|
|
824
|
+
Parameters
|
|
825
|
+
----------
|
|
826
|
+
data : array
|
|
827
|
+
Y values of points.
|
|
828
|
+
width : float
|
|
829
|
+
Maximum jitter width.
|
|
830
|
+
rng : Generator
|
|
831
|
+
Random number generator.
|
|
832
|
+
|
|
833
|
+
Returns
|
|
834
|
+
-------
|
|
835
|
+
array
|
|
836
|
+
X offsets for each point.
|
|
837
|
+
"""
|
|
838
|
+
n = len(data)
|
|
839
|
+
if n == 0:
|
|
840
|
+
return np.array([])
|
|
841
|
+
|
|
842
|
+
# Sort data and get order
|
|
843
|
+
order = np.argsort(data)
|
|
844
|
+
sorted_data = data[order]
|
|
845
|
+
|
|
846
|
+
# Group nearby points and offset them
|
|
847
|
+
x_offsets = np.zeros(n)
|
|
848
|
+
|
|
849
|
+
# Simple approach: bin by quantiles and spread within each bin
|
|
850
|
+
n_bins = max(1, int(np.sqrt(n)))
|
|
851
|
+
bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
|
|
852
|
+
|
|
853
|
+
for i in range(n_bins):
|
|
854
|
+
mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
|
|
855
|
+
n_in_bin = mask.sum()
|
|
856
|
+
if n_in_bin > 0:
|
|
857
|
+
# Spread points evenly within bin width
|
|
858
|
+
offsets = np.linspace(-width / 2, width / 2, n_in_bin)
|
|
859
|
+
# Add small random noise
|
|
860
|
+
offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
|
|
861
|
+
x_offsets[mask] = offsets
|
|
862
|
+
|
|
863
|
+
# Restore original order
|
|
864
|
+
result = np.zeros(n)
|
|
865
|
+
result[order] = x_offsets
|
|
866
|
+
return result
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _reconstruct_value(
|
|
870
|
+
arg_data: Dict[str, Any], result_cache: Optional[Dict[str, Any]] = None
|
|
871
|
+
) -> Any:
|
|
284
872
|
"""Reconstruct a value from serialized arg data.
|
|
285
873
|
|
|
286
874
|
Parameters
|
|
287
875
|
----------
|
|
288
876
|
arg_data : dict
|
|
289
877
|
Serialized argument data.
|
|
878
|
+
result_cache : dict, optional
|
|
879
|
+
Cache mapping call_id -> result for resolving references.
|
|
290
880
|
|
|
291
881
|
Returns
|
|
292
882
|
-------
|
|
293
883
|
Any
|
|
294
884
|
Reconstructed value.
|
|
295
885
|
"""
|
|
886
|
+
if result_cache is None:
|
|
887
|
+
result_cache = {}
|
|
888
|
+
|
|
296
889
|
# Check if we have a pre-loaded array
|
|
297
890
|
if "_loaded_array" in arg_data:
|
|
298
891
|
return arg_data["_loaded_array"]
|
|
299
892
|
|
|
300
893
|
data = arg_data.get("data")
|
|
301
894
|
|
|
895
|
+
# Check if it's a reference to another call's result (e.g., ContourSet for clabel)
|
|
896
|
+
if isinstance(data, dict) and "__ref__" in data:
|
|
897
|
+
ref_id = data["__ref__"]
|
|
898
|
+
if ref_id in result_cache:
|
|
899
|
+
return result_cache[ref_id]
|
|
900
|
+
else:
|
|
901
|
+
import warnings
|
|
902
|
+
|
|
903
|
+
warnings.warn(f"Could not resolve reference to {ref_id}")
|
|
904
|
+
return None
|
|
905
|
+
|
|
906
|
+
# Check if it's a list of arrays (e.g., boxplot, violinplot)
|
|
907
|
+
if arg_data.get("_is_array_list") and isinstance(data, list):
|
|
908
|
+
dtype = arg_data.get("dtype")
|
|
909
|
+
# Convert each inner list to numpy array
|
|
910
|
+
return [
|
|
911
|
+
np.array(arr_data, dtype=dtype if isinstance(dtype, str) else None)
|
|
912
|
+
for arr_data in data
|
|
913
|
+
]
|
|
914
|
+
|
|
302
915
|
# If data is a list, convert to numpy array
|
|
303
916
|
if isinstance(data, list):
|
|
304
917
|
dtype = arg_data.get("dtype")
|
|
@@ -334,18 +947,22 @@ def get_recipe_info(path: Union[str, Path]) -> Dict[str, Any]:
|
|
|
334
947
|
all_calls = []
|
|
335
948
|
for ax_record in record.axes.values():
|
|
336
949
|
for call in ax_record.calls:
|
|
337
|
-
all_calls.append(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
950
|
+
all_calls.append(
|
|
951
|
+
{
|
|
952
|
+
"id": call.id,
|
|
953
|
+
"function": call.function,
|
|
954
|
+
"n_args": len(call.args),
|
|
955
|
+
"kwargs": list(call.kwargs.keys()),
|
|
956
|
+
}
|
|
957
|
+
)
|
|
343
958
|
for call in ax_record.decorations:
|
|
344
|
-
all_calls.append(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
959
|
+
all_calls.append(
|
|
960
|
+
{
|
|
961
|
+
"id": call.id,
|
|
962
|
+
"function": call.function,
|
|
963
|
+
"type": "decoration",
|
|
964
|
+
}
|
|
965
|
+
)
|
|
349
966
|
|
|
350
967
|
return {
|
|
351
968
|
"id": record.id,
|