syd 0.1.5__py3-none-any.whl → 0.1.6__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 CHANGED
@@ -1,15 +1,11 @@
1
- from typing import Callable
1
+ from typing import Callable, Optional
2
2
  from .interactive_viewer import InteractiveViewer
3
3
 
4
- __version__ = "0.1.5"
4
+ __version__ = "0.1.6"
5
5
 
6
6
 
7
- class DefaultViewer(InteractiveViewer):
8
- def plot(self, state):
9
- pass
10
-
11
-
12
- def make_viewer(plot_func: Callable):
13
- viewer = DefaultViewer()
14
- viewer.plot = plot_func
7
+ def make_viewer(plot_func: Optional[Callable] = None):
8
+ viewer = InteractiveViewer()
9
+ if plot_func is not None:
10
+ viewer.set_plot(plot_func)
15
11
  return viewer
File without changes
@@ -0,0 +1,497 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Generic, List, TypeVar, Union, Optional
3
+ from dataclasses import dataclass
4
+ from markupsafe import Markup
5
+
6
+ from ..parameters import (
7
+ Parameter,
8
+ TextParameter,
9
+ SelectionParameter,
10
+ MultipleSelectionParameter,
11
+ BooleanParameter,
12
+ IntegerParameter,
13
+ FloatParameter,
14
+ IntegerRangeParameter,
15
+ FloatRangeParameter,
16
+ UnboundedIntegerParameter,
17
+ UnboundedFloatParameter,
18
+ ButtonParameter,
19
+ )
20
+
21
+ T = TypeVar("T", bound=Parameter[Any])
22
+
23
+
24
+ class BaseWebComponent(Generic[T], ABC):
25
+ """
26
+ Abstract base class for all web components.
27
+
28
+ This class defines the interface for HTML/JS components that correspond
29
+ to different parameter types in the web deployment.
30
+ """
31
+
32
+ def __init__(self, parameter: T, component_id: str):
33
+ """
34
+ Initialize the web component.
35
+
36
+ Args:
37
+ parameter: The parameter this component represents
38
+ component_id: Unique ID for this component in the DOM
39
+ """
40
+ self.parameter = parameter
41
+ self.component_id = component_id
42
+
43
+ @abstractmethod
44
+ def render_html(self) -> str:
45
+ """
46
+ Render the HTML for this component.
47
+
48
+ Returns:
49
+ str: HTML markup for the component
50
+ """
51
+ pass
52
+
53
+ @abstractmethod
54
+ def get_js_init(self) -> str:
55
+ """
56
+ Get JavaScript code needed to initialize this component.
57
+
58
+ Returns:
59
+ str: JavaScript code that sets up event listeners etc.
60
+ """
61
+ pass
62
+
63
+ def get_js_update(self, value: Any) -> str:
64
+ """
65
+ Get JavaScript code needed to update this component's value.
66
+
67
+ Args:
68
+ value: New value to set
69
+
70
+ Returns:
71
+ str: JavaScript code that updates the component's value
72
+ """
73
+ # Default implementation for simple components
74
+ return f"document.getElementById('{self.component_id}').value = {self._value_to_js(value)};"
75
+
76
+ def _value_to_js(self, value: Any) -> str:
77
+ """Convert a Python value to its JavaScript representation."""
78
+ if isinstance(value, bool):
79
+ return str(value).lower()
80
+ elif isinstance(value, (int, float)):
81
+ return str(value)
82
+ elif isinstance(value, str):
83
+ return f"'{value}'"
84
+ elif isinstance(value, (list, tuple)):
85
+ return f"[{', '.join(self._value_to_js(v) for v in value)}]"
86
+ else:
87
+ raise ValueError(f"Cannot convert {type(value)} to JavaScript")
88
+
89
+
90
+ class TextComponent(BaseWebComponent[TextParameter]):
91
+ """Component for text input parameters."""
92
+
93
+ def render_html(self) -> str:
94
+ return f"""
95
+ <div class="form-group">
96
+ <label for="{self.component_id}">{self.parameter.name}</label>
97
+ <input type="text"
98
+ class="form-control"
99
+ id="{self.component_id}"
100
+ value="{self.parameter.value}">
101
+ </div>
102
+ """
103
+
104
+ def get_js_init(self) -> str:
105
+ return f"""
106
+ document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
107
+ updateParameter('{self.parameter.name}', e.target.value);
108
+ }});
109
+ """
110
+
111
+
112
+ class BooleanComponent(BaseWebComponent[BooleanParameter]):
113
+ """Component for boolean parameters."""
114
+
115
+ def render_html(self) -> str:
116
+ checked = "checked" if self.parameter.value else ""
117
+ return f"""
118
+ <div class="form-check">
119
+ <input type="checkbox"
120
+ class="form-check-input"
121
+ id="{self.component_id}"
122
+ {checked}>
123
+ <label class="form-check-label"
124
+ for="{self.component_id}">
125
+ {self.parameter.name}
126
+ </label>
127
+ </div>
128
+ """
129
+
130
+ def get_js_init(self) -> str:
131
+ return f"""
132
+ document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
133
+ updateParameter('{self.parameter.name}', e.target.checked);
134
+ }});
135
+ """
136
+
137
+ def get_js_update(self, value: bool) -> str:
138
+ return f"document.getElementById('{self.component_id}').checked = {str(value).lower()};"
139
+
140
+
141
+ class SelectionComponent(BaseWebComponent[SelectionParameter]):
142
+ """Component for single selection parameters."""
143
+
144
+ def render_html(self) -> str:
145
+ options = []
146
+ for opt in self.parameter.options:
147
+ selected = "selected" if opt == self.parameter.value else ""
148
+ options.append(f'<option value="{opt}" {selected}>{opt}</option>')
149
+
150
+ return f"""
151
+ <div class="form-group">
152
+ <label for="{self.component_id}">{self.parameter.name}</label>
153
+ <select class="form-control" id="{self.component_id}">
154
+ {"".join(options)}
155
+ </select>
156
+ </div>
157
+ """
158
+
159
+ def get_js_init(self) -> str:
160
+ return f"""
161
+ document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
162
+ updateParameter('{self.parameter.name}', e.target.value);
163
+ }});
164
+ """
165
+
166
+
167
+ class MultipleSelectionComponent(BaseWebComponent[MultipleSelectionParameter]):
168
+ """Component for multiple selection parameters."""
169
+
170
+ def render_html(self) -> str:
171
+ options = []
172
+ for opt in self.parameter.options:
173
+ selected = "selected" if opt in self.parameter.value else ""
174
+ options.append(f'<option value="{opt}" {selected}>{opt}</option>')
175
+
176
+ return f"""
177
+ <div class="form-group">
178
+ <label for="{self.component_id}">{self.parameter.name}</label>
179
+ <select multiple class="form-control" id="{self.component_id}">
180
+ {"".join(options)}
181
+ </select>
182
+ </div>
183
+ """
184
+
185
+ def get_js_init(self) -> str:
186
+ return f"""
187
+ document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
188
+ const selected = Array.from(e.target.selectedOptions).map(opt => opt.value);
189
+ updateParameter('{self.parameter.name}', selected);
190
+ }});
191
+ """
192
+
193
+ def get_js_update(self, values: List[str]) -> str:
194
+ # More complex update for multi-select
195
+ return f"""
196
+ const sel = document.getElementById('{self.component_id}');
197
+ Array.from(sel.options).forEach(opt => {{
198
+ opt.selected = {self._value_to_js(values)}.includes(opt.value);
199
+ }});
200
+ """
201
+
202
+
203
+ class SliderMixin:
204
+ """Shared functionality for slider components."""
205
+
206
+ def _get_slider_js_init(self, component_id: str, param_name: str) -> str:
207
+ return f"""
208
+ noUiSlider.create(document.getElementById('{component_id}'), {{
209
+ start: {self._value_to_js(self.parameter.value)},
210
+ connect: true,
211
+ range: {{
212
+ 'min': {self.parameter.min_value},
213
+ 'max': {self.parameter.max_value}
214
+ }}
215
+ }}).on('change', (values) => {{
216
+ updateParameter('{param_name}', parseFloat(values[0]));
217
+ const value = parseFloat(values[0]);
218
+ // Update the display text
219
+ document.getElementById('{component_id}_display').textContent = value.toFixed(2);
220
+ debouncedUpdateParameter('{param_name}', value);
221
+ }});
222
+ """
223
+
224
+
225
+ class IntegerComponent(SliderMixin, BaseWebComponent[IntegerParameter]):
226
+ """Component for integer parameters."""
227
+
228
+ def render_html(self) -> str:
229
+ return f"""
230
+ <div class="form-group">
231
+ <label for="{self.component_id}">{self.parameter.name}</label>
232
+ <div id="{self.component_id}" class="slider"></div>
233
+ <span id="{self.component_id}_display" class="value-display">{self.parameter.value}</span>
234
+ </div>
235
+ """
236
+
237
+ def get_js_init(self) -> str:
238
+ return self._get_slider_js_init(self.component_id, self.parameter.name)
239
+
240
+
241
+ class FloatComponent(SliderMixin, BaseWebComponent[FloatParameter]):
242
+ """Component for float parameters."""
243
+
244
+ def render_html(self) -> str:
245
+ return f"""
246
+ <div class="form-group">
247
+ <label for="{self.component_id}">{self.parameter.name}</label>
248
+ <div id="{self.component_id}" class="slider"></div>
249
+ <span id="{self.component_id}_display" class="value-display">{self.parameter.value:.2f}</span>
250
+ </div>
251
+ """
252
+
253
+ def get_js_init(self) -> str:
254
+ return self._get_slider_js_init(self.component_id, self.parameter.name)
255
+
256
+
257
+ class RangeSliderMixin:
258
+ """Shared functionality for range slider components."""
259
+
260
+ def _get_range_slider_js_init(self, component_id: str, param_name: str) -> str:
261
+ return f"""
262
+ noUiSlider.create(document.getElementById('{component_id}'), {{
263
+ start: {self._value_to_js(self.parameter.value)},
264
+ connect: true,
265
+ range: {{
266
+ 'min': {self.parameter.min_value},
267
+ 'max': {self.parameter.max_value}
268
+ }}
269
+ }}).on('change', (values) => {{
270
+ updateParameter('{param_name}', [
271
+ parseFloat(values[0]),
272
+ parseFloat(values[1])
273
+ ]);
274
+ }});
275
+ """
276
+
277
+
278
+ class IntegerRangeComponent(RangeSliderMixin, BaseWebComponent[IntegerRangeParameter]):
279
+ """Component for integer range parameters."""
280
+
281
+ def render_html(self) -> str:
282
+ low, high = self.parameter.value
283
+ return f"""
284
+ <div class="form-group">
285
+ <label for="{self.component_id}">{self.parameter.name}</label>
286
+ <div id="{self.component_id}" class="range-slider"></div>
287
+ <span class="value-display">{low} - {high}</span>
288
+ </div>
289
+ """
290
+
291
+ def get_js_init(self) -> str:
292
+ return self._get_range_slider_js_init(self.component_id, self.parameter.name)
293
+
294
+
295
+ class FloatRangeComponent(RangeSliderMixin, BaseWebComponent[FloatRangeParameter]):
296
+ """Component for float range parameters."""
297
+
298
+ def render_html(self) -> str:
299
+ low, high = self.parameter.value
300
+ return f"""
301
+ <div class="form-group">
302
+ <label for="{self.component_id}">{self.parameter.name}</label>
303
+ <div id="{self.component_id}" class="range-slider"></div>
304
+ <span class="value-display">{low:.2f} - {high:.2f}</span>
305
+ </div>
306
+ """
307
+
308
+ def get_js_init(self) -> str:
309
+ return self._get_range_slider_js_init(self.component_id, self.parameter.name)
310
+
311
+
312
+ class UnboundedIntegerComponent(BaseWebComponent[UnboundedIntegerParameter]):
313
+ """Component for unbounded integer parameters."""
314
+
315
+ def render_html(self) -> str:
316
+ min_attr = (
317
+ f'min="{self.parameter.min_value}"'
318
+ if self.parameter.min_value is not None
319
+ else ""
320
+ )
321
+ max_attr = (
322
+ f'max="{self.parameter.max_value}"'
323
+ if self.parameter.max_value is not None
324
+ else ""
325
+ )
326
+
327
+ return f"""
328
+ <div class="form-group">
329
+ <label for="{self.component_id}">{self.parameter.name}</label>
330
+ <input type="number"
331
+ class="form-control"
332
+ id="{self.component_id}"
333
+ value="{self.parameter.value}"
334
+ {min_attr}
335
+ {max_attr}
336
+ step="1">
337
+ </div>
338
+ """
339
+
340
+ def get_js_init(self) -> str:
341
+ return f"""
342
+ document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
343
+ updateParameter('{self.parameter.name}', parseInt(e.target.value));
344
+ }});
345
+ """
346
+
347
+
348
+ class UnboundedFloatComponent(BaseWebComponent[UnboundedFloatParameter]):
349
+ """Component for unbounded float parameters."""
350
+
351
+ def render_html(self) -> str:
352
+ min_attr = (
353
+ f'min="{self.parameter.min_value}"'
354
+ if self.parameter.min_value is not None
355
+ else ""
356
+ )
357
+ max_attr = (
358
+ f'max="{self.parameter.max_value}"'
359
+ if self.parameter.max_value is not None
360
+ else ""
361
+ )
362
+ step_attr = (
363
+ f'step="{self.parameter.step}"' if self.parameter.step is not None else ""
364
+ )
365
+
366
+ return f"""
367
+ <div class="form-group">
368
+ <label for="{self.component_id}">{self.parameter.name}</label>
369
+ <input type="number"
370
+ class="form-control"
371
+ id="{self.component_id}"
372
+ value="{self.parameter.value}"
373
+ {min_attr}
374
+ {max_attr}
375
+ {step_attr}>
376
+ </div>
377
+ """
378
+
379
+ def get_js_init(self) -> str:
380
+ return f"""
381
+ document.getElementById('{self.component_id}').addEventListener('change', (e) => {{
382
+ updateParameter('{self.parameter.name}', parseFloat(e.target.value));
383
+ }});
384
+ """
385
+
386
+
387
+ class ButtonComponent(BaseWebComponent[ButtonParameter]):
388
+ """Component for button parameters."""
389
+
390
+ def render_html(self) -> str:
391
+ return f"""
392
+ <div class="form-group">
393
+ <button type="button"
394
+ class="btn btn-primary"
395
+ id="{self.component_id}">
396
+ {self.parameter.label}
397
+ </button>
398
+ </div>
399
+ """
400
+
401
+ def get_js_init(self) -> str:
402
+ return f"""
403
+ document.getElementById('{self.component_id}').addEventListener('click', () => {{
404
+ buttonClick('{self.parameter.name}');
405
+ }});
406
+ """
407
+
408
+
409
+ def create_web_component(
410
+ parameter: Parameter[Any], component_id: str
411
+ ) -> BaseWebComponent[Parameter[Any]]:
412
+ """Create and return the appropriate web component for the given parameter."""
413
+
414
+ component_map = {
415
+ TextParameter: TextComponent,
416
+ BooleanParameter: BooleanComponent,
417
+ SelectionParameter: SelectionComponent,
418
+ MultipleSelectionParameter: MultipleSelectionComponent,
419
+ IntegerParameter: IntegerComponent,
420
+ FloatParameter: FloatComponent,
421
+ IntegerRangeParameter: IntegerRangeComponent,
422
+ FloatRangeParameter: FloatRangeComponent,
423
+ UnboundedIntegerParameter: UnboundedIntegerComponent,
424
+ UnboundedFloatParameter: UnboundedFloatComponent,
425
+ ButtonParameter: ButtonComponent,
426
+ }
427
+
428
+ component_class = component_map.get(type(parameter))
429
+ if component_class is None:
430
+ raise ValueError(
431
+ f"No component implementation for parameter type: {type(parameter)}"
432
+ )
433
+
434
+ return component_class(parameter, component_id)
435
+
436
+
437
+ class WebComponentCollection:
438
+ """
439
+ Manages a collection of web components for a viewer's parameters.
440
+
441
+ This class helps organize all the components needed for a viewer,
442
+ handling their creation, initialization, and updates.
443
+ """
444
+
445
+ def __init__(self):
446
+ self.components: Dict[str, BaseWebComponent] = {}
447
+
448
+ def add_component(self, name: str, parameter: Parameter[Any]) -> None:
449
+ """Add a new component for the given parameter."""
450
+ component_id = f"param_{name}"
451
+ self.components[name] = create_web_component(parameter, component_id)
452
+
453
+ def get_all_html(self) -> str:
454
+ """Get the combined HTML for all components."""
455
+ return "\n".join(comp.render_html() for comp in self.components.values())
456
+
457
+ def get_init_js(self) -> str:
458
+ """Get the combined initialization JavaScript for all components."""
459
+ return "\n".join(comp.get_js_init() for comp in self.components.values())
460
+
461
+ def get_update_js(self, name: str, value: Any) -> str:
462
+ """Get the JavaScript to update a specific component's value."""
463
+ if name not in self.components:
464
+ raise ValueError(f"No component found for parameter: {name}")
465
+ return self.components[name].get_js_update(value)
466
+
467
+ def get_required_css(self) -> List[str]:
468
+ """Get list of CSS files required by the components."""
469
+ return [
470
+ "https://cdn.jsdelivr.net/npm/nouislider@14.6.3/distribute/nouislider.min.css",
471
+ "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css",
472
+ ]
473
+
474
+ def get_required_js(self) -> List[str]:
475
+ """Get list of JavaScript files required by the components."""
476
+ return [
477
+ "https://cdn.jsdelivr.net/npm/nouislider@14.6.3/distribute/nouislider.min.js",
478
+ "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js",
479
+ ]
480
+
481
+ def get_custom_styles(self) -> str:
482
+ """Get custom CSS styles needed for the components."""
483
+ return """
484
+ .slider, .range-slider {
485
+ margin: 10px 0;
486
+ }
487
+ .value-display {
488
+ display: block;
489
+ text-align: center;
490
+ margin-top: 5px;
491
+ font-size: 0.9em;
492
+ color: #666;
493
+ }
494
+ .form-group {
495
+ margin-bottom: 1rem;
496
+ }
497
+ """