mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
fix(crosshair): fixed crosshair for image and waveforms
This commit is contained in:
@ -3,8 +3,9 @@ import os
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from bec_qthemes import material_icon
|
||||||
from qtpy.QtCore import QSize
|
from qtpy.QtCore import QSize
|
||||||
from qtpy.QtGui import QAction, QIcon
|
from qtpy.QtGui import QAction, QGuiApplication, QIcon
|
||||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
|
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
|
||||||
|
|
||||||
import bec_widgets
|
import bec_widgets
|
||||||
@ -70,6 +71,33 @@ class IconAction(ToolBarAction):
|
|||||||
toolbar.addAction(self.action)
|
toolbar.addAction(self.action)
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialIconAction:
|
||||||
|
"""
|
||||||
|
Abstract base class for toolbar actions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
|
||||||
|
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
||||||
|
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, icon_name: str = None, tooltip: str = None, checkable: bool = False):
|
||||||
|
self.icon_name = icon_name
|
||||||
|
self.tooltip = tooltip
|
||||||
|
self.checkable = checkable
|
||||||
|
self.action = None
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
palette = QGuiApplication.palette()
|
||||||
|
color = "#FFFFFF" # FIXME: This should be a theme color but the toolbar doesn't respect the theme atm
|
||||||
|
# one fixed, change it to palette.toolTipBase().color()
|
||||||
|
|
||||||
|
icon = material_icon(self.icon_name, size=(20, 20), color=color)
|
||||||
|
self.action = QAction(QIcon(icon), self.tooltip, target)
|
||||||
|
self.action.setCheckable(self.checkable)
|
||||||
|
toolbar.addAction(self.action)
|
||||||
|
|
||||||
|
|
||||||
class DeviceSelectionAction(ToolBarAction):
|
class DeviceSelectionAction(ToolBarAction):
|
||||||
"""
|
"""
|
||||||
Action for selecting a device in a combobox.
|
Action for selecting a device in a combobox.
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
# from qtpy.QtCore import QObject, pyqtSignal
|
# from qtpy.QtCore import QObject, pyqtSignal
|
||||||
from qtpy.QtCore import QObject
|
from qtpy.QtCore import QObject, Qt
|
||||||
from qtpy.QtCore import Signal as pyqtSignal
|
from qtpy.QtCore import Signal as pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
@ -26,10 +28,13 @@ class Crosshair(QObject):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.is_log_y = None
|
self.is_log_y = None
|
||||||
self.is_log_x = None
|
self.is_log_x = None
|
||||||
|
self.is_derivative = None
|
||||||
self.plot_item = plot_item
|
self.plot_item = plot_item
|
||||||
self.precision = precision
|
self.precision = precision
|
||||||
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
||||||
|
self.v_line.skip_auto_range = True
|
||||||
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
||||||
|
self.h_line.skip_auto_range = True
|
||||||
self.plot_item.addItem(self.v_line, ignoreBounds=True)
|
self.plot_item.addItem(self.v_line, ignoreBounds=True)
|
||||||
self.plot_item.addItem(self.h_line, ignoreBounds=True)
|
self.plot_item.addItem(self.h_line, ignoreBounds=True)
|
||||||
self.proxy = pg.SignalProxy(
|
self.proxy = pg.SignalProxy(
|
||||||
@ -37,6 +42,10 @@ class Crosshair(QObject):
|
|||||||
)
|
)
|
||||||
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
||||||
|
|
||||||
|
self.plot_item.ctrl.derivativeCheck.checkStateChanged.connect(self.check_derivatives)
|
||||||
|
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||||
|
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||||
|
|
||||||
# Initialize markers
|
# Initialize markers
|
||||||
self.marker_moved_1d = []
|
self.marker_moved_1d = []
|
||||||
self.marker_clicked_1d = []
|
self.marker_clicked_1d = []
|
||||||
@ -53,8 +62,8 @@ class Crosshair(QObject):
|
|||||||
self.plot_item.removeItem(self.marker_2d)
|
self.plot_item.removeItem(self.marker_2d)
|
||||||
|
|
||||||
# Create new markers
|
# Create new markers
|
||||||
self.marker_moved_1d = []
|
self.marker_moved_1d = {}
|
||||||
self.marker_clicked_1d = []
|
self.marker_clicked_1d = {}
|
||||||
self.marker_2d = None
|
self.marker_2d = None
|
||||||
for item in self.plot_item.items:
|
for item in self.plot_item.items:
|
||||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||||
@ -63,48 +72,48 @@ class Crosshair(QObject):
|
|||||||
marker_moved = pg.ScatterPlotItem(
|
marker_moved = pg.ScatterPlotItem(
|
||||||
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
||||||
)
|
)
|
||||||
marker_clicked = pg.ScatterPlotItem(
|
marker_moved.skip_auto_range = True
|
||||||
size=10, pen=pg.mkPen(None), brush=pg.mkBrush(color)
|
self.marker_moved_1d[item.name()] = marker_moved
|
||||||
)
|
|
||||||
self.marker_moved_1d.append(marker_moved)
|
|
||||||
self.plot_item.addItem(marker_moved)
|
self.plot_item.addItem(marker_moved)
|
||||||
|
|
||||||
# Create glowing effect markers for clicked events
|
# Create glowing effect markers for clicked events
|
||||||
marker_clicked_list = []
|
|
||||||
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
||||||
marker_clicked = pg.ScatterPlotItem(
|
marker_clicked = pg.ScatterPlotItem(
|
||||||
size=size,
|
size=size,
|
||||||
pen=pg.mkPen(None),
|
pen=pg.mkPen(None),
|
||||||
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
||||||
)
|
)
|
||||||
marker_clicked_list.append(marker_clicked)
|
marker_clicked.skip_auto_range = True
|
||||||
|
self.marker_clicked_1d[item.name()] = marker_clicked
|
||||||
self.plot_item.addItem(marker_clicked)
|
self.plot_item.addItem(marker_clicked)
|
||||||
|
|
||||||
self.marker_clicked_1d.append(marker_clicked_list)
|
|
||||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||||
self.marker_2d = pg.ROI(
|
self.marker_2d = pg.ROI(
|
||||||
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
||||||
)
|
)
|
||||||
self.plot_item.addItem(self.marker_2d)
|
self.plot_item.addItem(self.marker_2d)
|
||||||
|
|
||||||
def snap_to_data(self, x, y) -> tuple:
|
def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]:
|
||||||
"""
|
"""
|
||||||
Finds the nearest data points to the given x and y coordinates.
|
Finds the nearest data points to the given x and y coordinates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
x: The x-coordinate
|
x: The x-coordinate of the mouse cursor
|
||||||
y: The y-coordinate
|
y: The y-coordinate of the mouse cursor
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: The nearest x and y values
|
tuple: x and y values snapped to the nearest data
|
||||||
"""
|
"""
|
||||||
y_values_1d = []
|
y_values = defaultdict(list)
|
||||||
x_values_1d = []
|
x_values = defaultdict(list)
|
||||||
image_2d = None
|
image_2d = None
|
||||||
|
|
||||||
# Iterate through items in the plot
|
# Iterate through items in the plot
|
||||||
for item in self.plot_item.items:
|
for item in self.plot_item.items:
|
||||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||||
x_data, y_data = item.xData, item.yData
|
name = item.name()
|
||||||
|
plot_data = item._getDisplayDataset()
|
||||||
|
x_data, y_data = plot_data.x, plot_data.y
|
||||||
if x_data is not None and y_data is not None:
|
if x_data is not None and y_data is not None:
|
||||||
if self.is_log_x:
|
if self.is_log_x:
|
||||||
min_x_data = np.min(x_data[x_data > 0])
|
min_x_data = np.min(x_data[x_data > 0])
|
||||||
@ -112,25 +121,25 @@ class Crosshair(QObject):
|
|||||||
min_x_data = np.min(x_data)
|
min_x_data = np.min(x_data)
|
||||||
max_x_data = np.max(x_data)
|
max_x_data = np.max(x_data)
|
||||||
if x < min_x_data or x > max_x_data:
|
if x < min_x_data or x > max_x_data:
|
||||||
return None, None
|
y_values[name] = None
|
||||||
|
x_values[name] = None
|
||||||
|
continue
|
||||||
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
||||||
y_values_1d.append(closest_y)
|
y_values[name] = closest_y
|
||||||
x_values_1d.append(closest_x)
|
x_values[name] = closest_x
|
||||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||||
|
name = item.config.monitor
|
||||||
image_2d = item.image
|
image_2d = item.image
|
||||||
|
# clip the x and y values to the image dimensions to avoid out of bounds errors
|
||||||
|
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||||
|
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||||
|
|
||||||
# Handle 1D plot
|
if x_values and y_values:
|
||||||
if y_values_1d:
|
if all(v is None for v in x_values.values()) or all(
|
||||||
if all(v is None for v in x_values_1d) or all(v is None for v in y_values_1d):
|
v is None for v in y_values.values()
|
||||||
|
):
|
||||||
return None, None
|
return None, None
|
||||||
closest_x = min(x_values_1d, key=lambda xi: abs(xi - x)) # Snap x to closest data point
|
return x_values, y_values
|
||||||
return closest_x, y_values_1d
|
|
||||||
|
|
||||||
# Handle 2D plot
|
|
||||||
if image_2d is not None:
|
|
||||||
x_idx = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
|
||||||
y_idx = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
|
||||||
return x_idx, y_idx
|
|
||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
@ -156,7 +165,6 @@ class Crosshair(QObject):
|
|||||||
Args:
|
Args:
|
||||||
event: The mouse moved event
|
event: The mouse moved event
|
||||||
"""
|
"""
|
||||||
self.check_log()
|
|
||||||
pos = event[0]
|
pos = event[0]
|
||||||
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
||||||
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
||||||
@ -168,27 +176,34 @@ class Crosshair(QObject):
|
|||||||
x = 10**x
|
x = 10**x
|
||||||
if self.is_log_y:
|
if self.is_log_y:
|
||||||
y = 10**y
|
y = 10**y
|
||||||
x, y_values = self.snap_to_data(x, y)
|
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||||
|
if x_snap_values is None or y_snap_values is None:
|
||||||
|
return
|
||||||
|
if all(v is None for v in x_snap_values.values()) or all(
|
||||||
|
v is None for v in y_snap_values.values()
|
||||||
|
):
|
||||||
|
# not sure how we got here, but just to be safe...
|
||||||
|
return
|
||||||
|
|
||||||
for item in self.plot_item.items:
|
for item in self.plot_item.items:
|
||||||
if isinstance(item, pg.PlotDataItem):
|
if isinstance(item, pg.PlotDataItem):
|
||||||
if x is None or all(v is None for v in y_values):
|
name = item.name()
|
||||||
return
|
x, y = x_snap_values[name], y_snap_values[name]
|
||||||
coordinate_to_emit = (
|
if x is None or y is None:
|
||||||
round(x, self.precision),
|
continue
|
||||||
[round(y_val, self.precision) for y_val in y_values],
|
self.marker_moved_1d[name].setData([x], [y])
|
||||||
)
|
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
||||||
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
||||||
for i, y_val in enumerate(y_values):
|
|
||||||
self.marker_moved_1d[i].setData(
|
|
||||||
[x if not self.is_log_x else np.log10(x)],
|
|
||||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
|
||||||
)
|
|
||||||
elif isinstance(item, pg.ImageItem):
|
elif isinstance(item, pg.ImageItem):
|
||||||
if x is None or y_values is None:
|
name = item.config.monitor
|
||||||
return
|
x, y = x_snap_values[name], y_snap_values[name]
|
||||||
coordinate_to_emit = (x, y_values)
|
if x is None or y is None:
|
||||||
|
continue
|
||||||
|
self.marker_2d.setPos([x, y])
|
||||||
|
coordinate_to_emit = (name, x, y)
|
||||||
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
def mouse_clicked(self, event):
|
def mouse_clicked(self, event):
|
||||||
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
||||||
@ -196,7 +211,11 @@ class Crosshair(QObject):
|
|||||||
Args:
|
Args:
|
||||||
event: The mouse clicked event
|
event: The mouse clicked event
|
||||||
"""
|
"""
|
||||||
self.check_log()
|
|
||||||
|
# we only accept left mouse clicks
|
||||||
|
if event.button() != Qt.MouseButton.LeftButton:
|
||||||
|
return
|
||||||
|
|
||||||
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
||||||
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
||||||
x, y = mouse_point.x(), mouse_point.y()
|
x, y = mouse_point.x(), mouse_point.y()
|
||||||
@ -205,31 +224,57 @@ class Crosshair(QObject):
|
|||||||
x = 10**x
|
x = 10**x
|
||||||
if self.is_log_y:
|
if self.is_log_y:
|
||||||
y = 10**y
|
y = 10**y
|
||||||
x, y_values = self.snap_to_data(x, y)
|
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||||
|
|
||||||
|
if x_snap_values is None or y_snap_values is None:
|
||||||
|
return
|
||||||
|
if all(v is None for v in x_snap_values.values()) or all(
|
||||||
|
v is None for v in y_snap_values.values()
|
||||||
|
):
|
||||||
|
# not sure how we got here, but just to be safe...
|
||||||
|
return
|
||||||
|
|
||||||
for item in self.plot_item.items:
|
for item in self.plot_item.items:
|
||||||
if isinstance(item, pg.PlotDataItem):
|
if isinstance(item, pg.PlotDataItem):
|
||||||
if x is None or all(v is None for v in y_values):
|
name = item.name()
|
||||||
return
|
x, y = x_snap_values[name], y_snap_values[name]
|
||||||
coordinate_to_emit = (
|
if x is None or y is None:
|
||||||
round(x, self.precision),
|
continue
|
||||||
[round(y_val, self.precision) for y_val in y_values],
|
self.marker_clicked_1d[name].setData([x], [y])
|
||||||
)
|
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
||||||
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
||||||
for i, y_val in enumerate(y_values):
|
|
||||||
for marker in self.marker_clicked_1d[i]:
|
|
||||||
marker.setData(
|
|
||||||
[x if not self.is_log_x else np.log10(x)],
|
|
||||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
|
||||||
)
|
|
||||||
elif isinstance(item, pg.ImageItem):
|
elif isinstance(item, pg.ImageItem):
|
||||||
if x is None or y_values is None:
|
name = item.config.monitor
|
||||||
return
|
x, y = x_snap_values[name], y_snap_values[name]
|
||||||
coordinate_to_emit = (x, y_values)
|
if x is None or y is None:
|
||||||
|
continue
|
||||||
|
self.marker_2d.setPos([x, y])
|
||||||
|
coordinate_to_emit = (name, x, y)
|
||||||
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
||||||
self.marker_2d.setPos([x, y_values])
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def clear_markers(self):
|
||||||
|
"""Clears the markers from the plot."""
|
||||||
|
for marker in self.marker_moved_1d.values():
|
||||||
|
marker.clear()
|
||||||
|
# marker.deleteLater()
|
||||||
|
for marker in self.marker_clicked_1d.values():
|
||||||
|
marker.clear()
|
||||||
|
# marker.deleteLater()
|
||||||
|
|
||||||
def check_log(self):
|
def check_log(self):
|
||||||
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
||||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
||||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
||||||
|
self.clear_markers()
|
||||||
|
|
||||||
|
def check_derivatives(self):
|
||||||
|
"""Checks if the derivatives are enabled and updates the internal state accordingly."""
|
||||||
|
self.is_derivative = self.plot_item.ctrl.derivativeCheck.isChecked()
|
||||||
|
self.clear_markers()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.v_line.deleteLater()
|
||||||
|
self.h_line.deleteLater()
|
||||||
|
self.clear_markers()
|
||||||
|
@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
|
|||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||||
|
from bec_widgets.utils.crosshair import Crosshair
|
||||||
|
|
||||||
|
|
||||||
class AxisConfig(BaseModel):
|
class AxisConfig(BaseModel):
|
||||||
@ -41,6 +42,18 @@ class SubplotConfig(ConnectionConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BECViewBox(pg.ViewBox):
|
||||||
|
|
||||||
|
def itemBoundsChanged(self, item):
|
||||||
|
self._itemBoundsCache.pop(item, None)
|
||||||
|
if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
|
||||||
|
# check if the call is coming from a mouse-move event
|
||||||
|
if hasattr(item, "skip_auto_range") and item.skip_auto_range:
|
||||||
|
return
|
||||||
|
self._autoRangeNeedsUpdate = True
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||||
USER_ACCESS = [
|
USER_ACCESS = [
|
||||||
"_config_dict",
|
"_config_dict",
|
||||||
@ -73,9 +86,13 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
|||||||
pg.GraphicsLayout.__init__(self, parent)
|
pg.GraphicsLayout.__init__(self, parent)
|
||||||
|
|
||||||
self.figure = parent_figure
|
self.figure = parent_figure
|
||||||
self.plot_item = self.addPlot(row=0, col=0)
|
|
||||||
|
# self.plot_item = self.addPlot(row=0, col=0)
|
||||||
|
self.plot_item = pg.PlotItem(viewBox=BECViewBox(parent=self, enableMenu=True), parent=self)
|
||||||
|
self.addItem(self.plot_item, row=0, col=0)
|
||||||
|
|
||||||
self.add_legend()
|
self.add_legend()
|
||||||
|
self.crosshair = None
|
||||||
|
|
||||||
def set(self, **kwargs) -> None:
|
def set(self, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
@ -304,6 +321,25 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
|||||||
"""
|
"""
|
||||||
self.plot_item.enableAutoRange(axis, enabled)
|
self.plot_item.enableAutoRange(axis, enabled)
|
||||||
|
|
||||||
|
def hook_crosshair(self) -> None:
|
||||||
|
"""Hook the crosshair to all plots."""
|
||||||
|
if self.crosshair is None:
|
||||||
|
self.crosshair = Crosshair(self.plot_item, precision=3)
|
||||||
|
|
||||||
|
def unhook_crosshair(self) -> None:
|
||||||
|
"""Unhook the crosshair from all plots."""
|
||||||
|
if self.crosshair is not None:
|
||||||
|
self.crosshair.cleanup()
|
||||||
|
self.crosshair.deleteLater()
|
||||||
|
self.crosshair = None
|
||||||
|
|
||||||
|
def toggle_crosshair(self) -> None:
|
||||||
|
"""Toggle the crosshair on all plots."""
|
||||||
|
if self.crosshair is None:
|
||||||
|
return self.hook_crosshair()
|
||||||
|
|
||||||
|
self.unhook_crosshair()
|
||||||
|
|
||||||
def export(self):
|
def export(self):
|
||||||
"""Show the Export Dialog of the plot widget."""
|
"""Show the Export Dialog of the plot widget."""
|
||||||
scene = self.plot_item.scene()
|
scene = self.plot_item.scene()
|
||||||
@ -317,6 +353,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
|||||||
|
|
||||||
def cleanup_pyqtgraph(self):
|
def cleanup_pyqtgraph(self):
|
||||||
"""Cleanup pyqtgraph items."""
|
"""Cleanup pyqtgraph items."""
|
||||||
|
self.unhook_crosshair()
|
||||||
item = self.plot_item
|
item = self.plot_item
|
||||||
item.vb.menu.close()
|
item.vb.menu.close()
|
||||||
item.vb.menu.deleteLater()
|
item.vb.menu.deleteLater()
|
||||||
|
@ -14,7 +14,7 @@ from qtpy.QtCore import Signal as pyqtSignal
|
|||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||||
from bec_widgets.utils import Colors, EntryValidator
|
from bec_widgets.utils import Colors, Crosshair, EntryValidator
|
||||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
||||||
BECCurve,
|
BECCurve,
|
||||||
|
@ -9,7 +9,12 @@ from qtpy.QtWidgets import QVBoxLayout, QWidget
|
|||||||
|
|
||||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.qt_utils.toolbar import IconAction, ModularToolBar, SeparatorAction
|
from bec_widgets.qt_utils.toolbar import (
|
||||||
|
IconAction,
|
||||||
|
MaterialIconAction,
|
||||||
|
ModularToolBar,
|
||||||
|
SeparatorAction,
|
||||||
|
)
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.widgets.figure import BECFigure
|
from bec_widgets.widgets.figure import BECFigure
|
||||||
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
|
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
|
||||||
@ -93,8 +98,11 @@ class BECWaveformWidget(BECWidget, QWidget):
|
|||||||
"fit_params": IconAction(
|
"fit_params": IconAction(
|
||||||
icon_path="fitting_parameters.svg", tooltip="Open Fitting Parameters"
|
icon_path="fitting_parameters.svg", tooltip="Open Fitting Parameters"
|
||||||
),
|
),
|
||||||
"axis_settings": IconAction(
|
"axis_settings": MaterialIconAction(
|
||||||
icon_path="settings.svg", tooltip="Open Configuration Dialog"
|
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||||
|
),
|
||||||
|
"crosshair": MaterialIconAction(
|
||||||
|
icon_name="grid_goldenratio", tooltip="Show Crosshair"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
target_widget=self,
|
target_widget=self,
|
||||||
@ -123,6 +131,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
|||||||
self.toolbar.widgets["curves"].action.triggered.connect(self.show_curve_settings)
|
self.toolbar.widgets["curves"].action.triggered.connect(self.show_curve_settings)
|
||||||
self.toolbar.widgets["fit_params"].action.triggered.connect(self.show_fit_summary_dialog)
|
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["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
||||||
|
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
|
||||||
# self.toolbar.widgets["import"].action.triggered.connect(
|
# self.toolbar.widgets["import"].action.triggered.connect(
|
||||||
# lambda: self.load_config(path=None, gui=True)
|
# lambda: self.load_config(path=None, gui=True)
|
||||||
# )
|
# )
|
||||||
|
@ -1,19 +1,40 @@
|
|||||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pytest
|
||||||
from qtpy.QtCore import QPointF
|
from qtpy.QtCore import QPointF
|
||||||
|
|
||||||
from bec_widgets.utils import Crosshair
|
from bec_widgets.widgets.image.image_widget import BECImageWidget
|
||||||
|
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
|
||||||
|
|
||||||
|
from .client_mocks import mocked_client
|
||||||
|
|
||||||
|
# pylint: disable = redefined-outer-name
|
||||||
|
|
||||||
|
|
||||||
def test_mouse_moved_lines(qtbot):
|
@pytest.fixture
|
||||||
# Create a PlotWidget and add a PlotItem
|
def plot_widget_with_crosshair(qtbot, mocked_client):
|
||||||
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
widget = BECWaveformWidget(client=mocked_client())
|
||||||
plot_item = plot_widget.getPlotItem()
|
widget.plot(x=[1, 2, 3], y=[4, 5, 6])
|
||||||
plot_item.plot([1, 2, 3], [4, 5, 6])
|
widget.waveform.hook_crosshair()
|
||||||
|
qtbot.addWidget(widget)
|
||||||
|
qtbot.waitExposed(widget)
|
||||||
|
|
||||||
# Create a Crosshair instance
|
yield widget.waveform.crosshair, widget.waveform.plot_item
|
||||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def image_widget_with_crosshair(qtbot, mocked_client):
|
||||||
|
widget = BECImageWidget(client=mocked_client())
|
||||||
|
widget._image.add_custom_image(name="test", data=np.random.random((100, 200)))
|
||||||
|
widget._image.hook_crosshair()
|
||||||
|
qtbot.addWidget(widget)
|
||||||
|
qtbot.waitExposed(widget)
|
||||||
|
|
||||||
|
yield widget._image.crosshair, widget._image.plot_item
|
||||||
|
|
||||||
|
|
||||||
|
def test_mouse_moved_lines(plot_widget_with_crosshair):
|
||||||
|
crosshair, plot_item = plot_widget_with_crosshair
|
||||||
|
|
||||||
# Connect the signals to slots that will store the emitted values
|
# Connect the signals to slots that will store the emitted values
|
||||||
emitted_values_1D = []
|
emitted_values_1D = []
|
||||||
@ -32,14 +53,8 @@ def test_mouse_moved_lines(qtbot):
|
|||||||
assert crosshair.h_line.pos().y() == 5
|
assert crosshair.h_line.pos().y() == 5
|
||||||
|
|
||||||
|
|
||||||
def test_mouse_moved_signals(qtbot):
|
def test_mouse_moved_signals(plot_widget_with_crosshair):
|
||||||
# Create a PlotWidget and add a PlotItem
|
crosshair, plot_item = plot_widget_with_crosshair
|
||||||
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
|
||||||
plot_item = plot_widget.getPlotItem()
|
|
||||||
plot_item.plot([1, 2, 3], [4, 5, 6])
|
|
||||||
|
|
||||||
# Create a Crosshair instance
|
|
||||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
|
||||||
|
|
||||||
# Create a slot that will store the emitted values as tuples
|
# Create a slot that will store the emitted values as tuples
|
||||||
emitted_values_1D = []
|
emitted_values_1D = []
|
||||||
@ -59,17 +74,11 @@ def test_mouse_moved_signals(qtbot):
|
|||||||
crosshair.mouse_moved(event_mock)
|
crosshair.mouse_moved(event_mock)
|
||||||
|
|
||||||
# Assert the expected behavior
|
# Assert the expected behavior
|
||||||
assert emitted_values_1D == [(2, [5])]
|
assert emitted_values_1D == [("Curve 1", 2, 5)]
|
||||||
|
|
||||||
|
|
||||||
def test_mouse_moved_signals_outside(qtbot):
|
def test_mouse_moved_signals_outside(plot_widget_with_crosshair):
|
||||||
# Create a PlotWidget and add a PlotItem
|
crosshair, plot_item = plot_widget_with_crosshair
|
||||||
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
|
||||||
plot_item = plot_widget.getPlotItem()
|
|
||||||
plot_item.plot([1, 2, 3], [4, 5, 6])
|
|
||||||
|
|
||||||
# Create a Crosshair instance
|
|
||||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
|
||||||
|
|
||||||
# Create a slot that will store the emitted values as tuples
|
# Create a slot that will store the emitted values as tuples
|
||||||
emitted_values_1D = []
|
emitted_values_1D = []
|
||||||
@ -92,17 +101,9 @@ def test_mouse_moved_signals_outside(qtbot):
|
|||||||
assert emitted_values_1D == []
|
assert emitted_values_1D == []
|
||||||
|
|
||||||
|
|
||||||
def test_mouse_moved_signals_2D(qtbot):
|
def test_mouse_moved_signals_2D(image_widget_with_crosshair):
|
||||||
# write similar test for 2D plot
|
crosshair, plot_item = image_widget_with_crosshair
|
||||||
|
|
||||||
# Create a PlotWidget and add a PlotItem
|
|
||||||
plot_widget = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
|
||||||
data_2D = np.random.random((100, 200))
|
|
||||||
plot_item = plot_widget.getPlotItem()
|
|
||||||
image_item = pg.ImageItem(data_2D)
|
|
||||||
plot_item.addItem(image_item)
|
|
||||||
# Create a Crosshair instance
|
|
||||||
crosshair = Crosshair(plot_item=plot_item)
|
|
||||||
# Create a slot that will store the emitted values as tuples
|
# Create a slot that will store the emitted values as tuples
|
||||||
emitted_values_2D = []
|
emitted_values_2D = []
|
||||||
|
|
||||||
@ -118,20 +119,12 @@ def test_mouse_moved_signals_2D(qtbot):
|
|||||||
# Call the mouse_moved method
|
# Call the mouse_moved method
|
||||||
crosshair.mouse_moved(event_mock)
|
crosshair.mouse_moved(event_mock)
|
||||||
# Assert the expected behavior
|
# Assert the expected behavior
|
||||||
assert emitted_values_2D == [(22.0, 55.0)]
|
assert emitted_values_2D == [("test", 22.0, 55.0)]
|
||||||
|
|
||||||
|
|
||||||
def test_mouse_moved_signals_2D_outside(qtbot):
|
def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
|
||||||
# write similar test for 2D plot
|
crosshair, plot_item = image_widget_with_crosshair
|
||||||
|
|
||||||
# Create a PlotWidget and add a PlotItem
|
|
||||||
plot_widget = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
|
||||||
data_2D = np.random.random((100, 200))
|
|
||||||
plot_item = plot_widget.getPlotItem()
|
|
||||||
image_item = pg.ImageItem(data_2D)
|
|
||||||
plot_item.addItem(image_item)
|
|
||||||
# Create a Crosshair instance
|
|
||||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
|
||||||
# Create a slot that will store the emitted values as tuples
|
# Create a slot that will store the emitted values as tuples
|
||||||
emitted_values_2D = []
|
emitted_values_2D = []
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user