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