1
0
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:
wyzula-jan
2023-08-10 18:29:06 +02:00
parent f75554bd7b
commit 20e9516595
3 changed files with 217 additions and 100 deletions

View File

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

View File

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

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