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.
- CHANGELOG.md +26 -24
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +431 -0
- bec_widgets/cli/rpc_register.py +2 -0
- bec_widgets/cli/rpc_wigdet_handler.py +2 -0
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +7 -3
- bec_widgets/utils/crosshair.py +88 -35
- bec_widgets/widgets/dock/dock_area.py +9 -0
- bec_widgets/widgets/figure/figure.py +32 -3
- bec_widgets/widgets/figure/plots/multi_waveform/__init__.py +0 -0
- bec_widgets/widgets/figure/plots/multi_waveform/multi_waveform.py +353 -0
- bec_widgets/widgets/multi_waveform/__init__.py +0 -0
- bec_widgets/widgets/multi_waveform/bec_multi_waveform_widget.pyproject +1 -0
- bec_widgets/widgets/multi_waveform/bec_multi_waveform_widget_plugin.py +54 -0
- bec_widgets/widgets/multi_waveform/multi_waveform_controls.ui +99 -0
- bec_widgets/widgets/multi_waveform/multi_waveform_widget.py +533 -0
- bec_widgets/widgets/multi_waveform/register_bec_multi_waveform_widget.py +17 -0
- bec_widgets/widgets/positioner_box/positioner_box.py +14 -5
- {bec_widgets-1.4.0.dist-info → bec_widgets-1.5.0.dist-info}/METADATA +1 -1
- {bec_widgets-1.4.0.dist-info → bec_widgets-1.5.0.dist-info}/RECORD +24 -16
- pyproject.toml +1 -1
- {bec_widgets-1.4.0.dist-info → bec_widgets-1.5.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.4.0.dist-info → bec_widgets-1.5.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.4.0.dist-info → bec_widgets-1.5.0.dist-info}/licenses/LICENSE +0 -0
bec_widgets/utils/crosshair.py
CHANGED
@@ -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
|
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
|
-
|
120
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
141
|
-
|
142
|
-
self.
|
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.
|
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
|
-
#
|
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.
|
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.
|
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]
|
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
|
-
|
341
|
-
for
|
342
|
-
marker
|
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
|
-
#
|
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,
|
File without changes
|
@@ -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']}
|