figrecipe 0.5.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 (189) hide show
  1. figrecipe/__init__.py +220 -819
  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 +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  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/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.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"]
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ HTML/CSS/JS template builder for figure editor.
5
+
6
+ Uses YAML-compatible flattened key names from to_subplots_kwargs() as the
7
+ single source of truth. No custom key mapping is needed since all keys
8
+ now match the HTML input IDs directly.
9
+ """
10
+
11
+ import base64
12
+ import json
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, Dict, Tuple
16
+
17
+ from ._html import HTML_TEMPLATE
18
+ from ._scripts import SCRIPTS
19
+ from ._styles import STYLES
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
+
33
+
34
+ def build_html_template(
35
+ image_base64: str,
36
+ bboxes: Dict[str, Any],
37
+ color_map: Dict[str, Any],
38
+ style: Dict[str, Any],
39
+ overrides: Dict[str, Any],
40
+ img_size: Tuple[int, int],
41
+ style_name: str = "SCITEX",
42
+ hot_reload: bool = False,
43
+ dark_mode: bool = False,
44
+ ) -> str:
45
+ """
46
+ Build complete HTML template for figure editor.
47
+
48
+ Style keys are expected to be YAML-compatible flattened names that
49
+ match the HTML input IDs directly (e.g., 'fonts_axis_label_pt').
50
+
51
+ Parameters
52
+ ----------
53
+ image_base64 : str
54
+ Base64-encoded preview image.
55
+ bboxes : dict
56
+ Element bounding boxes.
57
+ color_map : dict
58
+ Hitmap color-to-element mapping.
59
+ style : dict
60
+ Base style configuration with YAML-compatible keys.
61
+ overrides : dict
62
+ Current style overrides with YAML-compatible keys.
63
+ img_size : tuple
64
+ (width, height) of preview image.
65
+ style_name : str
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.
71
+
72
+ Returns
73
+ -------
74
+ str
75
+ Complete HTML document.
76
+ """
77
+ # Merge style and overrides for initial values
78
+ # Keys should already match HTML input IDs (YAML-compatible flattened)
79
+ initial_values = {**style, **overrides}
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
+
129
+ # Inject data into template
130
+ html = HTML_TEMPLATE
131
+ html = html.replace("/* STYLES_PLACEHOLDER */", STYLES)
132
+ html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS + hot_reload_script)
133
+ html = html.replace("IMAGE_BASE64_PLACEHOLDER", image_base64)
134
+ html = html.replace("BBOXES_PLACEHOLDER", json.dumps(bboxes))
135
+ html = html.replace("COLOR_MAP_PLACEHOLDER", json.dumps(color_map))
136
+ html = html.replace("INITIAL_VALUES_PLACEHOLDER", json.dumps(initial_values))
137
+ html = html.replace("IMG_WIDTH_PLACEHOLDER", str(img_size[0]))
138
+ html = html.replace("IMG_HEIGHT_PLACEHOLDER", str(img_size[1]))
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)
148
+
149
+ return html
150
+
151
+
152
+ __all__ = ["build_html_template"]