bec-widgets 1.0.2__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. CHANGELOG.md +30 -35
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +56 -0
  4. bec_widgets/tests/__init__.py +0 -0
  5. bec_widgets/tests/utils.py +226 -0
  6. bec_widgets/utils/colors.py +110 -27
  7. bec_widgets/utils/filter_io.py +156 -0
  8. bec_widgets/utils/widget_io.py +12 -9
  9. bec_widgets/widgets/base_classes/device_input_base.py +331 -62
  10. bec_widgets/widgets/base_classes/device_signal_input_base.py +280 -0
  11. bec_widgets/widgets/bec_status_box/status_item.py +1 -0
  12. bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py +1 -1
  13. bec_widgets/widgets/device_combobox/device_combo_box_plugin.py +1 -1
  14. bec_widgets/widgets/device_combobox/device_combobox.py +118 -41
  15. bec_widgets/widgets/device_line_edit/device_line_edit.py +122 -59
  16. bec_widgets/widgets/device_line_edit/device_line_edit_plugin.py +1 -1
  17. bec_widgets/widgets/image/image_widget.py +7 -1
  18. bec_widgets/widgets/motor_map/motor_map_widget.py +4 -2
  19. bec_widgets/widgets/positioner_box/positioner_box.py +4 -1
  20. bec_widgets/widgets/scan_control/scan_group_box.py +3 -1
  21. bec_widgets/widgets/signal_combobox/__init__.py +0 -0
  22. bec_widgets/widgets/signal_combobox/register_signal_combobox.py +15 -0
  23. bec_widgets/widgets/signal_combobox/signal_combobox.py +115 -0
  24. bec_widgets/widgets/signal_combobox/signal_combobox.pyproject +1 -0
  25. bec_widgets/widgets/signal_combobox/signal_combobox_plugin.py +54 -0
  26. bec_widgets/widgets/signal_line_edit/__init__.py +0 -0
  27. bec_widgets/widgets/signal_line_edit/register_signal_line_edit.py +15 -0
  28. bec_widgets/widgets/signal_line_edit/signal_line_edit.py +140 -0
  29. bec_widgets/widgets/signal_line_edit/signal_line_edit.pyproject +1 -0
  30. bec_widgets/widgets/signal_line_edit/signal_line_edit_plugin.py +54 -0
  31. {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/METADATA +1 -1
  32. {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/RECORD +36 -22
  33. pyproject.toml +1 -1
  34. {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/WHEEL +0 -0
  35. {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/entry_points.txt +0 -0
  36. {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v1.2.0 (2024-10-25)
5
+
6
+ ### Features
7
+
8
+ * feat(colors): evenly spaced color generation + new golden ratio calculation ([`40c9fea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40c9fea35f869ef52e05948dd1989bcd99f602e0))
9
+
10
+ ### Refactoring
11
+
12
+ * refactor: add bec_lib version to statusbox ([`5d4b86e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4b86e1c6e1800051afce4f991153e370767fa6))
13
+
14
+
15
+ ## v1.1.0 (2024-10-25)
16
+
17
+ ### Features
18
+
19
+ * feat: add filter i/o utility class ([`0350833`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0350833f36e0a7cadce4173f9b1d1fbfdf985375))
20
+
21
+ ### Refactoring
22
+
23
+ * refactor: do not flush selection upon receiving config update; allow widgetIO to receive kwargs to be able to use get_value to receive string instead of int for QComboBox ([`91959e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/91959e82de8586934af3ebb5aaa0923930effc51))
24
+
25
+ * refactor: allow to set selection in DeviceInput; automatic update of selection on device config update; cleanup ([`5eb15b7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5eb15b785f12e30eb8ccbc56d4ad9e759a4cf5eb))
26
+
27
+ * refactor: cleanup, added device_signal for signal inputs ([`6fb2055`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fb20552ff57978f4aeb79fd7f062f8d6b5581e7))
28
+
29
+ ### Testing
30
+
31
+ * test(scan_control): tests added for grid_scan to ensure scan_args signal validity ([`acb7902`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/acb79020d4be546efc001ff47b6f5cdba2ee9375))
32
+
33
+
4
34
  ## v1.0.2 (2024-10-22)
5
35
 
6
36
  ### Bug Fixes
@@ -139,38 +169,3 @@ complete UI ([`49268e3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/49268e38
139
169
  * fix: use new 'scan_axis' signal, to set_x and select x axis on waveform
140
170
 
141
171
  Fixes #361, do not try to change x axis when not permitted ([`efa2763`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/efa276358b0f5a45cce9fa84fa5f9aafaf4284f7))
142
-
143
- ### Features
144
-
145
- * feat: new 'scan_axis' signal
146
-
147
- Signal is emitted before "scan_started", to inform about scan positioner
148
- and (start, stop) positions. In case of multiple bundles, the signal
149
- is emitted multiple times. ([`f084e25`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f084e2514bc9459cccaa951b79044bc25884e738))
150
-
151
-
152
- ## v0.113.0 (2024-10-02)
153
-
154
- ### Bug Fixes
155
-
156
- * fix: add is_log checks and functionality to plot_indicator_items ([`0f9953e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0f9953e8fdcf3f9b5a09f994c69edb6b34756df9))
157
-
158
- ### Features
159
-
160
- * feat: add first draft for alignment_1d GUI ([`63c24f9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/63c24f97a355edaa928b6e222909252b276bcada))
161
-
162
- * feat: add move to position button to lmfit dialog ([`281cb27`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/281cb27d8b5433e27a7ba0ca0a19e4b45b9c544f))
163
-
164
- ### Refactoring
165
-
166
- * refactor: various minor improvements for the alignment gui ([`f554f3c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f554f3c1672c4fe32968a5991dc98802556a6f3b))
167
-
168
- * refactor: allow hiding of arg/kwarg boxes ([`efe90eb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/efe90eb163e2123a5b4d0bb59f66025a569336ad))
169
-
170
- ### Testing
171
-
172
- * test: add tests for scan_status_callback ([`dc0c825`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dc0c825fd594c093a24543ff803d6c6564010e92))
173
-
174
- ### Unknown
175
-
176
- * feat : Add bec_signal_proxy to handle signals with option to unblock them manually. ([`1dcfeb6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1dcfeb6cfce3c69f0c5401731d4d3f9a1981b22e))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 1.0.2
3
+ Version: 1.2.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
bec_widgets/cli/client.py CHANGED
@@ -38,6 +38,8 @@ class Widgets(str, enum.Enum):
38
38
  ResumeButton = "ResumeButton"
39
39
  RingProgressBar = "RingProgressBar"
40
40
  ScanControl = "ScanControl"
41
+ SignalComboBox = "SignalComboBox"
42
+ SignalLineEdit = "SignalLineEdit"
41
43
  StopButton = "StopButton"
42
44
  TextBox = "TextBox"
43
45
  VSCodeEditor = "VSCodeEditor"
@@ -2591,6 +2593,24 @@ class DeviceLineEdit(RPCBase):
2591
2593
  """
2592
2594
 
2593
2595
 
2596
+ class DeviceSignalInputBase(RPCBase):
2597
+ @property
2598
+ @rpc_call
2599
+ def _config_dict(self) -> "dict":
2600
+ """
2601
+ Get the configuration of the widget.
2602
+
2603
+ Returns:
2604
+ dict: The configuration of the widget.
2605
+ """
2606
+
2607
+ @rpc_call
2608
+ def _get_all_rpc(self) -> "dict":
2609
+ """
2610
+ Get all registered RPC objects.
2611
+ """
2612
+
2613
+
2594
2614
  class LMFitDialog(RPCBase):
2595
2615
  @property
2596
2616
  @rpc_call
@@ -3003,6 +3023,42 @@ class ScanControl(RPCBase):
3003
3023
  """
3004
3024
 
3005
3025
 
3026
+ class SignalComboBox(RPCBase):
3027
+ @property
3028
+ @rpc_call
3029
+ def _config_dict(self) -> "dict":
3030
+ """
3031
+ Get the configuration of the widget.
3032
+
3033
+ Returns:
3034
+ dict: The configuration of the widget.
3035
+ """
3036
+
3037
+ @rpc_call
3038
+ def _get_all_rpc(self) -> "dict":
3039
+ """
3040
+ Get all registered RPC objects.
3041
+ """
3042
+
3043
+
3044
+ class SignalLineEdit(RPCBase):
3045
+ @property
3046
+ @rpc_call
3047
+ def _config_dict(self) -> "dict":
3048
+ """
3049
+ Get the configuration of the widget.
3050
+
3051
+ Returns:
3052
+ dict: The configuration of the widget.
3053
+ """
3054
+
3055
+ @rpc_call
3056
+ def _get_all_rpc(self) -> "dict":
3057
+ """
3058
+ Get all registered RPC objects.
3059
+ """
3060
+
3061
+
3006
3062
  class StopButton(RPCBase):
3007
3063
  @property
3008
3064
  @rpc_call
File without changes
@@ -0,0 +1,226 @@
1
+ from unittest.mock import MagicMock
2
+
3
+ from bec_lib.device import Device as BECDevice
4
+ from bec_lib.device import Positioner as BECPositioner
5
+ from bec_lib.device import ReadoutPriority
6
+ from bec_lib.devicemanager import DeviceContainer
7
+
8
+
9
+ class FakeDevice(BECDevice):
10
+ """Fake minimal positioner class for testing."""
11
+
12
+ def __init__(self, name, enabled=True, readout_priority=ReadoutPriority.MONITORED):
13
+ super().__init__(name=name)
14
+ self._enabled = enabled
15
+ self.signals = {self.name: {"value": 1.0}}
16
+ self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
17
+ self._readout_priority = readout_priority
18
+ self._config = {
19
+ "readoutPriority": "baseline",
20
+ "deviceClass": "ophyd.Device",
21
+ "deviceConfig": {},
22
+ "deviceTags": ["user device"],
23
+ "enabled": enabled,
24
+ "readOnly": False,
25
+ "name": self.name,
26
+ }
27
+
28
+ @property
29
+ def readout_priority(self):
30
+ return self._readout_priority
31
+
32
+ @readout_priority.setter
33
+ def readout_priority(self, value):
34
+ self._readout_priority = value
35
+
36
+ @property
37
+ def limits(self) -> tuple[float, float]:
38
+ return self._limits
39
+
40
+ @limits.setter
41
+ def limits(self, value: tuple[float, float]):
42
+ self._limits = value
43
+
44
+ def __contains__(self, item):
45
+ return item == self.name
46
+
47
+ @property
48
+ def _hints(self):
49
+ return [self.name]
50
+
51
+ def set_value(self, fake_value: float = 1.0) -> None:
52
+ """
53
+ Setup fake value for device readout
54
+ Args:
55
+ fake_value(float): Desired fake value
56
+ """
57
+ self.signals[self.name]["value"] = fake_value
58
+
59
+ def describe(self) -> dict:
60
+ """
61
+ Get the description of the device
62
+ Returns:
63
+ dict: Description of the device
64
+ """
65
+ return self.description
66
+
67
+
68
+ class FakePositioner(BECPositioner):
69
+
70
+ def __init__(
71
+ self,
72
+ name,
73
+ enabled=True,
74
+ limits=None,
75
+ read_value=1.0,
76
+ readout_priority=ReadoutPriority.MONITORED,
77
+ ):
78
+ super().__init__(name=name)
79
+ # self.limits = limits if limits is not None else [0.0, 0.0]
80
+ self.read_value = read_value
81
+ self.setpoint_value = read_value
82
+ self.motor_is_moving_value = 0
83
+ self._enabled = enabled
84
+ self._limits = limits
85
+ self._readout_priority = readout_priority
86
+ self.signals = {self.name: {"value": 1.0}}
87
+ self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
88
+ self._config = {
89
+ "readoutPriority": "baseline",
90
+ "deviceClass": "ophyd_devices.SimPositioner",
91
+ "deviceConfig": {"delay": 1, "tolerance": 0.01, "update_frequency": 400},
92
+ "deviceTags": ["user motors"],
93
+ "enabled": enabled,
94
+ "readOnly": False,
95
+ "name": self.name,
96
+ }
97
+ self._info = {
98
+ "signals": {
99
+ "readback": {"kind_str": "5"}, # hinted
100
+ "setpoint": {"kind_str": "1"}, # normal
101
+ "velocity": {"kind_str": "2"}, # config
102
+ }
103
+ }
104
+ self.signals = {
105
+ self.name: {"value": self.read_value},
106
+ f"{self.name}_setpoint": {"value": self.setpoint_value},
107
+ f"{self.name}_motor_is_moving": {"value": self.motor_is_moving_value},
108
+ }
109
+
110
+ @property
111
+ def readout_priority(self):
112
+ return self._readout_priority
113
+
114
+ @readout_priority.setter
115
+ def readout_priority(self, value):
116
+ self._readout_priority = value
117
+
118
+ @property
119
+ def enabled(self) -> bool:
120
+ return self._enabled
121
+
122
+ @enabled.setter
123
+ def enabled(self, value: bool):
124
+ self._enabled = value
125
+
126
+ @property
127
+ def limits(self) -> tuple[float, float]:
128
+ return self._limits
129
+
130
+ @limits.setter
131
+ def limits(self, value: tuple[float, float]):
132
+ self._limits = value
133
+
134
+ def __contains__(self, item):
135
+ return item == self.name
136
+
137
+ @property
138
+ def _hints(self):
139
+ return [self.name]
140
+
141
+ def set_value(self, fake_value: float = 1.0) -> None:
142
+ """
143
+ Setup fake value for device readout
144
+ Args:
145
+ fake_value(float): Desired fake value
146
+ """
147
+ self.read_value = fake_value
148
+
149
+ def describe(self) -> dict:
150
+ """
151
+ Get the description of the device
152
+ Returns:
153
+ dict: Description of the device
154
+ """
155
+ return self.description
156
+
157
+ @property
158
+ def precision(self):
159
+ return 3
160
+
161
+ def set_read_value(self, value):
162
+ self.read_value = value
163
+
164
+ def read(self):
165
+ return self.signals
166
+
167
+ def set_limits(self, limits):
168
+ self.limits = limits
169
+
170
+ def move(self, value, relative=False):
171
+ """Simulates moving the device to a new position."""
172
+ if relative:
173
+ self.read_value += value
174
+ else:
175
+ self.read_value = value
176
+ # Respect the limits
177
+ self.read_value = max(min(self.read_value, self.limits[1]), self.limits[0])
178
+
179
+ @property
180
+ def readback(self):
181
+ return MagicMock(get=MagicMock(return_value=self.read_value))
182
+
183
+
184
+ class Positioner(FakePositioner):
185
+ """just placeholder for testing embedded isinstance check in DeviceCombobox"""
186
+
187
+ def __init__(self, name="test", limits=None, read_value=1.0):
188
+ super().__init__(name, limits, read_value)
189
+
190
+
191
+ class Device(FakeDevice):
192
+ """just placeholder for testing embedded isinstance check in DeviceCombobox"""
193
+
194
+ def __init__(self, name, enabled=True):
195
+ super().__init__(name, enabled)
196
+
197
+
198
+ class DMMock:
199
+ def __init__(self):
200
+ self.devices = DeviceContainer()
201
+ self.enabled_devices = [device for device in self.devices if device.enabled]
202
+
203
+ def add_devives(self, devices: list):
204
+ for device in devices:
205
+ self.devices[device.name] = device
206
+
207
+
208
+ DEVICES = [
209
+ FakePositioner("samx", limits=[-10, 10], read_value=2.0),
210
+ FakePositioner("samy", limits=[-5, 5], read_value=3.0),
211
+ FakePositioner("samz", limits=[-8, 8], read_value=4.0),
212
+ FakePositioner("aptrx", limits=None, read_value=4.0),
213
+ FakePositioner("aptry", limits=None, read_value=5.0),
214
+ FakeDevice("gauss_bpm"),
215
+ FakeDevice("gauss_adc1"),
216
+ FakeDevice("gauss_adc2"),
217
+ FakeDevice("gauss_adc3"),
218
+ FakeDevice("bpm4i"),
219
+ FakeDevice("bpm3a"),
220
+ FakeDevice("bpm3i"),
221
+ FakeDevice("eiger", readout_priority=ReadoutPriority.ASYNC),
222
+ FakeDevice("waveform1d"),
223
+ FakeDevice("async_device", readout_priority=ReadoutPriority.ASYNC),
224
+ Positioner("test", limits=[-10, 10], read_value=2.0),
225
+ Device("test_device"),
226
+ ]
@@ -107,9 +107,98 @@ class Colors:
107
107
  angles.append(angle)
108
108
  return angles
109
109
 
110
+ @staticmethod
111
+ def set_theme_offset(theme: Literal["light", "dark"] | None = None, offset=0.2) -> tuple:
112
+ """
113
+ Set the theme offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
114
+
115
+ Args:
116
+ theme(str): The theme to be applied.
117
+ offset(float): Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
118
+
119
+ Returns:
120
+ tuple: Tuple of min_pos and max_pos.
121
+
122
+ Raises:
123
+ ValueError: If theme_offset is not between 0 and 1.
124
+ """
125
+
126
+ if offset < 0 or offset > 1:
127
+ raise ValueError("theme_offset must be between 0 and 1")
128
+
129
+ if theme is None:
130
+ app = QApplication.instance()
131
+ if hasattr(app, "theme"):
132
+ theme = app.theme.theme
133
+
134
+ if theme == "light":
135
+ min_pos = 0.0
136
+ max_pos = 1 - offset
137
+ else:
138
+ min_pos = 0.0 + offset
139
+ max_pos = 1.0
140
+
141
+ return min_pos, max_pos
142
+
143
+ @staticmethod
144
+ def evenly_spaced_colors(
145
+ colormap: str,
146
+ num: int,
147
+ format: Literal["QColor", "HEX", "RGB"] = "QColor",
148
+ theme_offset=0.2,
149
+ theme: Literal["light", "dark"] | None = None,
150
+ ) -> list:
151
+ """
152
+ Extract `num` colors from the specified colormap, evenly spaced along its range,
153
+ and return them in the specified format.
154
+
155
+ Args:
156
+ colormap (str): Name of the colormap.
157
+ num (int): Number of requested colors.
158
+ format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
159
+ theme_offset (float): Has to be between 0-1. Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
160
+ theme (Literal['light', 'dark'] | None): The theme to be applied. Overrides the QApplication theme if specified.
161
+
162
+ Returns:
163
+ list: List of colors in the specified format.
164
+
165
+ Raises:
166
+ ValueError: If theme_offset is not between 0 and 1.
167
+ """
168
+ if theme_offset < 0 or theme_offset > 1:
169
+ raise ValueError("theme_offset must be between 0 and 1")
170
+
171
+ cmap = pg.colormap.get(colormap)
172
+ min_pos, max_pos = Colors.set_theme_offset(theme, theme_offset)
173
+
174
+ # Generate positions that are evenly spaced within the acceptable range
175
+ if num == 1:
176
+ positions = np.array([(min_pos + max_pos) / 2])
177
+ else:
178
+ positions = np.linspace(min_pos, max_pos, num)
179
+
180
+ # Sample colors from the colormap at the calculated positions
181
+ colors = cmap.map(positions, mode="float")
182
+ color_list = []
183
+
184
+ for color in colors:
185
+ if format.upper() == "HEX":
186
+ color_list.append(QColor.fromRgbF(*color).name())
187
+ elif format.upper() == "RGB":
188
+ color_list.append(tuple((np.array(color) * 255).astype(int)))
189
+ elif format.upper() == "QCOLOR":
190
+ color_list.append(QColor.fromRgbF(*color))
191
+ else:
192
+ raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
193
+ return color_list
194
+
110
195
  @staticmethod
111
196
  def golden_angle_color(
112
- colormap: str, num: int, format: Literal["QColor", "HEX", "RGB"] = "QColor"
197
+ colormap: str,
198
+ num: int,
199
+ format: Literal["QColor", "HEX", "RGB"] = "QColor",
200
+ theme_offset=0.2,
201
+ theme: Literal["dark", "light"] | None = None,
113
202
  ) -> list:
114
203
  """
115
204
  Extract num colors from the specified colormap following golden angle distribution and return them in the specified format.
@@ -118,45 +207,39 @@ class Colors:
118
207
  colormap (str): Name of the colormap.
119
208
  num (int): Number of requested colors.
120
209
  format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
210
+ theme_offset (float): Has to be between 0-1. Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
121
211
 
122
212
  Returns:
123
213
  list: List of colors in the specified format.
124
214
 
125
215
  Raises:
126
- ValueError: If the number of requested colors is greater than the number of colors in the colormap.
216
+ ValueError: If theme_offset is not between 0 and 1.
127
217
  """
218
+
128
219
  cmap = pg.colormap.get(colormap)
129
- cmap_colors = cmap.getColors(mode="float")
130
- if num > len(cmap_colors):
131
- raise ValueError(
132
- f"Number of colors requested ({num}) is greater than the number of colors in the colormap ({len(cmap_colors)})"
133
- )
134
- angles = Colors.golden_ratio(len(cmap_colors))
135
- color_selection = np.round(np.interp(angles, (-np.pi, np.pi), (0, len(cmap_colors))))
136
- colors = []
137
- ii = 0
138
- while len(colors) < num:
139
- color_index = int(color_selection[ii])
140
- color = cmap_colors[color_index]
141
- app = QApplication.instance()
142
- if hasattr(app, "theme") and app.theme.theme == "light":
143
- background = 255
144
- else:
145
- background = 0
146
- if np.abs(np.mean(color[:3] * 255) - background) < 50:
147
- ii += 1
148
- continue
220
+ phi = (1 + np.sqrt(5)) / 2 # Golden ratio
221
+ golden_angle_conjugate = 1 - (1 / phi) # Approximately 0.38196601125
222
+
223
+ min_pos, max_pos = Colors.set_theme_offset(theme, theme_offset)
224
+
225
+ # Generate positions within the acceptable range
226
+ positions = np.mod(np.arange(num) * golden_angle_conjugate, 1)
227
+ positions = min_pos + positions * (max_pos - min_pos)
228
+
229
+ # Sample colors from the colormap at the calculated positions
230
+ colors = cmap.map(positions, mode="float")
231
+ color_list = []
149
232
 
233
+ for color in colors:
150
234
  if format.upper() == "HEX":
151
- colors.append(QColor.fromRgbF(*color).name())
235
+ color_list.append(QColor.fromRgbF(*color).name())
152
236
  elif format.upper() == "RGB":
153
- colors.append(tuple((np.array(color) * 255).astype(int)))
237
+ color_list.append(tuple((np.array(color) * 255).astype(int)))
154
238
  elif format.upper() == "QCOLOR":
155
- colors.append(QColor.fromRgbF(*color))
239
+ color_list.append(QColor.fromRgbF(*color))
156
240
  else:
157
241
  raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
158
- ii += 1
159
- return colors
242
+ return color_list
160
243
 
161
244
  @staticmethod
162
245
  def hex_to_rgba(hex_color: str, alpha=255) -> tuple: