diff --git a/bec_widgets/utils/plot_indicator_items.py b/bec_widgets/utils/plot_indicator_items.py index 8121c0ec..975fd932 100644 --- a/bec_widgets/utils/plot_indicator_items.py +++ b/bec_widgets/utils/plot_indicator_items.py @@ -1,5 +1,6 @@ """Module to create an arrow item for a pyqtgraph plot""" +import numpy as np import pyqtgraph as pg from qtpy.QtCore import QObject, Qt, Signal, Slot @@ -12,6 +13,9 @@ class BECIndicatorItem(QObject): super().__init__(parent=parent) self.accent_colors = get_accent_colors() self.plot_item = plot_item + self._pos = None + self.is_log_x = False + self.is_log_y = False def add_to_plot(self) -> None: """Add the item to the plot""" @@ -25,6 +29,12 @@ class BECIndicatorItem(QObject): """Set the position of the item""" raise NotImplementedError("Method set_position not implemented") + def check_log(self): + """Checks if the x or y axis is in log scale and updates the internal state accordingly.""" + self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked() + self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked() + self.set_position(self._pos) + def cleanup(self) -> None: """Cleanup the item""" self.remove_from_plot() @@ -43,7 +53,8 @@ class BECTickItem(BECIndicatorItem): super().__init__(plot_item=plot_item, parent=parent) self.tick_item = pg.TickSliderItem(allowAdd=False, allowRemove=False) self.tick = None - self._tick_pos = 0 + # Set _pos to float + self._pos = 0 self._range = [0, 1] @Slot(float) @@ -53,8 +64,15 @@ class BECTickItem(BECIndicatorItem): Args: pos (float): The position of the tick item. """ - self._tick_pos = pos - self.update_range(self.plot_item.vb, self._range) + if self.is_log_x is True: + pos = pos if pos > 0 else 1e-10 + pos = np.log10(pos) + self._pos = pos + view_box = self.plot_item.getViewBox() # Ensure you're accessing the correct view box + view_range = view_box.viewRange()[0] + self.update_range(self.plot_item.vb, view_range) + self.position_changed.emit(pos) + self.position_changed_str.emit(str(pos)) def update_range(self, vb, viewRange) -> None: """Update the range of the tick item @@ -69,12 +87,9 @@ class BECTickItem(BECIndicatorItem): lengthIncludingPadding = length + self.tick_item.tickSize + 2 self._range = viewRange - - tickValueIncludingPadding = (self._tick_pos - viewRange[0]) / (viewRange[1] - viewRange[0]) + tickValueIncludingPadding = (self._pos - viewRange[0]) / (viewRange[1] - viewRange[0]) tickValue = (tickValueIncludingPadding * lengthIncludingPadding - origin) / length self.tick_item.setTickValue(self.tick, tickValue) - self.position_changed.emit(self._tick_pos) - self.position_changed_str.emit(str(self._tick_pos)) def add_to_plot(self): """Add the tick item to the view box or plot item.""" @@ -82,6 +97,8 @@ class BECTickItem(BECIndicatorItem): self.plot_item.layout.addItem(self.tick_item, 4, 1) self.tick = self.tick_item.addTick(0, movable=False, color=self.accent_colors.highlight) self.plot_item.vb.sigXRangeChanged.connect(self.update_range) + self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log) + self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log) def remove_from_plot(self): """Remove the tick item from the view box or plot item.""" @@ -110,6 +127,7 @@ class BECArrowItem(BECIndicatorItem): def __init__(self, plot_item: pg.PlotItem = None, parent=None): super().__init__(plot_item=plot_item, parent=parent) self.arrow_item = pg.ArrowItem() + self._pos = (0, 0) @Slot(dict) def set_style(self, style: dict) -> None: @@ -128,11 +146,23 @@ class BECArrowItem(BECIndicatorItem): Args: pos (tuple): The position of the arrow item as a tuple (x, y). """ + self._pos = pos pos_x = pos[0] pos_y = pos[1] + if self.is_log_x is True: + pos_x = np.log10(pos_x) if pos_x > 0 else 1e-10 + view_box = self.plot_item.getViewBox() # Ensure you're accessing the correct view box + view_range = view_box.viewRange()[0] + # Avoid values outside the view range in the negative direction. Otherwise, there is + # a buggy behaviour of the arrow item and it appears at the wrong position. + if pos_x < view_range[0]: + pos_x = view_range[0] + if self.is_log_y is True: + pos_y = np.log10(pos_y) if pos_y > 0 else 1e-10 + self.arrow_item.setPos(pos_x, pos_y) - self.position_changed.emit((pos_x, pos_y)) - self.position_changed_str.emit((str(pos_x), str(pos_y))) + self.position_changed.emit(self._pos) + self.position_changed_str.emit((str(self._pos[0]), str(self._pos[1]))) def add_to_plot(self): """Add the arrow item to the view box or plot item.""" @@ -144,8 +174,16 @@ class BECArrowItem(BECIndicatorItem): ) if self.plot_item is not None: self.plot_item.addItem(self.arrow_item) + self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log) + self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log) def remove_from_plot(self): """Remove the arrow item from the view box or plot item.""" if self.plot_item is not None: self.plot_item.removeItem(self.arrow_item) + + def check_log(self): + """Checks if the x or y axis is in log scale and updates the internal state accordingly.""" + self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked() + self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked() + self.set_position(self._pos) diff --git a/tests/unit_tests/test_utils_plot_indicators.py b/tests/unit_tests/test_utils_plot_indicators.py index 46282de6..5eb5e7d5 100644 --- a/tests/unit_tests/test_utils_plot_indicators.py +++ b/tests/unit_tests/test_utils_plot_indicators.py @@ -1,43 +1,55 @@ import pyqtgraph as pg import pytest -from bec_qthemes._main import AccentColors -from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem +from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget + +from .client_mocks import mocked_client -@pytest.fixture(scope="function") -def arrow_item(): - """Fixture for the BECArrowItem class""" - item = BECArrowItem(plot_item=pg.PlotItem()) - yield item +@pytest.fixture +def plot_widget_with_arrow_item(qtbot, mocked_client): + widget = BECWaveformWidget(client=mocked_client()) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + + yield widget.waveform.arrow_item, widget.waveform.plot_item -def test_arrow_item_add_to_plot(arrow_item): +@pytest.fixture +def plot_widget_with_tick_item(qtbot, mocked_client): + widget = BECWaveformWidget(client=mocked_client()) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + + yield widget.waveform.motor_pos_tick, widget.waveform.plot_item + + +def test_arrow_item_add_to_plot(plot_widget_with_arrow_item): """Test the add_to_plot method""" + arrow_item, plot_item = plot_widget_with_arrow_item assert arrow_item.plot_item is not None assert arrow_item.plot_item.items == [] - arrow_item.accent_colors = AccentColors(theme="dark") arrow_item.add_to_plot() assert arrow_item.plot_item.items == [arrow_item.arrow_item] -def test_arrow_item_remove_to_plot(arrow_item): +def test_arrow_item_remove_to_plot(plot_widget_with_arrow_item): """Test the remove_from_plot method""" - arrow_item.accent_colors = AccentColors(theme="dark") + arrow_item, plot_item = plot_widget_with_arrow_item arrow_item.add_to_plot() assert arrow_item.plot_item.items == [arrow_item.arrow_item] arrow_item.remove_from_plot() assert arrow_item.plot_item.items == [] -def test_arrow_item_set_position(arrow_item): +def test_arrow_item_set_position(plot_widget_with_arrow_item): """Test the set_position method""" + arrow_item, plot_item = plot_widget_with_arrow_item container = [] def signal_callback(tup: tuple): container.append(tup) - arrow_item.accent_colors = AccentColors(theme="dark") arrow_item.add_to_plot() arrow_item.position_changed.connect(signal_callback) arrow_item.set_position(pos=(1, 1)) @@ -47,44 +59,37 @@ def test_arrow_item_set_position(arrow_item): assert container == [(1, 1), (2, 2)] -@pytest.fixture(scope="function") -def tick_item(): - """Fixture for the BECArrowItem class""" - item = BECTickItem(plot_item=pg.PlotItem()) - yield item - - -def test_tick_item_add_to_plot(tick_item): +def test_tick_item_add_to_plot(plot_widget_with_tick_item): """Test the add_to_plot method""" + tick_item, plot_item = plot_widget_with_tick_item assert tick_item.plot_item is not None assert tick_item.plot_item.items == [] - tick_item.accent_colors = AccentColors(theme="dark") tick_item.add_to_plot() assert tick_item.plot_item.layout.itemAt(4, 1) == tick_item.tick_item -def test_tick_item_remove_to_plot(tick_item): +def test_tick_item_remove_to_plot(plot_widget_with_tick_item): """Test the remove_from_plot method""" - tick_item.accent_colors = AccentColors(theme="dark") + tick_item, plot_item = plot_widget_with_tick_item tick_item.add_to_plot() assert tick_item.plot_item.layout.itemAt(4, 1) == tick_item.tick_item tick_item.remove_from_plot() assert tick_item.plot_item.layout.itemAt(4, 1) is None -def test_tick_item_set_position(tick_item): +def test_tick_item_set_position(plot_widget_with_tick_item): """Test the set_position method""" + tick_item, plot_item = plot_widget_with_tick_item container = [] def signal_callback(val: float): container.append(val) - tick_item.accent_colors = AccentColors(theme="dark") tick_item.add_to_plot() tick_item.position_changed.connect(signal_callback) tick_item.set_position(pos=1) - assert tick_item._tick_pos == 1 + assert tick_item._pos == 1 tick_item.set_position(pos=2) - assert tick_item._tick_pos == 2 + assert tick_item._pos == 2 assert container == [1.0, 2.0]