figrecipe 0.6.0__py3-none-any.whl → 0.9.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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -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/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- 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/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- 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 +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -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 +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- 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 +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Axis-related Flask route handlers for the figure editor.
|
|
5
|
+
Handles labels, axis type, legend position, and ticks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import matplotlib
|
|
9
|
+
from flask import jsonify, request
|
|
10
|
+
|
|
11
|
+
from ._helpers import render_with_overrides
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_axis_routes(app, editor):
|
|
15
|
+
"""Register axis-related routes with the Flask app."""
|
|
16
|
+
|
|
17
|
+
@app.route("/update_label", methods=["POST"])
|
|
18
|
+
def update_label():
|
|
19
|
+
"""Update axis labels (title, xlabel, ylabel, suptitle)."""
|
|
20
|
+
data = request.get_json() or {}
|
|
21
|
+
label_type = data.get("label_type")
|
|
22
|
+
text = data.get("text", "")
|
|
23
|
+
ax_index = data.get("ax_index", 0)
|
|
24
|
+
|
|
25
|
+
if not label_type:
|
|
26
|
+
return jsonify({"error": "Missing label_type"}), 400
|
|
27
|
+
|
|
28
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
29
|
+
axes = mpl_fig.get_axes()
|
|
30
|
+
|
|
31
|
+
if not axes:
|
|
32
|
+
return jsonify({"error": "No axes found"}), 400
|
|
33
|
+
|
|
34
|
+
ax = axes[min(ax_index, len(axes) - 1)]
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
if label_type == "title":
|
|
38
|
+
ax.set_title(text)
|
|
39
|
+
elif label_type == "xlabel":
|
|
40
|
+
ax.set_xlabel(text)
|
|
41
|
+
elif label_type == "ylabel":
|
|
42
|
+
ax.set_ylabel(text)
|
|
43
|
+
elif label_type == "suptitle":
|
|
44
|
+
if text:
|
|
45
|
+
mpl_fig.suptitle(text)
|
|
46
|
+
else:
|
|
47
|
+
if mpl_fig._suptitle:
|
|
48
|
+
mpl_fig._suptitle.set_text("")
|
|
49
|
+
else:
|
|
50
|
+
return jsonify({"error": f"Unknown label_type: {label_type}"}), 400
|
|
51
|
+
|
|
52
|
+
editor.style_overrides.manual_overrides[f"label_{label_type}"] = text
|
|
53
|
+
|
|
54
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
55
|
+
editor.fig,
|
|
56
|
+
editor.get_effective_style(),
|
|
57
|
+
editor.dark_mode,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return jsonify(
|
|
61
|
+
{
|
|
62
|
+
"success": True,
|
|
63
|
+
"image": base64_img,
|
|
64
|
+
"bboxes": bboxes,
|
|
65
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
import traceback
|
|
71
|
+
|
|
72
|
+
traceback.print_exc()
|
|
73
|
+
return jsonify({"error": f"Update failed: {str(e)}"}), 500
|
|
74
|
+
|
|
75
|
+
@app.route("/get_labels")
|
|
76
|
+
def get_labels():
|
|
77
|
+
"""Get current axis labels (title, xlabel, ylabel, suptitle)."""
|
|
78
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
79
|
+
axes = mpl_fig.get_axes()
|
|
80
|
+
|
|
81
|
+
labels = {"title": "", "xlabel": "", "ylabel": "", "suptitle": ""}
|
|
82
|
+
|
|
83
|
+
if axes:
|
|
84
|
+
ax = axes[0]
|
|
85
|
+
labels["title"] = ax.get_title()
|
|
86
|
+
labels["xlabel"] = ax.get_xlabel()
|
|
87
|
+
labels["ylabel"] = ax.get_ylabel()
|
|
88
|
+
|
|
89
|
+
if mpl_fig._suptitle:
|
|
90
|
+
labels["suptitle"] = mpl_fig._suptitle.get_text()
|
|
91
|
+
|
|
92
|
+
return jsonify(labels)
|
|
93
|
+
|
|
94
|
+
@app.route("/update_axis_type", methods=["POST"])
|
|
95
|
+
def update_axis_type():
|
|
96
|
+
"""Update axis type (numerical vs categorical)."""
|
|
97
|
+
data = request.get_json() or {}
|
|
98
|
+
axis = data.get("axis")
|
|
99
|
+
axis_type = data.get("type")
|
|
100
|
+
labels = data.get("labels", [])
|
|
101
|
+
ax_index = data.get("ax_index", 0)
|
|
102
|
+
|
|
103
|
+
if not axis or not axis_type:
|
|
104
|
+
return jsonify({"error": "Missing axis or type"}), 400
|
|
105
|
+
|
|
106
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
107
|
+
axes_list = mpl_fig.get_axes()
|
|
108
|
+
|
|
109
|
+
if not axes_list:
|
|
110
|
+
return jsonify({"error": "No axes found"}), 400
|
|
111
|
+
|
|
112
|
+
ax = axes_list[min(ax_index, len(axes_list) - 1)]
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
if axis == "x":
|
|
116
|
+
if axis_type == "categorical" and labels:
|
|
117
|
+
positions = list(range(len(labels)))
|
|
118
|
+
ax.set_xticks(positions)
|
|
119
|
+
ax.set_xticklabels(labels)
|
|
120
|
+
else:
|
|
121
|
+
ax.xaxis.set_major_locator(matplotlib.ticker.AutoLocator())
|
|
122
|
+
ax.xaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter())
|
|
123
|
+
elif axis == "y":
|
|
124
|
+
if axis_type == "categorical" and labels:
|
|
125
|
+
positions = list(range(len(labels)))
|
|
126
|
+
ax.set_yticks(positions)
|
|
127
|
+
ax.set_yticklabels(labels)
|
|
128
|
+
else:
|
|
129
|
+
ax.yaxis.set_major_locator(matplotlib.ticker.AutoLocator())
|
|
130
|
+
ax.yaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter())
|
|
131
|
+
|
|
132
|
+
key = f"axis_{axis}_type"
|
|
133
|
+
editor.style_overrides.manual_overrides[key] = axis_type
|
|
134
|
+
if labels:
|
|
135
|
+
editor.style_overrides.manual_overrides[f"axis_{axis}_labels"] = labels
|
|
136
|
+
|
|
137
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
138
|
+
editor.fig,
|
|
139
|
+
editor.get_effective_style(),
|
|
140
|
+
editor.dark_mode,
|
|
141
|
+
)
|
|
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
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
import traceback
|
|
154
|
+
|
|
155
|
+
traceback.print_exc()
|
|
156
|
+
return jsonify({"error": f"Update failed: {str(e)}"}), 500
|
|
157
|
+
|
|
158
|
+
@app.route("/get_axis_info")
|
|
159
|
+
def get_axis_info():
|
|
160
|
+
"""Get current axis type info (numerical vs categorical)."""
|
|
161
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
162
|
+
axes_list = mpl_fig.get_axes()
|
|
163
|
+
|
|
164
|
+
info = {
|
|
165
|
+
"x_type": "numerical",
|
|
166
|
+
"y_type": "numerical",
|
|
167
|
+
"x_labels": [],
|
|
168
|
+
"y_labels": [],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if axes_list:
|
|
172
|
+
ax = axes_list[0]
|
|
173
|
+
|
|
174
|
+
x_ticklabels = [t.get_text() for t in ax.get_xticklabels()]
|
|
175
|
+
if x_ticklabels and any(t for t in x_ticklabels):
|
|
176
|
+
info["x_type"] = "categorical"
|
|
177
|
+
info["x_labels"] = x_ticklabels
|
|
178
|
+
|
|
179
|
+
y_ticklabels = [t.get_text() for t in ax.get_yticklabels()]
|
|
180
|
+
if y_ticklabels and any(t for t in y_ticklabels):
|
|
181
|
+
info["y_type"] = "categorical"
|
|
182
|
+
info["y_labels"] = y_ticklabels
|
|
183
|
+
|
|
184
|
+
return jsonify(info)
|
|
185
|
+
|
|
186
|
+
@app.route("/update_legend_position", methods=["POST"])
|
|
187
|
+
def update_legend_position():
|
|
188
|
+
"""Update legend position, visibility, or custom xy coordinates."""
|
|
189
|
+
data = request.get_json() or {}
|
|
190
|
+
loc = data.get("loc")
|
|
191
|
+
x = data.get("x")
|
|
192
|
+
y = data.get("y")
|
|
193
|
+
visible = data.get("visible")
|
|
194
|
+
ax_index = data.get("ax_index", 0)
|
|
195
|
+
|
|
196
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
197
|
+
axes_list = mpl_fig.get_axes()
|
|
198
|
+
|
|
199
|
+
if not axes_list:
|
|
200
|
+
return jsonify({"error": "No axes found"}), 400
|
|
201
|
+
|
|
202
|
+
ax = axes_list[min(ax_index, len(axes_list) - 1)]
|
|
203
|
+
legend = ax.get_legend()
|
|
204
|
+
|
|
205
|
+
if legend is None:
|
|
206
|
+
return jsonify({"error": "No legend found on this axes"}), 400
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
if visible is not None:
|
|
210
|
+
legend.set_visible(visible)
|
|
211
|
+
editor.style_overrides.manual_overrides["legend_visible"] = visible
|
|
212
|
+
|
|
213
|
+
if loc is not None:
|
|
214
|
+
if loc == "custom" and x is not None and y is not None:
|
|
215
|
+
legend.set_bbox_to_anchor((float(x), float(y)))
|
|
216
|
+
legend._loc = 2
|
|
217
|
+
else:
|
|
218
|
+
loc_map = {
|
|
219
|
+
"best": 0,
|
|
220
|
+
"upper right": 1,
|
|
221
|
+
"upper left": 2,
|
|
222
|
+
"lower left": 3,
|
|
223
|
+
"lower right": 4,
|
|
224
|
+
"right": 5,
|
|
225
|
+
"center left": 6,
|
|
226
|
+
"center right": 7,
|
|
227
|
+
"lower center": 8,
|
|
228
|
+
"upper center": 9,
|
|
229
|
+
"center": 10,
|
|
230
|
+
}
|
|
231
|
+
loc_code = loc_map.get(loc, 0)
|
|
232
|
+
legend._loc = loc_code
|
|
233
|
+
legend.set_bbox_to_anchor(None)
|
|
234
|
+
|
|
235
|
+
editor.style_overrides.manual_overrides["legend_loc"] = loc
|
|
236
|
+
if loc == "custom":
|
|
237
|
+
editor.style_overrides.manual_overrides["legend_x"] = x
|
|
238
|
+
editor.style_overrides.manual_overrides["legend_y"] = y
|
|
239
|
+
|
|
240
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
241
|
+
editor.fig,
|
|
242
|
+
editor.get_effective_style(),
|
|
243
|
+
editor.dark_mode,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return jsonify(
|
|
247
|
+
{
|
|
248
|
+
"success": True,
|
|
249
|
+
"image": base64_img,
|
|
250
|
+
"bboxes": bboxes,
|
|
251
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
import traceback
|
|
257
|
+
|
|
258
|
+
traceback.print_exc()
|
|
259
|
+
return jsonify({"error": f"Update failed: {str(e)}"}), 500
|
|
260
|
+
|
|
261
|
+
@app.route("/get_legend_info")
|
|
262
|
+
def get_legend_info():
|
|
263
|
+
"""Get current legend position info."""
|
|
264
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
265
|
+
axes_list = mpl_fig.get_axes()
|
|
266
|
+
|
|
267
|
+
info = {
|
|
268
|
+
"has_legend": False,
|
|
269
|
+
"visible": True,
|
|
270
|
+
"loc": "best",
|
|
271
|
+
"x": None,
|
|
272
|
+
"y": None,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if axes_list:
|
|
276
|
+
ax = axes_list[0]
|
|
277
|
+
legend = ax.get_legend()
|
|
278
|
+
|
|
279
|
+
if legend is not None:
|
|
280
|
+
info["has_legend"] = True
|
|
281
|
+
info["visible"] = legend.get_visible()
|
|
282
|
+
|
|
283
|
+
loc_code = legend._loc
|
|
284
|
+
loc_names = {
|
|
285
|
+
0: "best",
|
|
286
|
+
1: "upper right",
|
|
287
|
+
2: "upper left",
|
|
288
|
+
3: "lower left",
|
|
289
|
+
4: "lower right",
|
|
290
|
+
5: "right",
|
|
291
|
+
6: "center left",
|
|
292
|
+
7: "center right",
|
|
293
|
+
8: "lower center",
|
|
294
|
+
9: "upper center",
|
|
295
|
+
10: "center",
|
|
296
|
+
}
|
|
297
|
+
info["loc"] = loc_names.get(loc_code, "best")
|
|
298
|
+
|
|
299
|
+
bbox = legend.get_bbox_to_anchor()
|
|
300
|
+
if bbox is not None:
|
|
301
|
+
try:
|
|
302
|
+
bounds = bbox.bounds
|
|
303
|
+
if bounds[0] != 0 or bounds[1] != 0:
|
|
304
|
+
info["loc"] = "custom"
|
|
305
|
+
info["x"] = bounds[0]
|
|
306
|
+
info["y"] = bounds[1]
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
return jsonify(info)
|
|
311
|
+
|
|
312
|
+
@app.route("/get_axes_positions")
|
|
313
|
+
def get_axes_positions():
|
|
314
|
+
"""Get positions for all axes in mm with upper-left origin.
|
|
315
|
+
|
|
316
|
+
Returns positions as {left_mm, top_mm, width_mm, height_mm}
|
|
317
|
+
where origin is upper-left corner and positive is right/downward.
|
|
318
|
+
"""
|
|
319
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
320
|
+
axes = mpl_fig.get_axes()
|
|
321
|
+
|
|
322
|
+
# Get figure size in mm (inches * 25.4)
|
|
323
|
+
fig_size_inches = mpl_fig.get_size_inches()
|
|
324
|
+
fig_width_mm = fig_size_inches[0] * 25.4
|
|
325
|
+
fig_height_mm = fig_size_inches[1] * 25.4
|
|
326
|
+
|
|
327
|
+
positions = {}
|
|
328
|
+
for i, ax in enumerate(axes):
|
|
329
|
+
bbox = ax.get_position()
|
|
330
|
+
# Convert from matplotlib coords (0-1, bottom-left origin)
|
|
331
|
+
# to mm with upper-left origin
|
|
332
|
+
left_mm = bbox.x0 * fig_width_mm
|
|
333
|
+
width_mm = bbox.width * fig_width_mm
|
|
334
|
+
height_mm = bbox.height * fig_height_mm
|
|
335
|
+
# Y: convert from bottom-up to top-down
|
|
336
|
+
top_mm = (1 - bbox.y1) * fig_height_mm
|
|
337
|
+
|
|
338
|
+
positions[f"ax_{i}"] = {
|
|
339
|
+
"index": i,
|
|
340
|
+
"left": round(left_mm, 2),
|
|
341
|
+
"top": round(top_mm, 2),
|
|
342
|
+
"width": round(width_mm, 2),
|
|
343
|
+
"height": round(height_mm, 2),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Include figure size for reference
|
|
347
|
+
positions["_figsize"] = {
|
|
348
|
+
"width_mm": round(fig_width_mm, 2),
|
|
349
|
+
"height_mm": round(fig_height_mm, 2),
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return jsonify(positions)
|
|
353
|
+
|
|
354
|
+
@app.route("/update_axes_position", methods=["POST"])
|
|
355
|
+
def update_axes_position():
|
|
356
|
+
"""Update position of a specific axes.
|
|
357
|
+
|
|
358
|
+
Expects JSON: {ax_index: int, left, top, width, height}
|
|
359
|
+
Values are in mm with upper-left origin.
|
|
360
|
+
"""
|
|
361
|
+
from ._hitmap import generate_hitmap, hitmap_to_base64
|
|
362
|
+
|
|
363
|
+
data = request.get_json() or {}
|
|
364
|
+
ax_index = data.get("ax_index", 0)
|
|
365
|
+
left_mm = data.get("left")
|
|
366
|
+
top_mm = data.get("top")
|
|
367
|
+
width_mm = data.get("width")
|
|
368
|
+
height_mm = data.get("height")
|
|
369
|
+
|
|
370
|
+
if any(v is None for v in [left_mm, top_mm, width_mm, height_mm]):
|
|
371
|
+
return jsonify({"error": "Missing position values"}), 400
|
|
372
|
+
|
|
373
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
374
|
+
|
|
375
|
+
# Get figure size in mm for conversion
|
|
376
|
+
fig_size_inches = mpl_fig.get_size_inches()
|
|
377
|
+
fig_width_mm = fig_size_inches[0] * 25.4
|
|
378
|
+
fig_height_mm = fig_size_inches[1] * 25.4
|
|
379
|
+
|
|
380
|
+
# Validate range (must be within figure bounds)
|
|
381
|
+
if left_mm < 0 or left_mm + width_mm > fig_width_mm:
|
|
382
|
+
return jsonify(
|
|
383
|
+
{"error": f"Horizontal position out of bounds (0-{fig_width_mm:.1f}mm)"}
|
|
384
|
+
), 400
|
|
385
|
+
if top_mm < 0 or top_mm + height_mm > fig_height_mm:
|
|
386
|
+
return jsonify(
|
|
387
|
+
{"error": f"Vertical position out of bounds (0-{fig_height_mm:.1f}mm)"}
|
|
388
|
+
), 400
|
|
389
|
+
|
|
390
|
+
# Convert from mm (upper-left origin) to matplotlib coords (0-1, bottom-left)
|
|
391
|
+
left = left_mm / fig_width_mm
|
|
392
|
+
width = width_mm / fig_width_mm
|
|
393
|
+
height = height_mm / fig_height_mm
|
|
394
|
+
# Y: convert from top-down to bottom-up
|
|
395
|
+
bottom = 1 - (top_mm + height_mm) / fig_height_mm
|
|
396
|
+
|
|
397
|
+
axes = mpl_fig.get_axes()
|
|
398
|
+
|
|
399
|
+
if ax_index >= len(axes):
|
|
400
|
+
return jsonify({"error": f"Invalid ax_index: {ax_index}"}), 400
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
ax = axes[ax_index]
|
|
404
|
+
|
|
405
|
+
# CRITICAL: Get current position BEFORE changing it
|
|
406
|
+
# We need this to find the correct ax_record to update
|
|
407
|
+
current_pos = ax.get_position()
|
|
408
|
+
|
|
409
|
+
# Now set the new position
|
|
410
|
+
ax.set_position([left, bottom, width, height])
|
|
411
|
+
|
|
412
|
+
# Store position override in manual_overrides (mm values with upper-left origin)
|
|
413
|
+
# This allows restore functionality to revert position changes
|
|
414
|
+
editor.style_overrides.manual_overrides[f"axes_position_{ax_index}"] = {
|
|
415
|
+
"left_mm": left_mm,
|
|
416
|
+
"top_mm": top_mm,
|
|
417
|
+
"width_mm": width_mm,
|
|
418
|
+
"height_mm": height_mm,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# Update record if available - find ax_record by matching CURRENT position
|
|
422
|
+
if hasattr(editor.fig, "record"):
|
|
423
|
+
matched_ax_key = None
|
|
424
|
+
ax_keys = sorted(editor.fig.record.axes.keys())
|
|
425
|
+
|
|
426
|
+
# First, try to match by position_override (for previously dragged panels)
|
|
427
|
+
for ax_key in ax_keys:
|
|
428
|
+
ax_record = editor.fig.record.axes[ax_key]
|
|
429
|
+
if (
|
|
430
|
+
hasattr(ax_record, "position_override")
|
|
431
|
+
and ax_record.position_override
|
|
432
|
+
):
|
|
433
|
+
rec_pos = ax_record.position_override
|
|
434
|
+
if len(rec_pos) >= 4:
|
|
435
|
+
if (
|
|
436
|
+
abs(rec_pos[0] - current_pos.x0) < 0.01
|
|
437
|
+
and abs(rec_pos[1] - current_pos.y0) < 0.01
|
|
438
|
+
and abs(rec_pos[2] - current_pos.width) < 0.01
|
|
439
|
+
and abs(rec_pos[3] - current_pos.height) < 0.01
|
|
440
|
+
):
|
|
441
|
+
matched_ax_key = ax_key
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
# If no position_override match, fall back to index-based matching
|
|
445
|
+
if matched_ax_key is None and ax_index < len(ax_keys):
|
|
446
|
+
matched_ax_key = ax_keys[ax_index]
|
|
447
|
+
|
|
448
|
+
# Update the matched ax_record with new position
|
|
449
|
+
if matched_ax_key:
|
|
450
|
+
ax_record = editor.fig.record.axes[matched_ax_key]
|
|
451
|
+
ax_record.position_override = [left, bottom, width, height]
|
|
452
|
+
|
|
453
|
+
# Re-render
|
|
454
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
455
|
+
editor.fig,
|
|
456
|
+
editor.get_effective_style(),
|
|
457
|
+
editor.dark_mode,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Regenerate hitmap - use editor.fig to preserve record access
|
|
461
|
+
hitmap_img, color_map = generate_hitmap(editor.fig, dpi=150)
|
|
462
|
+
editor._color_map = color_map
|
|
463
|
+
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
464
|
+
editor._hitmap_generated = True
|
|
465
|
+
|
|
466
|
+
return jsonify(
|
|
467
|
+
{
|
|
468
|
+
"success": True,
|
|
469
|
+
"image": base64_img,
|
|
470
|
+
"bboxes": bboxes,
|
|
471
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
except Exception as e:
|
|
476
|
+
import traceback
|
|
477
|
+
|
|
478
|
+
traceback.print_exc()
|
|
479
|
+
return jsonify({"error": f"Update failed: {str(e)}"}), 500
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
__all__ = ["register_axis_routes"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Caption-related Flask route handlers for the figure editor.
|
|
5
|
+
Handles scientific figure captions and panel captions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from flask import jsonify, request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_caption_routes(app, editor):
|
|
12
|
+
"""Register caption-related routes with the Flask app."""
|
|
13
|
+
|
|
14
|
+
@app.route("/get_captions")
|
|
15
|
+
def get_captions():
|
|
16
|
+
"""Get current captions (figure and panel)."""
|
|
17
|
+
captions = {
|
|
18
|
+
"figure_number": 1,
|
|
19
|
+
"figure_caption": "",
|
|
20
|
+
"panel_captions": [], # List of panel captions
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Try to get caption from RecordingFigure's recorder
|
|
24
|
+
fig = editor.fig
|
|
25
|
+
if hasattr(fig, "caption") and fig.caption:
|
|
26
|
+
captions["figure_caption"] = fig.caption
|
|
27
|
+
|
|
28
|
+
# Get panel captions from axes
|
|
29
|
+
if hasattr(fig, "flat"):
|
|
30
|
+
for ax in fig.flat:
|
|
31
|
+
if hasattr(ax, "caption") and ax.caption:
|
|
32
|
+
captions["panel_captions"].append(ax.caption)
|
|
33
|
+
else:
|
|
34
|
+
captions["panel_captions"].append("")
|
|
35
|
+
|
|
36
|
+
# Check if we have recipe metadata (fallback)
|
|
37
|
+
if not captions["figure_caption"] and hasattr(fig, "_recipe_metadata"):
|
|
38
|
+
metadata = fig._recipe_metadata
|
|
39
|
+
if hasattr(metadata, "caption") and metadata.caption:
|
|
40
|
+
captions["figure_caption"] = metadata.caption
|
|
41
|
+
if hasattr(metadata, "figure_number") and metadata.figure_number:
|
|
42
|
+
captions["figure_number"] = metadata.figure_number
|
|
43
|
+
|
|
44
|
+
# Check editor's manual overrides for captions (highest priority)
|
|
45
|
+
if hasattr(editor, "style_overrides"):
|
|
46
|
+
manual = getattr(editor.style_overrides, "manual_overrides", {})
|
|
47
|
+
if "caption_figure_number" in manual:
|
|
48
|
+
captions["figure_number"] = manual["caption_figure_number"]
|
|
49
|
+
if "caption_figure_text" in manual:
|
|
50
|
+
captions["figure_caption"] = manual["caption_figure_text"]
|
|
51
|
+
# Load individual panel overrides
|
|
52
|
+
for i in range(len(captions["panel_captions"])):
|
|
53
|
+
key = f"caption_panel_{i}_text"
|
|
54
|
+
if key in manual:
|
|
55
|
+
captions["panel_captions"][i] = manual[key]
|
|
56
|
+
|
|
57
|
+
return jsonify(captions)
|
|
58
|
+
|
|
59
|
+
@app.route("/update_caption", methods=["POST"])
|
|
60
|
+
def update_caption():
|
|
61
|
+
"""Update figure or panel caption."""
|
|
62
|
+
data = request.get_json() or {}
|
|
63
|
+
caption_type = data.get("type") # 'figure' or 'panel'
|
|
64
|
+
|
|
65
|
+
if not caption_type:
|
|
66
|
+
return jsonify({"error": "Missing caption type"}), 400
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if caption_type == "figure":
|
|
70
|
+
figure_number = data.get("figure_number", 1)
|
|
71
|
+
text = data.get("text", "")
|
|
72
|
+
|
|
73
|
+
# Store in manual overrides
|
|
74
|
+
editor.style_overrides.manual_overrides["caption_figure_number"] = (
|
|
75
|
+
figure_number
|
|
76
|
+
)
|
|
77
|
+
editor.style_overrides.manual_overrides["caption_figure_text"] = text
|
|
78
|
+
|
|
79
|
+
# Also store in recipe metadata if available
|
|
80
|
+
if hasattr(editor.fig, "_recipe_metadata"):
|
|
81
|
+
editor.fig._recipe_metadata.caption = text
|
|
82
|
+
editor.fig._recipe_metadata.figure_number = figure_number
|
|
83
|
+
|
|
84
|
+
return jsonify(
|
|
85
|
+
{
|
|
86
|
+
"success": True,
|
|
87
|
+
"caption_type": "figure",
|
|
88
|
+
"figure_number": figure_number,
|
|
89
|
+
"text": text,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
elif caption_type == "panel":
|
|
94
|
+
panel_index = data.get("panel_index", 0)
|
|
95
|
+
text = data.get("text", "")
|
|
96
|
+
|
|
97
|
+
# Store in manual overrides
|
|
98
|
+
key = f"caption_panel_{panel_index}_text"
|
|
99
|
+
editor.style_overrides.manual_overrides[key] = text
|
|
100
|
+
# Also store general panel caption for current selection
|
|
101
|
+
editor.style_overrides.manual_overrides["caption_panel_text"] = text
|
|
102
|
+
|
|
103
|
+
# Store in axes metadata if available
|
|
104
|
+
if hasattr(editor.fig, "_axes_metadata"):
|
|
105
|
+
axes_meta = editor.fig._axes_metadata
|
|
106
|
+
if panel_index < len(axes_meta):
|
|
107
|
+
axes_meta[panel_index].caption = text
|
|
108
|
+
|
|
109
|
+
return jsonify(
|
|
110
|
+
{
|
|
111
|
+
"success": True,
|
|
112
|
+
"caption_type": "panel",
|
|
113
|
+
"panel_index": panel_index,
|
|
114
|
+
"text": text,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
return jsonify({"error": f"Unknown caption type: {caption_type}"}), 400
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
import traceback
|
|
123
|
+
|
|
124
|
+
traceback.print_exc()
|
|
125
|
+
return jsonify({"error": f"Caption update failed: {str(e)}"}), 500
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = ["register_caption_routes"]
|
|
129
|
+
|
|
130
|
+
# EOF
|