syd 0.1.7__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.
- syd/__init__.py +1 -1
- syd/flask_deployment/__init__.py +7 -1
- syd/flask_deployment/deployer.py +548 -209
- syd/flask_deployment/static/__init__.py +1 -0
- syd/flask_deployment/static/css/styles.css +246 -0
- syd/flask_deployment/static/js/viewer.js +710 -140
- syd/flask_deployment/templates/__init__.py +1 -0
- syd/flask_deployment/templates/index.html +34 -0
- syd/flask_deployment/testing_principles.md +4 -4
- syd/notebook_deployment/deployer.py +15 -5
- syd/notebook_deployment/widgets.py +76 -58
- syd/parameters.py +239 -250
- syd/support.py +168 -0
- syd/viewer.py +305 -346
- syd-0.2.0.dist-info/METADATA +126 -0
- syd-0.2.0.dist-info/RECORD +19 -0
- syd/flask_deployment/components.py +0 -510
- syd/flask_deployment/static/css/viewer.css +0 -82
- syd/flask_deployment/templates/base.html +0 -29
- syd/flask_deployment/templates/viewer.html +0 -51
- syd/notebook_deployment/_ipympl_deployer.py +0 -258
- syd/plotly_deployment/__init__.py +0 -1
- syd/plotly_deployment/components.py +0 -531
- syd/plotly_deployment/deployer.py +0 -376
- syd-0.1.7.dist-info/METADATA +0 -120
- syd-0.1.7.dist-info/RECORD +0 -22
- {syd-0.1.7.dist-info → syd-0.2.0.dist-info}/WHEEL +0 -0
- {syd-0.1.7.dist-info → syd-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
+
[](https://badge.fury.io/py/syd)
|
|
34
|
+
[](https://github.com/landoskape/syd/actions/workflows/tests.yml)
|
|
35
|
+
[](https://shareyourdata.readthedocs.io/en/stable/?badge=stable)
|
|
36
|
+
[](https://codecov.io/gh/landoskape/syd)
|
|
37
|
+
[](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,510 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Any, Dict, Generic, List, TypeVar, Union, Callable, Optional
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from html import escape
|
|
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
|
-
ButtonAction,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
T = TypeVar("T", bound=Parameter[Any])
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class ComponentStyle:
|
|
26
|
-
"""Style configuration for components."""
|
|
27
|
-
|
|
28
|
-
width: str = "auto"
|
|
29
|
-
margin: str = "3px 0px"
|
|
30
|
-
description_width: str = "initial"
|
|
31
|
-
input_class: str = "form-control"
|
|
32
|
-
label_class: str = "form-label"
|
|
33
|
-
container_class: str = "mb-3"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class BaseComponent(Generic[T], ABC):
|
|
37
|
-
"""
|
|
38
|
-
Abstract base class for all parameter components.
|
|
39
|
-
|
|
40
|
-
This class defines the common interface and shared functionality
|
|
41
|
-
for components that correspond to different parameter types.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
_callbacks: List[Callable]
|
|
45
|
-
_is_action: bool = False
|
|
46
|
-
_value: Any
|
|
47
|
-
_id: str
|
|
48
|
-
|
|
49
|
-
def __init__(
|
|
50
|
-
self,
|
|
51
|
-
parameter: T,
|
|
52
|
-
continuous: bool = False,
|
|
53
|
-
style: Optional[ComponentStyle] = None,
|
|
54
|
-
):
|
|
55
|
-
self._id = f"param_{parameter.name}"
|
|
56
|
-
self._value = parameter.value
|
|
57
|
-
self._callbacks = []
|
|
58
|
-
self._continuous = continuous
|
|
59
|
-
self._style = style or ComponentStyle()
|
|
60
|
-
self._html = self._create_html(parameter)
|
|
61
|
-
|
|
62
|
-
@abstractmethod
|
|
63
|
-
def _create_html(self, parameter: T) -> str:
|
|
64
|
-
"""Create and return the appropriate HTML markup."""
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def html(self) -> str:
|
|
69
|
-
"""Get the HTML representation of the component."""
|
|
70
|
-
return self._html
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def value(self) -> Any:
|
|
74
|
-
"""Get the current value of the component."""
|
|
75
|
-
return self._value
|
|
76
|
-
|
|
77
|
-
@value.setter
|
|
78
|
-
def value(self, new_value: Any) -> None:
|
|
79
|
-
"""Set the value of the component."""
|
|
80
|
-
self._value = new_value
|
|
81
|
-
# In a real implementation, we'd use JavaScript to update the DOM
|
|
82
|
-
# This would be handled by the Flask app's frontend code
|
|
83
|
-
|
|
84
|
-
def matches_parameter(self, parameter: T) -> bool:
|
|
85
|
-
"""Check if the component matches the parameter."""
|
|
86
|
-
return self.value == parameter.value
|
|
87
|
-
|
|
88
|
-
def update_from_parameter(self, parameter: T) -> None:
|
|
89
|
-
"""Update the component from the parameter."""
|
|
90
|
-
try:
|
|
91
|
-
self.disable_callbacks()
|
|
92
|
-
self.extra_updates_from_parameter(parameter)
|
|
93
|
-
self.value = parameter.value
|
|
94
|
-
finally:
|
|
95
|
-
self.reenable_callbacks()
|
|
96
|
-
|
|
97
|
-
def extra_updates_from_parameter(self, parameter: T) -> None:
|
|
98
|
-
"""Extra updates from the parameter."""
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
def observe(self, callback: Callable) -> None:
|
|
102
|
-
"""Register a callback for value changes."""
|
|
103
|
-
self._callbacks.append(callback)
|
|
104
|
-
|
|
105
|
-
def unobserve(self, callback: Callable) -> None:
|
|
106
|
-
"""Unregister a callback."""
|
|
107
|
-
self._callbacks.remove(callback)
|
|
108
|
-
|
|
109
|
-
def reenable_callbacks(self) -> None:
|
|
110
|
-
"""Reenable all callbacks."""
|
|
111
|
-
pass # Handled by Flask routes and JavaScript
|
|
112
|
-
|
|
113
|
-
def disable_callbacks(self) -> None:
|
|
114
|
-
"""Disable all callbacks."""
|
|
115
|
-
pass # Handled by Flask routes and JavaScript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class TextComponent(BaseComponent[TextParameter]):
|
|
119
|
-
"""Component for text parameters."""
|
|
120
|
-
|
|
121
|
-
def _create_html(self, parameter: TextParameter) -> str:
|
|
122
|
-
return f"""
|
|
123
|
-
<div class="{self._style.container_class}">
|
|
124
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
125
|
-
<input type="text" class="{self._style.input_class}" id="{self._id}"
|
|
126
|
-
name="{parameter.name}" value="{escape(str(parameter.value))}"
|
|
127
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
128
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
129
|
-
</div>
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class BooleanComponent(BaseComponent[BooleanParameter]):
|
|
134
|
-
"""Component for boolean parameters."""
|
|
135
|
-
|
|
136
|
-
def _create_html(self, parameter: BooleanParameter) -> str:
|
|
137
|
-
checked = "checked" if parameter.value else ""
|
|
138
|
-
return f"""
|
|
139
|
-
<div class="{self._style.container_class}">
|
|
140
|
-
<div class="form-check">
|
|
141
|
-
<input type="checkbox" class="form-check-input" id="{self._id}"
|
|
142
|
-
name="{parameter.name}" {checked}
|
|
143
|
-
style="margin: {self._style.margin};"
|
|
144
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
145
|
-
<label class="form-check-label" for="{self._id}">
|
|
146
|
-
{escape(parameter.name)}
|
|
147
|
-
</label>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class SelectionComponent(BaseComponent[SelectionParameter]):
|
|
154
|
-
"""Component for single selection parameters."""
|
|
155
|
-
|
|
156
|
-
def _create_html(self, parameter: SelectionParameter) -> str:
|
|
157
|
-
options_html = ""
|
|
158
|
-
for option in parameter.options:
|
|
159
|
-
selected = "selected" if option == parameter.value else ""
|
|
160
|
-
options_html += f'<option value="{escape(str(option))}" {selected}>{escape(str(option))}</option>'
|
|
161
|
-
|
|
162
|
-
return f"""
|
|
163
|
-
<div class="{self._style.container_class}">
|
|
164
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
165
|
-
<select class="{self._style.input_class}" id="{self._id}"
|
|
166
|
-
name="{parameter.name}"
|
|
167
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
168
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
169
|
-
{options_html}
|
|
170
|
-
</select>
|
|
171
|
-
</div>
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
def matches_parameter(self, parameter: SelectionParameter) -> bool:
|
|
175
|
-
"""Check if the component matches the parameter."""
|
|
176
|
-
return self.value == parameter.value and set(self._get_options()) == set(
|
|
177
|
-
parameter.options
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
def _get_options(self) -> List[Any]:
|
|
181
|
-
"""Get current options from the HTML."""
|
|
182
|
-
# In a real implementation, this would parse the HTML
|
|
183
|
-
# For now, we'll just return an empty list
|
|
184
|
-
return []
|
|
185
|
-
|
|
186
|
-
def extra_updates_from_parameter(self, parameter: SelectionParameter) -> None:
|
|
187
|
-
"""Extra updates from the parameter."""
|
|
188
|
-
# In a real implementation, this would update the options in the DOM
|
|
189
|
-
pass
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class MultipleSelectionComponent(BaseComponent[MultipleSelectionParameter]):
|
|
193
|
-
"""Component for multiple selection parameters."""
|
|
194
|
-
|
|
195
|
-
def _create_html(self, parameter: MultipleSelectionParameter) -> str:
|
|
196
|
-
options_html = ""
|
|
197
|
-
for option in parameter.options:
|
|
198
|
-
selected = "selected" if option in parameter.value else ""
|
|
199
|
-
options_html += f'<option value="{escape(str(option))}" {selected}>{escape(str(option))}</option>'
|
|
200
|
-
|
|
201
|
-
return f"""
|
|
202
|
-
<div class="{self._style.container_class}">
|
|
203
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
204
|
-
<select class="{self._style.input_class}" id="{self._id}"
|
|
205
|
-
name="{parameter.name}" multiple
|
|
206
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
207
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
208
|
-
{options_html}
|
|
209
|
-
</select>
|
|
210
|
-
</div>
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
def matches_parameter(self, parameter: MultipleSelectionParameter) -> bool:
|
|
214
|
-
"""Check if the component matches the parameter."""
|
|
215
|
-
return set(self.value) == set(parameter.value) and set(
|
|
216
|
-
self._get_options()
|
|
217
|
-
) == set(parameter.options)
|
|
218
|
-
|
|
219
|
-
def _get_options(self) -> List[Any]:
|
|
220
|
-
"""Get current options from the HTML."""
|
|
221
|
-
# In a real implementation, this would parse the HTML
|
|
222
|
-
return []
|
|
223
|
-
|
|
224
|
-
def extra_updates_from_parameter(
|
|
225
|
-
self, parameter: MultipleSelectionParameter
|
|
226
|
-
) -> None:
|
|
227
|
-
"""Extra updates from the parameter."""
|
|
228
|
-
# In a real implementation, this would update the options in the DOM
|
|
229
|
-
pass
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class IntegerComponent(BaseComponent[IntegerParameter]):
|
|
233
|
-
"""Component for integer parameters."""
|
|
234
|
-
|
|
235
|
-
def _create_html(self, parameter: IntegerParameter) -> str:
|
|
236
|
-
return f"""
|
|
237
|
-
<div class="{self._style.container_class}">
|
|
238
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
239
|
-
<input type="range" class="{self._style.input_class}" id="{self._id}"
|
|
240
|
-
name="{parameter.name}" value="{parameter.value}"
|
|
241
|
-
min="{parameter.min_value}" max="{parameter.max_value}"
|
|
242
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
243
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
244
|
-
<output for="{self._id}">{parameter.value}</output>
|
|
245
|
-
</div>
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
def matches_parameter(self, parameter: IntegerParameter) -> bool:
|
|
249
|
-
"""Check if the component matches the parameter."""
|
|
250
|
-
return (
|
|
251
|
-
self.value == parameter.value
|
|
252
|
-
and self._get_min() == parameter.min_value
|
|
253
|
-
and self._get_max() == parameter.max_value
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
def _get_min(self) -> int:
|
|
257
|
-
"""Get minimum value from the HTML."""
|
|
258
|
-
return 0 # Placeholder
|
|
259
|
-
|
|
260
|
-
def _get_max(self) -> int:
|
|
261
|
-
"""Get maximum value from the HTML."""
|
|
262
|
-
return 100 # Placeholder
|
|
263
|
-
|
|
264
|
-
def extra_updates_from_parameter(self, parameter: IntegerParameter) -> None:
|
|
265
|
-
"""Extra updates from the parameter."""
|
|
266
|
-
# In a real implementation, this would update min/max in the DOM
|
|
267
|
-
pass
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
class FloatComponent(BaseComponent[FloatParameter]):
|
|
271
|
-
"""Component for float parameters."""
|
|
272
|
-
|
|
273
|
-
def _create_html(self, parameter: FloatParameter) -> str:
|
|
274
|
-
return f"""
|
|
275
|
-
<div class="{self._style.container_class}">
|
|
276
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
277
|
-
<input type="range" class="{self._style.input_class}" id="{self._id}"
|
|
278
|
-
name="{parameter.name}" value="{parameter.value}"
|
|
279
|
-
min="{parameter.min_value}" max="{parameter.max_value}" step="{parameter.step}"
|
|
280
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
281
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
282
|
-
<output for="{self._id}">{parameter.value}</output>
|
|
283
|
-
</div>
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
def matches_parameter(self, parameter: FloatParameter) -> bool:
|
|
287
|
-
"""Check if the component matches the parameter."""
|
|
288
|
-
return (
|
|
289
|
-
self.value == parameter.value
|
|
290
|
-
and self._get_min() == parameter.min_value
|
|
291
|
-
and self._get_max() == parameter.max_value
|
|
292
|
-
and self._get_step() == parameter.step
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
def _get_min(self) -> float:
|
|
296
|
-
"""Get minimum value from the HTML."""
|
|
297
|
-
return 0.0 # Placeholder
|
|
298
|
-
|
|
299
|
-
def _get_max(self) -> float:
|
|
300
|
-
"""Get maximum value from the HTML."""
|
|
301
|
-
return 1.0 # Placeholder
|
|
302
|
-
|
|
303
|
-
def _get_step(self) -> float:
|
|
304
|
-
"""Get step value from the HTML."""
|
|
305
|
-
return 0.1 # Placeholder
|
|
306
|
-
|
|
307
|
-
def extra_updates_from_parameter(self, parameter: FloatParameter) -> None:
|
|
308
|
-
"""Extra updates from the parameter."""
|
|
309
|
-
# In a real implementation, this would update min/max/step in the DOM
|
|
310
|
-
pass
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
class IntegerRangeComponent(BaseComponent[IntegerRangeParameter]):
|
|
314
|
-
"""Component for integer range parameters."""
|
|
315
|
-
|
|
316
|
-
def _create_html(self, parameter: IntegerRangeParameter) -> str:
|
|
317
|
-
low, high = parameter.value
|
|
318
|
-
return f"""
|
|
319
|
-
<div class="{self._style.container_class}">
|
|
320
|
-
<label for="{self._id}_low" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
321
|
-
<div class="d-flex align-items-center">
|
|
322
|
-
<input type="range" class="{self._style.input_class}" id="{self._id}_low"
|
|
323
|
-
name="{parameter.name}_low" value="{low}"
|
|
324
|
-
min="{parameter.min_value}" max="{parameter.max_value}"
|
|
325
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
326
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
327
|
-
<output for="{self._id}_low">{low}</output>
|
|
328
|
-
</div>
|
|
329
|
-
<div class="d-flex align-items-center">
|
|
330
|
-
<input type="range" class="{self._style.input_class}" id="{self._id}_high"
|
|
331
|
-
name="{parameter.name}_high" value="{high}"
|
|
332
|
-
min="{parameter.min_value}" max="{parameter.max_value}"
|
|
333
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
334
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
335
|
-
<output for="{self._id}_high">{high}</output>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
"""
|
|
339
|
-
|
|
340
|
-
def matches_parameter(self, parameter: IntegerRangeParameter) -> bool:
|
|
341
|
-
"""Check if the component matches the parameter."""
|
|
342
|
-
return (
|
|
343
|
-
self.value == parameter.value
|
|
344
|
-
and self._get_min() == parameter.min_value
|
|
345
|
-
and self._get_max() == parameter.max_value
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
def _get_min(self) -> int:
|
|
349
|
-
"""Get minimum value from the HTML."""
|
|
350
|
-
return 0 # Placeholder
|
|
351
|
-
|
|
352
|
-
def _get_max(self) -> int:
|
|
353
|
-
"""Get maximum value from the HTML."""
|
|
354
|
-
return 100 # Placeholder
|
|
355
|
-
|
|
356
|
-
def extra_updates_from_parameter(self, parameter: IntegerRangeParameter) -> None:
|
|
357
|
-
"""Extra updates from the parameter."""
|
|
358
|
-
# In a real implementation, this would update min/max in the DOM
|
|
359
|
-
pass
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
class FloatRangeComponent(BaseComponent[FloatRangeParameter]):
|
|
363
|
-
"""Component for float range parameters."""
|
|
364
|
-
|
|
365
|
-
def _create_html(self, parameter: FloatRangeParameter) -> str:
|
|
366
|
-
low, high = parameter.value
|
|
367
|
-
return f"""
|
|
368
|
-
<div class="{self._style.container_class}">
|
|
369
|
-
<label for="{self._id}_low" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
370
|
-
<div class="d-flex align-items-center">
|
|
371
|
-
<input type="range" class="{self._style.input_class}" id="{self._id}_low"
|
|
372
|
-
name="{parameter.name}_low" value="{low}"
|
|
373
|
-
min="{parameter.min_value}" max="{parameter.max_value}" step="{parameter.step}"
|
|
374
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
375
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
376
|
-
<output for="{self._id}_low">{low}</output>
|
|
377
|
-
</div>
|
|
378
|
-
<div class="d-flex align-items-center">
|
|
379
|
-
<input type="range" class="{self._style.input_class}" id="{self._id}_high"
|
|
380
|
-
name="{parameter.name}_high" value="{high}"
|
|
381
|
-
min="{parameter.min_value}" max="{parameter.max_value}" step="{parameter.step}"
|
|
382
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
383
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
384
|
-
<output for="{self._id}_high">{high}</output>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
"""
|
|
388
|
-
|
|
389
|
-
def matches_parameter(self, parameter: FloatRangeParameter) -> bool:
|
|
390
|
-
"""Check if the component matches the parameter."""
|
|
391
|
-
return (
|
|
392
|
-
self.value == parameter.value
|
|
393
|
-
and self._get_min() == parameter.min_value
|
|
394
|
-
and self._get_max() == parameter.max_value
|
|
395
|
-
and self._get_step() == parameter.step
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
def _get_min(self) -> float:
|
|
399
|
-
"""Get minimum value from the HTML."""
|
|
400
|
-
return 0.0 # Placeholder
|
|
401
|
-
|
|
402
|
-
def _get_max(self) -> float:
|
|
403
|
-
"""Get maximum value from the HTML."""
|
|
404
|
-
return 1.0 # Placeholder
|
|
405
|
-
|
|
406
|
-
def _get_step(self) -> float:
|
|
407
|
-
"""Get step value from the HTML."""
|
|
408
|
-
return 0.1 # Placeholder
|
|
409
|
-
|
|
410
|
-
def extra_updates_from_parameter(self, parameter: FloatRangeParameter) -> None:
|
|
411
|
-
"""Extra updates from the parameter."""
|
|
412
|
-
# In a real implementation, this would update min/max/step in the DOM
|
|
413
|
-
pass
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
class UnboundedIntegerComponent(BaseComponent[UnboundedIntegerParameter]):
|
|
417
|
-
"""Component for unbounded integer parameters."""
|
|
418
|
-
|
|
419
|
-
def _create_html(self, parameter: UnboundedIntegerParameter) -> str:
|
|
420
|
-
return f"""
|
|
421
|
-
<div class="{self._style.container_class}">
|
|
422
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
423
|
-
<input type="number" class="{self._style.input_class}" id="{self._id}"
|
|
424
|
-
name="{parameter.name}" value="{parameter.value}"
|
|
425
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
426
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
427
|
-
</div>
|
|
428
|
-
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
class UnboundedFloatComponent(BaseComponent[UnboundedFloatParameter]):
|
|
432
|
-
"""Component for unbounded float parameters."""
|
|
433
|
-
|
|
434
|
-
def _create_html(self, parameter: UnboundedFloatParameter) -> str:
|
|
435
|
-
step = parameter.step if parameter.step is not None else "any"
|
|
436
|
-
return f"""
|
|
437
|
-
<div class="{self._style.container_class}">
|
|
438
|
-
<label for="{self._id}" class="{self._style.label_class}">{escape(parameter.name)}</label>
|
|
439
|
-
<input type="number" class="{self._style.input_class}" id="{self._id}"
|
|
440
|
-
name="{parameter.name}" value="{parameter.value}" step="{step}"
|
|
441
|
-
style="width: {self._style.width}; margin: {self._style.margin};"
|
|
442
|
-
data-continuous="{str(self._continuous).lower()}">
|
|
443
|
-
</div>
|
|
444
|
-
"""
|
|
445
|
-
|
|
446
|
-
def matches_parameter(self, parameter: UnboundedFloatParameter) -> bool:
|
|
447
|
-
"""Check if the component matches the parameter."""
|
|
448
|
-
return self.value == parameter.value and self._get_step() == parameter.step
|
|
449
|
-
|
|
450
|
-
def _get_step(self) -> Optional[float]:
|
|
451
|
-
"""Get step value from the HTML."""
|
|
452
|
-
return None # Placeholder
|
|
453
|
-
|
|
454
|
-
def extra_updates_from_parameter(self, parameter: UnboundedFloatParameter) -> None:
|
|
455
|
-
"""Extra updates from the parameter."""
|
|
456
|
-
# In a real implementation, this would update step in the DOM
|
|
457
|
-
pass
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
class ButtonComponent(BaseComponent[ButtonAction]):
|
|
461
|
-
"""Component for button parameters."""
|
|
462
|
-
|
|
463
|
-
_is_action: bool = True
|
|
464
|
-
|
|
465
|
-
def _create_html(self, parameter: ButtonAction) -> str:
|
|
466
|
-
return f"""
|
|
467
|
-
<div class="{self._style.container_class}">
|
|
468
|
-
<button type="button" class="btn btn-primary" id="{self._id}"
|
|
469
|
-
name="{parameter.name}"
|
|
470
|
-
style="width: {self._style.width}; margin: {self._style.margin};">
|
|
471
|
-
{escape(parameter.label)}
|
|
472
|
-
</button>
|
|
473
|
-
</div>
|
|
474
|
-
"""
|
|
475
|
-
|
|
476
|
-
def matches_parameter(self, parameter: ButtonAction) -> bool:
|
|
477
|
-
"""Check if the component matches the parameter."""
|
|
478
|
-
return True # Buttons don't have a value to match
|
|
479
|
-
|
|
480
|
-
def extra_updates_from_parameter(self, parameter: ButtonAction) -> None:
|
|
481
|
-
"""Extra updates from the parameter."""
|
|
482
|
-
# In a real implementation, this would update the button label in the DOM
|
|
483
|
-
pass
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def create_component(
|
|
487
|
-
parameter: Union[Parameter[Any], ButtonAction],
|
|
488
|
-
continuous: bool = False,
|
|
489
|
-
style: Optional[ComponentStyle] = None,
|
|
490
|
-
) -> BaseComponent[Union[Parameter[Any], ButtonAction]]:
|
|
491
|
-
"""Create the appropriate component for a parameter."""
|
|
492
|
-
component_map = {
|
|
493
|
-
TextParameter: TextComponent,
|
|
494
|
-
BooleanParameter: BooleanComponent,
|
|
495
|
-
SelectionParameter: SelectionComponent,
|
|
496
|
-
MultipleSelectionParameter: MultipleSelectionComponent,
|
|
497
|
-
IntegerParameter: IntegerComponent,
|
|
498
|
-
FloatParameter: FloatComponent,
|
|
499
|
-
IntegerRangeParameter: IntegerRangeComponent,
|
|
500
|
-
FloatRangeParameter: FloatRangeComponent,
|
|
501
|
-
UnboundedIntegerParameter: UnboundedIntegerComponent,
|
|
502
|
-
UnboundedFloatParameter: UnboundedFloatComponent,
|
|
503
|
-
ButtonAction: ButtonComponent,
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
for param_type, component_class in component_map.items():
|
|
507
|
-
if isinstance(parameter, param_type):
|
|
508
|
-
return component_class(parameter, continuous, style)
|
|
509
|
-
|
|
510
|
-
raise ValueError(f"No component available for parameter type: {type(parameter)}")
|