syd 0.1.6__py3-none-any.whl → 0.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 -3
- syd/flask_deployment/__init__.py +7 -0
- syd/flask_deployment/deployer.py +594 -291
- syd/flask_deployment/static/__init__.py +1 -0
- syd/flask_deployment/static/css/styles.css +226 -19
- syd/flask_deployment/static/js/viewer.js +744 -0
- syd/flask_deployment/templates/__init__.py +1 -0
- syd/flask_deployment/templates/index.html +34 -0
- syd/flask_deployment/testing_principles.md +300 -0
- syd/notebook_deployment/__init__.py +1 -1
- syd/notebook_deployment/deployer.py +139 -53
- syd/notebook_deployment/widgets.py +214 -123
- syd/parameters.py +295 -393
- syd/support.py +168 -0
- syd/{interactive_viewer.py → viewer.py} +393 -470
- syd-0.2.0.dist-info/METADATA +126 -0
- syd-0.2.0.dist-info/RECORD +19 -0
- syd/flask_deployment/components.py +0 -497
- syd/flask_deployment/static/js/components.js +0 -51
- syd/flask_deployment/templates/base.html +0 -26
- syd/flask_deployment/templates/viewer.html +0 -97
- syd-0.1.6.dist-info/METADATA +0 -106
- syd-0.1.6.dist-info/RECORD +0 -18
- {syd-0.1.6.dist-info → syd-0.2.0.dist-info}/WHEEL +0 -0
- {syd-0.1.6.dist-info → syd-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syd Viewer JavaScript for Flask deployment
|
|
3
|
+
* Handles dynamic creation of UI components and interaction with the Flask backend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// State object to store current values
|
|
7
|
+
let state = {};
|
|
8
|
+
let paramInfo = {};
|
|
9
|
+
|
|
10
|
+
// Config object parsed from HTML data attributes
|
|
11
|
+
const config = {
|
|
12
|
+
figureWidth: parseFloat(document.getElementById('viewer-config').dataset.figureWidth || 8.0),
|
|
13
|
+
figureHeight: parseFloat(document.getElementById('viewer-config').dataset.figureHeight || 6.0),
|
|
14
|
+
controlsPosition: document.getElementById('viewer-config').dataset.controlsPosition || 'left',
|
|
15
|
+
controlsWidthPercent: parseInt(document.getElementById('viewer-config').dataset.controlsWidthPercent || 30)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Track whether we're currently in an update operation
|
|
19
|
+
let isUpdating = false;
|
|
20
|
+
|
|
21
|
+
// Initialize the viewer
|
|
22
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
23
|
+
// Fetch initial parameter information from server
|
|
24
|
+
fetch('/init-data')
|
|
25
|
+
.then(response => response.json())
|
|
26
|
+
.then(data => {
|
|
27
|
+
paramInfo = data.params;
|
|
28
|
+
|
|
29
|
+
// Initialize state from parameter info
|
|
30
|
+
for (const [name, param] of Object.entries(paramInfo)) {
|
|
31
|
+
state[name] = param.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create UI controls for each parameter
|
|
35
|
+
createControls();
|
|
36
|
+
|
|
37
|
+
// Generate initial plot
|
|
38
|
+
updatePlot();
|
|
39
|
+
})
|
|
40
|
+
.catch(error => {
|
|
41
|
+
console.error('Error initializing viewer:', error);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create UI controls based on parameter types
|
|
47
|
+
*/
|
|
48
|
+
function createControls() {
|
|
49
|
+
const controlsContainer = document.getElementById('controls-container');
|
|
50
|
+
|
|
51
|
+
// Clear any existing controls
|
|
52
|
+
controlsContainer.innerHTML = '';
|
|
53
|
+
|
|
54
|
+
// Create controls for each parameter
|
|
55
|
+
for (const [name, param] of Object.entries(paramInfo)) {
|
|
56
|
+
// Create control group
|
|
57
|
+
const controlGroup = createControlGroup(name, param);
|
|
58
|
+
|
|
59
|
+
// Add to container
|
|
60
|
+
if (controlGroup) {
|
|
61
|
+
controlsContainer.appendChild(controlGroup);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a control group for a parameter
|
|
68
|
+
*/
|
|
69
|
+
function createControlGroup(name, param) {
|
|
70
|
+
// Skip if param type is unknown
|
|
71
|
+
if (!param.type || param.type === 'unknown') {
|
|
72
|
+
console.warn(`Unknown parameter type for ${name}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create control group div
|
|
77
|
+
const controlGroup = document.createElement('div');
|
|
78
|
+
controlGroup.className = 'control-group';
|
|
79
|
+
controlGroup.id = `control-group-${name}`;
|
|
80
|
+
|
|
81
|
+
// Add label
|
|
82
|
+
const label = document.createElement('span');
|
|
83
|
+
label.className = 'control-label';
|
|
84
|
+
label.textContent = formatLabel(name);
|
|
85
|
+
controlGroup.appendChild(label);
|
|
86
|
+
|
|
87
|
+
// Create specific control based on parameter type
|
|
88
|
+
const control = createControl(name, param);
|
|
89
|
+
if (control) {
|
|
90
|
+
controlGroup.appendChild(control);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return controlGroup;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a specific control based on parameter type
|
|
98
|
+
*/
|
|
99
|
+
function createControl(name, param) {
|
|
100
|
+
switch (param.type) {
|
|
101
|
+
case 'text':
|
|
102
|
+
return createTextControl(name, param);
|
|
103
|
+
case 'boolean':
|
|
104
|
+
return createBooleanControl(name, param);
|
|
105
|
+
case 'integer':
|
|
106
|
+
return createIntegerControl(name, param);
|
|
107
|
+
case 'float':
|
|
108
|
+
return createFloatControl(name, param);
|
|
109
|
+
case 'selection':
|
|
110
|
+
return createSelectionControl(name, param);
|
|
111
|
+
case 'multiple-selection':
|
|
112
|
+
return createMultipleSelectionControl(name, param);
|
|
113
|
+
case 'integer-range':
|
|
114
|
+
return createIntegerRangeControl(name, param);
|
|
115
|
+
case 'float-range':
|
|
116
|
+
return createFloatRangeControl(name, param);
|
|
117
|
+
case 'unbounded-integer':
|
|
118
|
+
return createUnboundedIntegerControl(name, param);
|
|
119
|
+
case 'unbounded-float':
|
|
120
|
+
return createUnboundedFloatControl(name, param);
|
|
121
|
+
case 'button':
|
|
122
|
+
return createButtonControl(name, param);
|
|
123
|
+
default:
|
|
124
|
+
console.warn(`No control implementation for type: ${param.type}`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create text input control
|
|
131
|
+
*/
|
|
132
|
+
function createTextControl(name, param) {
|
|
133
|
+
const input = document.createElement('input');
|
|
134
|
+
input.type = 'text';
|
|
135
|
+
input.id = `${name}-input`;
|
|
136
|
+
input.value = param.value || '';
|
|
137
|
+
|
|
138
|
+
input.addEventListener('change', function() {
|
|
139
|
+
updateParameter(name, this.value);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return input;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create boolean checkbox control
|
|
147
|
+
*/
|
|
148
|
+
function createBooleanControl(name, param) {
|
|
149
|
+
const container = document.createElement('div');
|
|
150
|
+
container.className = 'checkbox-container';
|
|
151
|
+
|
|
152
|
+
const checkbox = document.createElement('input');
|
|
153
|
+
checkbox.type = 'checkbox';
|
|
154
|
+
checkbox.id = `${name}-checkbox`;
|
|
155
|
+
checkbox.checked = param.value === true;
|
|
156
|
+
|
|
157
|
+
checkbox.addEventListener('change', function() {
|
|
158
|
+
updateParameter(name, this.checked);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
container.appendChild(checkbox);
|
|
162
|
+
return container;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create integer control with slider and number input
|
|
167
|
+
*/
|
|
168
|
+
function createIntegerControl(name, param) {
|
|
169
|
+
const container = document.createElement('div');
|
|
170
|
+
container.className = 'numeric-control';
|
|
171
|
+
|
|
172
|
+
// Create slider
|
|
173
|
+
const slider = document.createElement('input');
|
|
174
|
+
slider.type = 'range';
|
|
175
|
+
slider.id = `${name}-slider`;
|
|
176
|
+
slider.min = param.min;
|
|
177
|
+
slider.max = param.max;
|
|
178
|
+
slider.step = param.step || 1;
|
|
179
|
+
slider.value = param.value;
|
|
180
|
+
|
|
181
|
+
// Create number input
|
|
182
|
+
const input = document.createElement('input');
|
|
183
|
+
input.type = 'number';
|
|
184
|
+
input.id = `${name}-input`;
|
|
185
|
+
input.min = param.min;
|
|
186
|
+
input.max = param.max;
|
|
187
|
+
input.step = param.step || 1;
|
|
188
|
+
input.value = param.value;
|
|
189
|
+
|
|
190
|
+
// Add event listeners
|
|
191
|
+
slider.addEventListener('input', function() {
|
|
192
|
+
const value = parseInt(this.value, 10);
|
|
193
|
+
input.value = value;
|
|
194
|
+
updateParameter(name, value);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
input.addEventListener('change', function() {
|
|
198
|
+
const value = parseInt(this.value, 10);
|
|
199
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
200
|
+
slider.value = value;
|
|
201
|
+
updateParameter(name, value);
|
|
202
|
+
} else {
|
|
203
|
+
this.value = state[name]; // Revert to current state
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
container.appendChild(slider);
|
|
208
|
+
container.appendChild(input);
|
|
209
|
+
return container;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Create float control with slider and number input
|
|
214
|
+
*/
|
|
215
|
+
function createFloatControl(name, param) {
|
|
216
|
+
const container = document.createElement('div');
|
|
217
|
+
container.className = 'numeric-control';
|
|
218
|
+
|
|
219
|
+
// Create slider
|
|
220
|
+
const slider = document.createElement('input');
|
|
221
|
+
slider.type = 'range';
|
|
222
|
+
slider.id = `${name}-slider`;
|
|
223
|
+
slider.min = param.min;
|
|
224
|
+
slider.max = param.max;
|
|
225
|
+
slider.step = param.step || 0.01;
|
|
226
|
+
slider.value = param.value;
|
|
227
|
+
|
|
228
|
+
// Create number input
|
|
229
|
+
const input = document.createElement('input');
|
|
230
|
+
input.type = 'number';
|
|
231
|
+
input.id = `${name}-input`;
|
|
232
|
+
input.min = param.min;
|
|
233
|
+
input.max = param.max;
|
|
234
|
+
input.step = param.step || 0.01;
|
|
235
|
+
input.value = param.value;
|
|
236
|
+
|
|
237
|
+
// Add event listeners
|
|
238
|
+
slider.addEventListener('input', function() {
|
|
239
|
+
const value = parseFloat(this.value);
|
|
240
|
+
input.value = value;
|
|
241
|
+
updateParameter(name, value);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
input.addEventListener('change', function() {
|
|
245
|
+
const value = parseFloat(this.value);
|
|
246
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
247
|
+
slider.value = value;
|
|
248
|
+
updateParameter(name, value);
|
|
249
|
+
} else {
|
|
250
|
+
this.value = state[name]; // Revert to current state
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
container.appendChild(slider);
|
|
255
|
+
container.appendChild(input);
|
|
256
|
+
return container;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create selection dropdown control
|
|
261
|
+
*/
|
|
262
|
+
function createSelectionControl(name, param) {
|
|
263
|
+
const select = document.createElement('select');
|
|
264
|
+
select.id = `${name}-select`;
|
|
265
|
+
|
|
266
|
+
// Add options
|
|
267
|
+
param.options.forEach(option => {
|
|
268
|
+
const optionElement = document.createElement('option');
|
|
269
|
+
optionElement.value = option;
|
|
270
|
+
optionElement.textContent = formatLabel(String(option));
|
|
271
|
+
// Store the original type information as a data attribute
|
|
272
|
+
optionElement.dataset.originalType = typeof option;
|
|
273
|
+
// For float values, also store the original value for exact comparison
|
|
274
|
+
if (typeof option === 'number') {
|
|
275
|
+
optionElement.dataset.originalValue = option;
|
|
276
|
+
}
|
|
277
|
+
select.appendChild(optionElement);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Set default value
|
|
281
|
+
select.value = param.value;
|
|
282
|
+
|
|
283
|
+
// Add event listener
|
|
284
|
+
select.addEventListener('change', function() {
|
|
285
|
+
// Get the selected option element
|
|
286
|
+
const selectedOption = this.options[this.selectedIndex];
|
|
287
|
+
let valueToSend = this.value;
|
|
288
|
+
|
|
289
|
+
// Convert back to the original type if needed
|
|
290
|
+
if (selectedOption.dataset.originalType === 'number') {
|
|
291
|
+
// Use the original value from the dataset for exact precision with floats
|
|
292
|
+
if (selectedOption.dataset.originalValue) {
|
|
293
|
+
valueToSend = parseFloat(selectedOption.dataset.originalValue);
|
|
294
|
+
} else {
|
|
295
|
+
valueToSend = parseFloat(valueToSend);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
updateParameter(name, valueToSend);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return select;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create multiple selection control
|
|
307
|
+
*/
|
|
308
|
+
function createMultipleSelectionControl(name, param) {
|
|
309
|
+
const container = document.createElement('div');
|
|
310
|
+
container.className = 'multiple-selection-container';
|
|
311
|
+
|
|
312
|
+
// Create select element
|
|
313
|
+
const select = document.createElement('select');
|
|
314
|
+
select.id = `${name}-select`;
|
|
315
|
+
select.className = 'multiple-select';
|
|
316
|
+
select.multiple = true;
|
|
317
|
+
|
|
318
|
+
// Add options
|
|
319
|
+
param.options.forEach(option => {
|
|
320
|
+
const optionElement = document.createElement('option');
|
|
321
|
+
optionElement.value = option;
|
|
322
|
+
optionElement.textContent = formatLabel(String(option));
|
|
323
|
+
|
|
324
|
+
// Check if this option is selected
|
|
325
|
+
if (param.value.includes(option)) {
|
|
326
|
+
optionElement.selected = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
select.appendChild(optionElement);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Helper text
|
|
333
|
+
const helperText = document.createElement('div');
|
|
334
|
+
helperText.className = 'helper-text';
|
|
335
|
+
helperText.textContent = 'Ctrl+click to select multiple';
|
|
336
|
+
|
|
337
|
+
// Add event listener
|
|
338
|
+
select.addEventListener('change', function() {
|
|
339
|
+
// Get all selected options
|
|
340
|
+
const selectedValues = Array.from(this.selectedOptions).map(option => option.value);
|
|
341
|
+
updateParameter(name, selectedValues);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
container.appendChild(select);
|
|
345
|
+
container.appendChild(helperText);
|
|
346
|
+
return container;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Create integer range control with dual sliders
|
|
351
|
+
*/
|
|
352
|
+
function createIntegerRangeControl(name, param) {
|
|
353
|
+
return createRangeControl(name, param, parseInt);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Create float range control with dual sliders
|
|
358
|
+
*/
|
|
359
|
+
function createFloatRangeControl(name, param) {
|
|
360
|
+
return createRangeControl(name, param, parseFloat);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Generic range control creator
|
|
365
|
+
*/
|
|
366
|
+
function createRangeControl(name, param, converter) {
|
|
367
|
+
const container = document.createElement('div');
|
|
368
|
+
container.className = 'range-container';
|
|
369
|
+
|
|
370
|
+
// Create inputs container
|
|
371
|
+
const inputsContainer = document.createElement('div');
|
|
372
|
+
inputsContainer.className = 'range-inputs';
|
|
373
|
+
|
|
374
|
+
// Create min input
|
|
375
|
+
const minInput = document.createElement('input');
|
|
376
|
+
minInput.type = 'number';
|
|
377
|
+
minInput.id = `${name}-min-input`;
|
|
378
|
+
minInput.className = 'range-input';
|
|
379
|
+
minInput.min = param.min;
|
|
380
|
+
minInput.max = param.max;
|
|
381
|
+
minInput.step = param.step || 1;
|
|
382
|
+
minInput.value = param.value[0];
|
|
383
|
+
|
|
384
|
+
// Create slider container
|
|
385
|
+
const sliderContainer = document.createElement('div');
|
|
386
|
+
sliderContainer.className = 'range-slider-container';
|
|
387
|
+
|
|
388
|
+
// Create min slider
|
|
389
|
+
const minSlider = document.createElement('input');
|
|
390
|
+
minSlider.type = 'range';
|
|
391
|
+
minSlider.id = `${name}-min-slider`;
|
|
392
|
+
minSlider.className = 'range-slider min-slider';
|
|
393
|
+
minSlider.min = param.min;
|
|
394
|
+
minSlider.max = param.max;
|
|
395
|
+
minSlider.step = param.step || 1;
|
|
396
|
+
minSlider.value = param.value[0];
|
|
397
|
+
|
|
398
|
+
// Create max slider
|
|
399
|
+
const maxSlider = document.createElement('input');
|
|
400
|
+
maxSlider.type = 'range';
|
|
401
|
+
maxSlider.id = `${name}-max-slider`;
|
|
402
|
+
maxSlider.className = 'range-slider max-slider';
|
|
403
|
+
maxSlider.min = param.min;
|
|
404
|
+
maxSlider.max = param.max;
|
|
405
|
+
maxSlider.step = param.step || 1;
|
|
406
|
+
maxSlider.value = param.value[1];
|
|
407
|
+
|
|
408
|
+
// Create max input
|
|
409
|
+
const maxInput = document.createElement('input');
|
|
410
|
+
maxInput.type = 'number';
|
|
411
|
+
maxInput.id = `${name}-max-input`;
|
|
412
|
+
maxInput.className = 'range-input';
|
|
413
|
+
maxInput.min = param.min;
|
|
414
|
+
maxInput.max = param.max;
|
|
415
|
+
maxInput.step = param.step || 1;
|
|
416
|
+
maxInput.value = param.value[1];
|
|
417
|
+
|
|
418
|
+
// Range display
|
|
419
|
+
const rangeDisplay = document.createElement('div');
|
|
420
|
+
rangeDisplay.className = 'range-display';
|
|
421
|
+
rangeDisplay.id = `${name}-range-display`;
|
|
422
|
+
rangeDisplay.textContent = `Range: ${param.value[0]} - ${param.value[1]}`;
|
|
423
|
+
|
|
424
|
+
// Add event listeners
|
|
425
|
+
minSlider.addEventListener('input', function() {
|
|
426
|
+
const minVal = converter(this.value);
|
|
427
|
+
const maxVal = converter(maxSlider.value);
|
|
428
|
+
|
|
429
|
+
if (minVal <= maxVal) {
|
|
430
|
+
state[name] = [minVal, maxVal];
|
|
431
|
+
minInput.value = minVal;
|
|
432
|
+
updateRangeDisplay(rangeDisplay, minVal, maxVal);
|
|
433
|
+
updateParameter(name, [minVal, maxVal]);
|
|
434
|
+
} else {
|
|
435
|
+
this.value = maxVal;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
maxSlider.addEventListener('input', function() {
|
|
440
|
+
const minVal = converter(minSlider.value);
|
|
441
|
+
const maxVal = converter(this.value);
|
|
442
|
+
|
|
443
|
+
if (maxVal >= minVal) {
|
|
444
|
+
state[name] = [minVal, maxVal];
|
|
445
|
+
maxInput.value = maxVal;
|
|
446
|
+
updateRangeDisplay(rangeDisplay, minVal, maxVal);
|
|
447
|
+
updateParameter(name, [minVal, maxVal]);
|
|
448
|
+
} else {
|
|
449
|
+
this.value = minVal;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
minInput.addEventListener('change', function() {
|
|
454
|
+
const minVal = converter(this.value);
|
|
455
|
+
const maxVal = converter(maxInput.value);
|
|
456
|
+
|
|
457
|
+
if (!isNaN(minVal) && minVal >= param.min && minVal <= maxVal) {
|
|
458
|
+
state[name] = [minVal, maxVal];
|
|
459
|
+
minSlider.value = minVal;
|
|
460
|
+
updateRangeDisplay(rangeDisplay, minVal, maxVal);
|
|
461
|
+
updateParameter(name, [minVal, maxVal]);
|
|
462
|
+
} else {
|
|
463
|
+
this.value = state[name][0];
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
maxInput.addEventListener('change', function() {
|
|
468
|
+
const minVal = converter(minInput.value);
|
|
469
|
+
const maxVal = converter(this.value);
|
|
470
|
+
|
|
471
|
+
if (!isNaN(maxVal) && maxVal <= param.max && maxVal >= minVal) {
|
|
472
|
+
state[name] = [minVal, maxVal];
|
|
473
|
+
maxSlider.value = maxVal;
|
|
474
|
+
updateRangeDisplay(rangeDisplay, minVal, maxVal);
|
|
475
|
+
updateParameter(name, [minVal, maxVal]);
|
|
476
|
+
} else {
|
|
477
|
+
this.value = state[name][1];
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Assemble the control
|
|
482
|
+
inputsContainer.appendChild(minInput);
|
|
483
|
+
inputsContainer.appendChild(maxInput);
|
|
484
|
+
|
|
485
|
+
sliderContainer.appendChild(minSlider);
|
|
486
|
+
sliderContainer.appendChild(maxSlider);
|
|
487
|
+
|
|
488
|
+
container.appendChild(inputsContainer);
|
|
489
|
+
container.appendChild(sliderContainer);
|
|
490
|
+
container.appendChild(rangeDisplay);
|
|
491
|
+
|
|
492
|
+
return container;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Update range display text
|
|
497
|
+
*/
|
|
498
|
+
function updateRangeDisplay(displayElement, min, max) {
|
|
499
|
+
displayElement.textContent = `Range: ${min} - ${max}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Create unbounded integer control
|
|
504
|
+
*/
|
|
505
|
+
function createUnboundedIntegerControl(name, param) {
|
|
506
|
+
const input = document.createElement('input');
|
|
507
|
+
input.type = 'number';
|
|
508
|
+
input.id = `${name}-input`;
|
|
509
|
+
input.value = param.value;
|
|
510
|
+
input.step = 1;
|
|
511
|
+
|
|
512
|
+
input.addEventListener('change', function() {
|
|
513
|
+
const value = parseInt(this.value, 10);
|
|
514
|
+
if (!isNaN(value)) {
|
|
515
|
+
updateParameter(name, value);
|
|
516
|
+
} else {
|
|
517
|
+
this.value = state[name];
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return input;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Create unbounded float control
|
|
526
|
+
*/
|
|
527
|
+
function createUnboundedFloatControl(name, param) {
|
|
528
|
+
const input = document.createElement('input');
|
|
529
|
+
input.type = 'number';
|
|
530
|
+
input.id = `${name}-input`;
|
|
531
|
+
input.value = param.value;
|
|
532
|
+
input.step = param.step || 'any';
|
|
533
|
+
|
|
534
|
+
input.addEventListener('change', function() {
|
|
535
|
+
const value = parseFloat(this.value);
|
|
536
|
+
if (!isNaN(value)) {
|
|
537
|
+
updateParameter(name, value);
|
|
538
|
+
} else {
|
|
539
|
+
this.value = state[name];
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return input;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Create button control
|
|
548
|
+
*/
|
|
549
|
+
function createButtonControl(name, param) {
|
|
550
|
+
const button = document.createElement('button');
|
|
551
|
+
button.id = `${name}-button`;
|
|
552
|
+
button.textContent = param.label || name;
|
|
553
|
+
|
|
554
|
+
button.addEventListener('click', function() {
|
|
555
|
+
// Show button as active
|
|
556
|
+
button.classList.add('active');
|
|
557
|
+
|
|
558
|
+
// Send action to the server
|
|
559
|
+
fetch('/update-param', {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
headers: {
|
|
562
|
+
'Content-Type': 'application/json',
|
|
563
|
+
},
|
|
564
|
+
body: JSON.stringify({
|
|
565
|
+
name: name,
|
|
566
|
+
value: null, // Value is not used for buttons
|
|
567
|
+
action: true
|
|
568
|
+
}),
|
|
569
|
+
})
|
|
570
|
+
.then(response => response.json())
|
|
571
|
+
.then(data => {
|
|
572
|
+
// Remove active class
|
|
573
|
+
button.classList.remove('active');
|
|
574
|
+
|
|
575
|
+
if (data.error) {
|
|
576
|
+
console.error('Error:', data.error);
|
|
577
|
+
} else {
|
|
578
|
+
// Update state with any changes from callbacks
|
|
579
|
+
updateStateFromServer(data.state);
|
|
580
|
+
// Update plot if needed
|
|
581
|
+
updatePlot();
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
.catch(error => {
|
|
585
|
+
// Remove active class
|
|
586
|
+
button.classList.remove('active');
|
|
587
|
+
console.error('Error:', error);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return button;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Update a parameter value and send to server
|
|
596
|
+
*/
|
|
597
|
+
function updateParameter(name, value) {
|
|
598
|
+
// Prevent recursive updates
|
|
599
|
+
if (isUpdating) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Update local state
|
|
604
|
+
state[name] = value;
|
|
605
|
+
|
|
606
|
+
// Send update to server
|
|
607
|
+
fetch('/update-param', {
|
|
608
|
+
method: 'POST',
|
|
609
|
+
headers: {
|
|
610
|
+
'Content-Type': 'application/json',
|
|
611
|
+
},
|
|
612
|
+
body: JSON.stringify({
|
|
613
|
+
name: name,
|
|
614
|
+
value: value
|
|
615
|
+
}),
|
|
616
|
+
})
|
|
617
|
+
.then(response => response.json())
|
|
618
|
+
.then(data => {
|
|
619
|
+
if (data.error) {
|
|
620
|
+
console.error('Error:', data.error);
|
|
621
|
+
} else {
|
|
622
|
+
// Update state with any changes from callbacks
|
|
623
|
+
updateStateFromServer(data.state);
|
|
624
|
+
// Update plot
|
|
625
|
+
updatePlot();
|
|
626
|
+
}
|
|
627
|
+
})
|
|
628
|
+
.catch(error => {
|
|
629
|
+
console.error('Error:', error);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Update local state from server response
|
|
635
|
+
*/
|
|
636
|
+
function updateStateFromServer(serverState) {
|
|
637
|
+
// Set updating flag to prevent recursive updates
|
|
638
|
+
isUpdating = true;
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
// Update any parameters that changed due to callbacks
|
|
642
|
+
for (const [name, value] of Object.entries(serverState)) {
|
|
643
|
+
if (JSON.stringify(state[name]) !== JSON.stringify(value)) {
|
|
644
|
+
state[name] = value;
|
|
645
|
+
updateControlValue(name, value);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} finally {
|
|
649
|
+
// Clear updating flag
|
|
650
|
+
isUpdating = false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Update a control's value in the UI
|
|
656
|
+
*/
|
|
657
|
+
function updateControlValue(name, value) {
|
|
658
|
+
if (!paramInfo[name]) return;
|
|
659
|
+
|
|
660
|
+
const param = paramInfo[name];
|
|
661
|
+
|
|
662
|
+
switch (param.type) {
|
|
663
|
+
case 'text':
|
|
664
|
+
document.getElementById(`${name}-input`).value = value;
|
|
665
|
+
break;
|
|
666
|
+
case 'boolean':
|
|
667
|
+
document.getElementById(`${name}-checkbox`).checked = value === true;
|
|
668
|
+
break;
|
|
669
|
+
case 'integer':
|
|
670
|
+
case 'float':
|
|
671
|
+
document.getElementById(`${name}-slider`).value = value;
|
|
672
|
+
document.getElementById(`${name}-input`).value = value;
|
|
673
|
+
break;
|
|
674
|
+
case 'selection':
|
|
675
|
+
document.getElementById(`${name}-select`).value = value;
|
|
676
|
+
break;
|
|
677
|
+
case 'multiple-selection':
|
|
678
|
+
const select = document.getElementById(`${name}-select`);
|
|
679
|
+
if (select) {
|
|
680
|
+
Array.from(select.options).forEach(option => {
|
|
681
|
+
option.selected = value.includes(option.value);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
case 'integer-range':
|
|
686
|
+
case 'float-range':
|
|
687
|
+
document.getElementById(`${name}-min-slider`).value = value[0];
|
|
688
|
+
document.getElementById(`${name}-max-slider`).value = value[1];
|
|
689
|
+
document.getElementById(`${name}-min-input`).value = value[0];
|
|
690
|
+
document.getElementById(`${name}-max-input`).value = value[1];
|
|
691
|
+
const display = document.getElementById(`${name}-range-display`);
|
|
692
|
+
if (display) {
|
|
693
|
+
updateRangeDisplay(display, value[0], value[1]);
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
case 'unbounded-integer':
|
|
697
|
+
case 'unbounded-float':
|
|
698
|
+
document.getElementById(`${name}-input`).value = value;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Format parameter name as a label (capitalize each word)
|
|
705
|
+
*/
|
|
706
|
+
function formatLabel(name) {
|
|
707
|
+
return name
|
|
708
|
+
.replace(/_/g, ' ') // Replace underscores with spaces
|
|
709
|
+
.replace(/\w\S*/g, function(txt) {
|
|
710
|
+
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Update the plot with current state
|
|
716
|
+
*/
|
|
717
|
+
function updatePlot() {
|
|
718
|
+
// Build query string from state
|
|
719
|
+
const queryParams = new URLSearchParams();
|
|
720
|
+
|
|
721
|
+
for (const [name, value] of Object.entries(state)) {
|
|
722
|
+
// Handle arrays and special types by serializing to JSON
|
|
723
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
724
|
+
queryParams.append(name, JSON.stringify(value));
|
|
725
|
+
} else {
|
|
726
|
+
queryParams.append(name, value);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Set the image source to the plot endpoint with parameters
|
|
731
|
+
const url = `/plot?${queryParams.toString()}`;
|
|
732
|
+
const plotImage = document.getElementById('plot-image');
|
|
733
|
+
|
|
734
|
+
// Show loading indicator
|
|
735
|
+
plotImage.style.opacity = 0.5;
|
|
736
|
+
|
|
737
|
+
// Create a new image object
|
|
738
|
+
const newImage = new Image();
|
|
739
|
+
newImage.onload = function() {
|
|
740
|
+
plotImage.src = url;
|
|
741
|
+
plotImage.style.opacity = 1;
|
|
742
|
+
};
|
|
743
|
+
newImage.src = url;
|
|
744
|
+
}
|