qplotter 1.6.0b3__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 +63 -0
- qplot/_metadata.py +12 -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 +573 -0
- qplot/configuration/config_schema.json +319 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +579 -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 +2330 -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 +2561 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +127 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +868 -0
- qplot/datahandling/trusted_heatmap.py +241 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1894 -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 +1733 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +114 -0
- qplot/tools/general.py +134 -0
- qplot/tools/heatmap_geometry.py +381 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +625 -0
- qplot/tools/sample_statistics.py +56 -0
- qplot/tools/worker.py +2787 -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 +4904 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_dragdrop.py +71 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +169 -0
- qplot/windows/_native_averaging.py +173 -0
- qplot/windows/_native_transforms.py +389 -0
- qplot/windows/_plot1d_snap.py +608 -0
- qplot/windows/_plot1d_traces.py +2183 -0
- qplot/windows/_plot2d_colorbar.py +1126 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2197 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +1993 -0
- qplot/windows/_plot_actions.py +2695 -0
- qplot/windows/_plot_appearance.py +276 -0
- qplot/windows/_plot_axis_scaling.py +1485 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +110 -0
- qplot/windows/_plot_marquee.py +656 -0
- qplot/windows/_plot_refresh.py +942 -0
- qplot/windows/_plot_state.py +239 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +635 -0
- qplot/windows/_refresh_interval.py +70 -0
- qplot/windows/_run_controls.py +828 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +531 -0
- qplot/windows/_subplots/subplot2d.py +918 -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 +413 -0
- qplot/windows/main.py +1635 -0
- qplot/windows/plot1d.py +311 -0
- qplot/windows/plot2d.py +1901 -0
- qplotter-1.6.0b3.dist-info/METADATA +318 -0
- qplotter-1.6.0b3.dist-info/RECORD +119 -0
- qplotter-1.6.0b3.dist-info/WHEEL +5 -0
- qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
from itertools import count
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
from PyQt6 import QtCore, QtGui
|
|
7
|
+
from PyQt6 import QtWidgets as qtw
|
|
8
|
+
|
|
9
|
+
from ._subplots.subplot2d import sweeper
|
|
10
|
+
|
|
11
|
+
_SweepAxis = Literal["x", "y"]
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from qplot.tools.heatmap_geometry import HeatmapGeometry
|
|
15
|
+
|
|
16
|
+
class _Plot2DSweepBase(qtw.QMainWindow):
|
|
17
|
+
_dataset_key: Any
|
|
18
|
+
active_sweep_line_id: int | None
|
|
19
|
+
axis_options: dict[str, str]
|
|
20
|
+
close_sweeps_requested: Any
|
|
21
|
+
dataGrid: npt.NDArray[Any]
|
|
22
|
+
open_subplot: Any
|
|
23
|
+
param: Any
|
|
24
|
+
plot: Any
|
|
25
|
+
rotate: bool | None
|
|
26
|
+
sweep_lines: dict[int, Any]
|
|
27
|
+
sweep_moved: Any
|
|
28
|
+
|
|
29
|
+
def change_axis(self, key: str) -> None: ...
|
|
30
|
+
|
|
31
|
+
def clear_marquee(self) -> None: ...
|
|
32
|
+
|
|
33
|
+
def hide_hover_pixel_outline(self) -> None: ...
|
|
34
|
+
|
|
35
|
+
def _heatmap_geometry(self) -> HeatmapGeometry | None: ...
|
|
36
|
+
|
|
37
|
+
def _required_heatmap_geometry(self) -> HeatmapGeometry: ...
|
|
38
|
+
|
|
39
|
+
def _reset_heatmap_hover(self) -> None: ...
|
|
40
|
+
else:
|
|
41
|
+
class _Plot2DSweepBase:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_SWEEP_ID_COUNTER = count()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Plot2DSweepMixin(_Plot2DSweepBase):
|
|
49
|
+
"""Sweep and cut interactions for 2D heatmap plot windows."""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _reserve_sweep_id() -> int:
|
|
53
|
+
"""Return an application-session-unique identity for a heatmap cut."""
|
|
54
|
+
|
|
55
|
+
return next(_SWEEP_ID_COUNTER)
|
|
56
|
+
|
|
57
|
+
def openSweep(self, side: str) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Emits a signal to the Main window to open the sweep via
|
|
60
|
+
MainWindow.openWin()
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
side : str
|
|
65
|
+
"h": horizontal, or "v": vertical. Along which axes the sweep will
|
|
66
|
+
be performed.
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
KeyError
|
|
71
|
+
Invalid side parameter.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
# Quit out if not on heatmap
|
|
75
|
+
z_index = self.__dict__.get("z_index")
|
|
76
|
+
if not isinstance(z_index, list):
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Fetch axes names
|
|
80
|
+
axes = self.axis_options
|
|
81
|
+
|
|
82
|
+
# Get fixed and sweep parameter
|
|
83
|
+
if side == "v":
|
|
84
|
+
fixed_var = axes["x"]
|
|
85
|
+
sweep_var = axes["y"]
|
|
86
|
+
fixed_index = z_index[0]
|
|
87
|
+
fixed_axis: _SweepAxis = "x"
|
|
88
|
+
elif side == "h":
|
|
89
|
+
fixed_var = axes["y"]
|
|
90
|
+
sweep_var = axes["x"]
|
|
91
|
+
fixed_index = z_index[1]
|
|
92
|
+
fixed_axis = "y"
|
|
93
|
+
else:
|
|
94
|
+
raise KeyError(f"Invalid sweep side, {side=}, must be 'v' or 'h'.")
|
|
95
|
+
|
|
96
|
+
fixed_value = float(fixed_index)
|
|
97
|
+
geometry_getter = getattr(self, "_heatmap_geometry", None)
|
|
98
|
+
if callable(geometry_getter) and geometry_getter() is not None:
|
|
99
|
+
fixed_value = self.sweep_pixel_centre(fixed_axis, fixed_index)
|
|
100
|
+
|
|
101
|
+
sweep_id = self._reserve_sweep_id()
|
|
102
|
+
|
|
103
|
+
# Emit to Main window to open new window
|
|
104
|
+
self.open_subplot.emit(
|
|
105
|
+
sweeper,
|
|
106
|
+
self._dataset_key,
|
|
107
|
+
(
|
|
108
|
+
sweep_id,
|
|
109
|
+
sweep_var,
|
|
110
|
+
fixed_var,
|
|
111
|
+
fixed_value,
|
|
112
|
+
self.param
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@QtCore.pyqtSlot(int, str, str, float, object)
|
|
118
|
+
def update_sweep_line(
|
|
119
|
+
self,
|
|
120
|
+
sweep_id: int,
|
|
121
|
+
sweep_param: str,
|
|
122
|
+
fixed_param: str,
|
|
123
|
+
fixed_value: float,
|
|
124
|
+
line_col: Any,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Event handler for update to suplot sweep
|
|
128
|
+
Updates the sweep cursor on the main plot in response to changes in the
|
|
129
|
+
subplot
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
sweep_id : int
|
|
134
|
+
The track of subplots to know which subplot cursor to edit.
|
|
135
|
+
sweep_param : str
|
|
136
|
+
The parameter over which the sweep subplot looks. Used to confirm
|
|
137
|
+
that a cursor can be plotted
|
|
138
|
+
fixed_param : str
|
|
139
|
+
The static parameter and the parameter to place the line on.
|
|
140
|
+
fixed_value : float
|
|
141
|
+
Physical fixed-axis coordinate at which to place the line.
|
|
142
|
+
line_col : QPen
|
|
143
|
+
The plen color of the line.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
if self._heatmap_geometry() is None:
|
|
148
|
+
return
|
|
149
|
+
# Check if display is possible on current axes
|
|
150
|
+
if sweep_param not in self.axis_options.values() or fixed_param not in self.axis_options.values():
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# get axis of fixed_param
|
|
154
|
+
index = list(self.axis_options.values()).index(fixed_param)
|
|
155
|
+
axis_name = list(self.axis_options.keys())[index]
|
|
156
|
+
if axis_name not in ("x", "y"):
|
|
157
|
+
return
|
|
158
|
+
axis: _SweepAxis = "x" if axis_name == "x" else "y"
|
|
159
|
+
|
|
160
|
+
fixed_index = self.sweep_index_at_value(axis, fixed_value, clamp=False)
|
|
161
|
+
if fixed_index is None:
|
|
162
|
+
line = self.sweep_lines.get(sweep_id)
|
|
163
|
+
if line is not None:
|
|
164
|
+
line.sweep_index = None
|
|
165
|
+
set_visible = getattr(line, "setVisible", None)
|
|
166
|
+
if callable(set_visible):
|
|
167
|
+
set_visible(False)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
at_value = self.sweep_pixel_centre(axis, fixed_index)
|
|
171
|
+
|
|
172
|
+
if self.sweep_lines.get(sweep_id, None) is not None:
|
|
173
|
+
line = self.sweep_lines[sweep_id]
|
|
174
|
+
set_visible = getattr(line, "setVisible", None)
|
|
175
|
+
if callable(set_visible):
|
|
176
|
+
set_visible(True)
|
|
177
|
+
|
|
178
|
+
# Update line data
|
|
179
|
+
line.angle = (90 if axis == "x" else 0)
|
|
180
|
+
line.pen = line_col
|
|
181
|
+
line.hoverPen = line_col
|
|
182
|
+
line.currentPen = line_col
|
|
183
|
+
|
|
184
|
+
# refresh
|
|
185
|
+
line.resetTransform()
|
|
186
|
+
line.setRotation(line.angle)
|
|
187
|
+
line.setPos(at_value)
|
|
188
|
+
self.set_sweep_line_cursor(line)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Set up new line
|
|
192
|
+
else:
|
|
193
|
+
# Produce line
|
|
194
|
+
if axis == "x":
|
|
195
|
+
line = self.plot.addLine(
|
|
196
|
+
x=at_value,
|
|
197
|
+
pen=line_col,
|
|
198
|
+
movable=True
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
line = self.plot.addLine(
|
|
202
|
+
y=at_value,
|
|
203
|
+
pen=line_col,
|
|
204
|
+
movable=True
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
line.setZValue(1) # Move to top
|
|
209
|
+
line.sigDragged.connect(self.moving_sweep)
|
|
210
|
+
line.sigClicked.connect(self.activate_sweep_line)
|
|
211
|
+
self.sweep_lines[sweep_id] = line # Track for update/delete
|
|
212
|
+
line.sweep_id = sweep_id # give copy of id if needed
|
|
213
|
+
primary_viewbox = getattr(self, "_primary_heatmap_viewbox", None)
|
|
214
|
+
move_item = getattr(self, "_move_heatmap_item_to_viewbox", None)
|
|
215
|
+
if callable(primary_viewbox) and callable(move_item):
|
|
216
|
+
move_item(line, primary_viewbox())
|
|
217
|
+
self.set_sweep_line_cursor(line)
|
|
218
|
+
|
|
219
|
+
line.sweep_fixed_param = fixed_param
|
|
220
|
+
self.set_sweep_line_index(line, fixed_index, emit=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@QtCore.pyqtSlot(int)
|
|
224
|
+
def remove_sweep(self, sweep_id: int) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Event handler for subplot closing.
|
|
227
|
+
Removes line sweep display from plot
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
sweep_id : int
|
|
232
|
+
Number Id of Sweep.
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
#check exists, then remove
|
|
236
|
+
if self.sweep_lines.get(sweep_id, None) is None:
|
|
237
|
+
return
|
|
238
|
+
self.restore_sweep_line_hover_cursor(self.sweep_lines[sweep_id])
|
|
239
|
+
self.restore_sweep_line_drag_cursor(self.sweep_lines[sweep_id])
|
|
240
|
+
remove_item = getattr(self, "_remove_heatmap_render_item", None)
|
|
241
|
+
if callable(remove_item):
|
|
242
|
+
remove_item(self.sweep_lines[sweep_id])
|
|
243
|
+
else:
|
|
244
|
+
self.plot.removeItem(self.sweep_lines[sweep_id])
|
|
245
|
+
self.sweep_lines.pop(sweep_id)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@QtCore.pyqtSlot()
|
|
249
|
+
def change_axis(self, key: str) -> None:
|
|
250
|
+
self._reset_heatmap_hover()
|
|
251
|
+
if self.__dict__.get("marquee") is not None:
|
|
252
|
+
self.clear_marquee()
|
|
253
|
+
|
|
254
|
+
# Rotate lines in case of duplciates
|
|
255
|
+
options = self.axis_options
|
|
256
|
+
if options["x"] == options["y"]:
|
|
257
|
+
self.rotate = True
|
|
258
|
+
else: # Otherwise delete them
|
|
259
|
+
self.rotate = False
|
|
260
|
+
|
|
261
|
+
super().change_axis(key)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@QtCore.pyqtSlot()
|
|
265
|
+
def rotate_sweeps(self) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Event handler for changing assigned axes (is connected to self.end_wait
|
|
268
|
+
in self.refreshPlot)
|
|
269
|
+
|
|
270
|
+
Rotates sweep cursors if the axis is flipped. Otherwise removes them
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
None.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
if self.rotate is None: # Not from changing axis parameters
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# remote lines as parameters have changed
|
|
281
|
+
if not self.rotate:
|
|
282
|
+
for key in list(self.sweep_lines.keys()):
|
|
283
|
+
self.remove_sweep(key)
|
|
284
|
+
self.__dict__["rotate"] = None
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
# A queued refresh may publish axes that a later UI request superseded.
|
|
288
|
+
# Align markers with that publication, and keep the final rotation pending.
|
|
289
|
+
selected_axes = self.axis_options
|
|
290
|
+
axis_param = self.__dict__.get("axis_param", {})
|
|
291
|
+
published_axes = {
|
|
292
|
+
axis: getattr(axis_param.get(axis), "name", None)
|
|
293
|
+
for axis in ("x", "y")
|
|
294
|
+
}
|
|
295
|
+
axes = published_axes if all(published_axes.values()) else selected_axes
|
|
296
|
+
|
|
297
|
+
# Rotate lines as parameters switched
|
|
298
|
+
for key, line in self.sweep_lines.items():
|
|
299
|
+
line = self.sweep_lines[key]
|
|
300
|
+
# Rotate
|
|
301
|
+
pos = line.value()
|
|
302
|
+
# Several swaps can share one refresh. Resolve the cut's fixed
|
|
303
|
+
# parameter on the final axes rather than counting rotations.
|
|
304
|
+
fixed_param = getattr(line, "sweep_fixed_param", None)
|
|
305
|
+
if fixed_param in axes.values():
|
|
306
|
+
line.angle = 90 if fixed_param == axes["x"] else 0
|
|
307
|
+
else:
|
|
308
|
+
line.angle = 90 if line.angle == 0 else 0
|
|
309
|
+
|
|
310
|
+
line.resetTransform()
|
|
311
|
+
line.setRotation(line.angle)
|
|
312
|
+
line.setPos(pos) # force line placement into correct spot
|
|
313
|
+
self.set_sweep_line_cursor(line)
|
|
314
|
+
|
|
315
|
+
self.__dict__["rotate"] = None if axes == selected_axes else True
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def sweep_axis_count(self, axis: _SweepAxis) -> int:
|
|
319
|
+
geometry = self._heatmap_geometry()
|
|
320
|
+
if geometry is None:
|
|
321
|
+
return 0
|
|
322
|
+
return geometry.x.count if axis == "x" else geometry.y.count
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def sweep_pixel_centre(self, axis: _SweepAxis, index: int) -> float:
|
|
326
|
+
"""
|
|
327
|
+
Return the plot coordinate at the centre of a heatmap pixel.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
geometry = self._required_heatmap_geometry()
|
|
331
|
+
axis_geometry = geometry.x if axis == "x" else geometry.y
|
|
332
|
+
index = min(max(int(index), 0), axis_geometry.count - 1)
|
|
333
|
+
return axis_geometry.centre(index)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def sweep_index_at_value(
|
|
337
|
+
self,
|
|
338
|
+
axis: _SweepAxis,
|
|
339
|
+
value: float,
|
|
340
|
+
*,
|
|
341
|
+
clamp: bool = True,
|
|
342
|
+
) -> int | None:
|
|
343
|
+
"""
|
|
344
|
+
Return the heatmap pixel index containing a plot coordinate.
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
geometry = self._heatmap_geometry()
|
|
348
|
+
if geometry is None:
|
|
349
|
+
return None
|
|
350
|
+
axis_geometry = geometry.x if axis == "x" else geometry.y
|
|
351
|
+
return axis_geometry.index_at(value, clamp=clamp)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def line_sweep_axis(self, line: Any) -> _SweepAxis:
|
|
355
|
+
return "x" if line.angle == 90 else "y"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def sweep_line_cursor_shape(self, line: Any) -> QtCore.Qt.CursorShape:
|
|
359
|
+
if self.line_sweep_axis(line) == "x":
|
|
360
|
+
return QtCore.Qt.CursorShape.SizeHorCursor
|
|
361
|
+
return QtCore.Qt.CursorShape.SizeVerCursor
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def set_sweep_line_cursor(self, line: Any) -> None:
|
|
365
|
+
if not getattr(line, "movable", False):
|
|
366
|
+
self.restore_sweep_line_hover_cursor(line)
|
|
367
|
+
self.restore_sweep_line_drag_cursor(line)
|
|
368
|
+
line.unsetCursor()
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
line.setCursor(self.sweep_line_cursor_shape(line))
|
|
372
|
+
self.install_sweep_line_hover_cursor(line)
|
|
373
|
+
self.install_sweep_line_drag_cursor(line)
|
|
374
|
+
self.update_sweep_line_hover_cursor(line)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def install_sweep_line_drag_cursor(self, line: Any) -> None:
|
|
378
|
+
if getattr(line, "_qplot_sweep_drag_cursor_installed", False):
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
previous_mouse_drag_event = getattr(line, "mouseDragEvent", None)
|
|
382
|
+
if previous_mouse_drag_event is None:
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
def mouse_drag_event(event: Any) -> Any:
|
|
386
|
+
if self._sweep_line_drag_cursor_event_applies(line, event):
|
|
387
|
+
self.set_sweep_line_drag_cursor(line)
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
return previous_mouse_drag_event(event)
|
|
391
|
+
finally:
|
|
392
|
+
if event.isFinish():
|
|
393
|
+
self.restore_sweep_line_drag_cursor(line)
|
|
394
|
+
|
|
395
|
+
line.mouseDragEvent = mouse_drag_event
|
|
396
|
+
line._qplot_sweep_drag_cursor_installed = True
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def install_sweep_line_hover_cursor(self, line: Any) -> None:
|
|
400
|
+
if getattr(line, "_qplot_sweep_hover_cursor_installed", False):
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
previous_hover_event = getattr(line, "hoverEvent", None)
|
|
404
|
+
if previous_hover_event is None:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
def hover_event(event: Any) -> None:
|
|
408
|
+
previous_hover_event(event)
|
|
409
|
+
if getattr(line, "mouseHovering", False):
|
|
410
|
+
self.set_sweep_line_hover_cursor(line)
|
|
411
|
+
else:
|
|
412
|
+
self.restore_sweep_line_hover_cursor(line)
|
|
413
|
+
|
|
414
|
+
line.hoverEvent = hover_event
|
|
415
|
+
line._qplot_sweep_hover_cursor_installed = True
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _sweep_line_drag_cursor_event_applies(self, line: Any, event: Any) -> bool:
|
|
419
|
+
if not getattr(line, "movable", False):
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
button = getattr(event, "button", lambda: None)()
|
|
423
|
+
return button == QtCore.Qt.MouseButton.LeftButton
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def set_sweep_line_drag_cursor(self, line: Any) -> None:
|
|
427
|
+
self.set_sweep_line_override_cursor(line, "drag")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def restore_sweep_line_drag_cursor(self, line: Any) -> None:
|
|
431
|
+
self.restore_sweep_line_override_cursor(line, "drag")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def set_sweep_line_hover_cursor(self, line: Any) -> None:
|
|
435
|
+
self.set_sweep_line_override_cursor(line, "hover")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def restore_sweep_line_hover_cursor(self, line: Any) -> None:
|
|
439
|
+
self.restore_sweep_line_override_cursor(line, "hover")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def set_sweep_line_override_cursor(self, line: Any, reason: str) -> None:
|
|
443
|
+
if qtw.QApplication.instance() is None:
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
active_attribute = f"_qplot_sweep_{reason}_cursor_override_active"
|
|
447
|
+
shape_attribute = f"_qplot_sweep_{reason}_cursor_shape"
|
|
448
|
+
cursor_shape = self.sweep_line_cursor_shape(line)
|
|
449
|
+
cursor = QtGui.QCursor(cursor_shape)
|
|
450
|
+
if getattr(line, active_attribute, False):
|
|
451
|
+
if getattr(line, shape_attribute, None) != cursor_shape:
|
|
452
|
+
qtw.QApplication.changeOverrideCursor(cursor)
|
|
453
|
+
setattr(line, shape_attribute, cursor_shape)
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
qtw.QApplication.setOverrideCursor(cursor)
|
|
457
|
+
setattr(line, active_attribute, True)
|
|
458
|
+
setattr(line, shape_attribute, cursor_shape)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def restore_sweep_line_override_cursor(self, line: Any, reason: str) -> None:
|
|
462
|
+
active_attribute = f"_qplot_sweep_{reason}_cursor_override_active"
|
|
463
|
+
if not getattr(line, active_attribute, False):
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
if qtw.QApplication.instance() is not None:
|
|
467
|
+
qtw.QApplication.restoreOverrideCursor()
|
|
468
|
+
setattr(line, active_attribute, False)
|
|
469
|
+
setattr(line, f"_qplot_sweep_{reason}_cursor_shape", None)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def update_sweep_line_hover_cursor(self, line: Any) -> None:
|
|
473
|
+
if self.sweep_line_contains_global_cursor(line):
|
|
474
|
+
self.set_sweep_line_hover_cursor(line)
|
|
475
|
+
else:
|
|
476
|
+
self.restore_sweep_line_hover_cursor(line)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def sweep_line_contains_global_cursor(self, line: Any) -> bool:
|
|
480
|
+
widget = self.__dict__.get("widget")
|
|
481
|
+
if widget is None:
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
cursor_pos = QtGui.QCursor.pos()
|
|
486
|
+
view_pos = widget.mapFromGlobal(cursor_pos)
|
|
487
|
+
scene_pos = widget.mapToScene(view_pos)
|
|
488
|
+
return line.contains(line.mapFromScene(scene_pos))
|
|
489
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def activate_sweep_line(self, line: Any, event: Any = None) -> None:
|
|
494
|
+
self.active_sweep_line_id = line.sweep_id
|
|
495
|
+
if self.sweep_line_remove_requested(event):
|
|
496
|
+
self.request_sweep_line_removal(line, event)
|
|
497
|
+
if event is not None:
|
|
498
|
+
event.accept()
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
self.set_sweep_line_hover_cursor(line)
|
|
502
|
+
if event is not None:
|
|
503
|
+
event.accept()
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def sweep_line_remove_requested(self, event: Any) -> bool:
|
|
507
|
+
if event is None:
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
button = getattr(event, "button", lambda: None)()
|
|
511
|
+
double_clicked = getattr(event, "double", lambda: False)()
|
|
512
|
+
return button == QtCore.Qt.MouseButton.LeftButton and double_clicked
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def request_sweep_line_removal(self, line: Any, event: Any = None) -> None:
|
|
516
|
+
if self.sweep_line_remove_all_requested(event):
|
|
517
|
+
sweep_ids = tuple(sorted(self.sweep_lines.keys()))
|
|
518
|
+
else:
|
|
519
|
+
sweep_ids = (line.sweep_id,)
|
|
520
|
+
|
|
521
|
+
self.close_sweeps_requested.emit(self, sweep_ids)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def sweep_line_remove_all_requested(self, event: Any) -> bool:
|
|
525
|
+
if event is None:
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
modifiers = getattr(event, "modifiers", lambda: QtCore.Qt.KeyboardModifier.NoModifier)()
|
|
529
|
+
return bool(modifiers & QtCore.Qt.KeyboardModifier.ShiftModifier)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def set_sweep_line_index(self, line: Any, index: int, emit: bool = True) -> None:
|
|
533
|
+
axis = self.line_sweep_axis(line)
|
|
534
|
+
count = self.sweep_axis_count(axis)
|
|
535
|
+
if count <= 0:
|
|
536
|
+
return
|
|
537
|
+
index = min(max(int(index), 0), count - 1)
|
|
538
|
+
|
|
539
|
+
line.setBounds((
|
|
540
|
+
self.sweep_pixel_centre(axis, 0),
|
|
541
|
+
self.sweep_pixel_centre(axis, count - 1)
|
|
542
|
+
))
|
|
543
|
+
line.setPos(self.sweep_pixel_centre(axis, index))
|
|
544
|
+
line.sweep_index = index
|
|
545
|
+
self.active_sweep_line_id = line.sweep_id
|
|
546
|
+
|
|
547
|
+
if emit:
|
|
548
|
+
self.sweep_moved.emit(line.sweep_id, float(line.value()))
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _snap_sweep_lines_to_pixel_centres(self) -> None:
|
|
552
|
+
if self._heatmap_geometry() is None:
|
|
553
|
+
return
|
|
554
|
+
for line in self.__dict__.get("sweep_lines", {}).values():
|
|
555
|
+
axis = self.line_sweep_axis(line)
|
|
556
|
+
previous_value = float(line.value())
|
|
557
|
+
index = self.sweep_index_at_value(
|
|
558
|
+
axis,
|
|
559
|
+
previous_value,
|
|
560
|
+
clamp=False,
|
|
561
|
+
)
|
|
562
|
+
set_visible = getattr(line, "setVisible", None)
|
|
563
|
+
if index is None:
|
|
564
|
+
line.sweep_index = None
|
|
565
|
+
if callable(set_visible):
|
|
566
|
+
set_visible(False)
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
if callable(set_visible):
|
|
570
|
+
set_visible(True)
|
|
571
|
+
snapped_value = self.sweep_pixel_centre(axis, index)
|
|
572
|
+
self.set_sweep_line_index(
|
|
573
|
+
line,
|
|
574
|
+
index,
|
|
575
|
+
emit=not np.isclose(snapped_value, previous_value),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def move_sweep_with_arrow_key(self, key: QtCore.Qt.Key) -> None:
|
|
580
|
+
if self._heatmap_geometry() is None:
|
|
581
|
+
return
|
|
582
|
+
moves: dict[QtCore.Qt.Key, tuple[_SweepAxis, int]] = {
|
|
583
|
+
QtCore.Qt.Key.Key_Left: ("x", -1),
|
|
584
|
+
QtCore.Qt.Key.Key_Right: ("x", 1),
|
|
585
|
+
QtCore.Qt.Key.Key_Down: ("y", -1),
|
|
586
|
+
QtCore.Qt.Key.Key_Up: ("y", 1),
|
|
587
|
+
}
|
|
588
|
+
if key not in moves:
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
axis, step = moves[key]
|
|
592
|
+
line = self.sweep_line_for_keyboard_move(axis)
|
|
593
|
+
if line is None:
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
index = getattr(line, "sweep_index", None)
|
|
597
|
+
if index is None:
|
|
598
|
+
index = self.sweep_index_at_value(axis, line.value())
|
|
599
|
+
if index is None:
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
self.set_sweep_line_index(line, index + step)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def sweep_line_index(self, line: Any) -> int | None:
|
|
606
|
+
index = getattr(line, "sweep_index", None)
|
|
607
|
+
if index is not None:
|
|
608
|
+
return index
|
|
609
|
+
|
|
610
|
+
axis = self.line_sweep_axis(line)
|
|
611
|
+
return self.sweep_index_at_value(axis, line.value())
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def sweep_line_for_keyboard_move(self, axis: _SweepAxis) -> Any | None:
|
|
615
|
+
matching_lines = [
|
|
616
|
+
line for line in self.sweep_lines.values()
|
|
617
|
+
if self.line_sweep_axis(line) == axis
|
|
618
|
+
]
|
|
619
|
+
if not matching_lines:
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
active_line = (
|
|
623
|
+
self.sweep_lines.get(self.active_sweep_line_id)
|
|
624
|
+
if self.active_sweep_line_id is not None
|
|
625
|
+
else None
|
|
626
|
+
)
|
|
627
|
+
if active_line in matching_lines:
|
|
628
|
+
return active_line
|
|
629
|
+
|
|
630
|
+
return max(matching_lines, key=lambda line: line.sweep_id)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def sweep_group_drag_requested(self) -> bool:
|
|
634
|
+
if qtw.QApplication.instance() is None:
|
|
635
|
+
return False
|
|
636
|
+
|
|
637
|
+
return bool(qtw.QApplication.keyboardModifiers() & QtCore.Qt.KeyboardModifier.ShiftModifier)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def move_sweep_group(self, dragged_line: Any, dragged_index: int | None) -> None:
|
|
641
|
+
"""
|
|
642
|
+
Move all same-orientation sweep lines by the dragged line's index delta.
|
|
643
|
+
|
|
644
|
+
"""
|
|
645
|
+
axis = self.line_sweep_axis(dragged_line)
|
|
646
|
+
previous_index = self.sweep_line_index(dragged_line)
|
|
647
|
+
if previous_index is None or dragged_index is None:
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
requested_delta = dragged_index - previous_index
|
|
651
|
+
try:
|
|
652
|
+
group_lines = [
|
|
653
|
+
line for line in self.sweep_lines.values()
|
|
654
|
+
if self.line_sweep_axis(line) == axis
|
|
655
|
+
]
|
|
656
|
+
except (AttributeError, RuntimeError):
|
|
657
|
+
# A drag can arrive while a plot is still being initialised or torn
|
|
658
|
+
# down. Treat the active cursor as a one-line group in that case.
|
|
659
|
+
group_lines = []
|
|
660
|
+
if dragged_line not in group_lines:
|
|
661
|
+
group_lines.append(dragged_line)
|
|
662
|
+
|
|
663
|
+
indexed_lines: list[tuple[Any, int]] = []
|
|
664
|
+
for line in group_lines:
|
|
665
|
+
index = self.sweep_line_index(line)
|
|
666
|
+
if index is not None:
|
|
667
|
+
indexed_lines.append((line, index))
|
|
668
|
+
if not indexed_lines:
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
delta = self.bounded_sweep_group_delta(axis, indexed_lines, requested_delta)
|
|
672
|
+
for line, index in indexed_lines:
|
|
673
|
+
self.set_sweep_line_index(line, index + delta)
|
|
674
|
+
|
|
675
|
+
self.active_sweep_line_id = dragged_line.sweep_id
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def bounded_sweep_group_delta(
|
|
679
|
+
self,
|
|
680
|
+
axis: _SweepAxis,
|
|
681
|
+
indexed_lines: list[tuple[Any, int]],
|
|
682
|
+
requested_delta: int,
|
|
683
|
+
) -> int:
|
|
684
|
+
count = self.sweep_axis_count(axis)
|
|
685
|
+
min_delta = max(-index for _line, index in indexed_lines)
|
|
686
|
+
max_delta = min(count - 1 - index for _line, index in indexed_lines)
|
|
687
|
+
return min(max(requested_delta, min_delta), max_delta)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@QtCore.pyqtSlot(object)
|
|
691
|
+
def moving_sweep(self, line: Any) -> None:
|
|
692
|
+
"""
|
|
693
|
+
Event handler for dragging sweep cursor.
|
|
694
|
+
|
|
695
|
+
Uses line possition to find index of fixed parameter and sends to
|
|
696
|
+
signal to subplot window to move sweep scan to new location.
|
|
697
|
+
|
|
698
|
+
Parameters
|
|
699
|
+
----------
|
|
700
|
+
line : pyqtgraph.graphicsItems.InfiniteLine
|
|
701
|
+
The line being dragged.
|
|
702
|
+
|
|
703
|
+
"""
|
|
704
|
+
pos = line.value()
|
|
705
|
+
axis = self.line_sweep_axis(line)
|
|
706
|
+
index = self.sweep_index_at_value(axis, pos)
|
|
707
|
+
|
|
708
|
+
if index is not None:
|
|
709
|
+
if self.sweep_group_drag_requested():
|
|
710
|
+
self.move_sweep_group(line, index)
|
|
711
|
+
else:
|
|
712
|
+
self.set_sweep_line_index(line, index)
|