syd 0.1.6__py3-none-any.whl → 0.1.7__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 +3 -3
- syd/flask_deployment/__init__.py +1 -0
- syd/flask_deployment/components.py +376 -363
- syd/flask_deployment/deployer.py +247 -283
- syd/flask_deployment/static/css/viewer.css +82 -0
- syd/flask_deployment/static/js/viewer.js +174 -0
- syd/flask_deployment/templates/base.html +23 -20
- syd/flask_deployment/templates/viewer.html +49 -95
- syd/flask_deployment/testing_principles.md +300 -0
- syd/notebook_deployment/__init__.py +1 -1
- syd/notebook_deployment/_ipympl_deployer.py +258 -0
- syd/notebook_deployment/deployer.py +126 -50
- syd/notebook_deployment/widgets.py +142 -69
- syd/parameters.py +93 -180
- syd/plotly_deployment/__init__.py +1 -0
- syd/plotly_deployment/components.py +531 -0
- syd/plotly_deployment/deployer.py +376 -0
- syd/{interactive_viewer.py → viewer.py} +152 -188
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/METADATA +26 -12
- syd-0.1.7.dist-info/RECORD +22 -0
- syd/flask_deployment/static/css/styles.css +0 -39
- syd/flask_deployment/static/js/components.js +0 -51
- syd-0.1.6.dist-info/RECORD +0 -18
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/WHEEL +0 -0
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Any, Dict, Generic, List, TypeVar, Union, Optional
|
|
2
|
+
from typing import Any, Dict, Generic, List, TypeVar, Union, Callable, Optional
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from
|
|
4
|
+
from html import escape
|
|
5
5
|
|
|
6
6
|
from ..parameters import (
|
|
7
7
|
Parameter,
|
|
@@ -15,402 +15,480 @@ from ..parameters import (
|
|
|
15
15
|
FloatRangeParameter,
|
|
16
16
|
UnboundedIntegerParameter,
|
|
17
17
|
UnboundedFloatParameter,
|
|
18
|
-
|
|
18
|
+
ButtonAction,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
T = TypeVar("T", bound=Parameter[Any])
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
to different parameter types in the web deployment.
|
|
36
|
+
class BaseComponent(Generic[T], ABC):
|
|
30
37
|
"""
|
|
38
|
+
Abstract base class for all parameter components.
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
This class defines the common interface and shared functionality
|
|
41
|
+
for components that correspond to different parameter types.
|
|
42
|
+
"""
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
42
61
|
|
|
43
62
|
@abstractmethod
|
|
44
|
-
def
|
|
45
|
-
"""
|
|
46
|
-
|
|
63
|
+
def _create_html(self, parameter: T) -> str:
|
|
64
|
+
"""Create and return the appropriate HTML markup."""
|
|
65
|
+
pass
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"""
|
|
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."""
|
|
51
99
|
pass
|
|
52
100
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Get JavaScript code needed to initialize this component.
|
|
101
|
+
def observe(self, callback: Callable) -> None:
|
|
102
|
+
"""Register a callback for value changes."""
|
|
103
|
+
self._callbacks.append(callback)
|
|
57
104
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
pass
|
|
105
|
+
def unobserve(self, callback: Callable) -> None:
|
|
106
|
+
"""Unregister a callback."""
|
|
107
|
+
self._callbacks.remove(callback)
|
|
62
108
|
|
|
63
|
-
def
|
|
64
|
-
"""
|
|
65
|
-
|
|
109
|
+
def reenable_callbacks(self) -> None:
|
|
110
|
+
"""Reenable all callbacks."""
|
|
111
|
+
pass # Handled by Flask routes and JavaScript
|
|
66
112
|
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
def disable_callbacks(self) -> None:
|
|
114
|
+
"""Disable all callbacks."""
|
|
115
|
+
pass # Handled by Flask routes and JavaScript
|
|
69
116
|
|
|
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
117
|
|
|
104
|
-
|
|
118
|
+
class TextComponent(BaseComponent[TextParameter]):
|
|
119
|
+
"""Component for text parameters."""
|
|
120
|
+
|
|
121
|
+
def _create_html(self, parameter: TextParameter) -> str:
|
|
105
122
|
return f"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}}
|
|
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>
|
|
109
130
|
"""
|
|
110
131
|
|
|
111
132
|
|
|
112
|
-
class BooleanComponent(
|
|
133
|
+
class BooleanComponent(BaseComponent[BooleanParameter]):
|
|
113
134
|
"""Component for boolean parameters."""
|
|
114
135
|
|
|
115
|
-
def
|
|
116
|
-
checked = "checked" if
|
|
136
|
+
def _create_html(self, parameter: BooleanParameter) -> str:
|
|
137
|
+
checked = "checked" if parameter.value else ""
|
|
117
138
|
return f"""
|
|
139
|
+
<div class="{self._style.container_class}">
|
|
118
140
|
<div class="form-check">
|
|
119
|
-
<input type="checkbox"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
<label class="form-check-label"
|
|
124
|
-
|
|
125
|
-
{self.parameter.name}
|
|
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)}
|
|
126
147
|
</label>
|
|
127
148
|
</div>
|
|
149
|
+
</div>
|
|
128
150
|
"""
|
|
129
151
|
|
|
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
152
|
|
|
141
|
-
class SelectionComponent(
|
|
153
|
+
class SelectionComponent(BaseComponent[SelectionParameter]):
|
|
142
154
|
"""Component for single selection parameters."""
|
|
143
155
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
for
|
|
147
|
-
selected = "selected" if
|
|
148
|
-
|
|
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>'
|
|
149
161
|
|
|
150
162
|
return f"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
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>
|
|
157
172
|
"""
|
|
158
173
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"""
|
|
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
|
+
)
|
|
165
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 []
|
|
166
185
|
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
169
190
|
|
|
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
191
|
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
"""
|
|
192
|
+
class MultipleSelectionComponent(BaseComponent[MultipleSelectionParameter]):
|
|
193
|
+
"""Component for multiple selection parameters."""
|
|
184
194
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}});
|
|
191
|
-
"""
|
|
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>'
|
|
192
200
|
|
|
193
|
-
def get_js_update(self, values: List[str]) -> str:
|
|
194
|
-
# More complex update for multi-select
|
|
195
201
|
return f"""
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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>
|
|
200
211
|
"""
|
|
201
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
|
+
|
|
202
231
|
|
|
203
|
-
class
|
|
204
|
-
"""
|
|
232
|
+
class IntegerComponent(BaseComponent[IntegerParameter]):
|
|
233
|
+
"""Component for integer parameters."""
|
|
205
234
|
|
|
206
|
-
def
|
|
235
|
+
def _create_html(self, parameter: IntegerParameter) -> str:
|
|
207
236
|
return f"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
}});
|
|
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>
|
|
222
246
|
"""
|
|
223
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
|
+
)
|
|
224
255
|
|
|
225
|
-
|
|
226
|
-
|
|
256
|
+
def _get_min(self) -> int:
|
|
257
|
+
"""Get minimum value from the HTML."""
|
|
258
|
+
return 0 # Placeholder
|
|
227
259
|
|
|
228
|
-
def
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
"""
|
|
260
|
+
def _get_max(self) -> int:
|
|
261
|
+
"""Get maximum value from the HTML."""
|
|
262
|
+
return 100 # Placeholder
|
|
236
263
|
|
|
237
|
-
def
|
|
238
|
-
|
|
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
|
|
239
268
|
|
|
240
269
|
|
|
241
|
-
class FloatComponent(
|
|
270
|
+
class FloatComponent(BaseComponent[FloatParameter]):
|
|
242
271
|
"""Component for float parameters."""
|
|
243
272
|
|
|
244
|
-
def
|
|
273
|
+
def _create_html(self, parameter: FloatParameter) -> str:
|
|
245
274
|
return f"""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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>
|
|
251
284
|
"""
|
|
252
285
|
|
|
253
|
-
def
|
|
254
|
-
|
|
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
|
+
)
|
|
255
294
|
|
|
295
|
+
def _get_min(self) -> float:
|
|
296
|
+
"""Get minimum value from the HTML."""
|
|
297
|
+
return 0.0 # Placeholder
|
|
256
298
|
|
|
257
|
-
|
|
258
|
-
|
|
299
|
+
def _get_max(self) -> float:
|
|
300
|
+
"""Get maximum value from the HTML."""
|
|
301
|
+
return 1.0 # Placeholder
|
|
259
302
|
|
|
260
|
-
def
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}}
|
|
269
|
-
}}).on('change', (values) => {{
|
|
270
|
-
updateParameter('{param_name}', [
|
|
271
|
-
parseFloat(values[0]),
|
|
272
|
-
parseFloat(values[1])
|
|
273
|
-
]);
|
|
274
|
-
}});
|
|
275
|
-
"""
|
|
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
|
|
276
311
|
|
|
277
312
|
|
|
278
|
-
class IntegerRangeComponent(
|
|
313
|
+
class IntegerRangeComponent(BaseComponent[IntegerRangeParameter]):
|
|
279
314
|
"""Component for integer range parameters."""
|
|
280
315
|
|
|
281
|
-
def
|
|
282
|
-
low, high =
|
|
316
|
+
def _create_html(self, parameter: IntegerRangeParameter) -> str:
|
|
317
|
+
low, high = parameter.value
|
|
283
318
|
return f"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
<
|
|
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>
|
|
288
336
|
</div>
|
|
337
|
+
</div>
|
|
289
338
|
"""
|
|
290
339
|
|
|
291
|
-
def
|
|
292
|
-
|
|
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
|
|
293
360
|
|
|
294
361
|
|
|
295
|
-
class FloatRangeComponent(
|
|
362
|
+
class FloatRangeComponent(BaseComponent[FloatRangeParameter]):
|
|
296
363
|
"""Component for float range parameters."""
|
|
297
364
|
|
|
298
|
-
def
|
|
299
|
-
low, high =
|
|
365
|
+
def _create_html(self, parameter: FloatRangeParameter) -> str:
|
|
366
|
+
low, high = parameter.value
|
|
300
367
|
return f"""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
<
|
|
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>
|
|
305
385
|
</div>
|
|
386
|
+
</div>
|
|
306
387
|
"""
|
|
307
388
|
|
|
308
|
-
def
|
|
309
|
-
|
|
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
|
+
)
|
|
310
397
|
|
|
398
|
+
def _get_min(self) -> float:
|
|
399
|
+
"""Get minimum value from the HTML."""
|
|
400
|
+
return 0.0 # Placeholder
|
|
311
401
|
|
|
312
|
-
|
|
313
|
-
|
|
402
|
+
def _get_max(self) -> float:
|
|
403
|
+
"""Get maximum value from the HTML."""
|
|
404
|
+
return 1.0 # Placeholder
|
|
314
405
|
|
|
315
|
-
def
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if self.parameter.max_value is not None
|
|
324
|
-
else ""
|
|
325
|
-
)
|
|
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
|
|
326
414
|
|
|
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
415
|
|
|
340
|
-
|
|
416
|
+
class UnboundedIntegerComponent(BaseComponent[UnboundedIntegerParameter]):
|
|
417
|
+
"""Component for unbounded integer parameters."""
|
|
418
|
+
|
|
419
|
+
def _create_html(self, parameter: UnboundedIntegerParameter) -> str:
|
|
341
420
|
return f"""
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}}
|
|
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>
|
|
345
428
|
"""
|
|
346
429
|
|
|
347
430
|
|
|
348
|
-
class UnboundedFloatComponent(
|
|
431
|
+
class UnboundedFloatComponent(BaseComponent[UnboundedFloatParameter]):
|
|
349
432
|
"""Component for unbounded float parameters."""
|
|
350
433
|
|
|
351
|
-
def
|
|
352
|
-
|
|
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
|
-
|
|
434
|
+
def _create_html(self, parameter: UnboundedFloatParameter) -> str:
|
|
435
|
+
step = parameter.step if parameter.step is not None else "any"
|
|
366
436
|
return f"""
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
{max_attr}
|
|
375
|
-
{step_attr}>
|
|
376
|
-
</div>
|
|
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>
|
|
377
444
|
"""
|
|
378
445
|
|
|
379
|
-
def
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
updateParameter('{self.parameter.name}', parseFloat(e.target.value));
|
|
383
|
-
}});
|
|
384
|
-
"""
|
|
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
|
|
385
449
|
|
|
450
|
+
def _get_step(self) -> Optional[float]:
|
|
451
|
+
"""Get step value from the HTML."""
|
|
452
|
+
return None # Placeholder
|
|
386
453
|
|
|
387
|
-
|
|
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]):
|
|
388
461
|
"""Component for button parameters."""
|
|
389
462
|
|
|
390
|
-
|
|
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
|
-
"""
|
|
463
|
+
_is_action: bool = True
|
|
400
464
|
|
|
401
|
-
def
|
|
465
|
+
def _create_html(self, parameter: ButtonAction) -> str:
|
|
402
466
|
return f"""
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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>
|
|
406
474
|
"""
|
|
407
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
|
|
408
484
|
|
|
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
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."""
|
|
414
492
|
component_map = {
|
|
415
493
|
TextParameter: TextComponent,
|
|
416
494
|
BooleanParameter: BooleanComponent,
|
|
@@ -422,76 +500,11 @@ def create_web_component(
|
|
|
422
500
|
FloatRangeParameter: FloatRangeComponent,
|
|
423
501
|
UnboundedIntegerParameter: UnboundedIntegerComponent,
|
|
424
502
|
UnboundedFloatParameter: UnboundedFloatComponent,
|
|
425
|
-
|
|
503
|
+
ButtonAction: ButtonComponent,
|
|
426
504
|
}
|
|
427
505
|
|
|
428
|
-
component_class
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
f"No component implementation for parameter type: {type(parameter)}"
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
return component_class(parameter, component_id)
|
|
435
|
-
|
|
506
|
+
for param_type, component_class in component_map.items():
|
|
507
|
+
if isinstance(parameter, param_type):
|
|
508
|
+
return component_class(parameter, continuous, style)
|
|
436
509
|
|
|
437
|
-
|
|
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
|
-
"""
|
|
510
|
+
raise ValueError(f"No component available for parameter type: {type(parameter)}")
|