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
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """csd: cross spectral density demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_csd(plt, rng, ax=None):
9
+ """Cross spectral density demo.
10
+
11
+ Demonstrates: ax.csd()
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
+ fs = 1000
19
+ t = np.linspace(0, 1, fs)
20
+ x = np.sin(2 * np.pi * 50 * t) + rng.normal(0, 0.3, len(t))
21
+ y = np.sin(2 * np.pi * 50 * t + np.pi / 4) + rng.normal(0, 0.3, len(t))
22
+ ax.csd(x, y, Fs=fs, id="csd")
23
+ ax.set_xlabel("Frequency")
24
+ ax.set_ylabel("CSD")
25
+ ax.set_title("csd")
26
+ return fig, ax
27
+
28
+
29
+ # EOF
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """magnitude_spectrum: magnitude spectrum demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_magnitude_spectrum(plt, rng, ax=None):
9
+ """Magnitude spectrum demo.
10
+
11
+ Demonstrates: ax.magnitude_spectrum()
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
+ fs = 1000
19
+ t = np.linspace(0, 1, fs)
20
+ signal = np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t)
21
+ ax.magnitude_spectrum(signal, Fs=fs, id="magnitude_spectrum")
22
+ ax.set_xlabel("Frequency")
23
+ ax.set_ylabel("Magnitude")
24
+ ax.set_title("magnitude_spectrum")
25
+ return fig, ax
26
+
27
+
28
+ # EOF
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """phase_spectrum: phase spectrum demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_phase_spectrum(plt, rng, ax=None):
9
+ """Phase spectrum demo.
10
+
11
+ Demonstrates: ax.phase_spectrum()
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
+ fs = 1000
19
+ t = np.linspace(0, 1, fs)
20
+ signal = np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t + np.pi / 3)
21
+ ax.phase_spectrum(signal, Fs=fs, id="phase_spectrum")
22
+ ax.set_xlabel("Frequency")
23
+ ax.set_ylabel("Phase (radians)")
24
+ ax.set_title("phase_spectrum")
25
+ return fig, ax
26
+
27
+
28
+ # EOF
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """psd: power spectral density demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_psd(plt, rng, ax=None):
9
+ """Power spectral density demo.
10
+
11
+ Demonstrates: ax.psd()
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
+ fs = 1000
19
+ t = np.linspace(0, 1, fs)
20
+ signal = np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t)
21
+ signal += rng.normal(0, 0.3, len(t))
22
+ ax.psd(signal, Fs=fs, id="psd")
23
+ ax.set_xlabel("Frequency")
24
+ ax.set_ylabel("Power/Frequency (dB/Hz)")
25
+ ax.set_title("psd")
26
+ return fig, ax
27
+
28
+
29
+ # EOF
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """specgram: spectrogram demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_specgram(plt, rng, ax=None):
9
+ """Spectrogram demo.
10
+
11
+ Demonstrates: ax.specgram()
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 signal with varying frequency
19
+ fs = 1000
20
+ t = np.linspace(0, 2, 2000)
21
+ freq = 50 + 50 * t # Chirp signal
22
+ signal = np.sin(2 * np.pi * freq * t)
23
+ ax.specgram(signal, Fs=fs, id="specgram")
24
+ ax.set_xlabel("Time")
25
+ ax.set_ylabel("Frequency")
26
+ ax.set_title("specgram")
27
+ return fig, ax
28
+
29
+
30
+ # 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,4 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Demo plotters for vector_flow category."""
4
+
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """barbs: wind barbs demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_barbs(plt, rng, ax=None):
9
+ """Wind barbs demo.
10
+
11
+ Demonstrates: ax.barbs()
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(0, 5, 1)
19
+ y = np.arange(0, 5, 1)
20
+ X, Y = np.meshgrid(x, y)
21
+ U = rng.uniform(-10, 10, X.shape)
22
+ V = rng.uniform(-10, 10, Y.shape)
23
+ ax.barbs(X, Y, U, V, id="barbs")
24
+ ax.set_xlabel("X")
25
+ ax.set_ylabel("Y")
26
+ ax.set_title("barbs")
27
+ return fig, ax
28
+
29
+
30
+ # EOF
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """quiver: vector field demo."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def plot_quiver(plt, rng, ax=None):
9
+ """Vector field demo.
10
+
11
+ Demonstrates: ax.quiver()
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(0, 10, 1)
19
+ y = np.arange(0, 10, 1)
20
+ X, Y = np.meshgrid(x, y)
21
+ U = np.cos(X * 0.5)
22
+ V = np.sin(Y * 0.5)
23
+ ax.quiver(X, Y, U, V, id="quiver")
24
+ ax.set_xlabel("X")
25
+ ax.set_ylabel("Y")
26
+ ax.set_title("quiver")
27
+ return fig, ax
28
+
29
+
30
+ # 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,278 @@
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: Optional[Union[RecordingFigure, str, Path]] = None,
30
+ style: Optional[Union[str, Dict[str, Any]]] = None,
31
+ port: int = 5050,
32
+ open_browser: bool = True,
33
+ hot_reload: bool = False,
34
+ working_dir: Optional[Union[str, Path]] = None,
35
+ ) -> Dict[str, Any]:
36
+ """
37
+ Launch interactive GUI editor for figure styling.
38
+
39
+ Opens a browser-based editor that allows interactive adjustment of
40
+ figure styles using hitmap-based element selection.
41
+
42
+ Parameters
43
+ ----------
44
+ source : RecordingFigure, str, Path, or None
45
+ Either a live RecordingFigure object, path to a .yaml/.png file,
46
+ or None to create a new blank figure.
47
+ style : str or dict, optional
48
+ Style preset name (e.g., 'SCITEX', 'SCITEX_DARK') or style dict.
49
+ If None, uses the currently loaded global style.
50
+ port : int, optional
51
+ Flask server port (default: 5050). Auto-finds available port if occupied.
52
+ open_browser : bool, optional
53
+ Whether to open browser automatically (default: True).
54
+ hot_reload : bool, optional
55
+ Enable hot reload - server restarts when source files change (default: False).
56
+ Like Django's development server. Browser auto-refreshes on reconnect.
57
+ working_dir : str or Path, optional
58
+ Working directory for file switching feature (default: current directory).
59
+ The file switcher will list recipe files from this directory.
60
+
61
+ Returns
62
+ -------
63
+ dict
64
+ Final style overrides after editing session.
65
+
66
+ Raises
67
+ ------
68
+ ImportError
69
+ If Flask is not installed.
70
+ TypeError
71
+ If source is neither RecordingFigure nor valid path.
72
+ FileNotFoundError
73
+ If recipe file path does not exist.
74
+
75
+ Examples
76
+ --------
77
+ Edit a live figure:
78
+
79
+ >>> import figrecipe as fr
80
+ >>> fig, ax = fr.subplots()
81
+ >>> ax.plot([1, 2, 3], [1, 4, 9], id='quadratic')
82
+ >>> overrides = fr.edit(fig)
83
+
84
+ Edit a saved recipe:
85
+
86
+ >>> overrides = fr.edit('my_figure.yaml')
87
+
88
+ With explicit style:
89
+
90
+ >>> overrides = fr.edit(fig, style='SCITEX_DARK')
91
+ """
92
+ import importlib.util
93
+
94
+ if importlib.util.find_spec("flask") is None:
95
+ raise ImportError(
96
+ "Flask is required for the GUI editor. "
97
+ "Install with: pip install figrecipe[editor] or pip install flask"
98
+ )
99
+
100
+ import tempfile
101
+
102
+ from ._flask_app import FigureEditor
103
+ from ._hitmap import generate_hitmap, hitmap_to_base64
104
+
105
+ # Handle different input types
106
+ fig, recipe_path = _resolve_source(source)
107
+
108
+ # Load style if string preset name provided
109
+ style_dict = _resolve_style(style)
110
+
111
+ # Save static PNG FIRST - this is the source of truth for initial display
112
+ mpl_fig = fig.fig if hasattr(fig, "fig") else fig
113
+ static_png_path = Path(tempfile.mktemp(suffix="_figrecipe_static.png"))
114
+ mpl_fig.savefig(static_png_path, format="png", dpi=150, bbox_inches="tight")
115
+
116
+ # Generate hitmap ONCE at this point
117
+ # Pass RecordingFigure to preserve record for plot type detection
118
+ hitmap, color_map = generate_hitmap(fig)
119
+ hitmap_base64 = hitmap_to_base64(hitmap)
120
+
121
+ # Resolve working directory
122
+ resolved_working_dir = Path(working_dir) if working_dir else Path.cwd()
123
+
124
+ # Create and run editor with pre-rendered static PNG
125
+ editor = FigureEditor(
126
+ fig=fig,
127
+ recipe_path=recipe_path,
128
+ style=style_dict,
129
+ port=port,
130
+ static_png_path=static_png_path,
131
+ hitmap_base64=hitmap_base64,
132
+ color_map=color_map,
133
+ hot_reload=hot_reload,
134
+ working_dir=resolved_working_dir,
135
+ )
136
+
137
+ return editor.run(open_browser=open_browser)
138
+
139
+
140
+ def _resolve_source(source: Optional[Union[RecordingFigure, str, Path]]):
141
+ """
142
+ Resolve source to figure and optional recipe path.
143
+
144
+ Parameters
145
+ ----------
146
+ source : RecordingFigure, str, Path, or None
147
+ Input source. If None, creates a new blank figure.
148
+ If PNG path, tries to find associated YAML recipe.
149
+
150
+ Returns
151
+ -------
152
+ tuple
153
+ (RecordingFigure, Path or None)
154
+ """
155
+ # Handle None - create new blank figure
156
+ if source is None:
157
+ from .. import subplots
158
+
159
+ fig, ax = subplots()
160
+ ax.set_title("New Figure")
161
+ ax.text(
162
+ 0.5,
163
+ 0.5,
164
+ "Add plots using fr.edit(fig)",
165
+ ha="center",
166
+ va="center",
167
+ transform=ax.transAxes,
168
+ fontsize=12,
169
+ color="gray",
170
+ )
171
+ return fig, None
172
+
173
+ if isinstance(source, RecordingFigure):
174
+ return source, None
175
+
176
+ # Handle matplotlib Figure (e.g., from reproduce())
177
+ from matplotlib.figure import Figure
178
+
179
+ if isinstance(source, Figure):
180
+ from .._recorder import FigureRecord, Recorder
181
+ from .._wrappers._figure import RecordingFigure as RF
182
+
183
+ wrapped_fig = RF.__new__(RF)
184
+ wrapped_fig._fig = source
185
+ wrapped_fig._axes = [[ax] for ax in source.axes] # 2D list format
186
+ wrapped_fig._recorder = Recorder()
187
+ wrapped_fig._recorder._figure_record = FigureRecord(
188
+ figsize=tuple(source.get_size_inches()),
189
+ dpi=int(source.dpi),
190
+ )
191
+ return wrapped_fig, None
192
+
193
+ # Assume it's a path
194
+ path = Path(source)
195
+ if not path.exists():
196
+ raise FileNotFoundError(f"File not found: {path}")
197
+
198
+ # Handle PNG path - find associated YAML
199
+ if path.suffix.lower() == ".png":
200
+ yaml_path = path.with_suffix(".yaml")
201
+ if yaml_path.exists():
202
+ path = yaml_path
203
+ else:
204
+ yml_path = path.with_suffix(".yml")
205
+ if yml_path.exists():
206
+ path = yml_path
207
+ else:
208
+ raise FileNotFoundError(
209
+ f"No recipe found for {path.name}. "
210
+ f"Expected {yaml_path.name} or {yml_path.name}"
211
+ )
212
+
213
+ if path.suffix.lower() not in (".yaml", ".yml"):
214
+ raise ValueError(f"Expected .yaml, .yml, or .png file, got: {path.suffix}")
215
+
216
+ # Load recipe and reproduce figure
217
+ from .._reproducer import reproduce
218
+
219
+ fig, axes = reproduce(path)
220
+
221
+ # Wrap in RecordingFigure if needed
222
+ if not isinstance(fig, RecordingFigure):
223
+ from .._wrappers._figure import RecordingFigure as RF
224
+
225
+ # Create a minimal wrapper
226
+ wrapped_fig = RF.__new__(RF)
227
+ wrapped_fig.fig = fig
228
+ wrapped_fig._axes = axes if isinstance(axes, list) else [axes]
229
+ from .._recorder import FigureRecord
230
+
231
+ wrapped_fig.record = FigureRecord(
232
+ figsize=fig.get_size_inches().tolist(),
233
+ dpi=fig.dpi,
234
+ )
235
+ fig = wrapped_fig
236
+
237
+ return fig, path
238
+
239
+
240
+ def _resolve_style(
241
+ style: Optional[Union[str, Dict[str, Any]]],
242
+ ) -> Optional[Dict[str, Any]]:
243
+ """
244
+ Resolve style to dictionary.
245
+
246
+ Parameters
247
+ ----------
248
+ style : str, dict, or None
249
+ Style preset name or dict.
250
+
251
+ Returns
252
+ -------
253
+ dict or None
254
+ Style dictionary.
255
+ """
256
+ if style is None:
257
+ # Use global style if loaded
258
+ from ..styles._style_loader import _STYLE_CACHE
259
+
260
+ if _STYLE_CACHE is not None:
261
+ from ..styles import to_subplots_kwargs
262
+
263
+ return to_subplots_kwargs(_STYLE_CACHE)
264
+ return None
265
+
266
+ if isinstance(style, dict):
267
+ return style
268
+
269
+ if isinstance(style, str):
270
+ from ..styles import load_style, to_subplots_kwargs
271
+
272
+ loaded = load_style(style)
273
+ return to_subplots_kwargs(loaded) if loaded else None
274
+
275
+ raise TypeError(f"style must be str, dict, or None, got {type(style)}")
276
+
277
+
278
+ __all__ = ["edit", "FigureEditor"]
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Modular bbox extraction for figure elements.
5
+
6
+ This package provides functions for extracting bounding boxes from
7
+ matplotlib figure elements for hit detection in the GUI editor.
8
+
9
+ The main function `extract_bboxes` is re-exported from _bbox_main.py
10
+ for backward compatibility.
11
+
12
+ Modules:
13
+ - _transforms: Coordinate transformation utilities
14
+ - _elements: General element, text, and tick bbox extraction
15
+ - _lines: Line and quiver bbox extraction
16
+ - _collections: Collection (scatter, fill) and patch bbox extraction
17
+ """
18
+
19
+ # Re-export main function from _extract.py
20
+ # Import modular helpers
21
+ from ._collections import get_collection_bbox, get_patch_bbox
22
+ from ._elements import get_element_bbox, get_text_bbox, get_tick_labels_bbox
23
+ from ._extract import extract_bboxes
24
+ from ._lines import get_line_bbox, get_quiver_bbox
25
+ from ._transforms import display_to_image, transform_bbox
26
+
27
+ __all__ = [
28
+ # Main function
29
+ "extract_bboxes",
30
+ # Transforms
31
+ "transform_bbox",
32
+ "display_to_image",
33
+ # Elements
34
+ "get_element_bbox",
35
+ "get_text_bbox",
36
+ "get_tick_labels_bbox",
37
+ # Lines
38
+ "get_line_bbox",
39
+ "get_quiver_bbox",
40
+ # Collections
41
+ "get_collection_bbox",
42
+ "get_patch_bbox",
43
+ ]