syd 1.0.2__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syd/__init__.py +3 -10
- syd/flask_deployment/deployer.py +119 -26
- syd/flask_deployment/static/css/styles.css +100 -67
- syd/flask_deployment/static/css/viewer.css +48 -0
- syd/flask_deployment/static/js/modules/api.js +89 -0
- syd/flask_deployment/static/js/modules/config.js +22 -0
- syd/flask_deployment/static/js/modules/plot.js +75 -0
- syd/flask_deployment/static/js/modules/state.js +89 -0
- syd/flask_deployment/static/js/modules/system_controls.js +191 -0
- syd/flask_deployment/static/js/modules/ui_controls.js +812 -0
- syd/flask_deployment/static/js/modules/utils.js +49 -0
- syd/flask_deployment/static/js/old_viewer.js +1195 -0
- syd/flask_deployment/static/js/viewer.js +53 -826
- syd/flask_deployment/templates/index.html +1 -1
- syd/notebook_deployment/deployer.py +1 -3
- syd/notebook_deployment/widgets.py +45 -27
- syd/support.py +25 -0
- syd/viewer.py +35 -4
- {syd-1.0.2.dist-info → syd-1.2.0.dist-info}/METADATA +24 -10
- syd-1.2.0.dist-info/RECORD +28 -0
- syd-1.0.2.dist-info/RECORD +0 -19
- {syd-1.0.2.dist-info → syd-1.2.0.dist-info}/WHEEL +0 -0
- {syd-1.0.2.dist-info → syd-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
let state = {};
|
|
2
|
+
let paramInfo = {};
|
|
3
|
+
let paramOrder = [];
|
|
4
|
+
let isUpdating = false;
|
|
5
|
+
let updateThreshold = 1.0; // Default update threshold
|
|
6
|
+
let loadingTimeout = null; // Timeout for showing loading state
|
|
7
|
+
let slowLoadingImage = null; // Cache for slow loading image
|
|
8
|
+
|
|
9
|
+
// Config object parsed from HTML data attributes
|
|
10
|
+
const config = {
|
|
11
|
+
controlsPosition: document.getElementById('viewer-config').dataset.controlsPosition || 'left',
|
|
12
|
+
controlsWidthPercent: parseInt(document.getElementById('viewer-config').dataset.controlsWidthPercent || 20),
|
|
13
|
+
plotMarginPercent: parseInt(document.getElementById('viewer-config').dataset.plotMarginPercent || 0)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Initialize the viewer
|
|
17
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
18
|
+
// Create main controls container if it doesn't exist
|
|
19
|
+
const mainContainer = document.getElementById('controls-container');
|
|
20
|
+
|
|
21
|
+
// Create parameter controls section first
|
|
22
|
+
const paramControls = document.createElement('div');
|
|
23
|
+
paramControls.id = 'parameter-controls';
|
|
24
|
+
paramControls.className = 'parameter-controls';
|
|
25
|
+
|
|
26
|
+
// Add Parameters header
|
|
27
|
+
const paramHeader = document.createElement('div');
|
|
28
|
+
paramHeader.className = 'section-header';
|
|
29
|
+
paramHeader.innerHTML = '<b>Parameters</b>';
|
|
30
|
+
paramControls.appendChild(paramHeader);
|
|
31
|
+
|
|
32
|
+
// Create system controls section
|
|
33
|
+
const systemControls = document.createElement('div');
|
|
34
|
+
systemControls.id = 'system-controls';
|
|
35
|
+
systemControls.className = 'system-controls';
|
|
36
|
+
|
|
37
|
+
// Create status element
|
|
38
|
+
const statusElement = document.createElement('div');
|
|
39
|
+
statusElement.id = 'status-display';
|
|
40
|
+
statusElement.className = 'status-display';
|
|
41
|
+
systemControls.appendChild(statusElement);
|
|
42
|
+
updateStatus('Initializing...');
|
|
43
|
+
|
|
44
|
+
// Add sections to main container in the desired order
|
|
45
|
+
mainContainer.appendChild(paramControls);
|
|
46
|
+
mainContainer.appendChild(systemControls);
|
|
47
|
+
|
|
48
|
+
// Fetch initial parameter information from server
|
|
49
|
+
fetch('/init-data')
|
|
50
|
+
.then(response => response.json())
|
|
51
|
+
.then(data => {
|
|
52
|
+
paramInfo = data.params;
|
|
53
|
+
paramOrder = data.param_order;
|
|
54
|
+
updateThreshold = data.config.update_threshold;
|
|
55
|
+
|
|
56
|
+
// Initialize state from parameter info
|
|
57
|
+
for (const [name, param] of Object.entries(paramInfo)) {
|
|
58
|
+
state[name] = param.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create UI controls for each parameter
|
|
62
|
+
createControls();
|
|
63
|
+
|
|
64
|
+
// Create system controls if horizontal layout
|
|
65
|
+
if (config.controlsPosition === 'left' || config.controlsPosition === 'right') {
|
|
66
|
+
createSystemControls(systemControls);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate initial plot
|
|
70
|
+
updatePlot();
|
|
71
|
+
updateStatus('Ready!');
|
|
72
|
+
})
|
|
73
|
+
.catch(error => {
|
|
74
|
+
console.error('Error initializing viewer:', error);
|
|
75
|
+
updateStatus('Error initializing viewer');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create system controls (width and threshold)
|
|
81
|
+
*/
|
|
82
|
+
function createSystemControls(container) {
|
|
83
|
+
// Create controls width slider
|
|
84
|
+
const widthControl = createFloatController('controls_width', {
|
|
85
|
+
type: 'float',
|
|
86
|
+
value: config.controlsWidthPercent,
|
|
87
|
+
min: 10,
|
|
88
|
+
max: 50,
|
|
89
|
+
step: 1
|
|
90
|
+
});
|
|
91
|
+
widthControl.className = 'numeric-control system-control';
|
|
92
|
+
|
|
93
|
+
// Add label for width control
|
|
94
|
+
const widthLabel = document.createElement('span');
|
|
95
|
+
widthLabel.className = 'control-label';
|
|
96
|
+
widthLabel.textContent = 'Controls Width %';
|
|
97
|
+
|
|
98
|
+
const widthGroup = document.createElement('div');
|
|
99
|
+
widthGroup.className = 'control-group';
|
|
100
|
+
widthGroup.appendChild(widthLabel);
|
|
101
|
+
widthGroup.appendChild(widthControl);
|
|
102
|
+
|
|
103
|
+
// Create update threshold slider
|
|
104
|
+
const thresholdControl = createFloatController('update_threshold', {
|
|
105
|
+
type: 'float',
|
|
106
|
+
value: updateThreshold,
|
|
107
|
+
min: 0.1,
|
|
108
|
+
max: 10.0,
|
|
109
|
+
step: 0.1
|
|
110
|
+
});
|
|
111
|
+
thresholdControl.className = 'numeric-control system-control';
|
|
112
|
+
|
|
113
|
+
// Add label for threshold control
|
|
114
|
+
const thresholdLabel = document.createElement('span');
|
|
115
|
+
thresholdLabel.className = 'control-label';
|
|
116
|
+
thresholdLabel.textContent = 'Update Threshold';
|
|
117
|
+
|
|
118
|
+
const thresholdGroup = document.createElement('div');
|
|
119
|
+
thresholdGroup.className = 'control-group';
|
|
120
|
+
thresholdGroup.appendChild(thresholdLabel);
|
|
121
|
+
thresholdGroup.appendChild(thresholdControl);
|
|
122
|
+
|
|
123
|
+
// Create plot margin slider
|
|
124
|
+
const plotMarginControl = createFloatController('plot_margin', {
|
|
125
|
+
type: 'float',
|
|
126
|
+
value: config.plotMarginPercent,
|
|
127
|
+
min: 0,
|
|
128
|
+
max: 50,
|
|
129
|
+
step: 1
|
|
130
|
+
});
|
|
131
|
+
plotMarginControl.className = 'numeric-control system-control';
|
|
132
|
+
|
|
133
|
+
// Add label for margin control
|
|
134
|
+
const marginLabel = document.createElement('span');
|
|
135
|
+
marginLabel.className = 'control-label';
|
|
136
|
+
marginLabel.textContent = 'Plot Margin %';
|
|
137
|
+
|
|
138
|
+
const marginGroup = document.createElement('div');
|
|
139
|
+
marginGroup.className = 'control-group';
|
|
140
|
+
marginGroup.appendChild(marginLabel);
|
|
141
|
+
marginGroup.appendChild(plotMarginControl);
|
|
142
|
+
|
|
143
|
+
// Add custom event listeners
|
|
144
|
+
// Width Control Listeners
|
|
145
|
+
const widthSlider = widthControl.querySelector('input[type="range"]');
|
|
146
|
+
const widthInput = widthControl.querySelector('input[type="number"]');
|
|
147
|
+
|
|
148
|
+
widthSlider.addEventListener('input', function() { // Real-time update for number input
|
|
149
|
+
widthInput.value = this.value;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
widthSlider.addEventListener('change', function() {
|
|
153
|
+
const width = parseFloat(this.value);
|
|
154
|
+
config.controlsWidthPercent = width;
|
|
155
|
+
|
|
156
|
+
// Update the root containers using querySelector for classes
|
|
157
|
+
const rootContainer = document.querySelector('.viewer-container');
|
|
158
|
+
const controlsContainer = document.querySelector('.controls-container'); // Select the outer div by class
|
|
159
|
+
const plotContainer = document.querySelector('.plot-container');
|
|
160
|
+
|
|
161
|
+
if (rootContainer && controlsContainer && plotContainer) {
|
|
162
|
+
if (config.controlsPosition === 'left' || config.controlsPosition === 'right') {
|
|
163
|
+
controlsContainer.style.width = `${width}%`;
|
|
164
|
+
plotContainer.style.width = `${100 - width}%`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Update the slider to match
|
|
169
|
+
widthSlider.value = width;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Threshold Control Listeners
|
|
173
|
+
const thresholdSlider = thresholdControl.querySelector('input[type="range"]');
|
|
174
|
+
const thresholdInput = thresholdControl.querySelector('input[type="number"]');
|
|
175
|
+
|
|
176
|
+
thresholdSlider.addEventListener('input', function() { // Real-time update for number input
|
|
177
|
+
thresholdInput.value = this.value;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
thresholdSlider.addEventListener('change', function() {
|
|
181
|
+
updateThreshold = parseFloat(this.value);
|
|
182
|
+
thresholdInput.value = updateThreshold; // Ensure input matches final value
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Plot Margin Control Listeners
|
|
186
|
+
const marginSlider = plotMarginControl.querySelector('input[type="range"]');
|
|
187
|
+
const marginInput = plotMarginControl.querySelector('input[type="number"]');
|
|
188
|
+
const plotContainer = document.querySelector('.plot-container');
|
|
189
|
+
|
|
190
|
+
// Function to apply margin and adjust size of the plot image
|
|
191
|
+
function applyPlotMargin(marginPercent) {
|
|
192
|
+
const plotImage = document.getElementById('plot-image'); // Get the image element
|
|
193
|
+
if (plotImage) {
|
|
194
|
+
const effectiveMargin = parseFloat(marginPercent); // Ensure it's a number
|
|
195
|
+
// Apply margin to the image
|
|
196
|
+
plotImage.style.margin = `${effectiveMargin}%`;
|
|
197
|
+
// Adjust width and height to account for the margin
|
|
198
|
+
plotImage.style.width = `calc(100% - ${2 * effectiveMargin}%)`;
|
|
199
|
+
plotImage.style.height = `calc(100% - ${2 * effectiveMargin}%)`;
|
|
200
|
+
// Reset container padding just in case
|
|
201
|
+
if (plotContainer) {
|
|
202
|
+
plotContainer.style.padding = '0';
|
|
203
|
+
}
|
|
204
|
+
config.plotMarginPercent = effectiveMargin; // Update config
|
|
205
|
+
} else {
|
|
206
|
+
console.warn('Plot image element not found when applying margin.');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
marginSlider.addEventListener('input', function() { // Real-time update for number input
|
|
211
|
+
marginInput.value = this.value;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
marginSlider.addEventListener('change', function() {
|
|
215
|
+
const margin = parseFloat(this.value);
|
|
216
|
+
marginInput.value = margin; // Ensure input matches final value
|
|
217
|
+
applyPlotMargin(margin);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Add wheel event listener to plot container for margin control
|
|
221
|
+
if (plotContainer) {
|
|
222
|
+
plotContainer.addEventListener('wheel', function(event) {
|
|
223
|
+
event.preventDefault(); // Prevent page scrolling
|
|
224
|
+
|
|
225
|
+
const currentValue = parseFloat(marginSlider.value);
|
|
226
|
+
const step = parseFloat(marginSlider.step) || 1;
|
|
227
|
+
const min = parseFloat(marginSlider.min);
|
|
228
|
+
const max = parseFloat(marginSlider.max);
|
|
229
|
+
|
|
230
|
+
let newValue;
|
|
231
|
+
if (event.deltaY < 0) {
|
|
232
|
+
// Scrolling up (or zoom in) -> decrease margin
|
|
233
|
+
newValue = currentValue - step;
|
|
234
|
+
} else {
|
|
235
|
+
// Scrolling down (or zoom out) -> increase margin
|
|
236
|
+
newValue = currentValue + step;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Clamp the value within min/max bounds
|
|
240
|
+
newValue = Math.max(min, Math.min(max, newValue));
|
|
241
|
+
|
|
242
|
+
// Only update if the value actually changed
|
|
243
|
+
if (newValue !== currentValue) {
|
|
244
|
+
marginSlider.value = newValue;
|
|
245
|
+
marginInput.value = newValue;
|
|
246
|
+
applyPlotMargin(newValue);
|
|
247
|
+
}
|
|
248
|
+
}, { passive: false }); // Need passive: false to call preventDefault()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Apply initial margin
|
|
252
|
+
applyPlotMargin(config.plotMarginPercent);
|
|
253
|
+
|
|
254
|
+
container.appendChild(widthGroup);
|
|
255
|
+
container.appendChild(thresholdGroup);
|
|
256
|
+
container.appendChild(marginGroup);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create update threshold control
|
|
261
|
+
*/
|
|
262
|
+
function createUpdateThresholdControl() {
|
|
263
|
+
const container = document.createElement('div');
|
|
264
|
+
container.className = 'control-group';
|
|
265
|
+
|
|
266
|
+
const label = document.createElement('span');
|
|
267
|
+
label.className = 'control-label';
|
|
268
|
+
label.textContent = 'Update Threshold';
|
|
269
|
+
|
|
270
|
+
const input = document.createElement('input');
|
|
271
|
+
input.type = 'range';
|
|
272
|
+
input.min = '0.1';
|
|
273
|
+
input.max = '10.0';
|
|
274
|
+
input.step = '0.1';
|
|
275
|
+
input.value = updateThreshold;
|
|
276
|
+
input.className = 'update-threshold-slider';
|
|
277
|
+
|
|
278
|
+
input.addEventListener('change', function() {
|
|
279
|
+
updateThreshold = parseFloat(this.value);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
container.appendChild(label);
|
|
283
|
+
container.appendChild(input);
|
|
284
|
+
return container;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Update the status display
|
|
289
|
+
*/
|
|
290
|
+
function updateStatus(message) {
|
|
291
|
+
const statusElement = document.getElementById('status-display');
|
|
292
|
+
if (statusElement) {
|
|
293
|
+
statusElement.innerHTML = `<b>Syd Controls</b> <span class="status-message">Status: ${message}</span>`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create and cache the slow loading image
|
|
299
|
+
*/
|
|
300
|
+
function createSlowLoadingImage() {
|
|
301
|
+
const canvas = document.createElement('canvas');
|
|
302
|
+
canvas.width = 1200;
|
|
303
|
+
canvas.height = 900;
|
|
304
|
+
const ctx = canvas.getContext('2d');
|
|
305
|
+
|
|
306
|
+
// Fill background
|
|
307
|
+
ctx.fillStyle = '#ffffff';
|
|
308
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
309
|
+
|
|
310
|
+
// Add loading text
|
|
311
|
+
ctx.fillStyle = '#000000';
|
|
312
|
+
ctx.font = 'bold 16px Arial';
|
|
313
|
+
ctx.textAlign = 'center';
|
|
314
|
+
ctx.textBaseline = 'middle';
|
|
315
|
+
ctx.fillText('waiting for next figure...', canvas.width/2, canvas.height/2);
|
|
316
|
+
|
|
317
|
+
return canvas.toDataURL();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create UI controls based on parameter types
|
|
322
|
+
*/
|
|
323
|
+
function createControls() {
|
|
324
|
+
const paramControls = document.getElementById('parameter-controls');
|
|
325
|
+
|
|
326
|
+
// Clear any existing parameter controls
|
|
327
|
+
paramControls.innerHTML = '';
|
|
328
|
+
|
|
329
|
+
// Create controls for each parameter in the order specified by the viewer
|
|
330
|
+
paramOrder.forEach(name => {
|
|
331
|
+
const param = paramInfo[name];
|
|
332
|
+
if (!param) {
|
|
333
|
+
console.warn(`Parameter info not found for ${name} during control creation.`);
|
|
334
|
+
return; // Skip if param info is missing for some reason
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Create control group
|
|
338
|
+
const controlGroup = createControlGroup(name, param);
|
|
339
|
+
|
|
340
|
+
// Add to container
|
|
341
|
+
if (controlGroup) {
|
|
342
|
+
paramControls.appendChild(controlGroup);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create a control group for a parameter
|
|
349
|
+
*/
|
|
350
|
+
function createControlGroup(name, param) {
|
|
351
|
+
// Skip if param type is unknown
|
|
352
|
+
if (!param.type || param.type === 'unknown') {
|
|
353
|
+
console.warn(`Unknown parameter type for ${name}`);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Create control group div
|
|
358
|
+
const controlGroup = document.createElement('div');
|
|
359
|
+
controlGroup.className = 'control-group';
|
|
360
|
+
controlGroup.id = `control-group-${name}`;
|
|
361
|
+
|
|
362
|
+
// Add label
|
|
363
|
+
const label = document.createElement('span');
|
|
364
|
+
label.className = 'control-label';
|
|
365
|
+
label.textContent = formatLabel(name);
|
|
366
|
+
controlGroup.appendChild(label);
|
|
367
|
+
|
|
368
|
+
// Create specific control based on parameter type
|
|
369
|
+
const control = createControl(name, param);
|
|
370
|
+
if (control) {
|
|
371
|
+
controlGroup.appendChild(control);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return controlGroup;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Create a specific control based on parameter type
|
|
379
|
+
*/
|
|
380
|
+
function createControl(name, param) {
|
|
381
|
+
switch (param.type) {
|
|
382
|
+
case 'text':
|
|
383
|
+
return createTextControl(name, param);
|
|
384
|
+
case 'boolean':
|
|
385
|
+
return createBooleanControl(name, param);
|
|
386
|
+
case 'integer':
|
|
387
|
+
return createIntegerControl(name, param);
|
|
388
|
+
case 'float':
|
|
389
|
+
return createFloatControl(name, param);
|
|
390
|
+
case 'selection':
|
|
391
|
+
return createSelectionControl(name, param);
|
|
392
|
+
case 'multiple-selection':
|
|
393
|
+
return createMultipleSelectionControl(name, param);
|
|
394
|
+
case 'integer-range':
|
|
395
|
+
return createIntegerRangeControl(name, param);
|
|
396
|
+
case 'float-range':
|
|
397
|
+
return createFloatRangeControl(name, param);
|
|
398
|
+
case 'unbounded-integer':
|
|
399
|
+
return createUnboundedIntegerControl(name, param);
|
|
400
|
+
case 'unbounded-float':
|
|
401
|
+
return createUnboundedFloatControl(name, param);
|
|
402
|
+
case 'button':
|
|
403
|
+
return createButtonControl(name, param);
|
|
404
|
+
default:
|
|
405
|
+
console.warn(`No control implementation for type: ${param.type}`);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Create text input control
|
|
412
|
+
*/
|
|
413
|
+
function createTextControl(name, param) {
|
|
414
|
+
const input = document.createElement('input');
|
|
415
|
+
input.type = 'text';
|
|
416
|
+
input.id = `${name}-input`;
|
|
417
|
+
input.value = param.value || '';
|
|
418
|
+
|
|
419
|
+
input.addEventListener('change', function() {
|
|
420
|
+
updateParameter(name, this.value);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return input;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Create boolean checkbox control
|
|
428
|
+
*/
|
|
429
|
+
function createBooleanControl(name, param) {
|
|
430
|
+
const container = document.createElement('div');
|
|
431
|
+
container.className = 'checkbox-container';
|
|
432
|
+
|
|
433
|
+
const checkbox = document.createElement('input');
|
|
434
|
+
checkbox.type = 'checkbox';
|
|
435
|
+
checkbox.id = `${name}-checkbox`;
|
|
436
|
+
checkbox.checked = param.value === true;
|
|
437
|
+
|
|
438
|
+
checkbox.addEventListener('change', function() {
|
|
439
|
+
updateParameter(name, this.checked);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
container.appendChild(checkbox);
|
|
443
|
+
return container;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create integer control with slider and number input
|
|
448
|
+
*/
|
|
449
|
+
function createIntegerControl(name, param) {
|
|
450
|
+
const container = document.createElement('div');
|
|
451
|
+
container.className = 'numeric-control';
|
|
452
|
+
|
|
453
|
+
// Create slider
|
|
454
|
+
const slider = document.createElement('input');
|
|
455
|
+
slider.type = 'range';
|
|
456
|
+
slider.id = `${name}-slider`;
|
|
457
|
+
slider.min = param.min;
|
|
458
|
+
slider.max = param.max;
|
|
459
|
+
slider.step = param.step || 1;
|
|
460
|
+
slider.value = param.value;
|
|
461
|
+
|
|
462
|
+
// Create number input
|
|
463
|
+
const input = document.createElement('input');
|
|
464
|
+
input.type = 'number';
|
|
465
|
+
input.id = `${name}-input`;
|
|
466
|
+
input.min = param.min;
|
|
467
|
+
input.max = param.max;
|
|
468
|
+
input.step = param.step || 1;
|
|
469
|
+
input.value = param.value;
|
|
470
|
+
|
|
471
|
+
// Add event listeners
|
|
472
|
+
slider.addEventListener('input', function() {
|
|
473
|
+
input.value = this.value;
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
slider.addEventListener('change', function() {
|
|
477
|
+
const value = parseInt(this.value, 10);
|
|
478
|
+
input.value = value;
|
|
479
|
+
updateParameter(name, value);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
input.addEventListener('change', function() {
|
|
483
|
+
const value = parseInt(this.value, 10);
|
|
484
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
485
|
+
slider.value = value;
|
|
486
|
+
updateParameter(name, value);
|
|
487
|
+
} else {
|
|
488
|
+
this.value = state[name];
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
container.appendChild(slider);
|
|
493
|
+
container.appendChild(input);
|
|
494
|
+
return container;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create float control with slider and number input
|
|
499
|
+
*/
|
|
500
|
+
function createFloatControl(name, param) {
|
|
501
|
+
// create a container with the slider and input
|
|
502
|
+
const container = createFloatController(name, param);
|
|
503
|
+
const slider = container.querySelector('input[type="range"]');
|
|
504
|
+
const input = container.querySelector('input[type="number"]');
|
|
505
|
+
|
|
506
|
+
// Add event listeners
|
|
507
|
+
slider.addEventListener('input', function() {
|
|
508
|
+
input.value = this.value;
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
slider.addEventListener('change', function() {
|
|
512
|
+
const value = parseFloat(this.value);
|
|
513
|
+
input.value = value;
|
|
514
|
+
updateParameter(name, value);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
input.addEventListener('change', function() {
|
|
518
|
+
const value = parseFloat(this.value);
|
|
519
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
520
|
+
slider.value = value;
|
|
521
|
+
updateParameter(name, value);
|
|
522
|
+
} else {
|
|
523
|
+
this.value = state[name]; // Revert to current state
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return container;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Create float object with slider and number input
|
|
532
|
+
* Without the elements specific to "parameters"
|
|
533
|
+
*/
|
|
534
|
+
function createFloatController(name, param) {
|
|
535
|
+
const container = document.createElement('div');
|
|
536
|
+
container.className = 'numeric-control';
|
|
537
|
+
|
|
538
|
+
// Create slider
|
|
539
|
+
const slider = document.createElement('input');
|
|
540
|
+
slider.type = 'range';
|
|
541
|
+
slider.id = `${name}-slider`;
|
|
542
|
+
slider.min = param.min;
|
|
543
|
+
slider.max = param.max;
|
|
544
|
+
slider.step = param.step || 0.01;
|
|
545
|
+
slider.value = param.value;
|
|
546
|
+
|
|
547
|
+
// Create number input
|
|
548
|
+
const input = document.createElement('input');
|
|
549
|
+
input.type = 'number';
|
|
550
|
+
input.id = `${name}-input`;
|
|
551
|
+
input.min = param.min;
|
|
552
|
+
input.max = param.max;
|
|
553
|
+
input.step = param.step || 0.01;
|
|
554
|
+
input.value = param.value;
|
|
555
|
+
|
|
556
|
+
// return the container with the slider and input
|
|
557
|
+
container.appendChild(slider);
|
|
558
|
+
container.appendChild(input);
|
|
559
|
+
return container;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Create selection dropdown control
|
|
564
|
+
*/
|
|
565
|
+
function createSelectionControl(name, param) {
|
|
566
|
+
const select = document.createElement('select');
|
|
567
|
+
select.id = `${name}-select`;
|
|
568
|
+
|
|
569
|
+
// Add options
|
|
570
|
+
param.options.forEach(option => {
|
|
571
|
+
const optionElement = document.createElement('option');
|
|
572
|
+
optionElement.value = option;
|
|
573
|
+
optionElement.textContent = formatLabel(String(option));
|
|
574
|
+
// Store the original type information as a data attribute
|
|
575
|
+
optionElement.dataset.originalType = typeof option;
|
|
576
|
+
// For float values, also store the original value for exact comparison
|
|
577
|
+
if (typeof option === 'number') {
|
|
578
|
+
optionElement.dataset.originalValue = option;
|
|
579
|
+
}
|
|
580
|
+
select.appendChild(optionElement);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Set default value
|
|
584
|
+
select.value = param.value;
|
|
585
|
+
|
|
586
|
+
// Add event listener
|
|
587
|
+
select.addEventListener('change', function() {
|
|
588
|
+
// Get the selected option element
|
|
589
|
+
const selectedOption = this.options[this.selectedIndex];
|
|
590
|
+
let valueToSend = this.value;
|
|
591
|
+
|
|
592
|
+
// Convert back to the original type if needed
|
|
593
|
+
if (selectedOption.dataset.originalType === 'number') {
|
|
594
|
+
// Use the original value from the dataset for exact precision with floats
|
|
595
|
+
if (selectedOption.dataset.originalValue) {
|
|
596
|
+
valueToSend = parseFloat(selectedOption.dataset.originalValue);
|
|
597
|
+
} else {
|
|
598
|
+
valueToSend = parseFloat(valueToSend);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
updateParameter(name, valueToSend);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
return select;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Create multiple selection control
|
|
610
|
+
*/
|
|
611
|
+
function createMultipleSelectionControl(name, param) {
|
|
612
|
+
const container = document.createElement('div');
|
|
613
|
+
container.className = 'multiple-selection-container';
|
|
614
|
+
|
|
615
|
+
// Create select element
|
|
616
|
+
const select = document.createElement('select');
|
|
617
|
+
select.id = `${name}-select`;
|
|
618
|
+
select.className = 'multiple-select';
|
|
619
|
+
select.multiple = true;
|
|
620
|
+
|
|
621
|
+
// Add options
|
|
622
|
+
param.options.forEach(option => {
|
|
623
|
+
const optionElement = document.createElement('option');
|
|
624
|
+
optionElement.value = option;
|
|
625
|
+
optionElement.textContent = formatLabel(String(option));
|
|
626
|
+
|
|
627
|
+
// Check if this option is selected
|
|
628
|
+
if (param.value.includes(option)) {
|
|
629
|
+
optionElement.selected = true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
select.appendChild(optionElement);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Helper text
|
|
636
|
+
const helperText = document.createElement('div');
|
|
637
|
+
helperText.className = 'helper-text';
|
|
638
|
+
helperText.textContent = 'Ctrl+click to select multiple';
|
|
639
|
+
|
|
640
|
+
// Add event listener
|
|
641
|
+
select.addEventListener('change', function() {
|
|
642
|
+
// Get all selected options
|
|
643
|
+
const selectedValues = Array.from(this.selectedOptions).map(option => option.value);
|
|
644
|
+
updateParameter(name, selectedValues);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
container.appendChild(select);
|
|
648
|
+
container.appendChild(helperText);
|
|
649
|
+
return container;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Create integer range control with dual sliders
|
|
654
|
+
*/
|
|
655
|
+
function createIntegerRangeControl(name, param) {
|
|
656
|
+
return createRangeControl(name, param, parseInt);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Create float range control with dual sliders
|
|
661
|
+
*/
|
|
662
|
+
function createFloatRangeControl(name, param) {
|
|
663
|
+
return createRangeControl(name, param, parseFloat);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Generic range control creator
|
|
668
|
+
*/
|
|
669
|
+
function createRangeControl(name, param, converter) {
|
|
670
|
+
const container = document.createElement('div');
|
|
671
|
+
container.className = 'range-container';
|
|
672
|
+
|
|
673
|
+
// Create inputs container
|
|
674
|
+
const inputsContainer = document.createElement('div');
|
|
675
|
+
inputsContainer.className = 'range-inputs';
|
|
676
|
+
|
|
677
|
+
// Create min input
|
|
678
|
+
const minInput = document.createElement('input');
|
|
679
|
+
minInput.type = 'number';
|
|
680
|
+
minInput.id = `${name}-min-input`;
|
|
681
|
+
minInput.className = 'range-input';
|
|
682
|
+
minInput.min = param.min;
|
|
683
|
+
minInput.max = param.max;
|
|
684
|
+
minInput.step = param.step || (converter === parseInt ? 1 : 0.01); // Default step
|
|
685
|
+
minInput.value = param.value[0];
|
|
686
|
+
|
|
687
|
+
// Create slider container
|
|
688
|
+
const sliderContainer = document.createElement('div');
|
|
689
|
+
sliderContainer.className = 'range-slider-container';
|
|
690
|
+
|
|
691
|
+
// Create min slider
|
|
692
|
+
const minSlider = document.createElement('input');
|
|
693
|
+
minSlider.type = 'range';
|
|
694
|
+
minSlider.id = `${name}-min-slider`;
|
|
695
|
+
minSlider.className = 'range-slider min-slider';
|
|
696
|
+
minSlider.min = param.min;
|
|
697
|
+
minSlider.max = param.max;
|
|
698
|
+
minSlider.step = param.step || (converter === parseInt ? 1 : 0.01); // Default step
|
|
699
|
+
minSlider.value = param.value[0];
|
|
700
|
+
|
|
701
|
+
// Create max slider
|
|
702
|
+
const maxSlider = document.createElement('input');
|
|
703
|
+
maxSlider.type = 'range';
|
|
704
|
+
maxSlider.id = `${name}-max-slider`;
|
|
705
|
+
maxSlider.className = 'range-slider max-slider';
|
|
706
|
+
maxSlider.min = param.min;
|
|
707
|
+
maxSlider.max = param.max;
|
|
708
|
+
maxSlider.step = param.step || (converter === parseInt ? 1 : 0.01); // Default step
|
|
709
|
+
maxSlider.value = param.value[1];
|
|
710
|
+
|
|
711
|
+
// Create max input
|
|
712
|
+
const maxInput = document.createElement('input');
|
|
713
|
+
maxInput.type = 'number';
|
|
714
|
+
maxInput.id = `${name}-max-input`;
|
|
715
|
+
maxInput.className = 'range-input';
|
|
716
|
+
maxInput.min = param.min;
|
|
717
|
+
maxInput.max = param.max;
|
|
718
|
+
maxInput.step = param.step || (converter === parseInt ? 1 : 0.01); // Default step
|
|
719
|
+
maxInput.value = param.value[1];
|
|
720
|
+
|
|
721
|
+
// Add event listeners
|
|
722
|
+
// Input listeners for real-time updates of number inputs and gradient
|
|
723
|
+
minSlider.addEventListener('input', function() {
|
|
724
|
+
const minVal = converter(this.value);
|
|
725
|
+
const maxVal = converter(maxSlider.value);
|
|
726
|
+
if (minVal <= maxVal) {
|
|
727
|
+
minInput.value = minVal;
|
|
728
|
+
} else {
|
|
729
|
+
// Prevent slider crossing visually, update input to maxVal
|
|
730
|
+
this.value = maxVal;
|
|
731
|
+
minInput.value = maxVal;
|
|
732
|
+
}
|
|
733
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
maxSlider.addEventListener('input', function() {
|
|
737
|
+
const minVal = converter(minSlider.value);
|
|
738
|
+
const maxVal = converter(this.value);
|
|
739
|
+
if (maxVal >= minVal) {
|
|
740
|
+
maxInput.value = maxVal;
|
|
741
|
+
} else {
|
|
742
|
+
// Prevent slider crossing visually, update input to minVal
|
|
743
|
+
this.value = minVal;
|
|
744
|
+
maxInput.value = minVal;
|
|
745
|
+
}
|
|
746
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Change listeners for updating state and triggering backend calls
|
|
750
|
+
minSlider.addEventListener('change', function() {
|
|
751
|
+
const minVal = converter(this.value);
|
|
752
|
+
const maxVal = converter(maxSlider.value);
|
|
753
|
+
|
|
754
|
+
if (minVal <= maxVal) {
|
|
755
|
+
state[name] = [minVal, maxVal];
|
|
756
|
+
minInput.value = minVal;
|
|
757
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
758
|
+
updateParameter(name, [minVal, maxVal]);
|
|
759
|
+
} else {
|
|
760
|
+
this.value = maxVal; // Snap to maxVal if crossing
|
|
761
|
+
minInput.value = maxVal; // Also update input
|
|
762
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
maxSlider.addEventListener('change', function() {
|
|
767
|
+
const minVal = converter(minSlider.value);
|
|
768
|
+
const maxVal = converter(this.value);
|
|
769
|
+
|
|
770
|
+
if (maxVal >= minVal) {
|
|
771
|
+
state[name] = [minVal, maxVal];
|
|
772
|
+
maxInput.value = maxVal;
|
|
773
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
774
|
+
updateParameter(name, [minVal, maxVal]);
|
|
775
|
+
} else {
|
|
776
|
+
this.value = minVal; // Snap to minVal if crossing
|
|
777
|
+
maxInput.value = minVal; // Also update input
|
|
778
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
minInput.addEventListener('change', function() {
|
|
783
|
+
const minVal = converter(this.value);
|
|
784
|
+
const maxVal = converter(maxInput.value);
|
|
785
|
+
|
|
786
|
+
if (!isNaN(minVal) && minVal >= param.min && minVal <= maxVal) {
|
|
787
|
+
state[name] = [minVal, maxVal];
|
|
788
|
+
minSlider.value = minVal;
|
|
789
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
790
|
+
updateParameter(name, [minVal, maxVal]);
|
|
791
|
+
} else {
|
|
792
|
+
// Revert input value and ensure gradient matches state
|
|
793
|
+
this.value = state[name][0];
|
|
794
|
+
minSlider.value = state[name][0];
|
|
795
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
maxInput.addEventListener('change', function() {
|
|
800
|
+
const minVal = converter(minInput.value);
|
|
801
|
+
const maxVal = converter(this.value);
|
|
802
|
+
|
|
803
|
+
if (!isNaN(maxVal) && maxVal <= param.max && maxVal >= minVal) {
|
|
804
|
+
state[name] = [minVal, maxVal];
|
|
805
|
+
maxSlider.value = maxVal;
|
|
806
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
807
|
+
updateParameter(name, [minVal, maxVal]);
|
|
808
|
+
} else {
|
|
809
|
+
// Revert input value and ensure gradient matches state
|
|
810
|
+
this.value = state[name][1];
|
|
811
|
+
maxSlider.value = state[name][1];
|
|
812
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Assemble the control
|
|
817
|
+
inputsContainer.appendChild(minInput);
|
|
818
|
+
inputsContainer.appendChild(maxInput);
|
|
819
|
+
|
|
820
|
+
sliderContainer.appendChild(minSlider);
|
|
821
|
+
sliderContainer.appendChild(maxSlider);
|
|
822
|
+
|
|
823
|
+
container.appendChild(inputsContainer);
|
|
824
|
+
container.appendChild(sliderContainer);
|
|
825
|
+
|
|
826
|
+
// Set initial gradient state
|
|
827
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
828
|
+
|
|
829
|
+
return container;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Create unbounded integer control
|
|
834
|
+
*/
|
|
835
|
+
function createUnboundedIntegerControl(name, param) {
|
|
836
|
+
const input = document.createElement('input');
|
|
837
|
+
input.type = 'number';
|
|
838
|
+
input.id = `${name}-input`;
|
|
839
|
+
input.value = param.value;
|
|
840
|
+
input.step = 1;
|
|
841
|
+
|
|
842
|
+
input.addEventListener('change', function() {
|
|
843
|
+
const value = parseInt(this.value, 10);
|
|
844
|
+
if (!isNaN(value)) {
|
|
845
|
+
updateParameter(name, value);
|
|
846
|
+
} else {
|
|
847
|
+
this.value = state[name];
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
return input;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Create unbounded float control
|
|
856
|
+
*/
|
|
857
|
+
function createUnboundedFloatControl(name, param) {
|
|
858
|
+
const input = document.createElement('input');
|
|
859
|
+
input.type = 'number';
|
|
860
|
+
input.id = `${name}-input`;
|
|
861
|
+
input.value = param.value;
|
|
862
|
+
input.step = param.step || 'any';
|
|
863
|
+
|
|
864
|
+
input.addEventListener('change', function() {
|
|
865
|
+
const value = parseFloat(this.value);
|
|
866
|
+
if (!isNaN(value)) {
|
|
867
|
+
updateParameter(name, value);
|
|
868
|
+
} else {
|
|
869
|
+
this.value = state[name];
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
return input;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Create button control
|
|
878
|
+
*/
|
|
879
|
+
function createButtonControl(name, param) {
|
|
880
|
+
const button = document.createElement('button');
|
|
881
|
+
button.id = `${name}-button`;
|
|
882
|
+
button.textContent = param.label || name;
|
|
883
|
+
|
|
884
|
+
button.addEventListener('click', function() {
|
|
885
|
+
// Show button as active
|
|
886
|
+
button.classList.add('active');
|
|
887
|
+
|
|
888
|
+
// Send action to the server
|
|
889
|
+
fetch('/update-param', {
|
|
890
|
+
method: 'POST',
|
|
891
|
+
headers: {
|
|
892
|
+
'Content-Type': 'application/json',
|
|
893
|
+
},
|
|
894
|
+
body: JSON.stringify({
|
|
895
|
+
name: name,
|
|
896
|
+
value: null, // Value is not used for buttons
|
|
897
|
+
action: true
|
|
898
|
+
}),
|
|
899
|
+
})
|
|
900
|
+
.then(response => response.json())
|
|
901
|
+
.then(data => {
|
|
902
|
+
// Remove active class
|
|
903
|
+
button.classList.remove('active');
|
|
904
|
+
|
|
905
|
+
if (data.error) {
|
|
906
|
+
console.error('Error:', data.error);
|
|
907
|
+
} else {
|
|
908
|
+
// Update state with any changes from callbacks
|
|
909
|
+
updateStateFromServer(data.state, data.params);
|
|
910
|
+
// Update plot if needed
|
|
911
|
+
updatePlot();
|
|
912
|
+
}
|
|
913
|
+
})
|
|
914
|
+
.catch(error => {
|
|
915
|
+
// Remove active class
|
|
916
|
+
button.classList.remove('active');
|
|
917
|
+
console.error('Error:', error);
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
return button;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Update a parameter value and send to server
|
|
926
|
+
*/
|
|
927
|
+
function updateParameter(name, value) {
|
|
928
|
+
// Prevent recursive updates
|
|
929
|
+
if (isUpdating) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
// Indicate status update
|
|
933
|
+
updateStatus('Updating ' + name + '...');
|
|
934
|
+
|
|
935
|
+
// Update local state
|
|
936
|
+
state[name] = value;
|
|
937
|
+
|
|
938
|
+
// Send update to server
|
|
939
|
+
fetch('/update-param', {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers: {
|
|
942
|
+
'Content-Type': 'application/json',
|
|
943
|
+
},
|
|
944
|
+
body: JSON.stringify({
|
|
945
|
+
name: name,
|
|
946
|
+
value: value,
|
|
947
|
+
action: false
|
|
948
|
+
}),
|
|
949
|
+
})
|
|
950
|
+
.then(response => response.json())
|
|
951
|
+
.then(data => {
|
|
952
|
+
if (data.error) {
|
|
953
|
+
console.error('Error:', data.error);
|
|
954
|
+
} else {
|
|
955
|
+
// Update state with any changes from callbacks
|
|
956
|
+
updateStateFromServer(data.state, data.params);
|
|
957
|
+
// Update plot
|
|
958
|
+
updatePlot();
|
|
959
|
+
}
|
|
960
|
+
})
|
|
961
|
+
.catch(error => {
|
|
962
|
+
console.error('Error:', error);
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Indicate status update
|
|
966
|
+
updateStatus('Ready!');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Update local state from server response
|
|
971
|
+
*/
|
|
972
|
+
function updateStateFromServer(serverState, serverParamInfo) {
|
|
973
|
+
// Set updating flag to prevent recursive updates
|
|
974
|
+
isUpdating = true;
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
// Update any parameters that changed due to callbacks
|
|
978
|
+
for (const [name, value] of Object.entries(serverState)) {
|
|
979
|
+
if (JSON.stringify(state[name]) !== JSON.stringify(value) || JSON.stringify(paramInfo[name]) !== JSON.stringify(serverParamInfo[name])) {
|
|
980
|
+
state[name] = value;
|
|
981
|
+
updateControlValue(name, value, serverParamInfo[name]);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} finally {
|
|
985
|
+
// Clear updating flag
|
|
986
|
+
isUpdating = false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Update a control's value in the UI
|
|
992
|
+
*/
|
|
993
|
+
function updateControlValue(name, value, param) {
|
|
994
|
+
if (!paramInfo[name]) return;
|
|
995
|
+
|
|
996
|
+
switch (param.type) {
|
|
997
|
+
case 'text':
|
|
998
|
+
document.getElementById(`${name}-input`).value = value;
|
|
999
|
+
break;
|
|
1000
|
+
case 'boolean':
|
|
1001
|
+
document.getElementById(`${name}-checkbox`).checked = value === true;
|
|
1002
|
+
break;
|
|
1003
|
+
case 'integer':
|
|
1004
|
+
case 'float':
|
|
1005
|
+
const slider = document.getElementById(`${name}-slider`);
|
|
1006
|
+
slider.value = value;
|
|
1007
|
+
slider.min = param.min;
|
|
1008
|
+
slider.max = param.max;
|
|
1009
|
+
slider.step = param.step;
|
|
1010
|
+
const input = document.getElementById(`${name}-input`);
|
|
1011
|
+
input.value = value;
|
|
1012
|
+
input.min = param.min;
|
|
1013
|
+
input.max = param.max;
|
|
1014
|
+
input.step = param.step;
|
|
1015
|
+
break;
|
|
1016
|
+
case 'selection':
|
|
1017
|
+
const selectElement = document.getElementById(`${name}-select`);
|
|
1018
|
+
if (selectElement) {
|
|
1019
|
+
// 1. Clear existing options
|
|
1020
|
+
selectElement.innerHTML = '';
|
|
1021
|
+
|
|
1022
|
+
// 2. Add new options from the updated param info
|
|
1023
|
+
if (param.options && Array.isArray(param.options)) {
|
|
1024
|
+
param.options.forEach(option => {
|
|
1025
|
+
const optionElement = document.createElement('option');
|
|
1026
|
+
optionElement.value = option;
|
|
1027
|
+
optionElement.textContent = formatLabel(String(option));
|
|
1028
|
+
// Store original type/value info
|
|
1029
|
+
optionElement.dataset.originalType = typeof option;
|
|
1030
|
+
if (typeof option === 'number') {
|
|
1031
|
+
optionElement.dataset.originalValue = option;
|
|
1032
|
+
}
|
|
1033
|
+
selectElement.appendChild(optionElement);
|
|
1034
|
+
});
|
|
1035
|
+
} else {
|
|
1036
|
+
console.warn(`No options found or options is not an array for parameter: ${name}`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
selectElement.value = value;
|
|
1040
|
+
} else {
|
|
1041
|
+
console.warn(`No select element found for parameter: ${name}`);
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
case 'multiple-selection':
|
|
1045
|
+
const multiSelect = document.getElementById(`${name}-select`);
|
|
1046
|
+
if (multiSelect) {
|
|
1047
|
+
multiSelect.innerHTML = '';
|
|
1048
|
+
|
|
1049
|
+
if (param.options && Array.isArray(param.options)) {
|
|
1050
|
+
param.options.forEach(option => {
|
|
1051
|
+
const optionElement = document.createElement('option');
|
|
1052
|
+
optionElement.value = option;
|
|
1053
|
+
optionElement.textContent = formatLabel(String(option));
|
|
1054
|
+
multiSelect.appendChild(optionElement);
|
|
1055
|
+
});
|
|
1056
|
+
} else {
|
|
1057
|
+
console.warn(`No options found or options is not an array for parameter: ${name}`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
Array.from(multiSelect.options).forEach(option => {
|
|
1061
|
+
option.selected = value.includes(option.value);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
break;
|
|
1065
|
+
case 'integer-range':
|
|
1066
|
+
case 'float-range':
|
|
1067
|
+
const minSlider = document.getElementById(`${name}-min-slider`);
|
|
1068
|
+
const maxSlider = document.getElementById(`${name}-max-slider`);
|
|
1069
|
+
const minInput = document.getElementById(`${name}-min-input`);
|
|
1070
|
+
const maxInput = document.getElementById(`${name}-max-input`);
|
|
1071
|
+
|
|
1072
|
+
minSlider.min = param.min;
|
|
1073
|
+
minSlider.max = param.max;
|
|
1074
|
+
minSlider.step = param.step;
|
|
1075
|
+
maxSlider.min = param.min;
|
|
1076
|
+
maxSlider.max = param.max;
|
|
1077
|
+
maxSlider.step = param.step;
|
|
1078
|
+
minInput.min = param.min;
|
|
1079
|
+
minInput.max = param.max;
|
|
1080
|
+
minInput.step = param.step;
|
|
1081
|
+
maxInput.min = param.min;
|
|
1082
|
+
maxInput.max = param.max;
|
|
1083
|
+
maxInput.step = param.step;
|
|
1084
|
+
|
|
1085
|
+
minSlider.value = value[0];
|
|
1086
|
+
maxSlider.value = value[1];
|
|
1087
|
+
minInput.value = value[0];
|
|
1088
|
+
maxInput.value = value[1];
|
|
1089
|
+
|
|
1090
|
+
// Update the gradient background
|
|
1091
|
+
const sliderContainer = minSlider ? minSlider.closest('.range-slider-container') : null;
|
|
1092
|
+
if (sliderContainer) {
|
|
1093
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
1094
|
+
} else {
|
|
1095
|
+
console.warn(`Could not find slider container for range control: ${name}`);
|
|
1096
|
+
}
|
|
1097
|
+
break;
|
|
1098
|
+
case 'unbounded-integer':
|
|
1099
|
+
case 'unbounded-float':
|
|
1100
|
+
document.getElementById(`${name}-input`).value = value;
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Updates the background gradient for the dual range slider.
|
|
1107
|
+
* @param {HTMLInputElement} minSlider - The minimum value slider element.
|
|
1108
|
+
* @param {HTMLInputElement} maxSlider - The maximum value slider element.
|
|
1109
|
+
* @param {HTMLElement} container - The container element holding the sliders.
|
|
1110
|
+
*/
|
|
1111
|
+
function updateSliderGradient(minSlider, maxSlider, container) {
|
|
1112
|
+
const rangeMin = parseFloat(minSlider.min);
|
|
1113
|
+
const rangeMax = parseFloat(minSlider.max);
|
|
1114
|
+
const minVal = parseFloat(minSlider.value);
|
|
1115
|
+
const maxVal = parseFloat(maxSlider.value);
|
|
1116
|
+
|
|
1117
|
+
// Calculate percentages
|
|
1118
|
+
const range = rangeMax - rangeMin;
|
|
1119
|
+
// Prevent division by zero if min === max
|
|
1120
|
+
const minPercent = range === 0 ? 0 : ((minVal - rangeMin) / range) * 100;
|
|
1121
|
+
const maxPercent = range === 0 ? 100 : ((maxVal - rangeMin) / range) * 100;
|
|
1122
|
+
|
|
1123
|
+
// Update CSS custom properties
|
|
1124
|
+
container.style.setProperty('--min-pos', `${minPercent}%`);
|
|
1125
|
+
container.style.setProperty('--max-pos', `${maxPercent}%`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Format parameter name as a label (capitalize each word)
|
|
1130
|
+
*/
|
|
1131
|
+
function formatLabel(name) {
|
|
1132
|
+
return name
|
|
1133
|
+
.replace(/_/g, ' ') // Replace underscores with spaces
|
|
1134
|
+
.replace(/\w\S*/g, function(txt) {
|
|
1135
|
+
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Update the plot with current state
|
|
1141
|
+
*/
|
|
1142
|
+
function updatePlot() {
|
|
1143
|
+
const plotImage = document.getElementById('plot-image');
|
|
1144
|
+
if (!plotImage) return;
|
|
1145
|
+
|
|
1146
|
+
// Clear any existing loading timeout
|
|
1147
|
+
if (loadingTimeout) {
|
|
1148
|
+
clearTimeout(loadingTimeout);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Show loading state after threshold
|
|
1152
|
+
loadingTimeout = setTimeout(() => {
|
|
1153
|
+
// Create slow loading image if not cached
|
|
1154
|
+
if (!slowLoadingImage) {
|
|
1155
|
+
slowLoadingImage = createSlowLoadingImage();
|
|
1156
|
+
}
|
|
1157
|
+
plotImage.src = slowLoadingImage;
|
|
1158
|
+
plotImage.style.opacity = '0.5';
|
|
1159
|
+
}, updateThreshold * 1000);
|
|
1160
|
+
|
|
1161
|
+
// Build query string from state
|
|
1162
|
+
const queryParams = new URLSearchParams();
|
|
1163
|
+
for (const [name, value] of Object.entries(state)) {
|
|
1164
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
1165
|
+
queryParams.append(name, JSON.stringify(value));
|
|
1166
|
+
} else {
|
|
1167
|
+
queryParams.append(name, value);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Set the image source to the plot endpoint with parameters
|
|
1172
|
+
const url = `/plot?${queryParams.toString()}`;
|
|
1173
|
+
|
|
1174
|
+
// Create a new image object
|
|
1175
|
+
const newImage = new Image();
|
|
1176
|
+
newImage.onload = function() {
|
|
1177
|
+
// Clear loading timeout
|
|
1178
|
+
if (loadingTimeout) {
|
|
1179
|
+
clearTimeout(loadingTimeout);
|
|
1180
|
+
loadingTimeout = null;
|
|
1181
|
+
}
|
|
1182
|
+
plotImage.src = url;
|
|
1183
|
+
plotImage.style.opacity = 1;
|
|
1184
|
+
};
|
|
1185
|
+
newImage.onerror = function() {
|
|
1186
|
+
// Clear loading timeout
|
|
1187
|
+
if (loadingTimeout) {
|
|
1188
|
+
clearTimeout(loadingTimeout);
|
|
1189
|
+
loadingTimeout = null;
|
|
1190
|
+
}
|
|
1191
|
+
updateStatus('Error loading plot');
|
|
1192
|
+
plotImage.style.opacity = 1;
|
|
1193
|
+
};
|
|
1194
|
+
newImage.src = url;
|
|
1195
|
+
}
|