0
0
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:
2024-09-06 08:18:02 +02:00
parent 6b3ea0101e
commit 7bdca84314
4 changed files with 291 additions and 12 deletions

View File

@ -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

View 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()

View File

@ -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."""

View File

@ -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):
"""