0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

fix(crosshair): crosshair adapted for multi waveform widget

This commit is contained in:
2024-11-12 11:49:03 +01:00
committed by wyzula_j
parent 42d4f182f7
commit 0cd85ed9fa
2 changed files with 111 additions and 38 deletions

View File

@ -50,9 +50,15 @@ class Crosshair(QObject):
self.v_line.skip_auto_range = True 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.h_line.skip_auto_range = True
# Add custom attribute to identify crosshair lines
self.v_line.is_crosshair = True
self.h_line.is_crosshair = 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)
# Initialize highlighted curve in a case of multiple curves
self.highlighted_curve_index = None
# Add TextItem to display coordinates # Add TextItem to display coordinates
self.coord_label = pg.TextItem("", anchor=(1, 1), fill=(0, 0, 0, 100)) self.coord_label = pg.TextItem("", anchor=(1, 1), fill=(0, 0, 0, 100))
self.coord_label.setVisible(False) # Hide initially self.coord_label.setVisible(False) # Hide initially
@ -73,6 +79,7 @@ class Crosshair(QObject):
self.plot_item.ctrl.downsampleSpin.valueChanged.connect(self.clear_markers) self.plot_item.ctrl.downsampleSpin.valueChanged.connect(self.clear_markers)
# Initialize markers # Initialize markers
self.items = []
self.marker_moved_1d = {} self.marker_moved_1d = {}
self.marker_clicked_1d = {} self.marker_clicked_1d = {}
self.marker_2d = None self.marker_2d = None
@ -116,34 +123,74 @@ class Crosshair(QObject):
self.coord_label.fill = pg.mkBrush(label_bg_color) self.coord_label.fill = pg.mkBrush(label_bg_color)
self.coord_label.border = pg.mkPen(None) self.coord_label.border = pg.mkPen(None)
@Slot(int)
def update_highlighted_curve(self, curve_index: int):
"""
Update the highlighted curve in the case of multiple curves in a plot item.
Args:
curve_index(int): The index of curve to highlight
"""
self.highlighted_curve_index = curve_index
self.clear_markers()
self.update_markers()
def update_markers(self): def update_markers(self):
"""Update the markers for the crosshair, creating new ones if necessary.""" """Update the markers for the crosshair, creating new ones if necessary."""
# Create new markers if self.highlighted_curve_index is not None and hasattr(self.plot_item, "visible_curves"):
for item in self.plot_item.items: # Focus on the highlighted curve only
self.items = [self.plot_item.visible_curves[self.highlighted_curve_index]]
else:
# Handle all curves
self.items = self.plot_item.items
# Create or update markers
for item in self.items:
if isinstance(item, pg.PlotDataItem): # 1D plot if isinstance(item, pg.PlotDataItem): # 1D plot
if item.name() in self.marker_moved_1d:
continue
pen = item.opts["pen"] pen = item.opts["pen"]
color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen) color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
marker_moved = CrosshairScatterItem( name = item.name() or str(id(item))
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None) if name in self.marker_moved_1d:
) # Update existing markers
marker_moved.skip_auto_range = True marker_moved = self.marker_moved_1d[name]
self.marker_moved_1d[item.name()] = marker_moved marker_moved.setPen(pg.mkPen(color))
self.plot_item.addItem(marker_moved) # Update clicked markers' brushes
for marker_clicked in self.marker_clicked_1d[name]:
# Create glowing effect markers for clicked events alpha = marker_clicked.opts["brush"].color().alpha()
for size, alpha in [(18, 64), (14, 128), (10, 255)]: marker_clicked.setBrush(
marker_clicked = CrosshairScatterItem( pg.mkBrush(color.red(), color.green(), color.blue(), alpha)
size=size, )
pen=pg.mkPen(None), # Update z-values
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha), marker_moved.setZValue(item.zValue() + 1)
for marker_clicked in self.marker_clicked_1d[name]:
marker_clicked.setZValue(item.zValue() + 1)
else:
# Create new markers
marker_moved = CrosshairScatterItem(
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
) )
marker_clicked.skip_auto_range = True marker_moved.skip_auto_range = True
self.marker_clicked_1d[item.name()] = marker_clicked marker_moved.is_crosshair = True
self.plot_item.addItem(marker_clicked) self.marker_moved_1d[name] = marker_moved
self.plot_item.addItem(marker_moved)
# Set marker z-value higher than the curve
marker_moved.setZValue(item.zValue() + 1)
# Create glowing effect markers for clicked events
marker_clicked_list = []
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
marker_clicked = CrosshairScatterItem(
size=size,
pen=pg.mkPen(None),
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
)
marker_clicked.skip_auto_range = True
marker_clicked.is_crosshair = True
self.plot_item.addItem(marker_clicked)
marker_clicked.setZValue(item.zValue() + 1)
marker_clicked_list.append(marker_clicked)
self.marker_clicked_1d[name] = marker_clicked_list
elif isinstance(item, pg.ImageItem): # 2D plot elif isinstance(item, pg.ImageItem): # 2D plot
if self.marker_2d is not None: if self.marker_2d is not None:
continue continue
@ -165,12 +212,11 @@ class Crosshair(QObject):
""" """
y_values = defaultdict(list) y_values = defaultdict(list)
x_values = defaultdict(list) x_values = defaultdict(list)
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.items:
if isinstance(item, pg.PlotDataItem): # 1D plot if isinstance(item, pg.PlotDataItem): # 1D plot
name = item.name() name = item.name() or str(id(item))
plot_data = item._getDisplayDataset() plot_data = item._getDisplayDataset()
if plot_data is None: if plot_data is None:
continue continue
@ -191,7 +237,7 @@ class Crosshair(QObject):
elif isinstance(item, pg.ImageItem): # 2D plot elif isinstance(item, pg.ImageItem): # 2D plot
name = item.config.monitor 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 # 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)) 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)) x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
@ -259,9 +305,9 @@ class Crosshair(QObject):
# not sure how we got here, but just to be safe... # not sure how we got here, but just to be safe...
return return
for item in self.plot_item.items: for item in self.items:
if isinstance(item, pg.PlotDataItem): if isinstance(item, pg.PlotDataItem):
name = item.name() name = item.name() or str(id(item))
x, y = x_snap_values[name], y_snap_values[name] x, y = x_snap_values[name], y_snap_values[name]
if x is None or y is None: if x is None or y is None:
continue continue
@ -312,13 +358,14 @@ class Crosshair(QObject):
# not sure how we got here, but just to be safe... # not sure how we got here, but just to be safe...
return return
for item in self.plot_item.items: for item in self.items:
if isinstance(item, pg.PlotDataItem): if isinstance(item, pg.PlotDataItem):
name = item.name() name = item.name() or str(id(item))
x, y = x_snap_values[name], y_snap_values[name] x, y = x_snap_values[name], y_snap_values[name]
if x is None or y is None: if x is None or y is None:
continue continue
self.marker_clicked_1d[name].setData([x], [y]) for marker_clicked in self.marker_clicked_1d[name]:
marker_clicked.setData([x], [y])
x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y) x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
coordinate_to_emit = ( coordinate_to_emit = (
name, name,
@ -340,9 +387,12 @@ class Crosshair(QObject):
def clear_markers(self): def clear_markers(self):
"""Clears the markers from the plot.""" """Clears the markers from the plot."""
for marker in self.marker_moved_1d.values(): for marker in self.marker_moved_1d.values():
marker.clear() self.plot_item.removeItem(marker)
for marker in self.marker_clicked_1d.values(): for markers in self.marker_clicked_1d.values():
marker.clear() for marker in markers:
self.plot_item.removeItem(marker)
self.marker_moved_1d.clear()
self.marker_clicked_1d.clear()
def scale_emitted_coordinates(self, x, y): def scale_emitted_coordinates(self, x, y):
"""Scales the emitted coordinates if the axes are in log scale. """Scales the emitted coordinates if the axes are in log scale.
@ -369,7 +419,7 @@ class Crosshair(QObject):
x, y = pos x, y = pos
x_scaled, y_scaled = self.scale_emitted_coordinates(x, y) x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
# # Update coordinate label # Update coordinate label
self.coord_label.setText(f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})") self.coord_label.setText(f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})")
self.coord_label.setPos(x, y) self.coord_label.setPos(x, y)
self.coord_label.setVisible(True) self.coord_label.setVisible(True)

View File

@ -40,6 +40,7 @@ class BECMultiWaveformConfig(SubplotConfig):
class BECMultiWaveform(BECPlotBase): class BECMultiWaveform(BECPlotBase):
monitor_signal_updated = Signal() monitor_signal_updated = Signal()
highlighted_curve_index_changed = Signal(int)
USER_ACCESS = [ USER_ACCESS = [
"_rpc_id", "_rpc_id",
"_config_dict", "_config_dict",
@ -85,6 +86,7 @@ class BECMultiWaveform(BECPlotBase):
self.connected = False self.connected = False
self.current_highlight_index = 0 self.current_highlight_index = 0
self._curves = deque() self._curves = deque()
self.visible_curves = []
self.number_of_visible_curves = 0 self.number_of_visible_curves = 0
# Get bec shortcuts dev, scans, queue, scan_storage, dap # Get bec shortcuts dev, scans, queue, scan_storage, dap
@ -159,8 +161,10 @@ class BECMultiWaveform(BECPlotBase):
if current_scan_id != self.scan_id: if current_scan_id != self.scan_id:
self.scan_id = current_scan_id self.scan_id = current_scan_id
self.plot_item.clear() self.clear_curves()
self.curves.clear() self.curves.clear()
if self.crosshair:
self.crosshair.clear_markers()
# Always create a new curve and add it # Always create a new curve and add it
curve = pg.PlotDataItem() curve = pg.PlotDataItem()
@ -181,8 +185,8 @@ class BECMultiWaveform(BECPlotBase):
Args: Args:
index (int): The index of the curve to highlight among visible curves. index (int): The index of the curve to highlight among visible curves.
""" """
visible_curves = [curve for curve in self.curves if curve.isVisible()] self.plot_item.visible_curves = [curve for curve in self.curves if curve.isVisible()]
num_visible_curves = len(visible_curves) num_visible_curves = len(self.plot_item.visible_curves)
self.number_of_visible_curves = num_visible_curves self.number_of_visible_curves = num_visible_curves
if num_visible_curves == 0: if num_visible_curves == 0:
@ -197,7 +201,7 @@ class BECMultiWaveform(BECPlotBase):
colors = Colors.evenly_spaced_colors( colors = Colors.evenly_spaced_colors(
colormap=self.config.color_palette, num=num_colors, format="HEX" colormap=self.config.color_palette, num=num_colors, format="HEX"
) )
for i, curve in enumerate(visible_curves): for i, curve in enumerate(self.plot_item.visible_curves):
curve.setPen() curve.setPen()
if i == self.current_highlight_index: if i == self.current_highlight_index:
curve.setPen(pg.mkPen(color=colors[i], width=5)) curve.setPen(pg.mkPen(color=colors[i], width=5))
@ -208,6 +212,8 @@ class BECMultiWaveform(BECPlotBase):
curve.setAlpha(alpha=self.config.opacity / 100, auto=False) curve.setAlpha(alpha=self.config.opacity / 100, auto=False)
curve.setZValue(0) curve.setZValue(0)
self.highlighted_curve_index_changed.emit(self.current_highlight_index)
@Slot(int) @Slot(int)
def set_opacity(self, opacity: int): def set_opacity(self, opacity: int):
""" """
@ -269,6 +275,13 @@ class BECMultiWaveform(BECPlotBase):
self.config.color_palette = colormap self.config.color_palette = colormap
self.set_curve_highlight(self.current_highlight_index) self.set_curve_highlight(self.current_highlight_index)
def hook_crosshair(self) -> None:
super().hook_crosshair()
if self.crosshair:
self.highlighted_curve_index_changed.connect(self.crosshair.update_highlighted_curve)
if self.curves:
self.crosshair.update_highlighted_curve(self.current_highlight_index)
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict:
""" """
Extract all curve data into a dictionary or a pandas DataFrame. Extract all curve data into a dictionary or a pandas DataFrame.
@ -309,6 +322,17 @@ class BECMultiWaveform(BECPlotBase):
return combined_data return combined_data
return data return data
def clear_curves(self):
"""
Remove all curves from the plot, excluding crosshair items.
"""
items_to_remove = []
for item in self.plot_item.items:
if not getattr(item, "is_crosshair", False) and isinstance(item, pg.PlotDataItem):
items_to_remove.append(item)
for item in items_to_remove:
self.plot_item.removeItem(item)
def export_to_matplotlib(self): def export_to_matplotlib(self):
""" """
Export current waveform to matplotlib GUI. Available only if matplotlib is installed in the environment. Export current waveform to matplotlib GUI. Available only if matplotlib is installed in the environment.
@ -325,6 +349,5 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
widget = BECFigure() widget = BECFigure()
widget.multi_waveform(monitor="waveform")
widget.show() widget.show()
sys.exit(app.exec_()) sys.exit(app.exec_())