mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21:50 +02:00
feat(multi_waveform): multi-waveform widget based on new PlotBase
This commit is contained in:
@ -32,6 +32,7 @@ class Widgets(str, enum.Enum):
|
||||
LogPanel = "LogPanel"
|
||||
Minesweeper = "Minesweeper"
|
||||
MotorMap = "MotorMap"
|
||||
MultiWaveform = "MultiWaveform"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerBox2D = "PositionerBox2D"
|
||||
@ -3662,6 +3663,415 @@ class MotorMap(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class MultiWaveform(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_toolbar(self) -> "bool":
|
||||
"""
|
||||
Show Toolbar.
|
||||
"""
|
||||
|
||||
@enable_toolbar.setter
|
||||
@rpc_call
|
||||
def enable_toolbar(self) -> "bool":
|
||||
"""
|
||||
Show Toolbar.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_side_panel(self) -> "bool":
|
||||
"""
|
||||
Show Side Panel
|
||||
"""
|
||||
|
||||
@enable_side_panel.setter
|
||||
@rpc_call
|
||||
def enable_side_panel(self) -> "bool":
|
||||
"""
|
||||
Show Side Panel
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self) -> "bool":
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@enable_fps_monitor.setter
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self) -> "bool":
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def title(self) -> "str":
|
||||
"""
|
||||
Set title of the plot.
|
||||
"""
|
||||
|
||||
@title.setter
|
||||
@rpc_call
|
||||
def title(self) -> "str":
|
||||
"""
|
||||
Set title of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_label(self) -> "str":
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
"""
|
||||
|
||||
@x_label.setter
|
||||
@rpc_call
|
||||
def x_label(self) -> "str":
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_label(self) -> "str":
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
"""
|
||||
|
||||
@y_label.setter
|
||||
@rpc_call
|
||||
def y_label(self) -> "str":
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the x limits of the plot.
|
||||
"""
|
||||
|
||||
@x_limits.setter
|
||||
@rpc_call
|
||||
def x_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the x limits of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the y limits of the plot.
|
||||
"""
|
||||
|
||||
@y_limits.setter
|
||||
@rpc_call
|
||||
def y_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the y limits of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
"""
|
||||
|
||||
@x_grid.setter
|
||||
@rpc_call
|
||||
def x_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@y_grid.setter
|
||||
@rpc_call
|
||||
def y_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def inner_axes(self) -> "bool":
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
"""
|
||||
|
||||
@inner_axes.setter
|
||||
@rpc_call
|
||||
def inner_axes(self) -> "bool":
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def outer_axes(self) -> "bool":
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
"""
|
||||
|
||||
@outer_axes.setter
|
||||
@rpc_call
|
||||
def outer_axes(self) -> "bool":
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self) -> "bool":
|
||||
"""
|
||||
Lock aspect ratio of the plot widget.
|
||||
"""
|
||||
|
||||
@lock_aspect_ratio.setter
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self) -> "bool":
|
||||
"""
|
||||
Lock aspect ratio of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def auto_range_x(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the x-axis.
|
||||
"""
|
||||
|
||||
@auto_range_x.setter
|
||||
@rpc_call
|
||||
def auto_range_x(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def auto_range_y(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@auto_range_y.setter
|
||||
@rpc_call
|
||||
def auto_range_y(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@x_log.setter
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_log(self) -> "bool":
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@y_log.setter
|
||||
@rpc_call
|
||||
def y_log(self) -> "bool":
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def legend_label_size(self) -> "int":
|
||||
"""
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@legend_label_size.setter
|
||||
@rpc_call
|
||||
def legend_label_size(self) -> "int":
|
||||
"""
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def highlighted_index(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@highlighted_index.setter
|
||||
@rpc_call
|
||||
def highlighted_index(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def highlight_last_curve(self) -> "bool":
|
||||
"""
|
||||
Get the highlight_last_curve property.
|
||||
Returns:
|
||||
bool: The highlight_last_curve property.
|
||||
"""
|
||||
|
||||
@highlight_last_curve.setter
|
||||
@rpc_call
|
||||
def highlight_last_curve(self) -> "bool":
|
||||
"""
|
||||
Get the highlight_last_curve property.
|
||||
Returns:
|
||||
bool: The highlight_last_curve property.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_palette(self) -> "str":
|
||||
"""
|
||||
The color palette of the figure widget.
|
||||
"""
|
||||
|
||||
@color_palette.setter
|
||||
@rpc_call
|
||||
def color_palette(self) -> "str":
|
||||
"""
|
||||
The color palette of the figure widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def opacity(self) -> "int":
|
||||
"""
|
||||
The opacity of the figure widget.
|
||||
"""
|
||||
|
||||
@opacity.setter
|
||||
@rpc_call
|
||||
def opacity(self) -> "int":
|
||||
"""
|
||||
The opacity of the figure widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def flush_buffer(self) -> "bool":
|
||||
"""
|
||||
The flush_buffer property.
|
||||
"""
|
||||
|
||||
@flush_buffer.setter
|
||||
@rpc_call
|
||||
def flush_buffer(self) -> "bool":
|
||||
"""
|
||||
The flush_buffer property.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def max_trace(self) -> "int":
|
||||
"""
|
||||
The maximum number of traces to display on the plot.
|
||||
"""
|
||||
|
||||
@max_trace.setter
|
||||
@rpc_call
|
||||
def max_trace(self) -> "int":
|
||||
"""
|
||||
The maximum number of traces to display on the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def monitor(self) -> "str":
|
||||
"""
|
||||
The monitor of the figure widget.
|
||||
"""
|
||||
|
||||
@monitor.setter
|
||||
@rpc_call
|
||||
def monitor(self) -> "str":
|
||||
"""
|
||||
The monitor of the figure widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_curve_limit(self, max_trace: "int", flush_buffer: "bool"):
|
||||
"""
|
||||
Set the maximum number of traces to display on the plot.
|
||||
|
||||
Args:
|
||||
max_trace (int): The maximum number of traces to display.
|
||||
flush_buffer (bool): Flush the buffer.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def plot(self, monitor: "str", color_palette: "str | None" = "magma"):
|
||||
"""
|
||||
Create a plot for the given monitor.
|
||||
Args:
|
||||
monitor (str): The monitor to set.
|
||||
color_palette (str|None): The color palette to use for the plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_curve_highlight(self, index: "int"):
|
||||
"""
|
||||
Set the curve highlight based on visible curves.
|
||||
|
||||
Args:
|
||||
index (int): The index of the curve to highlight among visible curves.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def clear_curves(self):
|
||||
"""
|
||||
Remove all curves from the plot, excluding crosshair items.
|
||||
"""
|
||||
|
||||
|
||||
class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
|
@ -22,6 +22,7 @@ from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutM
|
||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
from bec_widgets.widgets.plots_next_gen.image.image import Image
|
||||
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import MotorMap
|
||||
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform import MultiWaveform
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
from bec_widgets.widgets.plots_next_gen.scatter_waveform.scatter_waveform import ScatterWaveform
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
@ -69,6 +70,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"wf": self.wf,
|
||||
"scatter": self.scatter,
|
||||
"scatter_mi": self.scatter,
|
||||
"mwf": self.mwf,
|
||||
}
|
||||
)
|
||||
|
||||
@ -152,6 +154,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
tab_widget.setCurrentIndex(7)
|
||||
|
||||
ninth_tab = QWidget()
|
||||
ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
self.mwf = MultiWaveform()
|
||||
ninth_tab_layout.addWidget(self.mwf)
|
||||
tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
tab_widget.setCurrentIndex(8)
|
||||
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
|
||||
|
@ -0,0 +1,501 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.qt_utils.side_panel import SidePanel
|
||||
from bec_widgets.utils import Colors, ConnectionConfig
|
||||
from bec_widgets.widgets.plots_next_gen.multi_waveform.settings.control_panel import (
|
||||
MultiWaveformControlPanel,
|
||||
)
|
||||
from bec_widgets.widgets.plots_next_gen.multi_waveform.toolbar_bundles.monitor_selection import (
|
||||
MultiWaveformSelectionToolbarBundle,
|
||||
)
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MultiWaveformConfig(ConnectionConfig):
|
||||
color_palette: str | None = Field(
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
curve_limit: int | None = Field(
|
||||
200, description="The maximum number of curves to display on the plot."
|
||||
)
|
||||
flush_buffer: bool | None = Field(
|
||||
False, description="Flush the buffer of the plot widget when the curve limit is reached."
|
||||
)
|
||||
monitor: str | None = Field(None, description="The monitor to set for the plot widget.")
|
||||
curve_width: int | None = Field(1, description="The width of the curve on the plot.")
|
||||
opacity: int | None = Field(50, description="The opacity of the curve on the plot.")
|
||||
highlight_last_curve: bool | None = Field(
|
||||
True, description="Highlight the last curve on the plot."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
_validate_color_map_z = field_validator("color_palette")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class MultiWaveform(PlotBase):
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
ICON_NAME = "ssid_chart"
|
||||
USER_ACCESS = [
|
||||
# General PlotBase Settings
|
||||
"enable_toolbar",
|
||||
"enable_toolbar.setter",
|
||||
"enable_side_panel",
|
||||
"enable_side_panel.setter",
|
||||
"enable_fps_monitor",
|
||||
"enable_fps_monitor.setter",
|
||||
"set",
|
||||
"title",
|
||||
"title.setter",
|
||||
"x_label",
|
||||
"x_label.setter",
|
||||
"y_label",
|
||||
"y_label.setter",
|
||||
"x_limits",
|
||||
"x_limits.setter",
|
||||
"y_limits",
|
||||
"y_limits.setter",
|
||||
"x_grid",
|
||||
"x_grid.setter",
|
||||
"y_grid",
|
||||
"y_grid.setter",
|
||||
"inner_axes",
|
||||
"inner_axes.setter",
|
||||
"outer_axes",
|
||||
"outer_axes.setter",
|
||||
"lock_aspect_ratio",
|
||||
"lock_aspect_ratio.setter",
|
||||
"auto_range_x",
|
||||
"auto_range_x.setter",
|
||||
"auto_range_y",
|
||||
"auto_range_y.setter",
|
||||
"x_log",
|
||||
"x_log.setter",
|
||||
"y_log",
|
||||
"y_log.setter",
|
||||
"legend_label_size",
|
||||
"legend_label_size.setter",
|
||||
# MultiWaveform Specific RPC Access
|
||||
"highlighted_index",
|
||||
"highlighted_index.setter",
|
||||
"highlight_last_curve",
|
||||
"highlight_last_curve.setter",
|
||||
"color_palette",
|
||||
"color_palette.setter",
|
||||
"opacity",
|
||||
"opacity.setter",
|
||||
"flush_buffer",
|
||||
"flush_buffer.setter",
|
||||
"max_trace",
|
||||
"max_trace.setter",
|
||||
"monitor",
|
||||
"monitor.setter",
|
||||
"set_curve_limit",
|
||||
"plot",
|
||||
"set_curve_highlight",
|
||||
"clear_curves",
|
||||
]
|
||||
|
||||
monitor_signal_updated = Signal()
|
||||
highlighted_curve_index_changed = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
config: MultiWaveformConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
popups: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = MultiWaveformConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("MultiWaveform")
|
||||
|
||||
# Scan Data
|
||||
self.old_scan_id = None
|
||||
self.scan_id = None
|
||||
self.connected = False
|
||||
self._current_highlight_index = 0
|
||||
self._curves = deque()
|
||||
self.visible_curves = []
|
||||
self.number_of_visible_curves = 0
|
||||
|
||||
self._init_control_panel()
|
||||
|
||||
################################################################################
|
||||
# Widget Specific GUI interactions
|
||||
################################################################################
|
||||
def _init_toolbar(self):
|
||||
self.monitor_selection_bundle = MultiWaveformSelectionToolbarBundle(
|
||||
bundle_id="motor_selection", target_widget=self
|
||||
)
|
||||
self.toolbar.add_bundle(self.monitor_selection_bundle, target_widget=self)
|
||||
super()._init_toolbar()
|
||||
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
||||
|
||||
def _init_control_panel(self):
|
||||
self.control_panel = SidePanel(self, orientation="top", panel_max_width=90)
|
||||
self.layout_manager.add_widget_relative(
|
||||
self.control_panel, self.round_plot_widget, "bottom"
|
||||
)
|
||||
self.controls = MultiWaveformControlPanel(target_widget=self)
|
||||
self.control_panel.add_menu(
|
||||
action_id="control",
|
||||
icon_name="tune",
|
||||
tooltip="Show Control panel",
|
||||
widget=self.controls,
|
||||
title=None,
|
||||
)
|
||||
self.control_panel.toolbar.widgets["control"].action.trigger()
|
||||
|
||||
################################################################################
|
||||
# Widget Specific Properties
|
||||
################################################################################
|
||||
|
||||
@property
|
||||
def curves(self) -> deque:
|
||||
"""
|
||||
Get the curves of the plot widget as a deque.
|
||||
Returns:
|
||||
deque: Deque of curves.
|
||||
"""
|
||||
return self._curves
|
||||
|
||||
@curves.setter
|
||||
def curves(self, value: deque):
|
||||
self._curves = value
|
||||
|
||||
@SafeProperty(int, designable=False)
|
||||
def highlighted_index(self):
|
||||
return self._current_highlight_index
|
||||
|
||||
@highlighted_index.setter
|
||||
def highlighted_index(self, value: int):
|
||||
self._current_highlight_index = value
|
||||
self.property_changed.emit("highlighted_index", value)
|
||||
self.set_curve_highlight(value)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def highlight_last_curve(self) -> bool:
|
||||
"""
|
||||
Get the highlight_last_curve property.
|
||||
Returns:
|
||||
bool: The highlight_last_curve property.
|
||||
"""
|
||||
return self.config.highlight_last_curve
|
||||
|
||||
@highlight_last_curve.setter
|
||||
def highlight_last_curve(self, value: bool):
|
||||
self.config.highlight_last_curve = value
|
||||
self.property_changed.emit("highlight_last_curve", value)
|
||||
self.set_curve_highlight(-1)
|
||||
|
||||
@SafeProperty(str)
|
||||
def color_palette(self) -> str:
|
||||
"""
|
||||
The color palette of the figure widget.
|
||||
"""
|
||||
return self.config.color_palette
|
||||
|
||||
@color_palette.setter
|
||||
def color_palette(self, value: str):
|
||||
"""
|
||||
Set the color palette of the figure widget.
|
||||
|
||||
Args:
|
||||
value(str): The color palette to set.
|
||||
"""
|
||||
try:
|
||||
self.config.color_palette = value
|
||||
except ValidationError:
|
||||
return
|
||||
self.set_curve_highlight(self._current_highlight_index)
|
||||
self._sync_monitor_selection_toolbar()
|
||||
|
||||
@SafeProperty(int)
|
||||
def opacity(self) -> int:
|
||||
"""
|
||||
The opacity of the figure widget.
|
||||
"""
|
||||
return self.config.opacity
|
||||
|
||||
@opacity.setter
|
||||
def opacity(self, value: int):
|
||||
"""
|
||||
Set the opacity of the figure widget.
|
||||
|
||||
Args:
|
||||
value(int): The opacity to set.
|
||||
"""
|
||||
self.config.opacity = max(0, min(100, value))
|
||||
self.property_changed.emit("opacity", value)
|
||||
self.set_curve_highlight(self._current_highlight_index)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def flush_buffer(self) -> bool:
|
||||
"""
|
||||
The flush_buffer property.
|
||||
"""
|
||||
return self.config.flush_buffer
|
||||
|
||||
@flush_buffer.setter
|
||||
def flush_buffer(self, value: bool):
|
||||
self.config.flush_buffer = value
|
||||
self.property_changed.emit("flush_buffer", value)
|
||||
self.set_curve_limit(
|
||||
max_trace=self.config.curve_limit, flush_buffer=self.config.flush_buffer
|
||||
)
|
||||
|
||||
@SafeProperty(int)
|
||||
def max_trace(self) -> int:
|
||||
"""
|
||||
The maximum number of traces to display on the plot.
|
||||
"""
|
||||
return self.config.curve_limit
|
||||
|
||||
@max_trace.setter
|
||||
def max_trace(self, value: int):
|
||||
"""
|
||||
Set the maximum number of traces to display on the plot.
|
||||
|
||||
Args:
|
||||
value(int): The maximum number of traces to display.
|
||||
"""
|
||||
self.config.curve_limit = value
|
||||
self.property_changed.emit("max_trace", value)
|
||||
self.set_curve_limit(
|
||||
max_trace=self.config.curve_limit, flush_buffer=self.config.flush_buffer
|
||||
)
|
||||
|
||||
@SafeProperty(str)
|
||||
def monitor(self) -> str:
|
||||
"""
|
||||
The monitor of the figure widget.
|
||||
"""
|
||||
return self.config.monitor
|
||||
|
||||
@monitor.setter
|
||||
def monitor(self, value: str):
|
||||
"""
|
||||
Set the monitor of the figure widget.
|
||||
|
||||
Args:
|
||||
value(str): The monitor to set.
|
||||
"""
|
||||
self.plot(value)
|
||||
|
||||
################################################################################
|
||||
# High Level methods for API
|
||||
################################################################################
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def plot(self, monitor: str, color_palette: str | None = "magma"):
|
||||
"""
|
||||
Create a plot for the given monitor.
|
||||
Args:
|
||||
monitor (str): The monitor to set.
|
||||
color_palette (str|None): The color palette to use for the plot.
|
||||
"""
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
self._disconnect_monitor()
|
||||
self.config.monitor = monitor
|
||||
self._connect_monitor()
|
||||
if color_palette is not None:
|
||||
self.color_palette = color_palette
|
||||
self._sync_monitor_selection_toolbar()
|
||||
|
||||
@SafeSlot(int, bool)
|
||||
def set_curve_limit(self, max_trace: int, flush_buffer: bool):
|
||||
"""
|
||||
Set the maximum number of traces to display on the plot.
|
||||
|
||||
Args:
|
||||
max_trace (int): The maximum number of traces to display.
|
||||
flush_buffer (bool): Flush the buffer.
|
||||
"""
|
||||
if max_trace != self.config.curve_limit:
|
||||
self.config.curve_limit = max_trace
|
||||
if flush_buffer != self.config.flush_buffer:
|
||||
self.config.flush_buffer = flush_buffer
|
||||
|
||||
if self.config.curve_limit is None:
|
||||
self.scale_colors()
|
||||
return
|
||||
|
||||
if self.config.flush_buffer:
|
||||
# Remove excess curves from the plot and the deque
|
||||
while len(self.curves) > self.config.curve_limit:
|
||||
curve = self.curves.popleft()
|
||||
self.plot_item.removeItem(curve)
|
||||
else:
|
||||
# Hide or show curves based on the new max_trace
|
||||
num_curves_to_show = min(self.config.curve_limit, len(self.curves))
|
||||
for i, curve in enumerate(self.curves):
|
||||
if i < len(self.curves) - num_curves_to_show:
|
||||
curve.hide()
|
||||
else:
|
||||
curve.show()
|
||||
self.scale_colors()
|
||||
self.monitor_signal_updated.emit()
|
||||
|
||||
################################################################################
|
||||
# BEC Update Methods
|
||||
################################################################################
|
||||
@SafeSlot(dict, dict)
|
||||
def on_monitor_1d_update(self, msg: dict, metadata: dict):
|
||||
"""
|
||||
Update the plot widget with the monitor data.
|
||||
|
||||
Args:
|
||||
msg(dict): The message data.
|
||||
metadata(dict): The metadata of the message.
|
||||
"""
|
||||
data = msg.get("data", None)
|
||||
current_scan_id = metadata.get("scan_id", None)
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self.scan_id = current_scan_id
|
||||
self.clear_curves()
|
||||
self.curves.clear()
|
||||
if self.crosshair:
|
||||
self.crosshair.clear_markers()
|
||||
|
||||
# Always create a new curve and add it
|
||||
curve = pg.PlotDataItem()
|
||||
curve.setData(data)
|
||||
self.plot_item.addItem(curve)
|
||||
self.curves.append(curve)
|
||||
|
||||
# Max Trace and scale colors
|
||||
self.set_curve_limit(self.config.curve_limit, self.config.flush_buffer)
|
||||
|
||||
@SafeSlot(int)
|
||||
def set_curve_highlight(self, index: int):
|
||||
"""
|
||||
Set the curve highlight based on visible curves.
|
||||
|
||||
Args:
|
||||
index (int): The index of the curve to highlight among visible curves.
|
||||
"""
|
||||
self.plot_item.visible_curves = [curve for curve in self.curves if curve.isVisible()]
|
||||
num_visible_curves = len(self.plot_item.visible_curves)
|
||||
self.number_of_visible_curves = num_visible_curves
|
||||
|
||||
if num_visible_curves == 0:
|
||||
return # No curves to highlight
|
||||
|
||||
if index >= num_visible_curves:
|
||||
index = num_visible_curves - 1
|
||||
elif index < 0:
|
||||
index = num_visible_curves + index
|
||||
self._current_highlight_index = index
|
||||
num_colors = num_visible_curves
|
||||
colors = Colors.evenly_spaced_colors(
|
||||
colormap=self.config.color_palette, num=num_colors, format="HEX"
|
||||
)
|
||||
for i, curve in enumerate(self.plot_item.visible_curves):
|
||||
curve.setPen()
|
||||
if i == self._current_highlight_index:
|
||||
curve.setPen(pg.mkPen(color=colors[i], width=5))
|
||||
curve.setAlpha(alpha=1, auto=False)
|
||||
curve.setZValue(1)
|
||||
else:
|
||||
curve.setPen(pg.mkPen(color=colors[i], width=1))
|
||||
curve.setAlpha(alpha=self.config.opacity / 100, auto=False)
|
||||
curve.setZValue(0)
|
||||
|
||||
self.highlighted_curve_index_changed.emit(self._current_highlight_index)
|
||||
|
||||
def _disconnect_monitor(self):
|
||||
try:
|
||||
previous_monitor = self.config.monitor
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
|
||||
if previous_monitor and self.connected is True:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(previous_monitor)
|
||||
)
|
||||
self.connected = False
|
||||
|
||||
def _connect_monitor(self):
|
||||
"""
|
||||
Connect the monitor to the plot widget.
|
||||
"""
|
||||
|
||||
if self.config.monitor and self.connected is False:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(self.config.monitor)
|
||||
)
|
||||
self.connected = True
|
||||
|
||||
################################################################################
|
||||
# Utility Methods
|
||||
################################################################################
|
||||
def scale_colors(self):
|
||||
"""
|
||||
Scale the colors of the curves based on the current colormap.
|
||||
"""
|
||||
# TODO probably has to be changed to property
|
||||
if self.config.highlight_last_curve:
|
||||
self.set_curve_highlight(-1) # Use -1 to highlight the last visible curve
|
||||
else:
|
||||
self.set_curve_highlight(self._current_highlight_index)
|
||||
|
||||
def hook_crosshair(self) -> None:
|
||||
"""
|
||||
Specific hookfor crosshair, since it is for multiple curves.
|
||||
"""
|
||||
super().hook_crosshair()
|
||||
if self.crosshair:
|
||||
self.highlighted_curve_index_changed.connect(self.crosshair.update_highlighted_curve)
|
||||
if self.curves:
|
||||
self.crosshair.update_highlighted_curve(self._current_highlight_index)
|
||||
|
||||
def clear_curves(self):
|
||||
"""
|
||||
Remove all curves from the plot, excluding crosshair items.
|
||||
"""
|
||||
items_to_remove = []
|
||||
for item in self.plot_item.items:
|
||||
if not getattr(item, "is_crosshair", False) and isinstance(item, pg.PlotDataItem):
|
||||
items_to_remove.append(item)
|
||||
for item in items_to_remove:
|
||||
self.plot_item.removeItem(item)
|
||||
|
||||
def _sync_monitor_selection_toolbar(self):
|
||||
"""
|
||||
Sync the motor map selection toolbar with the current motor map.
|
||||
"""
|
||||
if self.monitor_selection_bundle is not None:
|
||||
monitor = self.monitor_selection_bundle.monitor.currentText()
|
||||
color_palette = self.monitor_selection_bundle.colormap_widget.colormap
|
||||
|
||||
if monitor != self.config.monitor:
|
||||
self.monitor_selection_bundle.monitor.blockSignals(True)
|
||||
self.monitor_selection_bundle.monitor.set_device(self.config.monitor)
|
||||
self.monitor_selection_bundle.monitor.check_validity(self.config.monitor)
|
||||
self.monitor_selection_bundle.monitor.blockSignals(False)
|
||||
|
||||
if color_palette != self.config.color_palette:
|
||||
self.monitor_selection_bundle.colormap_widget.blockSignals(True)
|
||||
self.monitor_selection_bundle.colormap_widget.colormap = self.config.color_palette
|
||||
self.monitor_selection_bundle.colormap_widget.blockSignals(False)
|
@ -0,0 +1 @@
|
||||
{'files': ['multi_waveform.py']}
|
@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform import MultiWaveform
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='MultiWaveform' name='multi_waveform'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class MultiWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = MultiWaveform(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Plot Widgets Next Gen"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(MultiWaveform.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "multi_waveform"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "MultiWaveform"
|
||||
|
||||
def toolTip(self):
|
||||
return "MultiWaveform"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
@ -0,0 +1,17 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform_plugin import (
|
||||
MultiWaveformPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(MultiWaveformPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
@ -0,0 +1,145 @@
|
||||
import os
|
||||
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class MultiWaveformControlPanel(SettingWidget):
|
||||
"""
|
||||
A settings widget MultiWaveformControlPanel that allows the user to modify the properties.
|
||||
|
||||
The widget has skip_settings property set to True, which means it should not be saved
|
||||
in the settings file. It is used to mirror the properties of the target widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("MultiWaveformControlPanel")
|
||||
current_path = os.path.dirname(__file__)
|
||||
|
||||
form = UILoader().load_ui(os.path.join(current_path, "multi_waveform_controls.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.ui = form
|
||||
|
||||
self.ui_widget_list = [
|
||||
self.ui.opacity,
|
||||
self.ui.highlighted_index,
|
||||
self.ui.highlight_last_curve,
|
||||
self.ui.flush_buffer,
|
||||
self.ui.max_trace,
|
||||
]
|
||||
|
||||
if self.target_widget is not None:
|
||||
self.connect_all_signals()
|
||||
self.target_widget.property_changed.connect(self.update_property)
|
||||
self.target_widget.monitor_signal_updated.connect(self.update_controls_limits)
|
||||
self.ui.highlight_last_curve.toggled.connect(self.set_highlight_last_curve)
|
||||
|
||||
self.fetch_all_properties()
|
||||
|
||||
def connect_all_signals(self):
|
||||
for widget in self.ui_widget_list:
|
||||
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||
|
||||
@SafeSlot()
|
||||
def set_property(self, widget: QWidget, value):
|
||||
"""
|
||||
Set property of the target widget based on the widget that emitted the signal.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget that emitted the signal.
|
||||
value(): The value to set the property to.
|
||||
"""
|
||||
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
property_name = widget.objectName()
|
||||
setattr(self.target_widget, property_name, value)
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
@SafeSlot()
|
||||
def update_property(self, property_name: str, value):
|
||||
"""
|
||||
Update the value of the widget based on the property name and value.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
property_name(str): The name of the property to update.
|
||||
value: The value to set the property to.
|
||||
"""
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
widget_to_set = self.ui.findChild(QWidget, property_name)
|
||||
except RuntimeError:
|
||||
return
|
||||
if widget_to_set is None:
|
||||
return
|
||||
|
||||
WidgetIO.set_value(widget_to_set, value)
|
||||
|
||||
def fetch_all_properties(self):
|
||||
"""
|
||||
Fetch all properties from the target widget and update the settings widget.
|
||||
"""
|
||||
for widget in self.ui_widget_list:
|
||||
property_name = widget.objectName()
|
||||
value = getattr(self.target_widget, property_name)
|
||||
WidgetIO.set_value(widget, value)
|
||||
|
||||
def accept_changes(self):
|
||||
"""
|
||||
Apply all properties from the settings widget to the target widget.
|
||||
"""
|
||||
for widget in self.ui_widget_list:
|
||||
property_name = widget.objectName()
|
||||
value = WidgetIO.get_value(widget)
|
||||
setattr(self.target_widget, property_name, value)
|
||||
|
||||
@SafeSlot()
|
||||
def update_controls_limits(self):
|
||||
"""
|
||||
Update the limits of the controls.
|
||||
"""
|
||||
num_curves = len(self.target_widget.curves)
|
||||
if num_curves == 0:
|
||||
num_curves = 1 # Avoid setting max to 0
|
||||
current_index = num_curves - 1
|
||||
self.ui.highlighted_index.setMinimum(0)
|
||||
self.ui.highlighted_index.setMaximum(self.target_widget.number_of_visible_curves - 1)
|
||||
self.ui.spinbox_index.setMaximum(self.target_widget.number_of_visible_curves - 1)
|
||||
if self.ui.highlight_last_curve.isChecked():
|
||||
self.ui.highlighted_index.setValue(current_index)
|
||||
self.ui.spinbox_index.setValue(current_index)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def set_highlight_last_curve(self, enable: bool) -> None:
|
||||
"""
|
||||
Enable or disable highlighting of the last curve.
|
||||
|
||||
Args:
|
||||
enable(bool): True to enable highlighting of the last curve, False to disable.
|
||||
"""
|
||||
self.target_widget.config.highlight_last_curve = enable
|
||||
if enable:
|
||||
self.ui.highlighted_index.setEnabled(False)
|
||||
self.ui.spinbox_index.setEnabled(False)
|
||||
self.ui.highlight_last_curve.setChecked(True)
|
||||
self.target_widget.set_curve_highlight(-1)
|
||||
else:
|
||||
self.ui.highlighted_index.setEnabled(True)
|
||||
self.ui.spinbox_index.setEnabled(True)
|
||||
self.ui.highlight_last_curve.setChecked(False)
|
||||
index = self.ui.spinbox_index.value()
|
||||
self.target_widget.set_curve_highlight(index)
|
@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>561</width>
|
||||
<height>86</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_curve_index">
|
||||
<property name="text">
|
||||
<string>Curve Index</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSlider" name="highlighted_index">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QSpinBox" name="spinbox_index"/>
|
||||
</item>
|
||||
<item row="0" column="3" colspan="3">
|
||||
<widget class="QCheckBox" name="highlight_last_curve">
|
||||
<property name="text">
|
||||
<string>Highlight always last curve</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_opacity">
|
||||
<property name="text">
|
||||
<string>Opacity</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSlider" name="opacity">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QLabel" name="label_max_trace">
|
||||
<property name="text">
|
||||
<string>Max Trace</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QSpinBox" name="max_trace">
|
||||
<property name="toolTip">
|
||||
<string>How many curves should be displayed</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>500</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>200</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="5">
|
||||
<widget class="QCheckBox" name="flush_buffer">
|
||||
<property name="toolTip">
|
||||
<string>If hiddne curves should be deleted.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Flush Buffer</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QSpinBox" name="spinbox_opacity">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>opacity</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>spinbox_opacity</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>211</x>
|
||||
<y>66</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>260</x>
|
||||
<y>59</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>spinbox_opacity</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>opacity</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>269</x>
|
||||
<y>62</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>182</x>
|
||||
<y>62</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>highlighted_index</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>spinbox_index</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>191</x>
|
||||
<y>27</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>256</x>
|
||||
<y>27</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>spinbox_index</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>highlighted_index</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>264</x>
|
||||
<y>20</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>195</x>
|
||||
<y>24</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -0,0 +1,58 @@
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QStyledItemDelegate
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import ToolbarBundle, WidgetAction
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||
|
||||
|
||||
class NoCheckDelegate(QStyledItemDelegate):
|
||||
"""To reduce space in combo boxes by removing the checkmark."""
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super().initStyleOption(option, index)
|
||||
# Remove any check indicator
|
||||
option.checkState = Qt.Unchecked
|
||||
|
||||
|
||||
class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
"""
|
||||
A bundle of actions for a toolbar that selects motors.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="monitor_selection", target_widget=None, **kwargs):
|
||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||
self.target_widget = target_widget
|
||||
|
||||
# Monitor Selection
|
||||
self.monitor = DeviceComboBox(
|
||||
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=ReadoutPriority.ASYNC
|
||||
)
|
||||
self.monitor.addItem("", None)
|
||||
self.monitor.setCurrentText("")
|
||||
self.monitor.setToolTip("Select Monitor")
|
||||
self.monitor.setItemDelegate(NoCheckDelegate(self.monitor))
|
||||
self.add_action("monitor", WidgetAction(widget=self.monitor, adjust_size=False))
|
||||
|
||||
# Colormap Selection
|
||||
self.colormap_widget = BECColorMapWidget(cmap="magma")
|
||||
self.add_action("color_map", WidgetAction(widget=self.colormap_widget, adjust_size=False))
|
||||
|
||||
# Connect slots, a device will be connected upon change of any combobox
|
||||
self.monitor.currentTextChanged.connect(lambda: self.connect())
|
||||
self.colormap_widget.colormap_changed_signal.connect(self.change_colormap)
|
||||
|
||||
@SafeSlot()
|
||||
def connect(self):
|
||||
monitor = self.monitor.currentText()
|
||||
|
||||
if monitor != "":
|
||||
if monitor != self.target_widget.config.monitor:
|
||||
self.target_widget.monitor = monitor
|
||||
|
||||
@SafeSlot(str)
|
||||
def change_colormap(self, colormap: str):
|
||||
self.target_widget.color_palette = colormap
|
342
tests/unit_tests/test_multi_waveform_next_gen.py
Normal file
342
tests/unit_tests/test_multi_waveform_next_gen.py
Normal file
@ -0,0 +1,342 @@
|
||||
import numpy as np
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform import MultiWaveform
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
##################################################
|
||||
# MultiWaveform widget base functionality tests
|
||||
##################################################
|
||||
|
||||
|
||||
def test_multiwaveform_initialization(qtbot, mocked_client):
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
|
||||
assert mw.objectName() == "MultiWaveform"
|
||||
# Inherited from PlotBase
|
||||
assert mw.title == ""
|
||||
assert mw.x_label == ""
|
||||
assert mw.y_label == ""
|
||||
# No crosshair or FPS monitor by default
|
||||
assert mw.crosshair is None
|
||||
assert mw.fps_monitor is None
|
||||
# No curves initially
|
||||
assert len(mw.plot_item.curves) == 0
|
||||
# Multiwaveform specific
|
||||
assert mw.monitor is None
|
||||
assert mw.color_palette == "magma"
|
||||
assert mw.max_trace == 200
|
||||
assert mw.flush_buffer is False
|
||||
assert mw.highlight_last_curve is True
|
||||
assert mw.opacity == 50
|
||||
assert mw.scan_id is None
|
||||
assert mw.highlighted_index == 0
|
||||
|
||||
|
||||
def test_multiwaveform_set_monitor(qtbot, mocked_client):
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
assert mw.monitor is None
|
||||
|
||||
# Set a monitor
|
||||
mw.plot("waveform1d")
|
||||
assert mw.monitor == "waveform1d"
|
||||
assert mw.config.monitor == "waveform1d"
|
||||
assert mw.connected is True
|
||||
|
||||
|
||||
def test_multiwaveform_set_properties(qtbot, mocked_client):
|
||||
"""Check that MultiWaveform properties can be set and retrieved correctly."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
|
||||
# Default checks
|
||||
assert mw.color_palette == "magma"
|
||||
assert mw.max_trace == 200
|
||||
assert mw.flush_buffer is False
|
||||
assert mw.highlight_last_curve is True
|
||||
assert mw.opacity == 50
|
||||
|
||||
# Change properties
|
||||
mw.color_palette = "viridis"
|
||||
mw.max_trace = 10
|
||||
mw.flush_buffer = True
|
||||
mw.highlight_last_curve = False
|
||||
mw.opacity = 75
|
||||
|
||||
# Verify that changes took effect
|
||||
assert mw.color_palette == "viridis"
|
||||
assert mw.max_trace == 10
|
||||
assert mw.flush_buffer is True
|
||||
assert mw.highlight_last_curve is False
|
||||
assert mw.opacity == 75
|
||||
|
||||
|
||||
def test_multiwaveform_curve_limit_no_flush(qtbot, mocked_client):
|
||||
"""Check that limiting the number of curves without flush simply hides older ones."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
mw.max_trace = 3
|
||||
mw.flush_buffer = False
|
||||
|
||||
# Simulate updates that create multiple curves
|
||||
for i in range(5):
|
||||
msg_data = {"data": np.array([i, i + 0.5, i + 1])}
|
||||
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||
|
||||
# There should be 5 curves in total, but only the last 3 are visible
|
||||
assert len(mw.curves) == 5
|
||||
visible_curves = [c for c in mw.curves if c.isVisible()]
|
||||
assert len(visible_curves) == 3
|
||||
|
||||
|
||||
def test_multiwaveform_curve_limit_flush(qtbot, mocked_client):
|
||||
"""Check that limiting the number of curves with flush removes older ones."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
mw.max_trace = 3
|
||||
mw.flush_buffer = True
|
||||
|
||||
# Simulate adding multiple curves
|
||||
for i in range(5):
|
||||
msg_data = {"data": np.array([i, i + 0.5, i + 1])}
|
||||
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||
|
||||
# Only 3 curves remain after flush
|
||||
assert len(mw.curves) == 3
|
||||
# They should match the last 3 that were inserted
|
||||
x_data, y_data = mw.curves[0].getData()
|
||||
assert np.array_equal(y_data, [2, 2.5, 3])
|
||||
x_data, y_data = mw.curves[1].getData()
|
||||
assert np.array_equal(y_data, [3, 3.5, 4])
|
||||
x_data, y_data = mw.curves[2].getData()
|
||||
assert np.array_equal(y_data, [4, 4.5, 5])
|
||||
|
||||
|
||||
def test_multiwaveform_highlight_last_curve(qtbot, mocked_client):
|
||||
"""Check highlight_last_curve behavior."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
mw.max_trace = 5
|
||||
mw.flush_buffer = False
|
||||
|
||||
# Simulate adding multiple curves
|
||||
for i in range(3):
|
||||
msg_data = {"data": np.array([i, i + 1, i + 2])}
|
||||
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||
|
||||
# Initially highlight_last_curve is True, so the last visible curve is highlighted
|
||||
# The highlight index should be -1 in the code's logic
|
||||
assert mw.highlight_last_curve is True
|
||||
|
||||
# Disable highlight_last_curve
|
||||
mw.highlight_last_curve = False
|
||||
|
||||
# Force highlight of the 1st visible curve (index 0 among visible)
|
||||
mw.set_curve_highlight(0)
|
||||
assert mw.highlighted_index == 0
|
||||
|
||||
|
||||
def test_multiwaveform_opacity_changes(qtbot, mocked_client):
|
||||
"""Check changing opacity affects existing curves."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
mw.plot("waveform1d")
|
||||
|
||||
# Add one curve
|
||||
msg_data = {"data": np.array([10, 20, 30])}
|
||||
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||
assert len(mw.curves) == 1
|
||||
|
||||
# Default opacity is 50
|
||||
assert mw.opacity == 50
|
||||
|
||||
# Change opacity
|
||||
mw.opacity = 80
|
||||
assert mw.opacity == 80
|
||||
|
||||
|
||||
def test_multiwaveform_set_colormap(qtbot, mocked_client):
|
||||
"""Check that setting a new colormap updates curve colors."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
mw.plot("waveform1d")
|
||||
|
||||
# Simulate multiple curve updates
|
||||
for i in range(3):
|
||||
msg_data = {"data": np.array([i, i + 1, i + 2])}
|
||||
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||
|
||||
# Default color_palette is "magma"
|
||||
assert mw.color_palette == "magma"
|
||||
# Now change to a new colormap
|
||||
mw.color_palette = "viridis"
|
||||
assert mw.color_palette == "viridis"
|
||||
|
||||
|
||||
def test_multiwaveform_simulate_updates(qtbot, mocked_client):
|
||||
"""Simulate a series of 1D updates to ensure the data is appended and the correct number of curves appear."""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
mw.plot("waveform1d")
|
||||
|
||||
data_series = [np.random.rand(5), np.random.rand(5), np.random.rand(5)]
|
||||
for idx, arr in enumerate(data_series):
|
||||
msg_data = {"data": arr}
|
||||
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_99"})
|
||||
# Each update should add a new curve
|
||||
assert len(mw.curves) == idx + 1
|
||||
x_data, y_data = mw.curves[-1].getData()
|
||||
assert np.array_equal(y_data, arr)
|
||||
|
||||
# Check that the scan_id was updated
|
||||
assert mw.scan_id == "scan_99"
|
||||
|
||||
|
||||
##################################################
|
||||
# MultiWaveform control panel and toolbar
|
||||
##################################################
|
||||
|
||||
|
||||
def test_control_panel_updates_widget(qtbot, mocked_client):
|
||||
"""
|
||||
Interact with the control panel’s UI elements and confirm the widget’s properties are updated.
|
||||
"""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
|
||||
assert mw.opacity == 50
|
||||
assert mw.flush_buffer is False
|
||||
assert mw.max_trace == 200
|
||||
assert mw.highlight_last_curve is True
|
||||
|
||||
mw.controls.ui.opacity.setValue(80)
|
||||
assert mw.opacity == 80
|
||||
|
||||
mw.controls.ui.flush_buffer.setChecked(True)
|
||||
assert mw.flush_buffer is True
|
||||
|
||||
mw.controls.ui.max_trace.setValue(12)
|
||||
assert mw.max_trace == 12
|
||||
|
||||
mw.controls.ui.highlight_last_curve.setChecked(False)
|
||||
assert mw.highlight_last_curve is False
|
||||
|
||||
|
||||
def test_widget_updates_control_panel(qtbot, mocked_client):
|
||||
"""
|
||||
Change properties directly on the MultiWaveform and verify the control panel UI reflects those changes.
|
||||
"""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
|
||||
mw.opacity = 25
|
||||
qtbot.wait(100)
|
||||
assert mw.controls.ui.opacity.value() == 25
|
||||
|
||||
mw.flush_buffer = True
|
||||
qtbot.wait(100)
|
||||
assert mw.controls.ui.flush_buffer.isChecked() is True
|
||||
|
||||
mw.max_trace = 9
|
||||
qtbot.wait(100)
|
||||
assert mw.controls.ui.max_trace.value() == 9
|
||||
|
||||
mw.highlight_last_curve = False
|
||||
qtbot.wait(100)
|
||||
assert mw.controls.ui.highlight_last_curve.isChecked() is False
|
||||
|
||||
|
||||
def test_selection_toolbar_updates_widget(qtbot, mocked_client):
|
||||
"""
|
||||
Confirm that selecting a monitor and a colormap from the selection toolbar
|
||||
updates the widget properties.
|
||||
"""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
toolbar = mw.monitor_selection_bundle
|
||||
monitor_combo = toolbar.monitor
|
||||
colormap_widget = toolbar.colormap_widget
|
||||
|
||||
monitor_combo.addItem("waveform1d")
|
||||
monitor_combo.setCurrentText("waveform1d")
|
||||
assert mw.monitor == "waveform1d"
|
||||
|
||||
colormap_widget.colormap = "viridis"
|
||||
assert mw.color_palette == "viridis"
|
||||
|
||||
|
||||
def test_control_panel_opacity_slider_spinbox(qtbot, mocked_client):
|
||||
"""
|
||||
Verify that when the user moves the opacity slider or spinbox, the widget's
|
||||
opacity property updates, and vice versa. Also confirm they stay in sync.
|
||||
"""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
|
||||
slider_opacity = mw.controls.ui.opacity
|
||||
spinbox_opacity = mw.controls.ui.spinbox_opacity
|
||||
|
||||
# Default
|
||||
assert mw.opacity == 50
|
||||
assert slider_opacity.value() == 50
|
||||
assert spinbox_opacity.value() == 50
|
||||
|
||||
# Move the slider
|
||||
slider_opacity.setValue(75)
|
||||
assert mw.opacity == 75
|
||||
assert spinbox_opacity.value() == 75
|
||||
|
||||
# Move the spinbox
|
||||
spinbox_opacity.setValue(20)
|
||||
assert mw.opacity == 20
|
||||
assert slider_opacity.value() == 20
|
||||
|
||||
mw.opacity = 95
|
||||
qtbot.wait(100)
|
||||
assert slider_opacity.value() == 95
|
||||
assert spinbox_opacity.value() == 95
|
||||
|
||||
|
||||
def test_control_panel_highlight_slider_spinbox(qtbot, mocked_client):
|
||||
"""
|
||||
Test that the slider and spinbox for curve highlighting update
|
||||
the widget’s highlighted_index property, and are disabled if
|
||||
highlight_last_curve is True.
|
||||
"""
|
||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||
|
||||
slider_index = mw.controls.ui.highlighted_index
|
||||
spinbox_index = mw.controls.ui.spinbox_index
|
||||
checkbox_highlight_last = mw.controls.ui.highlight_last_curve
|
||||
|
||||
# By default highlight_last_curve is True, so slider/spinbox are disabled:
|
||||
assert checkbox_highlight_last.isChecked() is True
|
||||
assert not slider_index.isEnabled()
|
||||
assert not spinbox_index.isEnabled()
|
||||
|
||||
# Uncheck highlight_last_curve -> slider/spinbox become enabled
|
||||
checkbox_highlight_last.setChecked(False)
|
||||
assert checkbox_highlight_last.isChecked() is False
|
||||
assert slider_index.isEnabled()
|
||||
assert spinbox_index.isEnabled()
|
||||
|
||||
# Simulate a few curves so there's something to highlight
|
||||
data_arrays = [np.array([0, 1, 2]), np.array([3, 4, 5]), np.array([6, 7, 8])]
|
||||
for arr in data_arrays:
|
||||
mw.on_monitor_1d_update({"data": arr}, {"scan_id": "scan_123"})
|
||||
|
||||
# The number_of_visible_curves == 3 now
|
||||
max_index = mw.number_of_visible_curves - 1
|
||||
assert max_index == 2
|
||||
|
||||
# Move the slider to index 1
|
||||
slider_index.setValue(1)
|
||||
assert mw.highlighted_index == 1
|
||||
assert spinbox_index.value() == 1
|
||||
|
||||
# Move the spinbox to index 2
|
||||
spinbox_index.setValue(2)
|
||||
assert mw.highlighted_index == 2
|
||||
assert slider_index.value() == 2
|
||||
|
||||
# Directly set mw.highlighted_index
|
||||
mw.highlighted_index = 0
|
||||
qtbot.wait(100)
|
||||
assert slider_index.value() == 0
|
||||
assert spinbox_index.value() == 0
|
||||
|
||||
# Re-check highlight_last_curve -> slider/spinbox disabled again
|
||||
checkbox_highlight_last.setChecked(True)
|
||||
assert not slider_index.isEnabled()
|
||||
assert not spinbox_index.isEnabled()
|
||||
assert mw.highlighted_index == 2
|
Reference in New Issue
Block a user