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,94 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Routes for panel snapshot generation (isolated rendering)."""
4
+
5
+ import base64
6
+ import io
7
+ import threading
8
+
9
+ # Lock to prevent concurrent matplotlib figure access (not thread-safe)
10
+ _figure_lock = threading.Lock()
11
+
12
+
13
+ def register_snapshot_routes(app, editor):
14
+ """Register snapshot-related routes.
15
+
16
+ Parameters
17
+ ----------
18
+ app : Flask
19
+ Flask application instance.
20
+ editor : FigureEditor
21
+ Editor instance with figure state.
22
+ """
23
+
24
+ @app.route("/get_panel_snapshot/<int:ax_index>")
25
+ def get_panel_snapshot(ax_index):
26
+ """Render a single panel in isolation and return as base64 PNG.
27
+
28
+ This hides all other axes to produce a clean snapshot without
29
+ overlap artifacts from neighboring panels.
30
+
31
+ Parameters
32
+ ----------
33
+ ax_index : int
34
+ Index of the axis/panel to render.
35
+
36
+ Returns
37
+ -------
38
+ dict
39
+ JSON with success status and base64-encoded PNG image.
40
+ """
41
+ # DISABLED: Modifying figure visibility corrupts shared state
42
+ # TODO: Implement proper solution (deep copy figure or pre-render)
43
+ return {"success": False, "error": "Snapshot temporarily disabled"}
44
+
45
+ try:
46
+ # Use lock to prevent concurrent matplotlib access (not thread-safe)
47
+ with _figure_lock:
48
+ # Get matplotlib figure from RecordingFigure wrapper
49
+ mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
50
+ axes = mpl_fig.get_axes()
51
+
52
+ if ax_index < 0 or ax_index >= len(axes):
53
+ return {"success": False, "error": f"Invalid ax_index: {ax_index}"}
54
+
55
+ # Store original visibility states
56
+ original_visibility = [ax.get_visible() for ax in axes]
57
+
58
+ try:
59
+ # Hide all axes except the target
60
+ for i, ax in enumerate(axes):
61
+ ax.set_visible(i == ax_index)
62
+
63
+ # Render to buffer with transparent background
64
+ # Use full figure size (no bbox_inches="tight" to preserve dimensions)
65
+ buf = io.BytesIO()
66
+ mpl_fig.savefig(
67
+ buf,
68
+ format="png",
69
+ transparent=True,
70
+ facecolor="none",
71
+ edgecolor="none",
72
+ )
73
+ buf.seek(0)
74
+ image_base64 = base64.b64encode(buf.read()).decode("utf-8")
75
+
76
+ return {
77
+ "success": True,
78
+ "image": image_base64,
79
+ "ax_index": ax_index,
80
+ }
81
+
82
+ finally:
83
+ # Restore original visibility
84
+ for i, ax in enumerate(axes):
85
+ ax.set_visible(original_visibility[i])
86
+
87
+ except Exception as e:
88
+ # Return JSON error instead of 500 to avoid console errors
89
+ return {"success": False, "error": str(e)}
90
+
91
+
92
+ __all__ = ["register_snapshot_routes"]
93
+
94
+ # EOF
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Style and theme Flask route handlers for the figure editor.
5
+ """
6
+
7
+ from flask import jsonify, request
8
+
9
+ from ._helpers import get_form_values_from_style, render_with_overrides
10
+
11
+
12
+ def register_style_routes(app, editor):
13
+ """Register style/theme routes with the Flask app."""
14
+ from ._overrides import save_overrides
15
+
16
+ @app.route("/style")
17
+ def get_style():
18
+ """Get current style configuration."""
19
+ return jsonify(
20
+ {
21
+ "base_style": editor.style_overrides.base_style,
22
+ "programmatic_style": editor.style_overrides.programmatic_style,
23
+ "manual_overrides": editor.style_overrides.manual_overrides,
24
+ "effective_style": editor.get_effective_style(),
25
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
26
+ "manual_timestamp": editor.style_overrides.manual_timestamp,
27
+ }
28
+ )
29
+
30
+ @app.route("/overrides")
31
+ def get_overrides():
32
+ """Get current manual overrides."""
33
+ return jsonify(editor.style_overrides.manual_overrides)
34
+
35
+ @app.route("/theme")
36
+ def get_theme():
37
+ """Get current theme YAML content for display."""
38
+ import io as yaml_io
39
+
40
+ from ruamel.yaml import YAML
41
+
42
+ style = editor.get_effective_style()
43
+ style_name = style.get("_name", "SCITEX")
44
+
45
+ yaml = YAML()
46
+ yaml.default_flow_style = False
47
+ yaml.indent(mapping=2, sequence=4, offset=2)
48
+ stream = yaml_io.StringIO()
49
+ yaml.dump(style, stream)
50
+ yaml_content = stream.getvalue()
51
+
52
+ return jsonify(
53
+ {
54
+ "name": style_name,
55
+ "content": yaml_content,
56
+ }
57
+ )
58
+
59
+ @app.route("/list_themes")
60
+ def list_themes():
61
+ """List available theme presets."""
62
+ from ..styles._style_loader import list_presets
63
+
64
+ presets = list_presets()
65
+ current = editor.get_effective_style().get("_name", "SCITEX")
66
+
67
+ return jsonify(
68
+ {
69
+ "themes": presets,
70
+ "current": current,
71
+ }
72
+ )
73
+
74
+ @app.route("/switch_theme", methods=["POST"])
75
+ def switch_theme():
76
+ """Switch to a different theme preset by reproducing the figure."""
77
+ from .._reproducer import reproduce_from_record
78
+ from ..styles._style_loader import load_preset
79
+
80
+ data = request.get_json() or {}
81
+ theme_name = data.get("theme")
82
+
83
+ if not theme_name:
84
+ return jsonify({"error": "No theme specified"}), 400
85
+
86
+ try:
87
+ new_style = load_preset(theme_name)
88
+
89
+ if new_style is None:
90
+ return jsonify({"error": f"Theme '{theme_name}' not found"}), 404
91
+
92
+ # Convert nested style to flat style dict with color_palette
93
+ flat_style = dict(new_style)
94
+ flat_style["_name"] = theme_name
95
+
96
+ # Extract color_palette from nested colors.palette
97
+ if "colors" in new_style and isinstance(new_style["colors"], dict):
98
+ colors_dict = new_style["colors"]
99
+ if "palette" in colors_dict and colors_dict["palette"] is not None:
100
+ flat_style["color_palette"] = list(colors_dict["palette"])
101
+
102
+ editor.style_overrides.base_style = flat_style
103
+
104
+ if hasattr(editor.fig, "record") and editor.fig.record is not None:
105
+ editor.fig.record.style = flat_style
106
+ new_fig, _ = reproduce_from_record(editor.fig.record)
107
+ editor.fig = new_fig
108
+ # Keep the new style (don't restore old style)
109
+
110
+ mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
111
+ behavior = new_style.get("behavior", {})
112
+ for ax in mpl_fig.get_axes():
113
+ # Handle all four spine directions
114
+ for side, default in [
115
+ ("top", True),
116
+ ("right", True),
117
+ ("bottom", False),
118
+ ("left", False),
119
+ ]:
120
+ hide = behavior.get(f"hide_{side}_spine", default)
121
+ ax.spines[side].set_visible(not hide)
122
+
123
+ if behavior.get("grid", False):
124
+ ax.grid(True, alpha=0.3)
125
+ else:
126
+ ax.grid(False)
127
+
128
+ base64_img, bboxes, img_size = render_with_overrides(
129
+ editor.fig,
130
+ editor.get_effective_style(),
131
+ editor.dark_mode,
132
+ )
133
+
134
+ form_values = get_form_values_from_style(editor.get_effective_style())
135
+
136
+ return jsonify(
137
+ {
138
+ "success": True,
139
+ "theme": theme_name,
140
+ "image": base64_img,
141
+ "bboxes": bboxes,
142
+ "img_size": {"width": img_size[0], "height": img_size[1]},
143
+ "values": form_values,
144
+ }
145
+ )
146
+
147
+ except Exception as e:
148
+ import traceback
149
+
150
+ traceback.print_exc()
151
+ return jsonify({"error": f"Failed to switch theme: {str(e)}"}), 500
152
+
153
+ @app.route("/save", methods=["POST"])
154
+ def save():
155
+ """Save style overrides (stored separately from recipe)."""
156
+ data = request.get_json() or {}
157
+ editor.style_overrides.update_manual_overrides(data.get("overrides", {}))
158
+
159
+ if editor.recipe_path:
160
+ path = save_overrides(editor.style_overrides, editor.recipe_path)
161
+ return jsonify(
162
+ {
163
+ "success": True,
164
+ "path": str(path),
165
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
166
+ "timestamp": editor.style_overrides.manual_timestamp,
167
+ }
168
+ )
169
+
170
+ return jsonify(
171
+ {
172
+ "success": True,
173
+ "overrides": editor.overrides,
174
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
175
+ }
176
+ )
177
+
178
+ @app.route("/restore", methods=["POST"])
179
+ def restore():
180
+ """Restore to original style (clear manual overrides and axes positions)."""
181
+ from ._bbox import extract_bboxes
182
+
183
+ # Clear all manual overrides (including position overrides)
184
+ editor.style_overrides.clear_manual_overrides()
185
+
186
+ # Restore original axes positions
187
+ editor.restore_axes_positions()
188
+
189
+ # Restore original annotation positions (panel labels, text)
190
+ editor.restore_annotation_positions()
191
+
192
+ if editor._initial_base64 and not editor.dark_mode:
193
+ base64_img = editor._initial_base64
194
+ import base64 as b64
195
+ import io
196
+
197
+ from PIL import Image
198
+
199
+ img_data = b64.b64decode(base64_img)
200
+ img = Image.open(io.BytesIO(img_data))
201
+ img_size = img.size
202
+ mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
203
+ original_dpi = mpl_fig.dpi
204
+ try:
205
+ mpl_fig.set_dpi(150)
206
+ mpl_fig.canvas.draw()
207
+ except Exception:
208
+ # Ignore matplotlib/tkinter threading issues in background thread
209
+ pass
210
+ bboxes = extract_bboxes(mpl_fig, img_size[0], img_size[1])
211
+ try:
212
+ mpl_fig.set_dpi(original_dpi)
213
+ except Exception:
214
+ pass
215
+ else:
216
+ base64_img, bboxes, img_size = render_with_overrides(
217
+ editor.fig,
218
+ None,
219
+ editor.dark_mode,
220
+ )
221
+
222
+ return jsonify(
223
+ {
224
+ "success": True,
225
+ "image": base64_img,
226
+ "bboxes": bboxes,
227
+ "img_size": {"width": img_size[0], "height": img_size[1]},
228
+ "original_style": editor.style,
229
+ }
230
+ )
231
+
232
+ @app.route("/diff")
233
+ def get_diff():
234
+ """Get differences between original and manual overrides."""
235
+ return jsonify(
236
+ {
237
+ "diff": editor.style_overrides.get_diff(),
238
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
239
+ }
240
+ )
241
+
242
+
243
+ __all__ = ["register_style_routes"]
@@ -8,13 +8,32 @@ single source of truth. No custom key mapping is needed since all keys
8
8
  now match the HTML input IDs directly.
9
9
  """
10
10
 
11
+ import base64
11
12
  import json
13
+ from datetime import datetime
14
+ from pathlib import Path
12
15
  from typing import Any, Dict, Tuple
13
16
 
17
+ import figrecipe
18
+
14
19
  from ._html import HTML_TEMPLATE
20
+ from ._html_components import HTML_FILE_BROWSER
21
+ from ._html_datatable import HTML_DATATABLE_PANEL
15
22
  from ._scripts import SCRIPTS
16
23
  from ._styles import STYLES
17
24
 
25
+ # Server start time for debugging template reloads
26
+ _SERVER_START_TIME = datetime.now().strftime("%H:%M:%S")
27
+
28
+ # Load SciTeX icon as base64
29
+ _SCITEX_ICON_PATH = (
30
+ Path(__file__).parent.parent / "static" / "icons" / "scitex-icon.png"
31
+ )
32
+ _SCITEX_ICON_BASE64 = ""
33
+ if _SCITEX_ICON_PATH.exists():
34
+ with open(_SCITEX_ICON_PATH, "rb") as f:
35
+ _SCITEX_ICON_BASE64 = base64.b64encode(f.read()).decode("utf-8")
36
+
18
37
 
19
38
  def build_html_template(
20
39
  image_base64: str,
@@ -24,6 +43,10 @@ def build_html_template(
24
43
  overrides: Dict[str, Any],
25
44
  img_size: Tuple[int, int],
26
45
  style_name: str = "SCITEX",
46
+ hot_reload: bool = False,
47
+ dark_mode: bool = False,
48
+ figure_has_content: bool = True,
49
+ debug_mode: bool = False,
27
50
  ) -> str:
28
51
  """
29
52
  Build complete HTML template for figure editor.
@@ -47,6 +70,12 @@ def build_html_template(
47
70
  (width, height) of preview image.
48
71
  style_name : str
49
72
  Name of the applied style preset (e.g., "SCITEX", "MATPLOTLIB").
73
+ hot_reload : bool
74
+ Enable hot reload auto-reconnect JavaScript.
75
+ dark_mode : bool
76
+ Initial dark mode state from saved preferences.
77
+ figure_has_content : bool
78
+ Whether the figure has plot content (hides welcome overlay if True).
50
79
 
51
80
  Returns
52
81
  -------
@@ -57,10 +86,65 @@ def build_html_template(
57
86
  # Keys should already match HTML input IDs (YAML-compatible flattened)
58
87
  initial_values = {**style, **overrides}
59
88
 
89
+ # Hot reload JavaScript for auto-reconnect on server restart
90
+ hot_reload_script = ""
91
+ if hot_reload:
92
+ hot_reload_script = """
93
+ // Hot Reload: Auto-reconnect when server restarts
94
+ (function() {
95
+ let isReconnecting = false;
96
+ let pingInterval = null;
97
+
98
+ function showReloadBanner(show) {
99
+ let banner = document.getElementById('hot-reload-banner');
100
+ if (!banner && show) {
101
+ banner = document.createElement('div');
102
+ banner.id = 'hot-reload-banner';
103
+ banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#f59e0b;' +
104
+ 'color:#000;text-align:center;padding:8px;z-index:9999;font-weight:bold;';
105
+ banner.textContent = 'Server restarting... will reload automatically';
106
+ document.body.prepend(banner);
107
+ }
108
+ if (banner) {
109
+ banner.style.display = show ? 'block' : 'none';
110
+ }
111
+ }
112
+
113
+ function ping() {
114
+ fetch('/ping', {cache: 'no-store'})
115
+ .then(r => {
116
+ if (r.ok && isReconnecting) {
117
+ // Server is back! Reload the page
118
+ console.log('[Hot Reload] Server is back, reloading...');
119
+ window.location.reload();
120
+ }
121
+ })
122
+ .catch(() => {
123
+ if (!isReconnecting) {
124
+ console.log('[Hot Reload] Server disconnected, waiting for restart...');
125
+ isReconnecting = true;
126
+ showReloadBanner(true);
127
+ }
128
+ });
129
+ }
130
+
131
+ // Start pinging every 500ms
132
+ pingInterval = setInterval(ping, 500);
133
+ console.log('[Hot Reload] Enabled - watching for server restarts');
134
+ })();
135
+ """
136
+
60
137
  # Inject data into template
61
138
  html = HTML_TEMPLATE
139
+ html = html.replace("<!-- FILE_BROWSER_PLACEHOLDER -->", HTML_FILE_BROWSER)
140
+
141
+ # Insert datatable panel before preview panel
142
+ html = html.replace(
143
+ "<!-- Preview Panel -->",
144
+ HTML_DATATABLE_PANEL + "\n <!-- Preview Panel -->",
145
+ )
62
146
  html = html.replace("/* STYLES_PLACEHOLDER */", STYLES)
63
- html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS)
147
+ html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS + hot_reload_script)
64
148
  html = html.replace("IMAGE_BASE64_PLACEHOLDER", image_base64)
65
149
  html = html.replace("BBOXES_PLACEHOLDER", json.dumps(bboxes))
66
150
  html = html.replace("COLOR_MAP_PLACEHOLDER", json.dumps(color_map))
@@ -68,6 +152,37 @@ def build_html_template(
68
152
  html = html.replace("IMG_WIDTH_PLACEHOLDER", str(img_size[0]))
69
153
  html = html.replace("IMG_HEIGHT_PLACEHOLDER", str(img_size[1]))
70
154
  html = html.replace("STYLE_NAME_PLACEHOLDER", style_name)
155
+ html = html.replace("SCITEX_ICON_PLACEHOLDER", _SCITEX_ICON_BASE64)
156
+
157
+ # Dark mode preference - set initial state
158
+ html = html.replace("DARK_MODE_THEME_PLACEHOLDER", "dark" if dark_mode else "light")
159
+
160
+ # Server start time for debugging
161
+ html = html.replace("SERVER_START_TIME_PLACEHOLDER", _SERVER_START_TIME)
162
+
163
+ # Version number
164
+ html = html.replace("VERSION_PLACEHOLDER", figrecipe.__version__)
165
+
166
+ # Welcome overlay - show only for empty figures
167
+ welcome_display = "none" if figure_has_content else "flex"
168
+ html = html.replace("WELCOME_DISPLAY_PLACEHOLDER", welcome_display)
169
+
170
+ # Debug mode - enables Element Inspector and Show All Bboxes
171
+ html = html.replace("DEBUG_MODE_PLACEHOLDER", "true" if debug_mode else "false")
172
+
173
+ # Debug shortcuts section - only shown when debug mode is enabled
174
+ debug_shortcuts_html = ""
175
+ if debug_mode:
176
+ debug_shortcuts_html = """<div class="shortcut-section debug-shortcuts"><h4>Debug <span class="debug-badge">DEBUG MODE</span></h4>
177
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>I</kbd></span><span class="shortcut-desc">Element Inspector</span></div>
178
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>B</kbd></span><span class="shortcut-desc">Show All Bboxes</span></div>
179
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>I</kbd></span><span class="shortcut-desc">Debug Snapshot</span></div></div>"""
180
+ html = html.replace("DEBUG_SHORTCUTS_PLACEHOLDER", debug_shortcuts_html)
181
+
182
+ # Debug meta (server start time) - only shown in debug mode
183
+ html = html.replace(
184
+ "DEBUG_META_DISPLAY_PLACEHOLDER", "" if debug_mode else 'style="display:none"'
185
+ )
71
186
 
72
187
  return html
73
188