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.
- figrecipe/__init__.py +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {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"]
|