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.
- figrecipe/__init__.py +220 -819
- 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 +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- 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/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- 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 +258 -0
- 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/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- 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 +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- 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 +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- 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/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- 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 +193 -170
- 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 +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- 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 +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- 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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
figrecipe/_recorder.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-12-23 09:57:28 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/figrecipe/src/figrecipe/_recorder.py
|
|
5
|
+
|
|
6
|
+
|
|
3
7
|
"""Core recording functionality for figrecipe."""
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
import uuid
|
|
6
10
|
from dataclasses import dataclass, field
|
|
7
11
|
from datetime import datetime
|
|
8
12
|
from typing import Any, Dict, List, Optional, Tuple
|
|
9
|
-
import uuid
|
|
10
13
|
|
|
11
14
|
import matplotlib
|
|
12
15
|
import numpy as np
|
|
@@ -34,7 +37,9 @@ class CallRecord:
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
@classmethod
|
|
37
|
-
def from_dict(
|
|
40
|
+
def from_dict(
|
|
41
|
+
cls, data: Dict[str, Any], ax_position: Tuple[int, int] = (0, 0)
|
|
42
|
+
) -> "CallRecord":
|
|
38
43
|
"""Create from dictionary."""
|
|
39
44
|
return cls(
|
|
40
45
|
id=data["id"],
|
|
@@ -53,6 +58,8 @@ class AxesRecord:
|
|
|
53
58
|
position: Tuple[int, int]
|
|
54
59
|
calls: List[CallRecord] = field(default_factory=list)
|
|
55
60
|
decorations: List[CallRecord] = field(default_factory=list)
|
|
61
|
+
# Panel-level caption (e.g., "(A) Description of this panel")
|
|
62
|
+
caption: Optional[str] = None
|
|
56
63
|
|
|
57
64
|
def add_call(self, record: CallRecord) -> None:
|
|
58
65
|
"""Add a plotting call record."""
|
|
@@ -64,10 +71,13 @@ class AxesRecord:
|
|
|
64
71
|
|
|
65
72
|
def to_dict(self) -> Dict[str, Any]:
|
|
66
73
|
"""Convert to dictionary for serialization."""
|
|
67
|
-
|
|
74
|
+
result = {
|
|
68
75
|
"calls": [c.to_dict() for c in self.calls],
|
|
69
76
|
"decorations": [d.to_dict() for d in self.decorations],
|
|
70
77
|
}
|
|
78
|
+
if self.caption is not None:
|
|
79
|
+
result["caption"] = self.caption
|
|
80
|
+
return result
|
|
71
81
|
|
|
72
82
|
|
|
73
83
|
@dataclass
|
|
@@ -86,6 +96,15 @@ class FigureRecord:
|
|
|
86
96
|
style: Optional[Dict[str, Any]] = None
|
|
87
97
|
# Constrained layout flag
|
|
88
98
|
constrained_layout: bool = False
|
|
99
|
+
# Figure-level decorations (suptitle, supxlabel, supylabel)
|
|
100
|
+
suptitle: Optional[Dict[str, Any]] = None
|
|
101
|
+
supxlabel: Optional[Dict[str, Any]] = None
|
|
102
|
+
supylabel: Optional[Dict[str, Any]] = None
|
|
103
|
+
# Panel labels (A, B, C, D for multi-panel figures)
|
|
104
|
+
panel_labels: Optional[Dict[str, Any]] = None
|
|
105
|
+
# Metadata for scientific figures (not rendered, stored in recipe)
|
|
106
|
+
title_metadata: Optional[str] = None # Figure title for publication/reference
|
|
107
|
+
caption: Optional[str] = None # Figure caption (e.g., "Fig. 1. Description...")
|
|
89
108
|
|
|
90
109
|
def get_axes_key(self, row: int, col: int) -> str:
|
|
91
110
|
"""Get dictionary key for axes at position."""
|
|
@@ -120,12 +139,33 @@ class FigureRecord:
|
|
|
120
139
|
# Add constrained_layout if True
|
|
121
140
|
if self.constrained_layout:
|
|
122
141
|
result["figure"]["constrained_layout"] = True
|
|
142
|
+
# Add suptitle if set
|
|
143
|
+
if self.suptitle is not None:
|
|
144
|
+
result["figure"]["suptitle"] = self.suptitle
|
|
145
|
+
# Add supxlabel if set
|
|
146
|
+
if self.supxlabel is not None:
|
|
147
|
+
result["figure"]["supxlabel"] = self.supxlabel
|
|
148
|
+
# Add supylabel if set
|
|
149
|
+
if self.supylabel is not None:
|
|
150
|
+
result["figure"]["supylabel"] = self.supylabel
|
|
151
|
+
# Add panel_labels if set
|
|
152
|
+
if self.panel_labels is not None:
|
|
153
|
+
result["figure"]["panel_labels"] = self.panel_labels
|
|
154
|
+
# Add metadata section for scientific figures
|
|
155
|
+
metadata = {}
|
|
156
|
+
if self.title_metadata is not None:
|
|
157
|
+
metadata["title"] = self.title_metadata
|
|
158
|
+
if self.caption is not None:
|
|
159
|
+
metadata["caption"] = self.caption
|
|
160
|
+
if metadata:
|
|
161
|
+
result["metadata"] = metadata
|
|
123
162
|
return result
|
|
124
163
|
|
|
125
164
|
@classmethod
|
|
126
165
|
def from_dict(cls, data: Dict[str, Any]) -> "FigureRecord":
|
|
127
166
|
"""Create from dictionary."""
|
|
128
167
|
fig_data = data.get("figure", {})
|
|
168
|
+
metadata = data.get("metadata", {})
|
|
129
169
|
record = cls(
|
|
130
170
|
id=data.get("id", f"fig_{uuid.uuid4().hex[:8]}"),
|
|
131
171
|
created=data.get("created", ""),
|
|
@@ -135,6 +175,12 @@ class FigureRecord:
|
|
|
135
175
|
layout=fig_data.get("layout"),
|
|
136
176
|
style=fig_data.get("style"),
|
|
137
177
|
constrained_layout=fig_data.get("constrained_layout", False),
|
|
178
|
+
suptitle=fig_data.get("suptitle"),
|
|
179
|
+
supxlabel=fig_data.get("supxlabel"),
|
|
180
|
+
supylabel=fig_data.get("supylabel"),
|
|
181
|
+
panel_labels=fig_data.get("panel_labels"),
|
|
182
|
+
title_metadata=metadata.get("title"),
|
|
183
|
+
caption=metadata.get("caption"),
|
|
138
184
|
)
|
|
139
185
|
|
|
140
186
|
# Reconstruct axes
|
|
@@ -146,7 +192,10 @@ class FigureRecord:
|
|
|
146
192
|
else:
|
|
147
193
|
row, col = 0, 0
|
|
148
194
|
|
|
149
|
-
ax_record = AxesRecord(
|
|
195
|
+
ax_record = AxesRecord(
|
|
196
|
+
position=(row, col),
|
|
197
|
+
caption=ax_data.get("caption"),
|
|
198
|
+
)
|
|
150
199
|
for call_data in ax_data.get("calls", []):
|
|
151
200
|
ax_record.calls.append(CallRecord.from_dict(call_data, (row, col)))
|
|
152
201
|
for dec_data in ax_data.get("decorations", []):
|
|
@@ -160,26 +209,7 @@ class FigureRecord:
|
|
|
160
209
|
class Recorder:
|
|
161
210
|
"""Central recorder for tracking matplotlib calls."""
|
|
162
211
|
|
|
163
|
-
|
|
164
|
-
PLOTTING_METHODS = {
|
|
165
|
-
"plot", "scatter", "bar", "barh", "hist", "hist2d",
|
|
166
|
-
"boxplot", "violinplot", "pie", "errorbar", "fill",
|
|
167
|
-
"fill_between", "fill_betweenx", "stackplot", "stem",
|
|
168
|
-
"step", "imshow", "pcolor", "pcolormesh", "contour",
|
|
169
|
-
"contourf", "quiver", "barbs", "streamplot", "hexbin",
|
|
170
|
-
"tripcolor", "triplot", "tricontour", "tricontourf",
|
|
171
|
-
"eventplot", "stairs", "ecdf", "matshow", "spy",
|
|
172
|
-
"loglog", "semilogx", "semilogy", "acorr", "xcorr",
|
|
173
|
-
"specgram", "psd", "csd", "cohere", "angle_spectrum",
|
|
174
|
-
"magnitude_spectrum", "phase_spectrum",
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
# Decoration methods
|
|
178
|
-
DECORATION_METHODS = {
|
|
179
|
-
"set_xlabel", "set_ylabel", "set_title", "set_xlim",
|
|
180
|
-
"set_ylim", "legend", "grid", "axhline", "axvline",
|
|
181
|
-
"axhspan", "axvspan", "text", "annotate",
|
|
182
|
-
}
|
|
212
|
+
from ._params import DECORATION_METHODS, PLOTTING_METHODS
|
|
183
213
|
|
|
184
214
|
def __init__(self):
|
|
185
215
|
self._figure_record: Optional[FigureRecord] = None
|
|
@@ -270,74 +300,15 @@ class Recorder:
|
|
|
270
300
|
args: tuple,
|
|
271
301
|
method_name: str,
|
|
272
302
|
) -> List[Dict[str, Any]]:
|
|
273
|
-
"""Process positional arguments for storage.
|
|
303
|
+
"""Process positional arguments for storage."""
|
|
304
|
+
from ._recorder_utils import process_args
|
|
274
305
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
Raw positional arguments.
|
|
279
|
-
method_name : str
|
|
280
|
-
Name of the method.
|
|
281
|
-
|
|
282
|
-
Returns
|
|
283
|
-
-------
|
|
284
|
-
list
|
|
285
|
-
Processed args with name and data.
|
|
286
|
-
"""
|
|
287
|
-
from ._utils._numpy_io import should_store_inline, to_serializable
|
|
288
|
-
|
|
289
|
-
processed = []
|
|
290
|
-
# Simple arg names based on common patterns
|
|
291
|
-
arg_names = self._get_arg_names(method_name, len(args))
|
|
292
|
-
|
|
293
|
-
for i, (name, value) in enumerate(zip(arg_names, args)):
|
|
294
|
-
if isinstance(value, np.ndarray):
|
|
295
|
-
if should_store_inline(value):
|
|
296
|
-
processed.append({
|
|
297
|
-
"name": name,
|
|
298
|
-
"data": to_serializable(value),
|
|
299
|
-
"dtype": str(value.dtype),
|
|
300
|
-
})
|
|
301
|
-
else:
|
|
302
|
-
# Mark for file storage (will be handled by serializer)
|
|
303
|
-
processed.append({
|
|
304
|
-
"name": name,
|
|
305
|
-
"data": "__FILE__",
|
|
306
|
-
"dtype": str(value.dtype),
|
|
307
|
-
"_array": value, # Temporary, removed during serialization
|
|
308
|
-
})
|
|
309
|
-
elif hasattr(value, "values"): # pandas
|
|
310
|
-
arr = np.asarray(value)
|
|
311
|
-
if should_store_inline(arr):
|
|
312
|
-
processed.append({
|
|
313
|
-
"name": name,
|
|
314
|
-
"data": to_serializable(arr),
|
|
315
|
-
"dtype": str(arr.dtype),
|
|
316
|
-
})
|
|
317
|
-
else:
|
|
318
|
-
processed.append({
|
|
319
|
-
"name": name,
|
|
320
|
-
"data": "__FILE__",
|
|
321
|
-
"dtype": str(arr.dtype),
|
|
322
|
-
"_array": arr,
|
|
323
|
-
})
|
|
324
|
-
else:
|
|
325
|
-
# Scalar or other serializable value
|
|
326
|
-
try:
|
|
327
|
-
processed.append({
|
|
328
|
-
"name": name,
|
|
329
|
-
"data": value if self._is_serializable(value) else str(value),
|
|
330
|
-
})
|
|
331
|
-
except (TypeError, ValueError):
|
|
332
|
-
processed.append({
|
|
333
|
-
"name": name,
|
|
334
|
-
"data": str(value),
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
return processed
|
|
306
|
+
return process_args(
|
|
307
|
+
args, method_name, self._get_arg_names, self._is_serializable
|
|
308
|
+
)
|
|
338
309
|
|
|
339
310
|
def _get_arg_names(self, method_name: str, n_args: int) -> List[str]:
|
|
340
|
-
"""Get argument names for a method.
|
|
311
|
+
"""Get argument names for a method from signatures.
|
|
341
312
|
|
|
342
313
|
Parameters
|
|
343
314
|
----------
|
|
@@ -351,31 +322,18 @@ class Recorder:
|
|
|
351
322
|
list
|
|
352
323
|
List of argument names.
|
|
353
324
|
"""
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"plot": ["x", "y", "fmt"],
|
|
357
|
-
"scatter": ["x", "y", "s", "c"],
|
|
358
|
-
"bar": ["x", "height", "width", "bottom"],
|
|
359
|
-
"barh": ["y", "width", "height", "left"],
|
|
360
|
-
"hist": ["x", "bins"],
|
|
361
|
-
"imshow": ["X"],
|
|
362
|
-
"contour": ["X", "Y", "Z", "levels"],
|
|
363
|
-
"contourf": ["X", "Y", "Z", "levels"],
|
|
364
|
-
"fill_between": ["x", "y1", "y2"],
|
|
365
|
-
"errorbar": ["x", "y", "yerr", "xerr"],
|
|
366
|
-
"text": ["x", "y", "s"],
|
|
367
|
-
"annotate": ["text", "xy", "xytext"],
|
|
368
|
-
}
|
|
325
|
+
try:
|
|
326
|
+
from ._signatures import get_signature
|
|
369
327
|
|
|
370
|
-
|
|
371
|
-
names =
|
|
328
|
+
sig = get_signature(method_name)
|
|
329
|
+
names = [arg["name"] for arg in sig["args"][:n_args]]
|
|
372
330
|
# Pad with generic names if needed
|
|
373
331
|
while len(names) < n_args:
|
|
374
332
|
names.append(f"arg{len(names)}")
|
|
375
333
|
return names
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
334
|
+
except Exception:
|
|
335
|
+
# Fallback to generic names
|
|
336
|
+
return [f"arg{i}" for i in range(n_args)]
|
|
379
337
|
|
|
380
338
|
def _process_kwargs(
|
|
381
339
|
self,
|
|
@@ -384,6 +342,8 @@ class Recorder:
|
|
|
384
342
|
) -> Dict[str, Any]:
|
|
385
343
|
"""Process keyword arguments for storage.
|
|
386
344
|
|
|
345
|
+
Only stores non-default kwargs to keep recipes minimal.
|
|
346
|
+
|
|
387
347
|
Parameters
|
|
388
348
|
----------
|
|
389
349
|
kwargs : dict
|
|
@@ -396,6 +356,15 @@ class Recorder:
|
|
|
396
356
|
dict
|
|
397
357
|
Processed kwargs (non-default only).
|
|
398
358
|
"""
|
|
359
|
+
# Get defaults from signature
|
|
360
|
+
defaults = {}
|
|
361
|
+
try:
|
|
362
|
+
from ._signatures import get_defaults
|
|
363
|
+
|
|
364
|
+
defaults = get_defaults(method_name)
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
|
|
399
368
|
# Remove internal keys
|
|
400
369
|
skip_keys = {"id", "track", "_array"}
|
|
401
370
|
processed = {}
|
|
@@ -404,6 +373,16 @@ class Recorder:
|
|
|
404
373
|
if key in skip_keys:
|
|
405
374
|
continue
|
|
406
375
|
|
|
376
|
+
# Skip if value matches default
|
|
377
|
+
if key in defaults:
|
|
378
|
+
default_val = defaults[key]
|
|
379
|
+
# Compare values (handle None specially)
|
|
380
|
+
if default_val is not None and value == default_val:
|
|
381
|
+
continue
|
|
382
|
+
# Also skip if both are None
|
|
383
|
+
if default_val is None and value is None:
|
|
384
|
+
continue
|
|
385
|
+
|
|
407
386
|
if self._is_serializable(value):
|
|
408
387
|
processed[key] = value
|
|
409
388
|
elif isinstance(value, np.ndarray):
|
|
@@ -433,3 +412,6 @@ class Recorder:
|
|
|
433
412
|
for k, v in value.items()
|
|
434
413
|
)
|
|
435
414
|
return False
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# EOF
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Utilities for recorder argument processing."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_args(
|
|
11
|
+
args: tuple,
|
|
12
|
+
method_name: str,
|
|
13
|
+
get_arg_names_func,
|
|
14
|
+
is_serializable_func,
|
|
15
|
+
) -> List[Dict[str, Any]]:
|
|
16
|
+
"""Process positional arguments for storage.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
args : tuple
|
|
21
|
+
Raw positional arguments.
|
|
22
|
+
method_name : str
|
|
23
|
+
Name of the method.
|
|
24
|
+
get_arg_names_func : callable
|
|
25
|
+
Function to get argument names.
|
|
26
|
+
is_serializable_func : callable
|
|
27
|
+
Function to check serializability.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
list
|
|
32
|
+
Processed args with name and data.
|
|
33
|
+
"""
|
|
34
|
+
from ._utils._numpy_io import should_store_inline, to_serializable
|
|
35
|
+
|
|
36
|
+
processed = []
|
|
37
|
+
arg_names = get_arg_names_func(method_name, len(args))
|
|
38
|
+
|
|
39
|
+
for name, value in zip(arg_names, args):
|
|
40
|
+
processed_arg = _process_single_arg(
|
|
41
|
+
name, value, should_store_inline, to_serializable, is_serializable_func
|
|
42
|
+
)
|
|
43
|
+
processed.append(processed_arg)
|
|
44
|
+
|
|
45
|
+
return processed
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _process_single_arg(
|
|
49
|
+
name: str,
|
|
50
|
+
value: Any,
|
|
51
|
+
should_store_inline,
|
|
52
|
+
to_serializable,
|
|
53
|
+
is_serializable_func,
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""Process a single argument value."""
|
|
56
|
+
# Handle result references (e.g., ContourSet for clabel)
|
|
57
|
+
if isinstance(value, dict) and "__ref__" in value:
|
|
58
|
+
return {"name": name, "data": {"__ref__": value["__ref__"]}}
|
|
59
|
+
|
|
60
|
+
if isinstance(value, np.ndarray):
|
|
61
|
+
return _process_ndarray(name, value, should_store_inline, to_serializable)
|
|
62
|
+
|
|
63
|
+
if hasattr(value, "values"): # pandas
|
|
64
|
+
arr = np.asarray(value)
|
|
65
|
+
return _process_ndarray(name, arr, should_store_inline, to_serializable)
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
isinstance(value, (list, tuple))
|
|
69
|
+
and len(value) > 0
|
|
70
|
+
and isinstance(value[0], np.ndarray)
|
|
71
|
+
):
|
|
72
|
+
# List of arrays (e.g., boxplot, violinplot data)
|
|
73
|
+
return _process_array_list(name, value, to_serializable)
|
|
74
|
+
|
|
75
|
+
# Scalar or other serializable value
|
|
76
|
+
return _process_scalar(name, value, is_serializable_func)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _process_ndarray(
|
|
80
|
+
name: str, value: np.ndarray, should_store_inline, to_serializable
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Process numpy array argument."""
|
|
83
|
+
if should_store_inline(value):
|
|
84
|
+
return {
|
|
85
|
+
"name": name,
|
|
86
|
+
"data": to_serializable(value),
|
|
87
|
+
"dtype": str(value.dtype),
|
|
88
|
+
}
|
|
89
|
+
else:
|
|
90
|
+
# Mark for file storage (will be handled by serializer)
|
|
91
|
+
return {
|
|
92
|
+
"name": name,
|
|
93
|
+
"data": "__FILE__",
|
|
94
|
+
"dtype": str(value.dtype),
|
|
95
|
+
"_array": value, # Temporary, removed during serialization
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _process_array_list(name: str, value: list, to_serializable) -> Dict[str, Any]:
|
|
100
|
+
"""Process list of arrays argument."""
|
|
101
|
+
arrays_data = [to_serializable(arr) for arr in value]
|
|
102
|
+
dtypes = [str(arr.dtype) for arr in value]
|
|
103
|
+
return {
|
|
104
|
+
"name": name,
|
|
105
|
+
"data": arrays_data,
|
|
106
|
+
"dtype": (dtypes[0] if len(set(dtypes)) == 1 else dtypes),
|
|
107
|
+
"_is_array_list": True,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _process_scalar(name: str, value: Any, is_serializable_func) -> Dict[str, Any]:
|
|
112
|
+
"""Process scalar or other value."""
|
|
113
|
+
try:
|
|
114
|
+
return {
|
|
115
|
+
"name": name,
|
|
116
|
+
"data": value if is_serializable_func(value) else str(value),
|
|
117
|
+
}
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
return {"name": name, "data": str(value)}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["process_args"]
|
|
123
|
+
|
|
124
|
+
# EOF
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Figure reproduction module.
|
|
4
|
+
|
|
5
|
+
This module provides functionality to reproduce figures from recipe files.
|
|
6
|
+
The public API exposes three main functions:
|
|
7
|
+
- reproduce: Reproduce a figure from a recipe file
|
|
8
|
+
- reproduce_from_record: Reproduce a figure from a FigureRecord object
|
|
9
|
+
- get_recipe_info: Get information about a recipe without reproducing it
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ._core import get_recipe_info, reproduce, reproduce_from_record
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"reproduce",
|
|
16
|
+
"reproduce_from_record",
|
|
17
|
+
"get_recipe_info",
|
|
18
|
+
]
|