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,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: syd
3
+ Version: 0.2.0
4
+ Summary: A Python package for making GUIs for data science easy.
5
+ Project-URL: Homepage, https://github.com/landoskape/syd
6
+ Author-email: Andrew Landau <andrew+tyler+landau+getridofthisanddtheplusses@gmail.com>
7
+ License-Expression: GPL-3.0-or-later
8
+ License-File: LICENSE
9
+ Keywords: data-science,gui,interactive,jupyter,machine-learning,notebook,python
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: flask
22
+ Requires-Dist: ipykernel
23
+ Requires-Dist: ipympl
24
+ Requires-Dist: ipywidgets
25
+ Requires-Dist: matplotlib
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
28
+ Requires-Dist: pytest>=7.0.0; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Syd
32
+
33
+ [![PyPI version](https://badge.fury.io/py/syd.svg)](https://badge.fury.io/py/syd)
34
+ [![Tests](https://github.com/landoskape/syd/actions/workflows/tests.yml/badge.svg)](https://github.com/landoskape/syd/actions/workflows/tests.yml)
35
+ [![Documentation Status](https://readthedocs.org/projects/shareyourdata/badge/?version=stable)](https://shareyourdata.readthedocs.io/en/stable/?badge=stable)
36
+ [![codecov](https://codecov.io/gh/landoskape/syd/branch/main/graph/badge.svg)](https://codecov.io/gh/landoskape/syd)
37
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
38
+
39
+
40
+ A package to help you share your data!
41
+
42
+ Have you ever wanted to look through all your data really quickly interactively? Of course you have. Mo data mo problems, but only if you don't know what to do with it. And that's why Syd stands for show your data!
43
+
44
+ Syd is a system for creating a data viewing GUI that you can view in a jupyter notebook or in a web browser. And guess what? Since it can open in a web browser, you can even open it on any other computer on your local network! For example, your PI's computer. Gone are the days of single random examples that they make infinitely stubborn conclusions about. Now, you can look at all the examples, quickly and easily, on their computer. And that's why Syd stands for share your data!
45
+
46
+ Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about _**what**_ you want to plot and _**which**_ parameters you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And do you know what that means? It means you get to spend your time _thinking_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
47
+
48
+ ## Installation
49
+ It's easy, just use pip install. The dependencies are light so it should work in most environments.
50
+ ```bash
51
+ pip install syd
52
+ ```
53
+
54
+ ## Quick Start
55
+ This is an example of a sine wave viewer which is about as simple as it gets. You can choose which env to use - if you use ``env="notebook"`` then the GUI will deploy as the output of a jupyter cell (this only works in jupyter!). If you use ``env="browser"`` then the GUI will open a page in your default web browser and you can interact with the data there (works in jupyter notebooks and also from python scripts!).
56
+ ```python
57
+ import matplotlib.pyplot as plt
58
+ import numpy as np
59
+ from syd import make_viewer
60
+ def plot(viewer, state):
61
+ fig, ax = plt.subplots()
62
+ x = np.linspace(0, 10, 1000)
63
+ y = state['amplitude'] * np.sin(state['frequency'] * x)
64
+ ax.plot(x, y)
65
+ return fig
66
+
67
+ viewer = make_viewer()
68
+ viewer.set_plot(plot)
69
+ viewer.add_float('amplitude', value=1.0, min=0, max=2)
70
+ viewer.add_float('frequency', value=1.0, min=0.1, max=5)
71
+
72
+ # env = "browser" # for viewing in a web browser
73
+ env = "notebook" # for viewing within a jupyter notebook
74
+ viewer.deploy(env=env)
75
+ ```
76
+
77
+ We have several examples of more complex viewers in the [examples](examples) folder. A good one to start with is the [simple example](examples/1-simple_example.ipynb) because this has detailed explanations of how to use the core elements of Syd. To see an example that showcases everything you can do with Syd, try [complex example](examples/2a-complex_example.ipynb). And to see what the same viewer looks like when written as a class, check out [subclass example](examples/2b-subclass_example.ipynb). This format is pretty useful when you want complex functionality - for example if you want to add extra supporting methods for processing data and updating parameters that require more complex logic or if your data processing requires some clever preprocessing to make plotting fast.
78
+
79
+ #### Data loading
80
+ Thinking about how to get data into a Syd viewer can be non-intuitive. For some examples that showcase different ways to get your data into a Syd viewer, check out the [data loading example](examples/3-data_loading.ipynb).
81
+
82
+ ## Documentation
83
+
84
+ Full documentation is available at [shareyourdata.readthedocs.io](https://shareyourdata.readthedocs.io/).
85
+
86
+ ## License
87
+
88
+ This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
89
+
90
+ ## Contributing
91
+
92
+ Contributions are welcome! Here's how you can help:
93
+
94
+ 1. Fork the repository
95
+ 2. Create a new branch (`git checkout -b feature/amazing-feature`)
96
+ 3. Make your changes
97
+ 4. Run the tests (`pytest`)
98
+ 5. Commit your changes (`git commit -m 'Add amazing feature'`)
99
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
100
+ 7. Open a Pull Request online
101
+
102
+ Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines).
103
+
104
+
105
+ ## To-Do List
106
+ - Layout controls
107
+ - [ ] Improve the display and make it look better
108
+ - [ ] Add a "save" button that saves the current state of the viewer to a json file
109
+ - [ ] Add a "load" button that loads the viewer state from a json file
110
+ - [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
111
+ - [ ] Add a window for capturing any error messages that might be thrown by the plot function. Maybe we could have a little interface for looking at each one (up to a point) and the user could press a button to throw an error for the traceback.
112
+ - [ ] Consider "app_deployed" context for each deployer...
113
+ - [ ] Consider the error messages and if they can be more informative and less opaque -- especially when debugging (e.g. when we always get routed to the decorators that check things...)
114
+ - Notebook deployment debouncer:
115
+ - [ ] Probably make this dependent on whether the user is in %matplotlib widget mode or not
116
+ - [ ] Also probably make it dependent on whether the deployer is in continuous mode or not
117
+ - [ ] Potentially make the wait_time dynamic depending on how fast the plot method is and how frequently the no comm messages show up... (if we can catch them)
118
+ - [ ] Consider adding a step to the integer parameter...
119
+ - Idea for figure management:
120
+ - [ ] We could make fig=?, ax=? arguments optional for the plot function and add a
121
+ "recycle_figure: bool = False" flag be part of the deploy API. This way, an
122
+ advanced user that wants snappy responsivity or complex figure management can
123
+ do so, but the default is for the user to generate a new figure object each time.
124
+ - Export options:
125
+ - [ ] Export lite: export the viewer as a HTML/Java package that contains an incomplete set of renderings of figures -- using a certain set of parameters.
126
+ - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
@@ -0,0 +1,19 @@
1
+ syd/__init__.py,sha256=zvTnj6iGMWKHmfuwvTf6bt4A7laMPIJZgNmzVI7Ki3Y,250
2
+ syd/parameters.py,sha256=8LUJz7YwaC1sHmf3q0SevxgT_x78vBBtIpkZbs-WrOU,43846
3
+ syd/support.py,sha256=D-DbzyC1KfQlNYKGQTrDakPw2Yqqqa_Aqtk3bEIMYFw,5523
4
+ syd/viewer.py,sha256=NiQ29y5Kgqfq66OF0Mx70bsm74o4WHeGS6XEbSk4avA,49118
5
+ syd/flask_deployment/__init__.py,sha256=oEa9ttsX_uM27dGIhKKwgvVr-WsQYoD1tzahtRI4TMk,190
6
+ syd/flask_deployment/deployer.py,sha256=ig7LBuIPNkydA31YQBlt0z6LrJzNFQLqyk0p3MexfGU,20518
7
+ syd/flask_deployment/testing_principles.md,sha256=GyULM97sDeie8h3tSPoduOckdMNGyWuwm1RdHo5jzK0,10130
8
+ syd/flask_deployment/static/__init__.py,sha256=ieWE8NKR-APw7h4Ge0ooZGk6wZrneSSs_1cMyTPbQSA,65
9
+ syd/flask_deployment/static/css/styles.css,sha256=ADXqBfc9LaoteY44ZeUajoWluY6fMsh0aEPKtYYZSQM,4326
10
+ syd/flask_deployment/static/js/viewer.js,sha256=LihDHzJBAs8KU6RREl4uN8KOhUibpydzWDRFWN9cz2g,22700
11
+ syd/flask_deployment/templates/__init__.py,sha256=ieWE8NKR-APw7h4Ge0ooZGk6wZrneSSs_1cMyTPbQSA,65
12
+ syd/flask_deployment/templates/index.html,sha256=CAk08WJQ95GcqGjcvXU2GwI7UfBLvyp0BMEUVz04MUQ,1593
13
+ syd/notebook_deployment/__init__.py,sha256=0DZ-psAZBt6dx_a5Dc8wLK-rKTn8yDvTmQOcL6Le3l8,39
14
+ syd/notebook_deployment/deployer.py,sha256=cURP4V3TU5LsB8EDZK_x6oqIqyQAMuojn1onwmf-9_s,11671
15
+ syd/notebook_deployment/widgets.py,sha256=La-mFXyMRlGNgIZo3eWQ91NABVFBJMMLTL2zV5RK1pY,20060
16
+ syd-0.2.0.dist-info/METADATA,sha256=MjOvWcKSGmPbiNjrY1zNHgqAHn_Jv482Gz5Lly50BBI,8025
17
+ syd-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ syd-0.2.0.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
19
+ syd-0.2.0.dist-info/RECORD,,
@@ -1,497 +0,0 @@
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
- """