qplotter 1.6.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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +67 -0
- qplot/_build_info.json +1 -0
- qplot/_metadata.py +75 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +578 -0
- qplot/configuration/config_schema.json +357 -0
- qplot/configuration/fonts.py +3 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +592 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2340 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2566 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +148 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +876 -0
- qplot/datahandling/trusted_heatmap.py +246 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1919 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1804 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +123 -0
- qplot/tools/general.py +139 -0
- qplot/tools/heatmap_geometry.py +391 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +650 -0
- qplot/tools/sample_statistics.py +111 -0
- qplot/tools/worker.py +2838 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4933 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_display_precision.py +48 -0
- qplot/windows/_dragdrop.py +70 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +210 -0
- qplot/windows/_mixed_appearance.py +303 -0
- qplot/windows/_mixed_plots.py +461 -0
- qplot/windows/_native_averaging.py +323 -0
- qplot/windows/_native_transforms.py +551 -0
- qplot/windows/_plot1d_snap.py +612 -0
- qplot/windows/_plot1d_traces.py +2236 -0
- qplot/windows/_plot2d_colorbar.py +1127 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2291 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +2082 -0
- qplot/windows/_plot_actions.py +2703 -0
- qplot/windows/_plot_annotation.py +186 -0
- qplot/windows/_plot_appearance.py +283 -0
- qplot/windows/_plot_axis_scaling.py +1521 -0
- qplot/windows/_plot_data_viewbox.py +94 -0
- qplot/windows/_plot_drop_axes.py +224 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +112 -0
- qplot/windows/_plot_marquee.py +659 -0
- qplot/windows/_plot_refresh.py +950 -0
- qplot/windows/_plot_source_promotion.py +178 -0
- qplot/windows/_plot_state.py +237 -0
- qplot/windows/_plot_status.py +115 -0
- qplot/windows/_plot_tiling.py +35 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +724 -0
- qplot/windows/_refresh_interval.py +172 -0
- qplot/windows/_run_controls.py +789 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +532 -0
- qplot/windows/_subplots/subplot2d.py +919 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +414 -0
- qplot/windows/main.py +1632 -0
- qplot/windows/plot1d.py +313 -0
- qplot/windows/plot2d.py +1935 -0
- qplotter-1.6.0.dist-info/METADATA +322 -0
- qplotter-1.6.0.dist-info/RECORD +130 -0
- qplotter-1.6.0.dist-info/WHEEL +5 -0
- qplotter-1.6.0.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from PyQt6 import QtWidgets as qtw
|
|
4
|
+
from PyQt6.QtGui import QKeySequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def key_sequences(sequences):
|
|
8
|
+
"""
|
|
9
|
+
Converts shortcut text to QKeySequence instances.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
return [
|
|
13
|
+
sequence if isinstance(sequence, QKeySequence) else QKeySequence(sequence)
|
|
14
|
+
for sequence in sequences
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def platform_key_sequences(mac=None, windows=None, other=None):
|
|
19
|
+
"""
|
|
20
|
+
Returns shortcuts for the current platform.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
if sys.platform == "darwin":
|
|
24
|
+
sequences = mac or []
|
|
25
|
+
elif sys.platform.startswith("win"):
|
|
26
|
+
sequences = windows or []
|
|
27
|
+
else:
|
|
28
|
+
sequences = other if other is not None else windows or []
|
|
29
|
+
|
|
30
|
+
return key_sequences(sequences)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def standard_key_sequences(standard_key, fallback):
|
|
34
|
+
"""
|
|
35
|
+
Returns Qt's platform-specific shortcuts, with explicit fallbacks.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
shortcuts = []
|
|
39
|
+
if qtw.QApplication.instance() is not None:
|
|
40
|
+
shortcuts = QKeySequence.keyBindings(standard_key)
|
|
41
|
+
|
|
42
|
+
for sequence in reversed(key_sequences(fallback)):
|
|
43
|
+
if sequence in shortcuts:
|
|
44
|
+
shortcuts.remove(sequence)
|
|
45
|
+
shortcuts.insert(0, sequence)
|
|
46
|
+
|
|
47
|
+
return shortcuts
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
import pyqtgraph as pg
|
|
4
|
+
from PyQt6 import QtCore
|
|
5
|
+
from PyQt6.QtGui import QColor
|
|
6
|
+
|
|
7
|
+
from .._native_transforms import NativePlotDataItem
|
|
8
|
+
from .._plot_drop_axes import DroppedAxisMapping
|
|
9
|
+
from .._plot_refresh import plot_refresh_required
|
|
10
|
+
from .._refresh_interval import refresh_interval_value, set_refresh_interval
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _subplot_axis_order(
|
|
14
|
+
parent_options: dict[str, str],
|
|
15
|
+
source_options: dict[str, str],
|
|
16
|
+
*,
|
|
17
|
+
source_is_cut: bool = False,
|
|
18
|
+
shared_parameter: str | None = None,
|
|
19
|
+
) -> tuple[str, str] | None:
|
|
20
|
+
"""Map source data axes onto a host plot, or reject incompatible axes."""
|
|
21
|
+
|
|
22
|
+
if shared_parameter:
|
|
23
|
+
parent_axes = [
|
|
24
|
+
axis for axis in ("x", "y")
|
|
25
|
+
if parent_options.get(axis) == shared_parameter
|
|
26
|
+
]
|
|
27
|
+
source_axes = [
|
|
28
|
+
axis for axis in ("x", "y")
|
|
29
|
+
if source_options.get(axis) == shared_parameter
|
|
30
|
+
]
|
|
31
|
+
if len(parent_axes) != 1 or len(source_axes) != 1:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
parent_axis = parent_axes[0]
|
|
35
|
+
source_axis = source_axes[0]
|
|
36
|
+
# A cut's axis_options['y'] is its fixed setpoint selection, while its
|
|
37
|
+
# axis_data['y'] remains the dependent cut values. It therefore cannot
|
|
38
|
+
# act as the shared coordinate.
|
|
39
|
+
if source_is_cut and source_axis != "x":
|
|
40
|
+
return None
|
|
41
|
+
return ("x", "y") if parent_axis == source_axis else ("y", "x")
|
|
42
|
+
|
|
43
|
+
shared_x = parent_options.get("x")
|
|
44
|
+
if not shared_x:
|
|
45
|
+
return None
|
|
46
|
+
if shared_x == source_options.get("x"):
|
|
47
|
+
return "x", "y"
|
|
48
|
+
if not source_is_cut and shared_x == source_options.get("y"):
|
|
49
|
+
return "y", "x"
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _subplot_shared_parameter(parent) -> str | None:
|
|
55
|
+
"""Return the host plot's canonical 1D independent parameter name."""
|
|
56
|
+
|
|
57
|
+
default_names = getattr(parent, "_default_plot_axis_names", None)
|
|
58
|
+
if callable(default_names):
|
|
59
|
+
names = default_names()
|
|
60
|
+
if names is not None:
|
|
61
|
+
return names[0]
|
|
62
|
+
|
|
63
|
+
independent = tuple(getattr(getattr(parent, "param", None), "depends_on_", ()))
|
|
64
|
+
if len(independent) == 1:
|
|
65
|
+
return str(independent[0])
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class subplot1d(NativePlotDataItem):
|
|
70
|
+
"""
|
|
71
|
+
Class for handling secondary line plots on plot1d
|
|
72
|
+
"""
|
|
73
|
+
def __init__(self, parent, from_win, *args, axis_order=None, **kargs):
|
|
74
|
+
super().__init__(*args, **kargs)
|
|
75
|
+
|
|
76
|
+
self.label = from_win.label
|
|
77
|
+
self.param_dict = from_win.param_dict
|
|
78
|
+
self.running = plot_refresh_required(from_win)
|
|
79
|
+
|
|
80
|
+
self.parent = parent
|
|
81
|
+
self.from_win = from_win
|
|
82
|
+
self._dropped_axis_mapping = (
|
|
83
|
+
DroppedAxisMapping(parent, from_win, axis_order)
|
|
84
|
+
if axis_order is not None else None
|
|
85
|
+
)
|
|
86
|
+
self._source_consumer_registered = False
|
|
87
|
+
self._source_interval_signal = None
|
|
88
|
+
self._source_interval_slot = None
|
|
89
|
+
|
|
90
|
+
self.choose_from: tuple[str, str] | None = None
|
|
91
|
+
self.axis_param = {}
|
|
92
|
+
self._source_update_signal = getattr(from_win, "trace_updated", None)
|
|
93
|
+
if self._source_update_signal is not None:
|
|
94
|
+
self._source_update_signal.connect(self._source_trace_updated)
|
|
95
|
+
self._source_compatibility_signal = getattr(
|
|
96
|
+
from_win,
|
|
97
|
+
"merge_compatibility_changed",
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
if self._source_compatibility_signal is not None:
|
|
101
|
+
self._source_compatibility_signal.connect(
|
|
102
|
+
self._source_compatibility_changed
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.refresh()
|
|
106
|
+
|
|
107
|
+
self.side = "left"
|
|
108
|
+
self.parent.plot.addItem(self)
|
|
109
|
+
self._register_source_consumer()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def refresh(self, *, source_ready: bool = False):
|
|
113
|
+
"""
|
|
114
|
+
Fetches data from source window and updates view on parent window
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
from_win = self.from_win
|
|
118
|
+
|
|
119
|
+
self._disconnect_pending_update()
|
|
120
|
+
|
|
121
|
+
# Update live state
|
|
122
|
+
self.running = plot_refresh_required(from_win)
|
|
123
|
+
|
|
124
|
+
self.choose_from = self.source_axis_order()
|
|
125
|
+
if self.choose_from is None:
|
|
126
|
+
self.setData(x=[], y=[])
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Wait for data to finish
|
|
130
|
+
if from_win.worker.running and not source_ready:
|
|
131
|
+
if self._source_update_signal is None:
|
|
132
|
+
from_win.end_wait.connect(self.call_update)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self.call_update()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def source_axis_order(self):
|
|
139
|
+
if self._dropped_axis_mapping is not None:
|
|
140
|
+
return self._dropped_axis_mapping.axis_order()
|
|
141
|
+
return _subplot_axis_order(
|
|
142
|
+
self.parent.axis_options,
|
|
143
|
+
self.from_win.axis_options,
|
|
144
|
+
source_is_cut=hasattr(self.from_win, "sweep_id"),
|
|
145
|
+
shared_parameter=_subplot_shared_parameter(self.parent),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@QtCore.pyqtSlot()
|
|
150
|
+
def _source_trace_updated(self):
|
|
151
|
+
"""Refresh immediately after the source publishes ready-to-use data."""
|
|
152
|
+
|
|
153
|
+
self.refresh(source_ready=True)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@QtCore.pyqtSlot()
|
|
157
|
+
def _source_compatibility_changed(self):
|
|
158
|
+
"""Clear old cut data until the changed axes publish replacement data."""
|
|
159
|
+
|
|
160
|
+
self._disconnect_pending_update()
|
|
161
|
+
self.choose_from = self.source_axis_order()
|
|
162
|
+
self.setData(x=[], y=[])
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _disconnect_pending_update(self):
|
|
166
|
+
try:
|
|
167
|
+
self.from_win.end_wait.disconnect(self.call_update)
|
|
168
|
+
except (TypeError, RuntimeError): # Signal was absent or already gone.
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _register_source_consumer(self):
|
|
173
|
+
if self._source_consumer_registered:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
source = self.from_win
|
|
177
|
+
source._merged_trace_users = max(
|
|
178
|
+
int(getattr(source, "_merged_trace_users", 0)),
|
|
179
|
+
0,
|
|
180
|
+
) + 1
|
|
181
|
+
self._source_consumer_registered = True
|
|
182
|
+
|
|
183
|
+
if not getattr(source, "visible", True):
|
|
184
|
+
parent_spinbox = getattr(self.parent, "spinBox", None)
|
|
185
|
+
source_spinbox = getattr(source, "spinBox", None)
|
|
186
|
+
if parent_spinbox is not None and source_spinbox is not None:
|
|
187
|
+
def sync_interval(interval):
|
|
188
|
+
interval = refresh_interval_value(parent_spinbox)
|
|
189
|
+
set_refresh_interval(source_spinbox, interval)
|
|
190
|
+
source.monitorIntervalChanged(interval)
|
|
191
|
+
|
|
192
|
+
sync_interval(refresh_interval_value(parent_spinbox))
|
|
193
|
+
interval_signal = getattr(parent_spinbox, "valueChanged", None)
|
|
194
|
+
if interval_signal is not None:
|
|
195
|
+
interval_slot = sync_interval
|
|
196
|
+
interval_signal.connect(interval_slot)
|
|
197
|
+
self._source_interval_signal = interval_signal
|
|
198
|
+
self._source_interval_slot = interval_slot
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
getattr(source, "_closed", False)
|
|
202
|
+
and plot_refresh_required(source)
|
|
203
|
+
and not source.monitor.isActive()
|
|
204
|
+
):
|
|
205
|
+
source.monitorIntervalChanged(refresh_interval_value(source.spinBox))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _release_source_consumer(self):
|
|
209
|
+
if not self._source_consumer_registered:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
source = self.from_win
|
|
213
|
+
remaining = max(int(getattr(source, "_merged_trace_users", 1)) - 1, 0)
|
|
214
|
+
source._merged_trace_users = remaining
|
|
215
|
+
self._source_consumer_registered = False
|
|
216
|
+
|
|
217
|
+
if self._source_interval_signal is not None:
|
|
218
|
+
try:
|
|
219
|
+
self._source_interval_signal.disconnect(self._source_interval_slot)
|
|
220
|
+
except (TypeError, RuntimeError): # Signal was absent or already gone.
|
|
221
|
+
pass
|
|
222
|
+
self._source_interval_signal = None
|
|
223
|
+
self._source_interval_slot = None
|
|
224
|
+
|
|
225
|
+
if remaining == 0 and not getattr(source, "visible", True):
|
|
226
|
+
monitor = getattr(source, "monitor", None)
|
|
227
|
+
if monitor is not None:
|
|
228
|
+
monitor.stop()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def disconnect_source_updates(self):
|
|
232
|
+
"""Release source-update connections when a trace is removed."""
|
|
233
|
+
|
|
234
|
+
if self._source_update_signal is not None:
|
|
235
|
+
try:
|
|
236
|
+
self._source_update_signal.disconnect(self._source_trace_updated)
|
|
237
|
+
except (TypeError, RuntimeError): # Signal was absent or already gone.
|
|
238
|
+
pass
|
|
239
|
+
self._source_update_signal = None
|
|
240
|
+
if self._source_compatibility_signal is not None:
|
|
241
|
+
try:
|
|
242
|
+
self._source_compatibility_signal.disconnect(
|
|
243
|
+
self._source_compatibility_changed
|
|
244
|
+
)
|
|
245
|
+
except (TypeError, RuntimeError): # Signal was absent or already gone.
|
|
246
|
+
pass
|
|
247
|
+
self._source_compatibility_signal = None
|
|
248
|
+
self._disconnect_pending_update()
|
|
249
|
+
self._release_source_consumer()
|
|
250
|
+
|
|
251
|
+
@QtCore.pyqtSlot()
|
|
252
|
+
def call_update(self):
|
|
253
|
+
"""
|
|
254
|
+
Event handler for self.refresh/from_win.worker finish
|
|
255
|
+
Updates the subplot line data.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
data = {}
|
|
259
|
+
|
|
260
|
+
choose_from = self.choose_from
|
|
261
|
+
if choose_from is None:
|
|
262
|
+
self.setData(x=[], y=[])
|
|
263
|
+
self._disconnect_pending_update()
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Assign data to correct axis
|
|
267
|
+
for itr, axis in enumerate(["x", "y"]):
|
|
268
|
+
data[axis] = self.from_win.axis_data[choose_from[itr]]
|
|
269
|
+
|
|
270
|
+
# Updates display
|
|
271
|
+
self.setData(
|
|
272
|
+
x=data["x"],
|
|
273
|
+
y=data["y"],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Keep metadata with the samples we actually copied. Axis selections
|
|
277
|
+
# and operation controls can change before replacement data is ready.
|
|
278
|
+
source_params = getattr(self.from_win, "axis_param", {})
|
|
279
|
+
self.axis_param = {
|
|
280
|
+
axis: source_params[source_axis]
|
|
281
|
+
for axis, source_axis in zip(("x", "y"), choose_from, strict=True)
|
|
282
|
+
if source_axis in source_params
|
|
283
|
+
}
|
|
284
|
+
sync_labels = getattr(self.parent, "_sync_trace_axis_labels", None)
|
|
285
|
+
if callable(sync_labels):
|
|
286
|
+
sync_labels()
|
|
287
|
+
|
|
288
|
+
self._disconnect_pending_update()
|
|
289
|
+
|
|
290
|
+
@QtCore.pyqtSlot(QColor)
|
|
291
|
+
def set_color(self, col):
|
|
292
|
+
"""
|
|
293
|
+
Event handler connect to qplot.windows._widgets.dropbox.picker_1d.color_box
|
|
294
|
+
Updates the display color of line based on color_box selection
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
col : PyQt6.QtGui.QColor
|
|
299
|
+
The color to change line to.
|
|
300
|
+
|
|
301
|
+
"""
|
|
302
|
+
self.setPen(col)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@QtCore.pyqtSlot(str)
|
|
306
|
+
def set_side(self, side):
|
|
307
|
+
"""
|
|
308
|
+
Event handler connect to qplot.windows._widgets.dropbox.picker_1d.axis_side
|
|
309
|
+
Changes the axis the line is attached to on axis_side selection
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
side : str
|
|
314
|
+
'left' or 'right', connects plot display to the corresponding y axis.
|
|
315
|
+
|
|
316
|
+
"""
|
|
317
|
+
side = side.lower()
|
|
318
|
+
parent = self.parent
|
|
319
|
+
|
|
320
|
+
# Change cancelled
|
|
321
|
+
if self.side == side:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Use the same registration-preserving path as Trace Appearance.
|
|
325
|
+
parent._set_trace_y_axis(parent._window_trace_key(self.from_win), side)
|
|
326
|
+
|
|
327
|
+
parent.vb.enableAutoRange()
|
|
328
|
+
self.side = side
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class custom_viewbox(pg.ViewBox):
|
|
332
|
+
"""
|
|
333
|
+
A custom view box used in qplot.windows.plotWin.PlotWidget which builds on
|
|
334
|
+
the default viewbox of the plotItem
|
|
335
|
+
|
|
336
|
+
The additional functions allow the main (left) viewbox to control the right
|
|
337
|
+
viewbox and scale by the same relative amount.
|
|
338
|
+
|
|
339
|
+
Each function emits a signal which tells qplot.windows.plot1d.plot1d.right_vb
|
|
340
|
+
to do the same.
|
|
341
|
+
"""
|
|
342
|
+
main_moved = QtCore.pyqtSignal([object])
|
|
343
|
+
autoRange_triggered = QtCore.pyqtSignal()
|
|
344
|
+
|
|
345
|
+
def __init__(self, *args, **kargs):
|
|
346
|
+
super().__init__(*args, **kargs)
|
|
347
|
+
self._marquee_owner = None
|
|
348
|
+
self._auto_range_handler: Callable[[], None] | None = None
|
|
349
|
+
self._shift_pan_axis_constraint = True
|
|
350
|
+
self._shift_pan_axis = None
|
|
351
|
+
self._main_moved_axis = None
|
|
352
|
+
self.setAcceptHoverEvents(True)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def set_marquee_owner(self, owner):
|
|
356
|
+
self._marquee_owner = owner
|
|
357
|
+
|
|
358
|
+
def set_auto_range_handler(self, handler: Callable[[], None] | None) -> None:
|
|
359
|
+
"""Let a line-plot owner validate full-view requests before mutation."""
|
|
360
|
+
self._auto_range_handler = handler
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def set_shift_pan_axis_constraint(self, enabled):
|
|
364
|
+
"""Enable Shift-drag axis locking for panning in this view box."""
|
|
365
|
+
|
|
366
|
+
self._shift_pan_axis_constraint = bool(enabled)
|
|
367
|
+
self._shift_pan_axis = None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _shift_pan_axis_for_event(self, ev):
|
|
371
|
+
"""Return the axis selected by a Shift-pan drag, if applicable."""
|
|
372
|
+
|
|
373
|
+
if (
|
|
374
|
+
not self._shift_pan_axis_constraint
|
|
375
|
+
or self.state["mouseMode"] != self.PanMode
|
|
376
|
+
or ev.button() not in {
|
|
377
|
+
QtCore.Qt.MouseButton.LeftButton,
|
|
378
|
+
QtCore.Qt.MouseButton.MiddleButton,
|
|
379
|
+
}
|
|
380
|
+
):
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
if not ev.modifiers() & QtCore.Qt.KeyboardModifier.ShiftModifier:
|
|
384
|
+
self._shift_pan_axis = None
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
if ev.isStart():
|
|
388
|
+
self._shift_pan_axis = None
|
|
389
|
+
|
|
390
|
+
if self._shift_pan_axis is None:
|
|
391
|
+
drag = ev.pos() - ev.buttonDownPos(ev.button())
|
|
392
|
+
if drag.x() == 0 and drag.y() == 0:
|
|
393
|
+
return None
|
|
394
|
+
self._shift_pan_axis = 0 if abs(drag.x()) >= abs(drag.y()) else 1
|
|
395
|
+
|
|
396
|
+
return self._shift_pan_axis
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _handle_marquee_mouse_drag(self, ev):
|
|
400
|
+
owner = self._marquee_owner
|
|
401
|
+
if owner is None or ev.button() != QtCore.Qt.MouseButton.LeftButton:
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
if ev.isStart():
|
|
405
|
+
mode = owner.marquee_drag_mode_at(ev.buttonDownScenePos())
|
|
406
|
+
if mode is None and not ev.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier:
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
owner.begin_marquee_drag(
|
|
410
|
+
owner._marquee_viewbox().mapSceneToView(ev.buttonDownScenePos()),
|
|
411
|
+
mode,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if not owner.is_marquee_dragging():
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
self._update_marquee_cursor(ev.scenePos(), ev.modifiers())
|
|
418
|
+
owner.drag_marquee_to(
|
|
419
|
+
owner._marquee_viewbox().mapSceneToView(ev.scenePos()),
|
|
420
|
+
ev.modifiers(),
|
|
421
|
+
)
|
|
422
|
+
if ev.isFinish():
|
|
423
|
+
owner.finish_marquee_drag()
|
|
424
|
+
self._update_marquee_cursor(ev.scenePos(), ev.modifiers())
|
|
425
|
+
|
|
426
|
+
ev.accept()
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _update_marquee_cursor(self, scene_pos, modifiers=QtCore.Qt.KeyboardModifier.NoModifier):
|
|
431
|
+
owner = self._marquee_owner
|
|
432
|
+
if owner is None:
|
|
433
|
+
self.unsetCursor()
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
cursor_shape = owner.marquee_cursor_shape_at(scene_pos, modifiers)
|
|
437
|
+
if cursor_shape is None:
|
|
438
|
+
self.unsetCursor()
|
|
439
|
+
else:
|
|
440
|
+
self.setCursor(cursor_shape)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def hoverMoveEvent(self, ev):
|
|
444
|
+
self._update_marquee_cursor(ev.scenePos(), ev.modifiers())
|
|
445
|
+
super().hoverMoveEvent(ev)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def hoverLeaveEvent(self, ev):
|
|
449
|
+
owner = self._marquee_owner
|
|
450
|
+
if owner is None or not owner.is_marquee_dragging():
|
|
451
|
+
self.unsetCursor()
|
|
452
|
+
super().hoverLeaveEvent(ev)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def mouseDoubleClickEvent(self, ev):
|
|
456
|
+
owner = self._marquee_owner
|
|
457
|
+
if owner is not None and getattr(owner, "marquee", None) is not None:
|
|
458
|
+
owner.clear_marquee()
|
|
459
|
+
self.unsetCursor()
|
|
460
|
+
ev.accept()
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
super().mouseDoubleClickEvent(ev)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def mouseClickEvent(self, ev):
|
|
467
|
+
owner = self._marquee_owner
|
|
468
|
+
if (
|
|
469
|
+
owner is not None
|
|
470
|
+
and ev.button() == QtCore.Qt.MouseButton.RightButton
|
|
471
|
+
and owner.open_marquee_context_menu(
|
|
472
|
+
ev.scenePos(),
|
|
473
|
+
self._mouse_event_global_pos(ev),
|
|
474
|
+
)
|
|
475
|
+
):
|
|
476
|
+
ev.accept()
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
super().mouseClickEvent(ev)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _mouse_event_global_pos(self, ev):
|
|
483
|
+
for attr_name in ("screenPos", "globalPos"):
|
|
484
|
+
attr = getattr(ev, attr_name, None)
|
|
485
|
+
if attr is None:
|
|
486
|
+
continue
|
|
487
|
+
pos = attr() if callable(attr) else attr
|
|
488
|
+
if isinstance(pos, QtCore.QPointF):
|
|
489
|
+
return pos.toPoint()
|
|
490
|
+
if isinstance(pos, QtCore.QPoint):
|
|
491
|
+
return pos
|
|
492
|
+
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def mouseDragEvent(self, ev, axis=None):
|
|
497
|
+
if axis is None and self._handle_marquee_mouse_drag(ev):
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
constrained_axis = axis
|
|
501
|
+
if axis is None:
|
|
502
|
+
constrained_axis = self._shift_pan_axis_for_event(ev)
|
|
503
|
+
|
|
504
|
+
super().mouseDragEvent(ev, axis=constrained_axis)
|
|
505
|
+
|
|
506
|
+
if axis is None:
|
|
507
|
+
self._main_moved_axis = constrained_axis
|
|
508
|
+
try:
|
|
509
|
+
self.main_moved.emit(ev)
|
|
510
|
+
finally:
|
|
511
|
+
self._main_moved_axis = None
|
|
512
|
+
|
|
513
|
+
if ev.isFinish():
|
|
514
|
+
self._shift_pan_axis = None
|
|
515
|
+
|
|
516
|
+
def wheelEvent(self, ev, axis=None):
|
|
517
|
+
super().wheelEvent(ev, axis=axis)
|
|
518
|
+
|
|
519
|
+
if axis is None:
|
|
520
|
+
self.main_moved.emit(ev)
|
|
521
|
+
|
|
522
|
+
def autoRange(self, padding=None, items=None, item=None):
|
|
523
|
+
if (
|
|
524
|
+
self._auto_range_handler is not None
|
|
525
|
+
and padding is None and items is None and item is None
|
|
526
|
+
):
|
|
527
|
+
self._auto_range_handler()
|
|
528
|
+
self.autoRange_triggered.emit()
|
|
529
|
+
return
|
|
530
|
+
super().autoRange(padding=padding, items=items, item=item)
|
|
531
|
+
|
|
532
|
+
self.autoRange_triggered.emit()
|