bec-widgets 0.112.0__py3-none-any.whl → 0.113.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. CHANGELOG.md +46 -48
  2. PKG-INFO +1 -1
  3. bec_widgets/applications/__init__.py +0 -0
  4. bec_widgets/applications/alignment/__init__.py +0 -0
  5. bec_widgets/applications/alignment/alignment_1d/__init__.py +0 -0
  6. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +265 -0
  7. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +869 -0
  8. bec_widgets/assets/app_icons/alignment_1d.png +0 -0
  9. bec_widgets/qt_utils/toolbar.py +3 -1
  10. bec_widgets/utils/bec_signal_proxy.py +54 -0
  11. bec_widgets/utils/linear_region_selector.py +15 -3
  12. bec_widgets/utils/plot_indicator_items.py +247 -0
  13. bec_widgets/widgets/bec_status_box/bec_status_box.py +5 -9
  14. bec_widgets/widgets/bec_status_box/status_item.py +18 -9
  15. bec_widgets/widgets/dap_combo_box/dap_combo_box.py +12 -13
  16. bec_widgets/widgets/figure/plots/plot_base.py +5 -0
  17. bec_widgets/widgets/figure/plots/waveform/waveform.py +17 -15
  18. bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py +151 -16
  19. bec_widgets/widgets/lmfit_dialog/lmfit_dialog_vertical.ui +47 -14
  20. bec_widgets/widgets/positioner_box/positioner_box.py +4 -1
  21. bec_widgets/widgets/scan_control/scan_control.py +38 -0
  22. bec_widgets/widgets/stop_button/stop_button.py +14 -2
  23. {bec_widgets-0.112.0.dist-info → bec_widgets-0.113.0.dist-info}/METADATA +1 -1
  24. {bec_widgets-0.112.0.dist-info → bec_widgets-0.113.0.dist-info}/RECORD +28 -25
  25. pyproject.toml +1 -1
  26. bec_widgets/assets/status_icons/error.svg +0 -3
  27. bec_widgets/assets/status_icons/not_connected.svg +0 -3
  28. bec_widgets/assets/status_icons/refresh.svg +0 -3
  29. bec_widgets/assets/status_icons/running.svg +0 -3
  30. bec_widgets/assets/status_icons/warning.svg +0 -3
  31. {bec_widgets-0.112.0.dist-info → bec_widgets-0.113.0.dist-info}/WHEEL +0 -0
  32. {bec_widgets-0.112.0.dist-info → bec_widgets-0.113.0.dist-info}/entry_points.txt +0 -0
  33. {bec_widgets-0.112.0.dist-info → bec_widgets-0.113.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ from typing import Literal
9
9
  from bec_qthemes._icon.material_icons import material_icon
10
10
  from qtpy.QtCore import QSize
11
11
  from qtpy.QtGui import QAction, QColor, QIcon
12
- from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
12
+ from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QSizePolicy, QToolBar, QToolButton, QWidget
13
13
 
14
14
  import bec_widgets
15
15
 
@@ -165,7 +165,9 @@ class WidgetAction(ToolBarAction):
165
165
  layout.setContentsMargins(0, 0, 0, 0)
166
166
  if self.label is not None:
167
167
  label = QLabel(f"{self.label}")
168
+ label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
168
169
  layout.addWidget(label)
170
+ self.widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
169
171
  layout.addWidget(self.widget)
170
172
  toolbar.addWidget(widget)
171
173
 
@@ -0,0 +1,54 @@
1
+ """ This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
2
+ Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
3
+ when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
4
+ analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
5
+ will allow you to decide by yourself when to unblock and execute the callback again."""
6
+
7
+ from pyqtgraph import SignalProxy
8
+ from qtpy.QtCore import Signal, Slot
9
+
10
+
11
+ class BECSignalProxy(SignalProxy):
12
+ """Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored
13
+
14
+ Args:
15
+ *args: Arguments to pass to the SignalProxy class
16
+ rateLimit (int): The rateLimit of the proxy
17
+ **kwargs: Keyword arguments to pass to the SignalProxy class
18
+
19
+ Example:
20
+ >>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)"""
21
+
22
+ is_blocked = Signal(bool)
23
+
24
+ def __init__(self, *args, rateLimit=25, **kwargs):
25
+ super().__init__(*args, rateLimit=rateLimit, **kwargs)
26
+ self._blocking = False
27
+ self.old_args = None
28
+ self.new_args = None
29
+
30
+ @property
31
+ def blocked(self):
32
+ """Returns if the proxy is blocked"""
33
+ return self._blocking
34
+
35
+ @blocked.setter
36
+ def blocked(self, value: bool):
37
+ self._blocking = value
38
+ self.is_blocked.emit(value)
39
+
40
+ def signalReceived(self, *args):
41
+ """Receive signal, store the args and call signalReceived from the parent class if not blocked"""
42
+ self.new_args = args
43
+ if self.blocked is True:
44
+ return
45
+ self.blocked = True
46
+ self.old_args = args
47
+ super().signalReceived(*args)
48
+
49
+ @Slot()
50
+ def unblock_proxy(self):
51
+ """Unblock the proxy, and call the signalReceived method in case there was an update of the args."""
52
+ self.blocked = False
53
+ if self.new_args != self.old_args:
54
+ self.signalReceived(*self.new_args)
@@ -23,11 +23,14 @@ class LinearRegionWrapper(QObject):
23
23
  self, plot_item: pg.PlotItem, color: QColor = None, hover_color: QColor = None, parent=None
24
24
  ):
25
25
  super().__init__(parent)
26
+ self.is_log_x = None
26
27
  self._edge_width = 2
27
28
  self.plot_item = plot_item
28
29
  self.linear_region_selector = pg.LinearRegionItem()
29
30
  self.proxy = None
30
31
  self.change_roi_color((color, hover_color))
32
+ self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
33
+ self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
31
34
 
32
35
  # Slot for changing the color of the region selector (edge and fill)
33
36
  @Slot(tuple)
@@ -63,9 +66,18 @@ class LinearRegionWrapper(QObject):
63
66
  self.plot_item.removeItem(self.linear_region_selector)
64
67
 
65
68
  def _region_change_proxy(self):
66
- """Emit the region change signal"""
67
- region = self.linear_region_selector.getRegion()
68
- self.region_changed.emit(region)
69
+ """Emit the region change signal. If the plot is in log mode, convert the region to log."""
70
+ x_low, x_high = self.linear_region_selector.getRegion()
71
+ if self.is_log_x:
72
+ x_low = 10**x_low
73
+ x_high = 10**x_high
74
+ self.region_changed.emit((x_low, x_high))
75
+
76
+ @Slot()
77
+ def check_log(self):
78
+ """Check if the plot is in log mode."""
79
+ self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
80
+ self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
69
81
 
70
82
  def cleanup(self):
71
83
  """Cleanup the widget."""
@@ -0,0 +1,247 @@
1
+ """Module to create an arrow item for a pyqtgraph plot"""
2
+
3
+ import numpy as np
4
+ import pyqtgraph as pg
5
+ from bec_lib.logger import bec_logger
6
+ from qtpy.QtCore import QObject, QPointF, Signal, Slot
7
+
8
+ from bec_widgets.utils.colors import get_accent_colors
9
+
10
+ logger = bec_logger.logger
11
+
12
+
13
+ class BECIndicatorItem(QObject):
14
+
15
+ def __init__(self, plot_item: pg.PlotItem = None, parent=None):
16
+ super().__init__(parent=parent)
17
+ self.accent_colors = get_accent_colors()
18
+ self.plot_item = plot_item
19
+ self._item_on_plot = False
20
+ self._pos = None
21
+ self.is_log_x = False
22
+ self.is_log_y = False
23
+
24
+ @property
25
+ def item_on_plot(self) -> bool:
26
+ """Returns if the item is on the plot"""
27
+ return self._item_on_plot
28
+
29
+ @item_on_plot.setter
30
+ def item_on_plot(self, value: bool) -> None:
31
+ self._item_on_plot = value
32
+
33
+ def add_to_plot(self) -> None:
34
+ """Add the item to the plot"""
35
+ raise NotImplementedError("Method add_to_plot not implemented")
36
+
37
+ def remove_from_plot(self) -> None:
38
+ """Remove the item from the plot"""
39
+ raise NotImplementedError("Method remove_from_plot not implemented")
40
+
41
+ def set_position(self, pos) -> None:
42
+ """This method should implement the logic to set the position of the
43
+ item on the plot. Depending on the child class, the position can be
44
+ a tuple (x,y) or a single value, i.e. x position where y position is fixed.
45
+ """
46
+ raise NotImplementedError("Method set_position not implemented")
47
+
48
+ def check_log(self):
49
+ """Checks if the x or y axis is in log scale and updates the internal state accordingly."""
50
+ self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
51
+ self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
52
+ self.set_position(self._pos)
53
+
54
+
55
+ class BECTickItem(BECIndicatorItem):
56
+ """Class to create a tick item which can be added to a pyqtgraph plot.
57
+ The tick item will be added to the layout of the plot item and can be used to indicate
58
+ a position"""
59
+
60
+ position_changed = Signal(float)
61
+ position_changed_str = Signal(str)
62
+
63
+ def __init__(self, plot_item: pg.PlotItem = None, parent=None):
64
+ super().__init__(plot_item=plot_item, parent=parent)
65
+ self.tick_item = pg.TickSliderItem(
66
+ parent=parent, allowAdd=False, allowRemove=False, orientation="bottom"
67
+ )
68
+ self.tick_item.skip_auto_range = True
69
+ self.tick = None
70
+ self._pos = 0.0
71
+ self._range = [0, 1]
72
+
73
+ @Slot(float)
74
+ def set_position(self, pos: float) -> None:
75
+ """Set the x position of the tick item
76
+
77
+ Args:
78
+ pos (float): The position of the tick item.
79
+ """
80
+ if self.is_log_x is True:
81
+ pos = pos if pos > 0 else 1e-10
82
+ pos = np.log10(pos)
83
+ self._pos = pos
84
+ view_box = self.plot_item.getViewBox() # Ensure you're accessing the correct view box
85
+ view_range = view_box.viewRange()[0]
86
+ self.update_range(self.plot_item.vb, view_range)
87
+ self.position_changed.emit(pos)
88
+ self.position_changed_str.emit(str(pos))
89
+
90
+ @Slot()
91
+ def update_range(self, _, view_range: tuple[float, float]) -> None:
92
+ """Update the range of the tick item
93
+
94
+ Args:
95
+ vb (pg.ViewBox): The view box.
96
+ viewRange (tuple): The view range.
97
+ """
98
+ if self._pos < view_range[0] or self._pos > view_range[1]:
99
+ self.tick_item.setVisible(False)
100
+ else:
101
+ self.tick_item.setVisible(True)
102
+
103
+ if self.tick_item.isVisible():
104
+ origin = self.tick_item.tickSize / 2.0
105
+ length = self.tick_item.length
106
+
107
+ length_with_padding = length + self.tick_item.tickSize + 2
108
+
109
+ self._range = view_range
110
+ tick_with_padding = (self._pos - view_range[0]) / (view_range[1] - view_range[0])
111
+ tick_value = (tick_with_padding * length_with_padding - origin) / length
112
+ self.tick_item.setTickValue(self.tick, tick_value)
113
+
114
+ def add_to_plot(self):
115
+ """Add the tick item to the view box or plot item."""
116
+ if self.plot_item is None:
117
+ return
118
+
119
+ self.plot_item.layout.addItem(self.tick_item, 2, 1)
120
+ self.tick_item.setOrientation("top")
121
+ self.tick = self.tick_item.addTick(0, movable=False, color=self.accent_colors.highlight)
122
+ self.update_tick_pos_y()
123
+ self.plot_item.vb.sigXRangeChanged.connect(self.update_range)
124
+ self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
125
+ self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
126
+ self.plot_item.vb.geometryChanged.connect(self.update_tick_pos_y)
127
+ self.item_on_plot = True
128
+
129
+ @Slot()
130
+ def update_tick_pos_y(self):
131
+ """Update tick position, while respecting the tick_item coordinates"""
132
+ pos = self.tick.pos()
133
+ pos = self.tick_item.mapToParent(pos)
134
+ new_pos = self.plot_item.vb.geometry().bottom()
135
+ new_pos = self.tick_item.mapFromParent(QPointF(pos.x(), new_pos))
136
+ self.tick.setPos(new_pos)
137
+
138
+ def remove_from_plot(self):
139
+ """Remove the tick item from the view box or plot item."""
140
+ if self.plot_item is not None and self.item_on_plot is True:
141
+ self.plot_item.vb.sigXRangeChanged.disconnect(self.update_range)
142
+ self.plot_item.ctrl.logXCheck.checkStateChanged.disconnect(self.check_log)
143
+ self.plot_item.ctrl.logYCheck.checkStateChanged.disconnect(self.check_log)
144
+ if self.plot_item.layout is not None:
145
+ self.plot_item.layout.removeItem(self.tick_item)
146
+ self.item_on_plot = False
147
+
148
+ def cleanup(self) -> None:
149
+ """Cleanup the item"""
150
+ self.remove_from_plot()
151
+ if self.tick_item is not None:
152
+ self.tick_item.close()
153
+ self.tick_item.deleteLater()
154
+ self.tick_item = None
155
+
156
+
157
+ class BECArrowItem(BECIndicatorItem):
158
+ """Class to create an arrow item which can be added to a pyqtgraph plot.
159
+ It can be either added directly to a view box or a plot item.
160
+ To add the arrow item to a view box or plot item, use the add_to_plot method.
161
+
162
+ Args:
163
+ view_box (pg.ViewBox | pg.PlotItem): The view box or plot item to which the arrow item should be added.
164
+ parent (QObject): The parent object.
165
+
166
+ Signals:
167
+ position_changed (tuple[float, float]): Signal emitted when the position of the arrow item has changed.
168
+ position_changed_str (tuple[str, str]): Signal emitted when the position of the arrow item has changed.
169
+ """
170
+
171
+ # Signal to emit if the position of the arrow item has changed
172
+ position_changed = Signal(tuple)
173
+ position_changed_str = Signal(tuple)
174
+
175
+ def __init__(self, plot_item: pg.PlotItem = None, parent=None):
176
+ super().__init__(plot_item=plot_item, parent=parent)
177
+ self.arrow_item = pg.ArrowItem(parent=parent)
178
+ self.arrow_item.skip_auto_range = True
179
+ self._pos = (0, 0)
180
+ self.arrow_item.setVisible(False)
181
+
182
+ @Slot(dict)
183
+ def set_style(self, style: dict) -> None:
184
+ """Set the style of the arrow item
185
+
186
+ Args:
187
+ style (dict): The style of the arrow item. Dictionary with key,
188
+ value pairs which are accepted from the pg.ArrowItem.setStyle method.
189
+ """
190
+ self.arrow_item.setStyle(**style)
191
+
192
+ @Slot(tuple)
193
+ def set_position(self, pos: tuple[float, float]) -> None:
194
+ """Set the position of the arrow item
195
+
196
+ Args:
197
+ pos (tuple): The position of the arrow item as a tuple (x, y).
198
+ """
199
+ self._pos = pos
200
+ pos_x = pos[0]
201
+ pos_y = pos[1]
202
+ if self.is_log_x is True:
203
+ pos_x = np.log10(pos_x) if pos_x > 0 else 1e-10
204
+ view_box = self.plot_item.getViewBox() # Ensure you're accessing the correct view box
205
+ view_range = view_box.viewRange()[0]
206
+ # Avoid values outside the view range in the negative direction. Otherwise, there is
207
+ # a buggy behaviour of the arrow item and it appears at the wrong position.
208
+ if pos_x < view_range[0]:
209
+ pos_x = view_range[0]
210
+ if self.is_log_y is True:
211
+ pos_y = np.log10(pos_y) if pos_y > 0 else 1e-10
212
+
213
+ self.arrow_item.setPos(pos_x, pos_y)
214
+ self.position_changed.emit(self._pos)
215
+ self.position_changed_str.emit((str(self._pos[0]), str(self._pos[1])))
216
+
217
+ def add_to_plot(self):
218
+ """Add the arrow item to the view box or plot item."""
219
+ if not self.arrow_item:
220
+ logger.warning(f"Arrow item was already destroyed, cannot be created")
221
+ return
222
+
223
+ self.arrow_item.setStyle(
224
+ angle=-90,
225
+ pen=pg.mkPen(self.accent_colors.emergency, width=1),
226
+ brush=pg.mkBrush(self.accent_colors.highlight),
227
+ headLen=20,
228
+ )
229
+ self.arrow_item.setVisible(True)
230
+ if self.plot_item is not None:
231
+ self.plot_item.addItem(self.arrow_item)
232
+ self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
233
+ self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
234
+ self.item_on_plot = True
235
+
236
+ def remove_from_plot(self):
237
+ """Remove the arrow item from the view box or plot item."""
238
+ if self.plot_item is not None and self.item_on_plot is True:
239
+ self.plot_item.ctrl.logXCheck.checkStateChanged.disconnect(self.check_log)
240
+ self.plot_item.ctrl.logYCheck.checkStateChanged.disconnect(self.check_log)
241
+ self.plot_item.removeItem(self.arrow_item)
242
+ self.item_on_plot = False
243
+
244
+ def cleanup(self) -> None:
245
+ """Cleanup the item"""
246
+ self.remove_from_plot()
247
+ self.arrow_item = None
@@ -14,7 +14,7 @@ from qtpy.QtCore import QObject, QTimer, Signal, Slot
14
14
  from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
15
15
 
16
16
  from bec_widgets.utils.bec_widget import BECWidget
17
- from bec_widgets.utils.colors import apply_theme
17
+ from bec_widgets.utils.colors import set_theme
18
18
  from bec_widgets.widgets.bec_status_box.status_item import StatusItem
19
19
 
20
20
  if TYPE_CHECKING:
@@ -303,17 +303,13 @@ class BECStatusBox(BECWidget, QWidget):
303
303
  return super().cleanup()
304
304
 
305
305
 
306
- def main():
307
- """Main method to run the BECStatusBox widget."""
308
- # pylint: disable=import-outside-toplevel
306
+ if __name__ == "__main__": # pragma: no cover
307
+ import sys
308
+
309
309
  from qtpy.QtWidgets import QApplication
310
310
 
311
311
  app = QApplication(sys.argv)
312
- apply_theme("dark")
312
+ set_theme("dark")
313
313
  main_window = BECStatusBox()
314
314
  main_window.show()
315
315
  sys.exit(app.exec())
316
-
317
-
318
- if __name__ == "__main__":
319
- main()
@@ -6,11 +6,13 @@ import os
6
6
  from datetime import datetime
7
7
 
8
8
  from bec_lib.utils.import_utils import lazy_import_from
9
+ from bec_qthemes import material_icon
9
10
  from qtpy.QtCore import Qt, Slot
10
- from qtpy.QtGui import QIcon
11
+ from qtpy.QtGui import QIcon, QPainter
11
12
  from qtpy.QtWidgets import QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget
12
13
 
13
14
  import bec_widgets
15
+ from bec_widgets.utils.colors import get_accent_colors
14
16
 
15
17
  # TODO : Put normal imports back when Pydantic gets faster
16
18
  BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
@@ -21,11 +23,11 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
21
23
  class IconsEnum(enum.Enum):
22
24
  """Enum class for icons in the status item widget."""
23
25
 
24
- RUNNING = os.path.join(MODULE_PATH, "assets", "status_icons", "running.svg")
25
- BUSY = os.path.join(MODULE_PATH, "assets", "status_icons", "refresh.svg")
26
- IDLE = os.path.join(MODULE_PATH, "assets", "status_icons", "warning.svg")
27
- ERROR = os.path.join(MODULE_PATH, "assets", "status_icons", "error.svg")
28
- NOTCONNECTED = os.path.join(MODULE_PATH, "assets", "status_icons", "not_connected.svg")
26
+ RUNNING = "done_outline"
27
+ BUSY = "progress_activity"
28
+ IDLE = "progress_activity"
29
+ ERROR = "emergency_home"
30
+ NOTCONNECTED = "signal_disconnected"
29
31
 
30
32
 
31
33
  class StatusItem(QWidget):
@@ -43,13 +45,13 @@ class StatusItem(QWidget):
43
45
  raise ValueError(
44
46
  "Please initialize the StatusItem with a BECServiceInfoContainer for config, received None."
45
47
  )
48
+ self.accent_colors = get_accent_colors()
46
49
  self.config = config
47
50
  self.parent = parent
48
51
  self.layout = None
49
52
  self._label = None
50
53
  self._icon = None
51
54
  self.icon_size = (24, 24)
52
-
53
55
  self._popup_label_ref = {}
54
56
  self.init_ui()
55
57
 
@@ -97,8 +99,15 @@ class StatusItem(QWidget):
97
99
 
98
100
  def set_status(self) -> None:
99
101
  """Set the status icon for the status item widget."""
100
- icon_path = IconsEnum[self.config.status].value
101
- icon = QIcon(icon_path)
102
+ status = self.config.status
103
+ if status in ["RUNNING", "BUSY"]:
104
+ color = self.accent_colors.success
105
+ elif status == "IDLE":
106
+ color = self.accent_colors.warning
107
+ elif status in ["ERROR", "NOTCONNECTED"]:
108
+ color = self.accent_colors.emergency
109
+ icon = QIcon(material_icon(IconsEnum[status].value, filled=True, color=color))
110
+
102
111
  self._icon.setPixmap(icon.pixmap(*self.icon_size))
103
112
  self._icon.setAlignment(Qt.AlignmentFlag.AlignRight)
104
113
 
@@ -124,6 +124,7 @@ class DapComboBox(BECWidget, QWidget):
124
124
  x_axis(str): X axis.
125
125
  """
126
126
  self.x_axis = x_axis
127
+ self._update_current_fit(self.fit_model_combobox.currentText())
127
128
 
128
129
  @Slot(str)
129
130
  def select_y_axis(self, y_axis: str):
@@ -133,6 +134,7 @@ class DapComboBox(BECWidget, QWidget):
133
134
  y_axis(str): Y axis.
134
135
  """
135
136
  self.y_axis = y_axis
137
+ self._update_current_fit(self.fit_model_combobox.currentText())
136
138
 
137
139
  @Slot(str)
138
140
  def select_fit_model(self, fit_name: str | None):
@@ -165,21 +167,18 @@ class DapComboBox(BECWidget, QWidget):
165
167
  return True
166
168
 
167
169
 
168
- # pragma: no cover
169
- def main():
170
- """Main function to run the DapComboBox widget."""
171
- import sys
172
-
170
+ if __name__ == "__main__": # pragma: no cover
171
+ # pylint: disable=import-outside-toplevel
173
172
  from qtpy.QtWidgets import QApplication
174
173
 
175
174
  from bec_widgets.utils.colors import set_theme
176
175
 
177
- app = QApplication(sys.argv)
178
- set_theme("auto")
179
- widget = DapComboBox()
176
+ app = QApplication([])
177
+ set_theme("dark")
178
+ widget = QWidget()
179
+ widget.setFixedSize(200, 200)
180
+ layout = QVBoxLayout()
181
+ widget.setLayout(layout)
182
+ layout.addWidget(DapComboBox())
180
183
  widget.show()
181
- sys.exit(app.exec_())
182
-
183
-
184
- if __name__ == "__main__":
185
- main()
184
+ app.exec_()
@@ -12,6 +12,7 @@ from qtpy.QtWidgets import QApplication, QWidget
12
12
 
13
13
  from bec_widgets.utils import BECConnector, ConnectionConfig
14
14
  from bec_widgets.utils.crosshair import Crosshair
15
+ from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
15
16
 
16
17
  logger = bec_logger.logger
17
18
 
@@ -105,6 +106,8 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
105
106
 
106
107
  self.add_legend()
107
108
  self.crosshair = None
109
+ self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
110
+ self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
108
111
  self._connect_to_theme_change()
109
112
 
110
113
  def _connect_to_theme_change(self):
@@ -428,6 +431,8 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
428
431
  def cleanup_pyqtgraph(self):
429
432
  """Cleanup pyqtgraph items."""
430
433
  self.unhook_crosshair()
434
+ self.tick_item.cleanup()
435
+ self.arrow_item.cleanup()
431
436
  item = self.plot_item
432
437
  item.vb.menu.close()
433
438
  item.vb.menu.deleteLater()
@@ -1,3 +1,4 @@
1
+ # pylint: disable=too_many_lines
1
2
  from __future__ import annotations
2
3
 
3
4
  from collections import defaultdict
@@ -12,7 +13,7 @@ from bec_lib.logger import bec_logger
12
13
  from pydantic import Field, ValidationError, field_validator
13
14
  from pyqtgraph.exporters import MatplotlibExporter
14
15
  from qtpy.QtCore import Signal as pyqtSignal
15
- from qtpy.QtWidgets import QApplication, QWidget
16
+ from qtpy.QtWidgets import QWidget
16
17
 
17
18
  from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
18
19
  from bec_widgets.utils import Colors, EntryValidator
@@ -165,7 +166,7 @@ class BECWaveform(BECPlotBase):
165
166
  self.roi_select.linear_region_selector.setRegion(region)
166
167
  except Exception as e:
167
168
  logger.error(f"Error setting region {tuple}; Exception raised: {e}")
168
- raise ValueError(f"Error setting region {tuple}; Exception raised: {e}")
169
+ raise ValueError(f"Error setting region {tuple}; Exception raised: {e}") from e
169
170
 
170
171
  def _hook_roi(self):
171
172
  """Hook the linear region selector to the plot."""
@@ -219,7 +220,7 @@ class BECWaveform(BECPlotBase):
219
220
  # Reset curves
220
221
  self._curves_data = defaultdict(dict)
221
222
  self._curves = self.plot_item.curves
222
- for curve_id, curve_config in self.config.curves.items():
223
+ for curve_config in self.config.curves.values():
223
224
  self.add_curve_by_config(curve_config)
224
225
  if replot_last_scan:
225
226
  self.scan_history(scan_index=-1)
@@ -342,7 +343,7 @@ class BECWaveform(BECPlotBase):
342
343
  Returns:
343
344
  CurveConfig|dict: Configuration of the curve.
344
345
  """
345
- for source, curves in self._curves_data.items():
346
+ for curves in self._curves_data.values():
346
347
  if curve_id in curves:
347
348
  if dict_output:
348
349
  return curves[curve_id].config.model_dump()
@@ -362,7 +363,7 @@ class BECWaveform(BECPlotBase):
362
363
  if isinstance(identifier, int):
363
364
  return self.plot_item.curves[identifier]
364
365
  elif isinstance(identifier, str):
365
- for source_type, curves in self._curves_data.items():
366
+ for curves in self._curves_data.values():
366
367
  if identifier in curves:
367
368
  return curves[identifier]
368
369
  raise ValueError(f"Curve with ID '{identifier}' not found.")
@@ -513,6 +514,7 @@ class BECWaveform(BECPlotBase):
513
514
 
514
515
  @Slot()
515
516
  def auto_range(self):
517
+ """Manually set auto range of the plotitem"""
516
518
  self.plot_item.autoRange()
517
519
 
518
520
  def set_auto_range(self, enabled: bool, axis: str = "xy"):
@@ -551,7 +553,6 @@ class BECWaveform(BECPlotBase):
551
553
  Returns:
552
554
  BECCurve: The curve object.
553
555
  """
554
- curve_source = curve_source
555
556
  curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
556
557
 
557
558
  curve_exits = self._check_curve_id(curve_id, self._curves_data)
@@ -725,7 +726,7 @@ class BECWaveform(BECPlotBase):
725
726
 
726
727
  if self.x_axis_mode["readout_priority"] == "async":
727
728
  raise ValueError(
728
- f"Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
729
+ "Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
729
730
  )
730
731
 
731
732
  if validate_bec is True:
@@ -999,7 +1000,7 @@ class BECWaveform(BECPlotBase):
999
1000
  Args:
1000
1001
  curve_id(str): ID of the curve to be removed.
1001
1002
  """
1002
- for source, curves in self._curves_data.items():
1003
+ for curves in self._curves_data.values():
1003
1004
  if curve_id in curves:
1004
1005
  curve = curves.pop(curve_id)
1005
1006
  self.plot_item.removeItem(curve)
@@ -1022,7 +1023,7 @@ class BECWaveform(BECPlotBase):
1022
1023
  self.plot_item.removeItem(curve)
1023
1024
  del self.config.curves[curve_id]
1024
1025
  # Remove from self.curve_data
1025
- for source, curves in self._curves_data.items():
1026
+ for curves in self._curves_data.values():
1026
1027
  if curve_id in curves:
1027
1028
  del curves[curve_id]
1028
1029
  break
@@ -1052,7 +1053,7 @@ class BECWaveform(BECPlotBase):
1052
1053
  if self._curves_data["DAP"]:
1053
1054
  self.setup_dap(self.old_scan_id, self.scan_id)
1054
1055
  if self._curves_data["async"]:
1055
- for curve_id, curve in self._curves_data["async"].items():
1056
+ for curve in self._curves_data["async"].values():
1056
1057
  self.setup_async(
1057
1058
  name=curve.config.signals.y.name, entry=curve.config.signals.y.entry
1058
1059
  )
@@ -1184,7 +1185,9 @@ class BECWaveform(BECPlotBase):
1184
1185
 
1185
1186
  @Slot(dict, dict)
1186
1187
  def update_dap(self, msg, metadata):
1187
- self.msg = msg
1188
+ """Callback for DAP response message."""
1189
+
1190
+ # pylint: disable=unused-variable
1188
1191
  scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
1189
1192
  model = msg["dap_request"].content["config"]["class_kwargs"]["model"]
1190
1193
 
@@ -1213,7 +1216,7 @@ class BECWaveform(BECPlotBase):
1213
1216
  metadata(dict): Metadata of the message.
1214
1217
  """
1215
1218
  instruction = metadata.get("async_update")
1216
- for curve_id, curve in self._curves_data["async"].items():
1219
+ for curve in self._curves_data["async"].values():
1217
1220
  y_name = curve.config.signals.y.name
1218
1221
  y_entry = curve.config.signals.y.entry
1219
1222
  x_name = self._x_axis_mode["name"]
@@ -1337,7 +1340,7 @@ class BECWaveform(BECPlotBase):
1337
1340
  if self._x_axis_mode["name"] is None or self._x_axis_mode["name"] == "best_effort":
1338
1341
  if len(self._curves_data["async"]) > 0:
1339
1342
  x_data = None
1340
- self._x_axis_mode["label_suffix"] = f" [auto: index]"
1343
+ self._x_axis_mode["label_suffix"] = " [auto: index]"
1341
1344
  current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
1342
1345
  self.plot_item.setLabel(
1343
1346
  "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
@@ -1414,7 +1417,6 @@ class BECWaveform(BECPlotBase):
1414
1417
  self.scan_signal_update.emit()
1415
1418
  self.async_signal_update.emit()
1416
1419
 
1417
- # pylint: ignore: undefined-variable
1418
1420
  def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: # | pd.DataFrame:
1419
1421
  """
1420
1422
  Extract all curve data into a dictionary or a pandas DataFrame.
@@ -1484,7 +1486,7 @@ class BECWaveform(BECPlotBase):
1484
1486
  self.bec_dispatcher.disconnect_slot(
1485
1487
  self.update_dap, MessageEndpoints.dap_response(self.scan_id)
1486
1488
  )
1487
- for curve_id, curve in self._curves_data["async"].items():
1489
+ for curve_id in self._curves_data["async"]:
1488
1490
  self.bec_dispatcher.disconnect_slot(
1489
1491
  self.on_async_readback,
1490
1492
  MessageEndpoints.device_async_readback(self.scan_id, curve_id),