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.
- CHANGELOG.md +30 -35
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +56 -0
- bec_widgets/tests/__init__.py +0 -0
- bec_widgets/tests/utils.py +226 -0
- bec_widgets/utils/colors.py +110 -27
- bec_widgets/utils/filter_io.py +156 -0
- bec_widgets/utils/widget_io.py +12 -9
- bec_widgets/widgets/base_classes/device_input_base.py +331 -62
- bec_widgets/widgets/base_classes/device_signal_input_base.py +280 -0
- bec_widgets/widgets/bec_status_box/status_item.py +1 -0
- bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py +1 -1
- bec_widgets/widgets/device_combobox/device_combo_box_plugin.py +1 -1
- bec_widgets/widgets/device_combobox/device_combobox.py +118 -41
- bec_widgets/widgets/device_line_edit/device_line_edit.py +122 -59
- bec_widgets/widgets/device_line_edit/device_line_edit_plugin.py +1 -1
- bec_widgets/widgets/image/image_widget.py +7 -1
- bec_widgets/widgets/motor_map/motor_map_widget.py +4 -2
- bec_widgets/widgets/positioner_box/positioner_box.py +4 -1
- bec_widgets/widgets/scan_control/scan_group_box.py +3 -1
- bec_widgets/widgets/signal_combobox/__init__.py +0 -0
- bec_widgets/widgets/signal_combobox/register_signal_combobox.py +15 -0
- bec_widgets/widgets/signal_combobox/signal_combobox.py +115 -0
- bec_widgets/widgets/signal_combobox/signal_combobox.pyproject +1 -0
- bec_widgets/widgets/signal_combobox/signal_combobox_plugin.py +54 -0
- bec_widgets/widgets/signal_line_edit/__init__.py +0 -0
- bec_widgets/widgets/signal_line_edit/register_signal_line_edit.py +15 -0
- bec_widgets/widgets/signal_line_edit/signal_line_edit.py +140 -0
- bec_widgets/widgets/signal_line_edit/signal_line_edit.pyproject +1 -0
- bec_widgets/widgets/signal_line_edit/signal_line_edit_plugin.py +54 -0
- {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/METADATA +1 -1
- {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/RECORD +36 -22
- pyproject.toml +1 -1
- {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.0.2.dist-info → bec_widgets-1.2.0.dist-info}/entry_points.txt +0 -0
- {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
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
|
+
]
|
bec_widgets/utils/colors.py
CHANGED
@@ -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,
|
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
|
216
|
+
ValueError: If theme_offset is not between 0 and 1.
|
127
217
|
"""
|
218
|
+
|
128
219
|
cmap = pg.colormap.get(colormap)
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
235
|
+
color_list.append(QColor.fromRgbF(*color).name())
|
152
236
|
elif format.upper() == "RGB":
|
153
|
-
|
237
|
+
color_list.append(tuple((np.array(color) * 255).astype(int)))
|
154
238
|
elif format.upper() == "QCOLOR":
|
155
|
-
|
239
|
+
color_list.append(QColor.fromRgbF(*color))
|
156
240
|
else:
|
157
241
|
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
158
|
-
|
159
|
-
return colors
|
242
|
+
return color_list
|
160
243
|
|
161
244
|
@staticmethod
|
162
245
|
def hex_to_rgba(hex_color: str, alpha=255) -> tuple:
|