mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-08 09:47:48 +01:00
feat: cursor universal signals
* 1D plot universal for multiple curves * 2D plot rectangular selection * signals for move/click for both 1D and 2D
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
|
||||
|
||||
class Crosshair(QObject):
|
||||
coordinatesChanged = pyqtSignal(float, float)
|
||||
dataPointClicked = pyqtSignal(float, float)
|
||||
# Signal for 1D plot
|
||||
coordinatesChanged1D = pyqtSignal(float, list)
|
||||
coordinatesClicked1D = pyqtSignal(float, list)
|
||||
# Signal for 2D plot
|
||||
coordinatesChanged2D = pyqtSignal(float, float)
|
||||
coordinatesClicked2D = pyqtSignal(float, float)
|
||||
|
||||
def __init__(self, plot_item, precision=None, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -20,44 +24,92 @@ class Crosshair(QObject):
|
||||
)
|
||||
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
||||
|
||||
# Add marker for clicked and selected point
|
||||
data = self.get_data()
|
||||
if isinstance(data, list): # 1D plot
|
||||
num_curves = len(data)
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
for i in range(num_curves):
|
||||
color = plot_item.listDataItems()[i].opts["pen"].color()
|
||||
marker_moved = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
||||
) # Hollow
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(None), brush=pg.mkBrush(color)
|
||||
) # Full
|
||||
self.marker_moved_1d.append(marker_moved)
|
||||
self.marker_clicked_1d.append(marker_clicked)
|
||||
self.plot_item.addItem(marker_moved)
|
||||
self.plot_item.addItem(marker_clicked)
|
||||
else: # 2D plot
|
||||
self.marker_2d = pg.ROI([0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False)
|
||||
self.plot_item.addItem(self.marker_2d)
|
||||
|
||||
def get_data(self):
|
||||
x_data, y_data = [], []
|
||||
curves = []
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.ImageItem):
|
||||
return item.image, None # Return image data for 2D plot
|
||||
elif isinstance(item, pg.PlotDataItem):
|
||||
x_data.extend(item.xData)
|
||||
y_data.extend(item.yData)
|
||||
if x_data and y_data:
|
||||
return np.array(x_data), np.array(y_data) # Return x and y data for 1D plot
|
||||
return None, None
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
curves.append((item.xData, item.yData))
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
return item.image, None
|
||||
return curves
|
||||
|
||||
def snap_to_data(self, x, y):
|
||||
data = self.get_data()
|
||||
if isinstance(data, list): # 1D plot
|
||||
y_values = []
|
||||
for x_data, y_data in data:
|
||||
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
||||
y_values.append(closest_y)
|
||||
if self.precision is not None:
|
||||
x = round(x, self.precision)
|
||||
y_values = [round(y_val, self.precision) for y_val in y_values]
|
||||
return x, y_values
|
||||
elif isinstance(data[0], np.ndarray): # 2D plot
|
||||
x_idx = int(np.clip(x, 0, data[0].shape[1] - 1))
|
||||
y_idx = int(np.clip(y, 0, data[0].shape[0] - 1))
|
||||
return x_idx, y_idx
|
||||
return x, y
|
||||
|
||||
def closest_x_y_value(self, input_value, list_x, list_y):
|
||||
"""
|
||||
Find the closest x and y value to the input value.
|
||||
|
||||
Args:
|
||||
input_value (float): Input value
|
||||
list_x (list): List of x values
|
||||
list_y (list): List of y values
|
||||
|
||||
Returns:
|
||||
tuple: Closest x and y value
|
||||
"""
|
||||
arr = np.asarray(list_x)
|
||||
i = (np.abs(arr - input_value)).argmin()
|
||||
return list_x[i], list_y[i]
|
||||
|
||||
def mouse_moved(self, event):
|
||||
pos = event[0]
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
||||
x, y = self.snap_to_data(mouse_point.x(), mouse_point.y())
|
||||
self.v_line.setPos(x)
|
||||
self.h_line.setPos(y)
|
||||
self.coordinatesChanged.emit(x, y)
|
||||
x, y_values = self.snap_to_data(mouse_point.x(), mouse_point.y())
|
||||
self.v_line.setPos(mouse_point.x())
|
||||
self.h_line.setPos(mouse_point.y())
|
||||
if isinstance(y_values, list): # 1D plot
|
||||
self.coordinatesChanged1D.emit(x, y_values)
|
||||
for i, y_val in enumerate(y_values):
|
||||
self.marker_moved_1d[i].setData([x], [y_val])
|
||||
else: # 2D plot
|
||||
self.coordinatesChanged2D.emit(x, y_values)
|
||||
|
||||
def mouse_clicked(self, event):
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
||||
x, y = self.snap_to_data(mouse_point.x(), mouse_point.y())
|
||||
self.dataPointClicked.emit(x, y)
|
||||
|
||||
def snap_to_data(self, x, y):
|
||||
x_data, y_data = self.get_data()
|
||||
if x_data is not None and y_data is not None:
|
||||
distance = (x_data - x) ** 2 + (y_data - y) ** 2
|
||||
index = np.argmin(distance)
|
||||
x, y = x_data[index], y_data[index]
|
||||
if self.precision is not None:
|
||||
x, y = round(x, self.precision), round(y, self.precision)
|
||||
return x, y
|
||||
elif x_data is not None and y_data is None: # For 2D plot (ImageItem)
|
||||
x_idx = int(np.clip(x, 0, x_data.shape[1] - 1))
|
||||
y_idx = int(np.clip(y, 0, x_data.shape[0] - 1))
|
||||
return x_idx, y_idx
|
||||
return x, y
|
||||
x, y_values = self.snap_to_data(mouse_point.x(), mouse_point.y())
|
||||
if isinstance(y_values, list): # 1D plot
|
||||
self.coordinatesClicked1D.emit(x, y_values)
|
||||
for i, y_val in enumerate(y_values):
|
||||
self.marker_clicked_1d[i].setData([x], [y_val])
|
||||
else: # 2D plot
|
||||
self.coordinatesClicked2D.emit(x, y_values)
|
||||
self.marker_2d.setPos([x, y_values])
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import pyqtgraph as pg
|
||||
import numpy as np
|
||||
|
||||
from crosshair import Crosshair
|
||||
|
||||
|
||||
def add_crosshair(plot_item, is_image=False):
|
||||
return Crosshair(plot_item, is_image)
|
||||
|
||||
|
||||
app = pg.mkQApp()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.resize(1000, 500)
|
||||
|
||||
#####################
|
||||
# 1D Plot with labels
|
||||
#####################
|
||||
label_1d_move = win.addLabel("1D move label", row=0, col=0)
|
||||
label_1d_click = win.addLabel("1D click label", row=1, col=0)
|
||||
plot_item_1d = win.addPlot(row=2, col=0)
|
||||
x_data = np.linspace(0, 10, 1000)
|
||||
y_data_sine = np.sin(x_data)
|
||||
y_data_cosine = np.cos(x_data)
|
||||
plot_item_1d.plot(x_data, y_data_sine)
|
||||
plot_item_1d.plot(x_data, y_data_cosine)
|
||||
crosshair_1d = Crosshair(plot_item_1d, precision=2)
|
||||
|
||||
|
||||
def on_coordinates_changed_1d(x, y):
|
||||
label_1d_move.setText(f"1D Moved: ({x}, {y})")
|
||||
|
||||
|
||||
def on_data_point_clicked_1d(x, y):
|
||||
label_1d_click.setText(f"1D Clicked: ({x}, {y})")
|
||||
|
||||
|
||||
crosshair_1d.coordinatesChanged.connect(on_coordinates_changed_1d)
|
||||
crosshair_1d.dataPointClicked.connect(on_data_point_clicked_1d)
|
||||
|
||||
#####################
|
||||
# 2D Plot with labels
|
||||
#####################
|
||||
label_2d_move = win.addLabel("2D move label", row=0, col=1)
|
||||
label_2d_click = win.addLabel("2D click label", row=1, col=1)
|
||||
plot_item_2d = win.addPlot(row=2, col=1)
|
||||
|
||||
img = np.random.normal(size=(100, 100))
|
||||
image_item = pg.ImageItem(img)
|
||||
plot_item_2d.addItem(image_item)
|
||||
|
||||
crosshair_2d = Crosshair(plot_item_2d, precision=2)
|
||||
|
||||
|
||||
def on_coordinates_changed_2d(x, y):
|
||||
label_2d_move.setText(f"2D Moved: ({x}, {y})")
|
||||
|
||||
|
||||
def on_data_point_clicked_2d(x, y):
|
||||
label_2d_click.setText(f"2D Clicked: ({x}, {y})")
|
||||
|
||||
|
||||
#
|
||||
crosshair_2d.coordinatesChanged.connect(on_coordinates_changed_2d)
|
||||
crosshair_2d.dataPointClicked.connect(on_data_point_clicked_2d)
|
||||
|
||||
if __name__ == "__main__":
|
||||
pg.exec()
|
||||
132
bec_widgets/qt_utils/utils_example.py
Normal file
132
bec_widgets/qt_utils/utils_example.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
)
|
||||
from pyqtgraph import mkPen
|
||||
from pyqtgraph.Qt import QtCore
|
||||
from crosshair import Crosshair
|
||||
|
||||
|
||||
class ExampleApp(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Layout
|
||||
self.layout = QHBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
##########################
|
||||
# 1D Plot
|
||||
##########################
|
||||
|
||||
# PlotWidget
|
||||
self.plot_widget_1d = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
||||
self.plot_item_1d = self.plot_widget_1d.getPlotItem()
|
||||
# 1D Datasets
|
||||
self.x_data = np.linspace(0, 10, 1000)
|
||||
# same convention as in line_plot.py
|
||||
self.y_value_list = [
|
||||
np.sin(self.x_data),
|
||||
np.cos(self.x_data),
|
||||
np.sin(2 * self.x_data),
|
||||
] # List of y-values for multiple curves
|
||||
self.curve_names = ["Sine", "Cosine", "Sine2x"]
|
||||
|
||||
# Curves
|
||||
color_list = ["#384c6b", "#e28a2b", "#5E3023", "#e41a1c", "#984e83", "#4daf4a"]
|
||||
self.plot_item_1d.addLegend()
|
||||
self.curves = []
|
||||
for ii, y_value in enumerate(self.y_value_list):
|
||||
pen = mkPen(color=color_list[ii], width=2, style=QtCore.Qt.DashLine)
|
||||
curve = pg.PlotDataItem(
|
||||
self.x_data, y_value, pen=pen, skipFiniteCheck=True, name=self.curve_names[ii]
|
||||
)
|
||||
self.plot_item_1d.addItem(curve)
|
||||
self.curves.append(curve)
|
||||
|
||||
##########################
|
||||
# 2D Plot
|
||||
##########################
|
||||
self.plot_widget_2d = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
||||
self.data_2D = np.random.random((100, 100))
|
||||
self.plot_item_2d = self.plot_widget_2d.getPlotItem()
|
||||
self.image_item = pg.ImageItem(self.data_2D)
|
||||
self.plot_item_2d.addItem(self.image_item)
|
||||
|
||||
##########################
|
||||
# Table
|
||||
##########################
|
||||
self.table = QTableWidget(len(self.curve_names), 2)
|
||||
self.table.setHorizontalHeaderLabels(["(X, Y) - Moved", "(X, Y) - Clicked"])
|
||||
self.table.setVerticalHeaderLabels(self.curve_names)
|
||||
self.table.resizeColumnsToContents()
|
||||
|
||||
##########################
|
||||
# Signals & Cross-hairs
|
||||
##########################
|
||||
# 1D
|
||||
self.crosshair_1d = Crosshair(self.plot_item_1d, precision=2)
|
||||
self.crosshair_1d.coordinatesChanged1D.connect(
|
||||
lambda x, y: self.update_table(self.table, x, y, column=0)
|
||||
)
|
||||
self.crosshair_1d.coordinatesClicked1D.connect(
|
||||
lambda x, y: self.update_table(self.table, x, y, column=1)
|
||||
)
|
||||
# 2D
|
||||
self.crosshair_2d = Crosshair(self.plot_item_2d)
|
||||
self.crosshair_2d.coordinatesChanged2D.connect(
|
||||
lambda x, y: self.moved_label_2d.setText(f"Mouse Moved Coordinates (2D): x={x}, y={y}")
|
||||
)
|
||||
self.crosshair_2d.coordinatesClicked2D.connect(
|
||||
lambda x, y: self.clicked_label_2d.setText(f"Clicked Coordinates (2D): x={x}, y={y}")
|
||||
)
|
||||
|
||||
##########################
|
||||
# Adding widgets to layout
|
||||
##########################
|
||||
|
||||
##### left side #####
|
||||
self.column1 = QVBoxLayout()
|
||||
self.layout.addLayout(self.column1)
|
||||
|
||||
# label
|
||||
self.clicked_label_1d = QLabel("Clicked Coordinates (1D):")
|
||||
self.column1.addWidget(self.clicked_label_1d)
|
||||
|
||||
# table
|
||||
self.column1.addWidget(self.table)
|
||||
|
||||
# 1D plot
|
||||
self.column1.addWidget(self.plot_widget_1d)
|
||||
|
||||
##### left side #####
|
||||
self.column2 = QVBoxLayout()
|
||||
self.layout.addLayout(self.column2)
|
||||
|
||||
# labels
|
||||
self.clicked_label_2d = QLabel("Clicked Coordinates (2D):")
|
||||
self.moved_label_2d = QLabel("Moved Coordinates (2D):")
|
||||
self.column2.addWidget(self.clicked_label_2d)
|
||||
self.column2.addWidget(self.moved_label_2d)
|
||||
|
||||
# 2D plot
|
||||
self.column2.addWidget(self.plot_widget_2d)
|
||||
|
||||
def update_table(self, table_widget, x, y_values, column):
|
||||
for i, y in enumerate(y_values):
|
||||
table_widget.setItem(i, column, QTableWidgetItem(f"({x}, {y})"))
|
||||
table_widget.resizeColumnsToContents()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
window = ExampleApp()
|
||||
window.show()
|
||||
app.exec_()
|
||||
Reference in New Issue
Block a user