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,223 @@
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("/theme")
31
+ def get_theme():
32
+ """Get current theme YAML content for display."""
33
+ import io as yaml_io
34
+
35
+ from ruamel.yaml import YAML
36
+
37
+ style = editor.get_effective_style()
38
+ style_name = style.get("_name", "SCITEX")
39
+
40
+ yaml = YAML()
41
+ yaml.default_flow_style = False
42
+ yaml.indent(mapping=2, sequence=4, offset=2)
43
+ stream = yaml_io.StringIO()
44
+ yaml.dump(style, stream)
45
+ yaml_content = stream.getvalue()
46
+
47
+ return jsonify(
48
+ {
49
+ "name": style_name,
50
+ "content": yaml_content,
51
+ }
52
+ )
53
+
54
+ @app.route("/list_themes")
55
+ def list_themes():
56
+ """List available theme presets."""
57
+ from ..styles._style_loader import list_presets
58
+
59
+ presets = list_presets()
60
+ current = editor.get_effective_style().get("_name", "SCITEX")
61
+
62
+ return jsonify(
63
+ {
64
+ "themes": presets,
65
+ "current": current,
66
+ }
67
+ )
68
+
69
+ @app.route("/switch_theme", methods=["POST"])
70
+ def switch_theme():
71
+ """Switch to a different theme preset by reproducing the figure."""
72
+ from .._reproducer import reproduce_from_record
73
+ from ..styles._style_loader import load_preset
74
+
75
+ data = request.get_json() or {}
76
+ theme_name = data.get("theme")
77
+
78
+ if not theme_name:
79
+ return jsonify({"error": "No theme specified"}), 400
80
+
81
+ try:
82
+ new_style = load_preset(theme_name)
83
+
84
+ if new_style is None:
85
+ return jsonify({"error": f"Theme '{theme_name}' not found"}), 404
86
+
87
+ # Convert nested style to flat style dict with color_palette
88
+ flat_style = dict(new_style)
89
+ flat_style["_name"] = theme_name
90
+
91
+ # Extract color_palette from nested colors.palette
92
+ if "colors" in new_style and isinstance(new_style["colors"], dict):
93
+ colors_dict = new_style["colors"]
94
+ if "palette" in colors_dict:
95
+ flat_style["color_palette"] = list(colors_dict["palette"])
96
+
97
+ editor.style_overrides.base_style = flat_style
98
+
99
+ if hasattr(editor.fig, "record") and editor.fig.record is not None:
100
+ editor.fig.record.style = flat_style
101
+ new_fig, _ = reproduce_from_record(editor.fig.record)
102
+ editor.fig = new_fig
103
+ # Keep the new style (don't restore old style)
104
+
105
+ mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
106
+ behavior = new_style.get("behavior", {})
107
+ for ax in mpl_fig.get_axes():
108
+ hide_top = behavior.get("hide_top_spine", True)
109
+ hide_right = behavior.get("hide_right_spine", True)
110
+ ax.spines["top"].set_visible(not hide_top)
111
+ ax.spines["right"].set_visible(not hide_right)
112
+
113
+ if behavior.get("grid", False):
114
+ ax.grid(True, alpha=0.3)
115
+ else:
116
+ ax.grid(False)
117
+
118
+ base64_img, bboxes, img_size = render_with_overrides(
119
+ editor.fig,
120
+ editor.get_effective_style(),
121
+ editor.dark_mode,
122
+ )
123
+
124
+ form_values = get_form_values_from_style(editor.get_effective_style())
125
+
126
+ return jsonify(
127
+ {
128
+ "success": True,
129
+ "theme": theme_name,
130
+ "image": base64_img,
131
+ "bboxes": bboxes,
132
+ "img_size": {"width": img_size[0], "height": img_size[1]},
133
+ "values": form_values,
134
+ }
135
+ )
136
+
137
+ except Exception as e:
138
+ import traceback
139
+
140
+ traceback.print_exc()
141
+ return jsonify({"error": f"Failed to switch theme: {str(e)}"}), 500
142
+
143
+ @app.route("/save", methods=["POST"])
144
+ def save():
145
+ """Save style overrides (stored separately from recipe)."""
146
+ data = request.get_json() or {}
147
+ editor.style_overrides.update_manual_overrides(data.get("overrides", {}))
148
+
149
+ if editor.recipe_path:
150
+ path = save_overrides(editor.style_overrides, editor.recipe_path)
151
+ return jsonify(
152
+ {
153
+ "success": True,
154
+ "path": str(path),
155
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
156
+ "timestamp": editor.style_overrides.manual_timestamp,
157
+ }
158
+ )
159
+
160
+ return jsonify(
161
+ {
162
+ "success": True,
163
+ "overrides": editor.overrides,
164
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
165
+ }
166
+ )
167
+
168
+ @app.route("/restore", methods=["POST"])
169
+ def restore():
170
+ """Restore to original style (clear manual overrides and axes positions)."""
171
+ from ._bbox import extract_bboxes
172
+
173
+ # Clear all manual overrides (including position overrides)
174
+ editor.style_overrides.clear_manual_overrides()
175
+
176
+ # Restore original axes positions
177
+ editor.restore_axes_positions()
178
+
179
+ if editor._initial_base64 and not editor.dark_mode:
180
+ base64_img = editor._initial_base64
181
+ import base64 as b64
182
+ import io
183
+
184
+ from PIL import Image
185
+
186
+ img_data = b64.b64decode(base64_img)
187
+ img = Image.open(io.BytesIO(img_data))
188
+ img_size = img.size
189
+ mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
190
+ original_dpi = mpl_fig.dpi
191
+ mpl_fig.set_dpi(150)
192
+ mpl_fig.canvas.draw()
193
+ bboxes = extract_bboxes(mpl_fig, img_size[0], img_size[1])
194
+ mpl_fig.set_dpi(original_dpi)
195
+ else:
196
+ base64_img, bboxes, img_size = render_with_overrides(
197
+ editor.fig,
198
+ None,
199
+ editor.dark_mode,
200
+ )
201
+
202
+ return jsonify(
203
+ {
204
+ "success": True,
205
+ "image": base64_img,
206
+ "bboxes": bboxes,
207
+ "img_size": {"width": img_size[0], "height": img_size[1]},
208
+ "original_style": editor.style,
209
+ }
210
+ )
211
+
212
+ @app.route("/diff")
213
+ def get_diff():
214
+ """Get differences between original and manual overrides."""
215
+ return jsonify(
216
+ {
217
+ "diff": editor.style_overrides.get_diff(),
218
+ "has_overrides": editor.style_overrides.has_manual_overrides(),
219
+ }
220
+ )
221
+
222
+
223
+ __all__ = ["register_style_routes"]
@@ -8,13 +8,28 @@ 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
 
14
17
  from ._html import HTML_TEMPLATE
15
18
  from ._scripts import SCRIPTS
16
19
  from ._styles import STYLES
17
20
 
21
+ # Server start time for debugging template reloads
22
+ _SERVER_START_TIME = datetime.now().strftime("%H:%M:%S")
23
+
24
+ # Load SciTeX icon as base64
25
+ _SCITEX_ICON_PATH = (
26
+ Path(__file__).parent.parent.parent.parent.parent / "docs" / "scitex-icon.png"
27
+ )
28
+ _SCITEX_ICON_BASE64 = ""
29
+ if _SCITEX_ICON_PATH.exists():
30
+ with open(_SCITEX_ICON_PATH, "rb") as f:
31
+ _SCITEX_ICON_BASE64 = base64.b64encode(f.read()).decode("utf-8")
32
+
18
33
 
19
34
  def build_html_template(
20
35
  image_base64: str,
@@ -24,6 +39,8 @@ def build_html_template(
24
39
  overrides: Dict[str, Any],
25
40
  img_size: Tuple[int, int],
26
41
  style_name: str = "SCITEX",
42
+ hot_reload: bool = False,
43
+ dark_mode: bool = False,
27
44
  ) -> str:
28
45
  """
29
46
  Build complete HTML template for figure editor.
@@ -47,6 +64,10 @@ def build_html_template(
47
64
  (width, height) of preview image.
48
65
  style_name : str
49
66
  Name of the applied style preset (e.g., "SCITEX", "MATPLOTLIB").
67
+ hot_reload : bool
68
+ Enable hot reload auto-reconnect JavaScript.
69
+ dark_mode : bool
70
+ Initial dark mode state from saved preferences.
50
71
 
51
72
  Returns
52
73
  -------
@@ -57,10 +78,58 @@ def build_html_template(
57
78
  # Keys should already match HTML input IDs (YAML-compatible flattened)
58
79
  initial_values = {**style, **overrides}
59
80
 
81
+ # Hot reload JavaScript for auto-reconnect on server restart
82
+ hot_reload_script = ""
83
+ if hot_reload:
84
+ hot_reload_script = """
85
+ // Hot Reload: Auto-reconnect when server restarts
86
+ (function() {
87
+ let isReconnecting = false;
88
+ let pingInterval = null;
89
+
90
+ function showReloadBanner(show) {
91
+ let banner = document.getElementById('hot-reload-banner');
92
+ if (!banner && show) {
93
+ banner = document.createElement('div');
94
+ banner.id = 'hot-reload-banner';
95
+ banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#f59e0b;' +
96
+ 'color:#000;text-align:center;padding:8px;z-index:9999;font-weight:bold;';
97
+ banner.textContent = 'Server restarting... will reload automatically';
98
+ document.body.prepend(banner);
99
+ }
100
+ if (banner) {
101
+ banner.style.display = show ? 'block' : 'none';
102
+ }
103
+ }
104
+
105
+ function ping() {
106
+ fetch('/ping', {cache: 'no-store'})
107
+ .then(r => {
108
+ if (r.ok && isReconnecting) {
109
+ // Server is back! Reload the page
110
+ console.log('[Hot Reload] Server is back, reloading...');
111
+ window.location.reload();
112
+ }
113
+ })
114
+ .catch(() => {
115
+ if (!isReconnecting) {
116
+ console.log('[Hot Reload] Server disconnected, waiting for restart...');
117
+ isReconnecting = true;
118
+ showReloadBanner(true);
119
+ }
120
+ });
121
+ }
122
+
123
+ // Start pinging every 500ms
124
+ pingInterval = setInterval(ping, 500);
125
+ console.log('[Hot Reload] Enabled - watching for server restarts');
126
+ })();
127
+ """
128
+
60
129
  # Inject data into template
61
130
  html = HTML_TEMPLATE
62
131
  html = html.replace("/* STYLES_PLACEHOLDER */", STYLES)
63
- html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS)
132
+ html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS + hot_reload_script)
64
133
  html = html.replace("IMAGE_BASE64_PLACEHOLDER", image_base64)
65
134
  html = html.replace("BBOXES_PLACEHOLDER", json.dumps(bboxes))
66
135
  html = html.replace("COLOR_MAP_PLACEHOLDER", json.dumps(color_map))
@@ -68,6 +137,14 @@ def build_html_template(
68
137
  html = html.replace("IMG_WIDTH_PLACEHOLDER", str(img_size[0]))
69
138
  html = html.replace("IMG_HEIGHT_PLACEHOLDER", str(img_size[1]))
70
139
  html = html.replace("STYLE_NAME_PLACEHOLDER", style_name)
140
+ html = html.replace("SCITEX_ICON_PLACEHOLDER", _SCITEX_ICON_BASE64)
141
+
142
+ # Dark mode preference - set initial state
143
+ html = html.replace("DARK_MODE_THEME_PLACEHOLDER", "dark" if dark_mode else "light")
144
+ html = html.replace("DARK_MODE_CHECKED_PLACEHOLDER", "checked" if dark_mode else "")
145
+
146
+ # Server start time for debugging
147
+ html = html.replace("SERVER_START_TIME_PLACEHOLDER", _SERVER_START_TIME)
71
148
 
72
149
  return html
73
150
 
@@ -6,11 +6,12 @@ HTML template for figure editor.
6
6
 
7
7
  HTML_TEMPLATE = """
8
8
  <!DOCTYPE html>
9
- <html lang="en" data-theme="light">
9
+ <html lang="en" data-theme="DARK_MODE_THEME_PLACEHOLDER">
10
10
  <head>
11
11
  <meta charset="UTF-8">
12
12
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
13
  <title>figrecipe Editor</title>
14
+ <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
14
15
  <style>
15
16
  /* STYLES_PLACEHOLDER */
16
17
  </style>
@@ -20,7 +21,17 @@ HTML_TEMPLATE = """
20
21
  <!-- Preview Panel -->
21
22
  <div class="preview-panel">
22
23
  <div class="preview-header">
23
- <h2>Preview</h2>
24
+ <a href="https://scitex.ai" target="_blank" class="scitex-branding" title="FigRecipe - Part of SciTeX">
25
+ <img src="data:image/png;base64,SCITEX_ICON_PLACEHOLDER" alt="SciTeX" class="scitex-icon">
26
+ <span class="figrecipe-title">FigRecipe Editor</span>
27
+ </a>
28
+ <span id="server-start-time" style="font-size: 10px; color: #888; margin-left: 8px;">Started: SERVER_START_TIME_PLACEHOLDER</span>
29
+ <div class="file-switcher">
30
+ <select id="file-selector" class="file-selector" title="Switch between recipe files">
31
+ <option value="">Loading files...</option>
32
+ </select>
33
+ <button id="btn-new-figure" class="btn-new" title="Create new blank figure">+</button>
34
+ </div>
24
35
  <div class="preview-controls">
25
36
  <div class="download-dropdown">
26
37
  <button id="btn-download-main" class="btn-primary download-main" title="Download as PNG">Download PNG</button>
@@ -29,24 +40,36 @@ HTML_TEMPLATE = """
29
40
  <button id="btn-download-png-menu" class="download-option active" data-format="png">PNG</button>
30
41
  <button id="btn-download-svg-menu" class="download-option" data-format="svg">SVG</button>
31
42
  <button id="btn-download-pdf-menu" class="download-option" data-format="pdf">PDF</button>
43
+ <hr style="margin: 4px 0; border: none; border-top: 1px solid #ddd;">
44
+ <button id="btn-download-csv-menu" class="download-option" data-format="csv" title="Export plot data as CSV">CSV (Data)</button>
32
45
  </div>
33
46
  </div>
34
47
  <button id="btn-refresh" title="Refresh preview">Refresh</button>
35
- <button id="btn-show-hitmap" title="Toggle hitmap overlay for debugging" style="display: none;">Show Hit Regions</button>
48
+ <div class="zoom-controls">
49
+ <button id="btn-zoom-out" title="Zoom out (-)">−</button>
50
+ <span id="zoom-level">100%</span>
51
+ <button id="btn-zoom-in" title="Zoom in (+)">+</button>
52
+ <button id="btn-zoom-reset" title="Reset zoom (0)">⟲</button>
53
+ <button id="btn-zoom-fit" title="Fit to view (F)">Fit</button>
54
+ </div>
55
+ <button id="btn-ruler-grid" class="btn-ruler" title="Toggle rulers and grid overlay (G)">Ruler & Grid</button>
56
+ <button id="btn-shortcuts" class="btn-shortcuts" title="Show keyboard shortcuts (?)">⌨</button>
36
57
  <label class="theme-toggle">
37
- <input type="checkbox" id="dark-mode-toggle">
58
+ <input type="checkbox" id="dark-mode-toggle" DARK_MODE_CHECKED_PLACEHOLDER>
38
59
  <span>Dark Mode</span>
39
60
  </label>
40
61
  </div>
41
62
  </div>
42
- <div class="preview-wrapper">
43
- <img id="preview-image" src="data:image/png;base64,IMAGE_BASE64_PLACEHOLDER" alt="Figure preview">
44
- <svg id="hitregion-overlay" class="hitregion-overlay"></svg>
45
- <svg id="selection-overlay" class="selection-overlay"></svg>
46
- <canvas id="hitmap-canvas" style="display: none;"></canvas>
47
- </div>
48
- <div class="selected-element-info" id="selected-info">
49
- Click on an element to select it
63
+ <div class="preview-wrapper" id="preview-wrapper">
64
+ <div class="zoom-container" id="zoom-container">
65
+ <img id="preview-image" src="data:image/png;base64,IMAGE_BASE64_PLACEHOLDER" alt="Figure preview">
66
+ <svg id="hitregion-overlay" class="hitregion-overlay"></svg>
67
+ <svg id="selection-overlay" class="selection-overlay"></svg>
68
+ <svg id="ruler-overlay" class="ruler-overlay"></svg>
69
+ <svg id="grid-overlay" class="grid-overlay"></svg>
70
+ <svg id="column-overlay" class="column-overlay"></svg>
71
+ <canvas id="hitmap-canvas" style="display: none;"></canvas>
72
+ </div>
50
73
  </div>
51
74
  </div>
52
75
 
@@ -86,6 +109,45 @@ HTML_TEMPLATE = """
86
109
  </div>
87
110
  </div>
88
111
  </div>
112
+ <!-- Shortcuts Modal -->
113
+ <div id="shortcuts-modal" class="modal" style="display: none;">
114
+ <div class="modal-content shortcuts-modal-content">
115
+ <div class="modal-header">
116
+ <h3>Keyboard Shortcuts</h3>
117
+ <button id="shortcuts-modal-close" class="modal-close">&times;</button>
118
+ </div>
119
+ <div class="shortcuts-content">
120
+ <div class="shortcut-section">
121
+ <h4>General</h4>
122
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>S</kbd></span><span class="shortcut-desc">Save overrides</span></div>
123
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd></span><span class="shortcut-desc">Download PNG</span></div>
124
+ <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>
125
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>F5</kbd> / <kbd>Ctrl</kbd>+<kbd>R</kbd></span><span class="shortcut-desc">Refresh preview</span></div>
126
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Esc</kbd></span><span class="shortcut-desc">Clear selection</span></div>
127
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>R</kbd></span><span class="shortcut-desc">Reset to theme defaults</span></div>
128
+ </div>
129
+ <div class="shortcut-section">
130
+ <h4>Navigation</h4>
131
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>1</kbd></span><span class="shortcut-desc">Figure tab</span></div>
132
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>2</kbd></span><span class="shortcut-desc">Axis tab</span></div>
133
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>3</kbd></span><span class="shortcut-desc">Element tab</span></div>
134
+ </div>
135
+ <div class="shortcut-section">
136
+ <h4>View</h4>
137
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>G</kbd></span><span class="shortcut-desc">Toggle ruler &amp; grid</span></div>
138
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>+</kbd> / <kbd>-</kbd></span><span class="shortcut-desc">Zoom in/out</span></div>
139
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>0</kbd></span><span class="shortcut-desc">Reset zoom</span></div>
140
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>F</kbd></span><span class="shortcut-desc">Fit to view</span></div>
141
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>?</kbd></span><span class="shortcut-desc">Show this help</span></div>
142
+ </div>
143
+ <div class="shortcut-section">
144
+ <h4>Developer</h4>
145
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>I</kbd></span><span class="shortcut-desc">Toggle element inspector</span></div>
146
+ <div class="shortcut-row"><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd></span><span class="shortcut-desc">Screenshot + console logs</span></div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
89
151
  <div id="override-status" class="override-status" style="display: none;">
90
152
  <span class="override-indicator">Manual overrides active</span>
91
153
  <span id="override-timestamp" class="override-timestamp"></span>
@@ -160,8 +222,42 @@ HTML_TEMPLATE = """
160
222
  <div id="tab-content-axis" class="tab-content">
161
223
  <div class="tab-hint" id="axis-tab-hint">Select an axis element (title, label, ticks, legend) to edit</div>
162
224
 
163
- <!-- Axes Size Section -->
225
+ <!-- Panel Position Section -->
164
226
  <details class="section" open>
227
+ <summary>Panel Position</summary>
228
+ <div class="section-content">
229
+ <div class="form-row">
230
+ <label>Panel</label>
231
+ <select id="panel_selector">
232
+ <option value="0">Panel 0</option>
233
+ </select>
234
+ </div>
235
+ <div class="position-grid">
236
+ <div class="form-row">
237
+ <label>Left (mm)</label>
238
+ <input type="number" id="panel_left" step="1" min="0" placeholder="20">
239
+ </div>
240
+ <div class="form-row">
241
+ <label>Top (mm)</label>
242
+ <input type="number" id="panel_top" step="1" min="0" placeholder="15">
243
+ </div>
244
+ <div class="form-row">
245
+ <label>Width (mm)</label>
246
+ <input type="number" id="panel_width" step="1" min="0" placeholder="120">
247
+ </div>
248
+ <div class="form-row">
249
+ <label>Height (mm)</label>
250
+ <input type="number" id="panel_height" step="1" min="0" placeholder="90">
251
+ </div>
252
+ </div>
253
+ <div class="form-row" style="margin-top: 8px;">
254
+ <button id="apply_panel_position" class="btn-small">Apply Position</button>
255
+ </div>
256
+ </div>
257
+ </details>
258
+
259
+ <!-- Axes Size Section -->
260
+ <details class="section">
165
261
  <summary>Axes Size</summary>
166
262
  <div class="section-content">
167
263
  <div class="form-row">
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """JavaScript modules for the figure editor.
4
+
5
+ This package contains modular JavaScript organized by functionality:
6
+ - _api.py: API calls (save, load, update, download)
7
+ - _colors.py: Color presets and conversion utilities
8
+ - _core.py: State variables and initialization
9
+ - _debug_snapshot.py: Screenshot + console log capture (Ctrl+Alt+I)
10
+ - _element_editor.py: Dynamic form fields and call properties
11
+ - _files.py: File switching functionality
12
+ - _hitmap.py: Hitmap loading and hit region drawing
13
+ - _inspector.py: Element inspector debugging
14
+ - _labels.py: Label inputs and axis/legend controls
15
+ - _modals.py: Theme and shortcuts modals
16
+ - _panel_drag.py: Panel drag-to-move (click+drag on empty panel area)
17
+ - _legend_drag.py: Legend drag-to-move (click+drag on legend)
18
+ - _panel_position.py: Panel position editing (left, bottom, width, height)
19
+ - _overlays.py: Measurement overlays (ruler, grid, columns)
20
+ - _selection.py: Selection drawing and property sync
21
+ - _tabs.py: Tab navigation (Figure/Axis/Element)
22
+ - _view_mode.py: View mode management (all/selected)
23
+ - _zoom.py: Zoom/pan functionality
24
+ """
25
+
26
+ from ._api import SCRIPTS_API
27
+ from ._colors import SCRIPTS_COLORS
28
+ from ._core import SCRIPTS_CORE
29
+ from ._debug_snapshot import SCRIPTS_DEBUG_SNAPSHOT
30
+ from ._element_editor import SCRIPTS_ELEMENT_EDITOR
31
+ from ._files import SCRIPTS_FILES
32
+ from ._hitmap import SCRIPTS_HITMAP
33
+ from ._inspector import SCRIPTS_INSPECTOR
34
+ from ._labels import SCRIPTS_LABELS
35
+ from ._legend_drag import SCRIPTS_LEGEND_DRAG
36
+ from ._modals import SCRIPTS_MODALS
37
+ from ._overlays import SCRIPTS_OVERLAYS
38
+ from ._panel_drag import SCRIPTS_PANEL_DRAG
39
+ from ._panel_position import SCRIPTS_PANEL_POSITION
40
+ from ._selection import SCRIPTS_SELECTION
41
+ from ._tabs import SCRIPTS_TABS
42
+ from ._view_mode import SCRIPTS_VIEW_MODE
43
+ from ._zoom import SCRIPTS_ZOOM
44
+
45
+ # Combined SCRIPTS constant for backward compatibility
46
+ # Order matters: debug snapshot first (for console interception), then core/state, then features
47
+ SCRIPTS = (
48
+ SCRIPTS_DEBUG_SNAPSHOT
49
+ + SCRIPTS_CORE
50
+ + SCRIPTS_TABS
51
+ + SCRIPTS_VIEW_MODE
52
+ + SCRIPTS_COLORS
53
+ + SCRIPTS_HITMAP
54
+ + SCRIPTS_SELECTION
55
+ + SCRIPTS_ELEMENT_EDITOR
56
+ + SCRIPTS_LABELS
57
+ + SCRIPTS_API
58
+ + SCRIPTS_MODALS
59
+ + SCRIPTS_ZOOM
60
+ + SCRIPTS_OVERLAYS
61
+ + SCRIPTS_INSPECTOR
62
+ + SCRIPTS_FILES
63
+ + SCRIPTS_PANEL_POSITION
64
+ + SCRIPTS_PANEL_DRAG
65
+ + SCRIPTS_LEGEND_DRAG
66
+ )
67
+
68
+
69
+ def get_all_scripts():
70
+ """Return all scripts as a dictionary for testing.
71
+
72
+ Returns
73
+ -------
74
+ dict
75
+ Mapping of script name to script content.
76
+ """
77
+ return {
78
+ "api": SCRIPTS_API,
79
+ "colors": SCRIPTS_COLORS,
80
+ "core": SCRIPTS_CORE,
81
+ "debug_snapshot": SCRIPTS_DEBUG_SNAPSHOT,
82
+ "element_editor": SCRIPTS_ELEMENT_EDITOR,
83
+ "files": SCRIPTS_FILES,
84
+ "hitmap": SCRIPTS_HITMAP,
85
+ "inspector": SCRIPTS_INSPECTOR,
86
+ "labels": SCRIPTS_LABELS,
87
+ "legend_drag": SCRIPTS_LEGEND_DRAG,
88
+ "modals": SCRIPTS_MODALS,
89
+ "overlays": SCRIPTS_OVERLAYS,
90
+ "panel_drag": SCRIPTS_PANEL_DRAG,
91
+ "panel_position": SCRIPTS_PANEL_POSITION,
92
+ "selection": SCRIPTS_SELECTION,
93
+ "tabs": SCRIPTS_TABS,
94
+ "view_mode": SCRIPTS_VIEW_MODE,
95
+ "zoom": SCRIPTS_ZOOM,
96
+ }
97
+
98
+
99
+ __all__ = [
100
+ "SCRIPTS",
101
+ "SCRIPTS_API",
102
+ "SCRIPTS_COLORS",
103
+ "SCRIPTS_CORE",
104
+ "SCRIPTS_DEBUG_SNAPSHOT",
105
+ "SCRIPTS_ELEMENT_EDITOR",
106
+ "SCRIPTS_FILES",
107
+ "SCRIPTS_HITMAP",
108
+ "SCRIPTS_INSPECTOR",
109
+ "SCRIPTS_LABELS",
110
+ "SCRIPTS_LEGEND_DRAG",
111
+ "SCRIPTS_MODALS",
112
+ "SCRIPTS_OVERLAYS",
113
+ "SCRIPTS_PANEL_DRAG",
114
+ "SCRIPTS_PANEL_POSITION",
115
+ "SCRIPTS_SELECTION",
116
+ "SCRIPTS_TABS",
117
+ "SCRIPTS_VIEW_MODE",
118
+ "SCRIPTS_ZOOM",
119
+ "get_all_scripts",
120
+ ]