figrecipe 0.5.0__py3-none-any.whl → 0.7.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
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
- from collections import OrderedDict
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(cls, data: Dict[str, Any], ax_position: Tuple[int, int] = (0, 0)) -> "CallRecord":
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
- return {
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(position=(row, col))
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
- # Plotting methods that create artists
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
- Parameters
276
- ----------
277
- args : tuple
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
- # Common patterns
355
- patterns = {
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
- if method_name in patterns:
371
- names = patterns[method_name][:n_args]
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
- # Default generic names
378
- return [f"arg{i}" for i in range(n_args)]
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
+ ]