0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

fix: crosshair handles dynamic changes of number of curves in 1D plot

This commit is contained in:
wyzula-jan
2023-08-16 15:43:20 +02:00
parent ce54daf754
commit 242737b516
2 changed files with 154 additions and 94 deletions

View File

@ -26,59 +26,78 @@ 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()
# Initialize markers
self.marker_moved_1d = []
self.marker_clicked_1d = []
self.marker_2d = None
self.update_markers()
def update_markers(self):
# Clear existing markers
for marker in self.marker_moved_1d + self.marker_clicked_1d:
self.plot_item.removeItem(marker)
if self.marker_2d:
self.plot_item.removeItem(self.marker_2d)
# Create new markers
self.marker_moved_1d = []
self.marker_clicked_1d = []
self.marker_2d = None
for item in self.plot_item.items:
if isinstance(item, pg.PlotDataItem): # 1D plot
color = item.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):
curves = []
for item in self.plot_item.items:
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
# if curves:
# return curves
# else:
# return None
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 snap_to_data(self, x, y):
data = self.get_data()
# if data is None: #TODO hadle if data are None
# return x, y
y_values_1d = []
x_values_1d = []
image_2d = None
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)
return x, y_values
elif isinstance(data[0], np.ndarray): # 2D plot
x_idx = int(np.clip(x, 0, data[0].shape[0] - 1))
y_idx = int(np.clip(y, 0, data[0].shape[1] - 1))
# Iterate through items in the plot
for item in self.plot_item.items:
if isinstance(item, pg.PlotDataItem): # 1D plot
x_data, y_data = item.xData, item.yData
if x_data is not None and y_data is not None:
if self.is_log_x:
min_x_data = np.min(x_data[x_data > 0])
else:
min_x_data = np.min(x_data)
max_x_data = np.max(x_data)
if x < min_x_data or x > max_x_data:
return None, None
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
y_values_1d.append(closest_y)
x_values_1d.append(closest_x)
elif isinstance(item, pg.ImageItem): # 2D plot
image_2d = item.image
# Handle 1D plot
if y_values_1d:
if all(v is None for v in x_values_1d) or all(v is None for v in y_values_1d):
return None, None
return 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 x, y
return None, None
def closest_x_y_value(self, input_value, list_x, list_y):
"""
@ -111,40 +130,54 @@ class Crosshair(QObject):
y = 10**y
x, y_values = self.snap_to_data(x, y)
if isinstance(y_values, list): # 1D plot
self.coordinatesChanged1D.emit(
round(x, self.precision), [round(y_val, self.precision) for y_val in y_values]
)
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)],
for item in self.plot_item.items:
if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values):
return
self.coordinatesChanged1D.emit(
round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values],
)
else: # 2D plot
self.coordinatesChanged2D.emit(x, y_values)
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):
if x is None or y_values is None:
return
self.coordinatesChanged2D.emit(x, y_values)
def mouse_clicked(self, event):
self.check_log()
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
x, y = mouse_point.x(), mouse_point.y()
if self.is_log_x:
x = 10**x
if self.is_log_y:
y = 10**y
x, y_values = self.snap_to_data(x, y)
if isinstance(y_values, list): # 1D plot
self.coordinatesClicked1D.emit(
round(x, self.precision), [round(y_val, self.precision) for y_val in y_values]
)
for i, y_val in enumerate(y_values):
self.marker_clicked_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)],
for item in self.plot_item.items:
if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values):
return
self.coordinatesClicked1D.emit(
round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values],
)
else: # 2D plot
self.coordinatesClicked2D.emit(x, y_values)
self.marker_2d.setPos([x, y_values])
for i, y_val in enumerate(y_values):
self.marker_clicked_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):
if x is None or y_values is None:
return
self.coordinatesClicked2D.emit(x, y_values)
self.marker_2d.setPos([x, y_values])
def check_log(self):
"""

View File

@ -8,6 +8,7 @@ from PyQt5.QtWidgets import (
QHBoxLayout,
QTableWidget,
QTableWidgetItem,
QSpinBox,
)
from pyqtgraph import mkPen
from pyqtgraph.Qt import QtCore
@ -40,25 +41,15 @@ class ExampleApp(QWidget):
# 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),
gauss(self.x_data, 1, 1),
gauss(self.x_data, 1.5, 3),
abs(np.sin(self.x_data)),
abs(np.cos(self.x_data)),
abs(np.sin(2 * self.x_data)),
] # List of y-values for multiple curves
self.y_value_list = [gauss(self.x_data, 1, 1), gauss(self.x_data, 1.5, 3)]
self.curve_names = ["Gauss(1,1)", "Gauss(1.5,3)"] # ,"Sine", "Cosine", "Sine2x"]
# Curves
color_list = ["#384c6b", "#e28a2b", "#5E3023", "#e41a1c", "#984e83", "#4daf4a"]
self.plot_item_1d.addLegend()
self.curve_names = ["Gauss(1,1)", "Gauss(1.5,3)", "Abs(Sine)", "Abs(Cosine)", "Abs(Sine2x)"]
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
@ -78,24 +69,13 @@ class ExampleApp(QWidget):
self.table.resizeColumnsToContents()
##########################
# Signals & Cross-hairs
# Spinbox for N curves
##########################
# 1D
self.crosshair_1d = Crosshair(self.plot_item_1d, precision=4)
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}")
)
self.spin_box = QSpinBox()
self.spin_box.setMinimum(0)
self.spin_box.setMaximum(len(self.y_value_list))
self.spin_box.setValue(2)
self.spin_box.valueChanged.connect(lambda: self.update_curves(self.spin_box.value()))
##########################
# Adding widgets to layout
@ -105,6 +85,12 @@ class ExampleApp(QWidget):
self.column1 = QVBoxLayout()
self.layout.addLayout(self.column1)
# SpinBox
self.spin_row = QHBoxLayout()
self.column1.addLayout(self.spin_row)
self.spin_row.addWidget(QLabel("Number of curves:"))
self.spin_row.addWidget(self.spin_box)
# label
self.clicked_label_1d = QLabel("Clicked Coordinates (1D):")
self.column1.addWidget(self.clicked_label_1d)
@ -128,12 +114,53 @@ class ExampleApp(QWidget):
# 2D plot
self.column2.addWidget(self.plot_widget_2d)
self.update_curves(2) # just Gaussian curves
def hook_crosshair(self):
self.crosshair_1d = Crosshair(self.plot_item_1d, precision=10)
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}")
)
def update_table(self, table_widget, x, y_values, column):
"""Update the table with the new coordinates"""
for i, y in enumerate(y_values):
table_widget.setItem(i, column, QTableWidgetItem(f"({x}, {y})"))
table_widget.resizeColumnsToContents()
def update_curves(self, num_curves):
"""Update the number of curves"""
self.plot_item_1d.clear()
# Curves
color_list = ["#384c6b", "#e28a2b", "#5E3023", "#e41a1c", "#984e83", "#4daf4a"]
self.plot_item_1d.addLegend()
self.curves = []
y_value_list = self.y_value_list[:num_curves]
for ii, y_value in enumerate(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)
self.hook_crosshair()
if __name__ == "__main__":
app = QApplication([])