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.
Files changed (90) hide show
  1. figrecipe/__init__.py +361 -93
  2. figrecipe/_dev/__init__.py +120 -0
  3. figrecipe/_dev/demo_plotters/__init__.py +195 -0
  4. figrecipe/_dev/demo_plotters/plot_acorr.py +24 -0
  5. figrecipe/_dev/demo_plotters/plot_angle_spectrum.py +28 -0
  6. figrecipe/_dev/demo_plotters/plot_bar.py +25 -0
  7. figrecipe/_dev/demo_plotters/plot_barbs.py +30 -0
  8. figrecipe/_dev/demo_plotters/plot_barh.py +25 -0
  9. figrecipe/_dev/demo_plotters/plot_boxplot.py +24 -0
  10. figrecipe/_dev/demo_plotters/plot_cohere.py +29 -0
  11. figrecipe/_dev/demo_plotters/plot_contour.py +30 -0
  12. figrecipe/_dev/demo_plotters/plot_contourf.py +29 -0
  13. figrecipe/_dev/demo_plotters/plot_csd.py +29 -0
  14. figrecipe/_dev/demo_plotters/plot_ecdf.py +24 -0
  15. figrecipe/_dev/demo_plotters/plot_errorbar.py +28 -0
  16. figrecipe/_dev/demo_plotters/plot_eventplot.py +25 -0
  17. figrecipe/_dev/demo_plotters/plot_fill.py +29 -0
  18. figrecipe/_dev/demo_plotters/plot_fill_between.py +30 -0
  19. figrecipe/_dev/demo_plotters/plot_fill_betweenx.py +28 -0
  20. figrecipe/_dev/demo_plotters/plot_hexbin.py +25 -0
  21. figrecipe/_dev/demo_plotters/plot_hist.py +24 -0
  22. figrecipe/_dev/demo_plotters/plot_hist2d.py +25 -0
  23. figrecipe/_dev/demo_plotters/plot_imshow.py +23 -0
  24. figrecipe/_dev/demo_plotters/plot_loglog.py +27 -0
  25. figrecipe/_dev/demo_plotters/plot_magnitude_spectrum.py +28 -0
  26. figrecipe/_dev/demo_plotters/plot_matshow.py +23 -0
  27. figrecipe/_dev/demo_plotters/plot_pcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/plot_pcolormesh.py +29 -0
  29. figrecipe/_dev/demo_plotters/plot_phase_spectrum.py +28 -0
  30. figrecipe/_dev/demo_plotters/plot_pie.py +23 -0
  31. figrecipe/_dev/demo_plotters/plot_plot.py +27 -0
  32. figrecipe/_dev/demo_plotters/plot_psd.py +29 -0
  33. figrecipe/_dev/demo_plotters/plot_quiver.py +30 -0
  34. figrecipe/_dev/demo_plotters/plot_scatter.py +24 -0
  35. figrecipe/_dev/demo_plotters/plot_semilogx.py +27 -0
  36. figrecipe/_dev/demo_plotters/plot_semilogy.py +27 -0
  37. figrecipe/_dev/demo_plotters/plot_specgram.py +30 -0
  38. figrecipe/_dev/demo_plotters/plot_spy.py +29 -0
  39. figrecipe/_dev/demo_plotters/plot_stackplot.py +29 -0
  40. figrecipe/_dev/demo_plotters/plot_stairs.py +27 -0
  41. figrecipe/_dev/demo_plotters/plot_stem.py +27 -0
  42. figrecipe/_dev/demo_plotters/plot_step.py +27 -0
  43. figrecipe/_dev/demo_plotters/plot_streamplot.py +30 -0
  44. figrecipe/_dev/demo_plotters/plot_tricontour.py +28 -0
  45. figrecipe/_dev/demo_plotters/plot_tricontourf.py +28 -0
  46. figrecipe/_dev/demo_plotters/plot_tripcolor.py +29 -0
  47. figrecipe/_dev/demo_plotters/plot_triplot.py +25 -0
  48. figrecipe/_dev/demo_plotters/plot_violinplot.py +25 -0
  49. figrecipe/_dev/demo_plotters/plot_xcorr.py +25 -0
  50. figrecipe/_editor/__init__.py +230 -0
  51. figrecipe/_editor/_bbox.py +978 -0
  52. figrecipe/_editor/_flask_app.py +1229 -0
  53. figrecipe/_editor/_hitmap.py +937 -0
  54. figrecipe/_editor/_overrides.py +318 -0
  55. figrecipe/_editor/_renderer.py +349 -0
  56. figrecipe/_editor/_templates/__init__.py +75 -0
  57. figrecipe/_editor/_templates/_html.py +406 -0
  58. figrecipe/_editor/_templates/_scripts.py +2778 -0
  59. figrecipe/_editor/_templates/_styles.py +1326 -0
  60. figrecipe/_params/_DECORATION_METHODS.py +27 -0
  61. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  62. figrecipe/_params/__init__.py +9 -0
  63. figrecipe/_recorder.py +126 -73
  64. figrecipe/_reproducer.py +658 -41
  65. figrecipe/_seaborn.py +14 -9
  66. figrecipe/_serializer.py +2 -2
  67. figrecipe/_signatures/README.md +68 -0
  68. figrecipe/_signatures/__init__.py +12 -2
  69. figrecipe/_signatures/_loader.py +515 -56
  70. figrecipe/_utils/__init__.py +6 -4
  71. figrecipe/_utils/_crop.py +10 -4
  72. figrecipe/_utils/_image_diff.py +37 -33
  73. figrecipe/_utils/_numpy_io.py +0 -1
  74. figrecipe/_utils/_units.py +11 -3
  75. figrecipe/_validator.py +12 -3
  76. figrecipe/_wrappers/_axes.py +860 -46
  77. figrecipe/_wrappers/_figure.py +115 -18
  78. figrecipe/plt.py +0 -1
  79. figrecipe/pyplot.py +2 -1
  80. figrecipe/styles/__init__.py +9 -10
  81. figrecipe/styles/_style_applier.py +332 -28
  82. figrecipe/styles/_style_loader.py +172 -44
  83. figrecipe/styles/presets/MATPLOTLIB.yaml +94 -0
  84. figrecipe/styles/presets/SCITEX.yaml +176 -0
  85. figrecipe-0.6.0.dist-info/METADATA +394 -0
  86. figrecipe-0.6.0.dist-info/RECORD +90 -0
  87. figrecipe-0.5.0.dist-info/METADATA +0 -336
  88. figrecipe-0.5.0.dist-info/RECORD +0 -26
  89. {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/WHEEL +0 -0
  90. {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-23 09:55:04 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/figrecipe/src/figrecipe/DECORATION_METHODS.py
5
+
6
+
7
+ """Top-level docstring here"""
8
+
9
+ DECORATION_METHODS = {
10
+ "set_xlabel",
11
+ "set_ylabel",
12
+ "set_title",
13
+ "set_xlim",
14
+ "set_ylim",
15
+ "set_aspect",
16
+ "legend",
17
+ "grid",
18
+ "axhline",
19
+ "axvline",
20
+ "axhspan",
21
+ "axvspan",
22
+ "text",
23
+ "annotate",
24
+ "clabel",
25
+ }
26
+
27
+ # EOF
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-23 09:55:10 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/figrecipe/src/figrecipe/PLOTTING_METHODS.py
5
+
6
+
7
+ """Top-level docstring here"""
8
+
9
+ PLOTTING_METHODS = {
10
+ "plot",
11
+ "scatter",
12
+ "bar",
13
+ "barh",
14
+ "hist",
15
+ "hist2d",
16
+ "boxplot",
17
+ "violinplot",
18
+ "pie",
19
+ "errorbar",
20
+ "fill",
21
+ "fill_between",
22
+ "fill_betweenx",
23
+ "stackplot",
24
+ "stem",
25
+ "step",
26
+ "imshow",
27
+ "pcolor",
28
+ "pcolormesh",
29
+ "contour",
30
+ "contourf",
31
+ "quiver",
32
+ "barbs",
33
+ "streamplot",
34
+ "hexbin",
35
+ "tripcolor",
36
+ "triplot",
37
+ "tricontour",
38
+ "tricontourf",
39
+ "eventplot",
40
+ "stairs",
41
+ "ecdf",
42
+ "matshow",
43
+ "spy",
44
+ "loglog",
45
+ "semilogx",
46
+ "semilogy",
47
+ "acorr",
48
+ "xcorr",
49
+ "specgram",
50
+ "psd",
51
+ "csd",
52
+ "cohere",
53
+ "angle_spectrum",
54
+ "magnitude_spectrum",
55
+ "phase_spectrum",
56
+ }
57
+
58
+ # EOF
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2024-10-22 19:51:47 (ywatanabe)"
4
+
5
+
6
+ from ._DECORATION_METHODS import DECORATION_METHODS as DECORATION_METHODS
7
+ from ._PLOTTING_METHODS import PLOTTING_METHODS as PLOTTING_METHODS
8
+
9
+ # EOF
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"],
@@ -86,6 +91,10 @@ class FigureRecord:
86
91
  style: Optional[Dict[str, Any]] = None
87
92
  # Constrained layout flag
88
93
  constrained_layout: bool = False
94
+ # Figure-level decorations (suptitle, supxlabel, supylabel)
95
+ suptitle: Optional[Dict[str, Any]] = None
96
+ supxlabel: Optional[Dict[str, Any]] = None
97
+ supylabel: Optional[Dict[str, Any]] = None
89
98
 
90
99
  def get_axes_key(self, row: int, col: int) -> str:
91
100
  """Get dictionary key for axes at position."""
@@ -120,6 +129,15 @@ class FigureRecord:
120
129
  # Add constrained_layout if True
121
130
  if self.constrained_layout:
122
131
  result["figure"]["constrained_layout"] = True
132
+ # Add suptitle if set
133
+ if self.suptitle is not None:
134
+ result["figure"]["suptitle"] = self.suptitle
135
+ # Add supxlabel if set
136
+ if self.supxlabel is not None:
137
+ result["figure"]["supxlabel"] = self.supxlabel
138
+ # Add supylabel if set
139
+ if self.supylabel is not None:
140
+ result["figure"]["supylabel"] = self.supylabel
123
141
  return result
124
142
 
125
143
  @classmethod
@@ -135,6 +153,9 @@ class FigureRecord:
135
153
  layout=fig_data.get("layout"),
136
154
  style=fig_data.get("style"),
137
155
  constrained_layout=fig_data.get("constrained_layout", False),
156
+ suptitle=fig_data.get("suptitle"),
157
+ supxlabel=fig_data.get("supxlabel"),
158
+ supylabel=fig_data.get("supylabel"),
138
159
  )
139
160
 
140
161
  # Reconstruct axes
@@ -160,26 +181,7 @@ class FigureRecord:
160
181
  class Recorder:
161
182
  """Central recorder for tracking matplotlib calls."""
162
183
 
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
- }
184
+ from ._params import DECORATION_METHODS, PLOTTING_METHODS
183
185
 
184
186
  def __init__(self):
185
187
  self._figure_record: Optional[FigureRecord] = None
@@ -291,53 +293,93 @@ class Recorder:
291
293
  arg_names = self._get_arg_names(method_name, len(args))
292
294
 
293
295
  for i, (name, value) in enumerate(zip(arg_names, args)):
296
+ # Handle result references (e.g., ContourSet for clabel)
297
+ if isinstance(value, dict) and "__ref__" in value:
298
+ processed.append(
299
+ {
300
+ "name": name,
301
+ "data": {"__ref__": value["__ref__"]},
302
+ }
303
+ )
304
+ continue
305
+
294
306
  if isinstance(value, np.ndarray):
295
307
  if should_store_inline(value):
296
- processed.append({
297
- "name": name,
298
- "data": to_serializable(value),
299
- "dtype": str(value.dtype),
300
- })
308
+ processed.append(
309
+ {
310
+ "name": name,
311
+ "data": to_serializable(value),
312
+ "dtype": str(value.dtype),
313
+ }
314
+ )
301
315
  else:
302
316
  # 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
- })
317
+ processed.append(
318
+ {
319
+ "name": name,
320
+ "data": "__FILE__",
321
+ "dtype": str(value.dtype),
322
+ "_array": value, # Temporary, removed during serialization
323
+ }
324
+ )
309
325
  elif hasattr(value, "values"): # pandas
310
326
  arr = np.asarray(value)
311
327
  if should_store_inline(arr):
312
- processed.append({
313
- "name": name,
314
- "data": to_serializable(arr),
315
- "dtype": str(arr.dtype),
316
- })
328
+ processed.append(
329
+ {
330
+ "name": name,
331
+ "data": to_serializable(arr),
332
+ "dtype": str(arr.dtype),
333
+ }
334
+ )
317
335
  else:
318
- processed.append({
336
+ processed.append(
337
+ {
338
+ "name": name,
339
+ "data": "__FILE__",
340
+ "dtype": str(arr.dtype),
341
+ "_array": arr,
342
+ }
343
+ )
344
+ elif (
345
+ isinstance(value, (list, tuple))
346
+ and len(value) > 0
347
+ and isinstance(value[0], np.ndarray)
348
+ ):
349
+ # List of arrays (e.g., boxplot, violinplot data)
350
+ arrays_data = [to_serializable(arr) for arr in value]
351
+ dtypes = [str(arr.dtype) for arr in value]
352
+ processed.append(
353
+ {
319
354
  "name": name,
320
- "data": "__FILE__",
321
- "dtype": str(arr.dtype),
322
- "_array": arr,
323
- })
355
+ "data": arrays_data,
356
+ "dtype": (dtypes[0] if len(set(dtypes)) == 1 else dtypes),
357
+ "_is_array_list": True,
358
+ }
359
+ )
324
360
  else:
325
361
  # Scalar or other serializable value
326
362
  try:
327
- processed.append({
328
- "name": name,
329
- "data": value if self._is_serializable(value) else str(value),
330
- })
363
+ processed.append(
364
+ {
365
+ "name": name,
366
+ "data": (
367
+ value if self._is_serializable(value) else str(value)
368
+ ),
369
+ }
370
+ )
331
371
  except (TypeError, ValueError):
332
- processed.append({
333
- "name": name,
334
- "data": str(value),
335
- })
372
+ processed.append(
373
+ {
374
+ "name": name,
375
+ "data": str(value),
376
+ }
377
+ )
336
378
 
337
379
  return processed
338
380
 
339
381
  def _get_arg_names(self, method_name: str, n_args: int) -> List[str]:
340
- """Get argument names for a method.
382
+ """Get argument names for a method from signatures.
341
383
 
342
384
  Parameters
343
385
  ----------
@@ -351,31 +393,18 @@ class Recorder:
351
393
  list
352
394
  List of argument names.
353
395
  """
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
- }
396
+ try:
397
+ from ._signatures import get_signature
369
398
 
370
- if method_name in patterns:
371
- names = patterns[method_name][:n_args]
399
+ sig = get_signature(method_name)
400
+ names = [arg["name"] for arg in sig["args"][:n_args]]
372
401
  # Pad with generic names if needed
373
402
  while len(names) < n_args:
374
403
  names.append(f"arg{len(names)}")
375
404
  return names
376
-
377
- # Default generic names
378
- return [f"arg{i}" for i in range(n_args)]
405
+ except Exception:
406
+ # Fallback to generic names
407
+ return [f"arg{i}" for i in range(n_args)]
379
408
 
380
409
  def _process_kwargs(
381
410
  self,
@@ -384,6 +413,8 @@ class Recorder:
384
413
  ) -> Dict[str, Any]:
385
414
  """Process keyword arguments for storage.
386
415
 
416
+ Only stores non-default kwargs to keep recipes minimal.
417
+
387
418
  Parameters
388
419
  ----------
389
420
  kwargs : dict
@@ -396,6 +427,15 @@ class Recorder:
396
427
  dict
397
428
  Processed kwargs (non-default only).
398
429
  """
430
+ # Get defaults from signature
431
+ defaults = {}
432
+ try:
433
+ from ._signatures import get_defaults
434
+
435
+ defaults = get_defaults(method_name)
436
+ except Exception:
437
+ pass
438
+
399
439
  # Remove internal keys
400
440
  skip_keys = {"id", "track", "_array"}
401
441
  processed = {}
@@ -404,6 +444,16 @@ class Recorder:
404
444
  if key in skip_keys:
405
445
  continue
406
446
 
447
+ # Skip if value matches default
448
+ if key in defaults:
449
+ default_val = defaults[key]
450
+ # Compare values (handle None specially)
451
+ if default_val is not None and value == default_val:
452
+ continue
453
+ # Also skip if both are None
454
+ if default_val is None and value is None:
455
+ continue
456
+
407
457
  if self._is_serializable(value):
408
458
  processed[key] = value
409
459
  elif isinstance(value, np.ndarray):
@@ -433,3 +483,6 @@ class Recorder:
433
483
  for k, v in value.items()
434
484
  )
435
485
  return False
486
+
487
+
488
+ # EOF