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.
Files changed (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {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"]