mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat: add roi select for dap, allow automatic clear curves on plot request
This commit is contained in:
@ -1887,7 +1887,7 @@ class BECWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict | pd.DataFrame":
|
||||
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict":
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
@ -2050,6 +2050,25 @@ class BECWaveform(RPCBase):
|
||||
size(int): Font size of the legend.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def toggle_roi(self, toggled: "bool") -> "None":
|
||||
"""
|
||||
Toggle the linear region selector on the plot.
|
||||
|
||||
Args:
|
||||
toggled(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def select_roi(self, region: "tuple[float, float]"):
|
||||
"""
|
||||
Set the fit region of the plot widget. At the moment only a single region is supported.
|
||||
To remove the roi region again, use toggle_roi_region
|
||||
|
||||
Args:
|
||||
region(tuple[float, float]): The fit region.
|
||||
"""
|
||||
|
||||
|
||||
class BECWaveformWidget(RPCBase):
|
||||
@property
|
||||
@ -2321,6 +2340,24 @@ class BECWaveformWidget(RPCBase):
|
||||
Export the plot widget to Matplotlib.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def toggle_roi(self, checked: "bool"):
|
||||
"""
|
||||
Toggle the linear region selector.
|
||||
|
||||
Args:
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def select_roi(self, region: "tuple"):
|
||||
"""
|
||||
Set the region of interest of the plot widget.
|
||||
|
||||
Args:
|
||||
region(tuple): Region of interest.
|
||||
"""
|
||||
|
||||
|
||||
class DapComboBox(RPCBase):
|
||||
@rpc_call
|
||||
|
72
bec_widgets/utils/linear_region_selector.py
Normal file
72
bec_widgets/utils/linear_region_selector.py
Normal file
@ -0,0 +1,72 @@
|
||||
""" Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||
The class is mainly designed for usage with the BECWaveform and 1D plots. """
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
|
||||
class LinearRegionWrapper(QObject):
|
||||
"""Wrapper class for the LinearRegionItem in pyqtgraph for 1D plots (BECWaveform)
|
||||
|
||||
Args:
|
||||
plot_item (pg.PlotItem): The plot item to add the region selector to.
|
||||
parent (QObject): The parent object.
|
||||
color (QColor): The color of the region selector.
|
||||
hover_color (QColor): The color of the region selector when the mouse is over it.
|
||||
"""
|
||||
|
||||
# Signal with the region tuble (start, end)
|
||||
region_changed = Signal(tuple)
|
||||
|
||||
def __init__(
|
||||
self, plot_item: pg.PlotItem, color: QColor = None, hover_color: QColor = None, parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._edge_width = 2
|
||||
self.plot_item = plot_item
|
||||
self.linear_region_selector = pg.LinearRegionItem()
|
||||
self.proxy = None
|
||||
self.change_roi_color((color, hover_color))
|
||||
|
||||
# Slot for changing the color of the region selector (edge and fill)
|
||||
@Slot(tuple)
|
||||
def change_roi_color(self, colors: tuple[QColor | str | tuple, QColor | str | tuple]):
|
||||
"""Change the color and hover color of the region selector.
|
||||
Hover color means the color when the mouse is over the region.
|
||||
|
||||
Args:
|
||||
colors (tuple): Tuple with the color and hover color
|
||||
"""
|
||||
color, hover_color = colors
|
||||
if color is not None:
|
||||
self.linear_region_selector.setBrush(pg.mkBrush(color))
|
||||
if hover_color is not None:
|
||||
self.linear_region_selector.setHoverBrush(pg.mkBrush(hover_color))
|
||||
|
||||
@Slot()
|
||||
def add_region_selector(self):
|
||||
"""Add the region selector to the plot item"""
|
||||
self.plot_item.addItem(self.linear_region_selector)
|
||||
# Use proxy to limit the update rate of the region change signal to 10Hz
|
||||
self.proxy = pg.SignalProxy(
|
||||
self.linear_region_selector.sigRegionChanged,
|
||||
rateLimit=10,
|
||||
slot=self._region_change_proxy,
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def remove_region_selector(self):
|
||||
"""Remove the region selector from the plot item"""
|
||||
self.proxy.disconnect()
|
||||
self.proxy = None
|
||||
self.plot_item.removeItem(self.linear_region_selector)
|
||||
|
||||
def _region_change_proxy(self):
|
||||
"""Emit the region change signal"""
|
||||
region = self.linear_region_selector.getRegion()
|
||||
self.region_changed.emit(region)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self.remove_region_selector()
|
@ -16,6 +16,7 @@ from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import Colors, EntryValidator
|
||||
from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
||||
BECCurve,
|
||||
@ -74,6 +75,8 @@ class BECWaveform(BECPlotBase):
|
||||
"remove",
|
||||
"clear_all",
|
||||
"set_legend_label_size",
|
||||
"toggle_roi",
|
||||
"select_roi",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
async_signal_update = pyqtSignal()
|
||||
@ -81,6 +84,9 @@ class BECWaveform(BECPlotBase):
|
||||
dap_summary_update = pyqtSignal(dict, dict)
|
||||
autorange_signal = pyqtSignal()
|
||||
new_scan = pyqtSignal()
|
||||
roi_changed = pyqtSignal(tuple)
|
||||
roi_active = pyqtSignal(bool)
|
||||
request_dap_refresh = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -100,6 +106,8 @@ class BECWaveform(BECPlotBase):
|
||||
self.old_scan_id = None
|
||||
self.scan_id = None
|
||||
self.scan_item = None
|
||||
self._roi_region = None
|
||||
self.roi_select = None
|
||||
self._x_axis_mode = {
|
||||
"name": None,
|
||||
"entry": None,
|
||||
@ -130,6 +138,57 @@ class BECWaveform(BECPlotBase):
|
||||
self.add_legend()
|
||||
self.apply_config(self.config)
|
||||
|
||||
@Slot(bool)
|
||||
def toggle_roi(self, toggled: bool) -> None:
|
||||
"""Toggle the linear region selector on the plot.
|
||||
|
||||
Args:
|
||||
toggled(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
if toggled:
|
||||
return self._hook_roi()
|
||||
return self._unhook_roi()
|
||||
|
||||
@Slot(tuple)
|
||||
def select_roi(self, region: tuple[float, float]):
|
||||
"""Set the fit region of the plot widget. At the moment only a single region is supported.
|
||||
To remove the roi region again, use toggle_roi_region
|
||||
|
||||
Args:
|
||||
region(tuple[float, float]): The fit region.
|
||||
"""
|
||||
if self.roi_region == (None, None):
|
||||
self.toggle_roi(True)
|
||||
try:
|
||||
self.roi_select.linear_region_selector.setRegion(region)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting region {tuple}; Exception raised: {e}")
|
||||
raise ValueError(f"Error setting region {tuple}; Exception raised: {e}")
|
||||
|
||||
def _hook_roi(self):
|
||||
"""Hook the linear region selector to the plot."""
|
||||
if self.roi_select is None:
|
||||
self.roi_select = LinearRegionWrapper(self.plot_item, parent=self)
|
||||
self.roi_select.add_region_selector()
|
||||
self.roi_select.region_changed.connect(self.roi_changed)
|
||||
self.roi_select.region_changed.connect(self.set_roi_region)
|
||||
self.request_dap_refresh.connect(self.refresh_dap)
|
||||
self._emit_roi_region()
|
||||
self.roi_active.emit(True)
|
||||
|
||||
def _unhook_roi(self):
|
||||
"""Unhook the linear region selector from the plot."""
|
||||
if self.roi_select is not None:
|
||||
self.roi_select.region_changed.disconnect(self.roi_changed)
|
||||
self.roi_select.region_changed.disconnect(self.set_roi_region)
|
||||
self.request_dap_refresh.disconnect(self.refresh_dap)
|
||||
self.roi_active.emit(False)
|
||||
self.roi_region = None
|
||||
self.refresh_dap()
|
||||
self.roi_select.cleanup()
|
||||
self.roi_select.deleteLater()
|
||||
self.roi_select = None
|
||||
|
||||
def apply_config(self, config: dict | SubplotConfig, replot_last_scan: bool = False):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
@ -171,6 +230,48 @@ class BECWaveform(BECPlotBase):
|
||||
for curve in self.curves:
|
||||
curve.config.parent_id = new_gui_id
|
||||
|
||||
###################################
|
||||
# Fit Range Properties
|
||||
###################################
|
||||
|
||||
@property
|
||||
def roi_region(self) -> tuple[float, float] | None:
|
||||
"""
|
||||
Get the fit region of the plot widget.
|
||||
|
||||
Returns:
|
||||
tuple: The fit region.
|
||||
"""
|
||||
if self._roi_region is not None:
|
||||
return self._roi_region
|
||||
return None, None
|
||||
|
||||
@roi_region.setter
|
||||
def roi_region(self, value: tuple[float, float] | None):
|
||||
"""Set the fit region of the plot widget.
|
||||
|
||||
Args:
|
||||
value(tuple[float, float]|None): The fit region.
|
||||
"""
|
||||
self._roi_region = value
|
||||
if value is not None:
|
||||
self.request_dap_refresh.emit()
|
||||
|
||||
@Slot(tuple)
|
||||
def set_roi_region(self, region: tuple[float, float]):
|
||||
"""
|
||||
Set the fit region of the plot widget.
|
||||
|
||||
Args:
|
||||
region(tuple[float, float]): The fit region.
|
||||
"""
|
||||
self.roi_region = region
|
||||
|
||||
def _emit_roi_region(self):
|
||||
"""Emit the current ROI from selector the plot widget."""
|
||||
if self.roi_select is not None:
|
||||
self.set_roi_region(self.roi_select.linear_region_selector.getRegion())
|
||||
|
||||
###################################
|
||||
# Waveform Properties
|
||||
###################################
|
||||
@ -1058,13 +1159,14 @@ class BECWaveform(BECPlotBase):
|
||||
y_entry = curve.config.signals.y.entry
|
||||
model_name = curve.config.signals.dap
|
||||
model = getattr(self.dap, model_name)
|
||||
x_min, x_max = self.roi_region
|
||||
|
||||
msg = messages.DAPRequestMessage(
|
||||
dap_cls="LmfitService1D",
|
||||
dap_type="on_demand",
|
||||
config={
|
||||
"args": [self.scan_id, x_name, x_entry, y_name, y_entry],
|
||||
"kwargs": {},
|
||||
"kwargs": {"x_min": x_min, "x_max": x_max},
|
||||
"class_args": model._plugin_info["class_args"],
|
||||
"class_kwargs": model._plugin_info["class_kwargs"],
|
||||
},
|
||||
@ -1304,7 +1406,8 @@ class BECWaveform(BECPlotBase):
|
||||
self.scan_signal_update.emit()
|
||||
self.async_signal_update.emit()
|
||||
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict | pd.DataFrame:
|
||||
# pylint: ignore: undefined-variable
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: # | pd.DataFrame:
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
@ -1351,13 +1454,21 @@ class BECWaveform(BECPlotBase):
|
||||
"""
|
||||
MatplotlibExporter(self.plot_item).export()
|
||||
|
||||
def clear_all(self):
|
||||
def clear_source(self, source: Literal["DAP", "async", "scan_segment", "custom"]):
|
||||
"""Clear speicific source from self._curves_data.
|
||||
|
||||
Args:
|
||||
source (Literal["DAP", "async", "scan_segment", "custom"]): Source to be cleared.
|
||||
"""
|
||||
curves_data = self._curves_data
|
||||
sources = list(curves_data.keys())
|
||||
curve_ids_to_remove = list(curves_data[source].keys())
|
||||
for curve_id in curve_ids_to_remove:
|
||||
self.remove_curve(curve_id)
|
||||
|
||||
def clear_all(self):
|
||||
sources = list(self._curves_data.keys())
|
||||
for source in sources:
|
||||
curve_ids_to_remove = list(curves_data[source].keys())
|
||||
for curve_id in curve_ids_to_remove:
|
||||
self.remove_curve(curve_id)
|
||||
self.clear_source(source)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget connection from BECDispatcher."""
|
||||
|
@ -6,7 +6,7 @@ from typing import Literal
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
@ -55,6 +55,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"export_to_matplotlib",
|
||||
"toggle_roi",
|
||||
"select_roi",
|
||||
]
|
||||
scan_signal_update = Signal()
|
||||
async_signal_update = Signal()
|
||||
@ -70,6 +72,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
crosshair_coordinates_changed_string = Signal(str)
|
||||
crosshair_coordinates_clicked = Signal(tuple)
|
||||
crosshair_coordinates_clicked_string = Signal(str)
|
||||
roi_changed = Signal(tuple)
|
||||
roi_active = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -120,6 +124,9 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"crosshair": MaterialIconAction(
|
||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||
),
|
||||
"roi_select": MaterialIconAction(
|
||||
icon_name="select_all", tooltip="Add ROI region for DAP", checkable=True
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
@ -133,6 +140,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.waveform.apply_config(config)
|
||||
|
||||
self.config = config
|
||||
self._clear_curves_on_plot_update = False
|
||||
|
||||
self.hook_waveform_signals()
|
||||
self._hook_actions()
|
||||
@ -160,6 +168,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.waveform.crosshair_position_clicked.connect(
|
||||
self._emit_crosshair_position_clicked_string
|
||||
)
|
||||
self.waveform.roi_changed.connect(self.roi_changed)
|
||||
self.waveform.roi_active.connect(self.roi_active)
|
||||
|
||||
def _hook_actions(self):
|
||||
self.toolbar.widgets["save"].action.triggered.connect(self.export)
|
||||
@ -173,6 +183,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["fit_params"].action.triggered.connect(self.show_fit_summary_dialog)
|
||||
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
||||
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
|
||||
self.toolbar.widgets["roi_select"].action.toggled.connect(self.waveform.toggle_roi)
|
||||
# self.toolbar.widgets["import"].action.triggered.connect(
|
||||
# lambda: self.load_config(path=None, gui=True)
|
||||
# )
|
||||
@ -180,6 +191,29 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
# lambda: self.save_config(path=None, gui=True)
|
||||
# )
|
||||
|
||||
@Slot(bool)
|
||||
def toogle_roi_select(self, checked: bool):
|
||||
"""Toggle the linear region selector.
|
||||
|
||||
Args:
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
self.toolbar.widgets["roi_select"].action.setChecked(checked)
|
||||
|
||||
@Property(bool)
|
||||
def clear_curves_on_plot_update(self) -> bool:
|
||||
"""If True, clear curves on plot update."""
|
||||
return self._clear_curves_on_plot_update
|
||||
|
||||
@clear_curves_on_plot_update.setter
|
||||
def clear_curves_on_plot_update(self, value: bool):
|
||||
"""Set the clear curves on plot update property.
|
||||
|
||||
Args:
|
||||
value(bool): If True, clear curves on plot update.
|
||||
"""
|
||||
self._clear_curves_on_plot_update = value
|
||||
|
||||
@SafeSlot(tuple)
|
||||
def _emit_crosshair_coordinates_changed_string(self, coordinates):
|
||||
self.crosshair_coordinates_changed_string.emit(str(coordinates))
|
||||
@ -260,7 +294,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_colormap(colormap)
|
||||
|
||||
@SafeSlot(str, popup_error=True)
|
||||
@Slot(str, str) # Slot for x_name, x_entry
|
||||
@SafeSlot(str, popup_error=True) # Slot for x_name and
|
||||
def set_x(self, x_name: str, x_entry: str | None = None):
|
||||
"""
|
||||
Change the x axis of the plot widget.
|
||||
@ -275,7 +310,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_x(x_name, x_entry)
|
||||
|
||||
@SafeSlot(str, popup_error=True)
|
||||
@Slot(str) # Slot for y_name
|
||||
@SafeSlot(popup_error=True)
|
||||
def plot(
|
||||
self,
|
||||
arg1: list | np.ndarray | str | None = None,
|
||||
@ -315,7 +351,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
# self._check_if_scans_have_same_x(enabled=True, x_name_to_check=x_name)
|
||||
if self.clear_curves_on_plot_update is True:
|
||||
self.waveform.clear_source(source="scan_segment")
|
||||
return self.waveform.plot(
|
||||
arg1=arg1,
|
||||
x=x,
|
||||
@ -334,6 +371,9 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@Slot(
|
||||
str, str, str, str, str, str, bool
|
||||
) # Slot for x_name, y_name, x_entry, y_entry, color, validate_bec
|
||||
@SafeSlot(str, str, str, popup_error=True)
|
||||
def add_dap(
|
||||
self,
|
||||
@ -362,6 +402,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
if self.clear_curves_on_plot_update is True:
|
||||
self.waveform.clear_source(source="DAP")
|
||||
return self.waveform.add_dap(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
@ -543,6 +585,23 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_auto_range(enabled, axis)
|
||||
|
||||
def toggle_roi(self, checked: bool):
|
||||
"""Toggle the linear region selector.
|
||||
|
||||
Args:
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
self.waveform.toggle_roi(checked)
|
||||
|
||||
def select_roi(self, region: tuple):
|
||||
"""
|
||||
Set the region of interest of the plot widget.
|
||||
|
||||
Args:
|
||||
region(tuple): Region of interest.
|
||||
"""
|
||||
self.waveform.select_roi(region)
|
||||
|
||||
@SafeSlot()
|
||||
def _auto_range_from_toolbar(self):
|
||||
"""
|
||||
|
Reference in New Issue
Block a user