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
@@ -3,16 +3,17 @@
3
3
  """Wrapped Figure that manages recording."""
4
4
 
5
5
  from pathlib import Path
6
- from typing import Any, Dict, List, Literal, Optional, Tuple, Union, TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, Union
7
7
 
8
8
  import matplotlib.pyplot as plt
9
+ import numpy as np
9
10
  from matplotlib.figure import Figure
11
+ from numpy.typing import NDArray
10
12
 
11
13
  from ._axes import RecordingAxes
12
14
 
13
15
  if TYPE_CHECKING:
14
- from .._recorder import Recorder, FigureRecord
15
- from .._utils._numpy_io import DataFormat
16
+ from .._recorder import FigureRecord, Recorder
16
17
 
17
18
 
18
19
  class RecordingFigure:
@@ -79,6 +80,88 @@ class RecordingFigure:
79
80
  """Get the figure record."""
80
81
  return self._recorder.figure_record
81
82
 
83
+ def _get_style_fontsize(self, key: str, default: float) -> float:
84
+ """Get fontsize from loaded style."""
85
+ try:
86
+ from ..styles._style_loader import _STYLE_CACHE
87
+
88
+ if _STYLE_CACHE is not None:
89
+ fonts = getattr(_STYLE_CACHE, "fonts", None)
90
+ if fonts is not None:
91
+ return getattr(fonts, key, default)
92
+ except Exception:
93
+ pass
94
+ return default
95
+
96
+ def suptitle(self, t: str, **kwargs) -> Any:
97
+ """Set super title for the figure and record it.
98
+
99
+ Parameters
100
+ ----------
101
+ t : str
102
+ The super title text.
103
+ **kwargs
104
+ Additional arguments passed to matplotlib's suptitle().
105
+
106
+ Returns
107
+ -------
108
+ Text
109
+ The matplotlib Text object.
110
+ """
111
+ # Auto-apply fontsize from style if not specified
112
+ if "fontsize" not in kwargs:
113
+ kwargs["fontsize"] = self._get_style_fontsize("suptitle_pt", 10)
114
+ # Record the suptitle call
115
+ self._recorder.figure_record.suptitle = {"text": t, "kwargs": kwargs}
116
+ # Call the underlying figure's suptitle
117
+ return self._fig.suptitle(t, **kwargs)
118
+
119
+ def supxlabel(self, t: str, **kwargs) -> Any:
120
+ """Set super x-label for the figure and record it.
121
+
122
+ Parameters
123
+ ----------
124
+ t : str
125
+ The super x-label text.
126
+ **kwargs
127
+ Additional arguments passed to matplotlib's supxlabel().
128
+
129
+ Returns
130
+ -------
131
+ Text
132
+ The matplotlib Text object.
133
+ """
134
+ # Auto-apply fontsize from style if not specified
135
+ if "fontsize" not in kwargs:
136
+ kwargs["fontsize"] = self._get_style_fontsize("supxlabel_pt", 8)
137
+ # Record the supxlabel call
138
+ self._recorder.figure_record.supxlabel = {"text": t, "kwargs": kwargs}
139
+ # Call the underlying figure's supxlabel
140
+ return self._fig.supxlabel(t, **kwargs)
141
+
142
+ def supylabel(self, t: str, **kwargs) -> Any:
143
+ """Set super y-label for the figure and record it.
144
+
145
+ Parameters
146
+ ----------
147
+ t : str
148
+ The super y-label text.
149
+ **kwargs
150
+ Additional arguments passed to matplotlib's supylabel().
151
+
152
+ Returns
153
+ -------
154
+ Text
155
+ The matplotlib Text object.
156
+ """
157
+ # Auto-apply fontsize from style if not specified
158
+ if "fontsize" not in kwargs:
159
+ kwargs["fontsize"] = self._get_style_fontsize("supylabel_pt", 8)
160
+ # Record the supylabel call
161
+ self._recorder.figure_record.supylabel = {"text": t, "kwargs": kwargs}
162
+ # Call the underlying figure's supylabel
163
+ return self._fig.supylabel(t, **kwargs)
164
+
82
165
  def __getattr__(self, name: str) -> Any:
83
166
  """Delegate attribute access to underlying figure."""
84
167
  return getattr(self._fig, name)
@@ -88,6 +171,7 @@ class RecordingFigure:
88
171
  fname,
89
172
  save_recipe: bool = True,
90
173
  recipe_format: Literal["csv", "npz", "inline"] = "csv",
174
+ verbose: bool = True,
91
175
  **kwargs,
92
176
  ):
93
177
  """Save the figure image and optionally the recipe.
@@ -101,6 +185,8 @@ class RecordingFigure:
101
185
  Recipe will be saved with same name but .yaml extension.
102
186
  recipe_format : str
103
187
  Format for data in recipe: 'csv' (default), 'npz', or 'inline'.
188
+ verbose : bool
189
+ If True (default), print save status.
104
190
  **kwargs
105
191
  Passed to matplotlib's savefig().
106
192
 
@@ -118,7 +204,7 @@ class RecordingFigure:
118
204
  >>> fig.savefig('figure.png', save_recipe=False) # Image only
119
205
  """
120
206
  # Handle file-like objects (BytesIO, etc.) - just pass through
121
- if hasattr(fname, 'write'):
207
+ if hasattr(fname, "write"):
122
208
  self._fig.savefig(fname, **kwargs)
123
209
  return fname
124
210
 
@@ -128,8 +214,12 @@ class RecordingFigure:
128
214
  if save_recipe:
129
215
  recipe_path = fname.with_suffix(".yaml")
130
216
  self.save_recipe(recipe_path, include_data=True, data_format=recipe_format)
217
+ if verbose:
218
+ print(f"Saved: {fname} + {recipe_path}")
131
219
  return fname, recipe_path
132
220
 
221
+ if verbose:
222
+ print(f"Saved: {fname}")
133
223
  return fname
134
224
 
135
225
  def save_recipe(
@@ -155,7 +245,10 @@ class RecordingFigure:
155
245
  Path to saved recipe file.
156
246
  """
157
247
  from .._serializer import save_recipe
158
- return save_recipe(self._recorder.figure_record, path, include_data, data_format)
248
+
249
+ return save_recipe(
250
+ self._recorder.figure_record, path, include_data, data_format
251
+ )
159
252
 
160
253
 
161
254
  def create_recording_subplots(
@@ -163,7 +256,7 @@ def create_recording_subplots(
163
256
  ncols: int = 1,
164
257
  recorder: Optional["Recorder"] = None,
165
258
  **kwargs,
166
- ) -> Tuple[RecordingFigure, Union[RecordingAxes, List[RecordingAxes]]]:
259
+ ) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
167
260
  """Create a figure with recording-enabled axes.
168
261
 
169
262
  Parameters
@@ -181,8 +274,8 @@ def create_recording_subplots(
181
274
  -------
182
275
  fig : RecordingFigure
183
276
  Wrapped figure.
184
- axes : RecordingAxes or list
185
- Wrapped axes (single if 1x1, otherwise 2D array).
277
+ axes : RecordingAxes or ndarray
278
+ Wrapped axes (single if 1x1, otherwise numpy array matching matplotlib).
186
279
  """
187
280
  from .._recorder import Recorder
188
281
 
@@ -205,23 +298,27 @@ def create_recording_subplots(
205
298
  wrapped_fig = RecordingFigure(fig, recorder, wrapped_ax)
206
299
  return wrapped_fig, wrapped_ax
207
300
 
208
- # Handle 1D or 2D arrays
209
- import numpy as np
210
- mpl_axes = np.atleast_2d(mpl_axes)
301
+ # Handle 1D or 2D arrays - reshape to (nrows, ncols) for uniform processing
302
+ mpl_axes_arr = np.asarray(mpl_axes)
303
+ if mpl_axes_arr.ndim == 1:
304
+ mpl_axes_arr = mpl_axes_arr.reshape(nrows, ncols)
211
305
 
212
306
  wrapped_axes = []
213
- for i in range(mpl_axes.shape[0]):
307
+ for i in range(nrows):
214
308
  row = []
215
- for j in range(mpl_axes.shape[1]):
216
- row.append(RecordingAxes(mpl_axes[i, j], recorder, position=(i, j)))
309
+ for j in range(ncols):
310
+ row.append(RecordingAxes(mpl_axes_arr[i, j], recorder, position=(i, j)))
217
311
  wrapped_axes.append(row)
218
312
 
219
313
  wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes)
220
314
 
221
- # Return in same shape as matplotlib
315
+ # Return in same shape as matplotlib (numpy arrays for consistency)
222
316
  if nrows == 1:
223
- return wrapped_fig, wrapped_axes[0]
317
+ # 1xN -> 1D array of shape (N,)
318
+ return wrapped_fig, np.array(wrapped_axes[0], dtype=object)
224
319
  elif ncols == 1:
225
- return wrapped_fig, [row[0] for row in wrapped_axes]
320
+ # Nx1 -> 1D array of shape (N,)
321
+ return wrapped_fig, np.array([row[0] for row in wrapped_axes], dtype=object)
226
322
  else:
227
- return wrapped_fig, wrapped_axes
323
+ # NxM -> 2D array
324
+ return wrapped_fig, np.array(wrapped_axes, dtype=object)
figrecipe/plt.py CHANGED
@@ -9,4 +9,3 @@ Usage:
9
9
  """
10
10
 
11
11
  from .pyplot import * # noqa: F401, F403
12
- from .pyplot import __all__
figrecipe/pyplot.py CHANGED
@@ -34,9 +34,10 @@ Examples
34
34
  import matplotlib.pyplot as _plt
35
35
  from matplotlib.pyplot import * # noqa: F401, F403
36
36
 
37
+ from . import save as _ps_save
38
+
37
39
  # Import figrecipe functionality
38
40
  from . import subplots as _ps_subplots
39
- from . import save as _ps_save
40
41
  from ._wrappers import RecordingFigure
41
42
 
42
43
  # Override subplots with recording-enabled version
@@ -18,22 +18,21 @@ Usage:
18
18
  fig, ax = ps.subplots(**style.to_subplots_kwargs())
19
19
  """
20
20
 
21
- from ._style_loader import (
22
- load_style,
23
- unload_style,
24
- get_style,
25
- reload_style,
26
- list_presets,
27
- STYLE,
28
- to_subplots_kwargs,
29
- )
30
-
31
21
  from ._style_applier import (
32
22
  apply_style_mm,
33
23
  apply_theme_colors,
34
24
  check_font,
35
25
  list_available_fonts,
36
26
  )
27
+ from ._style_loader import (
28
+ STYLE,
29
+ get_style,
30
+ list_presets,
31
+ load_style,
32
+ reload_style,
33
+ to_subplots_kwargs,
34
+ unload_style,
35
+ )
37
36
 
38
37
  __all__ = [
39
38
  "load_style",