diff --git a/bec_widgets/utils/crosshair.py b/bec_widgets/utils/crosshair.py index 54e34aaf..d022f912 100644 --- a/bec_widgets/utils/crosshair.py +++ b/bec_widgets/utils/crosshair.py @@ -1,4 +1,7 @@ +from __future__ import annotations + from collections import defaultdict +from typing import Any import numpy as np import pyqtgraph as pg @@ -197,15 +200,18 @@ class Crosshair(QObject): self.marker_2d = pg.ROI( [0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False ) + self.marker_2d.skip_auto_range = True self.plot_item.addItem(self.marker_2d) - def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]: + def snap_to_data( + self, x: float, y: float + ) -> tuple[None, None] | tuple[defaultdict[Any, list], defaultdict[Any, list]]: """ Finds the nearest data points to the given x and y coordinates. Args: - x: The x-coordinate of the mouse cursor - y: The y-coordinate of the mouse cursor + x(float): The x-coordinate of the mouse cursor + y(float): The y-coordinate of the mouse cursor Returns: tuple: x and y values snapped to the nearest data @@ -235,7 +241,7 @@ class Crosshair(QObject): y_values[name] = closest_y x_values[name] = closest_x elif isinstance(item, pg.ImageItem): # 2D plot - name = item.config.monitor + name = item.config.monitor or str(id(item)) 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)) @@ -320,7 +326,7 @@ class Crosshair(QObject): ) self.coordinatesChanged1D.emit(coordinate_to_emit) elif isinstance(item, pg.ImageItem): - name = item.config.monitor + name = item.config.monitor or str(id(item)) x, y = x_snap_values[name], y_snap_values[name] if x is None or y is None: continue @@ -374,7 +380,7 @@ class Crosshair(QObject): ) self.coordinatesClicked1D.emit(coordinate_to_emit) elif isinstance(item, pg.ImageItem): - name = item.config.monitor + name = item.config.monitor or str(id(item)) x, y = x_snap_values[name], y_snap_values[name] if x is None or y is None: continue @@ -418,9 +424,17 @@ class Crosshair(QObject): """ x, y = pos x_scaled, y_scaled = self.scale_emitted_coordinates(x, y) - + text = f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})" + for item in self.items: + if isinstance(item, pg.ImageItem): + image = item.image + ix = int(np.clip(x, 0, image.shape[0] - 1)) + iy = int(np.clip(y, 0, image.shape[1] - 1)) + intensity = image[ix, iy] + text += f"\nIntensity: {intensity:.{self.precision}g}" + break # Update coordinate label - self.coord_label.setText(f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})") + self.coord_label.setText(text) self.coord_label.setPos(x, y) self.coord_label.setVisible(True) @@ -436,6 +450,9 @@ class Crosshair(QObject): self.clear_markers() def cleanup(self): + if self.marker_2d is not None: + self.plot_item.removeItem(self.marker_2d) + self.marker_2d = None self.plot_item.removeItem(self.v_line) self.plot_item.removeItem(self.h_line) self.plot_item.removeItem(self.coord_label) diff --git a/tests/unit_tests/test_crosshair.py b/tests/unit_tests/test_crosshair.py index 5f0bf98a..70b7be35 100644 --- a/tests/unit_tests/test_crosshair.py +++ b/tests/unit_tests/test_crosshair.py @@ -4,9 +4,6 @@ import pytest from qtpy.QtCore import QPointF, Qt from bec_widgets.utils import Crosshair -from bec_widgets.widgets.plots.image.image_widget import BECImageWidget - -from .client_mocks import mocked_client # pylint: disable = redefined-outer-name @@ -25,14 +22,20 @@ def plot_widget_with_crosshair(qtbot): @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() +def image_widget_with_crosshair(qtbot): + widget = pg.PlotWidget() qtbot.addWidget(widget) qtbot.waitExposed(widget) - yield widget._image.crosshair, widget._image.plot_item + image_item = pg.ImageItem() + image_item.setImage(np.random.rand(100, 100)) + image_item.config = type("obj", (object,), {"monitor": "test"}) + + widget.addItem(image_item) + plot_item = widget.getPlotItem() + crosshair = Crosshair(plot_item=plot_item, precision=3) + + yield crosshair, plot_item def test_mouse_moved_lines(plot_widget_with_crosshair): @@ -104,13 +107,13 @@ def test_mouse_moved_signals_2D(image_widget_with_crosshair): crosshair.coordinatesChanged2D.connect(slot) - pos_in_view = QPointF(22.0, 55.0) + pos_in_view = QPointF(21.0, 55.0) pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) event_mock = [pos_in_scene] crosshair.mouse_moved(event_mock) - assert emitted_values_2D == [("test", 22.0, 55.0)] + assert emitted_values_2D == [("test", 21, 55)] def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair): @@ -226,3 +229,41 @@ def test_crosshair_clicked_signal(qtbot, plot_widget_with_crosshair): assert np.isclose(round(x, 1), 2) assert np.isclose(round(y, 1), 5) + + +def test_update_coord_label_1D(plot_widget_with_crosshair): + crosshair, _ = plot_widget_with_crosshair + # Provide a test position + pos = (10, 20) + crosshair.update_coord_label(pos) + expected_text = f"({10:.3g}, {20:.3g})" + # Verify that the coordinate label shows only the 1D coordinates (no intensity line) + assert crosshair.coord_label.toPlainText() == expected_text + label_pos = crosshair.coord_label.pos() + assert np.isclose(label_pos.x(), 10) + assert np.isclose(label_pos.y(), 20) + assert crosshair.coord_label.isVisible() + + +def test_update_coord_label_2D(image_widget_with_crosshair): + crosshair, plot_item = image_widget_with_crosshair + + known_image = np.array([[10, 20], [30, 40]], dtype=float) + + for item in plot_item.items: + if isinstance(item, pg.ImageItem): + item.setImage(known_image) + + pos = (0.5, 1.2) + crosshair.update_coord_label(pos) + + ix = int(np.clip(0.5, 0, known_image.shape[0] - 1)) # 0 + iy = int(np.clip(1.2, 0, known_image.shape[1] - 1)) # 1 + intensity = known_image[ix, iy] # Expected: 20 + expected_text = f"({0.5:.3g}, {1.2:.3g})\nIntensity: {intensity:.3g}" + + assert crosshair.coord_label.toPlainText() == expected_text + label_pos = crosshair.coord_label.pos() + assert np.isclose(label_pos.x(), 0.5) + assert np.isclose(label_pos.y(), 1.2) + assert crosshair.coord_label.isVisible()