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.
@@ -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
+ }