bec-widgets 1.25.1__py3-none-any.whl → 2.0.1__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 (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +639 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +188 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  143. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  144. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  145. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/RECORD +168 -153
  147. pyproject.toml +3 -3
  148. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  150. bec_widgets/applications/bec_app.py +0 -84
  151. bec_widgets/cli/auto_updates.py +0 -168
  152. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  153. bec_widgets/widgets/containers/figure/figure.py +0 -796
  154. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  156. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  157. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  158. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  159. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  160. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  161. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  162. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  163. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  164. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  165. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  167. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  168. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  169. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  171. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  172. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  174. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  175. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  176. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  177. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  178. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  179. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  180. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  181. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  186. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  189. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  191. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  192. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  194. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,796 +0,0 @@
1
- # pylint: disable = no-name-in-module,missing-module-docstring
2
- from __future__ import annotations
3
-
4
- import uuid
5
- from collections import defaultdict
6
- from typing import Literal, Optional
7
-
8
- import numpy as np
9
- import pyqtgraph as pg
10
- from bec_lib.logger import bec_logger
11
- from pydantic import Field, ValidationError, field_validator
12
- from qtpy.QtCore import Signal as pyqtSignal
13
- from qtpy.QtWidgets import QWidget
14
- from typeguard import typechecked
15
-
16
- from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
17
- from bec_widgets.utils.bec_widget import BECWidget
18
- from bec_widgets.utils.colors import apply_theme
19
- from bec_widgets.widgets.containers.figure.plots.image.image import BECImageShow, ImageConfig
20
- from bec_widgets.widgets.containers.figure.plots.motor_map.motor_map import (
21
- BECMotorMap,
22
- MotorMapConfig,
23
- )
24
- from bec_widgets.widgets.containers.figure.plots.multi_waveform.multi_waveform import (
25
- BECMultiWaveform,
26
- BECMultiWaveformConfig,
27
- )
28
- from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig
29
- from bec_widgets.widgets.containers.figure.plots.waveform.waveform import (
30
- BECWaveform,
31
- Waveform1DConfig,
32
- )
33
-
34
- logger = bec_logger.logger
35
-
36
-
37
- class FigureConfig(ConnectionConfig):
38
- """Configuration for BECFigure. Inheriting from ConnectionConfig widget_class and gui_id"""
39
-
40
- theme: Literal["dark", "light"] = Field("dark", description="The theme of the figure widget.")
41
- num_cols: int = Field(1, description="The number of columns in the figure widget.")
42
- num_rows: int = Field(1, description="The number of rows in the figure widget.")
43
- widgets: dict[str, Waveform1DConfig | ImageConfig | MotorMapConfig | SubplotConfig] = Field(
44
- {}, description="The list of widgets to be added to the figure widget."
45
- )
46
-
47
- @field_validator("widgets", mode="before")
48
- @classmethod
49
- def validate_widgets(cls, v):
50
- """Validate the widgets configuration."""
51
- widget_class_map = {
52
- "BECWaveform": Waveform1DConfig,
53
- "BECImageShow": ImageConfig,
54
- "BECMotorMap": MotorMapConfig,
55
- }
56
- validated_widgets = {}
57
- for key, widget_config in v.items():
58
- if "widget_class" not in widget_config:
59
- raise ValueError(f"Widget config for {key} does not contain 'widget_class'.")
60
- widget_class = widget_config["widget_class"]
61
- if widget_class not in widget_class_map:
62
- raise ValueError(f"Unknown widget_class '{widget_class}' for widget '{key}'.")
63
- config_class = widget_class_map[widget_class]
64
- validated_widgets[key] = config_class(**widget_config)
65
- return validated_widgets
66
-
67
-
68
- class WidgetHandler:
69
- """Factory for creating and configuring BEC widgets for BECFigure."""
70
-
71
- def __init__(self):
72
- self.widget_factory = {
73
- "BECPlotBase": (BECPlotBase, SubplotConfig),
74
- "BECWaveform": (BECWaveform, Waveform1DConfig),
75
- "BECImageShow": (BECImageShow, ImageConfig),
76
- "BECMotorMap": (BECMotorMap, MotorMapConfig),
77
- "BECMultiWaveform": (BECMultiWaveform, BECMultiWaveformConfig),
78
- }
79
-
80
- def create_widget(
81
- self,
82
- widget_type: str,
83
- widget_id: str,
84
- parent_figure,
85
- parent_id: str,
86
- config: dict = None,
87
- **axis_kwargs,
88
- ) -> BECPlotBase:
89
- """
90
- Create and configure a widget based on its type.
91
-
92
- Args:
93
- widget_type (str): The type of the widget to create.
94
- widget_id (str): Unique identifier for the widget.
95
- parent_id (str): Identifier of the parent figure.
96
- config (dict, optional): Additional configuration for the widget.
97
- **axis_kwargs: Additional axis properties to set on the widget after creation.
98
-
99
- Returns:
100
- BECPlotBase: The created and configured widget instance.
101
- """
102
- entry = self.widget_factory.get(widget_type)
103
- if not entry:
104
- raise ValueError(f"Unsupported widget type: {widget_type}")
105
-
106
- widget_class, config_class = entry
107
- if config is not None and isinstance(config, config_class):
108
- config = config.model_dump()
109
- widget_config_dict = {
110
- "widget_class": widget_class.__name__,
111
- "parent_id": parent_id,
112
- "gui_id": widget_id,
113
- **(config if config is not None else {}),
114
- }
115
- widget_config = config_class(**widget_config_dict)
116
- widget = widget_class(
117
- config=widget_config, parent_figure=parent_figure, client=parent_figure.client
118
- )
119
-
120
- if axis_kwargs:
121
- widget.set(**axis_kwargs)
122
-
123
- return widget
124
-
125
-
126
- class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
127
- USER_ACCESS = [
128
- "_rpc_id",
129
- "_config_dict",
130
- "_get_all_rpc",
131
- "axes",
132
- "widgets",
133
- "plot",
134
- "image",
135
- "motor_map",
136
- "remove",
137
- "change_layout",
138
- "change_theme",
139
- "export",
140
- "clear_all",
141
- "widget_list",
142
- ]
143
- subplot_map = {
144
- "PlotBase": BECPlotBase,
145
- "BECWaveform": BECWaveform,
146
- "BECImageShow": BECImageShow,
147
- "BECMotorMap": BECMotorMap,
148
- "BECMultiWaveform": BECMultiWaveform,
149
- }
150
- widget_method_map = {
151
- "BECWaveform": "plot",
152
- "BECImageShow": "image",
153
- "BECMotorMap": "motor_map",
154
- "BECMultiWaveform": "multi_waveform",
155
- }
156
-
157
- clean_signal = pyqtSignal()
158
-
159
- def __init__(
160
- self,
161
- parent: Optional[QWidget] = None,
162
- config: Optional[FigureConfig] = None,
163
- client=None,
164
- gui_id: Optional[str] = None,
165
- **kwargs,
166
- ) -> None:
167
- if config is None:
168
- config = FigureConfig(widget_class=self.__class__.__name__)
169
- else:
170
- if isinstance(config, dict):
171
- config = FigureConfig(**config)
172
- super().__init__(client=client, gui_id=gui_id, config=config, **kwargs)
173
- pg.GraphicsLayoutWidget.__init__(self, parent)
174
-
175
- self.widget_handler = WidgetHandler()
176
-
177
- # Widget container to reference widgets by 'widget_id'
178
- self._widgets = defaultdict(dict)
179
-
180
- # Container to keep track of the grid
181
- self.grid = []
182
- # Create config and apply it
183
- self.apply_config(config)
184
-
185
- def __getitem__(self, key: tuple | str):
186
- if isinstance(key, tuple) and len(key) == 2:
187
- return self.axes(*key)
188
- if isinstance(key, str):
189
- widget = self._widgets.get(key)
190
- if widget is None:
191
- raise KeyError(f"No widget with ID {key}")
192
- return self._widgets.get(key)
193
- else:
194
- raise TypeError(
195
- "Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
196
- )
197
-
198
- def apply_config(self, config: dict | FigureConfig): # ,generate_new_id: bool = False):
199
- if isinstance(config, dict):
200
- try:
201
- config = FigureConfig(**config)
202
- except ValidationError as e:
203
- logger.error(f"Error in applying config: {e}")
204
- return
205
- self.config = config
206
-
207
- # widget_config has to be reset for not have each widget config twice when added to the figure
208
- widget_configs = list(self.config.widgets.values())
209
- self.config.widgets = {}
210
- for widget_config in widget_configs:
211
- getattr(self, self.widget_method_map[widget_config.widget_class])(
212
- config=widget_config.model_dump(), row=widget_config.row, col=widget_config.col
213
- )
214
-
215
- @property
216
- def widget_list(self) -> list[BECPlotBase]:
217
- """
218
- Access all widget in BECFigure as a list
219
- Returns:
220
- list[BECPlotBase]: List of all widgets in the figure.
221
- """
222
- axes = [value for value in self._widgets.values() if isinstance(value, BECPlotBase)]
223
- return axes
224
-
225
- @widget_list.setter
226
- def widget_list(self, value: list[BECPlotBase]):
227
- """
228
- Access all widget in BECFigure as a list
229
- Returns:
230
- list[BECPlotBase]: List of all widgets in the figure.
231
- """
232
- self._axes = value
233
-
234
- @property
235
- def widgets(self) -> dict:
236
- """
237
- All widgets within the figure with gui ids as keys.
238
- Returns:
239
- dict: All widgets within the figure.
240
- """
241
- return self._widgets
242
-
243
- @widgets.setter
244
- def widgets(self, value: dict):
245
- """
246
- All widgets within the figure with gui ids as keys.
247
- Returns:
248
- dict: All widgets within the figure.
249
- """
250
- self._widgets = value
251
-
252
- def export(self):
253
- """Export the plot widget."""
254
- try:
255
- plot_item = self.widget_list[0]
256
- except Exception as exc:
257
- raise ValueError("No plot widget available to export.") from exc
258
-
259
- scene = plot_item.scene()
260
- scene.contextMenuItem = plot_item
261
- scene.showExportDialog()
262
-
263
- @typechecked
264
- def plot(
265
- self,
266
- arg1: list | np.ndarray | str | None = None,
267
- y: list | np.ndarray | None = None,
268
- x: list | np.ndarray | None = None,
269
- x_name: str | None = None,
270
- y_name: str | None = None,
271
- z_name: str | None = None,
272
- x_entry: str | None = None,
273
- y_entry: str | None = None,
274
- z_entry: str | None = None,
275
- color: str | None = None,
276
- color_map_z: str | None = "magma",
277
- label: str | None = None,
278
- validate: bool = True,
279
- new: bool = False,
280
- row: int | None = None,
281
- col: int | None = None,
282
- dap: str | None = None,
283
- config: dict | None = None, # TODO make logic more transparent
284
- **axis_kwargs,
285
- ) -> BECWaveform:
286
- """
287
- Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
288
-
289
- Args:
290
- arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
291
- y(list | np.ndarray): Custom y data to plot.
292
- x(list | np.ndarray): Custom x data to plot.
293
- x_name(str): The name of the device for the x-axis.
294
- y_name(str): The name of the device for the y-axis.
295
- z_name(str): The name of the device for the z-axis.
296
- x_entry(str): The name of the entry for the x-axis.
297
- y_entry(str): The name of the entry for the y-axis.
298
- z_entry(str): The name of the entry for the z-axis.
299
- color(str): The color of the curve.
300
- color_map_z(str): The color map to use for the z-axis.
301
- label(str): The label of the curve.
302
- validate(bool): If True, validate the device names and entries.
303
- new(bool): If True, create a new plot instead of using the first plot.
304
- row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
305
- col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
306
- dap(str): The DAP model to use for the curve.
307
- config(dict): Recreates the whole BECWaveform widget from provided configuration.
308
- **axis_kwargs: Additional axis properties to set on the widget after creation.
309
-
310
- Returns:
311
- BECWaveform: The waveform plot widget.
312
- """
313
- waveform = self.subplot_factory(
314
- widget_type="BECWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
315
- )
316
- if config is not None:
317
- return waveform
318
-
319
- if arg1 is not None or y_name is not None or (y is not None and x is not None):
320
- waveform.plot(
321
- arg1=arg1,
322
- y=y,
323
- x=x,
324
- x_name=x_name,
325
- y_name=y_name,
326
- z_name=z_name,
327
- x_entry=x_entry,
328
- y_entry=y_entry,
329
- z_entry=z_entry,
330
- color=color,
331
- color_map_z=color_map_z,
332
- label=label,
333
- validate=validate,
334
- dap=dap,
335
- )
336
- return waveform
337
-
338
- def _init_image(
339
- self,
340
- image,
341
- monitor: str = None,
342
- monitor_type: Literal["1d", "2d"] = "2d",
343
- color_bar: Literal["simple", "full"] = "full",
344
- color_map: str = "magma",
345
- data: np.ndarray = None,
346
- vrange: tuple[float, float] = None,
347
- ) -> BECImageShow:
348
- """
349
- Configure the image based on the provided parameters.
350
-
351
- Args:
352
- image (BECImageShow): The image to configure.
353
- monitor (str): The name of the monitor to display.
354
- color_bar (Literal["simple","full"]): The type of color bar to display.
355
- color_map (str): The color map to use for the image.
356
- data (np.ndarray): Custom data to display.
357
- """
358
- if monitor is not None and data is None:
359
- image.image(
360
- monitor=monitor,
361
- monitor_type=monitor_type,
362
- color_map=color_map,
363
- vrange=vrange,
364
- color_bar=color_bar,
365
- )
366
- elif data is not None and monitor is None:
367
- image.add_custom_image(
368
- name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
369
- )
370
- elif data is None and monitor is None:
371
- # Setting appearance
372
- if vrange is not None:
373
- image.set_vrange(vmin=vrange[0], vmax=vrange[1])
374
- if color_map is not None:
375
- image.set_color_map(color_map)
376
- else:
377
- raise ValueError("Invalid input. Provide either monitor name or custom data.")
378
- return image
379
-
380
- def image(
381
- self,
382
- monitor: str = None,
383
- monitor_type: Literal["1d", "2d"] = "2d",
384
- color_bar: Literal["simple", "full"] = "full",
385
- color_map: str = "magma",
386
- data: np.ndarray = None,
387
- vrange: tuple[float, float] = None,
388
- new: bool = False,
389
- row: int | None = None,
390
- col: int | None = None,
391
- config: dict | None = None,
392
- **axis_kwargs,
393
- ) -> BECImageShow:
394
- """
395
- Add an image to the figure. Always access the first image widget in the figure.
396
-
397
- Args:
398
- monitor(str): The name of the monitor to display.
399
- color_bar(Literal["simple","full"]): The type of color bar to display.
400
- color_map(str): The color map to use for the image.
401
- data(np.ndarray): Custom data to display.
402
- vrange(tuple[float, float]): The range of values to display.
403
- new(bool): If True, create a new plot instead of using the first plot.
404
- row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
405
- col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
406
- config(dict): Recreates the whole BECImageShow widget from provided configuration.
407
- **axis_kwargs: Additional axis properties to set on the widget after creation.
408
-
409
- Returns:
410
- BECImageShow: The image widget.
411
- """
412
-
413
- image = self.subplot_factory(
414
- widget_type="BECImageShow", config=config, row=row, col=col, new=new, **axis_kwargs
415
- )
416
- if config is not None:
417
- return image
418
-
419
- image = self._init_image(
420
- image=image,
421
- monitor=monitor,
422
- monitor_type=monitor_type,
423
- color_bar=color_bar,
424
- color_map=color_map,
425
- data=data,
426
- vrange=vrange,
427
- )
428
- return image
429
-
430
- def motor_map(
431
- self,
432
- motor_x: str = None,
433
- motor_y: str = None,
434
- new: bool = False,
435
- row: int | None = None,
436
- col: int | None = None,
437
- config: dict | None = None,
438
- **axis_kwargs,
439
- ) -> BECMotorMap:
440
- """
441
- Add a motor map to the figure. Always access the first motor map widget in the figure.
442
-
443
- Args:
444
- motor_x(str): The name of the motor for the X axis.
445
- motor_y(str): The name of the motor for the Y axis.
446
- new(bool): If True, create a new plot instead of using the first plot.
447
- row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
448
- col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
449
- config(dict): Recreates the whole BECImageShow widget from provided configuration.
450
- **axis_kwargs: Additional axis properties to set on the widget after creation.
451
-
452
- Returns:
453
- BECMotorMap: The motor map widget.
454
- """
455
- motor_map = self.subplot_factory(
456
- widget_type="BECMotorMap", config=config, row=row, col=col, new=new, **axis_kwargs
457
- )
458
- if config is not None:
459
- return motor_map
460
-
461
- if motor_x is not None and motor_y is not None:
462
- motor_map.change_motors(motor_x, motor_y)
463
-
464
- return motor_map
465
-
466
- def multi_waveform(
467
- self,
468
- monitor: str = None,
469
- new: bool = False,
470
- row: int | None = None,
471
- col: int | None = None,
472
- config: dict | None = None,
473
- **axis_kwargs,
474
- ):
475
- multi_waveform = self.subplot_factory(
476
- widget_type="BECMultiWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
477
- )
478
- if config is not None:
479
- return multi_waveform
480
- multi_waveform.set_monitor(monitor)
481
- return multi_waveform
482
-
483
- def subplot_factory(
484
- self,
485
- widget_type: Literal[
486
- "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap", "BECMultiWaveform"
487
- ] = "BECPlotBase",
488
- row: int = None,
489
- col: int = None,
490
- config=None,
491
- new: bool = False,
492
- **axis_kwargs,
493
- ) -> BECPlotBase:
494
- # Case 1 - config provided, new plot, possible to define coordinates
495
- if config is not None:
496
- widget_cls = config["widget_class"]
497
- if widget_cls != widget_type:
498
- raise ValueError(
499
- f"Widget type '{widget_type}' does not match the provided configuration ({widget_cls})."
500
- )
501
- widget = self.add_widget(
502
- widget_type=widget_type, config=config, row=row, col=col, **axis_kwargs
503
- )
504
- return widget
505
-
506
- # Case 2 - find first plot or create first plot if no plot available, no config provided, no coordinates
507
- if new is False and (row is None or col is None):
508
- widget = WidgetContainerUtils.find_first_widget_by_class(
509
- self._widgets, self.subplot_map[widget_type], can_fail=True
510
- )
511
- if widget is not None:
512
- if axis_kwargs:
513
- widget.set(**axis_kwargs)
514
- else:
515
- widget = self.add_widget(widget_type=widget_type, **axis_kwargs)
516
- return widget
517
-
518
- # Case 3 - modifying existing plot wit coordinates provided
519
- if new is False and (row is not None and col is not None):
520
- try:
521
- widget = self.axes(row, col)
522
- except ValueError:
523
- widget = None
524
- if widget is not None:
525
- if axis_kwargs:
526
- widget.set(**axis_kwargs)
527
- else:
528
- widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
529
- return widget
530
-
531
- # Case 4 - no previous plot or new plot, no config provided, possible to define coordinates
532
- widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
533
- return widget
534
-
535
- def add_widget(
536
- self,
537
- widget_type: Literal[
538
- "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap", "BECMultiWaveform"
539
- ] = "BECPlotBase",
540
- widget_id: str = None,
541
- row: int = None,
542
- col: int = None,
543
- config=None,
544
- **axis_kwargs,
545
- ) -> BECPlotBase:
546
- """
547
- Add a widget to the figure at the specified position.
548
-
549
- Args:
550
- widget_type(Literal["PlotBase","Waveform1D"]): The type of the widget to add.
551
- widget_id(str): The unique identifier of the widget. If not provided, a unique ID will be generated.
552
- row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
553
- col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
554
- config(dict): Additional configuration for the widget.
555
- **axis_kwargs(dict): Additional axis properties to set on the widget after creation.
556
- """
557
- if not widget_id:
558
- widget_id = str(uuid.uuid4())
559
- if widget_id in self._widgets:
560
- raise ValueError(f"Widget with ID '{widget_id}' already exists.")
561
-
562
- # Check if position is occupied
563
- if row is not None and col is not None:
564
- if self.getItem(row, col):
565
- raise ValueError(f"Position at row {row} and column {col} is already occupied.")
566
- else:
567
- row, col = self._find_next_empty_position()
568
-
569
- widget = self.widget_handler.create_widget(
570
- widget_type=widget_type,
571
- widget_id=widget_id,
572
- parent_figure=self,
573
- parent_id=self.gui_id,
574
- config=config,
575
- **axis_kwargs,
576
- )
577
- widget.set_gui_id(widget_id)
578
- widget.config.row = row
579
- widget.config.col = col
580
-
581
- # Add widget to the figure
582
- self.addItem(widget, row=row, col=col)
583
-
584
- # Update num_cols and num_rows based on the added widget
585
- self.config.num_rows = max(self.config.num_rows, row + 1)
586
- self.config.num_cols = max(self.config.num_cols, col + 1)
587
-
588
- # Saving config for future referencing
589
-
590
- self.config.widgets[widget_id] = widget.config
591
- self._widgets[widget_id] = widget
592
-
593
- # Reflect the grid coordinates
594
- self._change_grid(widget_id, row, col)
595
-
596
- return widget
597
-
598
- def remove(
599
- self,
600
- row: int = None,
601
- col: int = None,
602
- widget_id: str = None,
603
- coordinates: tuple[int, int] = None,
604
- ) -> None:
605
- """
606
- Remove a widget from the figure. Can be removed by its unique identifier or by its coordinates.
607
-
608
- Args:
609
- row(int): The row coordinate of the widget to remove.
610
- col(int): The column coordinate of the widget to remove.
611
- widget_id(str): The unique identifier of the widget to remove.
612
- coordinates(tuple[int, int], optional): The coordinates of the widget to remove.
613
- """
614
- if widget_id:
615
- self._remove_by_id(widget_id)
616
- elif row is not None and col is not None:
617
- self._remove_by_coordinates(row, col)
618
- elif coordinates:
619
- self._remove_by_coordinates(*coordinates)
620
- else:
621
- raise ValueError("Must provide either widget_id or coordinates for removal.")
622
-
623
- def change_theme(self, theme: Literal["dark", "light"]) -> None:
624
- """
625
- Change the theme of the figure widget.
626
-
627
- Args:
628
- theme(Literal["dark","light"]): The theme to set for the figure widget.
629
- """
630
- self.config.theme = theme
631
- apply_theme(theme)
632
- for plot in self.widget_list:
633
- plot.set_x_label(plot.plot_item.getAxis("bottom").label.toPlainText())
634
- plot.set_y_label(plot.plot_item.getAxis("left").label.toPlainText())
635
- if plot.plot_item.titleLabel.text:
636
- plot.set_title(plot.plot_item.titleLabel.text)
637
- plot.set_legend_label_size()
638
-
639
- def _remove_by_coordinates(self, row: int, col: int) -> None:
640
- """
641
- Remove a widget from the figure by its coordinates.
642
-
643
- Args:
644
- row(int): The row coordinate of the widget to remove.
645
- col(int): The column coordinate of the widget to remove.
646
- """
647
- widget = self.axes(row, col)
648
- if widget:
649
- widget_id = widget.config.gui_id
650
- if widget_id in self._widgets:
651
- self._remove_by_id(widget_id)
652
-
653
- def _remove_by_id(self, widget_id: str) -> None:
654
- """
655
- Remove a widget from the figure by its unique identifier.
656
-
657
- Args:
658
- widget_id(str): The unique identifier of the widget to remove.
659
- """
660
- if widget_id in self._widgets:
661
- widget = self._widgets.pop(widget_id)
662
- widget.cleanup_pyqtgraph()
663
- widget.cleanup()
664
- self.removeItem(widget)
665
- self.grid[widget.config.row][widget.config.col] = None
666
- self._reindex_grid()
667
- if widget_id in self.config.widgets:
668
- self.config.widgets.pop(widget_id)
669
- widget.deleteLater()
670
- else:
671
- raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
672
-
673
- def axes(self, row: int, col: int) -> BECPlotBase:
674
- """
675
- Get widget by its coordinates in the figure.
676
-
677
- Args:
678
- row(int): the row coordinate
679
- col(int): the column coordinate
680
-
681
- Returns:
682
- BECPlotBase: the widget at the given coordinates
683
- """
684
- widget = self.getItem(row, col)
685
- if widget is None:
686
- raise ValueError(f"No widget at coordinates ({row}, {col})")
687
- return widget
688
-
689
- def _find_next_empty_position(self):
690
- """Find the next empty position (new row) in the figure."""
691
- row, col = 0, 0
692
- while self.getItem(row, col):
693
- row += 1
694
- return row, col
695
-
696
- def _change_grid(self, widget_id: str, row: int, col: int):
697
- """
698
- Change the grid to reflect the new position of the widget.
699
-
700
- Args:
701
- widget_id(str): The unique identifier of the widget.
702
- row(int): The new row coordinate of the widget in the figure.
703
- col(int): The new column coordinate of the widget in the figure.
704
- """
705
- while len(self.grid) <= row:
706
- self.grid.append([])
707
- row = self.grid[row]
708
- while len(row) <= col:
709
- row.append(None)
710
- row[col] = widget_id
711
-
712
- def _reindex_grid(self):
713
- """Reindex the grid to remove empty rows and columns."""
714
- new_grid = []
715
- for row in self.grid:
716
- new_row = [widget for widget in row if widget is not None]
717
- if new_row:
718
- new_grid.append(new_row)
719
- #
720
- # Update the config of each object to reflect its new position
721
- for row_idx, row in enumerate(new_grid):
722
- for col_idx, widget in enumerate(row):
723
- self._widgets[widget].config.row, self._widgets[widget].config.col = (
724
- row_idx,
725
- col_idx,
726
- )
727
-
728
- self.grid = new_grid
729
- self._replot_layout()
730
-
731
- def _replot_layout(self):
732
- """Replot the layout based on the current grid configuration."""
733
- self.clear()
734
- for row_idx, row in enumerate(self.grid):
735
- for col_idx, widget in enumerate(row):
736
- self.addItem(self._widgets[widget], row=row_idx, col=col_idx)
737
-
738
- def change_layout(self, max_columns=None, max_rows=None):
739
- """
740
- Reshuffle the layout of the figure to adjust to a new number of max_columns or max_rows.
741
- If both max_columns and max_rows are provided, max_rows is ignored.
742
-
743
- Args:
744
- max_columns (Optional[int]): The new maximum number of columns in the figure.
745
- max_rows (Optional[int]): The new maximum number of rows in the figure.
746
- """
747
- # Calculate total number of widgets
748
- total_widgets = len(self._widgets)
749
-
750
- if max_columns:
751
- # Calculate the required number of rows based on max_columns
752
- required_rows = (total_widgets + max_columns - 1) // max_columns
753
- new_grid = [[None for _ in range(max_columns)] for _ in range(required_rows)]
754
- elif max_rows:
755
- # Calculate the required number of columns based on max_rows
756
- required_columns = (total_widgets + max_rows - 1) // max_rows
757
- new_grid = [[None for _ in range(required_columns)] for _ in range(max_rows)]
758
- else:
759
- # If neither max_columns nor max_rows is specified, just return without changing the layout
760
- return
761
-
762
- # Populate the new grid with widgets' IDs
763
- current_idx = 0
764
- for widget_id in self._widgets:
765
- row = current_idx // len(new_grid[0])
766
- col = current_idx % len(new_grid[0])
767
- new_grid[row][col] = widget_id
768
- current_idx += 1
769
-
770
- self.config.num_rows = row
771
- self.config.num_cols = col
772
-
773
- # Update widgets' positions and replot them according to the new grid
774
- self.grid = new_grid
775
- self._reindex_grid() # This method should be updated to handle reshuffling correctly
776
- self._replot_layout() # Assumes this method re-adds widgets to the layout based on self.grid
777
-
778
- def clear_all(self):
779
- """Clear all widgets from the figure and reset to default state"""
780
- for widget in list(self._widgets.values()):
781
- widget.remove()
782
- self._widgets.clear()
783
- self.grid = []
784
- theme = self.config.theme
785
- self.config = FigureConfig(
786
- widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
787
- )
788
-
789
- def cleanup_pyqtgraph_all_widgets(self):
790
- """Clean up the pyqtgraph widget."""
791
- for widget in self.widget_list:
792
- widget.cleanup_pyqtgraph()
793
-
794
- def cleanup(self):
795
- """Close the figure widget."""
796
- self.cleanup_pyqtgraph_all_widgets()