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