figrecipe 0.6.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 +106 -973
- 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 +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -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 +57 -9
- 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 +68 -1039
- 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/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- 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 +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- 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 +6 -0
- figrecipe/_recorder.py +35 -106
- 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/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- 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 +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -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 +32 -478
- 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 +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -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/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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
ax.set_position([left, bottom, width, height])
|
|
405
|
+
|
|
406
|
+
# Store position override in manual_overrides (mm values with upper-left origin)
|
|
407
|
+
# This allows restore functionality to revert position changes
|
|
408
|
+
editor.style_overrides.manual_overrides[f"axes_position_{ax_index}"] = {
|
|
409
|
+
"left_mm": left_mm,
|
|
410
|
+
"top_mm": top_mm,
|
|
411
|
+
"width_mm": width_mm,
|
|
412
|
+
"height_mm": height_mm,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Update record if available
|
|
416
|
+
if hasattr(editor.fig, "record"):
|
|
417
|
+
# Find the axes record key
|
|
418
|
+
ax_keys = sorted(editor.fig.record.axes.keys())
|
|
419
|
+
if ax_index < len(ax_keys):
|
|
420
|
+
ax_key = ax_keys[ax_index]
|
|
421
|
+
ax_record = editor.fig.record.axes[ax_key]
|
|
422
|
+
ax_record.position_override = [left, bottom, width, height]
|
|
423
|
+
|
|
424
|
+
# Re-render
|
|
425
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
426
|
+
editor.fig,
|
|
427
|
+
editor.get_effective_style(),
|
|
428
|
+
editor.dark_mode,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Regenerate hitmap
|
|
432
|
+
hitmap_img, color_map = generate_hitmap(mpl_fig, img_size[0], img_size[1])
|
|
433
|
+
editor._color_map = color_map
|
|
434
|
+
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
435
|
+
editor._hitmap_generated = True
|
|
436
|
+
|
|
437
|
+
return jsonify(
|
|
438
|
+
{
|
|
439
|
+
"success": True,
|
|
440
|
+
"image": base64_img,
|
|
441
|
+
"bboxes": bboxes,
|
|
442
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
import traceback
|
|
448
|
+
|
|
449
|
+
traceback.print_exc()
|
|
450
|
+
return jsonify({"error": f"Update failed: {str(e)}"}), 500
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
__all__ = ["register_axis_routes"]
|