syd 0.1.3__py3-none-any.whl → 0.1.5__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/interactive_viewer.py CHANGED
@@ -1,12 +1,35 @@
1
- from typing import List, Any, Callable, Dict, Tuple
1
+ from typing import List, Any, Callable, Dict, Tuple, Union, Optional
2
2
  from functools import wraps
3
3
  from contextlib import contextmanager
4
+ from abc import ABC, abstractmethod
4
5
  from matplotlib.figure import Figure
5
6
 
6
- from .parameters import ParameterType, Parameter
7
+ from .parameters import (
8
+ ParameterType,
9
+ Parameter,
10
+ ParameterAddError,
11
+ ParameterUpdateError,
12
+ )
7
13
 
8
14
 
9
- def validate_parameter_operation(operation: str, parameter_type: ParameterType) -> Callable:
15
+ class _NoUpdate:
16
+ """Singleton class to represent a non-update in parameter operations."""
17
+
18
+ _instance = None
19
+
20
+ def __new__(cls):
21
+ if cls._instance is None:
22
+ cls._instance = super().__new__(cls)
23
+ return cls._instance
24
+
25
+
26
+ # Create the singleton instance
27
+ _NO_UPDATE = _NoUpdate()
28
+
29
+
30
+ def validate_parameter_operation(
31
+ operation: str, parameter_type: ParameterType
32
+ ) -> Callable:
10
33
  """
11
34
  Decorator that validates parameter operations for the InteractiveViewer class.
12
35
 
@@ -36,52 +59,166 @@ def validate_parameter_operation(operation: str, parameter_type: ParameterType)
36
59
  """
37
60
 
38
61
  def decorator(func: Callable) -> Callable:
62
+ if operation not in ["add", "update"]:
63
+ raise ValueError(
64
+ "Incorrect use of validate_parameter_operation decorator. Must be called with 'add' or 'update' as the first argument."
65
+ )
66
+
39
67
  @wraps(func)
40
- def wrapper(self: "InteractiveViewer", name: str, *args, **kwargs):
68
+ def wrapper(self: "InteractiveViewer", name: Any, *args, **kwargs):
41
69
  # Validate operation matches method name (add/update)
42
70
  if not func.__name__.startswith(operation):
43
- raise ValueError(f"Invalid operation type specified ({operation}) for method {func.__name__}")
71
+ raise ValueError(
72
+ f"Invalid operation type specified ({operation}) for method {func.__name__}"
73
+ )
74
+
75
+ # Validate parameter name is a string
76
+ if not isinstance(name, str):
77
+ if operation == "add":
78
+ raise ParameterAddError(
79
+ name, parameter_type.name, "Parameter name must be a string"
80
+ )
81
+ elif operation == "update":
82
+ raise ParameterUpdateError(
83
+ name, parameter_type.name, "Parameter name must be a string"
84
+ )
44
85
 
45
86
  # Validate deployment state
46
87
  if operation == "add" and self._app_deployed:
47
- raise RuntimeError("The app is currently deployed, cannot add a new parameter right now.")
88
+ raise RuntimeError(
89
+ "The app is currently deployed, cannot add a new parameter right now."
90
+ )
91
+
92
+ if operation == "add":
93
+ if name in self.parameters:
94
+ raise ParameterAddError(
95
+ name, parameter_type.name, "Parameter already exists!"
96
+ )
48
97
 
49
98
  # For updates, validate parameter existence and type
50
99
  if operation == "update":
51
100
  if name not in self.parameters:
52
- raise ValueError(f"Parameter called {name} not found - you can only update registered parameters!")
53
- if type(self.parameters[name]) != parameter_type:
101
+ raise ParameterUpdateError(
102
+ name,
103
+ parameter_type.name,
104
+ "Parameter not found - you can only update registered parameters!",
105
+ )
106
+ if type(self.parameters[name]) != parameter_type.value:
54
107
  msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])})"
55
- raise TypeError(msg)
108
+ raise ParameterUpdateError(name, parameter_type.name, msg)
56
109
 
57
- return func(self, name, *args, **kwargs)
110
+ try:
111
+ return func(self, name, *args, **kwargs)
112
+ except Exception as e:
113
+ if operation == "add":
114
+ raise ParameterAddError(name, parameter_type.name, str(e)) from e
115
+ elif operation == "update":
116
+ raise ParameterUpdateError(name, parameter_type.name, str(e)) from e
117
+ else:
118
+ raise e
58
119
 
59
120
  return wrapper
60
121
 
61
122
  return decorator
62
123
 
63
124
 
64
- class InteractiveViewer:
125
+ class InteractiveViewer(ABC):
126
+ """
127
+ Base class for creating interactive matplotlib figures with GUI controls.
128
+
129
+ This class helps you create interactive visualizations by adding GUI elements
130
+ (like sliders, dropdowns, etc.) that update your plot in real-time. To use it:
131
+
132
+ 1. Create a subclass and implement the plot() method
133
+ 2. Add parameters using add_* methods before deploying
134
+ 3. Use on_change() to make parameters update the plot
135
+ 4. Use update_* methods to update parameter values and properties
136
+ 5. Deploy the app to show the interactive figure
137
+
138
+ Examples
139
+ --------
140
+ >>> class MyViewer(InteractiveViewer):
141
+ ... def plot(self, state: Dict[str, Any]):
142
+ ... fig = plt.figure()
143
+ ... plt.plot([0, state['x']])
144
+ ... return fig
145
+ ...
146
+ ... def update_based_on_x(self, state: Dict[str, Any]):
147
+ ... self.update_float('x', value=state['x'])
148
+ ...
149
+ >>> viewer = MyViewer()
150
+ >>> viewer.add_float('x', value=1.0, min_value=0, max_value=10)
151
+ >>> viewer.on_change('x', viewer.update_based_on_x)
152
+ """
153
+
154
+ parameters: Dict[str, Parameter]
155
+ callbacks: Dict[str, List[Callable]]
156
+ state: Dict[str, Any]
157
+ _app_deployed: bool
158
+ _in_callbacks: bool
159
+
65
160
  def __new__(cls, *args, **kwargs):
66
161
  instance = super().__new__(cls)
67
162
  instance.parameters = {}
68
163
  instance.callbacks = {}
69
164
  instance.state = {}
70
165
  instance._app_deployed = False
166
+ instance._in_callbacks = False
71
167
  return instance
72
168
 
73
- def __init__(self):
74
- self.parameters: Dict[str, Parameter] = {}
75
- self.callbacks: Dict[str, List[Callable]] = {}
76
- self.state = {}
77
- self._app_deployed = False
78
-
79
169
  def get_state(self) -> Dict[str, Any]:
170
+ """
171
+ Get the current values of all parameters.
172
+
173
+ Returns
174
+ -------
175
+ dict
176
+ Dictionary mapping parameter names to their current values
177
+
178
+ Examples
179
+ --------
180
+ >>> viewer.add_float('x', value=1.0, min_value=0, max_value=10)
181
+ >>> viewer.add_text('label', value='data')
182
+ >>> viewer.get_state()
183
+ {'x': 1.0, 'label': 'data'}
184
+ """
80
185
  return {name: param.value for name, param in self.parameters.items()}
81
186
 
187
+ @abstractmethod
82
188
  def plot(self, **kwargs) -> Figure:
189
+ """
190
+ Create and return a matplotlib figure.
191
+
192
+ This method must be implemented in your subclass. It should create a new
193
+ figure using the current parameter values from self.parameters.
194
+
195
+ Returns
196
+ -------
197
+ matplotlib.figure.Figure
198
+ The figure to display
199
+
200
+ Notes
201
+ -----
202
+ - Create a new figure each time, don't reuse old ones
203
+ - Access parameter values using self.parameters['name'].value
204
+ - Return the figure object, don't call plt.show()
205
+ """
83
206
  raise NotImplementedError("Subclasses must implement the plot method")
84
207
 
208
+ def deploy(self, env: str = "notebook", **kwargs):
209
+ """Deploy the app in a notebook or standalone environment"""
210
+ if env == "notebook":
211
+ from .notebook_deploy import NotebookDeployment
212
+
213
+ deployer = NotebookDeployment(self, **kwargs)
214
+ deployer.deploy()
215
+
216
+ return deployer
217
+ else:
218
+ raise ValueError(
219
+ f"Unsupported environment: {env}, only 'notebook' is supported right now."
220
+ )
221
+
85
222
  @contextmanager
86
223
  def deploy_app(self):
87
224
  """Internal context manager to control app deployment state"""
@@ -93,21 +230,68 @@ class InteractiveViewer:
93
230
 
94
231
  def perform_callbacks(self, name: str) -> bool:
95
232
  """Perform callbacks for all parameters that have changed"""
96
- if name in self.callbacks:
97
- state = self.get_state()
98
- for callback in self.callbacks[name]:
99
- callback(state)
100
-
101
- def on_change(self, parameter_name: str, callback: Callable):
102
- """Register a function to be called when a parameter changes."""
103
- if parameter_name not in self.parameters:
104
- raise ValueError(f"Parameter '{parameter_name}' is not registered!")
105
- if parameter_name not in self.callbacks:
106
- self.callbacks[parameter_name] = []
107
- self.callbacks[parameter_name].append(callback)
233
+ if self._in_callbacks:
234
+ return
235
+ try:
236
+ self._in_callbacks = True
237
+ if name in self.callbacks:
238
+ state = self.get_state()
239
+ for callback in self.callbacks[name]:
240
+ callback(state)
241
+ finally:
242
+ self._in_callbacks = False
243
+
244
+ def on_change(self, parameter_name: Union[str, List[str]], callback: Callable):
245
+ """
246
+ Register a function to run when parameters change.
247
+
248
+ The callback function will receive a dictionary of all current parameter
249
+ values whenever any of the specified parameters change.
250
+
251
+ Parameters
252
+ ----------
253
+ parameter_name : str or list of str
254
+ Name(s) of parameters to watch for changes
255
+ callback : callable
256
+ Function to call when changes occur. Should accept a single dict argument
257
+ containing the current state.
258
+
259
+ Examples
260
+ --------
261
+ >>> def update_plot(state):
262
+ ... print(f"x changed to {state['x']}")
263
+ >>> viewer.on_change('x', update_plot)
264
+ >>> viewer.on_change(['x', 'y'], lambda s: viewer.plot()) # Update on either change
265
+ """
266
+ if isinstance(parameter_name, str):
267
+ parameter_name = [parameter_name]
268
+
269
+ for param_name in parameter_name:
270
+ if param_name not in self.parameters:
271
+ raise ValueError(f"Parameter '{param_name}' is not registered!")
272
+ if param_name not in self.callbacks:
273
+ self.callbacks[param_name] = []
274
+ self.callbacks[param_name].append(callback)
108
275
 
109
276
  def set_parameter_value(self, name: str, value: Any) -> None:
110
- """Set a parameter value and trigger dependency updates"""
277
+ """
278
+ Update a parameter's value and trigger any callbacks.
279
+
280
+ This is a lower-level method - usually you'll want to use the update_*
281
+ methods instead (e.g., update_float, update_text, etc.).
282
+
283
+ Parameters
284
+ ----------
285
+ name : str
286
+ Name of the parameter to update
287
+ value : Any
288
+ New value for the parameter
289
+
290
+ Raises
291
+ ------
292
+ ValueError
293
+ If the parameter doesn't exist or the value is invalid
294
+ """
111
295
  if name not in self.parameters:
112
296
  raise ValueError(f"Parameter {name} not found")
113
297
 
@@ -119,92 +303,966 @@ class InteractiveViewer:
119
303
 
120
304
  # -------------------- parameter registration methods --------------------
121
305
  @validate_parameter_operation("add", ParameterType.text)
122
- def add_text(self, name: str, default: str = "") -> None:
123
- self.parameters[name] = ParameterType.text.value(name, default)
306
+ def add_text(self, name: str, *, value: str) -> None:
307
+ """
308
+ Add a text input parameter to the viewer.
309
+
310
+ Creates a text box in the GUI that accepts any string input.
311
+ See :class:`~syd.parameters.TextParameter` for details.
312
+
313
+ Parameters
314
+ ----------
315
+ name : str
316
+ Name of the parameter (used as label in GUI)
317
+ value : str
318
+ Initial text value
319
+
320
+ Examples
321
+ --------
322
+ >>> viewer.add_text('title', value='My Plot')
323
+ >>> viewer.get_state()['title']
324
+ 'My Plot'
325
+ """
326
+ try:
327
+ new_param = ParameterType.text.value(name, value)
328
+ except Exception as e:
329
+ raise ParameterAddError(name, "text", str(e)) from e
330
+ else:
331
+ self.parameters[name] = new_param
332
+
333
+ @validate_parameter_operation("add", ParameterType.boolean)
334
+ def add_boolean(self, name: str, *, value: bool) -> None:
335
+ """
336
+ Add a boolean parameter to the viewer.
337
+
338
+ Creates a checkbox in the GUI that can be toggled on/off.
339
+ See :class:`~syd.parameters.BooleanParameter` for details.
340
+
341
+ Parameters
342
+ ----------
343
+ name : str
344
+ Name of the parameter (used as label in GUI)
345
+ value : bool
346
+ Initial state (True=checked, False=unchecked)
347
+
348
+ Examples
349
+ --------
350
+ >>> viewer.add_boolean('show_grid', value=True)
351
+ >>> viewer.get_state()['show_grid']
352
+ True
353
+ """
354
+ try:
355
+ new_param = ParameterType.boolean.value(name, value)
356
+ except Exception as e:
357
+ raise ParameterAddError(name, "boolean", str(e)) from e
358
+ else:
359
+ self.parameters[name] = new_param
124
360
 
125
361
  @validate_parameter_operation("add", ParameterType.selection)
126
- def add_selection(self, name: str, options: List[Any], default: Any = None) -> None:
127
- self.parameters[name] = ParameterType.selection.value(name, options, default)
362
+ def add_selection(self, name: str, *, value: Any, options: List[Any]) -> None:
363
+ """
364
+ Add a single-selection parameter to the viewer.
365
+
366
+ Creates a dropdown menu in the GUI where users can select one option.
367
+ See :class:`~syd.parameters.SelectionParameter` for details.
368
+
369
+ Parameters
370
+ ----------
371
+ name : str
372
+ Name of the parameter (used as label in GUI)
373
+ value : Any
374
+ Initially selected value (must be one of the options)
375
+ options : list
376
+ List of values that can be selected
377
+
378
+ Examples
379
+ --------
380
+ >>> viewer.add_selection('color', value='red',
381
+ ... options=['red', 'green', 'blue'])
382
+ >>> viewer.get_state()['color']
383
+ 'red'
384
+ """
385
+ try:
386
+ new_param = ParameterType.selection.value(name, value, options)
387
+ except Exception as e:
388
+ raise ParameterAddError(name, "selection", str(e)) from e
389
+ else:
390
+ self.parameters[name] = new_param
128
391
 
129
392
  @validate_parameter_operation("add", ParameterType.multiple_selection)
130
- def add_multiple_selection(self, name: str, options: List[Any], default: List[Any] = None) -> None:
131
- self.parameters[name] = ParameterType.multiple_selection.value(name, options, default)
393
+ def add_multiple_selection(
394
+ self, name: str, *, value: List[Any], options: List[Any]
395
+ ) -> None:
396
+ """
397
+ Add a multiple-selection parameter to the viewer.
132
398
 
133
- @validate_parameter_operation("add", ParameterType.boolean)
134
- def add_boolean(self, name: str, default: bool = True) -> None:
135
- self.parameters[name] = ParameterType.boolean.value(name, default)
399
+ Creates a set of checkboxes or a multi-select dropdown in the GUI where
400
+ users can select any number of options.
401
+ See :class:`~syd.parameters.MultipleSelectionParameter` for details.
402
+
403
+ Parameters
404
+ ----------
405
+ name : str
406
+ Name of the parameter (used as label in GUI)
407
+ value : list
408
+ Initially selected values (must all be in options)
409
+ options : list
410
+ List of values that can be selected
411
+
412
+ Examples
413
+ --------
414
+ >>> viewer.add_multiple_selection('toppings',
415
+ ... value=['cheese'],
416
+ ... options=['cheese', 'pepperoni', 'mushrooms'])
417
+ >>> viewer.get_state()['toppings']
418
+ ['cheese']
419
+ """
420
+ try:
421
+ new_param = ParameterType.multiple_selection.value(name, value, options)
422
+ except Exception as e:
423
+ raise ParameterAddError(name, "multiple_selection", str(e)) from e
424
+ else:
425
+ self.parameters[name] = new_param
136
426
 
137
427
  @validate_parameter_operation("add", ParameterType.integer)
138
- def add_integer(self, name: str, min_value: int = None, max_value: int = None, default: int = 0) -> None:
139
- self.parameters[name] = ParameterType.integer.value(name, min_value, max_value, default)
428
+ def add_integer(
429
+ self,
430
+ name: str,
431
+ *,
432
+ value: Union[float, int],
433
+ min_value: Union[float, int],
434
+ max_value: Union[float, int],
435
+ ) -> None:
436
+ """
437
+ Add an integer parameter to the viewer.
438
+
439
+ Creates a slider in the GUI that lets users select whole numbers between
440
+ min_value and max_value. Values will be clamped to stay within bounds.
441
+ See :class:`~syd.parameters.IntegerParameter` for details.
442
+
443
+ Parameters
444
+ ----------
445
+ name : str
446
+ Name of the parameter (used as label in GUI)
447
+ value : int
448
+ Initial value (will be clamped between min_value and max_value)
449
+ min_value : int
450
+ Minimum allowed value
451
+ max_value : int
452
+ Maximum allowed value
453
+
454
+ Examples
455
+ --------
456
+ >>> viewer.add_integer('count', value=5, min_value=0, max_value=10)
457
+ >>> viewer.get_state()['count']
458
+ 5
459
+ >>> viewer.update_integer('count', value=15) # Will be clamped to 10
460
+ >>> viewer.get_state()['count']
461
+ 10
462
+ """
463
+ try:
464
+ new_param = ParameterType.integer.value(
465
+ name,
466
+ value,
467
+ min_value,
468
+ max_value,
469
+ )
470
+ except Exception as e:
471
+ raise ParameterAddError(name, "number", str(e)) from e
472
+ else:
473
+ self.parameters[name] = new_param
140
474
 
141
475
  @validate_parameter_operation("add", ParameterType.float)
142
- def add_float(self, name: str, min_value: float = None, max_value: float = None, default: float = 0.0, step: float = 0.1) -> None:
143
- self.parameters[name] = ParameterType.float.value(name, min_value, max_value, default, step)
476
+ def add_float(
477
+ self,
478
+ name: str,
479
+ *,
480
+ value: Union[float, int],
481
+ min_value: Union[float, int],
482
+ max_value: Union[float, int],
483
+ step: float = 0.1,
484
+ ) -> None:
485
+ """
486
+ Add a decimal number parameter to the viewer.
487
+
488
+ Creates a slider in the GUI that lets users select numbers between
489
+ min_value and max_value. Values will be rounded to the nearest step
490
+ and clamped to stay within bounds.
491
+
492
+ Parameters
493
+ ----------
494
+ name : str
495
+ Name of the parameter (used as label in GUI)
496
+ value : float
497
+ Initial value (will be clamped between min_value and max_value)
498
+ min_value : float
499
+ Minimum allowed value
500
+ max_value : float
501
+ Maximum allowed value
502
+ step : float, optional
503
+ Size of each increment (default: 0.1)
504
+
505
+ Examples
506
+ --------
507
+ >>> viewer.add_float('temperature', value=20.0,
508
+ ... min_value=0.0, max_value=100.0, step=0.5)
509
+ >>> viewer.get_state()['temperature']
510
+ 20.0
511
+ >>> viewer.update_float('temperature', value=20.7) # Will round to 20.5
512
+ >>> viewer.get_state()['temperature']
513
+ 20.5
514
+ """
515
+ try:
516
+ new_param = ParameterType.float.value(
517
+ name,
518
+ value,
519
+ min_value,
520
+ max_value,
521
+ step,
522
+ )
523
+ except Exception as e:
524
+ raise ParameterAddError(name, "number", str(e)) from e
525
+ else:
526
+ self.parameters[name] = new_param
144
527
 
145
- @validate_parameter_operation("add", ParameterType.integer_pair)
146
- def add_integer_pair(
528
+ @validate_parameter_operation("add", ParameterType.integer_range)
529
+ def add_integer_range(
147
530
  self,
148
531
  name: str,
149
- default: Tuple[int, int],
150
- min_value: int = None,
151
- max_value: int = None,
532
+ *,
533
+ value: Tuple[Union[float, int], Union[float, int]],
534
+ min_value: Union[float, int],
535
+ max_value: Union[float, int],
152
536
  ) -> None:
153
- self.parameters[name] = ParameterType.integer_pair.value(name, default, min_value, max_value)
537
+ """
538
+ Add a range parameter for whole numbers to the viewer.
539
+
540
+ Creates a range slider in the GUI that lets users select a range of integers
541
+ between min_value and max_value. The range is specified as (low, high) and
542
+ both values will be clamped to stay within bounds.
543
+ See :class:`~syd.parameters.IntegerRangeParameter` for details.
544
+
545
+ Parameters
546
+ ----------
547
+ name : str
548
+ Name of the parameter (used as label in GUI)
549
+ value : tuple[int, int]
550
+ Initial (low, high) values
551
+ min_value : int
552
+ Minimum allowed value for both low and high
553
+ max_value : int
554
+ Maximum allowed value for both low and high
555
+
556
+ Examples
557
+ --------
558
+ >>> viewer.add_integer_range('age_range',
559
+ ... value=(25, 35),
560
+ ... min_value=18, max_value=100)
561
+ >>> viewer.get_state()['age_range']
562
+ (25, 35)
563
+ >>> # Values will be swapped if low > high
564
+ >>> viewer.update_integer_range('age_range', value=(40, 30))
565
+ >>> viewer.get_state()['age_range']
566
+ (30, 40)
567
+ """
568
+ try:
569
+ new_param = ParameterType.integer_range.value(
570
+ name,
571
+ value,
572
+ min_value,
573
+ max_value,
574
+ )
575
+ except Exception as e:
576
+ raise ParameterAddError(name, "integer_range", str(e)) from e
577
+ else:
578
+ self.parameters[name] = new_param
154
579
 
155
- @validate_parameter_operation("add", ParameterType.float_pair)
156
- def add_float_pair(
580
+ @validate_parameter_operation("add", ParameterType.float_range)
581
+ def add_float_range(
157
582
  self,
158
583
  name: str,
159
- default: Tuple[float, float],
160
- min_value: float = None,
161
- max_value: float = None,
584
+ *,
585
+ value: Tuple[Union[float, int], Union[float, int]],
586
+ min_value: Union[float, int],
587
+ max_value: Union[float, int],
162
588
  step: float = 0.1,
163
589
  ) -> None:
164
- self.parameters[name] = ParameterType.float_pair.value(name, default, min_value, max_value, step)
590
+ """
591
+ Add a range parameter for decimal numbers to the viewer.
592
+
593
+ Creates a range slider in the GUI that lets users select a range of numbers
594
+ between min_value and max_value. The range is specified as (low, high) and
595
+ both values will be rounded to the nearest step and clamped to stay within bounds.
596
+ See :class:`~syd.parameters.FloatRangeParameter` for details.
597
+
598
+ Parameters
599
+ ----------
600
+ name : str
601
+ Name of the parameter (used as label in GUI)
602
+ value : tuple[float, float]
603
+ Initial (low, high) values
604
+ min_value : float
605
+ Minimum allowed value for both low and high
606
+ max_value : float
607
+ Maximum allowed value for both low and high
608
+ step : float, optional
609
+ Size of each increment (default: 0.1)
610
+
611
+ Examples
612
+ --------
613
+ >>> viewer.add_float_range('price_range',
614
+ ... value=(10.0, 20.0),
615
+ ... min_value=0.0, max_value=100.0, step=0.5)
616
+ >>> viewer.get_state()['price_range']
617
+ (10.0, 20.0)
618
+ >>> # Values will be rounded to nearest step
619
+ >>> viewer.update_float_range('price_range', value=(10.7, 19.2))
620
+ >>> viewer.get_state()['price_range']
621
+ (10.5, 19.0)
622
+ """
623
+ try:
624
+ new_param = ParameterType.float_range.value(
625
+ name,
626
+ value,
627
+ min_value,
628
+ max_value,
629
+ step,
630
+ )
631
+ except Exception as e:
632
+ raise ParameterAddError(name, "float_range", str(e)) from e
633
+ else:
634
+ self.parameters[name] = new_param
635
+
636
+ @validate_parameter_operation("add", ParameterType.unbounded_integer)
637
+ def add_unbounded_integer(
638
+ self,
639
+ name: str,
640
+ *,
641
+ value: Union[float, int],
642
+ min_value: Optional[Union[float, int]] = None,
643
+ max_value: Optional[Union[float, int]] = None,
644
+ ) -> None:
645
+ """
646
+ Add an unbounded integer parameter to the viewer.
647
+
648
+ Creates a text input box in the GUI for entering whole numbers. Unlike
649
+ add_integer(), this allows very large numbers and optionally no minimum
650
+ or maximum bounds.
651
+ See :class:`~syd.parameters.UnboundedIntegerParameter` for details.
652
+
653
+ Parameters
654
+ ----------
655
+ name : str
656
+ Name of the parameter (used as label in GUI)
657
+ value : int
658
+ Initial value
659
+ min_value : int, optional
660
+ Minimum allowed value (or None for no minimum)
661
+ max_value : int, optional
662
+ Maximum allowed value (or None for no maximum)
663
+
664
+ Examples
665
+ --------
666
+ >>> viewer.add_unbounded_integer('population',
667
+ ... value=1000000,
668
+ ... min_value=0) # No maximum
669
+ >>> viewer.get_state()['population']
670
+ 1000000
671
+ >>> # Values below minimum will be clamped
672
+ >>> viewer.update_unbounded_integer('population', value=-5)
673
+ >>> viewer.get_state()['population']
674
+ 0
675
+ """
676
+ try:
677
+ new_param = ParameterType.unbounded_integer.value(
678
+ name,
679
+ value,
680
+ min_value,
681
+ max_value,
682
+ )
683
+ except Exception as e:
684
+ raise ParameterAddError(name, "unbounded_integer", str(e)) from e
685
+ else:
686
+ self.parameters[name] = new_param
687
+
688
+ @validate_parameter_operation("add", ParameterType.unbounded_float)
689
+ def add_unbounded_float(
690
+ self,
691
+ name: str,
692
+ *,
693
+ value: Union[float, int],
694
+ min_value: Optional[Union[float, int]] = None,
695
+ max_value: Optional[Union[float, int]] = None,
696
+ step: Optional[float] = None,
697
+ ) -> None:
698
+ """
699
+ Add an unbounded decimal number parameter to the viewer.
700
+
701
+ Creates a text input box in the GUI for entering numbers. Unlike add_float(),
702
+ this allows very large or precise numbers and optionally no minimum or
703
+ maximum bounds. Values can optionally be rounded to a step size.
704
+ See :class:`~syd.parameters.UnboundedFloatParameter` for details.
705
+
706
+ Parameters
707
+ ----------
708
+ name : str
709
+ Name of the parameter (used as label in GUI)
710
+ value : float
711
+ Initial value
712
+ min_value : float, optional
713
+ Minimum allowed value (or None for no minimum)
714
+ max_value : float, optional
715
+ Maximum allowed value (or None for no maximum)
716
+ step : float, optional
717
+ Size of each increment (or None for no rounding)
718
+
719
+ Examples
720
+ --------
721
+ >>> viewer.add_unbounded_float('wavelength',
722
+ ... value=550e-9, # Nanometers
723
+ ... min_value=0.0,
724
+ ... step=1e-9) # Round to nearest nanometer
725
+ >>> viewer.get_state()['wavelength']
726
+ 5.5e-07
727
+ >>> # Values will be rounded if step is provided
728
+ >>> viewer.update_unbounded_float('wavelength', value=550.7e-9)
729
+ >>> viewer.get_state()['wavelength']
730
+ 5.51e-07
731
+ """
732
+ try:
733
+ new_param = ParameterType.unbounded_float.value(
734
+ name,
735
+ value,
736
+ min_value,
737
+ max_value,
738
+ step,
739
+ )
740
+ except Exception as e:
741
+ raise ParameterAddError(name, "unbounded_float", str(e)) from e
742
+ else:
743
+ self.parameters[name] = new_param
744
+
745
+ @validate_parameter_operation("add", ParameterType.button)
746
+ def add_button(
747
+ self,
748
+ name: str,
749
+ *,
750
+ label: str,
751
+ callback: Callable[[], None],
752
+ ) -> None:
753
+ """
754
+ Add a button parameter to the viewer.
755
+
756
+ Creates a clickable button in the GUI that triggers the provided callback function
757
+ when clicked. The button's display text can be different from its parameter name.
758
+ See :class:`~syd.parameters.ButtonParameter` for details.
759
+
760
+ Parameters
761
+ ----------
762
+ name : str
763
+ Name of the parameter (internal identifier)
764
+ label : str
765
+ Text to display on the button
766
+ callback : callable
767
+ Function to call when the button is clicked (takes no arguments)
768
+
769
+ Examples
770
+ --------
771
+ >>> def reset_plot():
772
+ ... print("Resetting plot...")
773
+ >>> viewer.add_button('reset', label='Reset Plot', callback=reset_plot)
774
+ """
775
+ try:
776
+
777
+ # Wrap the callback to include state as an input argument
778
+ def wrapped_callback(button):
779
+ callback(self.get_state())
780
+
781
+ new_param = ParameterType.button.value(name, label, wrapped_callback)
782
+ except Exception as e:
783
+ raise ParameterAddError(name, "button", str(e)) from e
784
+ else:
785
+ self.parameters[name] = new_param
165
786
 
166
787
  # -------------------- parameter update methods --------------------
167
788
  @validate_parameter_operation("update", ParameterType.text)
168
- def update_text(self, name: str, default: str = "") -> None:
169
- self.parameters[name] = ParameterType.text.value(name, default)
789
+ def update_text(
790
+ self, name: str, *, value: Union[str, _NoUpdate] = _NO_UPDATE
791
+ ) -> None:
792
+ """
793
+ Update a text parameter's value.
794
+
795
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_text`.
796
+ See :class:`~syd.parameters.TextParameter` for details about value validation.
797
+
798
+ Parameters
799
+ ----------
800
+ name : str
801
+ Name of the text parameter to update
802
+ value : str, optional
803
+ New text value (if not provided, no change)
804
+
805
+ Examples
806
+ --------
807
+ >>> viewer.add_text('title', value='Original Title')
808
+ >>> viewer.update_text('title', value='New Title')
809
+ >>> viewer.get_state()['title']
810
+ 'New Title'
811
+ """
812
+ updates = {}
813
+ if value is not _NO_UPDATE:
814
+ updates["value"] = value
815
+ if updates:
816
+ self.parameters[name].update(updates)
817
+
818
+ @validate_parameter_operation("update", ParameterType.boolean)
819
+ def update_boolean(
820
+ self, name: str, *, value: Union[bool, _NoUpdate] = _NO_UPDATE
821
+ ) -> None:
822
+ """
823
+ Update a boolean parameter's value.
824
+
825
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_boolean`.
826
+ See :class:`~syd.parameters.BooleanParameter` for details about value validation.
827
+
828
+ Parameters
829
+ ----------
830
+ name : str
831
+ Name of the boolean parameter to update
832
+ value : bool, optional
833
+ New state (True/False) (if not provided, no change)
834
+
835
+ Examples
836
+ --------
837
+ >>> viewer.add_boolean('show_grid', value=True)
838
+ >>> viewer.update_boolean('show_grid', value=False)
839
+ >>> viewer.get_state()['show_grid']
840
+ False
841
+ """
842
+ updates = {}
843
+ if value is not _NO_UPDATE:
844
+ updates["value"] = value
845
+ if updates:
846
+ self.parameters[name].update(updates)
170
847
 
171
848
  @validate_parameter_operation("update", ParameterType.selection)
172
- def update_selection(self, name: str, options: List[Any], default: Any = None) -> None:
173
- self.parameters[name] = ParameterType.selection.value(name, options, default)
849
+ def update_selection(
850
+ self,
851
+ name: str,
852
+ *,
853
+ value: Union[Any, _NoUpdate] = _NO_UPDATE,
854
+ options: Union[List[Any], _NoUpdate] = _NO_UPDATE,
855
+ ) -> None:
856
+ """
857
+ Update a selection parameter's value and/or options.
858
+
859
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_selection`.
860
+ See :class:`~syd.parameters.SelectionParameter` for details about value validation.
861
+
862
+ Parameters
863
+ ----------
864
+ name : str
865
+ Name of the selection parameter to update
866
+ value : Any, optional
867
+ New selected value (must be in options) (if not provided, no change)
868
+ options : list, optional
869
+ New list of selectable options (if not provided, no change)
870
+
871
+ Examples
872
+ --------
873
+ >>> viewer.add_selection('color', value='red',
874
+ ... options=['red', 'green', 'blue'])
875
+ >>> # Update just the value
876
+ >>> viewer.update_selection('color', value='blue')
877
+ >>> # Update options and value together
878
+ >>> viewer.update_selection('color',
879
+ ... options=['purple', 'orange'],
880
+ ... value='purple')
881
+ """
882
+ updates = {}
883
+ if value is not _NO_UPDATE:
884
+ updates["value"] = value
885
+ if options is not _NO_UPDATE:
886
+ updates["options"] = options
887
+ if updates:
888
+ self.parameters[name].update(updates)
174
889
 
175
890
  @validate_parameter_operation("update", ParameterType.multiple_selection)
176
- def update_multiple_selection(self, name: str, options: List[Any], default: List[Any] = None) -> None:
177
- self.parameters[name] = ParameterType.multiple_selection.value(name, options, default)
891
+ def update_multiple_selection(
892
+ self,
893
+ name: str,
894
+ *,
895
+ value: Union[List[Any], _NoUpdate] = _NO_UPDATE,
896
+ options: Union[List[Any], _NoUpdate] = _NO_UPDATE,
897
+ ) -> None:
898
+ """
899
+ Update a multiple selection parameter's values and/or options.
178
900
 
179
- @validate_parameter_operation("update", ParameterType.boolean)
180
- def update_boolean(self, name: str, default: bool = True) -> None:
181
- self.parameters[name] = ParameterType.boolean.value(name, default)
901
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_multiple_selection`.
902
+ See :class:`~syd.parameters.MultipleSelectionParameter` for details about value validation.
903
+
904
+ Parameters
905
+ ----------
906
+ name : str
907
+ Name of the multiple selection parameter to update
908
+ value : list, optional
909
+ New list of selected values (all must be in options) (if not provided, no change)
910
+ options : list, optional
911
+ New list of selectable options (if not provided, no change)
912
+
913
+ Examples
914
+ --------
915
+ >>> viewer.add_multiple_selection('toppings',
916
+ ... value=['cheese'],
917
+ ... options=['cheese', 'pepperoni', 'mushrooms'])
918
+ >>> # Update selected values
919
+ >>> viewer.update_multiple_selection('toppings',
920
+ ... value=['cheese', 'mushrooms'])
921
+ >>> # Update options (will reset value if current selections not in new options)
922
+ >>> viewer.update_multiple_selection('toppings',
923
+ ... options=['cheese', 'bacon', 'olives'],
924
+ ... value=['cheese', 'bacon'])
925
+ """
926
+ updates = {}
927
+ if value is not _NO_UPDATE:
928
+ updates["value"] = value
929
+ if options is not _NO_UPDATE:
930
+ updates["options"] = options
931
+ if updates:
932
+ self.parameters[name].update(updates)
182
933
 
183
934
  @validate_parameter_operation("update", ParameterType.integer)
184
- def update_integer(self, name: str, min_value: int = None, max_value: int = None, default: int = 0) -> None:
185
- self.parameters[name] = ParameterType.integer.value(name, min_value, max_value, default)
935
+ def update_integer(
936
+ self,
937
+ name: str,
938
+ *,
939
+ value: Union[int, _NoUpdate] = _NO_UPDATE,
940
+ min_value: Union[int, _NoUpdate] = _NO_UPDATE,
941
+ max_value: Union[int, _NoUpdate] = _NO_UPDATE,
942
+ ) -> None:
943
+ """
944
+ Update an integer parameter's value and/or bounds.
945
+
946
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer`.
947
+ See :class:`~syd.parameters.IntegerParameter` for details about value validation.
948
+
949
+ Parameters
950
+ ----------
951
+ name : str
952
+ Name of the integer parameter to update
953
+ value : int, optional
954
+ New value (will be clamped to bounds) (if not provided, no change)
955
+ min_value : int, optional
956
+ New minimum value (if not provided, no change)
957
+ max_value : int, optional
958
+ New maximum value (if not provided, no change)
959
+
960
+ Examples
961
+ --------
962
+ >>> viewer.add_integer('count', value=5, min_value=0, max_value=10)
963
+ >>> # Update just the value
964
+ >>> viewer.update_integer('count', value=8)
965
+ >>> # Update bounds (current value will be clamped if needed)
966
+ >>> viewer.update_integer('count', min_value=7, max_value=15)
967
+ """
968
+ updates = {}
969
+ if value is not _NO_UPDATE:
970
+ updates["value"] = value
971
+ if min_value is not _NO_UPDATE:
972
+ updates["min_value"] = min_value
973
+ if max_value is not _NO_UPDATE:
974
+ updates["max_value"] = max_value
975
+ if updates:
976
+ self.parameters[name].update(updates)
186
977
 
187
978
  @validate_parameter_operation("update", ParameterType.float)
188
- def update_float(self, name: str, min_value: float = None, max_value: float = None, default: float = 0.0, step: float = 0.1) -> None:
189
- self.parameters[name] = ParameterType.float.value(name, min_value, max_value, default, step)
979
+ def update_float(
980
+ self,
981
+ name: str,
982
+ *,
983
+ value: Union[float, _NoUpdate] = _NO_UPDATE,
984
+ min_value: Union[float, _NoUpdate] = _NO_UPDATE,
985
+ max_value: Union[float, _NoUpdate] = _NO_UPDATE,
986
+ step: Union[float, _NoUpdate] = _NO_UPDATE,
987
+ ) -> None:
988
+ """
989
+ Update a float parameter's value, bounds, and/or step size.
990
+
991
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_float`.
992
+ See :class:`~syd.parameters.FloatParameter` for details about value validation.
190
993
 
191
- @validate_parameter_operation("update", ParameterType.integer_pair)
192
- def update_integer_pair(
994
+ Parameters
995
+ ----------
996
+ name : str
997
+ Name of the float parameter to update
998
+ value : float, optional
999
+ New value (will be rounded and clamped) (if not provided, no change)
1000
+ min_value : float, optional
1001
+ New minimum value (if not provided, no change)
1002
+ max_value : float, optional
1003
+ New maximum value (if not provided, no change)
1004
+ step : float, optional
1005
+ New step size (if not provided, no change)
1006
+
1007
+ Examples
1008
+ --------
1009
+ >>> viewer.add_float('temperature', value=20.0,
1010
+ ... min_value=0.0, max_value=100.0, step=0.5)
1011
+ >>> # Update just the value (will round to step)
1012
+ >>> viewer.update_float('temperature', value=20.7) # Becomes 20.5
1013
+ >>> # Update bounds and step size
1014
+ >>> viewer.update_float('temperature',
1015
+ ... min_value=15.0, max_value=30.0, step=0.1)
1016
+ """
1017
+ updates = {}
1018
+ if value is not _NO_UPDATE:
1019
+ updates["value"] = value
1020
+ if min_value is not _NO_UPDATE:
1021
+ updates["min_value"] = min_value
1022
+ if max_value is not _NO_UPDATE:
1023
+ updates["max_value"] = max_value
1024
+ if step is not _NO_UPDATE:
1025
+ updates["step"] = step
1026
+ if updates:
1027
+ self.parameters[name].update(updates)
1028
+
1029
+ @validate_parameter_operation("update", ParameterType.integer_range)
1030
+ def update_integer_range(
193
1031
  self,
194
1032
  name: str,
195
- default: Tuple[int, int],
196
- min_value: int = None,
197
- max_value: int = None,
1033
+ *,
1034
+ value: Union[Tuple[int, int], _NoUpdate] = _NO_UPDATE,
1035
+ min_value: Union[int, _NoUpdate] = _NO_UPDATE,
1036
+ max_value: Union[int, _NoUpdate] = _NO_UPDATE,
198
1037
  ) -> None:
199
- self.parameters[name] = ParameterType.integer_pair.value(name, default, min_value, max_value)
1038
+ """
1039
+ Update an integer range parameter's values and/or bounds.
1040
+
1041
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_integer_range`.
1042
+ See :class:`~syd.parameters.IntegerRangeParameter` for details about value validation.
1043
+
1044
+ Parameters
1045
+ ----------
1046
+ name : str
1047
+ Name of the integer range parameter to update
1048
+ value : tuple[int, int], optional
1049
+ New (low, high) values (will be clamped) (if not provided, no change)
1050
+ min_value : int, optional
1051
+ New minimum value for both low and high (if not provided, no change)
1052
+ max_value : int, optional
1053
+ New maximum value for both low and high (if not provided, no change)
1054
+
1055
+ Examples
1056
+ --------
1057
+ >>> viewer.add_integer_range('age_range',
1058
+ ... value=(25, 35),
1059
+ ... min_value=18, max_value=100)
1060
+ >>> # Update just the range (values will be swapped if needed)
1061
+ >>> viewer.update_integer_range('age_range', value=(40, 30)) # Becomes (30, 40)
1062
+ >>> # Update bounds (current values will be clamped if needed)
1063
+ >>> viewer.update_integer_range('age_range', min_value=20, max_value=80)
1064
+ """
1065
+ updates = {}
1066
+ if value is not _NO_UPDATE:
1067
+ updates["value"] = value
1068
+ if min_value is not _NO_UPDATE:
1069
+ updates["min_value"] = min_value
1070
+ if max_value is not _NO_UPDATE:
1071
+ updates["max_value"] = max_value
1072
+ if updates:
1073
+ self.parameters[name].update(updates)
200
1074
 
201
- @validate_parameter_operation("update", ParameterType.float_pair)
202
- def update_float_pair(
1075
+ @validate_parameter_operation("update", ParameterType.float_range)
1076
+ def update_float_range(
203
1077
  self,
204
1078
  name: str,
205
- default: Tuple[float, float],
206
- min_value: float = None,
207
- max_value: float = None,
208
- step: float = 0.1,
1079
+ *,
1080
+ value: Union[Tuple[float, float], _NoUpdate] = _NO_UPDATE,
1081
+ min_value: Union[float, _NoUpdate] = _NO_UPDATE,
1082
+ max_value: Union[float, _NoUpdate] = _NO_UPDATE,
1083
+ step: Union[float, _NoUpdate] = _NO_UPDATE,
209
1084
  ) -> None:
210
- self.parameters[name] = ParameterType.float_pair.value(name, default, min_value, max_value, step)
1085
+ """
1086
+ Update a float range parameter's values, bounds, and/or step size.
1087
+
1088
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_float_range`.
1089
+ See :class:`~syd.parameters.FloatRangeParameter` for details about value validation.
1090
+
1091
+ Parameters
1092
+ ----------
1093
+ name : str
1094
+ Name of the float range parameter to update
1095
+ value : tuple[float, float], optional
1096
+ New (low, high) values (will be rounded and clamped) (if not provided, no change)
1097
+ min_value : float, optional
1098
+ New minimum value for both low and high (if not provided, no change)
1099
+ max_value : float, optional
1100
+ New maximum value for both low and high (if not provided, no change)
1101
+ step : float, optional
1102
+ New step size for rounding values (if not provided, no change)
1103
+
1104
+ Examples
1105
+ --------
1106
+ >>> viewer.add_float_range('price_range',
1107
+ ... value=(10.0, 20.0),
1108
+ ... min_value=0.0, max_value=100.0, step=0.5)
1109
+ >>> # Update just the range (values will be rounded and swapped if needed)
1110
+ >>> viewer.update_float_range('price_range', value=(15.7, 14.2)) # Becomes (14.0, 15.5)
1111
+ >>> # Update bounds and step size
1112
+ >>> viewer.update_float_range('price_range',
1113
+ ... min_value=5.0, max_value=50.0, step=0.1)
1114
+ """
1115
+ updates = {}
1116
+ if value is not _NO_UPDATE:
1117
+ updates["value"] = value
1118
+ if min_value is not _NO_UPDATE:
1119
+ updates["min_value"] = min_value
1120
+ if max_value is not _NO_UPDATE:
1121
+ updates["max_value"] = max_value
1122
+ if step is not _NO_UPDATE:
1123
+ updates["step"] = step
1124
+ if updates:
1125
+ self.parameters[name].update(updates)
1126
+
1127
+ @validate_parameter_operation("update", ParameterType.unbounded_integer)
1128
+ def update_unbounded_integer(
1129
+ self,
1130
+ name: str,
1131
+ *,
1132
+ value: Union[int, _NoUpdate] = _NO_UPDATE,
1133
+ min_value: Union[Optional[int], _NoUpdate] = _NO_UPDATE,
1134
+ max_value: Union[Optional[int], _NoUpdate] = _NO_UPDATE,
1135
+ ) -> None:
1136
+ """
1137
+ Update an unbounded integer parameter's value and/or bounds.
1138
+
1139
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_integer`.
1140
+ See :class:`~syd.parameters.UnboundedIntegerParameter` for details about value validation.
1141
+
1142
+ Parameters
1143
+ ----------
1144
+ name : str
1145
+ Name of the unbounded integer parameter to update
1146
+ value : int, optional
1147
+ New value (will be clamped to any bounds) (if not provided, no change)
1148
+ min_value : int or None, optional
1149
+ New minimum value, or None for no minimum (if not provided, no change)
1150
+ max_value : int or None, optional
1151
+ New maximum value, or None for no maximum (if not provided, no change)
1152
+
1153
+ Examples
1154
+ --------
1155
+ >>> viewer.add_unbounded_integer('population',
1156
+ ... value=1000000,
1157
+ ... min_value=0) # No maximum
1158
+ >>> # Update just the value
1159
+ >>> viewer.update_unbounded_integer('population', value=2000000)
1160
+ >>> # Add a maximum bound (current value will be clamped if needed)
1161
+ >>> viewer.update_unbounded_integer('population', max_value=1500000)
1162
+ >>> # Remove the minimum bound
1163
+ >>> viewer.update_unbounded_integer('population', min_value=None)
1164
+ """
1165
+ updates = {}
1166
+ if value is not _NO_UPDATE:
1167
+ updates["value"] = value
1168
+ if min_value is not _NO_UPDATE:
1169
+ updates["min_value"] = min_value
1170
+ if max_value is not _NO_UPDATE:
1171
+ updates["max_value"] = max_value
1172
+ if updates:
1173
+ self.parameters[name].update(updates)
1174
+
1175
+ @validate_parameter_operation("update", ParameterType.unbounded_float)
1176
+ def update_unbounded_float(
1177
+ self,
1178
+ name: str,
1179
+ *,
1180
+ value: Union[float, _NoUpdate] = _NO_UPDATE,
1181
+ min_value: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1182
+ max_value: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1183
+ step: Union[Optional[float], _NoUpdate] = _NO_UPDATE,
1184
+ ) -> None:
1185
+ """
1186
+ Update an unbounded float parameter's value, bounds, and/or step size.
1187
+
1188
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_unbounded_float`.
1189
+ See :class:`~syd.parameters.UnboundedFloatParameter` for details about value validation.
1190
+
1191
+ Parameters
1192
+ ----------
1193
+ name : str
1194
+ Name of the unbounded float parameter to update
1195
+ value : float, optional
1196
+ New value (will be rounded if step is set) (if not provided, no change)
1197
+ min_value : float or None, optional
1198
+ New minimum value, or None for no minimum (if not provided, no change)
1199
+ max_value : float or None, optional
1200
+ New maximum value, or None for no maximum (if not provided, no change)
1201
+ step : float or None, optional
1202
+ New step size for rounding, or None for no rounding (if not provided, no change)
1203
+
1204
+ Examples
1205
+ --------
1206
+ >>> viewer.add_unbounded_float('wavelength',
1207
+ ... value=550e-9, # Nanometers
1208
+ ... min_value=0.0,
1209
+ ... step=1e-9)
1210
+ >>> # Update value (will be rounded if step is set)
1211
+ >>> viewer.update_unbounded_float('wavelength', value=632.8e-9) # HeNe laser
1212
+ >>> # Change step size and add maximum
1213
+ >>> viewer.update_unbounded_float('wavelength',
1214
+ ... step=0.1e-9, # Finer control
1215
+ ... max_value=1000e-9) # Infrared limit
1216
+ >>> # Remove step size (allow any precision)
1217
+ >>> viewer.update_unbounded_float('wavelength', step=None)
1218
+ """
1219
+ updates = {}
1220
+ if value is not _NO_UPDATE:
1221
+ updates["value"] = value
1222
+ if min_value is not _NO_UPDATE:
1223
+ updates["min_value"] = min_value
1224
+ if max_value is not _NO_UPDATE:
1225
+ updates["max_value"] = max_value
1226
+ if step is not _NO_UPDATE:
1227
+ updates["step"] = step
1228
+ if updates:
1229
+ self.parameters[name].update(updates)
1230
+
1231
+ @validate_parameter_operation("update", ParameterType.button)
1232
+ def update_button(
1233
+ self,
1234
+ name: str,
1235
+ *,
1236
+ label: Union[str, _NoUpdate] = _NO_UPDATE,
1237
+ callback: Union[Callable[[], None], _NoUpdate] = _NO_UPDATE,
1238
+ ) -> None:
1239
+ """
1240
+ Update a button parameter's label and/or callback function.
1241
+
1242
+ Updates a parameter created by :meth:`~syd.interactive_viewer.InteractiveViewer.add_button`.
1243
+ See :class:`~syd.parameters.ButtonParameter` for details.
1244
+
1245
+ Parameters
1246
+ ----------
1247
+ name : str
1248
+ Name of the button parameter to update
1249
+ label : str, optional
1250
+ New text to display on the button (if not provided, no change)
1251
+ callback : callable, optional
1252
+ New function to call when clicked (if not provided, no change)
1253
+
1254
+ Examples
1255
+ --------
1256
+ >>> def new_callback():
1257
+ ... print("New action...")
1258
+ >>> viewer.update_button('reset',
1259
+ ... label='Clear Plot',
1260
+ ... callback=new_callback)
1261
+ """
1262
+ updates = {}
1263
+ if label is not _NO_UPDATE:
1264
+ updates["label"] = label
1265
+ if callback is not _NO_UPDATE:
1266
+ updates["callback"] = callback
1267
+ if updates:
1268
+ self.parameters[name].update(updates)