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.
- figrecipe/__init__.py +220 -819
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +258 -0
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- 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 +193 -170
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- 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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Core Flask route handlers for the figure editor.
|
|
5
|
+
Handles main page, preview, update, hitmap, and file switching routes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from flask import jsonify, render_template_string, request
|
|
9
|
+
|
|
10
|
+
from ._helpers import render_with_overrides
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_core_routes(app, editor):
|
|
14
|
+
"""Register core routes with the Flask app."""
|
|
15
|
+
from ._hitmap import generate_hitmap, hitmap_to_base64
|
|
16
|
+
from ._templates import build_html_template
|
|
17
|
+
|
|
18
|
+
@app.route("/")
|
|
19
|
+
def index():
|
|
20
|
+
"""Main editor page."""
|
|
21
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
22
|
+
editor.fig,
|
|
23
|
+
editor.get_effective_style(),
|
|
24
|
+
editor.dark_mode,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
style_name = getattr(editor, "_style_name", "SCITEX")
|
|
28
|
+
|
|
29
|
+
html = build_html_template(
|
|
30
|
+
image_base64=base64_img,
|
|
31
|
+
bboxes=bboxes,
|
|
32
|
+
color_map=editor._color_map,
|
|
33
|
+
style=editor.style,
|
|
34
|
+
overrides=editor.get_effective_style(),
|
|
35
|
+
img_size=img_size,
|
|
36
|
+
style_name=style_name,
|
|
37
|
+
hot_reload=editor.hot_reload,
|
|
38
|
+
dark_mode=editor.dark_mode,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return render_template_string(html)
|
|
42
|
+
|
|
43
|
+
@app.route("/preview")
|
|
44
|
+
def preview():
|
|
45
|
+
"""Get current preview image."""
|
|
46
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
47
|
+
editor.fig,
|
|
48
|
+
editor.get_effective_style(),
|
|
49
|
+
editor.dark_mode,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return jsonify(
|
|
53
|
+
{
|
|
54
|
+
"image": base64_img,
|
|
55
|
+
"bboxes": bboxes,
|
|
56
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@app.route("/ping")
|
|
61
|
+
def ping():
|
|
62
|
+
"""Health check endpoint for hot reload detection."""
|
|
63
|
+
return jsonify({"status": "ok"})
|
|
64
|
+
|
|
65
|
+
@app.route("/update", methods=["POST"])
|
|
66
|
+
def update():
|
|
67
|
+
"""Update preview with new style overrides."""
|
|
68
|
+
from ._preferences import set_preference
|
|
69
|
+
|
|
70
|
+
data = request.get_json() or {}
|
|
71
|
+
|
|
72
|
+
editor.overrides.update(data.get("overrides", {}))
|
|
73
|
+
|
|
74
|
+
# Update and persist dark mode preference
|
|
75
|
+
new_dark_mode = data.get("dark_mode")
|
|
76
|
+
if new_dark_mode is not None and new_dark_mode != editor.dark_mode:
|
|
77
|
+
editor.dark_mode = new_dark_mode
|
|
78
|
+
set_preference("dark_mode", new_dark_mode)
|
|
79
|
+
|
|
80
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
81
|
+
editor.fig,
|
|
82
|
+
editor.get_effective_style(),
|
|
83
|
+
editor.dark_mode,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return jsonify(
|
|
87
|
+
{
|
|
88
|
+
"image": base64_img,
|
|
89
|
+
"bboxes": bboxes,
|
|
90
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@app.route("/hitmap")
|
|
95
|
+
def hitmap():
|
|
96
|
+
"""Get hitmap image and color map (lazy generation on first request)."""
|
|
97
|
+
if not editor._hitmap_generated:
|
|
98
|
+
print("Generating hitmap (first request)...")
|
|
99
|
+
hitmap_img, editor._color_map = generate_hitmap(editor.fig)
|
|
100
|
+
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
101
|
+
editor._hitmap_generated = True
|
|
102
|
+
print("Hitmap ready.")
|
|
103
|
+
|
|
104
|
+
return jsonify(
|
|
105
|
+
{
|
|
106
|
+
"image": editor._hitmap_base64,
|
|
107
|
+
"color_map": editor._color_map,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@app.route("/api/files")
|
|
112
|
+
def list_files():
|
|
113
|
+
"""List available recipe files in working directory."""
|
|
114
|
+
from pathlib import Path
|
|
115
|
+
|
|
116
|
+
working_dir = getattr(editor, "working_dir", Path.cwd())
|
|
117
|
+
files = []
|
|
118
|
+
|
|
119
|
+
# Find all YAML recipe files
|
|
120
|
+
for pattern in ["*.yaml", "*.yml"]:
|
|
121
|
+
for f in working_dir.glob(pattern):
|
|
122
|
+
# Skip hidden files and overrides files
|
|
123
|
+
if f.name.startswith(".") or f.name.endswith(".overrides.yaml"):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Check for associated PNG
|
|
127
|
+
png_path = f.with_suffix(".png")
|
|
128
|
+
has_png = png_path.exists()
|
|
129
|
+
|
|
130
|
+
files.append(
|
|
131
|
+
{
|
|
132
|
+
"path": str(f.relative_to(working_dir)),
|
|
133
|
+
"name": f.stem,
|
|
134
|
+
"has_image": has_png,
|
|
135
|
+
"is_current": (
|
|
136
|
+
editor.recipe_path
|
|
137
|
+
and f.resolve() == editor.recipe_path.resolve()
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Sort by name
|
|
143
|
+
files.sort(key=lambda x: x["name"].lower())
|
|
144
|
+
|
|
145
|
+
return jsonify(
|
|
146
|
+
{
|
|
147
|
+
"files": files,
|
|
148
|
+
"working_dir": str(working_dir),
|
|
149
|
+
"current_file": (
|
|
150
|
+
str(editor.recipe_path.name) if editor.recipe_path else None
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@app.route("/api/switch", methods=["POST"])
|
|
156
|
+
def switch_file():
|
|
157
|
+
"""Switch to a different recipe file."""
|
|
158
|
+
from pathlib import Path
|
|
159
|
+
|
|
160
|
+
from .._reproducer import reproduce
|
|
161
|
+
|
|
162
|
+
data = request.get_json() or {}
|
|
163
|
+
file_path = data.get("path")
|
|
164
|
+
|
|
165
|
+
if not file_path:
|
|
166
|
+
return jsonify({"error": "No file path provided"}), 400
|
|
167
|
+
|
|
168
|
+
working_dir = getattr(editor, "working_dir", Path.cwd())
|
|
169
|
+
full_path = working_dir / file_path
|
|
170
|
+
|
|
171
|
+
if not full_path.exists():
|
|
172
|
+
return jsonify({"error": f"File not found: {file_path}"}), 404
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Reproduce the figure from the new recipe
|
|
176
|
+
fig, axes = reproduce(full_path)
|
|
177
|
+
|
|
178
|
+
# Wrap in RecordingFigure if needed
|
|
179
|
+
from .._wrappers._figure import RecordingFigure
|
|
180
|
+
|
|
181
|
+
if not isinstance(fig, RecordingFigure):
|
|
182
|
+
from .._recorder import FigureRecord, Recorder
|
|
183
|
+
|
|
184
|
+
wrapped_fig = RecordingFigure.__new__(RecordingFigure)
|
|
185
|
+
wrapped_fig._fig = fig
|
|
186
|
+
wrapped_fig._axes = [[ax] for ax in fig.axes]
|
|
187
|
+
wrapped_fig._recorder = Recorder()
|
|
188
|
+
wrapped_fig._recorder._figure_record = FigureRecord(
|
|
189
|
+
figsize=tuple(fig.get_size_inches()),
|
|
190
|
+
dpi=int(fig.dpi),
|
|
191
|
+
)
|
|
192
|
+
fig = wrapped_fig
|
|
193
|
+
|
|
194
|
+
# Update editor state
|
|
195
|
+
editor.fig = fig
|
|
196
|
+
editor.recipe_path = full_path
|
|
197
|
+
editor._hitmap_generated = False
|
|
198
|
+
editor._color_map = {}
|
|
199
|
+
|
|
200
|
+
# Re-init style overrides
|
|
201
|
+
editor._init_style_overrides(None)
|
|
202
|
+
|
|
203
|
+
# Regenerate hitmap
|
|
204
|
+
hitmap_img, editor._color_map = generate_hitmap(editor.fig)
|
|
205
|
+
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
206
|
+
editor._hitmap_generated = True
|
|
207
|
+
|
|
208
|
+
# Render new preview
|
|
209
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
210
|
+
editor.fig,
|
|
211
|
+
editor.get_effective_style(),
|
|
212
|
+
editor.dark_mode,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return jsonify(
|
|
216
|
+
{
|
|
217
|
+
"success": True,
|
|
218
|
+
"image": base64_img,
|
|
219
|
+
"bboxes": bboxes,
|
|
220
|
+
"color_map": editor._color_map,
|
|
221
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
222
|
+
"file": file_path,
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return jsonify({"error": str(e)}), 500
|
|
228
|
+
|
|
229
|
+
@app.route("/api/new", methods=["POST"])
|
|
230
|
+
def new_figure():
|
|
231
|
+
"""Create a new blank figure."""
|
|
232
|
+
from .. import subplots
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Create new blank figure
|
|
236
|
+
fig, ax = subplots()
|
|
237
|
+
ax.set_title("New Figure")
|
|
238
|
+
ax.text(
|
|
239
|
+
0.5,
|
|
240
|
+
0.5,
|
|
241
|
+
"Add plots using fr.edit(fig)",
|
|
242
|
+
ha="center",
|
|
243
|
+
va="center",
|
|
244
|
+
transform=ax.transAxes,
|
|
245
|
+
fontsize=12,
|
|
246
|
+
color="gray",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Update editor state
|
|
250
|
+
editor.fig = fig
|
|
251
|
+
editor.recipe_path = None
|
|
252
|
+
editor._hitmap_generated = False
|
|
253
|
+
editor._color_map = {}
|
|
254
|
+
|
|
255
|
+
# Re-init style overrides
|
|
256
|
+
editor._init_style_overrides(None)
|
|
257
|
+
|
|
258
|
+
# Regenerate hitmap
|
|
259
|
+
hitmap_img, editor._color_map = generate_hitmap(editor.fig)
|
|
260
|
+
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
261
|
+
editor._hitmap_generated = True
|
|
262
|
+
|
|
263
|
+
# Render new preview
|
|
264
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
265
|
+
editor.fig,
|
|
266
|
+
editor.get_effective_style(),
|
|
267
|
+
editor.dark_mode,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return jsonify(
|
|
271
|
+
{
|
|
272
|
+
"success": True,
|
|
273
|
+
"image": base64_img,
|
|
274
|
+
"bboxes": bboxes,
|
|
275
|
+
"color_map": editor._color_map,
|
|
276
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return jsonify({"error": str(e)}), 500
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
__all__ = ["register_core_routes"]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Element-related Flask route handlers for the figure editor.
|
|
5
|
+
Handles calls, download, and shutdown routes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from flask import jsonify, request, send_file
|
|
9
|
+
|
|
10
|
+
from ._helpers import render_with_overrides, to_json_serializable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_element_routes(app, editor):
|
|
14
|
+
"""Register element-related routes with the Flask app."""
|
|
15
|
+
from ._hitmap import generate_hitmap, hitmap_to_base64
|
|
16
|
+
from ._renderer import render_download
|
|
17
|
+
|
|
18
|
+
@app.route("/calls")
|
|
19
|
+
def get_calls():
|
|
20
|
+
"""Get all recorded calls with their signatures."""
|
|
21
|
+
from .._signatures import get_signature
|
|
22
|
+
|
|
23
|
+
calls_data = {}
|
|
24
|
+
if hasattr(editor.fig, "record"):
|
|
25
|
+
for ax_key, ax_record in editor.fig.record.axes.items():
|
|
26
|
+
for call in ax_record.calls:
|
|
27
|
+
call_id = call.id
|
|
28
|
+
func_name = call.function
|
|
29
|
+
sig = get_signature(func_name)
|
|
30
|
+
|
|
31
|
+
calls_data[call_id] = {
|
|
32
|
+
"function": func_name,
|
|
33
|
+
"ax_key": ax_key,
|
|
34
|
+
"args": to_json_serializable(call.args),
|
|
35
|
+
"kwargs": to_json_serializable(call.kwargs),
|
|
36
|
+
"signature": {
|
|
37
|
+
"args": sig.get("args", []),
|
|
38
|
+
"kwargs": {
|
|
39
|
+
k: v
|
|
40
|
+
for k, v in sig.get("kwargs", {}).items()
|
|
41
|
+
if k != "**kwargs"
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return jsonify(calls_data)
|
|
47
|
+
|
|
48
|
+
@app.route("/call/<call_id>")
|
|
49
|
+
def get_call(call_id):
|
|
50
|
+
"""Get recorded call data by call_id."""
|
|
51
|
+
from .._signatures import get_signature
|
|
52
|
+
|
|
53
|
+
if hasattr(editor.fig, "record"):
|
|
54
|
+
for ax_key, ax_record in editor.fig.record.axes.items():
|
|
55
|
+
for call in ax_record.calls:
|
|
56
|
+
if call.id == call_id:
|
|
57
|
+
sig = get_signature(call.function)
|
|
58
|
+
return jsonify(
|
|
59
|
+
{
|
|
60
|
+
"call_id": call_id,
|
|
61
|
+
"function": call.function,
|
|
62
|
+
"ax_key": ax_key,
|
|
63
|
+
"args": call.args,
|
|
64
|
+
"kwargs": call.kwargs,
|
|
65
|
+
"signature": {
|
|
66
|
+
"args": sig.get("args", []),
|
|
67
|
+
"kwargs": {
|
|
68
|
+
k: v
|
|
69
|
+
for k, v in sig.get("kwargs", {}).items()
|
|
70
|
+
if k != "**kwargs"
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return jsonify({"error": f"Call {call_id} not found"}), 404
|
|
77
|
+
|
|
78
|
+
@app.route("/update_call", methods=["POST"])
|
|
79
|
+
def update_call():
|
|
80
|
+
"""Update a call's kwargs and re-render."""
|
|
81
|
+
from .._reproducer import reproduce_from_record
|
|
82
|
+
|
|
83
|
+
data = request.get_json() or {}
|
|
84
|
+
call_id = data.get("call_id")
|
|
85
|
+
param = data.get("param")
|
|
86
|
+
value = data.get("value")
|
|
87
|
+
|
|
88
|
+
if not call_id or not param:
|
|
89
|
+
return jsonify({"error": "Missing call_id or param"}), 400
|
|
90
|
+
|
|
91
|
+
updated = False
|
|
92
|
+
if hasattr(editor.fig, "record"):
|
|
93
|
+
for ax_key, ax_record in editor.fig.record.axes.items():
|
|
94
|
+
for call in ax_record.calls:
|
|
95
|
+
if call.id == call_id:
|
|
96
|
+
# Debug: log the update
|
|
97
|
+
print(f"[DEBUG] update_call: {call_id}.{param} = {value}")
|
|
98
|
+
print(
|
|
99
|
+
f"[DEBUG] Before: call.kwargs[{param}] = {call.kwargs.get(param)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
editor.style_overrides.set_call_override(call_id, param, value)
|
|
103
|
+
|
|
104
|
+
if value is None or value == "" or value == "null":
|
|
105
|
+
call.kwargs.pop(param, None)
|
|
106
|
+
else:
|
|
107
|
+
call.kwargs[param] = value
|
|
108
|
+
|
|
109
|
+
print(
|
|
110
|
+
f"[DEBUG] After: call.kwargs[{param}] = {call.kwargs.get(param)}"
|
|
111
|
+
)
|
|
112
|
+
updated = True
|
|
113
|
+
break
|
|
114
|
+
if updated:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if not updated:
|
|
118
|
+
return jsonify({"error": f"Call {call_id} not found"}), 404
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
new_fig, _ = reproduce_from_record(editor.fig.record)
|
|
122
|
+
|
|
123
|
+
effective_style = editor.get_effective_style()
|
|
124
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
125
|
+
new_fig,
|
|
126
|
+
effective_style if effective_style else None,
|
|
127
|
+
editor.dark_mode,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
editor.fig = new_fig
|
|
131
|
+
|
|
132
|
+
hitmap_img, color_map = generate_hitmap(new_fig, img_size[0], img_size[1])
|
|
133
|
+
editor._color_map = color_map
|
|
134
|
+
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
135
|
+
editor._hitmap_generated = True
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
import traceback
|
|
139
|
+
|
|
140
|
+
traceback.print_exc()
|
|
141
|
+
return jsonify({"error": f"Re-render failed: {str(e)}"}), 500
|
|
142
|
+
|
|
143
|
+
return jsonify(
|
|
144
|
+
{
|
|
145
|
+
"success": True,
|
|
146
|
+
"image": base64_img,
|
|
147
|
+
"bboxes": bboxes,
|
|
148
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
149
|
+
"call_id": call_id,
|
|
150
|
+
"param": param,
|
|
151
|
+
"value": value,
|
|
152
|
+
"has_call_overrides": editor.style_overrides.has_call_overrides(),
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@app.route("/download/csv")
|
|
157
|
+
def download_csv():
|
|
158
|
+
"""Download plotted data as CSV."""
|
|
159
|
+
import csv
|
|
160
|
+
import io
|
|
161
|
+
|
|
162
|
+
# Get the recorder from the figure
|
|
163
|
+
fig = editor.fig
|
|
164
|
+
if not hasattr(fig, "_recorder") or fig._recorder is None:
|
|
165
|
+
return jsonify({"error": "No recorded data available"}), 400
|
|
166
|
+
|
|
167
|
+
record = fig._recorder._figure_record
|
|
168
|
+
|
|
169
|
+
# Collect all plot data
|
|
170
|
+
all_data = {}
|
|
171
|
+
decoration_funcs = {
|
|
172
|
+
"set_xlabel",
|
|
173
|
+
"set_ylabel",
|
|
174
|
+
"set_title",
|
|
175
|
+
"set_xlim",
|
|
176
|
+
"set_ylim",
|
|
177
|
+
"legend",
|
|
178
|
+
"grid",
|
|
179
|
+
"axhline",
|
|
180
|
+
"axvline",
|
|
181
|
+
"text",
|
|
182
|
+
"annotate",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for ax_key, ax_record in record.axes.items():
|
|
186
|
+
for call in ax_record.calls:
|
|
187
|
+
if call.function in decoration_funcs:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
call_id = call.id or f"{ax_key}_{call.function}_{id(call)}"
|
|
191
|
+
call_data = {}
|
|
192
|
+
|
|
193
|
+
def extract_data(val):
|
|
194
|
+
"""Extract raw data from value, handling dict wrappers."""
|
|
195
|
+
if isinstance(val, dict) and "data" in val:
|
|
196
|
+
return val["data"]
|
|
197
|
+
if isinstance(val, list):
|
|
198
|
+
return val
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
# Convert args to serializable format
|
|
202
|
+
args = to_json_serializable(call.args)
|
|
203
|
+
|
|
204
|
+
# Extract x, y data from args
|
|
205
|
+
if args:
|
|
206
|
+
if len(args) >= 2:
|
|
207
|
+
x_data = extract_data(args[0])
|
|
208
|
+
y_data = extract_data(args[1])
|
|
209
|
+
if x_data:
|
|
210
|
+
call_data["x"] = x_data
|
|
211
|
+
if y_data:
|
|
212
|
+
call_data["y"] = y_data
|
|
213
|
+
elif len(args) == 1:
|
|
214
|
+
data = extract_data(args[0])
|
|
215
|
+
if data:
|
|
216
|
+
call_data["y"] = data
|
|
217
|
+
call_data["x"] = list(range(len(call_data["y"])))
|
|
218
|
+
|
|
219
|
+
# Extract from kwargs
|
|
220
|
+
kwargs = to_json_serializable(call.kwargs)
|
|
221
|
+
for key in ["x", "y", "height", "width", "c", "s"]:
|
|
222
|
+
if key in kwargs:
|
|
223
|
+
val = extract_data(kwargs[key])
|
|
224
|
+
if val:
|
|
225
|
+
call_data[key] = val
|
|
226
|
+
|
|
227
|
+
if call_data:
|
|
228
|
+
all_data[call_id] = call_data
|
|
229
|
+
|
|
230
|
+
if not all_data:
|
|
231
|
+
return jsonify({"error": "No plot data found"}), 400
|
|
232
|
+
|
|
233
|
+
# Create CSV content
|
|
234
|
+
output = io.StringIO()
|
|
235
|
+
|
|
236
|
+
# Find max length for padding
|
|
237
|
+
max_len = max(max(len(v) for v in data.values()) for data in all_data.values())
|
|
238
|
+
|
|
239
|
+
# Write header
|
|
240
|
+
headers = []
|
|
241
|
+
for call_id, data in all_data.items():
|
|
242
|
+
for key in sorted(data.keys()):
|
|
243
|
+
headers.append(f"{call_id}_{key}")
|
|
244
|
+
|
|
245
|
+
writer = csv.writer(output)
|
|
246
|
+
writer.writerow(headers)
|
|
247
|
+
|
|
248
|
+
# Write data rows
|
|
249
|
+
for i in range(max_len):
|
|
250
|
+
row = []
|
|
251
|
+
for call_id, data in all_data.items():
|
|
252
|
+
for key in sorted(data.keys()):
|
|
253
|
+
values = data[key]
|
|
254
|
+
if i < len(values):
|
|
255
|
+
row.append(values[i])
|
|
256
|
+
else:
|
|
257
|
+
row.append("")
|
|
258
|
+
writer.writerow(row)
|
|
259
|
+
|
|
260
|
+
# Return CSV file
|
|
261
|
+
csv_content = output.getvalue().encode("utf-8")
|
|
262
|
+
filename = "figure_data.csv"
|
|
263
|
+
if editor.recipe_path:
|
|
264
|
+
filename = f"{editor.recipe_path.stem}_data.csv"
|
|
265
|
+
|
|
266
|
+
return send_file(
|
|
267
|
+
io.BytesIO(csv_content),
|
|
268
|
+
mimetype="text/csv",
|
|
269
|
+
as_attachment=True,
|
|
270
|
+
download_name=filename,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
@app.route("/download/<fmt>")
|
|
274
|
+
def download(fmt: str):
|
|
275
|
+
"""Download figure in specified format."""
|
|
276
|
+
import io
|
|
277
|
+
|
|
278
|
+
fmt = fmt.lower()
|
|
279
|
+
if fmt not in ("png", "svg", "pdf"):
|
|
280
|
+
return jsonify({"error": f"Unsupported format: {fmt}"}), 400
|
|
281
|
+
|
|
282
|
+
effective_style = editor.get_effective_style()
|
|
283
|
+
content = render_download(
|
|
284
|
+
editor.fig,
|
|
285
|
+
fmt=fmt,
|
|
286
|
+
dpi=300,
|
|
287
|
+
overrides=effective_style if effective_style else None,
|
|
288
|
+
dark_mode=False,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
mimetype = {
|
|
292
|
+
"png": "image/png",
|
|
293
|
+
"svg": "image/svg+xml",
|
|
294
|
+
"pdf": "application/pdf",
|
|
295
|
+
}[fmt]
|
|
296
|
+
|
|
297
|
+
filename = f"figure.{fmt}"
|
|
298
|
+
if editor.recipe_path:
|
|
299
|
+
filename = f"{editor.recipe_path.stem}.{fmt}"
|
|
300
|
+
|
|
301
|
+
return send_file(
|
|
302
|
+
io.BytesIO(content),
|
|
303
|
+
mimetype=mimetype,
|
|
304
|
+
as_attachment=True,
|
|
305
|
+
download_name=filename,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@app.route("/shutdown", methods=["POST"])
|
|
309
|
+
def shutdown():
|
|
310
|
+
"""Shutdown the server."""
|
|
311
|
+
func = request.environ.get("werkzeug.server.shutdown")
|
|
312
|
+
if func:
|
|
313
|
+
func()
|
|
314
|
+
return jsonify({"success": True})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
__all__ = ["register_element_routes"]
|