diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index de6d965a..d32dcc63 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -538,7 +538,14 @@ class BECFigure(RPCBase): @rpc_call def motor_map( - self, motor_x: "str" = None, motor_y: "str" = None, **axis_kwargs + self, + motor_x: "str" = None, + motor_y: "str" = None, + new: "bool" = False, + row: "int | None" = None, + col: "int | None" = None, + config: "dict | None" = None, + **axis_kwargs, ) -> "BECMotorMap": """ Add a motor map to the figure. Always access the first motor map widget in the figure. @@ -546,6 +553,10 @@ class BECFigure(RPCBase): Args: motor_x(str): The name of the motor for the X axis. motor_y(str): The name of the motor for the Y axis. + new(bool): If True, create a new plot instead of using the first plot. + row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used. + col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used. + config(dict): Recreates the whole BECImageShow widget from provided configuration. **axis_kwargs: Additional axis properties to set on the widget after creation. Returns: @@ -611,6 +622,12 @@ class BECFigure(RPCBase): list[BECPlotBase]: List of all widgets in the figure. """ + @rpc_call + def apply_config(self, config: "dict | FigureConfig"): + """ + None + """ + class BECImageItem(RPCBase): @property @@ -823,7 +840,7 @@ class BECImageShow(RPCBase): self, monitor: "str", color_map: "Optional[str]" = "magma", - color_bar: "Optional[Literal['simple', 'full']]" = "simple", + color_bar: "Optional[Literal['simple', 'full']]" = "full", downsample: "Optional[bool]" = True, opacity: "Optional[float]" = 1.0, vrange: "Optional[tuple[int, int]]" = None, @@ -839,7 +856,7 @@ class BECImageShow(RPCBase): name: "str", data: "Optional[np.ndarray]" = None, color_map: "Optional[str]" = "magma", - color_bar: "Optional[Literal['simple', 'full']]" = "simple", + color_bar: "Optional[Literal['simple', 'full']]" = "full", downsample: "Optional[bool]" = True, opacity: "Optional[float]" = 1.0, vrange: "Optional[tuple[int, int]]" = None, @@ -1124,6 +1141,16 @@ class BECImageShow(RPCBase): list[BECImageItem]: The list of images. """ + @rpc_call + def apply_config(self, config: "dict | SubplotConfig"): + """ + Apply the configuration to the 1D waveform widget. + + Args: + config(dict|SubplotConfig): Configuration settings. + replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False. + """ + class BECMotorMap(RPCBase): @property @@ -1222,6 +1249,15 @@ class BECMotorMap(RPCBase): Remove the plot widget from the figure. """ + @rpc_call + def apply_config(self, config: "dict | MotorMapConfig"): + """ + Apply the config to the motor map. + + Args: + config(dict|MotorMapConfig): Config to be applied. + """ + class BECPlotBase(RPCBase): @property @@ -1702,6 +1738,16 @@ class BECWaveform(RPCBase): size(int): Font size of the legend. """ + @rpc_call + def apply_config(self, config: "dict | SubplotConfig", replot_last_scan: "bool" = False): + """ + Apply the configuration to the 1D waveform widget. + + Args: + config(dict|SubplotConfig): Configuration settings. + replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False. + """ + class DeviceComboBox(RPCBase): @property diff --git a/bec_widgets/widgets/figure/figure.py b/bec_widgets/widgets/figure/figure.py index a014b908..98a13c82 100644 --- a/bec_widgets/widgets/figure/figure.py +++ b/bec_widgets/widgets/figure/figure.py @@ -8,7 +8,7 @@ from typing import Literal, Optional import numpy as np import pyqtgraph as pg import qdarktheme -from pydantic import Field +from pydantic import Field, ValidationError, field_validator from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtWidgets import QWidget from typeguard import typechecked @@ -30,6 +30,26 @@ class FigureConfig(ConnectionConfig): {}, description="The list of widgets to be added to the figure widget." ) + @field_validator("widgets", mode="before") + @classmethod + def validate_widgets(cls, v): + """Validate the widgets configuration.""" + widget_class_map = { + "BECWaveform": Waveform1DConfig, + "BECImageShow": ImageConfig, + "BECMotorMap": MotorMapConfig, + } + validated_widgets = {} + for key, widget_config in v.items(): + if "widget_class" not in widget_config: + raise ValueError(f"Widget config for {key} does not contain 'widget_class'.") + widget_class = widget_config["widget_class"] + if widget_class not in widget_class_map: + raise ValueError(f"Unknown widget_class '{widget_class}' for widget '{key}'.") + config_class = widget_class_map[widget_class] + validated_widgets[key] = config_class(**widget_config) + return validated_widgets + class WidgetHandler: """Factory for creating and configuring BEC widgets for BECFigure.""" @@ -103,6 +123,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): "clear_all", "get_all_rpc", "widget_list", + "apply_config", ] subplot_map = { "PlotBase": BECPlotBase, @@ -110,6 +131,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): "BECImageShow": BECImageShow, "BECMotorMap": BECMotorMap, } + widget_method_map = {"BECWaveform": "plot", "BECImageShow": "image", "BECMotorMap": "motor_map"} clean_signal = pyqtSignal() @@ -125,8 +147,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): else: if isinstance(config, dict): config = FigureConfig(**config) - self.config = config - super().__init__(client=client, config=config, gui_id=gui_id) + super().__init__(client=client, gui_id=gui_id) pg.GraphicsLayoutWidget.__init__(self, parent) self.widget_handler = WidgetHandler() @@ -136,6 +157,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): # Container to keep track of the grid self.grid = [] + # Create config and apply it + self.apply_config(config) def __getitem__(self, key: tuple | str): if isinstance(key, tuple) and len(key) == 2: @@ -150,6 +173,24 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): "Key must be a string (widget id) or a tuple of two integers (grid coordinates)" ) + def apply_config(self, config: dict | FigureConfig): + if isinstance(config, dict): + try: + config = FigureConfig(**config) + except ValidationError as e: + print(f"Error in applying config: {e}") + return + self.config = config + self.change_theme(self.config.theme) + + # widget_config has to be reset for not have each widget config twice when added to the figure + widget_configs = [config for config in self.config.widgets.values()] + self.config.widgets = {} + for widget_config in widget_configs: + getattr(self, self.widget_method_map[widget_config.widget_class])( + config=widget_config.model_dump(), row=widget_config.row, col=widget_config.col + ) + @property def widget_list(self) -> list[BECPlotBase]: """ diff --git a/bec_widgets/widgets/figure/plots/image/image.py b/bec_widgets/widgets/figure/plots/image/image.py index 3572dc0a..09610e7d 100644 --- a/bec_widgets/widgets/figure/plots/image/image.py +++ b/bec_widgets/widgets/figure/plots/image/image.py @@ -60,6 +60,7 @@ class BECImageShow(BECPlotBase): "lock_aspect_ratio", "remove", "images", + "apply_config", ] def __init__( diff --git a/bec_widgets/widgets/figure/plots/motor_map/motor_map.py b/bec_widgets/widgets/figure/plots/motor_map/motor_map.py index 190081ca..f6d12adb 100644 --- a/bec_widgets/widgets/figure/plots/motor_map/motor_map.py +++ b/bec_widgets/widgets/figure/plots/motor_map/motor_map.py @@ -62,6 +62,7 @@ class BECMotorMap(BECPlotBase): "set_scatter_size", "get_data", "remove", + "apply_config", ] # QT Signals diff --git a/bec_widgets/widgets/figure/plots/waveform/waveform.py b/bec_widgets/widgets/figure/plots/waveform/waveform.py index 46d925e9..76a430b0 100644 --- a/bec_widgets/widgets/figure/plots/waveform/waveform.py +++ b/bec_widgets/widgets/figure/plots/waveform/waveform.py @@ -59,6 +59,7 @@ class BECWaveform(BECPlotBase): "lock_aspect_ratio", "remove", "set_legend_label_size", + "apply_config", ] scan_signal_update = pyqtSignal() dap_params_update = pyqtSignal(dict)