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.
Files changed (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. 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)