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.
- figrecipe/__init__.py +361 -93
- figrecipe/_dev/__init__.py +120 -0
- figrecipe/_dev/demo_plotters/__init__.py +195 -0
- figrecipe/_dev/demo_plotters/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_pie.py +23 -0
- figrecipe/_dev/demo_plotters/plot_plot.py +27 -0
- figrecipe/_dev/demo_plotters/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/plot_streamplot.py +30 -0
- figrecipe/_dev/demo_plotters/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_xcorr.py +25 -0
- figrecipe/_editor/__init__.py +230 -0
- figrecipe/_editor/_bbox.py +978 -0
- figrecipe/_editor/_flask_app.py +1229 -0
- figrecipe/_editor/_hitmap.py +937 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_renderer.py +349 -0
- figrecipe/_editor/_templates/__init__.py +75 -0
- figrecipe/_editor/_templates/_html.py +406 -0
- figrecipe/_editor/_templates/_scripts.py +2778 -0
- figrecipe/_editor/_templates/_styles.py +1326 -0
- figrecipe/_params/_DECORATION_METHODS.py +27 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +126 -73
- figrecipe/_reproducer.py +658 -41
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_loader.py +515 -56
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +860 -46
- figrecipe/_wrappers/_figure.py +115 -18
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +9 -10
- figrecipe/styles/_style_applier.py +332 -28
- figrecipe/styles/_style_loader.py +172 -44
- figrecipe/styles/presets/MATPLOTLIB.yaml +94 -0
- figrecipe/styles/presets/SCITEX.yaml +176 -0
- figrecipe-0.6.0.dist-info/METADATA +394 -0
- figrecipe-0.6.0.dist-info/RECORD +90 -0
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/WHEEL +0 -0
- {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"]
|