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
+ """stem: stem plot demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_stem(plt, rng, ax=None):
9
+ """Stem plot demo.
10
+
11
+ Demonstrates: ax.stem()
12
+ """
13
+ if ax is None:
14
+ fig, ax = plt.subplots()
15
+ else:
16
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
17
+
18
+ x = np.arange(10)
19
+ y = rng.uniform(0, 1, 10)
20
+ ax.stem(x, y, id="stem")
21
+ ax.set_xlabel("X")
22
+ ax.set_ylabel("Y")
23
+ ax.set_title("stem")
24
+ return fig, ax
25
+
26
+
27
+ # EOF
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """step: step plot demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_step(plt, rng, ax=None):
9
+ """Step plot demo.
10
+
11
+ Demonstrates: ax.step()
12
+ """
13
+ if ax is None:
14
+ fig, ax = plt.subplots()
15
+ else:
16
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
17
+
18
+ x = np.arange(10)
19
+ y = rng.uniform(0, 1, 10)
20
+ ax.step(x, y, where="mid", id="step")
21
+ ax.set_xlabel("X")
22
+ ax.set_ylabel("Y")
23
+ ax.set_title("step")
24
+ return fig, ax
25
+
26
+
27
+ # EOF
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """streamplot: streamline plot demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_streamplot(plt, rng, ax=None):
9
+ """Streamline plot demo.
10
+
11
+ Demonstrates: ax.streamplot()
12
+ """
13
+ if ax is None:
14
+ fig, ax = plt.subplots()
15
+ else:
16
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
17
+
18
+ x = np.linspace(-3, 3, 30)
19
+ y = np.linspace(-3, 3, 30)
20
+ X, Y = np.meshgrid(x, y)
21
+ U = -Y
22
+ V = X
23
+ ax.streamplot(X, Y, U, V, id="streamplot")
24
+ ax.set_xlabel("X")
25
+ ax.set_ylabel("Y")
26
+ ax.set_title("streamplot")
27
+ return fig, ax
28
+
29
+
30
+ # EOF
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """tricontour: triangular contour demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_tricontour(plt, rng, ax=None):
9
+ """Triangular contour demo.
10
+
11
+ Demonstrates: ax.tricontour()
12
+ """
13
+ if ax is None:
14
+ fig, ax = plt.subplots()
15
+ else:
16
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
17
+
18
+ x = rng.uniform(0, 1, 50)
19
+ y = rng.uniform(0, 1, 50)
20
+ z = np.sin(x * 2 * np.pi) * np.cos(y * 2 * np.pi)
21
+ ax.tricontour(x, y, z, id="tricontour")
22
+ ax.set_xlabel("X")
23
+ ax.set_ylabel("Y")
24
+ ax.set_title("tricontour")
25
+ return fig, ax
26
+
27
+
28
+ # EOF
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """tricontourf: filled triangular contour demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_tricontourf(plt, rng, ax=None):
9
+ """Filled triangular contour demo.
10
+
11
+ Demonstrates: ax.tricontourf()
12
+ """
13
+ if ax is None:
14
+ fig, ax = plt.subplots()
15
+ else:
16
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
17
+
18
+ x = rng.uniform(0, 1, 50)
19
+ y = rng.uniform(0, 1, 50)
20
+ z = np.sin(x * 2 * np.pi) * np.cos(y * 2 * np.pi)
21
+ ax.tricontourf(x, y, z, id="tricontourf")
22
+ ax.set_xlabel("X")
23
+ ax.set_ylabel("Y")
24
+ ax.set_title("tricontourf")
25
+ return fig, ax
26
+
27
+
28
+ # EOF
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """tripcolor: unstructured triangular grid demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_tripcolor(plt, rng, ax=None):
9
+ """Unstructured triangular grid demo.
10
+
11
+ Demonstrates: ax.tripcolor()
12
+ """
13
+ if ax is None:
14
+ fig, ax = plt.subplots()
15
+ else:
16
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
17
+
18
+ # Create random triangulation
19
+ x = rng.uniform(0, 1, 30)
20
+ y = rng.uniform(0, 1, 30)
21
+ z = np.sin(x * 2 * np.pi) * np.cos(y * 2 * np.pi)
22
+ ax.tripcolor(x, y, z, id="tripcolor")
23
+ ax.set_xlabel("X")
24
+ ax.set_ylabel("Y")
25
+ ax.set_title("tripcolor")
26
+ return fig, ax
27
+
28
+
29
+ # EOF
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """triplot: triangular mesh plot demo."""
4
+
5
+
6
+ def plot_triplot(plt, rng, ax=None):
7
+ """Triangular mesh plot demo.
8
+
9
+ Demonstrates: ax.triplot()
10
+ """
11
+ if ax is None:
12
+ fig, ax = plt.subplots()
13
+ else:
14
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
15
+
16
+ x = rng.uniform(0, 1, 20)
17
+ y = rng.uniform(0, 1, 20)
18
+ ax.triplot(x, y, id="triplot")
19
+ ax.set_xlabel("X")
20
+ ax.set_ylabel("Y")
21
+ ax.set_title("triplot")
22
+ return fig, ax
23
+
24
+
25
+ # EOF
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """violinplot: violin plot demo."""
4
+
5
+
6
+ def plot_violinplot(plt, rng, ax=None):
7
+ """Violin plot demo.
8
+
9
+ Demonstrates: ax.violinplot()
10
+ """
11
+ if ax is None:
12
+ fig, ax = plt.subplots()
13
+ else:
14
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
15
+
16
+ data = [rng.normal(i, 1, 100) for i in range(4)]
17
+ # Modern style: show box inside (default from SCITEX style)
18
+ ax.violinplot(data, id="violinplot")
19
+ ax.set_xlabel("Group")
20
+ ax.set_ylabel("Value")
21
+ ax.set_title("violinplot")
22
+ return fig, ax
23
+
24
+
25
+ # EOF
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """xcorr: cross-correlation demo."""
4
+
5
+
6
+ def plot_xcorr(plt, rng, ax=None):
7
+ """Cross-correlation demo.
8
+
9
+ Demonstrates: ax.xcorr()
10
+ """
11
+ if ax is None:
12
+ fig, ax = plt.subplots()
13
+ else:
14
+ fig = ax.get_figure() if hasattr(ax, "get_figure") else ax.fig
15
+
16
+ x = rng.normal(0, 1, 100)
17
+ y = rng.normal(0, 1, 100)
18
+ ax.xcorr(x, y, maxlags=50, id="xcorr")
19
+ ax.set_xlabel("Lag")
20
+ ax.set_ylabel("Cross-correlation")
21
+ ax.set_title("xcorr")
22
+ return fig, ax
23
+
24
+
25
+ # EOF
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ figrecipe GUI Editor - Interactive figure styling with hitmap-based element selection.
5
+
6
+ This module provides a Flask-based web editor for interactively adjusting
7
+ figure styles. It supports both live RecordingFigure objects and saved
8
+ recipe files (.yaml).
9
+
10
+ Usage
11
+ -----
12
+ >>> import figrecipe as fr
13
+ >>> fig, ax = fr.subplots()
14
+ >>> ax.plot(x, y, id='data')
15
+ >>> fr.edit(fig) # Opens browser with interactive editor
16
+
17
+ >>> # Or from saved recipe
18
+ >>> fr.edit('recipe.yaml')
19
+ """
20
+
21
+ from pathlib import Path
22
+ from typing import Any, Dict, Optional, Union
23
+
24
+ from .._wrappers import RecordingFigure
25
+ from ._flask_app import FigureEditor
26
+
27
+
28
+ def edit(
29
+ source: Union[RecordingFigure, str, Path],
30
+ style: Optional[Union[str, Dict[str, Any]]] = None,
31
+ port: int = 5050,
32
+ open_browser: bool = True,
33
+ ) -> Dict[str, Any]:
34
+ """
35
+ Launch interactive GUI editor for figure styling.
36
+
37
+ Opens a browser-based editor that allows interactive adjustment of
38
+ figure styles using hitmap-based element selection.
39
+
40
+ Parameters
41
+ ----------
42
+ source : RecordingFigure, str, or Path
43
+ Either a live RecordingFigure object or path to a .yaml recipe file.
44
+ style : str or dict, optional
45
+ Style preset name (e.g., 'SCITEX', 'SCITEX_DARK') or style dict.
46
+ If None, uses the currently loaded global style.
47
+ port : int, optional
48
+ Flask server port (default: 5050). Auto-finds available port if occupied.
49
+ open_browser : bool, optional
50
+ Whether to open browser automatically (default: True).
51
+
52
+ Returns
53
+ -------
54
+ dict
55
+ Final style overrides after editing session.
56
+
57
+ Raises
58
+ ------
59
+ ImportError
60
+ If Flask is not installed.
61
+ TypeError
62
+ If source is neither RecordingFigure nor valid path.
63
+ FileNotFoundError
64
+ If recipe file path does not exist.
65
+
66
+ Examples
67
+ --------
68
+ Edit a live figure:
69
+
70
+ >>> import figrecipe as fr
71
+ >>> fig, ax = fr.subplots()
72
+ >>> ax.plot([1, 2, 3], [1, 4, 9], id='quadratic')
73
+ >>> overrides = fr.edit(fig)
74
+
75
+ Edit a saved recipe:
76
+
77
+ >>> overrides = fr.edit('my_figure.yaml')
78
+
79
+ With explicit style:
80
+
81
+ >>> overrides = fr.edit(fig, style='SCITEX_DARK')
82
+ """
83
+ import importlib.util
84
+
85
+ if importlib.util.find_spec("flask") is None:
86
+ raise ImportError(
87
+ "Flask is required for the GUI editor. "
88
+ "Install with: pip install figrecipe[editor] or pip install flask"
89
+ )
90
+
91
+ import tempfile
92
+
93
+ from ._flask_app import FigureEditor
94
+ from ._hitmap import generate_hitmap, hitmap_to_base64
95
+
96
+ # Handle different input types
97
+ fig, recipe_path = _resolve_source(source)
98
+
99
+ # Load style if string preset name provided
100
+ style_dict = _resolve_style(style)
101
+
102
+ # Save static PNG FIRST - this is the source of truth for initial display
103
+ mpl_fig = fig.fig if hasattr(fig, "fig") else fig
104
+ static_png_path = Path(tempfile.mktemp(suffix="_figrecipe_static.png"))
105
+ mpl_fig.savefig(static_png_path, format="png", dpi=150, bbox_inches="tight")
106
+
107
+ # Generate hitmap ONCE at this point
108
+ # Pass RecordingFigure to preserve record for plot type detection
109
+ hitmap, color_map = generate_hitmap(fig)
110
+ hitmap_base64 = hitmap_to_base64(hitmap)
111
+
112
+ # Create and run editor with pre-rendered static PNG
113
+ editor = FigureEditor(
114
+ fig=fig,
115
+ recipe_path=recipe_path,
116
+ style=style_dict,
117
+ port=port,
118
+ static_png_path=static_png_path,
119
+ hitmap_base64=hitmap_base64,
120
+ color_map=color_map,
121
+ )
122
+
123
+ return editor.run(open_browser=open_browser)
124
+
125
+
126
+ def _resolve_source(source: Union[RecordingFigure, str, Path]):
127
+ """
128
+ Resolve source to figure and optional recipe path.
129
+
130
+ Parameters
131
+ ----------
132
+ source : RecordingFigure, str, or Path
133
+ Input source.
134
+
135
+ Returns
136
+ -------
137
+ tuple
138
+ (RecordingFigure or None, Path or None)
139
+ """
140
+ if isinstance(source, RecordingFigure):
141
+ return source, None
142
+
143
+ # Handle matplotlib Figure (e.g., from reproduce())
144
+ from matplotlib.figure import Figure
145
+
146
+ if isinstance(source, Figure):
147
+ from .._recorder import FigureRecord, Recorder
148
+ from .._wrappers._figure import RecordingFigure as RF
149
+
150
+ wrapped_fig = RF.__new__(RF)
151
+ wrapped_fig._fig = source
152
+ wrapped_fig._axes = [[ax] for ax in source.axes] # 2D list format
153
+ wrapped_fig._recorder = Recorder()
154
+ wrapped_fig._recorder._figure_record = FigureRecord(
155
+ figsize=tuple(source.get_size_inches()),
156
+ dpi=int(source.dpi),
157
+ )
158
+ return wrapped_fig, None
159
+
160
+ # Assume it's a path
161
+ path = Path(source)
162
+ if not path.exists():
163
+ raise FileNotFoundError(f"Recipe file not found: {path}")
164
+
165
+ if path.suffix.lower() not in (".yaml", ".yml"):
166
+ raise ValueError(f"Expected .yaml or .yml file, got: {path.suffix}")
167
+
168
+ # Load recipe and reproduce figure
169
+ from .._reproducer import reproduce
170
+
171
+ fig, axes = reproduce(path)
172
+
173
+ # Wrap in RecordingFigure if needed
174
+ if not isinstance(fig, RecordingFigure):
175
+ from .._wrappers._figure import RecordingFigure as RF
176
+
177
+ # Create a minimal wrapper
178
+ wrapped_fig = RF.__new__(RF)
179
+ wrapped_fig.fig = fig
180
+ wrapped_fig._axes = axes if isinstance(axes, list) else [axes]
181
+ from .._recorder import FigureRecord
182
+
183
+ wrapped_fig.record = FigureRecord(
184
+ figsize=fig.get_size_inches().tolist(),
185
+ dpi=fig.dpi,
186
+ )
187
+ fig = wrapped_fig
188
+
189
+ return fig, path
190
+
191
+
192
+ def _resolve_style(
193
+ style: Optional[Union[str, Dict[str, Any]]],
194
+ ) -> Optional[Dict[str, Any]]:
195
+ """
196
+ Resolve style to dictionary.
197
+
198
+ Parameters
199
+ ----------
200
+ style : str, dict, or None
201
+ Style preset name or dict.
202
+
203
+ Returns
204
+ -------
205
+ dict or None
206
+ Style dictionary.
207
+ """
208
+ if style is None:
209
+ # Use global style if loaded
210
+ from ..styles._style_loader import _STYLE_CACHE
211
+
212
+ if _STYLE_CACHE is not None:
213
+ from ..styles import to_subplots_kwargs
214
+
215
+ return to_subplots_kwargs(_STYLE_CACHE)
216
+ return None
217
+
218
+ if isinstance(style, dict):
219
+ return style
220
+
221
+ if isinstance(style, str):
222
+ from ..styles import load_style, to_subplots_kwargs
223
+
224
+ loaded = load_style(style)
225
+ return to_subplots_kwargs(loaded) if loaded else None
226
+
227
+ raise TypeError(f"style must be str, dict, or None, got {type(style)}")
228
+
229
+
230
+ __all__ = ["edit", "FigureEditor"]