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.
Files changed (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {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