syd 0.1.5__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syd/__init__.py +6 -10
- syd/flask_deployment/__init__.py +0 -0
- syd/flask_deployment/components.py +497 -0
- syd/flask_deployment/deployer.py +338 -0
- syd/flask_deployment/static/css/styles.css +39 -0
- syd/flask_deployment/static/js/components.js +51 -0
- syd/flask_deployment/static/js/viewer.js +0 -0
- syd/flask_deployment/templates/base.html +26 -0
- syd/flask_deployment/templates/viewer.html +97 -0
- syd/interactive_viewer.py +200 -31
- syd/{notebook_deploy → notebook_deployment}/deployer.py +39 -22
- syd/{notebook_deploy → notebook_deployment}/widgets.py +69 -62
- syd/parameters.py +356 -73
- {syd-0.1.5.dist-info → syd-0.1.6.dist-info}/METADATA +68 -3
- syd-0.1.6.dist-info/RECORD +18 -0
- syd-0.1.5.dist-info/RECORD +0 -10
- /syd/{notebook_deploy → notebook_deployment}/__init__.py +0 -0
- {syd-0.1.5.dist-info → syd-0.1.6.dist-info}/WHEEL +0 -0
- {syd-0.1.5.dist-info → syd-0.1.6.dist-info}/licenses/LICENSE +0 -0
syd/interactive_viewer.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from typing import List, Any, Callable, Dict, Tuple, Union, Optional
|
|
2
|
-
from functools import wraps
|
|
2
|
+
from functools import wraps, partial
|
|
3
|
+
import inspect
|
|
3
4
|
from contextlib import contextmanager
|
|
4
|
-
from abc import ABC, abstractmethod
|
|
5
5
|
from matplotlib.figure import Figure
|
|
6
6
|
|
|
7
7
|
from .parameters import (
|
|
8
8
|
ParameterType,
|
|
9
|
+
ActionType,
|
|
9
10
|
Parameter,
|
|
10
11
|
ParameterAddError,
|
|
11
12
|
ParameterUpdateError,
|
|
@@ -28,7 +29,7 @@ _NO_UPDATE = _NoUpdate()
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def validate_parameter_operation(
|
|
31
|
-
operation: str, parameter_type: ParameterType
|
|
32
|
+
operation: str, parameter_type: Union[ParameterType, ActionType]
|
|
32
33
|
) -> Callable:
|
|
33
34
|
"""
|
|
34
35
|
Decorator that validates parameter operations for the InteractiveViewer class.
|
|
@@ -64,14 +65,14 @@ def validate_parameter_operation(
|
|
|
64
65
|
"Incorrect use of validate_parameter_operation decorator. Must be called with 'add' or 'update' as the first argument."
|
|
65
66
|
)
|
|
66
67
|
|
|
68
|
+
# Validate operation matches method name (add/update)
|
|
69
|
+
if not func.__name__.startswith(operation):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Invalid operation type specified ({operation}) for method {func.__name__}"
|
|
72
|
+
)
|
|
73
|
+
|
|
67
74
|
@wraps(func)
|
|
68
75
|
def wrapper(self: "InteractiveViewer", name: Any, *args, **kwargs):
|
|
69
|
-
# Validate operation matches method name (add/update)
|
|
70
|
-
if not func.__name__.startswith(operation):
|
|
71
|
-
raise ValueError(
|
|
72
|
-
f"Invalid operation type specified ({operation}) for method {func.__name__}"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
76
|
# Validate parameter name is a string
|
|
76
77
|
if not isinstance(name, str):
|
|
77
78
|
if operation == "add":
|
|
@@ -103,8 +104,8 @@ def validate_parameter_operation(
|
|
|
103
104
|
parameter_type.name,
|
|
104
105
|
"Parameter not found - you can only update registered parameters!",
|
|
105
106
|
)
|
|
106
|
-
if
|
|
107
|
-
msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])})"
|
|
107
|
+
if not isinstance(self.parameters[name], parameter_type.value):
|
|
108
|
+
msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])}). Expecting {parameter_type.value}."
|
|
108
109
|
raise ParameterUpdateError(name, parameter_type.name, msg)
|
|
109
110
|
|
|
110
111
|
try:
|
|
@@ -122,7 +123,7 @@ def validate_parameter_operation(
|
|
|
122
123
|
return decorator
|
|
123
124
|
|
|
124
125
|
|
|
125
|
-
class InteractiveViewer
|
|
126
|
+
class InteractiveViewer:
|
|
126
127
|
"""
|
|
127
128
|
Base class for creating interactive matplotlib figures with GUI controls.
|
|
128
129
|
|
|
@@ -184,13 +185,29 @@ class InteractiveViewer(ABC):
|
|
|
184
185
|
"""
|
|
185
186
|
return {name: param.value for name, param in self.parameters.items()}
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
def plot(self, state: Dict[str, Any]) -> Figure:
|
|
189
|
+
"""Create and return a matplotlib figure.
|
|
190
|
+
|
|
191
|
+
This is a placeholder. You must either:
|
|
192
|
+
|
|
193
|
+
1. Call set_plot() with your plotting function
|
|
194
|
+
This will look like this:
|
|
195
|
+
>>> def plot(viewer, state):
|
|
196
|
+
>>> ... generate figure, plot stuff ...
|
|
197
|
+
>>> return fig
|
|
198
|
+
>>> viewer.set_plot(plot))
|
|
199
|
+
|
|
200
|
+
2. Subclass InteractiveViewer and override this method
|
|
201
|
+
This will look like this:
|
|
202
|
+
>>> class YourViewer(InteractiveViewer):
|
|
203
|
+
>>> def plot(self, state):
|
|
204
|
+
>>> ... generate figure, plot stuff ...
|
|
205
|
+
>>> return fig
|
|
191
206
|
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
state : dict
|
|
210
|
+
Current parameter values
|
|
194
211
|
|
|
195
212
|
Returns
|
|
196
213
|
-------
|
|
@@ -200,27 +217,36 @@ class InteractiveViewer(ABC):
|
|
|
200
217
|
Notes
|
|
201
218
|
-----
|
|
202
219
|
- Create a new figure each time, don't reuse old ones
|
|
203
|
-
- Access parameter values using
|
|
204
|
-
-
|
|
220
|
+
- Access parameter values using state['param_name']
|
|
221
|
+
- Access your viewer class using self (or viewer for the set_plot() method)
|
|
222
|
+
- Return the figure object, don't call plt.show()!
|
|
205
223
|
"""
|
|
206
|
-
raise NotImplementedError(
|
|
224
|
+
raise NotImplementedError(
|
|
225
|
+
"Plot method not implemented. Either subclass "
|
|
226
|
+
"InteractiveViewer and override plot(), or use "
|
|
227
|
+
"set_plot() to provide a plotting function."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def set_plot(self, func: Callable) -> None:
|
|
231
|
+
"""Set the plot method for the viewer"""
|
|
232
|
+
self.plot = self._prepare_function(func, context="Setting plot:")
|
|
207
233
|
|
|
208
234
|
def deploy(self, env: str = "notebook", **kwargs):
|
|
209
235
|
"""Deploy the app in a notebook or standalone environment"""
|
|
210
236
|
if env == "notebook":
|
|
211
|
-
from .
|
|
237
|
+
from .notebook_deployment import NotebookDeployment
|
|
212
238
|
|
|
213
239
|
deployer = NotebookDeployment(self, **kwargs)
|
|
214
240
|
deployer.deploy()
|
|
215
241
|
|
|
216
|
-
return
|
|
242
|
+
return self
|
|
217
243
|
else:
|
|
218
244
|
raise ValueError(
|
|
219
245
|
f"Unsupported environment: {env}, only 'notebook' is supported right now."
|
|
220
246
|
)
|
|
221
247
|
|
|
222
248
|
@contextmanager
|
|
223
|
-
def
|
|
249
|
+
def _deploy_app(self):
|
|
224
250
|
"""Internal context manager to control app deployment state"""
|
|
225
251
|
self._app_deployed = True
|
|
226
252
|
try:
|
|
@@ -228,6 +254,139 @@ class InteractiveViewer(ABC):
|
|
|
228
254
|
finally:
|
|
229
255
|
self._app_deployed = False
|
|
230
256
|
|
|
257
|
+
def _prepare_function(
|
|
258
|
+
self,
|
|
259
|
+
func: Callable,
|
|
260
|
+
context: Optional[str] = "",
|
|
261
|
+
) -> Callable:
|
|
262
|
+
# Check if func is Callable
|
|
263
|
+
if not callable(func):
|
|
264
|
+
raise ValueError(f"Function {func} is not callable")
|
|
265
|
+
|
|
266
|
+
# Handle partial functions
|
|
267
|
+
if isinstance(func, partial):
|
|
268
|
+
# Create new partial with self as first arg if not already there
|
|
269
|
+
get_self = (
|
|
270
|
+
lambda func: hasattr(func.func, "__self__") and func.func.__self__
|
|
271
|
+
)
|
|
272
|
+
get_name = lambda func: func.func.__name__
|
|
273
|
+
else:
|
|
274
|
+
get_self = lambda func: hasattr(func, "__self__") and func.__self__
|
|
275
|
+
get_name = lambda func: func.__name__
|
|
276
|
+
|
|
277
|
+
# Check if it's a class method
|
|
278
|
+
class_method = get_self(func) is self.__class__
|
|
279
|
+
if class_method:
|
|
280
|
+
raise ValueError(context + "Class methods are not supported.")
|
|
281
|
+
|
|
282
|
+
# Check if it's a bound method to another instance other than this one
|
|
283
|
+
if get_self(func) and get_self(func) is not self:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
context + "Bound methods to other instances are not supported."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Get function signature
|
|
289
|
+
try:
|
|
290
|
+
params = list(inspect.signature(func).parameters.values())
|
|
291
|
+
except ValueError:
|
|
292
|
+
# Handle built-ins or other objects without signatures
|
|
293
|
+
raise ValueError(context + f"Cannot inspect function signature for {func}")
|
|
294
|
+
|
|
295
|
+
# Look through params and check if there are two positional parameters (including self for bound methods)
|
|
296
|
+
bound_method = get_self(func) is self
|
|
297
|
+
positional_params = 0 + bound_method
|
|
298
|
+
optional_part = ""
|
|
299
|
+
for param in params:
|
|
300
|
+
# Check if it's a positional parameter. If it is, count it.
|
|
301
|
+
# As long as we have less than 2 positional parameters, we're good.
|
|
302
|
+
# When we already have 2 positional parameters, we need to make sure any other positional parameters have defaults.
|
|
303
|
+
if param.kind in (
|
|
304
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
305
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
306
|
+
):
|
|
307
|
+
if positional_params < 2:
|
|
308
|
+
positional_params += 1
|
|
309
|
+
else:
|
|
310
|
+
if param.default == inspect.Parameter.empty:
|
|
311
|
+
positional_params += 1
|
|
312
|
+
else:
|
|
313
|
+
optional_part += f", {param.name}={param.default!r}"
|
|
314
|
+
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
315
|
+
optional_part += f", **{param.name}"
|
|
316
|
+
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
317
|
+
optional_part += (
|
|
318
|
+
f", {param.name}={param.default!r}"
|
|
319
|
+
if param.default != inspect.Parameter.empty
|
|
320
|
+
else f", {param.name}"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if positional_params != 2:
|
|
324
|
+
func_name = get_name(func)
|
|
325
|
+
if isinstance(func, partial):
|
|
326
|
+
func_sig = str(inspect.signature(func))
|
|
327
|
+
if bound_method:
|
|
328
|
+
func_sig = "(" + "self, " + func_sig[1:]
|
|
329
|
+
msg = (
|
|
330
|
+
context
|
|
331
|
+
+ "\n"
|
|
332
|
+
+ f"Your partial function '{func_name}' has an incorrect signature.\n"
|
|
333
|
+
"Partial functions must have exactly two positional parameters.\n"
|
|
334
|
+
"The first parameter should be self -- it corresponds to the viewer.\n"
|
|
335
|
+
"The second parameter should be state -- a dictionary of the current state of the viewer.\n"
|
|
336
|
+
"\nYour partial function effectivelylooks like this:\n"
|
|
337
|
+
f"def {func_name}{func_sig}:\n"
|
|
338
|
+
" ... your function code ..."
|
|
339
|
+
)
|
|
340
|
+
raise ValueError(msg)
|
|
341
|
+
|
|
342
|
+
if bound_method:
|
|
343
|
+
original_method = getattr(get_self(func).__class__, get_name(func))
|
|
344
|
+
func_sig = str(inspect.signature(original_method))
|
|
345
|
+
|
|
346
|
+
msg = (
|
|
347
|
+
context + "\n"
|
|
348
|
+
f"Your bound method '{func_name}{func_sig}' has an incorrect signature.\n"
|
|
349
|
+
"Bound methods must have exactly one positional parameter in addition to self.\n"
|
|
350
|
+
"The first parameter should be self -- it corresponds to the viewer.\n"
|
|
351
|
+
"The second parameter should be state -- a dictionary of the current state of the viewer.\n"
|
|
352
|
+
"\nYour method looks like this:\n"
|
|
353
|
+
"class YourViewer(InteractiveViewer):\n"
|
|
354
|
+
f" def {func_name}{func_sig}:\n"
|
|
355
|
+
" ... your function code ...\n"
|
|
356
|
+
"\nIt should look like this:\n"
|
|
357
|
+
"class YourViewer(InteractiveViewer):\n"
|
|
358
|
+
f" def {func_name}(self, state{optional_part}):\n"
|
|
359
|
+
" ... your function code ..."
|
|
360
|
+
)
|
|
361
|
+
raise ValueError(msg)
|
|
362
|
+
else:
|
|
363
|
+
func_sig = str(inspect.signature(func))
|
|
364
|
+
|
|
365
|
+
msg = (
|
|
366
|
+
context + "\n"
|
|
367
|
+
f"Your function '{func_name}{func_sig}' has an incorrect signature.\n"
|
|
368
|
+
"External functions must have exactly two positional parameters.\n"
|
|
369
|
+
"The first parameter should be viewer -- it corresponds to the viewer.\n"
|
|
370
|
+
"The second parameter should be state -- a dictionary of the current state of the viewer.\n"
|
|
371
|
+
"\nYour function looks like this:\n"
|
|
372
|
+
f"def {func_name}{func_sig}:\n"
|
|
373
|
+
" ... your function code ...\n"
|
|
374
|
+
"\nIt should look like this:\n"
|
|
375
|
+
f"def {func_name}(viewer, state{optional_part}):\n"
|
|
376
|
+
" ... your function code ..."
|
|
377
|
+
)
|
|
378
|
+
raise ValueError(msg)
|
|
379
|
+
|
|
380
|
+
# If not a bound method, wrap it to inject self when called with just the state
|
|
381
|
+
if bound_method:
|
|
382
|
+
return func
|
|
383
|
+
|
|
384
|
+
@wraps(func)
|
|
385
|
+
def func_with_self(*args, **kwargs):
|
|
386
|
+
return func(self, *args, **kwargs)
|
|
387
|
+
|
|
388
|
+
return func_with_self
|
|
389
|
+
|
|
231
390
|
def perform_callbacks(self, name: str) -> bool:
|
|
232
391
|
"""Perform callbacks for all parameters that have changed"""
|
|
233
392
|
if self._in_callbacks:
|
|
@@ -266,6 +425,11 @@ class InteractiveViewer(ABC):
|
|
|
266
425
|
if isinstance(parameter_name, str):
|
|
267
426
|
parameter_name = [parameter_name]
|
|
268
427
|
|
|
428
|
+
callback = self._prepare_function(
|
|
429
|
+
callback,
|
|
430
|
+
context="Setting on_change callback:",
|
|
431
|
+
)
|
|
432
|
+
|
|
269
433
|
for param_name in parameter_name:
|
|
270
434
|
if param_name not in self.parameters:
|
|
271
435
|
raise ValueError(f"Parameter '{param_name}' is not registered!")
|
|
@@ -742,7 +906,7 @@ class InteractiveViewer(ABC):
|
|
|
742
906
|
else:
|
|
743
907
|
self.parameters[name] = new_param
|
|
744
908
|
|
|
745
|
-
@validate_parameter_operation("add",
|
|
909
|
+
@validate_parameter_operation("add", ActionType.button)
|
|
746
910
|
def add_button(
|
|
747
911
|
self,
|
|
748
912
|
name: str,
|
|
@@ -774,11 +938,12 @@ class InteractiveViewer(ABC):
|
|
|
774
938
|
"""
|
|
775
939
|
try:
|
|
776
940
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
callback
|
|
941
|
+
callback = self._prepare_function(
|
|
942
|
+
callback,
|
|
943
|
+
context="Setting button callback:",
|
|
944
|
+
)
|
|
780
945
|
|
|
781
|
-
new_param =
|
|
946
|
+
new_param = ActionType.button.value(name, label, callback)
|
|
782
947
|
except Exception as e:
|
|
783
948
|
raise ParameterAddError(name, "button", str(e)) from e
|
|
784
949
|
else:
|
|
@@ -1228,7 +1393,7 @@ class InteractiveViewer(ABC):
|
|
|
1228
1393
|
if updates:
|
|
1229
1394
|
self.parameters[name].update(updates)
|
|
1230
1395
|
|
|
1231
|
-
@validate_parameter_operation("update",
|
|
1396
|
+
@validate_parameter_operation("update", ActionType.button)
|
|
1232
1397
|
def update_button(
|
|
1233
1398
|
self,
|
|
1234
1399
|
name: str,
|
|
@@ -1240,7 +1405,7 @@ class InteractiveViewer(ABC):
|
|
|
1240
1405
|
Update a button parameter's label and/or callback function.
|
|
1241
1406
|
|
|
1242
1407
|
Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_button`.
|
|
1243
|
-
See :class:`~syd.parameters.
|
|
1408
|
+
See :class:`~syd.parameters.ButtonAction` for details.
|
|
1244
1409
|
|
|
1245
1410
|
Parameters
|
|
1246
1411
|
----------
|
|
@@ -1263,6 +1428,10 @@ class InteractiveViewer(ABC):
|
|
|
1263
1428
|
if label is not _NO_UPDATE:
|
|
1264
1429
|
updates["label"] = label
|
|
1265
1430
|
if callback is not _NO_UPDATE:
|
|
1431
|
+
callback = self._prepare_function(
|
|
1432
|
+
callback,
|
|
1433
|
+
context="Updating button callback:",
|
|
1434
|
+
)
|
|
1266
1435
|
updates["callback"] = callback
|
|
1267
1436
|
if updates:
|
|
1268
1437
|
self.parameters[name].update(updates)
|
|
@@ -4,9 +4,11 @@ from contextlib import contextmanager
|
|
|
4
4
|
import ipywidgets as widgets
|
|
5
5
|
from IPython.display import display
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
|
+
import warnings
|
|
8
|
+
from ..parameters import ParameterUpdateWarning
|
|
7
9
|
|
|
8
10
|
from ..interactive_viewer import InteractiveViewer
|
|
9
|
-
from .widgets import
|
|
11
|
+
from .widgets import BaseWidget, create_widget
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@contextmanager
|
|
@@ -26,7 +28,6 @@ class LayoutConfig:
|
|
|
26
28
|
figure_width: float = 8.0
|
|
27
29
|
figure_height: float = 6.0
|
|
28
30
|
controls_width_percent: int = 30
|
|
29
|
-
continuous_update: bool = False
|
|
30
31
|
|
|
31
32
|
def __post_init__(self):
|
|
32
33
|
valid_positions = ["left", "top"]
|
|
@@ -50,7 +51,8 @@ class NotebookDeployment:
|
|
|
50
51
|
self,
|
|
51
52
|
viewer: InteractiveViewer,
|
|
52
53
|
layout_config: Optional[LayoutConfig] = None,
|
|
53
|
-
|
|
54
|
+
continuous: bool = False,
|
|
55
|
+
suppress_warnings: bool = False,
|
|
54
56
|
):
|
|
55
57
|
if not isinstance(viewer, InteractiveViewer): # type: ignore
|
|
56
58
|
raise TypeError(
|
|
@@ -59,10 +61,11 @@ class NotebookDeployment:
|
|
|
59
61
|
|
|
60
62
|
self.viewer = viewer
|
|
61
63
|
self.config = layout_config or LayoutConfig()
|
|
62
|
-
self.
|
|
64
|
+
self.continuous = continuous
|
|
65
|
+
self.suppress_warnings = suppress_warnings
|
|
63
66
|
|
|
64
67
|
# Initialize containers
|
|
65
|
-
self.parameter_widgets: Dict[str,
|
|
68
|
+
self.parameter_widgets: Dict[str, BaseWidget] = {}
|
|
66
69
|
self.layout_widgets = self._create_layout_controls()
|
|
67
70
|
self.plot_output = widgets.Output()
|
|
68
71
|
|
|
@@ -82,7 +85,7 @@ class NotebookDeployment:
|
|
|
82
85
|
min=20,
|
|
83
86
|
max=80,
|
|
84
87
|
description="Controls Width %",
|
|
85
|
-
|
|
88
|
+
continuous=True,
|
|
86
89
|
layout=widgets.Layout(width="95%"),
|
|
87
90
|
style={"description_width": "initial"},
|
|
88
91
|
)
|
|
@@ -95,38 +98,52 @@ class NotebookDeployment:
|
|
|
95
98
|
def _create_parameter_widgets(self) -> None:
|
|
96
99
|
"""Create widget instances for all parameters."""
|
|
97
100
|
for name, param in self.viewer.parameters.items():
|
|
98
|
-
widget =
|
|
101
|
+
widget = create_widget(
|
|
99
102
|
param,
|
|
100
|
-
|
|
103
|
+
continuous=self.continuous,
|
|
101
104
|
)
|
|
102
105
|
|
|
103
106
|
# Store in widget dict
|
|
104
107
|
self.parameter_widgets[name] = widget
|
|
105
108
|
|
|
106
|
-
def
|
|
107
|
-
"""Handle
|
|
109
|
+
def _handle_widget_engagement(self, name: str) -> None:
|
|
110
|
+
"""Handle engagement with an interactive widget."""
|
|
108
111
|
if self._updating:
|
|
112
|
+
print(
|
|
113
|
+
"Already updating -- there's a circular dependency!"
|
|
114
|
+
"This is probably caused by failing to disable callbacks for a parameter."
|
|
115
|
+
"It's a bug --- tell the developer on github issues please."
|
|
116
|
+
)
|
|
109
117
|
return
|
|
110
118
|
|
|
111
119
|
try:
|
|
112
120
|
self._updating = True
|
|
113
|
-
widget = self.parameter_widgets[name]
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.viewer.set_parameter_value(name, widget.value)
|
|
122
|
+
# Optionally suppress warnings during parameter updates
|
|
123
|
+
with warnings.catch_warnings():
|
|
124
|
+
if self.suppress_warnings:
|
|
125
|
+
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
self._sync_widgets_with_state(exclude=name)
|
|
127
|
+
widget = self.parameter_widgets[name]
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
if widget._is_action:
|
|
130
|
+
parameter = self.viewer.parameters[name]
|
|
131
|
+
parameter.callback(self.viewer.get_state())
|
|
132
|
+
else:
|
|
133
|
+
self.viewer.set_parameter_value(name, widget.value)
|
|
134
|
+
|
|
135
|
+
# Update any widgets that changed due to dependencies
|
|
136
|
+
self._sync_widgets_with_state(exclude=name)
|
|
137
|
+
|
|
138
|
+
# Update the plot
|
|
139
|
+
self._update_plot()
|
|
126
140
|
|
|
127
141
|
finally:
|
|
128
142
|
self._updating = False
|
|
129
143
|
|
|
144
|
+
def _handle_action(self, name: str) -> None:
|
|
145
|
+
"""Handle actions for parameter widgets."""
|
|
146
|
+
|
|
130
147
|
def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
|
|
131
148
|
"""Sync widget values with viewer state."""
|
|
132
149
|
for name, parameter in self.viewer.parameters.items():
|
|
@@ -181,7 +198,7 @@ class NotebookDeployment:
|
|
|
181
198
|
|
|
182
199
|
# Set up parameter widgets with their observe callbacks
|
|
183
200
|
for name, widget in self.parameter_widgets.items():
|
|
184
|
-
widget.observe(lambda change, n=name: self.
|
|
201
|
+
widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
|
|
185
202
|
|
|
186
203
|
# Create parameter controls section
|
|
187
204
|
param_box = widgets.VBox(
|
|
@@ -225,7 +242,7 @@ class NotebookDeployment:
|
|
|
225
242
|
|
|
226
243
|
def deploy(self) -> None:
|
|
227
244
|
"""Deploy the interactive viewer with proper state management."""
|
|
228
|
-
with self.viewer.
|
|
245
|
+
with self.viewer._deploy_app():
|
|
229
246
|
# Create widgets
|
|
230
247
|
self._create_parameter_widgets()
|
|
231
248
|
|