bec-widgets 0.44.5__py3-none-any.whl → 0.46.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.
@@ -0,0 +1,423 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import Optional, Union
5
+
6
+ import numpy as np
7
+ import pyqtgraph as pg
8
+ from bec_lib import MessageEndpoints
9
+ from pydantic import Field
10
+ from qtpy import QtCore, QtGui
11
+ from qtpy.QtCore import Signal as pyqtSignal
12
+ from qtpy.QtCore import Slot as pyqtSlot
13
+ from qtpy.QtWidgets import QWidget
14
+
15
+ from bec_widgets.utils import EntryValidator
16
+ from bec_widgets.widgets.plots.plot_base import BECPlotBase, WidgetConfig
17
+ from bec_widgets.widgets.plots.waveform import Signal, SignalData
18
+
19
+
20
+ class MotorMapConfig(WidgetConfig):
21
+ signals: Optional[Signal] = Field(None, description="Signals of the motor map")
22
+ color_map: Optional[str] = Field(
23
+ "Greys", description="Color scheme of the motor position gradient."
24
+ ) # TODO decide if useful for anything, or just keep GREYS always
25
+ scatter_size: Optional[int] = Field(5, description="Size of the scatter points.")
26
+ max_points: Optional[int] = Field(1000, description="Maximum number of points to display.")
27
+ num_dim_points: Optional[int] = Field(
28
+ 100,
29
+ description="Number of points to dim before the color remains same for older recorded position.",
30
+ )
31
+ precision: Optional[int] = Field(2, description="Decimal precision of the motor position.")
32
+ background_value: Optional[int] = Field(
33
+ 25, description="Background value of the motor map."
34
+ ) # TODO can be percentage from 255 calculated
35
+
36
+
37
+ class BECMotorMap(BECPlotBase):
38
+ USER_ACCESS = [
39
+ "change_motors",
40
+ "set_max_points",
41
+ "set_precision",
42
+ "set_num_dim_points",
43
+ "set_background_value",
44
+ "set_scatter_size",
45
+ ]
46
+
47
+ # QT Signals
48
+ update_signal = pyqtSignal()
49
+
50
+ def __init__(
51
+ self,
52
+ parent: Optional[QWidget] = None,
53
+ parent_figure=None,
54
+ config: Optional[MotorMapConfig] = None,
55
+ client=None,
56
+ gui_id: Optional[str] = None,
57
+ ):
58
+ if config is None:
59
+ config = MotorMapConfig(widget_class=self.__class__.__name__)
60
+ super().__init__(
61
+ parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
62
+ )
63
+
64
+ # Get bec shortcuts dev, scans, queue, scan_storage, dap
65
+ self.get_bec_shortcuts()
66
+ self.entry_validator = EntryValidator(self.dev)
67
+
68
+ self.motor_x = None
69
+ self.motor_y = None
70
+ self.database_buffer = {"x": [], "y": []}
71
+ self.plot_components = defaultdict(dict) # container for plot components
72
+
73
+ # connect update signal to update plot
74
+ self.proxy_update_plot = pg.SignalProxy(
75
+ self.update_signal, rateLimit=25, slot=self._update_plot
76
+ )
77
+
78
+ # TODO decide if needed to implement, maybe there will be no children widgets for motormap for now...
79
+ # def find_widget_by_id(self, item_id: str) -> BECCurve:
80
+ # """
81
+ # Find the curve by its ID.
82
+ # Args:
83
+ # item_id(str): ID of the curve.
84
+ #
85
+ # Returns:
86
+ # BECCurve: The curve object.
87
+ # """
88
+ # for curve in self.plot_item.curves:
89
+ # if curve.gui_id == item_id:
90
+ # return curve
91
+
92
+ @pyqtSlot(str, str, str, str, bool)
93
+ def change_motors(
94
+ self,
95
+ motor_x: str,
96
+ motor_y: str,
97
+ motor_x_entry: str = None,
98
+ motor_y_entry: str = None,
99
+ validate_bec: bool = True,
100
+ ) -> None:
101
+ """
102
+ Change the active motors for the plot.
103
+ Args:
104
+ motor_x(str): Motor name for the X axis.
105
+ motor_y(str): Motor name for the Y axis.
106
+ motor_x_entry(str): Motor entry for the X axis.
107
+ motor_y_entry(str): Motor entry for the Y axis.
108
+ validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
109
+ """
110
+ motor_x_entry, motor_y_entry = self._validate_signal_entries(
111
+ motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec
112
+ )
113
+
114
+ motor_x_limit = self._get_motor_limit(motor_x)
115
+ motor_y_limit = self._get_motor_limit(motor_y)
116
+
117
+ signal = Signal(
118
+ source="device_readback",
119
+ x=SignalData(name=motor_x, entry=motor_x_entry, limits=motor_x_limit),
120
+ y=SignalData(name=motor_y, entry=motor_y_entry, limits=motor_y_limit),
121
+ )
122
+ self.config.signals = signal
123
+
124
+ # reconnect the signals
125
+ self._connect_motor_to_slots()
126
+
127
+ # Redraw the motor map
128
+ self._make_motor_map()
129
+
130
+ # TODO setup all visual properties
131
+ def set_max_points(self, max_points: int) -> None:
132
+ """
133
+ Set the maximum number of points to display.
134
+ Args:
135
+ max_points(int): Maximum number of points to display.
136
+ """
137
+ self.config.max_points = max_points
138
+
139
+ def set_precision(self, precision: int) -> None:
140
+ """
141
+ Set the decimal precision of the motor position.
142
+ Args:
143
+ precision(int): Decimal precision of the motor position.
144
+ """
145
+ self.config.precision = precision
146
+
147
+ def set_num_dim_points(self, num_dim_points: int) -> None:
148
+ """
149
+ Set the number of dim points for the motor map.
150
+ Args:
151
+ num_dim_points(int): Number of dim points.
152
+ """
153
+ self.config.num_dim_points = num_dim_points
154
+
155
+ def set_background_value(self, background_value: int) -> None:
156
+ """
157
+ Set the background value of the motor map.
158
+ Args:
159
+ background_value(int): Background value of the motor map.
160
+ """
161
+ self.config.background_value = background_value
162
+
163
+ def set_scatter_size(self, scatter_size: int) -> None:
164
+ """
165
+ Set the scatter size of the motor map plot.
166
+ Args:
167
+ scatter_size(int): Size of the scatter points.
168
+ """
169
+ self.config.scatter_size = scatter_size
170
+
171
+ def _connect_motor_to_slots(self):
172
+ """Connect motors to slots."""
173
+ if self.motor_x is not None and self.motor_y is not None:
174
+ old_endpoints = [
175
+ MessageEndpoints.device_readback(self.motor_x),
176
+ MessageEndpoints.device_readback(self.motor_y),
177
+ ]
178
+ self.bec_dispatcher.disconnect_slot(self.on_device_readback, old_endpoints)
179
+
180
+ self.motor_x = self.config.signals.x.name
181
+ self.motor_y = self.config.signals.y.name
182
+
183
+ endpoints = [
184
+ MessageEndpoints.device_readback(self.motor_x),
185
+ MessageEndpoints.device_readback(self.motor_y),
186
+ ]
187
+
188
+ self.bec_dispatcher.connect_slot(
189
+ self.on_device_readback, endpoints, single_callback_for_all_topics=True
190
+ )
191
+
192
+ def _make_motor_map(self):
193
+ """
194
+ Create the motor map plot.
195
+ """
196
+ # Create limit map
197
+ motor_x_limit = self.config.signals.x.limits
198
+ motor_y_limit = self.config.signals.y.limits
199
+ self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
200
+ self.plot_item.addItem(self.plot_components["limit_map"])
201
+ self.plot_components["limit_map"].setZValue(-1)
202
+
203
+ # Create scatter plot
204
+ scatter_size = self.config.scatter_size
205
+ self.plot_components["scatter"] = pg.ScatterPlotItem(
206
+ size=scatter_size, brush=pg.mkBrush(255, 255, 255, 255)
207
+ )
208
+ self.plot_item.addItem(self.plot_components["scatter"])
209
+ self.plot_components["scatter"].setZValue(0)
210
+
211
+ # Enable Grid
212
+ self.set_grid(True, True)
213
+
214
+ # Add the crosshair for initial motor coordinates
215
+ initial_position_x = self._get_motor_init_position(
216
+ self.motor_x, self.config.signals.x.entry, self.config.precision
217
+ )
218
+ initial_position_y = self._get_motor_init_position(
219
+ self.motor_y, self.config.signals.y.entry, self.config.precision
220
+ )
221
+
222
+ self.database_buffer["x"] = [initial_position_x]
223
+ self.database_buffer["y"] = [initial_position_y]
224
+
225
+ self.plot_components["scatter"].setData([initial_position_x], [initial_position_y])
226
+ self._add_coordinantes_crosshair(initial_position_x, initial_position_y)
227
+
228
+ # Set default labels for the plot
229
+ self.set(x_label=f"Motor X ({self.motor_x})", y_label=f"Motor Y ({self.motor_y})")
230
+
231
+ def _add_coordinantes_crosshair(self, x: float, y: float) -> None:
232
+ """
233
+ Add crosshair to the plot to highlight the current position.
234
+ Args:
235
+ x(float): X coordinate.
236
+ y(float): Y coordinate.
237
+ """
238
+
239
+ # Crosshair to highlight the current position
240
+ highlight_H = pg.InfiniteLine(
241
+ angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
242
+ )
243
+ highlight_V = pg.InfiniteLine(
244
+ angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
245
+ )
246
+
247
+ # Add crosshair to the curve list for future referencing
248
+ self.plot_components["highlight_H"] = highlight_H
249
+ self.plot_components["highlight_V"] = highlight_V
250
+
251
+ # Add crosshair to the plot
252
+ self.plot_item.addItem(highlight_H)
253
+ self.plot_item.addItem(highlight_V)
254
+
255
+ highlight_V.setPos(x)
256
+ highlight_H.setPos(y)
257
+
258
+ def _make_limit_map(self, limits_x: list, limits_y: list) -> pg.ImageItem:
259
+ """
260
+ Create a limit map for the motor map plot.
261
+ Args:
262
+ limits_x(list): Motor limits for the x axis.
263
+ limits_y(list): Motor limits for the y axis.
264
+
265
+ Returns:
266
+ pg.ImageItem: Limit map.
267
+ """
268
+ limit_x_min, limit_x_max = limits_x
269
+ limit_y_min, limit_y_max = limits_y
270
+
271
+ map_width = int(limit_x_max - limit_x_min + 1)
272
+ map_height = int(limit_y_max - limit_y_min + 1)
273
+
274
+ # Create limits map
275
+ background_value = self.config.background_value
276
+ limit_map_data = np.full((map_width, map_height), background_value, dtype=np.float32)
277
+ limit_map = pg.ImageItem()
278
+ limit_map.setImage(limit_map_data)
279
+
280
+ # Translate and scale the image item to match the motor coordinates
281
+ tr = QtGui.QTransform()
282
+ tr.translate(limit_x_min, limit_y_min)
283
+ limit_map.setTransform(tr)
284
+
285
+ return limit_map
286
+
287
+ def _get_motor_init_position(self, name: str, entry: str, precision: int) -> float:
288
+ """
289
+ Get the motor initial position from the config.
290
+ Args:
291
+ name(str): Motor name.
292
+ entry(str): Motor entry.
293
+ precision(int): Decimal precision of the motor position.
294
+ Returns:
295
+ float: Motor initial position.
296
+ """
297
+ init_position = round(self.dev[name].read()[entry]["value"], precision)
298
+ return init_position
299
+
300
+ def _validate_signal_entries(
301
+ self,
302
+ x_name: str,
303
+ y_name: str,
304
+ x_entry: str | None,
305
+ y_entry: str | None,
306
+ validate_bec: bool = True,
307
+ ) -> tuple[str, str]:
308
+ """
309
+ Validate the signal name and entry.
310
+ Args:
311
+ x_name(str): Name of the x signal.
312
+ y_name(str): Name of the y signal.
313
+ x_entry(str|None): Entry of the x signal.
314
+ y_entry(str|None): Entry of the y signal.
315
+ validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
316
+ Returns:
317
+ tuple[str,str]: Validated x and y entries.
318
+ """
319
+ if validate_bec:
320
+ x_entry = self.entry_validator.validate_signal(x_name, x_entry)
321
+ y_entry = self.entry_validator.validate_signal(y_name, y_entry)
322
+ else:
323
+ x_entry = x_name if x_entry is None else x_entry
324
+ y_entry = y_name if y_entry is None else y_entry
325
+ return x_entry, y_entry
326
+
327
+ def _get_motor_limit(self, motor: str) -> Union[list | None]: # TODO check if works correctly
328
+ """
329
+ Get the motor limit from the config.
330
+ Args:
331
+ motor(str): Motor name.
332
+
333
+ Returns:
334
+ float: Motor limit.
335
+ """
336
+ try:
337
+ limits = self.dev[motor].limits
338
+ if limits == [0, 0]:
339
+ return None
340
+ return limits
341
+ except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
342
+ # If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
343
+ print(f"The device '{motor}' does not have defined limits.")
344
+ return None
345
+
346
+ def _update_plot(self):
347
+ """Update the motor map plot."""
348
+ x = self.database_buffer["x"]
349
+ y = self.database_buffer["y"]
350
+
351
+ # Setup gradient brush for history
352
+ brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
353
+
354
+ # Calculate the decrement step based on self.num_dim_points
355
+ num_dim_points = self.config.num_dim_points
356
+ decrement_step = (255 - 50) / num_dim_points
357
+ for i in range(1, min(num_dim_points + 1, len(x) + 1)):
358
+ brightness = max(60, 255 - decrement_step * (i - 1))
359
+ brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
360
+ brushes[-1] = pg.mkBrush(255, 255, 255, 255) # Newest point is always full brightness
361
+ scatter_size = self.config.scatter_size
362
+
363
+ # Update the scatter plot
364
+ self.plot_components["scatter"].setData(
365
+ x=x,
366
+ y=y,
367
+ brush=brushes,
368
+ pen=None,
369
+ size=scatter_size,
370
+ )
371
+
372
+ # Get last know position for crosshair
373
+ current_x = x[-1]
374
+ current_y = y[-1]
375
+
376
+ # Update the crosshair
377
+ self.plot_components["highlight_V"].setPos(current_x)
378
+ self.plot_components["highlight_H"].setPos(current_y)
379
+
380
+ # TODO not update title but some label
381
+ # Update plot title
382
+ precision = self.config.precision
383
+ self.set_title(
384
+ f"Motor position: ({round(current_x,precision)}, {round(current_y,precision)})"
385
+ )
386
+
387
+ @pyqtSlot(dict)
388
+ def on_device_readback(self, msg: dict) -> None:
389
+ """
390
+ Update the motor map plot with the new motor position.
391
+ Args:
392
+ msg(dict): Message from the device readback.
393
+ """
394
+ if self.motor_x is None or self.motor_y is None:
395
+ return
396
+
397
+ if self.motor_x in msg["signals"]:
398
+ x = msg["signals"][self.motor_x]["value"]
399
+ self.database_buffer["x"].append(x)
400
+ self.database_buffer["y"].append(self.database_buffer["y"][-1])
401
+
402
+ elif self.motor_y in msg["signals"]:
403
+ y = msg["signals"][self.motor_y]["value"]
404
+ self.database_buffer["y"].append(y)
405
+ self.database_buffer["x"].append(self.database_buffer["x"][-1])
406
+
407
+ self.update_signal.emit()
408
+
409
+
410
+ if __name__ == "__main__": # pragma: no cover
411
+ import sys
412
+
413
+ import pyqtgraph as pg
414
+ from qtpy.QtWidgets import QApplication
415
+
416
+ app = QApplication(sys.argv)
417
+ glw = pg.GraphicsLayoutWidget()
418
+ motor_map = BECMotorMap()
419
+ motor_map.change_motors("samx", "samy")
420
+ glw.addItem(motor_map)
421
+ widget = glw
422
+ widget.show()
423
+ sys.exit(app.exec_())
@@ -66,7 +66,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
66
66
  pg.GraphicsLayout.__init__(self, parent)
67
67
 
68
68
  self.figure = parent_figure
69
- self.plot_item = self.addPlot()
69
+ self.plot_item = self.addPlot(row=0, col=0)
70
70
 
71
71
  self.add_legend()
72
72
 
@@ -15,7 +15,7 @@ from qtpy.QtCore import Slot as pyqtSlot
15
15
  from qtpy.QtWidgets import QWidget
16
16
 
17
17
  from bec_widgets.utils import BECConnector, Colors, ConnectionConfig, EntryValidator
18
- from bec_widgets.widgets.plots import BECPlotBase, WidgetConfig
18
+ from bec_widgets.widgets.plots.plot_base import BECPlotBase, WidgetConfig
19
19
 
20
20
 
21
21
  class SignalData(BaseModel):
@@ -25,14 +25,16 @@ class SignalData(BaseModel):
25
25
  entry: str
26
26
  unit: Optional[str] = None # todo implement later
27
27
  modifier: Optional[str] = None # todo implement later
28
+ limits: Optional[list[float]] = None # todo implement later
28
29
 
29
30
 
30
31
  class Signal(BaseModel):
31
32
  """The configuration of a signal in the 1D waveform widget."""
32
33
 
33
34
  source: str
34
- x: SignalData
35
+ x: SignalData # TODO maybe add metadata for config gui later
35
36
  y: SignalData
37
+ z: Optional[SignalData] = None
36
38
 
37
39
 
38
40
  class CurveConfig(ConnectionConfig):
@@ -48,12 +50,13 @@ class CurveConfig(ConnectionConfig):
48
50
  )
49
51
  source: Optional[str] = Field(None, description="The source of the curve.")
50
52
  signals: Optional[Signal] = Field(None, description="The signal of the curve.")
53
+ colormap: Optional[str] = Field("plasma", description="The colormap of the curves z gradient.")
51
54
 
52
55
 
53
56
  class Waveform1DConfig(WidgetConfig):
54
57
  color_palette: Literal["plasma", "viridis", "inferno", "magma"] = Field(
55
58
  "plasma", description="The color palette of the figure widget."
56
- )
59
+ ) # TODO can be extended to all colormaps from current pyqtgraph session
57
60
  curves: dict[str, CurveConfig] = Field(
58
61
  {}, description="The list of curves to be added to the 1D waveform widget."
59
62
  )
@@ -64,6 +67,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
64
67
  "set",
65
68
  "set_data",
66
69
  "set_color",
70
+ "set_colormap",
67
71
  "set_symbol",
68
72
  "set_symbol_color",
69
73
  "set_symbol_size",
@@ -134,6 +138,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
134
138
  # Mapping of keywords to setter methods
135
139
  method_map = {
136
140
  "color": self.set_color,
141
+ "colormap": self.set_colormap,
137
142
  "symbol": self.set_symbol,
138
143
  "symbol_color": self.set_symbol_color,
139
144
  "symbol_size": self.set_symbol_size,
@@ -202,6 +207,14 @@ class BECCurve(BECConnector, pg.PlotDataItem):
202
207
  self.config.pen_style = pen_style
203
208
  self.apply_config()
204
209
 
210
+ def set_colormap(self, colormap: str):
211
+ """
212
+ Set the colormap for the scatter plot z gradient.
213
+ Args:
214
+ colormap(str): Colormap for the scatter plot.
215
+ """
216
+ self.config.colormap = colormap
217
+
205
218
  def get_data(self) -> tuple[np.ndarray, np.ndarray]:
206
219
  """
207
220
  Get the data of the curve.
@@ -212,7 +225,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
212
225
  return x_data, y_data
213
226
 
214
227
 
215
- class BECWaveform1D(BECPlotBase):
228
+ class BECWaveform(BECPlotBase):
216
229
  USER_ACCESS = [
217
230
  "add_curve_scan",
218
231
  "add_curve_custom",
@@ -466,9 +479,12 @@ class BECWaveform1D(BECPlotBase):
466
479
  self,
467
480
  x_name: str,
468
481
  y_name: str,
482
+ z_name: Optional[str] = None,
469
483
  x_entry: Optional[str] = None,
470
484
  y_entry: Optional[str] = None,
485
+ z_entry: Optional[str] = None,
471
486
  color: Optional[str] = None,
487
+ color_map_z: Optional[str] = "plasma",
472
488
  label: Optional[str] = None,
473
489
  validate_bec: bool = True,
474
490
  **kwargs,
@@ -480,7 +496,10 @@ class BECWaveform1D(BECPlotBase):
480
496
  x_entry(str): Entry of the x signal.
481
497
  y_name(str): Name of the y signal.
482
498
  y_entry(str): Entry of the y signal.
499
+ z_name(str): Name of the z signal.
500
+ z_entry(str): Entry of the z signal.
483
501
  color(str, optional): Color of the curve. Defaults to None.
502
+ color_map_z(str): The color map to use for the z-axis.
484
503
  label(str, optional): Label of the curve. Defaults to None.
485
504
  **kwargs: Additional keyword arguments for the curve configuration.
486
505
 
@@ -491,11 +510,14 @@ class BECWaveform1D(BECPlotBase):
491
510
  curve_source = "scan_segment"
492
511
 
493
512
  # Get entry if not provided and validate
494
- x_entry, y_entry = self._validate_signal_entries(
495
- x_name, y_name, x_entry, y_entry, validate_bec
513
+ x_entry, y_entry, z_entry = self._validate_signal_entries(
514
+ x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
496
515
  )
497
516
 
498
- label = label or f"{y_name}-{y_entry}"
517
+ if z_name is not None and z_entry is not None:
518
+ label = label or f"{z_name}-{z_entry}"
519
+ else:
520
+ label = label or f"{y_name}-{y_entry}"
499
521
 
500
522
  curve_exits = self._check_curve_id(label, self._curves_data)
501
523
  if curve_exits:
@@ -514,11 +536,13 @@ class BECWaveform1D(BECPlotBase):
514
536
  parent_id=self.gui_id,
515
537
  label=label,
516
538
  color=color,
539
+ color_map=color_map_z,
517
540
  source=curve_source,
518
541
  signals=Signal(
519
542
  source=curve_source,
520
543
  x=SignalData(name=x_name, entry=x_entry),
521
544
  y=SignalData(name=y_name, entry=y_entry),
545
+ z=SignalData(name=z_name, entry=z_entry) if z_name else None,
522
546
  ),
523
547
  **kwargs,
524
548
  )
@@ -529,28 +553,35 @@ class BECWaveform1D(BECPlotBase):
529
553
  self,
530
554
  x_name: str,
531
555
  y_name: str,
556
+ z_name: str | None,
532
557
  x_entry: str | None,
533
558
  y_entry: str | None,
559
+ z_entry: str | None,
534
560
  validate_bec: bool = True,
535
- ) -> tuple[str, str]:
561
+ ) -> tuple[str, str, str | None]:
536
562
  """
537
563
  Validate the signal name and entry.
538
564
  Args:
539
565
  x_name(str): Name of the x signal.
540
566
  y_name(str): Name of the y signal.
567
+ z_name(str): Name of the z signal.
541
568
  x_entry(str|None): Entry of the x signal.
542
569
  y_entry(str|None): Entry of the y signal.
570
+ z_entry(str|None): Entry of the z signal.
543
571
  validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
544
572
  Returns:
545
- tuple[str,str]: Validated x and y entries.
573
+ tuple[str,str,str|None]: Validated x, y, z entries.
546
574
  """
547
575
  if validate_bec:
548
576
  x_entry = self.entry_validator.validate_signal(x_name, x_entry)
549
577
  y_entry = self.entry_validator.validate_signal(y_name, y_entry)
578
+ if z_name:
579
+ z_entry = self.entry_validator.validate_signal(z_name, z_entry)
550
580
  else:
551
581
  x_entry = x_name if x_entry is None else x_entry
552
582
  y_entry = y_name if y_entry is None else y_entry
553
- return x_entry, y_entry
583
+ z_entry = z_name if z_entry is None else z_entry
584
+ return x_entry, y_entry, z_entry
554
585
 
555
586
  def _check_curve_id(self, val: Any, dict_to_check: dict) -> bool:
556
587
  """
@@ -653,19 +684,54 @@ class BECWaveform1D(BECPlotBase):
653
684
  Args:
654
685
  data(ScanData): Data from the scan segment.
655
686
  """
687
+ data_x = None
688
+ data_y = None
689
+ data_z = None
656
690
  for curve_id, curve in self._curves_data["scan_segment"].items():
657
691
  x_name = curve.config.signals.x.name
658
692
  x_entry = curve.config.signals.x.entry
659
693
  y_name = curve.config.signals.y.name
660
694
  y_entry = curve.config.signals.y.entry
695
+ if curve.config.signals.z:
696
+ z_name = curve.config.signals.z.name
697
+ z_entry = curve.config.signals.z.entry
661
698
 
662
699
  try:
663
700
  data_x = data[x_name][x_entry].val
664
701
  data_y = data[y_name][y_entry].val
702
+ if curve.config.signals.z:
703
+ data_z = data[z_name][z_entry].val
704
+ color_z = self._make_z_gradient(
705
+ data_z, curve.config.colormap
706
+ ) # TODO decide how to implement custom gradient
665
707
  except TypeError:
666
708
  continue
667
709
 
668
- curve.setData(data_x, data_y)
710
+ if data_z is not None and color_z is not None:
711
+ curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
712
+ else:
713
+ curve.setData(data_x, data_y)
714
+
715
+ def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None:
716
+ """
717
+ Make a gradient color for the z values.
718
+ Args:
719
+ data_z(list|np.ndarray): Z values.
720
+ colormap(str): Colormap for the gradient color.
721
+
722
+ Returns:
723
+ list: List of colors for the z values.
724
+ """
725
+ # Normalize z_values for color mapping
726
+ z_min, z_max = np.min(data_z), np.max(data_z)
727
+
728
+ if z_max != z_min: # Ensure that there is a range in the z values
729
+ z_values_norm = (data_z - z_min) / (z_max - z_min)
730
+ colormap = pg.colormap.get(colormap) # using colormap from global settings
731
+ colors = [colormap.map(z, mode="qcolor") for z in z_values_norm]
732
+ return colors
733
+ else:
734
+ return None
669
735
 
670
736
  def scan_history(self, scan_index: int = None, scan_id: str = None):
671
737
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bec-widgets
3
- Version: 0.44.5
3
+ Version: 0.46.0
4
4
  Summary: BEC Widgets
5
5
  Home-page: https://gitlab.psi.ch/bec/bec-widgets
6
6
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec-widgets/issues