bec-widgets 1.4.0__py3-none-any.whl → 1.5.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.
@@ -6,13 +6,16 @@ from qtpy.QtCore import QObject, Qt, Signal, Slot
6
6
  from qtpy.QtWidgets import QApplication
7
7
 
8
8
 
9
- class NonDownsamplingScatterPlotItem(pg.ScatterPlotItem):
9
+ class CrosshairScatterItem(pg.ScatterPlotItem):
10
10
  def setDownsampling(self, ds=None, auto=None, method=None):
11
11
  pass
12
12
 
13
13
  def setClipToView(self, state):
14
14
  pass
15
15
 
16
+ def setAlpha(self, *args, **kwargs):
17
+ pass
18
+
16
19
 
17
20
  class Crosshair(QObject):
18
21
  # QT Position of mouse cursor
@@ -47,9 +50,15 @@ class Crosshair(QObject):
47
50
  self.v_line.skip_auto_range = True
48
51
  self.h_line = pg.InfiniteLine(angle=0, movable=False)
49
52
  self.h_line.skip_auto_range = True
53
+ # Add custom attribute to identify crosshair lines
54
+ self.v_line.is_crosshair = True
55
+ self.h_line.is_crosshair = True
50
56
  self.plot_item.addItem(self.v_line, ignoreBounds=True)
51
57
  self.plot_item.addItem(self.h_line, ignoreBounds=True)
52
58
 
59
+ # Initialize highlighted curve in a case of multiple curves
60
+ self.highlighted_curve_index = None
61
+
53
62
  # Add TextItem to display coordinates
54
63
  self.coord_label = pg.TextItem("", anchor=(1, 1), fill=(0, 0, 0, 100))
55
64
  self.coord_label.setVisible(False) # Hide initially
@@ -70,6 +79,7 @@ class Crosshair(QObject):
70
79
  self.plot_item.ctrl.downsampleSpin.valueChanged.connect(self.clear_markers)
71
80
 
72
81
  # Initialize markers
82
+ self.items = []
73
83
  self.marker_moved_1d = {}
74
84
  self.marker_clicked_1d = {}
75
85
  self.marker_2d = None
@@ -113,34 +123,74 @@ class Crosshair(QObject):
113
123
  self.coord_label.fill = pg.mkBrush(label_bg_color)
114
124
  self.coord_label.border = pg.mkPen(None)
115
125
 
126
+ @Slot(int)
127
+ def update_highlighted_curve(self, curve_index: int):
128
+ """
129
+ Update the highlighted curve in the case of multiple curves in a plot item.
130
+
131
+ Args:
132
+ curve_index(int): The index of curve to highlight
133
+ """
134
+ self.highlighted_curve_index = curve_index
135
+ self.clear_markers()
136
+ self.update_markers()
137
+
116
138
  def update_markers(self):
117
139
  """Update the markers for the crosshair, creating new ones if necessary."""
118
140
 
119
- # Create new markers
120
- for item in self.plot_item.items:
141
+ if self.highlighted_curve_index is not None and hasattr(self.plot_item, "visible_curves"):
142
+ # Focus on the highlighted curve only
143
+ self.items = [self.plot_item.visible_curves[self.highlighted_curve_index]]
144
+ else:
145
+ # Handle all curves
146
+ self.items = self.plot_item.items
147
+
148
+ # Create or update markers
149
+ for item in self.items:
121
150
  if isinstance(item, pg.PlotDataItem): # 1D plot
122
- if item.name() in self.marker_moved_1d:
123
- continue
124
151
  pen = item.opts["pen"]
125
152
  color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
126
- marker_moved = NonDownsamplingScatterPlotItem(
127
- size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
128
- )
129
- marker_moved.skip_auto_range = True
130
- self.marker_moved_1d[item.name()] = marker_moved
131
- self.plot_item.addItem(marker_moved)
132
-
133
- # Create glowing effect markers for clicked events
134
- for size, alpha in [(18, 64), (14, 128), (10, 255)]:
135
- marker_clicked = NonDownsamplingScatterPlotItem(
136
- size=size,
137
- pen=pg.mkPen(None),
138
- brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
153
+ name = item.name() or str(id(item))
154
+ if name in self.marker_moved_1d:
155
+ # Update existing markers
156
+ marker_moved = self.marker_moved_1d[name]
157
+ marker_moved.setPen(pg.mkPen(color))
158
+ # Update clicked markers' brushes
159
+ for marker_clicked in self.marker_clicked_1d[name]:
160
+ alpha = marker_clicked.opts["brush"].color().alpha()
161
+ marker_clicked.setBrush(
162
+ pg.mkBrush(color.red(), color.green(), color.blue(), alpha)
163
+ )
164
+ # Update z-values
165
+ marker_moved.setZValue(item.zValue() + 1)
166
+ for marker_clicked in self.marker_clicked_1d[name]:
167
+ marker_clicked.setZValue(item.zValue() + 1)
168
+ else:
169
+ # Create new markers
170
+ marker_moved = CrosshairScatterItem(
171
+ size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
139
172
  )
140
- marker_clicked.skip_auto_range = True
141
- self.marker_clicked_1d[item.name()] = marker_clicked
142
- self.plot_item.addItem(marker_clicked)
143
-
173
+ marker_moved.skip_auto_range = True
174
+ marker_moved.is_crosshair = True
175
+ self.marker_moved_1d[name] = marker_moved
176
+ self.plot_item.addItem(marker_moved)
177
+ # Set marker z-value higher than the curve
178
+ marker_moved.setZValue(item.zValue() + 1)
179
+
180
+ # Create glowing effect markers for clicked events
181
+ marker_clicked_list = []
182
+ for size, alpha in [(18, 64), (14, 128), (10, 255)]:
183
+ marker_clicked = CrosshairScatterItem(
184
+ size=size,
185
+ pen=pg.mkPen(None),
186
+ brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
187
+ )
188
+ marker_clicked.skip_auto_range = True
189
+ marker_clicked.is_crosshair = True
190
+ self.plot_item.addItem(marker_clicked)
191
+ marker_clicked.setZValue(item.zValue() + 1)
192
+ marker_clicked_list.append(marker_clicked)
193
+ self.marker_clicked_1d[name] = marker_clicked_list
144
194
  elif isinstance(item, pg.ImageItem): # 2D plot
145
195
  if self.marker_2d is not None:
146
196
  continue
@@ -162,12 +212,11 @@ class Crosshair(QObject):
162
212
  """
163
213
  y_values = defaultdict(list)
164
214
  x_values = defaultdict(list)
165
- image_2d = None
166
215
 
167
216
  # Iterate through items in the plot
168
- for item in self.plot_item.items:
217
+ for item in self.items:
169
218
  if isinstance(item, pg.PlotDataItem): # 1D plot
170
- name = item.name()
219
+ name = item.name() or str(id(item))
171
220
  plot_data = item._getDisplayDataset()
172
221
  if plot_data is None:
173
222
  continue
@@ -188,7 +237,7 @@ class Crosshair(QObject):
188
237
  elif isinstance(item, pg.ImageItem): # 2D plot
189
238
  name = item.config.monitor
190
239
  image_2d = item.image
191
- # clip the x and y values to the image dimensions to avoid out of bounds errors
240
+ # Clip the x and y values to the image dimensions to avoid out of bounds errors
192
241
  y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
193
242
  x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
194
243
 
@@ -256,9 +305,9 @@ class Crosshair(QObject):
256
305
  # not sure how we got here, but just to be safe...
257
306
  return
258
307
 
259
- for item in self.plot_item.items:
308
+ for item in self.items:
260
309
  if isinstance(item, pg.PlotDataItem):
261
- name = item.name()
310
+ name = item.name() or str(id(item))
262
311
  x, y = x_snap_values[name], y_snap_values[name]
263
312
  if x is None or y is None:
264
313
  continue
@@ -309,13 +358,14 @@ class Crosshair(QObject):
309
358
  # not sure how we got here, but just to be safe...
310
359
  return
311
360
 
312
- for item in self.plot_item.items:
361
+ for item in self.items:
313
362
  if isinstance(item, pg.PlotDataItem):
314
- name = item.name()
363
+ name = item.name() or str(id(item))
315
364
  x, y = x_snap_values[name], y_snap_values[name]
316
365
  if x is None or y is None:
317
366
  continue
318
- self.marker_clicked_1d[name].setData([x], [y])
367
+ for marker_clicked in self.marker_clicked_1d[name]:
368
+ marker_clicked.setData([x], [y])
319
369
  x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
320
370
  coordinate_to_emit = (
321
371
  name,
@@ -337,9 +387,12 @@ class Crosshair(QObject):
337
387
  def clear_markers(self):
338
388
  """Clears the markers from the plot."""
339
389
  for marker in self.marker_moved_1d.values():
340
- marker.clear()
341
- for marker in self.marker_clicked_1d.values():
342
- marker.clear()
390
+ self.plot_item.removeItem(marker)
391
+ for markers in self.marker_clicked_1d.values():
392
+ for marker in markers:
393
+ self.plot_item.removeItem(marker)
394
+ self.marker_moved_1d.clear()
395
+ self.marker_clicked_1d.clear()
343
396
 
344
397
  def scale_emitted_coordinates(self, x, y):
345
398
  """Scales the emitted coordinates if the axes are in log scale.
@@ -366,7 +419,7 @@ class Crosshair(QObject):
366
419
  x, y = pos
367
420
  x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
368
421
 
369
- # # Update coordinate label
422
+ # Update coordinate label
370
423
  self.coord_label.setText(f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})")
371
424
  self.coord_label.setPos(x, y)
372
425
  self.coord_label.setVisible(True)
@@ -24,6 +24,7 @@ from bec_widgets.widgets.dark_mode_button.dark_mode_button import DarkModeButton
24
24
  from bec_widgets.widgets.dock.dock import BECDock, DockConfig
25
25
  from bec_widgets.widgets.image.image_widget import BECImageWidget
26
26
  from bec_widgets.widgets.motor_map.motor_map_widget import BECMotorMapWidget
27
+ from bec_widgets.widgets.multi_waveform.multi_waveform_widget import BECMultiWaveformWidget
27
28
  from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
28
29
  from bec_widgets.widgets.ring_progress_bar.ring_progress_bar import RingProgressBar
29
30
  from bec_widgets.widgets.scan_control.scan_control import ScanControl
@@ -85,6 +86,11 @@ class BECDockArea(BECWidget, QWidget):
85
86
  tooltip="Add Waveform",
86
87
  filled=True,
87
88
  ),
89
+ "multi_waveform": MaterialIconAction(
90
+ icon_name=BECMultiWaveformWidget.ICON_NAME,
91
+ tooltip="Add Multi Waveform",
92
+ filled=True,
93
+ ),
88
94
  "image": MaterialIconAction(
89
95
  icon_name=BECImageWidget.ICON_NAME, tooltip="Add Image", filled=True
90
96
  ),
@@ -154,6 +160,9 @@ class BECDockArea(BECWidget, QWidget):
154
160
  self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect(
155
161
  lambda: self.add_dock(widget="BECWaveformWidget", prefix="waveform")
156
162
  )
163
+ self.toolbar.widgets["menu_plots"].widgets["multi_waveform"].triggered.connect(
164
+ lambda: self.add_dock(widget="BECMultiWaveformWidget", prefix="multi_waveform")
165
+ )
157
166
  self.toolbar.widgets["menu_plots"].widgets["image"].triggered.connect(
158
167
  lambda: self.add_dock(widget="BECImageWidget", prefix="image")
159
168
  )
@@ -11,6 +11,7 @@ from bec_lib.logger import bec_logger
11
11
  from pydantic import Field, ValidationError, field_validator
12
12
  from qtpy.QtCore import Signal as pyqtSignal
13
13
  from qtpy.QtWidgets import QWidget
14
+ from tornado.gen import multi
14
15
  from typeguard import typechecked
15
16
 
16
17
  from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
@@ -18,6 +19,10 @@ from bec_widgets.utils.bec_widget import BECWidget
18
19
  from bec_widgets.utils.colors import apply_theme
19
20
  from bec_widgets.widgets.figure.plots.image.image import BECImageShow, ImageConfig
20
21
  from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap, MotorMapConfig
22
+ from bec_widgets.widgets.figure.plots.multi_waveform.multi_waveform import (
23
+ BECMultiWaveform,
24
+ BECMultiWaveformConfig,
25
+ )
21
26
  from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
22
27
  from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform, Waveform1DConfig
23
28
 
@@ -64,6 +69,7 @@ class WidgetHandler:
64
69
  "BECWaveform": (BECWaveform, Waveform1DConfig),
65
70
  "BECImageShow": (BECImageShow, ImageConfig),
66
71
  "BECMotorMap": (BECMotorMap, MotorMapConfig),
72
+ "BECMultiWaveform": (BECMultiWaveform, BECMultiWaveformConfig),
67
73
  }
68
74
 
69
75
  def create_widget(
@@ -134,8 +140,14 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
134
140
  "BECWaveform": BECWaveform,
135
141
  "BECImageShow": BECImageShow,
136
142
  "BECMotorMap": BECMotorMap,
143
+ "BECMultiWaveform": BECMultiWaveform,
144
+ }
145
+ widget_method_map = {
146
+ "BECWaveform": "plot",
147
+ "BECImageShow": "image",
148
+ "BECMotorMap": "motor_map",
149
+ "BECMultiWaveform": "multi_waveform",
137
150
  }
138
- widget_method_map = {"BECWaveform": "plot", "BECImageShow": "image", "BECMotorMap": "motor_map"}
139
151
 
140
152
  clean_signal = pyqtSignal()
141
153
 
@@ -445,10 +457,27 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
445
457
 
446
458
  return motor_map
447
459
 
460
+ def multi_waveform(
461
+ self,
462
+ monitor: str = None,
463
+ new: bool = False,
464
+ row: int | None = None,
465
+ col: int | None = None,
466
+ config: dict | None = None,
467
+ **axis_kwargs,
468
+ ):
469
+ multi_waveform = self.subplot_factory(
470
+ widget_type="BECMultiWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
471
+ )
472
+ if config is not None:
473
+ return multi_waveform
474
+ multi_waveform.set_monitor(monitor)
475
+ return multi_waveform
476
+
448
477
  def subplot_factory(
449
478
  self,
450
479
  widget_type: Literal[
451
- "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap"
480
+ "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap", "BECMultiWaveform"
452
481
  ] = "BECPlotBase",
453
482
  row: int = None,
454
483
  col: int = None,
@@ -500,7 +529,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
500
529
  def add_widget(
501
530
  self,
502
531
  widget_type: Literal[
503
- "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap"
532
+ "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap", "BECMultiWaveform"
504
533
  ] = "BECPlotBase",
505
534
  widget_id: str = None,
506
535
  row: int = None,
@@ -0,0 +1,353 @@
1
+ from collections import deque
2
+ from typing import Literal, Optional
3
+
4
+ import pyqtgraph as pg
5
+ from bec_lib.endpoints import MessageEndpoints
6
+ from bec_lib.logger import bec_logger
7
+ from pydantic import Field, field_validator
8
+ from pyqtgraph.exporters import MatplotlibExporter
9
+ from qtpy.QtCore import Signal, Slot
10
+ from qtpy.QtWidgets import QWidget
11
+
12
+ from bec_widgets.utils import Colors
13
+ from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
14
+
15
+ logger = bec_logger.logger
16
+
17
+
18
+ class BECMultiWaveformConfig(SubplotConfig):
19
+ color_palette: Optional[str] = Field(
20
+ "magma", description="The color palette of the figure widget.", validate_default=True
21
+ )
22
+ curve_limit: Optional[int] = Field(
23
+ 200, description="The maximum number of curves to display on the plot."
24
+ )
25
+ flush_buffer: Optional[bool] = Field(
26
+ False, description="Flush the buffer of the plot widget when the curve limit is reached."
27
+ )
28
+ monitor: Optional[str] = Field(
29
+ None, description="The monitor to set for the plot widget."
30
+ ) # TODO validate monitor in bec -> maybe make it as SignalData class for validation purpose
31
+ curve_width: Optional[int] = Field(1, description="The width of the curve on the plot.")
32
+ opacity: Optional[int] = Field(50, description="The opacity of the curve on the plot.")
33
+ highlight_last_curve: Optional[bool] = Field(
34
+ True, description="Highlight the last curve on the plot."
35
+ )
36
+
37
+ model_config: dict = {"validate_assignment": True}
38
+ _validate_color_map_z = field_validator("color_palette")(Colors.validate_color_map)
39
+
40
+
41
+ class BECMultiWaveform(BECPlotBase):
42
+ monitor_signal_updated = Signal()
43
+ highlighted_curve_index_changed = Signal(int)
44
+ USER_ACCESS = [
45
+ "_rpc_id",
46
+ "_config_dict",
47
+ "curves",
48
+ "set_monitor",
49
+ "set_opacity",
50
+ "set_curve_limit",
51
+ "set_curve_highlight",
52
+ "set_colormap",
53
+ "set",
54
+ "set_title",
55
+ "set_x_label",
56
+ "set_y_label",
57
+ "set_x_scale",
58
+ "set_y_scale",
59
+ "set_x_lim",
60
+ "set_y_lim",
61
+ "set_grid",
62
+ "set_colormap",
63
+ "enable_fps_monitor",
64
+ "lock_aspect_ratio",
65
+ "export",
66
+ "get_all_data",
67
+ "remove",
68
+ ]
69
+
70
+ def __init__(
71
+ self,
72
+ parent: Optional[QWidget] = None,
73
+ parent_figure=None,
74
+ config: Optional[BECMultiWaveformConfig] = None,
75
+ client=None,
76
+ gui_id: Optional[str] = None,
77
+ ):
78
+ if config is None:
79
+ config = BECMultiWaveformConfig(widget_class=self.__class__.__name__)
80
+ super().__init__(
81
+ parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
82
+ )
83
+ self.old_scan_id = None
84
+ self.scan_id = None
85
+ self.monitor = None
86
+ self.connected = False
87
+ self.current_highlight_index = 0
88
+ self._curves = deque()
89
+ self.visible_curves = []
90
+ self.number_of_visible_curves = 0
91
+
92
+ # Get bec shortcuts dev, scans, queue, scan_storage, dap
93
+ self.get_bec_shortcuts()
94
+
95
+ @property
96
+ def curves(self) -> deque:
97
+ """
98
+ Get the curves of the plot widget as a deque.
99
+ Returns:
100
+ deque: Deque of curves.
101
+ """
102
+ return self._curves
103
+
104
+ @curves.setter
105
+ def curves(self, value: deque):
106
+ self._curves = value
107
+
108
+ @property
109
+ def highlight_last_curve(self) -> bool:
110
+ """
111
+ Get the highlight_last_curve property.
112
+ Returns:
113
+ bool: The highlight_last_curve property.
114
+ """
115
+ return self.config.highlight_last_curve
116
+
117
+ @highlight_last_curve.setter
118
+ def highlight_last_curve(self, value: bool):
119
+ self.config.highlight_last_curve = value
120
+
121
+ def set_monitor(self, monitor: str):
122
+ """
123
+ Set the monitor for the plot widget.
124
+ Args:
125
+ monitor (str): The monitor to set.
126
+ """
127
+ self.config.monitor = monitor
128
+ self._connect_monitor()
129
+
130
+ def _connect_monitor(self):
131
+ """
132
+ Connect the monitor to the plot widget.
133
+ """
134
+ try:
135
+ previous_monitor = self.monitor
136
+ except AttributeError:
137
+ previous_monitor = None
138
+
139
+ if previous_monitor and self.connected is True:
140
+ self.bec_dispatcher.disconnect_slot(
141
+ self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(previous_monitor)
142
+ )
143
+ if self.config.monitor and self.connected is False:
144
+ self.bec_dispatcher.connect_slot(
145
+ self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(self.config.monitor)
146
+ )
147
+ self.connected = True
148
+ self.monitor = self.config.monitor
149
+
150
+ @Slot(dict, dict)
151
+ def on_monitor_1d_update(self, msg: dict, metadata: dict):
152
+ """
153
+ Update the plot widget with the monitor data.
154
+
155
+ Args:
156
+ msg(dict): The message data.
157
+ metadata(dict): The metadata of the message.
158
+ """
159
+ data = msg.get("data", None)
160
+ current_scan_id = metadata.get("scan_id", None)
161
+
162
+ if current_scan_id != self.scan_id:
163
+ self.scan_id = current_scan_id
164
+ self.clear_curves()
165
+ self.curves.clear()
166
+ if self.crosshair:
167
+ self.crosshair.clear_markers()
168
+
169
+ # Always create a new curve and add it
170
+ curve = pg.PlotDataItem()
171
+ curve.setData(data)
172
+ self.plot_item.addItem(curve)
173
+ self.curves.append(curve)
174
+
175
+ # Max Trace and scale colors
176
+ self.set_curve_limit(self.config.curve_limit, self.config.flush_buffer)
177
+
178
+ self.monitor_signal_updated.emit()
179
+
180
+ @Slot(int)
181
+ def set_curve_highlight(self, index: int):
182
+ """
183
+ Set the curve highlight based on visible curves.
184
+
185
+ Args:
186
+ index (int): The index of the curve to highlight among visible curves.
187
+ """
188
+ self.plot_item.visible_curves = [curve for curve in self.curves if curve.isVisible()]
189
+ num_visible_curves = len(self.plot_item.visible_curves)
190
+ self.number_of_visible_curves = num_visible_curves
191
+
192
+ if num_visible_curves == 0:
193
+ return # No curves to highlight
194
+
195
+ if index >= num_visible_curves:
196
+ index = num_visible_curves - 1
197
+ elif index < 0:
198
+ index = num_visible_curves + index
199
+ self.current_highlight_index = index
200
+ num_colors = num_visible_curves
201
+ colors = Colors.evenly_spaced_colors(
202
+ colormap=self.config.color_palette, num=num_colors, format="HEX"
203
+ )
204
+ for i, curve in enumerate(self.plot_item.visible_curves):
205
+ curve.setPen()
206
+ if i == self.current_highlight_index:
207
+ curve.setPen(pg.mkPen(color=colors[i], width=5))
208
+ curve.setAlpha(alpha=1, auto=False)
209
+ curve.setZValue(1)
210
+ else:
211
+ curve.setPen(pg.mkPen(color=colors[i], width=1))
212
+ curve.setAlpha(alpha=self.config.opacity / 100, auto=False)
213
+ curve.setZValue(0)
214
+
215
+ self.highlighted_curve_index_changed.emit(self.current_highlight_index)
216
+
217
+ @Slot(int)
218
+ def set_opacity(self, opacity: int):
219
+ """
220
+ Set the opacity of the curve on the plot.
221
+
222
+ Args:
223
+ opacity(int): The opacity of the curve. 0-100.
224
+ """
225
+ self.config.opacity = max(0, min(100, opacity))
226
+ self.set_curve_highlight(self.current_highlight_index)
227
+
228
+ @Slot(int, bool)
229
+ def set_curve_limit(self, max_trace: int, flush_buffer: bool = False):
230
+ """
231
+ Set the maximum number of traces to display on the plot.
232
+
233
+ Args:
234
+ max_trace (int): The maximum number of traces to display.
235
+ flush_buffer (bool): Flush the buffer.
236
+ """
237
+ self.config.curve_limit = max_trace
238
+ self.config.flush_buffer = flush_buffer
239
+
240
+ if self.config.curve_limit is None:
241
+ self.scale_colors()
242
+ return
243
+
244
+ if self.config.flush_buffer:
245
+ # Remove excess curves from the plot and the deque
246
+ while len(self.curves) > self.config.curve_limit:
247
+ curve = self.curves.popleft()
248
+ self.plot_item.removeItem(curve)
249
+ else:
250
+ # Hide or show curves based on the new max_trace
251
+ num_curves_to_show = min(self.config.curve_limit, len(self.curves))
252
+ for i, curve in enumerate(self.curves):
253
+ if i < len(self.curves) - num_curves_to_show:
254
+ curve.hide()
255
+ else:
256
+ curve.show()
257
+ self.scale_colors()
258
+
259
+ def scale_colors(self):
260
+ """
261
+ Scale the colors of the curves based on the current colormap.
262
+ """
263
+ if self.config.highlight_last_curve:
264
+ self.set_curve_highlight(-1) # Use -1 to highlight the last visible curve
265
+ else:
266
+ self.set_curve_highlight(self.current_highlight_index)
267
+
268
+ def set_colormap(self, colormap: str):
269
+ """
270
+ Set the colormap for the curves.
271
+
272
+ Args:
273
+ colormap(str): Colormap for the curves.
274
+ """
275
+ self.config.color_palette = colormap
276
+ self.set_curve_highlight(self.current_highlight_index)
277
+
278
+ def hook_crosshair(self) -> None:
279
+ super().hook_crosshair()
280
+ if self.crosshair:
281
+ self.highlighted_curve_index_changed.connect(self.crosshair.update_highlighted_curve)
282
+ if self.curves:
283
+ self.crosshair.update_highlighted_curve(self.current_highlight_index)
284
+
285
+ def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict:
286
+ """
287
+ Extract all curve data into a dictionary or a pandas DataFrame.
288
+
289
+ Args:
290
+ output (Literal["dict", "pandas"]): Format of the output data.
291
+
292
+ Returns:
293
+ dict | pd.DataFrame: Data of all curves in the specified format.
294
+ """
295
+ data = {}
296
+ try:
297
+ import pandas as pd
298
+ except ImportError:
299
+ pd = None
300
+ if output == "pandas":
301
+ logger.warning(
302
+ "Pandas is not installed. "
303
+ "Please install pandas using 'pip install pandas'."
304
+ "Output will be dictionary instead."
305
+ )
306
+ output = "dict"
307
+
308
+ curve_keys = []
309
+ curves_list = list(self.curves)
310
+ for i, curve in enumerate(curves_list):
311
+ x_data, y_data = curve.getData()
312
+ if x_data is not None or y_data is not None:
313
+ key = f"curve_{i}"
314
+ curve_keys.append(key)
315
+ if output == "dict":
316
+ data[key] = {"x": x_data.tolist(), "y": y_data.tolist()}
317
+ elif output == "pandas" and pd is not None:
318
+ data[key] = pd.DataFrame({"x": x_data, "y": y_data})
319
+
320
+ if output == "pandas" and pd is not None:
321
+ combined_data = pd.concat([data[key] for key in curve_keys], axis=1, keys=curve_keys)
322
+ return combined_data
323
+ return data
324
+
325
+ def clear_curves(self):
326
+ """
327
+ Remove all curves from the plot, excluding crosshair items.
328
+ """
329
+ items_to_remove = []
330
+ for item in self.plot_item.items:
331
+ if not getattr(item, "is_crosshair", False) and isinstance(item, pg.PlotDataItem):
332
+ items_to_remove.append(item)
333
+ for item in items_to_remove:
334
+ self.plot_item.removeItem(item)
335
+
336
+ def export_to_matplotlib(self):
337
+ """
338
+ Export current waveform to matplotlib GUI. Available only if matplotlib is installed in the environment.
339
+ """
340
+ MatplotlibExporter(self.plot_item).export()
341
+
342
+
343
+ if __name__ == "__main__":
344
+ import sys
345
+
346
+ from qtpy.QtWidgets import QApplication
347
+
348
+ from bec_widgets.widgets.figure import BECFigure
349
+
350
+ app = QApplication(sys.argv)
351
+ widget = BECFigure()
352
+ widget.show()
353
+ sys.exit(app.exec_())
File without changes
@@ -0,0 +1 @@
1
+ {'files': ['multi_waveform_widget.py','multi-waveform_controls.ui']}