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
@@ -1,2778 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- JavaScript for figure editor.
5
- """
6
-
7
- SCRIPTS = """
8
- // State
9
- let currentBboxes = initialBboxes;
10
- let colorMap = initialColorMap;
11
- let callsData = {}; // Recorded calls with signatures
12
- let selectedElement = null;
13
- let hitmapLoaded = false;
14
- let hitmapCtx = null;
15
- let hitmapImg = null;
16
- let updateTimeout = null;
17
- let currentImgWidth = imgWidth; // Track current preview dimensions
18
- let currentImgHeight = imgHeight;
19
- let hitmapVisible = true; // Hitmap overlay visibility (default visible for development)
20
- const UPDATE_DEBOUNCE = 500; // ms
21
-
22
- // Overlapping element cycling state
23
- let lastClickPosition = null;
24
- let overlappingElements = [];
25
- let cycleIndex = 0;
26
- let hoveredElement = null; // Track currently hovered element for click priority
27
-
28
- // View mode: 'all' shows all properties, 'selected' shows only element-specific
29
- let viewMode = 'all';
30
-
31
- // Initialize
32
- document.addEventListener('DOMContentLoaded', function() {
33
- initializeValues();
34
- initializeEventListeners();
35
- loadHitmap();
36
- loadLabels(); // Load current axis labels
37
-
38
- // Update hit regions on window resize
39
- window.addEventListener('resize', updateHitRegions);
40
-
41
- // Update hit regions when preview image loads
42
- const previewImg = document.getElementById('preview-image');
43
- previewImg.addEventListener('load', updateHitRegions);
44
-
45
- // Initialize hit regions visibility state
46
- const overlay = document.getElementById('hitregion-overlay');
47
- const btn = document.getElementById('btn-show-hitmap');
48
-
49
- if (hitmapVisible) {
50
- if (overlay) overlay.classList.add('visible');
51
- if (btn) {
52
- btn.classList.add('active');
53
- btn.textContent = 'Hide Hit Regions';
54
- }
55
- } else {
56
- // Hover-only mode when hidden
57
- if (overlay) overlay.classList.add('hover-mode');
58
- }
59
-
60
- // Always draw hit regions for hover detection
61
- setTimeout(() => drawHitRegions(), 100);
62
- });
63
-
64
- // Theme values are passed from server via initialValues
65
- // These come from the applied theme (SCITEX, MATPLOTLIB, etc.)
66
- // initialValues is populated by the server from the loaded style preset
67
-
68
- // Store original theme defaults for comparison
69
- const themeDefaults = {...initialValues};
70
-
71
- // Initialize form values and placeholders from applied theme
72
- function initializeValues() {
73
- // initialValues contains the theme's default values from the server
74
- // These are the actual values from the applied style preset (not hardcoded)
75
-
76
- for (const [key, value] of Object.entries(initialValues)) {
77
- const element = document.getElementById(key);
78
- if (element) {
79
- if (element.type === 'checkbox') {
80
- element.checked = Boolean(value);
81
- } else if (element.type === 'range') {
82
- element.value = value;
83
- const valueSpan = document.getElementById(key + '_value');
84
- if (valueSpan) valueSpan.textContent = value;
85
- } else {
86
- // Set the value
87
- element.value = value;
88
- // Set placeholder to show theme default (visible when field is cleared)
89
- if (element.type === 'number' || element.type === 'text') {
90
- element.placeholder = value;
91
- }
92
- }
93
- }
94
- }
95
-
96
- // Log applied theme info
97
- const styleNameEl = document.getElementById('style-name');
98
- if (styleNameEl) {
99
- console.log('Applied theme:', styleNameEl.textContent);
100
- }
101
- }
102
-
103
- // Check if a field value differs from the theme default
104
- function updateModifiedState(element) {
105
- const key = element.id;
106
- const defaultValue = themeDefaults[key];
107
- const formRow = element.closest('.form-row');
108
- if (!formRow || defaultValue === undefined) return;
109
-
110
- let currentValue;
111
- if (element.type === 'checkbox') {
112
- currentValue = element.checked;
113
- } else if (element.type === 'number') {
114
- currentValue = parseFloat(element.value);
115
- } else {
116
- currentValue = element.value;
117
- }
118
-
119
- // Compare values (handle type conversion)
120
- const isModified = String(currentValue) !== String(defaultValue);
121
- formRow.classList.toggle('value-modified', isModified);
122
- }
123
-
124
- // Update all modified states
125
- function updateAllModifiedStates() {
126
- const inputs = document.querySelectorAll('input, select');
127
- inputs.forEach(input => {
128
- if (input.id && input.id !== 'dark-mode-toggle') {
129
- updateModifiedState(input);
130
- }
131
- });
132
- }
133
-
134
- // Initialize event listeners
135
- function initializeEventListeners() {
136
- // Preview image click for element selection
137
- const previewImg = document.getElementById('preview-image');
138
- previewImg.addEventListener('click', handlePreviewClick);
139
-
140
- // SVG overlay click - deselect when clicking on empty area (not on a shape)
141
- const hitregionOverlay = document.getElementById('hitregion-overlay');
142
- hitregionOverlay.addEventListener('click', function(event) {
143
- // Only clear if clicking directly on the SVG (not on a shape inside it)
144
- if (event.target === hitregionOverlay) {
145
- clearSelection();
146
- }
147
- });
148
-
149
- // Selection overlay click - same behavior
150
- const selectionOverlay = document.getElementById('selection-overlay');
151
- selectionOverlay.addEventListener('click', function(event) {
152
- if (event.target === selectionOverlay) {
153
- clearSelection();
154
- }
155
- });
156
-
157
- // Dark mode toggle
158
- const darkModeToggle = document.getElementById('dark-mode-toggle');
159
- darkModeToggle.addEventListener('change', function() {
160
- document.documentElement.setAttribute('data-theme', this.checked ? 'dark' : 'light');
161
- scheduleUpdate();
162
- });
163
-
164
- // Form inputs - auto update on change
165
- const inputs = document.querySelectorAll('input, select');
166
- inputs.forEach(input => {
167
- if (input.id === 'dark-mode-toggle') return;
168
-
169
- // Update modified state and trigger preview update
170
- input.addEventListener('change', function() {
171
- updateModifiedState(this);
172
- scheduleUpdate();
173
- });
174
- if (input.type === 'number' || input.type === 'text') {
175
- input.addEventListener('input', function() {
176
- updateModifiedState(this);
177
- scheduleUpdate();
178
- });
179
- }
180
-
181
- // Range slider value display
182
- if (input.type === 'range') {
183
- input.addEventListener('input', function() {
184
- const valueSpan = document.getElementById(this.id + '_value');
185
- if (valueSpan) valueSpan.textContent = this.value;
186
- updateModifiedState(this);
187
- });
188
- }
189
- });
190
-
191
- // Buttons
192
- document.getElementById('btn-refresh').addEventListener('click', updatePreview);
193
- document.getElementById('btn-reset').addEventListener('click', resetValues);
194
- document.getElementById('btn-save').addEventListener('click', saveOverrides);
195
- document.getElementById('btn-restore').addEventListener('click', restoreOriginal);
196
- document.getElementById('btn-show-hitmap').addEventListener('click', toggleHitmapOverlay);
197
-
198
- // Download dropdown buttons
199
- initializeDownloadDropdown();
200
-
201
- // Label input handlers
202
- initializeLabelInputs();
203
-
204
- // View mode toggle buttons (legacy - replaced by tabs)
205
- const btnAll = document.getElementById('btn-show-all');
206
- const btnSelected = document.getElementById('btn-show-selected');
207
- if (btnAll) btnAll.addEventListener('click', () => setViewMode('all'));
208
- if (btnSelected) btnSelected.addEventListener('click', () => setViewMode('selected'));
209
-
210
- // Tab navigation
211
- document.getElementById('tab-figure').addEventListener('click', () => switchTab('figure'));
212
- document.getElementById('tab-axis').addEventListener('click', () => switchTab('axis'));
213
- document.getElementById('tab-element').addEventListener('click', () => switchTab('element'));
214
-
215
- // Theme modal handlers
216
- initializeThemeModal();
217
-
218
- // Check initial override status
219
- checkOverrideStatus();
220
-
221
- // Check modified states after initial values are set
222
- setTimeout(updateAllModifiedStates, 100);
223
- }
224
-
225
- // Load hitmap for element detection
226
- async function loadHitmap() {
227
- try {
228
- // Load hitmap and calls data in parallel
229
- const [hitmapResponse, callsResponse] = await Promise.all([
230
- fetch('/hitmap'),
231
- fetch('/calls')
232
- ]);
233
- const data = await hitmapResponse.json();
234
- callsData = await callsResponse.json();
235
-
236
- colorMap = data.color_map;
237
- console.log('Loaded colorMap:', Object.keys(colorMap));
238
-
239
- // Create canvas for hitmap
240
- const canvas = document.getElementById('hitmap-canvas');
241
- hitmapCtx = canvas.getContext('2d', { willReadFrequently: true });
242
-
243
- // Load hitmap image
244
- hitmapImg = new Image();
245
- hitmapImg.onload = function() {
246
- canvas.width = hitmapImg.width;
247
- canvas.height = hitmapImg.height;
248
- hitmapCtx.drawImage(hitmapImg, 0, 0);
249
- hitmapLoaded = true;
250
- console.log('Hitmap loaded:', hitmapImg.width, 'x', hitmapImg.height);
251
-
252
- // Update overlay image source
253
- const overlay = document.getElementById('hitmap-overlay');
254
- overlay.src = hitmapImg.src;
255
- };
256
- hitmapImg.src = 'data:image/png;base64,' + data.image;
257
- } catch (error) {
258
- console.error('Failed to load hitmap:', error);
259
- }
260
- }
261
-
262
- // Load current axis labels from server
263
- async function loadLabels() {
264
- try {
265
- const response = await fetch('/get_labels');
266
- const labels = await response.json();
267
-
268
- // Populate label input fields
269
- const titleInput = document.getElementById('label_title');
270
- const xlabelInput = document.getElementById('label_xlabel');
271
- const ylabelInput = document.getElementById('label_ylabel');
272
- const suptitleInput = document.getElementById('label_suptitle');
273
-
274
- if (titleInput) titleInput.value = labels.title || '';
275
- if (xlabelInput) xlabelInput.value = labels.xlabel || '';
276
- if (ylabelInput) ylabelInput.value = labels.ylabel || '';
277
- if (suptitleInput) suptitleInput.value = labels.suptitle || '';
278
-
279
- console.log('Loaded labels:', labels);
280
- } catch (error) {
281
- console.error('Failed to load labels:', error);
282
- }
283
- }
284
-
285
- // Update axis label on server
286
- async function updateLabel(labelType, text) {
287
- console.log(`Updating ${labelType} to: "${text}"`);
288
-
289
- document.body.classList.add('loading');
290
-
291
- try {
292
- const response = await fetch('/update_label', {
293
- method: 'POST',
294
- headers: { 'Content-Type': 'application/json' },
295
- body: JSON.stringify({
296
- label_type: labelType,
297
- text: text
298
- })
299
- });
300
-
301
- const data = await response.json();
302
-
303
- if (data.success) {
304
- // Update preview image
305
- const img = document.getElementById('preview-image');
306
- img.src = 'data:image/png;base64,' + data.image;
307
-
308
- // Update dimensions
309
- if (data.img_size) {
310
- currentImgWidth = data.img_size.width;
311
- currentImgHeight = data.img_size.height;
312
- }
313
-
314
- // Update bboxes
315
- currentBboxes = data.bboxes;
316
-
317
- // Redraw hit regions
318
- updateHitRegions();
319
-
320
- console.log('Label updated successfully');
321
- } else {
322
- console.error('Label update failed:', data.error);
323
- alert('Update failed: ' + data.error);
324
- }
325
- } catch (error) {
326
- console.error('Label update failed:', error);
327
- alert('Update failed: ' + error.message);
328
- }
329
-
330
- document.body.classList.remove('loading');
331
- }
332
-
333
- // Toggle hit regions overlay visibility mode
334
- function toggleHitmapOverlay() {
335
- hitmapVisible = !hitmapVisible;
336
- const overlay = document.getElementById('hitregion-overlay');
337
- const btn = document.getElementById('btn-show-hitmap');
338
-
339
- if (hitmapVisible) {
340
- // Show all hit regions
341
- overlay.classList.add('visible');
342
- overlay.classList.remove('hover-mode');
343
- btn.classList.add('active');
344
- btn.textContent = 'Hide Hit Regions';
345
- } else {
346
- // Hover-only mode: hit regions visible only on hover
347
- overlay.classList.remove('visible');
348
- overlay.classList.add('hover-mode');
349
- btn.classList.remove('active');
350
- btn.textContent = 'Show Hit Regions';
351
- }
352
- // Always draw hit regions for hover detection
353
- drawHitRegions();
354
- }
355
-
356
- // Draw hit region shapes from bboxes (polylines for lines, rectangles for others)
357
- function drawHitRegions() {
358
- const overlay = document.getElementById('hitregion-overlay');
359
- overlay.innerHTML = '';
360
-
361
- const img = document.getElementById('preview-image');
362
- const imgRect = img.getBoundingClientRect();
363
- const wrapperRect = img.parentElement.getBoundingClientRect();
364
-
365
- // Calculate offset of image within wrapper
366
- const offsetX = imgRect.left - wrapperRect.left;
367
- const offsetY = imgRect.top - wrapperRect.top;
368
-
369
- // Scale factors from image natural size to display size
370
- const scaleX = imgRect.width / img.naturalWidth;
371
- const scaleY = imgRect.height / img.naturalHeight;
372
-
373
- console.log('Drawing hit regions:', Object.keys(currentBboxes).length, 'elements');
374
- console.log('Image natural:', img.naturalWidth, 'x', img.naturalHeight);
375
- console.log('Image display:', imgRect.width, 'x', imgRect.height);
376
- console.log('Scale:', scaleX, scaleY);
377
-
378
- // Sort by z-order: background first, foreground last (so foreground is on top)
379
- // Higher z-order = drawn later = on top = can be clicked first
380
- const zOrderPriority = {
381
- 'axes': 0, // Background - lowest priority, drawn first
382
- 'spine': 1,
383
- 'fill': 2,
384
- 'bar': 3,
385
- 'xticks': 4,
386
- 'yticks': 4,
387
- 'line': 5, // Foreground
388
- 'scatter': 6,
389
- 'title': 7,
390
- 'xlabel': 7,
391
- 'ylabel': 7,
392
- 'legend': 8, // Topmost - highest priority, drawn last
393
- };
394
-
395
- // Convert to array, filter, and sort by z-order
396
- // Skip 'axes' type - users should click on individual elements or spines instead
397
- const sortedEntries = Object.entries(currentBboxes)
398
- .filter(([key, bbox]) => key !== '_meta' && bbox && typeof bbox.x !== 'undefined' && bbox.type !== 'axes')
399
- .sort((a, b) => (zOrderPriority[a[1].type] || 5) - (zOrderPriority[b[1].type] || 5));
400
-
401
- // Draw shapes for each bbox (in z-order)
402
- for (const [key, bbox] of sortedEntries) {
403
-
404
- // Create group for shape and label
405
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
406
- group.setAttribute('class', 'hitregion-group');
407
- group.setAttribute('data-key', key);
408
-
409
- let shape;
410
- let labelX, labelY;
411
-
412
- // Use polyline for lines with points, circles for scatter, rectangle for others
413
- if (bbox.type === 'line' && bbox.points && bbox.points.length > 1) {
414
- // Create polyline from points for lines
415
- const points = bbox.points.map(pt => {
416
- const x = offsetX + pt[0] * scaleX;
417
- const y = offsetY + pt[1] * scaleY;
418
- return `${x},${y}`;
419
- }).join(' ');
420
-
421
- shape = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
422
- shape.setAttribute('points', points);
423
- shape.setAttribute('class', 'hitregion-polyline');
424
- shape.setAttribute('data-key', key);
425
- // Set element color as CSS custom property for hover effect
426
- if (bbox.original_color) {
427
- shape.style.setProperty('--element-color', bbox.original_color);
428
- }
429
-
430
- // Label position at first point
431
- const firstPt = bbox.points[0];
432
- labelX = offsetX + firstPt[0] * scaleX + 5;
433
- labelY = offsetY + firstPt[1] * scaleY - 5;
434
- } else if (bbox.type === 'scatter' && bbox.points && bbox.points.length > 0) {
435
- // Create circles at each scatter point
436
- shape = document.createElementNS('http://www.w3.org/2000/svg', 'g');
437
- shape.setAttribute('class', 'scatter-group');
438
- shape.setAttribute('data-key', key);
439
- // Set element color as CSS custom property for hover effect
440
- if (bbox.original_color) {
441
- shape.style.setProperty('--element-color', bbox.original_color);
442
- }
443
-
444
- const hitRadius = 5; // Hit region radius in display pixels
445
- const allCircles = []; // Track all circles for group hover effect
446
-
447
- bbox.points.forEach((pt, idx) => {
448
- const cx = offsetX + pt[0] * scaleX;
449
- const cy = offsetY + pt[1] * scaleY;
450
-
451
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
452
- circle.setAttribute('cx', cx);
453
- circle.setAttribute('cy', cy);
454
- circle.setAttribute('r', hitRadius);
455
- circle.setAttribute('class', 'hitregion-circle');
456
- circle.setAttribute('data-key', key);
457
- circle.setAttribute('data-point-index', idx);
458
-
459
- allCircles.push(circle);
460
- shape.appendChild(circle);
461
- });
462
-
463
- // Add event handlers to the scatter group (not individual circles)
464
- // This ensures all circles highlight together
465
- shape.addEventListener('mouseenter', () => {
466
- handleHitRegionHover(key, bbox);
467
- // Add hovered class to all circles for visual effect
468
- allCircles.forEach(c => c.classList.add('hovered'));
469
- shape.classList.add('hovered');
470
- });
471
- shape.addEventListener('mouseleave', () => {
472
- handleHitRegionLeave();
473
- // Remove hovered class from all circles
474
- allCircles.forEach(c => c.classList.remove('hovered'));
475
- shape.classList.remove('hovered');
476
- });
477
- shape.addEventListener('click', (e) => handleHitRegionClick(e, key, bbox));
478
-
479
- // Label position at first point
480
- const firstPt = bbox.points[0];
481
- labelX = offsetX + firstPt[0] * scaleX + 5;
482
- labelY = offsetY + firstPt[1] * scaleY - 5;
483
- } else {
484
- // Determine region type for styling (rectangles only)
485
- let regionClass = 'hitregion-rect';
486
- if (bbox.type === 'line' || bbox.type === 'scatter') {
487
- regionClass += ' line-region';
488
- } else if (bbox.type === 'title' || bbox.type === 'xlabel' || bbox.type === 'ylabel' ||
489
- bbox.type === 'suptitle' || bbox.type === 'supxlabel' || bbox.type === 'supylabel') {
490
- regionClass += ' text-region';
491
- } else if (bbox.type === 'legend') {
492
- regionClass += ' legend-region';
493
- } else if (bbox.type === 'xticks' || bbox.type === 'yticks') {
494
- regionClass += ' tick-region';
495
- }
496
-
497
- // Create rectangle for other elements
498
- const x = offsetX + bbox.x * scaleX;
499
- const y = offsetY + bbox.y * scaleY;
500
- const width = bbox.width * scaleX;
501
- const height = bbox.height * scaleY;
502
-
503
- shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
504
- shape.setAttribute('x', x);
505
- shape.setAttribute('y', y);
506
- shape.setAttribute('width', Math.max(width, 5));
507
- shape.setAttribute('height', Math.max(height, 5));
508
- shape.setAttribute('class', regionClass);
509
- shape.setAttribute('data-key', key);
510
- // Set element color as CSS custom property for hover effect
511
- if (bbox.original_color) {
512
- shape.style.setProperty('--element-color', bbox.original_color);
513
- }
514
-
515
- labelX = x + 2;
516
- labelY = y - 3;
517
- }
518
-
519
- // Add hover and click handlers
520
- shape.addEventListener('mouseenter', () => handleHitRegionHover(key, bbox));
521
- shape.addEventListener('mouseleave', () => handleHitRegionLeave());
522
- shape.addEventListener('click', (e) => handleHitRegionClick(e, key, bbox));
523
-
524
- group.appendChild(shape);
525
-
526
- // Create label - use colorMap type if available (for boxplot, violin detection)
527
- const colorMapInfo = colorMap[key] || {};
528
- const elemType = colorMapInfo.type || bbox.type || 'element';
529
- const elemLabel = colorMapInfo.label || bbox.label || key;
530
- const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
531
- label.setAttribute('x', labelX);
532
- label.setAttribute('y', labelY);
533
- label.setAttribute('class', 'hitregion-label');
534
- label.textContent = `${elemType}: ${elemLabel}`;
535
- group.appendChild(label);
536
-
537
- overlay.appendChild(group);
538
- }
539
-
540
- // Also draw colorMap elements (from hitmap)
541
- for (const [key, info] of Object.entries(colorMap)) {
542
- // Skip if already in bboxes
543
- if (currentBboxes[key]) continue;
544
-
545
- // ColorMap entries without bboxes - show as small indicator
546
- console.log('ColorMap element without bbox:', key, info);
547
- }
548
- }
549
-
550
- // Handle hover on hit region
551
- function handleHitRegionHover(key, bbox) {
552
- // Merge colorMap info for correct type (boxplot, violin, etc.)
553
- const colorMapInfo = colorMap[key] || {};
554
- hoveredElement = { key, ...bbox, ...colorMapInfo };
555
-
556
- const info = document.getElementById('selected-info');
557
- const elemType = colorMapInfo.type || bbox.type || 'element';
558
- const elemLabel = colorMapInfo.label || bbox.label || key;
559
- const callId = colorMapInfo.call_id;
560
-
561
- // Check if this element is part of a group
562
- if (callId) {
563
- const groupElements = findGroupElements(callId);
564
- if (groupElements.length > 1) {
565
- info.textContent = `Hover: ${elemType} group (${callId}) - ${groupElements.length} elements`;
566
- // Highlight all group elements on hover
567
- highlightGroupElements(groupElements.map(e => e.key));
568
- return;
569
- }
570
- }
571
-
572
- info.textContent = `Hover: ${elemType} (${elemLabel})`;
573
- }
574
-
575
- // Highlight all elements in a group (for hover effect)
576
- function highlightGroupElements(keys) {
577
- // Add hover class to all matching hit regions
578
- keys.forEach(key => {
579
- const hitRegion = document.querySelector(`[data-key="${key}"]`);
580
- if (hitRegion) {
581
- hitRegion.classList.add('group-hovered');
582
- }
583
- });
584
- }
585
-
586
- // Handle leaving hit region
587
- function handleHitRegionLeave() {
588
- hoveredElement = null;
589
-
590
- // Clear all group hover highlights
591
- document.querySelectorAll('.group-hovered').forEach(el => {
592
- el.classList.remove('group-hovered');
593
- });
594
-
595
- const info = document.getElementById('selected-info');
596
- if (selectedElement) {
597
- // Show group info if selected element is part of a group
598
- if (selectedElement.groupElements) {
599
- const callId = selectedElement.call_id || selectedElement.label;
600
- info.textContent = `Selected: ${selectedElement.type} group (${callId}) - ${selectedElement.groupElements.length} elements`;
601
- } else {
602
- info.textContent = `Selected: ${selectedElement.type} (${selectedElement.label || selectedElement.key})`;
603
- }
604
- } else {
605
- info.textContent = 'Click on an element to select it';
606
- }
607
- }
608
-
609
- // Handle click on hit region with Alt+Click cycling support
610
- function handleHitRegionClick(event, key, bbox) {
611
- event.stopPropagation();
612
- event.preventDefault(); // Prevent browser default Alt+Click behavior
613
-
614
- // Merge colorMap info (which has correct type like 'boxplot', 'violin')
615
- // with bbox info (which has geometry)
616
- const colorMapInfo = colorMap[key] || {};
617
- const element = { key, ...bbox, ...colorMapInfo };
618
- console.log('Hit region click:', key, 'altKey:', event.altKey);
619
-
620
- if (event.altKey) {
621
- // Alt+Click: cycle through overlapping elements at this position
622
- const clickPos = { x: event.clientX, y: event.clientY };
623
-
624
- // Check if same position as last click
625
- const samePosition = lastClickPosition &&
626
- Math.abs(lastClickPosition.x - clickPos.x) < 5 &&
627
- Math.abs(lastClickPosition.y - clickPos.y) < 5;
628
-
629
- if (samePosition && overlappingElements.length > 1) {
630
- // Cycle to next overlapping element
631
- cycleIndex = (cycleIndex + 1) % overlappingElements.length;
632
- selectElement(overlappingElements[cycleIndex]);
633
- } else {
634
- // Find all overlapping elements at this position
635
- overlappingElements = findOverlappingElements(clickPos);
636
- cycleIndex = 0;
637
- lastClickPosition = clickPos;
638
-
639
- if (overlappingElements.length > 0) {
640
- selectElement(overlappingElements[0]);
641
- } else {
642
- selectElement(element);
643
- }
644
- }
645
- } else {
646
- // Normal click: select the hovered element (topmost in z-order)
647
- selectElement(element);
648
- // Reset cycling state
649
- lastClickPosition = null;
650
- overlappingElements = [];
651
- cycleIndex = 0;
652
- }
653
- }
654
-
655
- // Find all elements overlapping at a given screen position
656
- function findOverlappingElements(screenPos) {
657
- const img = document.getElementById('preview-image');
658
- const imgRect = img.getBoundingClientRect();
659
-
660
- // Convert to image coordinates
661
- const imgX = (screenPos.x - imgRect.left) * (img.naturalWidth / imgRect.width);
662
- const imgY = (screenPos.y - imgRect.top) * (img.naturalHeight / imgRect.height);
663
-
664
- const overlapping = [];
665
-
666
- // Check all bboxes for overlap
667
- for (const [key, bbox] of Object.entries(currentBboxes)) {
668
- if (key === '_meta') continue;
669
-
670
- // Check if point is inside bbox
671
- if (imgX >= bbox.x && imgX <= bbox.x + bbox.width &&
672
- imgY >= bbox.y && imgY <= bbox.y + bbox.height) {
673
- overlapping.push({ key, ...bbox });
674
- }
675
-
676
- // For lines with points, check proximity
677
- if (bbox.points && bbox.points.length > 1) {
678
- for (const pt of bbox.points) {
679
- const dist = Math.sqrt(Math.pow(imgX - pt[0], 2) + Math.pow(imgY - pt[1], 2));
680
- if (dist < 15) { // 15 pixel tolerance
681
- if (!overlapping.find(e => e.key === key)) {
682
- overlapping.push({ key, ...bbox });
683
- }
684
- break;
685
- }
686
- }
687
- }
688
- }
689
-
690
- // Sort by z-order priority (foreground elements first)
691
- // Priority: line > scatter > legend > text > ticks > spines > axes
692
- const priority = { 'line': 0, 'scatter': 1, 'legend': 2, 'title': 3, 'xlabel': 4, 'ylabel': 4,
693
- 'xticks': 5, 'yticks': 5, 'spine': 6, 'bar': 3, 'fill': 4, 'axes': 7 };
694
- overlapping.sort((a, b) => (priority[a.type] || 5) - (priority[b.type] || 5));
695
-
696
- return overlapping;
697
- }
698
-
699
- // Update hit regions when image loads or resizes
700
- function updateHitRegions() {
701
- // Always draw hit regions (for hover detection in both modes)
702
- drawHitRegions();
703
- }
704
-
705
- // Handle click on preview image
706
- function handlePreviewClick(event) {
707
- const img = event.target;
708
- const rect = img.getBoundingClientRect();
709
-
710
- // Get click position relative to image
711
- const x = event.clientX - rect.left;
712
- const y = event.clientY - rect.top;
713
-
714
- // Scale to image coordinates
715
- const scaleX = img.naturalWidth / rect.width;
716
- const scaleY = img.naturalHeight / rect.height;
717
- const imgX = Math.floor(x * scaleX);
718
- const imgY = Math.floor(y * scaleY);
719
-
720
- // Find element at position
721
- const element = getElementAtPosition(imgX, imgY);
722
-
723
- if (element) {
724
- selectElement(element);
725
- } else {
726
- clearSelection();
727
- }
728
- }
729
-
730
- // Get element at image position using hitmap
731
- function getElementAtPosition(imgX, imgY) {
732
- if (!hitmapLoaded) {
733
- console.log('Hitmap not loaded yet');
734
- return null;
735
- }
736
-
737
- // Scale to hitmap coordinates (use current dimensions)
738
- const scaleX = hitmapImg.width / currentImgWidth;
739
- const scaleY = hitmapImg.height / currentImgHeight;
740
- const hitmapX = Math.floor(imgX * scaleX);
741
- const hitmapY = Math.floor(imgY * scaleY);
742
-
743
- console.log(`Click: img(${imgX},${imgY}) -> hitmap(${hitmapX},${hitmapY}), scale(${scaleX.toFixed(2)},${scaleY.toFixed(2)})`);
744
- console.log(`Hitmap size: ${hitmapImg.width}x${hitmapImg.height}, Current img: ${currentImgWidth}x${currentImgHeight}`);
745
-
746
- // Get pixel color
747
- try {
748
- const pixel = hitmapCtx.getImageData(hitmapX, hitmapY, 1, 1).data;
749
- const [r, g, b, a] = pixel;
750
-
751
- console.log(`Pixel color: rgb(${r},${g},${b}) alpha=${a}`);
752
-
753
- // Skip transparent or background
754
- if (a < 128) {
755
- console.log('Skipping: transparent pixel');
756
- return null;
757
- }
758
- if (r === 26 && g === 26 && b === 26) {
759
- console.log('Skipping: background color');
760
- return null;
761
- }
762
- if (r === 64 && g === 64 && b === 64) {
763
- console.log('Skipping: axes color');
764
- return null;
765
- }
766
-
767
- // Find element by RGB color
768
- for (const [key, info] of Object.entries(colorMap)) {
769
- if (info.rgb[0] === r && info.rgb[1] === g && info.rgb[2] === b) {
770
- console.log(`Found element via hitmap: ${key} (${info.type})`);
771
- return { key, ...info };
772
- }
773
- }
774
- console.log('No matching element in colorMap for this color');
775
- } catch (error) {
776
- console.error('Hitmap pixel read error:', error);
777
- }
778
-
779
- // Fallback: check bboxes
780
- console.log('Falling back to bbox detection...');
781
- for (const [key, bbox] of Object.entries(currentBboxes)) {
782
- if (key === '_meta') continue;
783
- if (imgX >= bbox.x && imgX <= bbox.x + bbox.width &&
784
- imgY >= bbox.y && imgY <= bbox.y + bbox.height) {
785
- console.log(`Found element via bbox: ${key}`);
786
- return { key, ...bbox };
787
- }
788
- }
789
-
790
- console.log('No element found');
791
- return null;
792
- }
793
-
794
- // Find all elements belonging to the same logical group (same call_id)
795
- function findGroupElements(callId) {
796
- if (!callId) return [];
797
-
798
- const groupElements = [];
799
- for (const [key, info] of Object.entries(colorMap)) {
800
- if (info.call_id === callId) {
801
- groupElements.push({ key, ...info });
802
- }
803
- }
804
- return groupElements;
805
- }
806
-
807
- // Select an element (and its logical group if applicable)
808
- function selectElement(element) {
809
- selectedElement = element;
810
-
811
- // Find all elements in the same logical group
812
- const callId = element.call_id || element.label; // Fallback to label for backwards compat
813
- const groupElements = findGroupElements(callId);
814
-
815
- // Store group info for multi-selection rendering
816
- selectedElement.groupElements = groupElements.length > 1 ? groupElements : null;
817
-
818
- // Update info display
819
- const info = document.getElementById('selected-info');
820
- if (groupElements.length > 1) {
821
- info.textContent = `Selected: ${element.type} group (${callId}) - ${groupElements.length} elements`;
822
- } else {
823
- info.textContent = `Selected: ${element.type} (${element.label || element.key})`;
824
- }
825
-
826
- // Draw selection overlay (handles group selection)
827
- drawSelection(element.key);
828
-
829
- // Auto-switch to appropriate tab based on element type
830
- autoSwitchTab(element.type);
831
-
832
- // Update tab hints
833
- updateTabHints();
834
-
835
- // Sync properties panel to show relevant section
836
- syncPropertiesToElement(element);
837
- }
838
-
839
- // Sync properties panel to selected element
840
- function syncPropertiesToElement(element) {
841
- // In 'selected' mode, skip section management - only show call properties
842
- if (viewMode === 'selected') {
843
- // Just show call properties, sections are hidden
844
- showDynamicCallProperties(element);
845
- return;
846
- }
847
-
848
- // Map element types to section IDs (for 'all' mode)
849
- const sectionMap = {
850
- 'axes': 'section-dimensions',
851
- 'line': 'section-lines',
852
- 'scatter': 'section-markers',
853
- 'bar': 'section-lines',
854
- 'fill': 'section-lines',
855
- 'boxplot': 'section-boxplot',
856
- 'violin': 'section-violin',
857
- 'title': 'section-fonts',
858
- 'xlabel': 'section-fonts',
859
- 'ylabel': 'section-fonts',
860
- 'xticks': 'section-ticks',
861
- 'yticks': 'section-ticks',
862
- 'legend': 'section-legend',
863
- 'spine': 'section-dimensions',
864
- };
865
-
866
- // Get the relevant section ID
867
- const sectionId = sectionMap[element.type] || 'section-dimensions';
868
-
869
- // Close all sections and remove highlights (accordion behavior)
870
- document.querySelectorAll('.section').forEach(section => {
871
- section.classList.remove('section-highlighted');
872
- // Close all sections except Download (which should stay open)
873
- if (section.id && section.id !== 'section-download') {
874
- section.removeAttribute('open');
875
- }
876
- });
877
-
878
- // Find and highlight the relevant section
879
- const section = document.getElementById(sectionId);
880
- if (section) {
881
- // Open the section
882
- section.setAttribute('open', '');
883
- // Add highlight class
884
- section.classList.add('section-highlighted');
885
- // Scroll to section with small delay to allow animation
886
- setTimeout(() => {
887
- section.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
888
- }, 50);
889
- }
890
-
891
- // Update displayed values for the selected element type
892
- updateElementProperties(element);
893
- }
894
-
895
- // Update property values for selected element
896
- function updateElementProperties(element) {
897
- // Clear previous field highlights
898
- document.querySelectorAll('.form-row').forEach(row => {
899
- row.classList.remove('field-highlighted');
900
- });
901
-
902
- // Map element types to relevant form field IDs
903
- const fieldMap = {
904
- 'line': ['lines_trace_mm', 'lines_errorbar_mm', 'lines_errorbar_cap_mm'],
905
- 'scatter': ['markers_size_mm', 'markers_scatter_mm', 'markers_edge_width_mm'],
906
- 'bar': ['lines_trace_mm'],
907
- 'fill': ['lines_trace_mm'],
908
- 'boxplot': ['lines_trace_mm', 'markers_flier_mm', 'boxplot_median_color'],
909
- 'violin': ['lines_trace_mm'],
910
- 'title': ['fonts_title_pt', 'fonts_family'],
911
- 'xlabel': ['fonts_axis_label_pt', 'fonts_family'],
912
- 'ylabel': ['fonts_axis_label_pt', 'fonts_family'],
913
- 'xticks': ['fonts_tick_label_pt', 'ticks_length_mm', 'ticks_thickness_mm', 'ticks_direction'],
914
- 'yticks': ['fonts_tick_label_pt', 'ticks_length_mm', 'ticks_thickness_mm', 'ticks_direction'],
915
- 'legend': ['fonts_legend_pt', 'legend_frameon', 'legend_loc', 'legend_alpha', 'legend_bg', 'legend_edgecolor'],
916
- 'spine': ['axes_thickness_mm'],
917
- 'axes': ['axes_width_mm', 'axes_height_mm', 'axes_thickness_mm', 'margins_left_mm', 'margins_right_mm', 'margins_bottom_mm', 'margins_top_mm'],
918
- };
919
-
920
- // Get relevant fields for this element type
921
- const relevantFields = fieldMap[element.type] || [];
922
-
923
- // Highlight relevant form fields
924
- relevantFields.forEach(fieldId => {
925
- const input = document.getElementById(fieldId);
926
- if (input) {
927
- const formRow = input.closest('.form-row');
928
- if (formRow) {
929
- formRow.classList.add('field-highlighted');
930
- }
931
- }
932
- });
933
-
934
- console.log('Selected element:', element.type, element.key);
935
- console.log('Relevant fields:', relevantFields);
936
-
937
- // Apply filtering if in selected mode
938
- if (viewMode === 'selected') {
939
- filterPropertiesByElementType(element.type);
940
- }
941
-
942
- // Show dynamic call properties if element has a call_id
943
- showDynamicCallProperties(element);
944
- }
945
-
946
- // Show dynamic properties based on recorded call
947
- function showDynamicCallProperties(element) {
948
- const container = document.getElementById('dynamic-call-properties');
949
- if (!container) return;
950
-
951
- // Clear previous content
952
- container.innerHTML = '';
953
-
954
- // Get call_id from element label (e.g., "bp1", "vp1")
955
- const callId = element.label;
956
- if (!callId || !callsData[callId]) {
957
- container.style.display = 'none';
958
- return;
959
- }
960
-
961
- const callData = callsData[callId];
962
- container.style.display = 'block';
963
-
964
- // Create header
965
- const header = document.createElement('div');
966
- header.className = 'dynamic-props-header';
967
- header.innerHTML = `<strong>${callData.function}()</strong> <span class="call-id">${callId}</span>`;
968
- container.appendChild(header);
969
-
970
- // Get args and kwargs
971
- const usedArgs = callData.args || [];
972
- const usedKwargs = callData.kwargs || {};
973
- const sigArgs = callData.signature?.args || [];
974
- const sigKwargs = callData.signature?.kwargs || {};
975
-
976
- // Show args (positional arguments) - display only, not editable
977
- if (usedArgs.length > 0) {
978
- const argsSection = document.createElement('div');
979
- argsSection.className = 'dynamic-props-section';
980
- argsSection.innerHTML = '<div class="dynamic-props-label">Arguments:</div>';
981
-
982
- for (let i = 0; i < usedArgs.length; i++) {
983
- const arg = usedArgs[i];
984
- const sigArg = sigArgs[i] || {};
985
- const row = document.createElement('div');
986
- row.className = 'form-row dynamic-field arg-field';
987
-
988
- const label = document.createElement('label');
989
- label.textContent = arg.name;
990
- if (sigArg.type) {
991
- label.title = `Type: ${sigArg.type}`;
992
- }
993
- if (sigArg.optional) {
994
- label.textContent += ' (opt)';
995
- }
996
-
997
- const valueSpan = document.createElement('span');
998
- valueSpan.className = 'arg-value';
999
- // Show array shape/type instead of full data
1000
- if (arg.data && Array.isArray(arg.data)) {
1001
- valueSpan.textContent = `[${arg.data.length} items]`;
1002
- } else if (arg.data === '__FILE__') {
1003
- valueSpan.textContent = '[external file]';
1004
- } else {
1005
- valueSpan.textContent = String(arg.data).substring(0, 30);
1006
- }
1007
-
1008
- row.appendChild(label);
1009
- row.appendChild(valueSpan);
1010
- argsSection.appendChild(row);
1011
- }
1012
- container.appendChild(argsSection);
1013
- }
1014
-
1015
- // Create form fields for used kwargs
1016
- if (Object.keys(usedKwargs).length > 0) {
1017
- const usedSection = document.createElement('div');
1018
- usedSection.className = 'dynamic-props-section';
1019
- usedSection.innerHTML = '<div class="dynamic-props-label">Used Parameters:</div>';
1020
-
1021
- for (const [key, value] of Object.entries(usedKwargs)) {
1022
- const field = createDynamicField(callId, key, value, sigKwargs[key]);
1023
- usedSection.appendChild(field);
1024
- }
1025
- container.appendChild(usedSection);
1026
- }
1027
-
1028
- // Create expandable section for available (unused) params - open by default
1029
- const availableParams = Object.keys(sigKwargs).filter(k => !(k in usedKwargs));
1030
- if (availableParams.length > 0) {
1031
- const availSection = document.createElement('details');
1032
- availSection.className = 'dynamic-props-available';
1033
- availSection.setAttribute('open', ''); // Open by default
1034
- availSection.innerHTML = `<summary>Available Parameters (${availableParams.length})</summary>`;
1035
-
1036
- const availContent = document.createElement('div');
1037
- availContent.className = 'dynamic-props-section';
1038
- for (const key of availableParams) { // Show all available parameters
1039
- const sigInfo = sigKwargs[key];
1040
- const field = createDynamicField(callId, key, sigInfo?.default, sigInfo, true);
1041
- availContent.appendChild(field);
1042
- }
1043
- availSection.appendChild(availContent);
1044
- container.appendChild(availSection);
1045
- }
1046
- }
1047
-
1048
- // Check if a field is a color field
1049
- function isColorField(key, sigInfo) {
1050
- const colorKeywords = ['color', 'facecolor', 'edgecolor', 'markerfacecolor', 'markeredgecolor', 'c'];
1051
- if (colorKeywords.includes(key.toLowerCase())) return true;
1052
- if (sigInfo?.type && sigInfo.type.toLowerCase().includes('color')) return true;
1053
- return false;
1054
- }
1055
-
1056
- // Convert color to RGB string for display
1057
- function colorToRGB(color) {
1058
- if (!color) return '';
1059
- // Already RGB format
1060
- if (typeof color === 'string' && color.match(/^rgb/i)) return color;
1061
- // Hex format
1062
- if (typeof color === 'string' && color.startsWith('#')) {
1063
- const hex = color.slice(1);
1064
- if (hex.length === 3) {
1065
- const r = parseInt(hex[0] + hex[0], 16);
1066
- const g = parseInt(hex[1] + hex[1], 16);
1067
- const b = parseInt(hex[2] + hex[2], 16);
1068
- return `rgb(${r}, ${g}, ${b})`;
1069
- } else if (hex.length === 6) {
1070
- const r = parseInt(hex.slice(0, 2), 16);
1071
- const g = parseInt(hex.slice(2, 4), 16);
1072
- const b = parseInt(hex.slice(4, 6), 16);
1073
- return `rgb(${r}, ${g}, ${b})`;
1074
- }
1075
- }
1076
- // Return as-is for named colors
1077
- return color;
1078
- }
1079
-
1080
- // Convert color to hex for color picker (uses priority-based resolution)
1081
- function colorToHex(color) {
1082
- return resolveColorToHex(color);
1083
- }
1084
-
1085
- // Color presets from SCITEX theme (priority 1 - highest)
1086
- const COLOR_PRESETS = {
1087
- 'blue': '#0080c0',
1088
- 'red': '#ff4632',
1089
- 'green': '#14b414',
1090
- 'yellow': '#e6a014',
1091
- 'purple': '#c832ff',
1092
- 'lightblue': '#14c8c8',
1093
- 'orange': '#e45e32',
1094
- 'pink': '#ff96c8',
1095
- 'black': '#000000',
1096
- 'white': '#ffffff',
1097
- 'gray': '#808080'
1098
- };
1099
-
1100
- // Matplotlib single-letter colors (priority 2)
1101
- const MATPLOTLIB_SINGLE = {
1102
- 'b': '#1f77b4',
1103
- 'g': '#2ca02c',
1104
- 'r': '#d62728',
1105
- 'c': '#17becf',
1106
- 'm': '#9467bd',
1107
- 'y': '#bcbd22',
1108
- 'k': '#000000',
1109
- 'w': '#ffffff'
1110
- };
1111
-
1112
- // Matplotlib/CSS named colors (priority 3 - common subset)
1113
- const MATPLOTLIB_NAMED = {
1114
- 'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff',
1115
- 'aquamarine': '#7fffd4', 'azure': '#f0ffff', 'beige': '#f5f5dc',
1116
- 'bisque': '#ffe4c4', 'blanchedalmond': '#ffebcd', 'blueviolet': '#8a2be2',
1117
- 'brown': '#a52a2a', 'burlywood': '#deb887', 'cadetblue': '#5f9ea0',
1118
- 'chartreuse': '#7fff00', 'chocolate': '#d2691e', 'coral': '#ff7f50',
1119
- 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc', 'crimson': '#dc143c',
1120
- 'cyan': '#00ffff', 'darkblue': '#00008b', 'darkcyan': '#008b8b',
1121
- 'darkgoldenrod': '#b8860b', 'darkgray': '#a9a9a9', 'darkgreen': '#006400',
1122
- 'darkgrey': '#a9a9a9', 'darkkhaki': '#bdb76b', 'darkmagenta': '#8b008b',
1123
- 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc',
1124
- 'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f',
1125
- 'darkslateblue': '#483d8b', 'darkslategray': '#2f4f4f', 'darkturquoise': '#00ced1',
1126
- 'darkviolet': '#9400d3', 'deeppink': '#ff1493', 'deepskyblue': '#00bfff',
1127
- 'dimgray': '#696969', 'dodgerblue': '#1e90ff', 'firebrick': '#b22222',
1128
- 'floralwhite': '#fffaf0', 'forestgreen': '#228b22', 'fuchsia': '#ff00ff',
1129
- 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700',
1130
- 'goldenrod': '#daa520', 'greenyellow': '#adff2f', 'honeydew': '#f0fff0',
1131
- 'hotpink': '#ff69b4', 'indianred': '#cd5c5c', 'indigo': '#4b0082',
1132
- 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa',
1133
- 'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd',
1134
- 'lightblue': '#add8e6', 'lightcoral': '#f08080', 'lightcyan': '#e0ffff',
1135
- 'lightgoldenrodyellow': '#fafad2', 'lightgray': '#d3d3d3', 'lightgreen': '#90ee90',
1136
- 'lightgrey': '#d3d3d3', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a',
1137
- 'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899',
1138
- 'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00',
1139
- 'limegreen': '#32cd32', 'linen': '#faf0e6', 'magenta': '#ff00ff',
1140
- 'maroon': '#800000', 'mediumaquamarine': '#66cdaa', 'mediumblue': '#0000cd',
1141
- 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370db', 'mediumseagreen': '#3cb371',
1142
- 'mediumslateblue': '#7b68ee', 'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc',
1143
- 'mediumvioletred': '#c71585', 'midnightblue': '#191970', 'mintcream': '#f5fffa',
1144
- 'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5', 'navajowhite': '#ffdead',
1145
- 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000',
1146
- 'olivedrab': '#6b8e23', 'orangered': '#ff4500', 'orchid': '#da70d6',
1147
- 'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee',
1148
- 'palevioletred': '#db7093', 'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9',
1149
- 'peru': '#cd853f', 'plum': '#dda0dd', 'powderblue': '#b0e0e6',
1150
- 'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513',
1151
- 'salmon': '#fa8072', 'sandybrown': '#f4a460', 'seagreen': '#2e8b57',
1152
- 'seashell': '#fff5ee', 'sienna': '#a0522d', 'silver': '#c0c0c0',
1153
- 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090',
1154
- 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4',
1155
- 'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8',
1156
- 'tomato': '#ff6347', 'turquoise': '#40e0d0', 'violet': '#ee82ee',
1157
- 'wheat': '#f5deb3', 'whitesmoke': '#f5f5f5', 'yellowgreen': '#9acd32'
1158
- };
1159
-
1160
- // Resolve color string to hex with priority: theme presets -> matplotlib -> CSS named
1161
- // Returns {name, hex, source} or null if not found in any preset
1162
- function findPresetColor(input) {
1163
- if (!input) return null;
1164
- const lower = input.toLowerCase().trim();
1165
-
1166
- // Priority 1: SCITEX theme presets
1167
- for (const [name, hex] of Object.entries(COLOR_PRESETS)) {
1168
- if (name === lower || hex.toLowerCase() === lower) {
1169
- return { name, hex, source: 'theme' };
1170
- }
1171
- }
1172
-
1173
- // Priority 2: Matplotlib single-letter colors
1174
- if (MATPLOTLIB_SINGLE[lower]) {
1175
- return { name: lower, hex: MATPLOTLIB_SINGLE[lower], source: 'matplotlib' };
1176
- }
1177
-
1178
- // Priority 3: Matplotlib/CSS named colors
1179
- if (MATPLOTLIB_NAMED[lower]) {
1180
- return { name: lower, hex: MATPLOTLIB_NAMED[lower], source: 'css' };
1181
- }
1182
-
1183
- return null;
1184
- }
1185
-
1186
- // Convert any color string to hex (handles presets, hex, rgb, and CSS names)
1187
- function resolveColorToHex(input) {
1188
- if (!input) return '#000000';
1189
-
1190
- // Check presets first
1191
- const preset = findPresetColor(input);
1192
- if (preset) return preset.hex;
1193
-
1194
- // Already hex format
1195
- if (input.startsWith('#')) {
1196
- if (input.length === 4) {
1197
- return '#' + input[1] + input[1] + input[2] + input[2] + input[3] + input[3];
1198
- }
1199
- return input;
1200
- }
1201
-
1202
- // RGB tuple format: (r, g, b) or r, g, b
1203
- const rgbMatch = input.match(/^\\(?(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\)?$/);
1204
- if (rgbMatch) {
1205
- const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0');
1206
- const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0');
1207
- const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0');
1208
- return `#${r}${g}${b}`;
1209
- }
1210
-
1211
- // Try browser's color parsing as last resort
1212
- const ctx = document.createElement('canvas').getContext('2d');
1213
- ctx.fillStyle = input;
1214
- return ctx.fillStyle; // Returns hex
1215
- }
1216
-
1217
- // Format color for display - prefer preset name, then RGB
1218
- function formatColorDisplay(value) {
1219
- if (!value) return '';
1220
-
1221
- // Check if it matches a preset
1222
- const preset = findPresetColor(value);
1223
- if (preset) {
1224
- return preset.name; // Show preset name
1225
- }
1226
-
1227
- // If hex, convert to RGB tuple
1228
- if (value.startsWith('#')) {
1229
- return hexToRGBTuple(value) || value;
1230
- }
1231
-
1232
- // Return as-is (could be matplotlib color name)
1233
- return value;
1234
- }
1235
-
1236
- // Convert hex to RGB tuple string for matplotlib
1237
- function hexToRGBTuple(hex) {
1238
- if (!hex || !hex.startsWith('#')) return null;
1239
- const h = hex.slice(1);
1240
- if (h.length === 3) {
1241
- const r = parseInt(h[0] + h[0], 16);
1242
- const g = parseInt(h[1] + h[1], 16);
1243
- const b = parseInt(h[2] + h[2], 16);
1244
- return `(${r}, ${g}, ${b})`;
1245
- } else if (h.length === 6) {
1246
- const r = parseInt(h.slice(0, 2), 16);
1247
- const g = parseInt(h.slice(2, 4), 16);
1248
- const b = parseInt(h.slice(4, 6), 16);
1249
- return `(${r}, ${g}, ${b})`;
1250
- }
1251
- return null;
1252
- }
1253
-
1254
- // Create a unified color input with dropdown (presets + custom option)
1255
- function createColorInput(callId, key, value) {
1256
- const wrapper = document.createElement('div');
1257
- wrapper.className = 'color-input-wrapper';
1258
-
1259
- // Color preview swatch (click to open color picker)
1260
- const swatch = document.createElement('div');
1261
- swatch.className = 'color-swatch';
1262
- swatch.style.backgroundColor = value || '#000000';
1263
- swatch.title = 'Click to pick color';
1264
-
1265
- // Unified dropdown with presets + custom option
1266
- const select = document.createElement('select');
1267
- select.className = 'dynamic-input color-select';
1268
- select.dataset.callId = callId;
1269
- select.dataset.param = key;
1270
-
1271
- // Add preset options
1272
- for (const [name, hex] of Object.entries(COLOR_PRESETS)) {
1273
- const opt = document.createElement('option');
1274
- opt.value = name;
1275
- opt.textContent = name;
1276
- opt.style.backgroundColor = hex;
1277
- select.appendChild(opt);
1278
- }
1279
-
1280
- // Add separator and custom option
1281
- const separator = document.createElement('option');
1282
- separator.disabled = true;
1283
- separator.textContent = '───────────';
1284
- select.appendChild(separator);
1285
-
1286
- const customOpt = document.createElement('option');
1287
- customOpt.value = '__custom__';
1288
- customOpt.textContent = 'Custom...';
1289
- select.appendChild(customOpt);
1290
-
1291
- // Set initial selection
1292
- const initialPreset = findPresetColor(value);
1293
- if (initialPreset) {
1294
- select.value = initialPreset.name;
1295
- } else if (value) {
1296
- // Add current value as option if not a preset
1297
- const currentOpt = document.createElement('option');
1298
- currentOpt.value = value;
1299
- currentOpt.textContent = formatColorDisplay(value);
1300
- select.insertBefore(currentOpt, separator);
1301
- select.value = value;
1302
- }
1303
-
1304
- // Custom input (hidden by default, shown when "Custom..." selected)
1305
- const customInput = document.createElement('input');
1306
- customInput.type = 'text';
1307
- customInput.className = 'dynamic-input color-custom-input';
1308
- customInput.placeholder = '(R,G,B) or color name';
1309
- customInput.style.display = 'none';
1310
-
1311
- // RGB display
1312
- const rgbDisplay = document.createElement('span');
1313
- rgbDisplay.className = 'rgb-display';
1314
- rgbDisplay.textContent = colorToRGB(value);
1315
-
1316
- // Hidden color picker
1317
- const colorPicker = document.createElement('input');
1318
- colorPicker.type = 'color';
1319
- colorPicker.className = 'color-picker-hidden';
1320
- colorPicker.value = colorToHex(value);
1321
-
1322
- // Update display helper
1323
- function updateDisplay(colorValue) {
1324
- const preset = findPresetColor(colorValue);
1325
- const hex = preset ? preset.hex : colorToHex(colorValue);
1326
- swatch.style.backgroundColor = hex;
1327
- rgbDisplay.textContent = colorToRGB(hex);
1328
- colorPicker.value = hex;
1329
- }
1330
-
1331
- // Event handlers
1332
- select.addEventListener('change', function() {
1333
- if (this.value === '__custom__') {
1334
- // Hide swatch and rgb display, open color picker
1335
- swatch.style.display = 'none';
1336
- rgbDisplay.style.display = 'none';
1337
- customInput.style.display = 'none';
1338
- colorPicker.click(); // Open color picker dialog
1339
- } else {
1340
- // Show swatch and rgb display
1341
- swatch.style.display = '';
1342
- rgbDisplay.style.display = '';
1343
- customInput.style.display = 'none';
1344
- updateDisplay(this.value);
1345
- // Create a fake input for the handler
1346
- const fakeInput = { value: this.value };
1347
- handleDynamicParamChange(callId, key, fakeInput);
1348
- }
1349
- });
1350
-
1351
- customInput.addEventListener('change', function() {
1352
- const inputValue = this.value.trim();
1353
- if (inputValue) {
1354
- // Check if it matches a preset
1355
- const preset = findPresetColor(inputValue);
1356
- if (preset) {
1357
- select.value = preset.name;
1358
- customInput.style.display = 'none';
1359
- updateDisplay(preset.hex);
1360
- } else {
1361
- // Add as new option
1362
- let existingOpt = Array.from(select.options).find(o => o.value === inputValue);
1363
- if (!existingOpt) {
1364
- const newOpt = document.createElement('option');
1365
- newOpt.value = inputValue;
1366
- newOpt.textContent = formatColorDisplay(inputValue);
1367
- select.insertBefore(newOpt, separator);
1368
- }
1369
- select.value = inputValue;
1370
- customInput.style.display = 'none';
1371
- updateDisplay(inputValue);
1372
- }
1373
- const fakeInput = { value: inputValue };
1374
- handleDynamicParamChange(callId, key, fakeInput);
1375
- }
1376
- });
1377
-
1378
- customInput.addEventListener('keydown', function(e) {
1379
- if (e.key === 'Escape') {
1380
- customInput.style.display = 'none';
1381
- // Revert to previous selection
1382
- if (select.value === '__custom__') {
1383
- select.selectedIndex = 0;
1384
- }
1385
- }
1386
- });
1387
-
1388
- swatch.addEventListener('click', function() {
1389
- colorPicker.click();
1390
- });
1391
-
1392
- colorPicker.addEventListener('input', function() {
1393
- updateDisplay(this.value);
1394
- });
1395
-
1396
- colorPicker.addEventListener('change', function() {
1397
- const pickedColor = this.value;
1398
- const preset = findPresetColor(pickedColor);
1399
- if (preset) {
1400
- select.value = preset.name;
1401
- } else {
1402
- // Add picked color as option
1403
- const rgbTuple = hexToRGBTuple(pickedColor);
1404
- let existingOpt = Array.from(select.options).find(o => o.value === pickedColor || o.value === rgbTuple);
1405
- if (!existingOpt) {
1406
- const newOpt = document.createElement('option');
1407
- newOpt.value = rgbTuple || pickedColor;
1408
- newOpt.textContent = rgbTuple || pickedColor;
1409
- select.insertBefore(newOpt, separator);
1410
- }
1411
- select.value = rgbTuple || pickedColor;
1412
- }
1413
- // Restore display elements and update
1414
- swatch.style.display = '';
1415
- rgbDisplay.style.display = '';
1416
- customInput.style.display = 'none';
1417
- updateDisplay(select.value);
1418
- const fakeInput = { value: select.value };
1419
- handleDynamicParamChange(callId, key, fakeInput);
1420
- });
1421
-
1422
- wrapper.appendChild(swatch);
1423
- wrapper.appendChild(select);
1424
- wrapper.appendChild(customInput);
1425
- wrapper.appendChild(rgbDisplay);
1426
- wrapper.appendChild(colorPicker);
1427
-
1428
- return wrapper;
1429
- }
1430
-
1431
- // Create a dynamic form field for call parameter
1432
- function createDynamicField(callId, key, value, sigInfo, isUnused = false) {
1433
- const container = document.createElement('div');
1434
- container.className = 'dynamic-field-container' + (isUnused ? ' unused' : '');
1435
-
1436
- const row = document.createElement('div');
1437
- row.className = 'form-row dynamic-field';
1438
-
1439
- const label = document.createElement('label');
1440
- label.textContent = key;
1441
-
1442
- let input;
1443
-
1444
- // Check if this is a color field
1445
- if (isColorField(key, sigInfo)) {
1446
- input = createColorInput(callId, key, value);
1447
- row.appendChild(label);
1448
- row.appendChild(input);
1449
- container.appendChild(row);
1450
- return container; // Skip type hint for color fields
1451
- }
1452
-
1453
- if (typeof value === 'boolean' || value === true || value === false) {
1454
- input = document.createElement('input');
1455
- input.type = 'checkbox';
1456
- input.checked = value === true;
1457
- } else if (typeof value === 'number') {
1458
- input = document.createElement('input');
1459
- input.type = 'number';
1460
- input.step = 'any';
1461
- input.value = value;
1462
- } else if (value === null || value === undefined) {
1463
- input = document.createElement('input');
1464
- input.type = 'text';
1465
- input.value = '';
1466
- input.placeholder = 'null';
1467
- } else {
1468
- input = document.createElement('input');
1469
- input.type = 'text';
1470
- input.value = String(value);
1471
- }
1472
-
1473
- input.dataset.callId = callId;
1474
- input.dataset.param = key;
1475
- input.className = 'dynamic-input';
1476
-
1477
- // Add change handler
1478
- input.addEventListener('change', function() {
1479
- handleDynamicParamChange(callId, key, this);
1480
- });
1481
-
1482
- row.appendChild(label);
1483
- row.appendChild(input);
1484
- container.appendChild(row);
1485
-
1486
- // Add type hint below the field
1487
- if (sigInfo?.type) {
1488
- const typeHint = document.createElement('div');
1489
- typeHint.className = 'type-hint';
1490
- // Clean up matplotlib docstring formatting
1491
- let typeText = sigInfo.type
1492
- .replace(/:mpltype:`([^`]+)`/g, '$1') // :mpltype:`color` -> color
1493
- .replace(/`~[^`]+`/g, '') // Remove `~.xxx` references
1494
- .replace(/`([^`]+)`/g, '$1'); // `xxx` -> xxx
1495
- typeHint.textContent = typeText;
1496
- container.appendChild(typeHint);
1497
- }
1498
-
1499
- return container;
1500
- }
1501
-
1502
- // Handle change to dynamic call parameter
1503
- async function handleDynamicParamChange(callId, param, input) {
1504
- let value;
1505
- if (input.type === 'checkbox') {
1506
- value = input.checked;
1507
- } else if (input.type === 'number') {
1508
- value = parseFloat(input.value);
1509
- if (isNaN(value)) value = null;
1510
- } else {
1511
- value = input.value || null;
1512
- // Convert string "null" to actual null
1513
- if (value === 'null') value = null;
1514
- }
1515
-
1516
- // For color parameters, resolve to hex using priority system (theme > matplotlib > CSS)
1517
- // This ensures "red" becomes SCITEX red (#ff4632), not pure red (#ff0000)
1518
- const colorParams = ['color', 'facecolor', 'edgecolor', 'markerfacecolor', 'markeredgecolor', 'c'];
1519
- if (value && typeof value === 'string' && colorParams.includes(param.toLowerCase())) {
1520
- const resolvedHex = resolveColorToHex(value);
1521
- console.log(`Color resolved: ${value} -> ${resolvedHex}`);
1522
- value = resolvedHex;
1523
- }
1524
-
1525
- console.log(`Dynamic param change: ${callId}.${param} = ${value}`);
1526
-
1527
- // Show loading state
1528
- document.body.classList.add('loading');
1529
- input.disabled = true;
1530
-
1531
- try {
1532
- const response = await fetch('/update_call', {
1533
- method: 'POST',
1534
- headers: { 'Content-Type': 'application/json' },
1535
- body: JSON.stringify({ call_id: callId, param: param, value: value })
1536
- });
1537
-
1538
- const data = await response.json();
1539
-
1540
- if (data.success) {
1541
- // Update preview image
1542
- const img = document.getElementById('preview-image');
1543
- img.src = 'data:image/png;base64,' + data.image;
1544
-
1545
- // Update dimensions for hitmap scaling
1546
- if (data.img_size) {
1547
- currentImgWidth = data.img_size.width;
1548
- currentImgHeight = data.img_size.height;
1549
- }
1550
-
1551
- // Update bboxes
1552
- currentBboxes = data.bboxes;
1553
-
1554
- // Reload hitmap
1555
- loadHitmap();
1556
-
1557
- // Redraw hit regions
1558
- updateHitRegions();
1559
-
1560
- // Update callsData with new value
1561
- if (callsData[callId]) {
1562
- if (value === null) {
1563
- delete callsData[callId].kwargs[param];
1564
- } else {
1565
- callsData[callId].kwargs[param] = value;
1566
- }
1567
- }
1568
-
1569
- console.log('Call updated successfully');
1570
- } else {
1571
- console.error('Update failed:', data.error);
1572
- alert('Update failed: ' + data.error);
1573
- }
1574
- } catch (error) {
1575
- console.error('Update failed:', error);
1576
- alert('Update failed: ' + error.message);
1577
- }
1578
-
1579
- input.disabled = false;
1580
- document.body.classList.remove('loading');
1581
- }
1582
-
1583
- // Set view mode (all or selected)
1584
- // Current active tab
1585
- let currentTab = 'figure';
1586
-
1587
- // Element type to tab mapping
1588
- const AXIS_TYPES = ['title', 'xlabel', 'ylabel', 'suptitle', 'supxlabel', 'supylabel', 'legend'];
1589
- const ELEMENT_TYPES = ['line', 'scatter', 'bar', 'fill', 'boxplot', 'violin', 'image', 'linecollection'];
1590
-
1591
- // Switch between Figure/Axis/Element tabs
1592
- function switchTab(tabName) {
1593
- currentTab = tabName;
1594
-
1595
- // Update tab buttons
1596
- document.querySelectorAll('.tab-btn').forEach(btn => {
1597
- btn.classList.toggle('active', btn.id === `tab-${tabName}`);
1598
- });
1599
-
1600
- // Update tab content
1601
- document.querySelectorAll('.tab-content').forEach(content => {
1602
- content.classList.toggle('active', content.id === `tab-content-${tabName}`);
1603
- });
1604
-
1605
- // Update hints based on selection state
1606
- updateTabHints();
1607
- }
1608
-
1609
- // Get appropriate tab for element type
1610
- function getTabForElementType(elementType) {
1611
- if (!elementType) return 'figure';
1612
- if (AXIS_TYPES.includes(elementType)) return 'axis';
1613
- if (ELEMENT_TYPES.includes(elementType)) return 'element';
1614
- return 'figure';
1615
- }
1616
-
1617
- // Auto-switch to appropriate tab based on selected element
1618
- function autoSwitchTab(elementType) {
1619
- const targetTab = getTabForElementType(elementType);
1620
- if (targetTab !== currentTab) {
1621
- switchTab(targetTab);
1622
- }
1623
- }
1624
-
1625
- // Update tab hints based on current state
1626
- function updateTabHints() {
1627
- const axisHint = document.getElementById('axis-tab-hint');
1628
- const elementHint = document.getElementById('element-tab-hint');
1629
- const elementPanel = document.getElementById('selected-element-panel');
1630
- const dynamicProps = document.getElementById('dynamic-call-properties');
1631
-
1632
- if (currentTab === 'axis') {
1633
- if (selectedElement && AXIS_TYPES.includes(selectedElement.type)) {
1634
- if (axisHint) axisHint.style.display = 'none';
1635
- } else {
1636
- if (axisHint) axisHint.style.display = 'block';
1637
- }
1638
- }
1639
-
1640
- if (currentTab === 'element') {
1641
- if (selectedElement && ELEMENT_TYPES.includes(selectedElement.type)) {
1642
- if (elementHint) elementHint.style.display = 'none';
1643
- if (elementPanel) {
1644
- elementPanel.style.display = 'block';
1645
- document.getElementById('element-type-badge').textContent = selectedElement.type;
1646
- document.getElementById('element-name').textContent = selectedElement.label || selectedElement.key;
1647
- }
1648
- } else {
1649
- if (elementHint) elementHint.style.display = 'block';
1650
- if (elementPanel) elementPanel.style.display = 'none';
1651
- if (dynamicProps) dynamicProps.style.display = 'none';
1652
- }
1653
- }
1654
- }
1655
-
1656
- function setViewMode(mode) {
1657
- viewMode = mode;
1658
-
1659
- // Update toggle buttons (legacy)
1660
- const btnAll = document.getElementById('btn-show-all');
1661
- const btnSelected = document.getElementById('btn-show-selected');
1662
- if (btnAll) btnAll.classList.toggle('active', mode === 'all');
1663
- if (btnSelected) btnSelected.classList.toggle('active', mode === 'selected');
1664
-
1665
- // Update controls sections class
1666
- const controlsSections = document.querySelector('.controls-sections');
1667
- controlsSections.classList.toggle('filter-mode', mode === 'selected');
1668
-
1669
- // Update selection hint
1670
- const hint = document.getElementById('selection-hint');
1671
- if (mode === 'selected') {
1672
- if (selectedElement) {
1673
- hint.textContent = `Showing: ${selectedElement.type}`;
1674
- hint.style.color = 'var(--accent-color)';
1675
- // Hide all style sections - only show call properties
1676
- hideAllStyleSections();
1677
- } else {
1678
- hint.textContent = '';
1679
- hint.style.color = '';
1680
- // Show all when no selection in filter mode
1681
- showAllProperties();
1682
- }
1683
- } else {
1684
- hint.textContent = '';
1685
- showAllProperties();
1686
- }
1687
- }
1688
-
1689
- // Hide all style sections (for Selected mode - only show call properties)
1690
- function hideAllStyleSections() {
1691
- const sections = document.querySelectorAll('.section[data-element-types]');
1692
- sections.forEach(section => {
1693
- section.classList.add('section-hidden');
1694
- section.classList.remove('section-visible');
1695
- });
1696
- }
1697
-
1698
- // Filter properties by element type
1699
- function filterPropertiesByElementType(elementType) {
1700
- const sections = document.querySelectorAll('.section[data-element-types]');
1701
-
1702
- sections.forEach(section => {
1703
- const types = section.getAttribute('data-element-types').split(',');
1704
- const isGlobal = types.includes('global');
1705
- const matches = isGlobal || types.includes(elementType);
1706
-
1707
- section.classList.toggle('section-hidden', !matches);
1708
- section.classList.toggle('section-visible', matches);
1709
-
1710
- // If section matches, filter individual form-rows within it
1711
- if (matches && !isGlobal) {
1712
- const formRows = section.querySelectorAll('.form-row[data-element-types]');
1713
- formRows.forEach(row => {
1714
- const rowTypes = row.getAttribute('data-element-types').split(',');
1715
- const rowMatches = rowTypes.includes(elementType);
1716
- row.classList.toggle('field-hidden', !rowMatches);
1717
- });
1718
-
1719
- // Open matching sections
1720
- section.setAttribute('open', '');
1721
- }
1722
- });
1723
-
1724
- // Update hint
1725
- const hint = document.getElementById('selection-hint');
1726
- hint.textContent = `Showing: ${elementType}`;
1727
- hint.style.color = 'var(--accent-color)';
1728
- }
1729
-
1730
- // Show all properties (remove filtering)
1731
- function showAllProperties() {
1732
- const sections = document.querySelectorAll('.section[data-element-types]');
1733
-
1734
- sections.forEach(section => {
1735
- section.classList.remove('section-hidden', 'section-visible');
1736
-
1737
- const formRows = section.querySelectorAll('.form-row[data-element-types]');
1738
- formRows.forEach(row => {
1739
- row.classList.remove('field-hidden');
1740
- });
1741
- });
1742
- }
1743
-
1744
- // Clear selection
1745
- function clearSelection() {
1746
- selectedElement = null;
1747
- document.getElementById('selected-info').textContent = 'Click on an element to select it';
1748
- clearSelectionOverlay();
1749
-
1750
- // Clear section and field highlights
1751
- document.querySelectorAll('.section-highlighted').forEach(s => s.classList.remove('section-highlighted'));
1752
- document.querySelectorAll('.field-highlighted').forEach(f => f.classList.remove('field-highlighted'));
1753
-
1754
- // Switch back to Figure tab when nothing selected
1755
- switchTab('figure');
1756
-
1757
- // Update hint and show all if in filter mode (legacy)
1758
- const hint = document.getElementById('selection-hint');
1759
- if (hint && viewMode === 'selected') {
1760
- hint.textContent = '';
1761
- hint.style.color = '';
1762
- showAllProperties();
1763
- }
1764
- }
1765
-
1766
- // Draw selection shape(s) - handles lines, scatter, and rectangles like hover effect
1767
- function drawSelection(key) {
1768
- const overlay = document.getElementById('selection-overlay');
1769
- overlay.innerHTML = '';
1770
-
1771
- // Get preview image dimensions and position
1772
- const img = document.getElementById('preview-image');
1773
- const imgRect = img.getBoundingClientRect();
1774
- const wrapperRect = img.parentElement.getBoundingClientRect();
1775
-
1776
- // Scale bbox to display coordinates
1777
- const scaleX = imgRect.width / img.naturalWidth;
1778
- const scaleY = imgRect.height / img.naturalHeight;
1779
- const offsetX = imgRect.left - wrapperRect.left;
1780
- const offsetY = imgRect.top - wrapperRect.top;
1781
-
1782
- // Determine which elements to highlight
1783
- let elementsToHighlight = [key];
1784
-
1785
- // If this element has a group, highlight all group elements
1786
- if (selectedElement && selectedElement.groupElements) {
1787
- elementsToHighlight = selectedElement.groupElements.map(e => e.key);
1788
- }
1789
-
1790
- // Draw selection for each element (same shape as hover)
1791
- for (const elemKey of elementsToHighlight) {
1792
- const bbox = currentBboxes[elemKey];
1793
- if (!bbox) continue;
1794
-
1795
- // Get element color from bbox or use accent color as fallback
1796
- const elementColor = bbox.original_color || '#2563eb';
1797
- const isPrimary = elemKey === key;
1798
-
1799
- // Use polyline for lines with points (same as hover)
1800
- if (bbox.type === 'line' && bbox.points && bbox.points.length > 1) {
1801
- const points = bbox.points.map(pt => {
1802
- const x = offsetX + pt[0] * scaleX;
1803
- const y = offsetY + pt[1] * scaleY;
1804
- return `${x},${y}`;
1805
- }).join(' ');
1806
-
1807
- const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
1808
- polyline.setAttribute('points', points);
1809
- polyline.setAttribute('class', 'selection-polyline');
1810
- polyline.style.setProperty('--element-color', elementColor);
1811
- if (isPrimary) {
1812
- polyline.style.strokeWidth = '10';
1813
- polyline.style.strokeOpacity = '0.6';
1814
- }
1815
- overlay.appendChild(polyline);
1816
- }
1817
- // Use circles for scatter points (same as hover)
1818
- else if (bbox.type === 'scatter' && bbox.points && bbox.points.length > 0) {
1819
- const hitRadius = isPrimary ? 7 : 5;
1820
- bbox.points.forEach(pt => {
1821
- const cx = offsetX + pt[0] * scaleX;
1822
- const cy = offsetY + pt[1] * scaleY;
1823
-
1824
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1825
- circle.setAttribute('cx', cx);
1826
- circle.setAttribute('cy', cy);
1827
- circle.setAttribute('r', hitRadius);
1828
- circle.setAttribute('class', 'selection-circle');
1829
- circle.style.setProperty('--element-color', elementColor);
1830
- overlay.appendChild(circle);
1831
- });
1832
- }
1833
- // Use rectangle for other elements
1834
- else {
1835
- const x = offsetX + bbox.x * scaleX;
1836
- const y = offsetY + bbox.y * scaleY;
1837
- const width = bbox.width * scaleX;
1838
- const height = bbox.height * scaleY;
1839
-
1840
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
1841
- rect.setAttribute('x', x);
1842
- rect.setAttribute('y', y);
1843
- rect.setAttribute('width', Math.max(width, 2));
1844
- rect.setAttribute('height', Math.max(height, 2));
1845
- rect.setAttribute('class', 'selection-rect');
1846
- rect.style.setProperty('--element-color', elementColor);
1847
-
1848
- // Mark the primary selection differently
1849
- if (isPrimary) {
1850
- rect.classList.add('selection-primary');
1851
- }
1852
-
1853
- overlay.appendChild(rect);
1854
- }
1855
- }
1856
- }
1857
-
1858
- // Clear selection overlay
1859
- function clearSelectionOverlay() {
1860
- document.getElementById('selection-overlay').innerHTML = '';
1861
- }
1862
-
1863
- // Schedule update with debounce
1864
- function scheduleUpdate() {
1865
- if (updateTimeout) {
1866
- clearTimeout(updateTimeout);
1867
- }
1868
- updateTimeout = setTimeout(updatePreview, UPDATE_DEBOUNCE);
1869
- }
1870
-
1871
- // Collect current overrides from form
1872
- function collectOverrides() {
1873
- const overrides = {};
1874
-
1875
- const inputs = document.querySelectorAll('input, select');
1876
- inputs.forEach(input => {
1877
- if (input.id === 'dark-mode-toggle') return;
1878
- if (!input.id) return;
1879
-
1880
- let value;
1881
- if (input.type === 'checkbox') {
1882
- value = input.checked;
1883
- } else if (input.type === 'number' || input.type === 'range') {
1884
- value = parseFloat(input.value);
1885
- if (isNaN(value)) return;
1886
- } else {
1887
- value = input.value;
1888
- }
1889
-
1890
- overrides[input.id] = value;
1891
- });
1892
-
1893
- return overrides;
1894
- }
1895
-
1896
- // Update preview
1897
- async function updatePreview() {
1898
- const overrides = collectOverrides();
1899
- const darkMode = document.getElementById('dark-mode-toggle').checked;
1900
-
1901
- document.body.classList.add('loading');
1902
-
1903
- try {
1904
- const response = await fetch('/update', {
1905
- method: 'POST',
1906
- headers: { 'Content-Type': 'application/json' },
1907
- body: JSON.stringify({ overrides, dark_mode: darkMode })
1908
- });
1909
-
1910
- const data = await response.json();
1911
-
1912
- // Update preview image
1913
- const img = document.getElementById('preview-image');
1914
- img.src = 'data:image/png;base64,' + data.image;
1915
-
1916
- // Update dimensions for hitmap scaling
1917
- if (data.img_size) {
1918
- currentImgWidth = data.img_size.width;
1919
- currentImgHeight = data.img_size.height;
1920
- }
1921
-
1922
- // Update bboxes
1923
- currentBboxes = data.bboxes;
1924
-
1925
- // Redraw selection if element still exists
1926
- if (selectedElement && currentBboxes[selectedElement.key]) {
1927
- drawSelection(selectedElement.key);
1928
- } else {
1929
- clearSelection();
1930
- }
1931
-
1932
- // Reload hitmap
1933
- loadHitmap();
1934
-
1935
- // Redraw hit regions if visible
1936
- updateHitRegions();
1937
-
1938
- } catch (error) {
1939
- console.error('Update failed:', error);
1940
- }
1941
-
1942
- document.body.classList.remove('loading');
1943
- }
1944
-
1945
- // Reset values to initial
1946
- function resetValues() {
1947
- initializeValues();
1948
- updatePreview();
1949
- }
1950
-
1951
- // Save overrides
1952
- async function saveOverrides() {
1953
- const overrides = collectOverrides();
1954
-
1955
- try {
1956
- const response = await fetch('/save', {
1957
- method: 'POST',
1958
- headers: { 'Content-Type': 'application/json' },
1959
- body: JSON.stringify({ overrides })
1960
- });
1961
-
1962
- const data = await response.json();
1963
- if (data.success) {
1964
- // Update override status indicator
1965
- if (data.has_overrides) {
1966
- showOverrideStatus(data.timestamp);
1967
- }
1968
- alert('Saved successfully!' + (data.path ? '\\nPath: ' + data.path : ''));
1969
- }
1970
- } catch (error) {
1971
- console.error('Save failed:', error);
1972
- alert('Save failed: ' + error.message);
1973
- }
1974
- }
1975
-
1976
- // Download dropdown state
1977
- let currentDownloadFormat = 'png';
1978
-
1979
- // Initialize download dropdown
1980
- function initializeDownloadDropdown() {
1981
- const mainBtn = document.getElementById('btn-download-main');
1982
- const toggleBtn = document.getElementById('btn-download-toggle');
1983
- const menu = document.getElementById('download-menu');
1984
-
1985
- // Main button click - download with current format
1986
- mainBtn.addEventListener('click', () => {
1987
- downloadFigure(currentDownloadFormat);
1988
- });
1989
-
1990
- // Toggle button click - show/hide menu
1991
- toggleBtn.addEventListener('click', (e) => {
1992
- e.stopPropagation();
1993
- menu.classList.toggle('open');
1994
- });
1995
-
1996
- // Menu option clicks
1997
- document.querySelectorAll('.download-option').forEach(option => {
1998
- option.addEventListener('click', (e) => {
1999
- const format = option.dataset.format;
2000
- currentDownloadFormat = format;
2001
-
2002
- // Update main button text
2003
- mainBtn.textContent = 'Download ' + format.toUpperCase();
2004
-
2005
- // Update active state
2006
- document.querySelectorAll('.download-option').forEach(opt => {
2007
- opt.classList.toggle('active', opt.dataset.format === format);
2008
- });
2009
-
2010
- // Close menu
2011
- menu.classList.remove('open');
2012
-
2013
- // Download immediately
2014
- downloadFigure(format);
2015
- });
2016
- });
2017
-
2018
- // Close menu when clicking outside
2019
- document.addEventListener('click', (e) => {
2020
- if (!e.target.closest('.download-dropdown')) {
2021
- menu.classList.remove('open');
2022
- }
2023
- });
2024
- }
2025
-
2026
- // Initialize label input event handlers
2027
- function initializeLabelInputs() {
2028
- const labelMap = {
2029
- 'label_title': 'title',
2030
- 'label_xlabel': 'xlabel',
2031
- 'label_ylabel': 'ylabel',
2032
- 'label_suptitle': 'suptitle'
2033
- };
2034
-
2035
- for (const [inputId, labelType] of Object.entries(labelMap)) {
2036
- const input = document.getElementById(inputId);
2037
- if (input) {
2038
- // Debounced update on input
2039
- let timeout;
2040
- input.addEventListener('input', function() {
2041
- clearTimeout(timeout);
2042
- timeout = setTimeout(() => {
2043
- updateLabel(labelType, this.value);
2044
- }, UPDATE_DEBOUNCE);
2045
- });
2046
-
2047
- // Immediate update on Enter key
2048
- input.addEventListener('keydown', function(e) {
2049
- if (e.key === 'Enter') {
2050
- clearTimeout(timeout);
2051
- updateLabel(labelType, this.value);
2052
- }
2053
- });
2054
-
2055
- // Immediate update on blur
2056
- input.addEventListener('blur', function() {
2057
- clearTimeout(timeout);
2058
- updateLabel(labelType, this.value);
2059
- });
2060
- }
2061
- }
2062
-
2063
- // Initialize axis type toggles
2064
- initializeAxisTypeToggles();
2065
-
2066
- // Initialize legend position controls
2067
- initializeLegendPosition();
2068
- }
2069
-
2070
- // Initialize theme modal handlers
2071
- function initializeThemeModal() {
2072
- const modal = document.getElementById('theme-modal');
2073
- const themeSelector = document.getElementById('theme-selector');
2074
- const btnView = document.getElementById('btn-view-theme');
2075
- const btnDownload = document.getElementById('btn-download-theme');
2076
- const btnCopy = document.getElementById('btn-copy-theme');
2077
- const modalClose = document.getElementById('theme-modal-close');
2078
- const modalDownload = document.getElementById('theme-modal-download');
2079
- const modalCopy = document.getElementById('theme-modal-copy');
2080
- const themeContent = document.getElementById('theme-content');
2081
- const themeModalName = document.getElementById('theme-modal-name');
2082
-
2083
- // Theme selector change handler
2084
- if (themeSelector) {
2085
- // Load current theme and set selector
2086
- loadCurrentTheme();
2087
-
2088
- themeSelector.addEventListener('change', function() {
2089
- switchTheme(this.value);
2090
- });
2091
- }
2092
-
2093
- // View button opens modal
2094
- if (btnView) {
2095
- btnView.addEventListener('click', showThemeModal);
2096
- }
2097
-
2098
- // Download button
2099
- if (btnDownload) {
2100
- btnDownload.addEventListener('click', downloadTheme);
2101
- }
2102
-
2103
- // Copy button
2104
- if (btnCopy) {
2105
- btnCopy.addEventListener('click', copyTheme);
2106
- }
2107
-
2108
- // Modal close
2109
- if (modalClose) {
2110
- modalClose.addEventListener('click', hideThemeModal);
2111
- }
2112
-
2113
- // Modal buttons
2114
- if (modalDownload) {
2115
- modalDownload.addEventListener('click', downloadTheme);
2116
- }
2117
- if (modalCopy) {
2118
- modalCopy.addEventListener('click', copyTheme);
2119
- }
2120
-
2121
- // Close modal on outside click
2122
- if (modal) {
2123
- modal.addEventListener('click', function(e) {
2124
- if (e.target === modal) {
2125
- hideThemeModal();
2126
- }
2127
- });
2128
- }
2129
- }
2130
-
2131
- // Show theme modal
2132
- async function showThemeModal() {
2133
- const modal = document.getElementById('theme-modal');
2134
- const themeContent = document.getElementById('theme-content');
2135
- const themeModalName = document.getElementById('theme-modal-name');
2136
- const themeSelector = document.getElementById('theme-selector');
2137
-
2138
- try {
2139
- const response = await fetch('/theme');
2140
- const data = await response.json();
2141
-
2142
- // Use selector value if available, otherwise use data.name
2143
- const themeName = themeSelector ? themeSelector.value : data.name;
2144
- if (themeModalName) themeModalName.textContent = themeName;
2145
- if (themeContent) themeContent.textContent = data.content;
2146
- if (modal) modal.style.display = 'flex';
2147
- } catch (error) {
2148
- console.error('Failed to load theme:', error);
2149
- }
2150
- }
2151
-
2152
- // Hide theme modal
2153
- function hideThemeModal() {
2154
- const modal = document.getElementById('theme-modal');
2155
- if (modal) modal.style.display = 'none';
2156
- }
2157
-
2158
- // Download theme as YAML
2159
- async function downloadTheme() {
2160
- try {
2161
- const response = await fetch('/theme');
2162
- const data = await response.json();
2163
-
2164
- const blob = new Blob([data.content], { type: 'text/yaml' });
2165
- const url = URL.createObjectURL(blob);
2166
- const a = document.createElement('a');
2167
- a.href = url;
2168
- a.download = data.name + '.yaml';
2169
- document.body.appendChild(a);
2170
- a.click();
2171
- document.body.removeChild(a);
2172
- URL.revokeObjectURL(url);
2173
- } catch (error) {
2174
- console.error('Failed to download theme:', error);
2175
- }
2176
- }
2177
-
2178
- // Copy theme to clipboard
2179
- async function copyTheme() {
2180
- try {
2181
- const response = await fetch('/theme');
2182
- const data = await response.json();
2183
-
2184
- await navigator.clipboard.writeText(data.content);
2185
-
2186
- // Visual feedback
2187
- const btn = document.getElementById('btn-copy-theme');
2188
- const originalText = btn.textContent;
2189
- btn.textContent = 'Copied!';
2190
- setTimeout(() => { btn.textContent = originalText; }, 1500);
2191
- } catch (error) {
2192
- console.error('Failed to copy theme:', error);
2193
- }
2194
- }
2195
-
2196
- // Load current theme and set selector
2197
- async function loadCurrentTheme() {
2198
- try {
2199
- const response = await fetch('/list_themes');
2200
- const data = await response.json();
2201
-
2202
- const selector = document.getElementById('theme-selector');
2203
- if (selector && data.current) {
2204
- selector.value = data.current;
2205
- }
2206
-
2207
- console.log('Current theme:', data.current);
2208
- } catch (error) {
2209
- console.error('Failed to load current theme:', error);
2210
- }
2211
- }
2212
-
2213
- // Switch to a different theme preset
2214
- async function switchTheme(themeName) {
2215
- console.log('Switching theme to:', themeName);
2216
-
2217
- // Show loading state
2218
- document.body.classList.add('loading');
2219
-
2220
- try {
2221
- const response = await fetch('/switch_theme', {
2222
- method: 'POST',
2223
- headers: { 'Content-Type': 'application/json' },
2224
- body: JSON.stringify({ theme: themeName })
2225
- });
2226
-
2227
- const result = await response.json();
2228
-
2229
- if (result.success) {
2230
- // Update preview with new image
2231
- const previewImg = document.getElementById('preview-image');
2232
- previewImg.src = 'data:image/png;base64,' + result.image;
2233
-
2234
- // Update dimensions
2235
- if (result.img_size) {
2236
- currentImgWidth = result.img_size.width;
2237
- currentImgHeight = result.img_size.height;
2238
- }
2239
-
2240
- // Update form values from new theme
2241
- if (result.values) {
2242
- for (const [key, value] of Object.entries(result.values)) {
2243
- const element = document.getElementById(key);
2244
- if (element) {
2245
- if (element.type === 'checkbox') {
2246
- element.checked = Boolean(value);
2247
- } else {
2248
- element.value = value;
2249
- }
2250
- // Update placeholder as well for theme defaults
2251
- if (element.placeholder !== undefined) {
2252
- element.placeholder = value;
2253
- }
2254
- }
2255
- }
2256
- // Update theme defaults for modified state comparison
2257
- Object.assign(themeDefaults, result.values);
2258
- updateAllModifiedStates();
2259
- }
2260
-
2261
- // Update bboxes and redraw hit regions
2262
- if (result.bboxes) {
2263
- currentBboxes = result.bboxes;
2264
- previewImg.onload = () => {
2265
- updateHitRegions();
2266
- loadHitmap();
2267
- };
2268
- }
2269
-
2270
- console.log('Theme switched to:', themeName);
2271
- } else {
2272
- console.error('Theme switch failed:', result.error);
2273
- // Reset selector to previous value
2274
- loadCurrentTheme();
2275
- }
2276
- } catch (error) {
2277
- console.error('Failed to switch theme:', error);
2278
- // Reset selector to previous value
2279
- loadCurrentTheme();
2280
- } finally {
2281
- document.body.classList.remove('loading');
2282
- }
2283
- }
2284
-
2285
- // Initialize axis type toggle buttons
2286
- function initializeAxisTypeToggles() {
2287
- const xNumerical = document.getElementById('xaxis-numerical');
2288
- const xCategorical = document.getElementById('xaxis-categorical');
2289
- const yNumerical = document.getElementById('yaxis-numerical');
2290
- const yCategorical = document.getElementById('yaxis-categorical');
2291
- const xLabelsRow = document.getElementById('xaxis-labels-row');
2292
- const yLabelsRow = document.getElementById('yaxis-labels-row');
2293
- const xLabelsInput = document.getElementById('xaxis_labels');
2294
- const yLabelsInput = document.getElementById('yaxis_labels');
2295
-
2296
- // X-axis buttons
2297
- if (xNumerical) {
2298
- xNumerical.addEventListener('click', () => {
2299
- xNumerical.classList.add('active');
2300
- xCategorical.classList.remove('active');
2301
- xLabelsRow.style.display = 'none';
2302
- updateAxisType('x', 'numerical');
2303
- });
2304
- }
2305
-
2306
- if (xCategorical) {
2307
- xCategorical.addEventListener('click', () => {
2308
- xCategorical.classList.add('active');
2309
- xNumerical.classList.remove('active');
2310
- xLabelsRow.style.display = 'flex';
2311
- });
2312
- }
2313
-
2314
- // Y-axis buttons
2315
- if (yNumerical) {
2316
- yNumerical.addEventListener('click', () => {
2317
- yNumerical.classList.add('active');
2318
- yCategorical.classList.remove('active');
2319
- yLabelsRow.style.display = 'none';
2320
- updateAxisType('y', 'numerical');
2321
- });
2322
- }
2323
-
2324
- if (yCategorical) {
2325
- yCategorical.addEventListener('click', () => {
2326
- yCategorical.classList.add('active');
2327
- yNumerical.classList.remove('active');
2328
- yLabelsRow.style.display = 'flex';
2329
- });
2330
- }
2331
-
2332
- // Labels input handlers
2333
- if (xLabelsInput) {
2334
- let timeout;
2335
- xLabelsInput.addEventListener('input', function() {
2336
- clearTimeout(timeout);
2337
- timeout = setTimeout(() => {
2338
- const labels = this.value.split(',').map(l => l.trim()).filter(l => l);
2339
- if (labels.length > 0) {
2340
- updateAxisType('x', 'categorical', labels);
2341
- }
2342
- }, UPDATE_DEBOUNCE);
2343
- });
2344
-
2345
- xLabelsInput.addEventListener('keydown', function(e) {
2346
- if (e.key === 'Enter') {
2347
- clearTimeout(timeout);
2348
- const labels = this.value.split(',').map(l => l.trim()).filter(l => l);
2349
- if (labels.length > 0) {
2350
- updateAxisType('x', 'categorical', labels);
2351
- }
2352
- }
2353
- });
2354
- }
2355
-
2356
- if (yLabelsInput) {
2357
- let timeout;
2358
- yLabelsInput.addEventListener('input', function() {
2359
- clearTimeout(timeout);
2360
- timeout = setTimeout(() => {
2361
- const labels = this.value.split(',').map(l => l.trim()).filter(l => l);
2362
- if (labels.length > 0) {
2363
- updateAxisType('y', 'categorical', labels);
2364
- }
2365
- }, UPDATE_DEBOUNCE);
2366
- });
2367
-
2368
- yLabelsInput.addEventListener('keydown', function(e) {
2369
- if (e.key === 'Enter') {
2370
- clearTimeout(timeout);
2371
- const labels = this.value.split(',').map(l => l.trim()).filter(l => l);
2372
- if (labels.length > 0) {
2373
- updateAxisType('y', 'categorical', labels);
2374
- }
2375
- }
2376
- });
2377
- }
2378
-
2379
- // Load current axis info
2380
- loadAxisInfo();
2381
- }
2382
-
2383
- // Load current axis type info
2384
- async function loadAxisInfo() {
2385
- try {
2386
- const response = await fetch('/get_axis_info');
2387
- const info = await response.json();
2388
-
2389
- // Update X axis toggle
2390
- if (info.x_type === 'categorical') {
2391
- document.getElementById('xaxis-categorical').classList.add('active');
2392
- document.getElementById('xaxis-numerical').classList.remove('active');
2393
- document.getElementById('xaxis-labels-row').style.display = 'flex';
2394
- if (info.x_labels && info.x_labels.length > 0) {
2395
- document.getElementById('xaxis_labels').value = info.x_labels.join(', ');
2396
- }
2397
- }
2398
-
2399
- // Update Y axis toggle
2400
- if (info.y_type === 'categorical') {
2401
- document.getElementById('yaxis-categorical').classList.add('active');
2402
- document.getElementById('yaxis-numerical').classList.remove('active');
2403
- document.getElementById('yaxis-labels-row').style.display = 'flex';
2404
- if (info.y_labels && info.y_labels.length > 0) {
2405
- document.getElementById('yaxis_labels').value = info.y_labels.join(', ');
2406
- }
2407
- }
2408
-
2409
- console.log('Loaded axis info:', info);
2410
- } catch (error) {
2411
- console.error('Failed to load axis info:', error);
2412
- }
2413
- }
2414
-
2415
- // Update axis type on server
2416
- async function updateAxisType(axis, type, labels = []) {
2417
- console.log(`Updating ${axis} axis to ${type}`, labels);
2418
-
2419
- document.body.classList.add('loading');
2420
-
2421
- try {
2422
- const response = await fetch('/update_axis_type', {
2423
- method: 'POST',
2424
- headers: { 'Content-Type': 'application/json' },
2425
- body: JSON.stringify({
2426
- axis: axis,
2427
- type: type,
2428
- labels: labels
2429
- })
2430
- });
2431
-
2432
- const data = await response.json();
2433
-
2434
- if (data.success) {
2435
- // Update preview image
2436
- const img = document.getElementById('preview-image');
2437
- img.src = 'data:image/png;base64,' + data.image;
2438
-
2439
- // Update dimensions
2440
- if (data.img_size) {
2441
- currentImgWidth = data.img_size.width;
2442
- currentImgHeight = data.img_size.height;
2443
- }
2444
-
2445
- // Update bboxes
2446
- currentBboxes = data.bboxes;
2447
-
2448
- // Redraw hit regions
2449
- updateHitRegions();
2450
-
2451
- console.log('Axis type updated successfully');
2452
- } else {
2453
- console.error('Axis type update failed:', data.error);
2454
- alert('Update failed: ' + data.error);
2455
- }
2456
- } catch (error) {
2457
- console.error('Axis type update failed:', error);
2458
- alert('Update failed: ' + error.message);
2459
- }
2460
-
2461
- document.body.classList.remove('loading');
2462
- }
2463
-
2464
- // Initialize legend position controls
2465
- function initializeLegendPosition() {
2466
- const locSelect = document.getElementById('legend_loc');
2467
- const customPosDiv = document.getElementById('legend-custom-pos');
2468
- const xInput = document.getElementById('legend_x');
2469
- const yInput = document.getElementById('legend_y');
2470
- const visibleCheckbox = document.getElementById('legend_visible');
2471
-
2472
- if (!locSelect) return;
2473
-
2474
- // Legend visibility toggle
2475
- if (visibleCheckbox) {
2476
- visibleCheckbox.addEventListener('change', function() {
2477
- updateLegendVisibility(this.checked);
2478
- });
2479
- }
2480
-
2481
- // Show/hide custom position inputs based on selection
2482
- locSelect.addEventListener('change', function() {
2483
- if (this.value === 'custom') {
2484
- customPosDiv.style.display = 'block';
2485
- } else {
2486
- customPosDiv.style.display = 'none';
2487
- // Update legend with new location
2488
- updateLegendPosition(this.value);
2489
- }
2490
- });
2491
-
2492
- // Custom x/y coordinate handlers
2493
- if (xInput && yInput) {
2494
- let timeout;
2495
- const updateCustomPos = () => {
2496
- clearTimeout(timeout);
2497
- timeout = setTimeout(() => {
2498
- const x = parseFloat(xInput.value);
2499
- const y = parseFloat(yInput.value);
2500
- if (!isNaN(x) && !isNaN(y)) {
2501
- updateLegendPosition('custom', x, y);
2502
- }
2503
- }, UPDATE_DEBOUNCE);
2504
- };
2505
-
2506
- xInput.addEventListener('input', updateCustomPos);
2507
- yInput.addEventListener('input', updateCustomPos);
2508
-
2509
- xInput.addEventListener('keydown', (e) => {
2510
- if (e.key === 'Enter') {
2511
- clearTimeout(timeout);
2512
- const x = parseFloat(xInput.value);
2513
- const y = parseFloat(yInput.value);
2514
- if (!isNaN(x) && !isNaN(y)) {
2515
- updateLegendPosition('custom', x, y);
2516
- }
2517
- }
2518
- });
2519
-
2520
- yInput.addEventListener('keydown', (e) => {
2521
- if (e.key === 'Enter') {
2522
- clearTimeout(timeout);
2523
- const x = parseFloat(xInput.value);
2524
- const y = parseFloat(yInput.value);
2525
- if (!isNaN(x) && !isNaN(y)) {
2526
- updateLegendPosition('custom', x, y);
2527
- }
2528
- }
2529
- });
2530
- }
2531
-
2532
- // Load current legend info
2533
- loadLegendInfo();
2534
- }
2535
-
2536
- // Load current legend position info
2537
- async function loadLegendInfo() {
2538
- try {
2539
- const response = await fetch('/get_legend_info');
2540
- const info = await response.json();
2541
-
2542
- if (!info.has_legend) {
2543
- console.log('No legend found');
2544
- return;
2545
- }
2546
-
2547
- const locSelect = document.getElementById('legend_loc');
2548
- const customPosDiv = document.getElementById('legend-custom-pos');
2549
- const xInput = document.getElementById('legend_x');
2550
- const yInput = document.getElementById('legend_y');
2551
- const visibleCheckbox = document.getElementById('legend_visible');
2552
-
2553
- // Set visibility checkbox
2554
- if (visibleCheckbox) {
2555
- visibleCheckbox.checked = info.visible !== false;
2556
- }
2557
-
2558
- if (locSelect) {
2559
- locSelect.value = info.loc;
2560
- }
2561
-
2562
- if (info.loc === 'custom' && customPosDiv) {
2563
- customPosDiv.style.display = 'block';
2564
- if (xInput && info.x !== null) xInput.value = info.x;
2565
- if (yInput && info.y !== null) yInput.value = info.y;
2566
- }
2567
-
2568
- console.log('Loaded legend info:', info);
2569
- } catch (error) {
2570
- console.error('Failed to load legend info:', error);
2571
- }
2572
- }
2573
-
2574
- // Update legend visibility
2575
- async function updateLegendVisibility(visible) {
2576
- console.log('Updating legend visibility:', visible);
2577
-
2578
- try {
2579
- const response = await fetch('/update_legend_position', {
2580
- method: 'POST',
2581
- headers: { 'Content-Type': 'application/json' },
2582
- body: JSON.stringify({ visible: visible })
2583
- });
2584
-
2585
- const result = await response.json();
2586
-
2587
- if (result.success) {
2588
- // Update preview with new image
2589
- const previewImg = document.getElementById('preview-image');
2590
- previewImg.src = 'data:image/png;base64,' + result.image;
2591
-
2592
- // Update dimensions
2593
- if (result.img_size) {
2594
- currentImgWidth = result.img_size.width;
2595
- currentImgHeight = result.img_size.height;
2596
- }
2597
-
2598
- // Update bboxes and redraw hit regions
2599
- if (result.bboxes) {
2600
- currentBboxes = result.bboxes;
2601
- previewImg.onload = () => {
2602
- updateHitRegions();
2603
- loadHitmap();
2604
- };
2605
- }
2606
- } else {
2607
- console.error('Legend visibility update failed:', result.error);
2608
- }
2609
- } catch (error) {
2610
- console.error('Failed to update legend visibility:', error);
2611
- }
2612
- }
2613
-
2614
- // Update legend position on server
2615
- async function updateLegendPosition(loc, x = null, y = null) {
2616
- console.log(`Updating legend position: loc=${loc}, x=${x}, y=${y}`);
2617
-
2618
- document.body.classList.add('loading');
2619
-
2620
- try {
2621
- const body = { loc };
2622
- if (loc === 'custom' && x !== null && y !== null) {
2623
- body.x = x;
2624
- body.y = y;
2625
- }
2626
-
2627
- const response = await fetch('/update_legend_position', {
2628
- method: 'POST',
2629
- headers: { 'Content-Type': 'application/json' },
2630
- body: JSON.stringify(body)
2631
- });
2632
-
2633
- const data = await response.json();
2634
-
2635
- if (data.success) {
2636
- // Update preview image
2637
- const img = document.getElementById('preview-image');
2638
- img.src = 'data:image/png;base64,' + data.image;
2639
-
2640
- // Update dimensions
2641
- if (data.img_size) {
2642
- currentImgWidth = data.img_size.width;
2643
- currentImgHeight = data.img_size.height;
2644
- }
2645
-
2646
- // Update bboxes
2647
- currentBboxes = data.bboxes;
2648
-
2649
- // Redraw hit regions
2650
- updateHitRegions();
2651
-
2652
- console.log('Legend position updated successfully');
2653
- } else {
2654
- console.error('Legend position update failed:', data.error);
2655
- // Don't show alert for "No legend found" - it's expected for some figures
2656
- if (!data.error.includes('No legend')) {
2657
- alert('Update failed: ' + data.error);
2658
- }
2659
- }
2660
- } catch (error) {
2661
- console.error('Legend position update failed:', error);
2662
- }
2663
-
2664
- document.body.classList.remove('loading');
2665
- }
2666
-
2667
- // Download figure
2668
- function downloadFigure(format) {
2669
- window.location.href = '/download/' + format;
2670
- }
2671
-
2672
- // Restore to original programmatic style (clear manual overrides)
2673
- async function restoreOriginal() {
2674
- if (!confirm('Restore to original programmatic style? This will clear all manual overrides.')) {
2675
- return;
2676
- }
2677
-
2678
- document.body.classList.add('loading');
2679
-
2680
- try {
2681
- const response = await fetch('/restore', {
2682
- method: 'POST',
2683
- headers: { 'Content-Type': 'application/json' }
2684
- });
2685
-
2686
- const data = await response.json();
2687
-
2688
- if (data.success) {
2689
- // Update preview image
2690
- const img = document.getElementById('preview-image');
2691
- img.src = 'data:image/png;base64,' + data.image;
2692
-
2693
- // Update bboxes
2694
- currentBboxes = data.bboxes;
2695
-
2696
- // Reset form values to original style
2697
- if (data.original_style) {
2698
- for (const [key, value] of Object.entries(data.original_style)) {
2699
- const element = document.getElementById(key);
2700
- if (element) {
2701
- if (element.type === 'checkbox') {
2702
- element.checked = Boolean(value);
2703
- } else if (element.type === 'range') {
2704
- element.value = value;
2705
- const valueSpan = document.getElementById(key + '_value');
2706
- if (valueSpan) valueSpan.textContent = value;
2707
- } else {
2708
- element.value = value;
2709
- }
2710
- }
2711
- }
2712
- }
2713
-
2714
- // Clear selection
2715
- clearSelection();
2716
-
2717
- // Hide override status
2718
- hideOverrideStatus();
2719
-
2720
- // Reload hitmap
2721
- loadHitmap();
2722
- }
2723
- } catch (error) {
2724
- console.error('Restore failed:', error);
2725
- alert('Restore failed: ' + error.message);
2726
- }
2727
-
2728
- document.body.classList.remove('loading');
2729
- }
2730
-
2731
- // Check and display override status
2732
- async function checkOverrideStatus() {
2733
- try {
2734
- const response = await fetch('/style');
2735
- const data = await response.json();
2736
-
2737
- if (data.has_overrides) {
2738
- showOverrideStatus(data.manual_timestamp);
2739
- } else {
2740
- hideOverrideStatus();
2741
- }
2742
- } catch (error) {
2743
- console.error('Failed to check override status:', error);
2744
- }
2745
- }
2746
-
2747
- // Show override status indicator
2748
- function showOverrideStatus(timestamp) {
2749
- const statusEl = document.getElementById('override-status');
2750
- const timestampEl = document.getElementById('override-timestamp');
2751
-
2752
- if (statusEl) {
2753
- statusEl.style.display = 'flex';
2754
- }
2755
-
2756
- if (timestampEl && timestamp) {
2757
- const date = new Date(timestamp);
2758
- timestampEl.textContent = 'Last modified: ' + date.toLocaleString();
2759
- }
2760
- }
2761
-
2762
- // Hide override status indicator
2763
- function hideOverrideStatus() {
2764
- const statusEl = document.getElementById('override-status');
2765
- if (statusEl) {
2766
- statusEl.style.display = 'none';
2767
- }
2768
- }
2769
-
2770
- // Update override status after save
2771
- async function updateOverrideStatusAfterSave(data) {
2772
- if (data.has_overrides) {
2773
- showOverrideStatus(data.timestamp);
2774
- }
2775
- }
2776
- """
2777
-
2778
- __all__ = ["SCRIPTS"]