from typing import List, Any, Callable, Dict, Tuple, Union, Optional, Literal
from functools import wraps, partial
import inspect
from contextlib import contextmanager
from matplotlib.figure import Figure
from .parameters import ParameterType, ActionType, Parameter
from .support import NoUpdate, NoInitialValue, ParameterAddError, ParameterUpdateError
# Create the singleton instances
NO_UPDATE = NoUpdate()
NO_INITIAL_VALUE = NoInitialValue()
[docs]
def make_viewer(plot_func: Optional[Callable] = None) -> "Viewer":
"""Create an empty viewer object.
Parameters
----------
plot_func : Callable, optional
A function that takes a state dictionary and returns a matplotlib figure.
Returns
-------
viewer : Viewer
A new viewer object
Examples
--------
>>> from syd import make_viewer
>>> def plot(state):
>>> ... generate figure, plot stuff ...
>>> return fig
>>> viewer = make_viewer(plot)
>>> viewer.add_float('x', value=1.0, min=0, max=10)
>>> viewer.on_change('x', viewer.update_based_on_x)
>>> viewer.show()
"""
viewer = Viewer()
if plot_func is not None:
viewer.set_plot(plot_func)
return viewer
def validate_parameter_operation(
operation: str,
parameter_type: Union[ParameterType, ActionType],
) -> Callable:
"""
Decorator that validates parameter operations for the viewer class.
This decorator ensures that:
1. The operation type matches the method name (add/update)
2. The parameter type matches the method's intended parameter type
3. Parameters can only be added when the app is not deployed
4. For updates, validates that the parameter exists and is of the correct type
Args:
operation (str): The type of operation to validate. Must be either 'add' or 'update'.
parameter_type (ParameterType): The expected parameter type from the ParameterType enum.
Returns:
Callable: A decorated function that includes parameter validation.
Raises:
ValueError: If the operation type doesn't match the method name or if updating a non-existent parameter
TypeError: If updating a parameter with an incorrect type
RuntimeError: If adding parameters while deployed
Example:
@validate_parameter_operation('add', ParameterType.text)
def add_text(self, name: str, default: str = "") -> None:
...
"""
def decorator(func: Callable) -> Callable:
if operation not in ["add", "update"]:
raise ValueError(
"Incorrect use of validate_parameter_operation decorator. Must be called with 'add' or 'update' as the first argument."
)
# Validate operation matches method name (add/update)
if not func.__name__.startswith(operation):
raise ValueError(
f"Invalid operation type specified ({operation}) for method {func.__name__}"
)
@wraps(func)
def wrapper(self: "Viewer", name: Any, *args, **kwargs):
# Validate parameter name is a string
if not isinstance(name, str):
if operation == "add":
raise ParameterAddError(
name, parameter_type.name, "Parameter name must be a string"
)
elif operation == "update":
raise ParameterUpdateError(
name, parameter_type.name, "Parameter name must be a string"
)
# Validate deployment state
if operation == "add" and self._app_deployed:
raise RuntimeError(
"The app is currently deployed, cannot add a new parameter right now."
)
if operation == "add":
if name in self.parameters:
raise ParameterAddError(
name, parameter_type.name, "Parameter already exists!"
)
# For updates, validate parameter existence and type
if operation == "update":
if name not in self.parameters:
raise ParameterUpdateError(
name,
parameter_type.name,
"Parameter not found - you can only update registered parameters!",
)
if not isinstance(self.parameters[name], parameter_type.value):
msg = f"Parameter called {name} was found but is registered as a different parameter type ({type(self.parameters[name])}). Expecting {parameter_type.value}."
raise ParameterUpdateError(name, parameter_type.name, msg)
return func(self, name, *args, **kwargs)
return wrapper
return decorator
[docs]
class Viewer:
"""
Base class for creating interactive matplotlib figures with GUI controls.
This class helps you create interactive visualizations by adding GUI elements
(like sliders, dropdowns, etc.) that update your plot in real-time. To use it:
1. Create a subclass and implement the plot() method
2. Add parameters using add_* methods before deploying
3. Use on_change() to make parameters update the plot
4. Use update_* methods to update parameter values and properties
5. Deploy the app to show the interactive figure
Examples
--------
>>> class MyViewer(Viewer):
... def plot(self, state: Dict[str, Any]):
... fig = plt.figure()
... plt.plot([0, state['x']])
... return fig
...
... def update_based_on_x(self, state: Dict[str, Any]):
... self.update_float('x', value=state['x'])
...
>>> viewer = MyViewer()
>>> viewer.add_float('x', value=1.0, min=0, max=10)
>>> viewer.on_change('x', viewer.update_based_on_x)
>>> viewer.show()
"""
parameters: Dict[str, Parameter]
callbacks: Dict[str, List[Callable]]
_app_deployed: bool
_in_callbacks: bool
_figure: Figure
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
instance.parameters = {}
instance.callbacks = {}
instance._app_deployed = False
instance._in_callbacks = False
instance._figure = None
return instance
@property
def state(self) -> Dict[str, Any]:
"""
Get the current values of all parameters.
Returns
-------
dict
Dictionary mapping parameter names to their current values
Examples
--------
>>> viewer.add_float('x', value=1.0, min=0, max=10)
>>> viewer.add_text('label', value='data')
>>> viewer.state
{'x': 1.0, 'label': 'data'}
"""
return {
name: param.value
for name, param in self.parameters.items()
if not param._is_action
}
@property
def figure(self) -> Figure:
"""
Get the last opened figure. Returns None if no figure has been opened yet.
"""
return self._figure
[docs]
def plot(self, state: Dict[str, Any]) -> Figure:
"""Create and return a matplotlib figure.
Hello user! This is a placeholder that raises a NotImplementedError. You must either:
1. Call set_plot() with your plotting function
This will look like this:
>>> def plot(state):
>>> ... generate figure, plot stuff ...
>>> return fig
>>> viewer.set_plot(plot))
2. Subclass Viewer and override this method
This will look like this:
>>> class YourViewer(Viewer):
>>> def plot(self, state):
>>> ... generate figure, plot stuff ...
>>> return fig
Parameters
----------
state : dict
Current parameter values
Returns
-------
matplotlib.figure.Figure
The figure to display
Notes
-----
- Create a new figure each time, don't reuse old ones
- Access parameter values using state['param_name']
- Access your viewer class using self (or viewer for the set_plot() method)
- Return the figure object, don't call plt.show()!
"""
raise NotImplementedError(
"Plot method not implemented. Either subclass "
"Viewer and override plot(), or use "
"set_plot() to provide a plotting function."
)
[docs]
def set_plot(self, func: Callable) -> None:
"""Set the plot method for the viewer.
For viewers created with make_viewer(), this function is used to set the plot method.
The input must be a callable function that takes a state dictionary and returns a matplotlib figure.
Examples
--------
>>> def plot(state):
>>> ... generate figure, plot stuff ...
>>> return fig
>>> viewer = make_viewer()
>>> viewer.set_plot(plot)
"""
self.plot = self._prepare_function(func, context="Setting plot:")
[docs]
def show(
self,
controls_position: Literal["left", "top", "right", "bottom"] = "left",
controls_width_percent: int = 20,
suppress_warnings: bool = True,
update_threshold: float = 1.0,
):
"""
Show the viewer locally in a notebook.
This method displays the viewer in a Jupyter notebook environment with interactive controls.
Parameters
----------
controls_position : {'left', 'top', 'right', 'bottom'}, optional
Position of the controls relative to the plot (default is 'left').
controls_width_percent : int, optional
Width of the controls as a percentage of the total viewer width (default is 20).
suppress_warnings : bool, optional
If True, suppress warnings during deployment (default is True).
update_threshold : float, optional
Minimum time in seconds between updates to the viewer (default is 1.0).
Notes
-----
This method is equivalent to calling `deploy(env="notebook")` but does not return the viewer object.
"""
_ = self.deploy(
env="notebook",
controls_position=controls_position,
controls_width_percent=controls_width_percent,
suppress_warnings=suppress_warnings,
update_threshold=update_threshold,
)
[docs]
def share(
self,
controls_position: str = "left",
fig_dpi: int = 300,
controls_width_percent: int = 20,
plot_margin_percent: float = 2.5,
suppress_warnings: bool = True,
debug: bool = False,
host: Optional[str] = None,
port: Optional[int] = None,
open_browser: bool = True,
update_threshold: float = 1.0,
timeout_threshold: float = 10.0,
):
"""
Share the viewer on a web browser using Flask.
Parameters
----------
controls_position : str, optional
Position of the controls relative to the plot (default is 'left').
fig_dpi : int, optional
Dots per inch for the figure resolution (default is 300).
controls_width_percent : int, optional
Width of the controls as a percentage of the total viewer width (default is 20).
plot_margin_percent : float, optional
Margin around the plot as a percentage of the plot size (default is 2.5).
suppress_warnings : bool, optional
If True, suppress warnings during deployment (default is True).
debug : bool, optional
If True, run the server in debug mode (default is False).
host : str, optional
Hostname to use for the server (default is None, which uses 'localhost').
port : int, optional
Port number to use for the server (default is None, which selects the first available port).
open_browser : bool, optional
If True, automatically open the web browser to the viewer (default is True).
update_threshold : float, optional
Minimum time in seconds between updates to the viewer (default is 1.0).
timeout_threshold : float, optional
Maximum time in seconds to wait for a response before timing out (default is 10.0).
Notes
-----
This method is equivalent to calling `deploy(env="browser")` but does not return the viewer object.
"""
_ = self.deploy(
env="browser",
controls_position=controls_position,
fig_dpi=fig_dpi,
controls_width_percent=controls_width_percent,
plot_margin_percent=plot_margin_percent,
suppress_warnings=suppress_warnings,
debug=debug,
host=host,
port=port,
open_browser=open_browser,
update_threshold=update_threshold,
timeout_threshold=timeout_threshold,
)
[docs]
def deploy(self, env: str = "notebook", **kwargs):
"""Deploy the app in a notebook or standalone environment"""
env = env.lower()
if env == "notebook":
# On demand import because the deployers need to import the viewer
from .notebook_deployment.deployer import NotebookDeployer
deployer = NotebookDeployer(self, **kwargs)
deployer.deploy()
return self
elif env == "browser":
# On demand import because the deployers need to import the viewer
from .flask_deployment.deployer import FlaskDeployer
if "port" not in kwargs:
kwargs["port"] = None
deployer = FlaskDeployer(self, **kwargs)
deployer.deploy()
return self
else:
raise ValueError(
f"Unsupported environment: {env}, only 'notebook', 'browser' are supported right now."
)
@contextmanager
def _deploy_app(self):
"""Internal context manager to control app deployment state"""
self._app_deployed = True
try:
yield
finally:
self._app_deployed = False
def _prepare_function(
self,
func: Callable,
context: Optional[str] = "",
) -> Callable:
# Check if func is Callable
if not callable(func):
raise ValueError(f"Function {func} is not callable")
# Handle partial functions
if isinstance(func, partial):
get_self = (
lambda func: hasattr(func.func, "__self__") and func.func.__self__
)
get_name = lambda func: func.func.__name__
else:
get_self = lambda func: hasattr(func, "__self__") and func.__self__
get_name = lambda func: func.__name__
# Get function signature
try:
params = list(inspect.signature(func).parameters.values())
except ValueError:
# Handle built-ins or other objects without signatures
raise ValueError(context + f"Cannot inspect function signature for {func}")
# Look through params and check if there are two positional parameters (including self for bound methods)
bound_method = get_self(func) is self
positional_params = 0
required_kwargs = 0
optional_part = ""
for param in params:
# Check if it's a positional parameter. If it is, count it.
# We need at least 1 positional parameter. When we already have 1,
# we need to make sure any other positional parameters have defaults.
if param.kind in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.POSITIONAL_ONLY,
):
if positional_params < 1:
positional_params += 1
else:
if param.default == inspect.Parameter.empty:
positional_params += 1
else:
optional_part += f", {param.name}={param.default!r}"
elif param.kind == inspect.Parameter.VAR_KEYWORD:
optional_part += f", **{param.name}"
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
optional_part += (
f", {param.name}={param.default!r}"
if param.default != inspect.Parameter.empty
else f""
)
if param.default == inspect.Parameter.empty:
required_kwargs += 1
if positional_params != 1 or required_kwargs != 0:
func_name = get_name(func)
if isinstance(func, partial):
func_sig = str(inspect.signature(func))
if bound_method:
func_sig = "(" + "self, " + func_sig[1:]
msg = (
context
+ "\n"
+ f"Your partial function '{func_name}' has an incorrect signature.\n"
"Partial functions must have exactly one positional parameter\n"
"which corresponds to a dictionary of the current state of the viewer.\n"
"\nYour partial function effectivelylooks like this:\n"
f"def {func_name}{func_sig}:\n"
" ... your function code ..."
)
raise ValueError(msg)
if bound_method:
original_method = getattr(get_self(func).__class__, get_name(func))
func_sig = str(inspect.signature(original_method))
msg = (
context + "\n"
f"Your bound method '{func_name}{func_sig}' has an incorrect signature.\n"
"Bound methods must have exactly one positional parameter in addition to self.\n"
"The first parameter should be self (required for bound methods).\n"
"The second parameter should be state -- a dictionary of the current state of the viewer.\n"
"\nYour method looks like this:\n"
"class YourViewer(Viewer):\n"
f" def {func_name}{func_sig}:\n"
" ... your function code ...\n"
"\nIt should look like this:\n"
"class YourViewer(Viewer):\n"
f" def {func_name}(self, state{optional_part}):\n"
" ... your function code ..."
)
raise ValueError(msg)
else:
func_sig = str(inspect.signature(func))
bound_elsewhere = get_self(func) and get_self(func) is not self
if bound_elsewhere:
func_name = f"self.{func_name}"
func_sig = f"(self, {func_sig[1:]})"
add_self = True
else:
add_self = False
msg = (
context + "\n"
f"Your function '{func_name}{func_sig}' has an incorrect signature.\n"
"Functions must have exactly one positional parameter\n"
"which corresponds to a dictionary of the current state of the viewer.\n"
"\nYour function looks like this:\n"
f"def {func_name}{func_sig}:\n"
" ... your function code ...\n"
"\nIt should look like this:\n"
f"def {func_name}({'self, ' if add_self else ''}state{optional_part}):\n"
" ... your function code ..."
)
raise ValueError(msg)
# If we've made it here, the function has exactly one required positional parameter
# which means it's callable by the viewer.
return func
[docs]
def on_change(self, parameter_name: Union[str, List[str]], callback: Callable):
"""
Register a function to run when parameters change.
The callback function will receive a dictionary of all current parameter
values whenever any of the specified parameters change.
Parameters
----------
parameter_name : str or list of str
Name(s) of parameters to watch for changes
callback : callable
Function to call when changes occur. Should accept a single dict argument
containing the current state.
Examples
--------
>>> def update_plot(state):
... print(f"x changed to {state['x']}")
>>> viewer.on_change('x', update_plot)
>>> viewer.on_change(['x', 'y'], lambda s: viewer.plot()) # Update on either change
"""
if isinstance(parameter_name, str):
parameter_name = [parameter_name]
callback = self._prepare_function(
callback,
context="Setting on_change callback:",
)
for param_name in parameter_name:
if param_name not in self.parameters:
raise ValueError(f"Parameter '{param_name}' is not registered!")
if param_name not in self.callbacks:
self.callbacks[param_name] = []
self.callbacks[param_name].append(callback)
[docs]
def set_parameter_value(self, name: str, value: Any) -> None:
"""
Update a parameter's value and trigger any callbacks.
This is a lower-level method - usually you'll want to use the update_*
methods instead (e.g., update_float, update_text, etc.).
Parameters
----------
name : str
Name of the parameter to update
value : Any
New value for the parameter
Raises
------
ValueError
If the parameter doesn't exist or the value is invalid
"""
if name not in self.parameters:
raise ValueError(f"Parameter {name} not found")
# Update the parameter value
self.parameters[name].value = value
# Perform callbacks
self.perform_callbacks(name)
# -------------------- parameter registration methods --------------------
[docs]
def remove_parameter(self, name: str) -> None:
"""
Remove a parameter from the viewer.
Parameters
----------
name : str
Name of the parameter to remove
"""
if name in self.parameters:
del self.parameters[name]
[docs]
@validate_parameter_operation("add", ParameterType.text)
def add_text(
self,
name: str,
*,
value: Union[str, NoInitialValue] = NO_INITIAL_VALUE,
) -> None:
"""
Add a text input parameter to the viewer.
Creates a text box in the GUI that accepts any string input.
See :class:`~syd.parameters.TextParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI)
value : Union[str, NoInitialValue]
Initial text value
If not provided, the parameter will be empty.
Examples
--------
>>> viewer.add_text('title', value='My Plot')
>>> viewer.state['title']
'My Plot'
"""
try:
new_param = ParameterType.text.value(name, value)
except Exception as e:
raise ParameterAddError(name, "text", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.boolean)
def add_boolean(
self,
name: str,
*,
value: Union[bool, NoInitialValue] = NO_INITIAL_VALUE,
) -> None:
"""
Add a boolean parameter to the viewer.
Creates a checkbox in the GUI that can be toggled on/off.
See :class:`~syd.parameters.BooleanParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI)
value : Union[bool, NoInitialValue]
Initial state (True=checked, False=unchecked)
If not provided, the parameter will be checked.
Examples
--------
>>> viewer.add_boolean('show_grid', value=True)
>>> viewer.state['show_grid']
True
"""
try:
new_param = ParameterType.boolean.value(name, value)
except Exception as e:
raise ParameterAddError(name, "boolean", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.selection)
def add_selection(
self,
name: str,
*,
value: Union[Any, NoInitialValue] = NO_INITIAL_VALUE,
options: List[Any],
) -> None:
"""
Add a single-selection parameter to the viewer.
Creates a dropdown menu in the GUI where users can select one option.
See :class:`~syd.parameters.SelectionParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI)
value : Any
Initially selected value (must be one of the options)
options : list
List of values that can be selected
Examples
--------
>>> viewer.add_selection('color', value='red',
... options=['red', 'green', 'blue'])
>>> viewer.state['color']
'red'
"""
try:
new_param = ParameterType.selection.value(name, value, options)
except Exception as e:
raise ParameterAddError(name, "selection", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.multiple_selection)
def add_multiple_selection(
self,
name: str,
*,
value: Union[List[Any], NoInitialValue] = NO_INITIAL_VALUE,
options: List[Any],
) -> None:
"""
Add a multiple-selection parameter to the viewer.
Creates a set of checkboxes or a multi-select dropdown in the GUI where
users can select any number of options.
See :class:`~syd.parameters.MultipleSelectionParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI)
value : Union[list, NoInitialValue]
Initially selected values (must all be in options)
If not provided, the parameter will be empty.
options : list
List of values that can be selected
Examples
--------
>>> viewer.add_multiple_selection('toppings',
... value=['cheese'],
... options=['cheese', 'pepperoni', 'mushrooms'])
>>> viewer.state['toppings']
['cheese']
"""
try:
new_param = ParameterType.multiple_selection.value(name, value, options)
except Exception as e:
raise ParameterAddError(name, "multiple_selection", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.integer)
def add_integer(
self,
name: str,
*,
value: Union[Union[float, int], NoInitialValue] = NO_INITIAL_VALUE,
min: Union[float, int],
max: Union[float, int],
) -> None:
"""
Add an integer parameter to the viewer.
Creates a slider to select whole numbers between a minimum and maximum.
See :class:`~syd.parameters.IntegerParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI and internal identifier)
value : Union[int, NoInitialValue]
Initial value (default position of the slider)
If not provided, the parameter will be set to the minimum value.
min : int
Minimum allowed value
max : int
Maximum allowed value
Examples
--------
>>> viewer.add_integer('age', value=25, min=0, max=120)
>>> viewer.add_integer('year', value=2023, min=1900, max=2100)
"""
try:
new_param = ParameterType.integer.value(name, value, min, max)
except Exception as e:
raise ParameterAddError(name, "integer", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.float)
def add_float(
self,
name: str,
*,
value: Union[Union[float, int], NoInitialValue] = NO_INITIAL_VALUE,
min: Union[float, int],
max: Union[float, int],
step: float = 0.01,
) -> None:
"""
Add a float parameter to the viewer.
Creates a slider to select decimal numbers between a minimum and maximum.
See :class:`~syd.parameters.FloatParameter` for details.
Parameters
----------
name : str
Name of the parameter (internal identifier)
value : Union[float, NoInitialValue]
Initial value (default position of the slider)
If not provided, the parameter will be set to the minimum value.
min : float
Minimum allowed value
max : float
Maximum allowed value
step : float, optional
Step size for the slider (default: 0.01)
Examples
--------
>>> viewer.add_float('temperature', value=98.6, min=95.0, max=105.0, step=0.1)
>>> viewer.add_float('price', value=9.99, min=0.0, max=100.0, step=0.01)
"""
try:
new_param = ParameterType.float.value(name, value, min, max, step)
except Exception as e:
raise ParameterAddError(name, "float", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.integer_range)
def add_integer_range(
self,
name: str,
*,
value: Union[
Tuple[Union[float, int], Union[float, int]], NoInitialValue
] = NO_INITIAL_VALUE,
min: Union[float, int],
max: Union[float, int],
) -> None:
"""
Add an integer range parameter to the viewer.
Creates a range slider to select a range of whole numbers between bounds.
See :class:`~syd.parameters.IntegerRangeParameter` for details.
Parameters
----------
name : str
Name of the parameter (internal identifier)
value : Union[tuple[int, int], NoInitialValue]
Initial (low, high) values for the range
If not provided, the parameter will be set to the full range.
min : int
Minimum allowed value for the range
max : int
Maximum allowed value for the range
Examples
--------
>>> viewer.add_integer_range('age_range', value=(25, 45), min=18, max=100)
>>> viewer.add_integer_range('year_range', value=(2000, 2020), min=1900, max=2100)
"""
try:
new_param = ParameterType.integer_range.value(name, value, min, max)
except Exception as e:
raise ParameterAddError(name, "integer_range", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.float_range)
def add_float_range(
self,
name: str,
*,
value: Union[
Tuple[Union[float, int], Union[float, int]], NoInitialValue
] = NO_INITIAL_VALUE,
min: Union[float, int],
max: Union[float, int],
step: float = 0.01,
) -> None:
"""
Add a float range parameter to the viewer.
Creates a range slider to select a range of decimal numbers between bounds.
See :class:`~syd.parameters.FloatRangeParameter` for details.
Parameters
----------
name : str
Name of the parameter (internal identifier)
value : Union[tuple[float, float], NoInitialValue]
Initial (low, high) values for the range
If not provided, the parameter will be set to the full range.
min : float
Minimum allowed value for the range
max : float
Maximum allowed value for the range
step : float, optional
Step size for the slider (default: 0.01)
Examples
--------
>>> viewer.add_float_range('temp_range', value=(97.0, 99.0), min=95.0, max=105.0, step=0.1)
>>> viewer.add_float_range('price_range', value=(10.0, 50.0), min=0.0, max=100.0, step=0.01)
"""
try:
new_param = ParameterType.float_range.value(name, value, min, max, step)
except Exception as e:
raise ParameterAddError(name, "float_range", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.unbounded_integer)
def add_unbounded_integer(
self,
name: str,
*,
value: Union[Union[float, int], NoInitialValue] = NO_INITIAL_VALUE,
) -> None:
"""
Add an unbounded integer parameter to the viewer.
Creates a text input box in the GUI for entering whole numbers. Unlike
add_integer(), this allows very large numbers without bounds.
See :class:`~syd.parameters.UnboundedIntegerParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI)
value : Union[int, NoInitialValue]
Initial value
If not provided, the parameter will be set to 0.
Examples
--------
>>> viewer.add_unbounded_integer('population', value=1000000)
>>> viewer.state['population']
1000000
"""
try:
new_param = ParameterType.unbounded_integer.value(name, value)
except Exception as e:
raise ParameterAddError(name, "unbounded_integer", str(e)) from e
else:
self.parameters[name] = new_param
[docs]
@validate_parameter_operation("add", ParameterType.unbounded_float)
def add_unbounded_float(
self,
name: str,
*,
value: Union[Union[float, int], NoInitialValue] = NO_INITIAL_VALUE,
step: Optional[float] = None,
) -> None:
"""
Add an unbounded decimal number parameter to the viewer.
Creates a text input box in the GUI for entering numbers. Unlike add_float(),
this allows very large or precise numbers without bounds. Values can optionally
be rounded to a step size.
See :class:`~syd.parameters.UnboundedFloatParameter` for details.
Parameters
----------
name : str
Name of the parameter (used as label in GUI)
value : Union[float, NoInitialValue]
Initial value
If not provided, the parameter will be set to 0.
step : float, optional
Size of each increment (or None for no rounding)
Examples
--------
>>> viewer.add_unbounded_float('wavelength', value=550e-9, step=1e-9)
>>> viewer.state['wavelength']
5.5e-07
>>> # Values will be rounded if step is provided
>>> viewer.update_unbounded_float('wavelength', value=550.7e-9)
>>> viewer.state['wavelength']
5.51e-07
"""
try:
new_param = ParameterType.unbounded_float.value(name, value, step)
except Exception as e:
raise ParameterAddError(name, "unbounded_float", str(e)) from e
else:
self.parameters[name] = new_param
# -------------------- parameter update methods --------------------
[docs]
@validate_parameter_operation("update", ParameterType.text)
def update_text(
self, name: str, *, value: Union[str, NoUpdate] = NO_UPDATE
) -> None:
"""
Update a text parameter's value.
Updates a parameter created by :meth:`~syd.viewer.Viewer.add_text`.
See :class:`~syd.parameters.TextParameter` for details about value validation.
Parameters
----------
name : str
Name of the text parameter to update
value : Union[str, NoUpdate], optional
New text value (if not provided, no change)
Examples
--------
>>> viewer.add_text('title', value='Original Title')
>>> viewer.update_text('title', value='New Title')
>>> viewer.state['title']
'New Title'
"""
updates = {}
if not value == NO_UPDATE:
updates["value"] = value
if updates:
self.parameters[name].update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.boolean)
def update_boolean(
self, name: str, *, value: Union[bool, NoUpdate] = NO_UPDATE
) -> None:
"""
Update a boolean parameter's value.
Updates a parameter created by :meth:`~syd.viewer.Viewer.add_boolean`.
See :class:`~syd.parameters.BooleanParameter` for details about value validation.
Parameters
----------
name : str
Name of the boolean parameter to update
value : Union[bool, NoUpdate], optional
New state (True/False) (if not provided, no change)
Examples
--------
>>> viewer.add_boolean('show_grid', value=True)
>>> viewer.update_boolean('show_grid', value=False)
>>> viewer.state['show_grid']
False
"""
updates = {}
if not value == NO_UPDATE:
updates["value"] = value
if updates:
self.parameters[name].update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.selection)
def update_selection(
self,
name: str,
*,
value: Union[Any, NoUpdate] = NO_UPDATE,
options: Union[List[Any], NoUpdate] = NO_UPDATE,
) -> None:
"""
Update a selection parameter's value and/or options.
Updates a parameter created by :meth:`~syd.viewer.Viewer.add_selection`.
See :class:`~syd.parameters.SelectionParameter` for details about value validation.
Parameters
----------
name : str
Name of the selection parameter to update
value : Union[Any, NoUpdate], optional
New selected value (must be in options) (if not provided, no change)
options : Union[list, NoUpdate], optional
New list of selectable options (if not provided, no change)
Examples
--------
>>> viewer.add_selection('color', value='red',
... options=['red', 'green', 'blue'])
>>> # Update just the value
>>> viewer.update_selection('color', value='blue')
>>> # Update options and value together
>>> viewer.update_selection('color',
... options=['purple', 'orange'],
... value='purple')
"""
updates = {}
if not value == NO_UPDATE:
updates["value"] = value
if not options == NO_UPDATE:
updates["options"] = options
if updates:
self.parameters[name].update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.multiple_selection)
def update_multiple_selection(
self,
name: str,
*,
value: Union[List[Any], NoUpdate] = NO_UPDATE,
options: Union[List[Any], NoUpdate] = NO_UPDATE,
) -> None:
"""
Update a multiple selection parameter's values and/or options.
Updates a parameter created by :meth:`~syd.viewer.Viewer.add_multiple_selection`.
See :class:`~syd.parameters.MultipleSelectionParameter` for details about value validation.
Parameters
----------
name : str
Name of the multiple selection parameter to update
value : Union[list, NoUpdate], optional
New list of selected values (all must be in options) (if not provided, no change)
options : Union[list, NoUpdate], optional
New list of selectable options (if not provided, no change)
Examples
--------
>>> viewer.add_multiple_selection('toppings',
... value=['cheese'],
... options=['cheese', 'pepperoni', 'mushrooms'])
>>> # Update selected values
>>> viewer.update_multiple_selection('toppings',
... value=['cheese', 'mushrooms'])
>>> # Update options (will reset value if current selections not in new options)
>>> viewer.update_multiple_selection('toppings',
... options=['cheese', 'bacon', 'olives'],
... value=['cheese', 'bacon'])
"""
updates = {}
if not value == NO_UPDATE:
updates["value"] = value
if not options == NO_UPDATE:
updates["options"] = options
if updates:
self.parameters[name].update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.integer)
def update_integer(
self,
name: str,
*,
value: Union[int, NoUpdate] = NO_UPDATE,
min: Union[int, NoUpdate] = NO_UPDATE,
max: Union[int, NoUpdate] = NO_UPDATE,
) -> None:
"""
Update an integer parameter.
Change the value or bounds of an existing integer parameter.
See :class:`~syd.parameters.IntegerParameter` for details.
Parameters
----------
name : str
Name of the parameter to update
value : Union[int, NoUpdate], optional
New value
min : Union[int, NoUpdate], optional
New minimum allowed value
max : Union[int, NoUpdate], optional
New maximum allowed value
Examples
--------
>>> viewer.update_integer('age', value=30) # Update just the value
>>> viewer.update_integer('year', min=2000, max=2023) # Update just the bounds
"""
updates = {}
if not isinstance(value, NoUpdate):
updates["value"] = int(value)
if not isinstance(min, NoUpdate):
updates["min"] = int(min)
if not isinstance(max, NoUpdate):
updates["max"] = int(max)
parameter = self.parameters[name]
parameter.update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.float)
def update_float(
self,
name: str,
*,
value: Union[float, NoUpdate] = NO_UPDATE,
min: Union[float, NoUpdate] = NO_UPDATE,
max: Union[float, NoUpdate] = NO_UPDATE,
step: Union[float, NoUpdate] = NO_UPDATE,
) -> None:
"""
Update a float parameter.
Change the value, bounds, or step size of an existing float parameter.
See :class:`~syd.parameters.FloatParameter` for details.
Parameters
----------
name : str
Name of the parameter to update
value : Union[float, NoUpdate], optional
New value
min : Union[float, NoUpdate], optional
New minimum allowed value
max : Union[float, NoUpdate], optional
New maximum allowed value
step : Union[float, NoUpdate], optional
New step size for the slider
Examples
--------
>>> viewer.update_float('temperature', value=99.5) # Update just the value
>>> viewer.update_float('price', min=5.0, max=200.0, step=0.05) # Update bounds and step
"""
updates = {}
if not isinstance(value, NoUpdate):
updates["value"] = float(value)
if not isinstance(min, NoUpdate):
updates["min"] = float(min)
if not isinstance(max, NoUpdate):
updates["max"] = float(max)
if not isinstance(step, NoUpdate):
updates["step"] = float(step)
parameter = self.parameters[name]
parameter.update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.integer_range)
def update_integer_range(
self,
name: str,
*,
value: Union[Tuple[int, int], NoUpdate] = NO_UPDATE,
min: Union[int, NoUpdate] = NO_UPDATE,
max: Union[int, NoUpdate] = NO_UPDATE,
) -> None:
"""
Update an integer range parameter.
Change the range values or bounds of an existing integer range parameter.
See :class:`~syd.parameters.IntegerRangeParameter` for details.
Parameters
----------
name : str
Name of the parameter to update
value : Union[tuple[int, int], NoUpdate], optional
New (low, high) values
min : Union[int, NoUpdate], optional
New minimum allowed value
max : Union[int, NoUpdate], optional
New maximum allowed value
Examples
--------
>>> viewer.update_integer_range('age_range', value=(30, 50)) # Update just the values
>>> viewer.update_integer_range('year_range', min=1950, max=2023) # Update just the bounds
"""
updates = {}
if not isinstance(value, NoUpdate):
val_low, val_high = value
updates["value"] = (int(val_low), int(val_high))
if not isinstance(min, NoUpdate):
updates["min"] = int(min)
if not isinstance(max, NoUpdate):
updates["max"] = int(max)
parameter = self.parameters[name]
parameter.update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.float_range)
def update_float_range(
self,
name: str,
*,
value: Union[Tuple[float, float], NoUpdate] = NO_UPDATE,
min: Union[float, NoUpdate] = NO_UPDATE,
max: Union[float, NoUpdate] = NO_UPDATE,
step: Union[float, NoUpdate] = NO_UPDATE,
) -> None:
"""
Update a float range parameter.
Change the range values, bounds, or step size of an existing float range parameter.
See :class:`~syd.parameters.FloatRangeParameter` for details.
Parameters
----------
name : str
Name of the parameter to update
value : Union[tuple[float, float], NoUpdate], optional
New (low, high) values
min : Union[float, NoUpdate], optional
New minimum allowed value
max : Union[float, NoUpdate], optional
New maximum allowed value
step : Union[float, NoUpdate], optional
New step size for the slider
Examples
--------
>>> viewer.update_float_range('temp_range', value=(97.5, 98.5)) # Update just the values
>>> viewer.update_float_range(
... 'price_range',
... min=10.0,
... max=500.0,
... step=0.5
... ) # Update bounds and step
"""
updates = {}
if not isinstance(value, NoUpdate):
val_low, val_high = value
updates["value"] = (float(val_low), float(val_high))
if not isinstance(min, NoUpdate):
updates["min"] = float(min)
if not isinstance(max, NoUpdate):
updates["max"] = float(max)
if not isinstance(step, NoUpdate):
updates["step"] = float(step)
parameter = self.parameters[name]
parameter.update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.unbounded_integer)
def update_unbounded_integer(
self,
name: str,
*,
value: Union[int, NoUpdate] = NO_UPDATE,
) -> None:
"""
Update an unbounded integer parameter's value and/or bounds.
Updates a parameter created by :meth:`~syd.viewer.Viewer.add_unbounded_integer`.
See :class:`~syd.parameters.UnboundedIntegerParameter` for details about value validation.
Parameters
----------
name : str
Name of the unbounded integer parameter to update
value : Union[int, NoUpdate], optional
New value (if not provided, no change)
Examples
--------
>>> viewer.add_unbounded_integer('population', value=1000000)
>>> # Update just the value
>>> viewer.update_unbounded_integer('population', value=2000000)
"""
updates = {}
if not value == NO_UPDATE:
updates["value"] = value
if updates:
self.parameters[name].update(updates)
[docs]
@validate_parameter_operation("update", ParameterType.unbounded_float)
def update_unbounded_float(
self,
name: str,
*,
value: Union[float, NoUpdate] = NO_UPDATE,
step: Union[Optional[float], NoUpdate] = NO_UPDATE,
) -> None:
"""
Update an unbounded float parameter's value, bounds, and/or step size.
Updates a parameter created by :meth:`~syd.viewer.Viewer.add_unbounded_float`.
See :class:`~syd.parameters.UnboundedFloatParameter` for details about value validation.
Parameters
----------
name : str
Name of the unbounded float parameter to update
value : Union[float, NoUpdate], optional
New value (will be rounded if step is set) (if not provided, no change)
step : Union[Optional[float], NoUpdate], optional
New step size for rounding, or None for no rounding (if not provided, no change)
Examples
--------
>>> viewer.add_unbounded_float('wavelength', value=550e-9, step=1e-9)
>>> # Update value (will be rounded if step is set)
>>> viewer.update_unbounded_float('wavelength', value=632.8e-9)
>>> # Change step size
>>> viewer.update_unbounded_float('wavelength', step=0.1e-9)
>>> # Remove step size (allow any precision)
>>> viewer.update_unbounded_float('wavelength', step=None)
"""
updates = {}
if not value == NO_UPDATE:
updates["value"] = value
if not step == NO_UPDATE:
updates["step"] = step
if updates:
self.parameters[name].update(updates)