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,608 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
import pyqtgraph as pg
|
|
7
|
+
from PyQt6 import QtCore, QtGui
|
|
8
|
+
from PyQt6 import QtWidgets as qtw
|
|
9
|
+
|
|
10
|
+
from ._commands import command_spec, command_with_status, create_action
|
|
11
|
+
from ._native_transforms import NativePlotDataItem
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
class _Plot1DSnapBase(qtw.QMainWindow):
|
|
15
|
+
toolbarCo_ord: qtw.QToolBar
|
|
16
|
+
snap_to_trace_action: QtGui.QAction | None
|
|
17
|
+
trace_label: qtw.QLabel | None
|
|
18
|
+
lines: dict[Any, Any]
|
|
19
|
+
pos_labels: dict[str, qtw.QLabel]
|
|
20
|
+
plot: Any
|
|
21
|
+
right_vb: Any
|
|
22
|
+
snap_marker: Any
|
|
23
|
+
_snap_marker_view: Any
|
|
24
|
+
ds: Any
|
|
25
|
+
param: Any
|
|
26
|
+
|
|
27
|
+
def initLabels(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
def initMenu(self) -> None: ...
|
|
30
|
+
|
|
31
|
+
def mouseMoved(self, pos: object) -> None: ...
|
|
32
|
+
|
|
33
|
+
def register_shortcut(
|
|
34
|
+
self,
|
|
35
|
+
action: QtGui.QAction,
|
|
36
|
+
shortcut: object,
|
|
37
|
+
status_tip: str | None = None,
|
|
38
|
+
) -> None: ...
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def formatNum(num: float, sf: int = 3) -> str: ...
|
|
42
|
+
|
|
43
|
+
def _set_cursor_index_label(self, text: str) -> None: ...
|
|
44
|
+
def _cursor_1d_x_data(self) -> Any: ...
|
|
45
|
+
else:
|
|
46
|
+
class _Plot1DSnapBase:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
SNAP_TO_TRACE_COMMAND = command_spec("plot.snap_to_trace")
|
|
51
|
+
SNAP_TO_TRACE_SHORTCUT_LABEL = SNAP_TO_TRACE_COMMAND.shortcut_display_text()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class _SnapTraceSample:
|
|
56
|
+
x_value: float
|
|
57
|
+
y_value: float
|
|
58
|
+
point_number: int
|
|
59
|
+
x_view_value: float = float("nan")
|
|
60
|
+
y_view_value: float = float("nan")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class _LineData:
|
|
65
|
+
x_view: npt.ArrayLike
|
|
66
|
+
y_view: npt.ArrayLike
|
|
67
|
+
x_raw: npt.ArrayLike
|
|
68
|
+
y_raw: npt.ArrayLike
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class _NearestTracePoint:
|
|
73
|
+
label: Any
|
|
74
|
+
x_value: float
|
|
75
|
+
y_value: float
|
|
76
|
+
viewbox: Any
|
|
77
|
+
point_number: int
|
|
78
|
+
x_view_value: float
|
|
79
|
+
y_view_value: float
|
|
80
|
+
|
|
81
|
+
def __iter__(self):
|
|
82
|
+
"""Retain the historical five-value private-method unpacking API."""
|
|
83
|
+
|
|
84
|
+
yield self.label
|
|
85
|
+
yield self.x_value
|
|
86
|
+
yield self.y_value
|
|
87
|
+
yield self.viewbox
|
|
88
|
+
yield self.point_number
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _nearest_trace_sample(
|
|
92
|
+
x_data: npt.ArrayLike,
|
|
93
|
+
y_data: npt.ArrayLike,
|
|
94
|
+
cursor_x: float,
|
|
95
|
+
cursor_y: float | None = None,
|
|
96
|
+
) -> _SnapTraceSample | None:
|
|
97
|
+
"""
|
|
98
|
+
Return the finite plotted sample nearest to a cursor X coordinate.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
x_data = np.asarray(x_data, dtype=float)
|
|
102
|
+
y_data = np.asarray(y_data, dtype=float)
|
|
103
|
+
count = min(x_data.size, y_data.size)
|
|
104
|
+
if count == 0:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
x_data = x_data[:count]
|
|
108
|
+
y_data = y_data[:count]
|
|
109
|
+
finite = np.isfinite(x_data) & np.isfinite(y_data)
|
|
110
|
+
if not np.any(finite):
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
finite_indices = np.flatnonzero(finite)
|
|
114
|
+
x_values = x_data[finite]
|
|
115
|
+
y_values = y_data[finite]
|
|
116
|
+
if cursor_y is None:
|
|
117
|
+
distances = np.abs(x_values - cursor_x)
|
|
118
|
+
else:
|
|
119
|
+
distances = np.square(x_values - cursor_x) + np.square(y_values - cursor_y)
|
|
120
|
+
index = int(np.argmin(distances))
|
|
121
|
+
|
|
122
|
+
return _SnapTraceSample(
|
|
123
|
+
x_value=float(x_values[index]),
|
|
124
|
+
y_value=float(y_values[index]),
|
|
125
|
+
point_number=int(finite_indices[index]) + 1,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _line_is_snap_visible(line: object) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Return whether a plotted line should participate in snap selection.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
is_visible = getattr(line, "isVisible", None)
|
|
135
|
+
if callable(is_visible):
|
|
136
|
+
return bool(is_visible())
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _line_snap_data(line: object | None) -> _LineData | None:
|
|
141
|
+
"""
|
|
142
|
+
Return full trace samples with aligned physical and view coordinates.
|
|
143
|
+
|
|
144
|
+
Downsampling, clipping and dynamic range limiting only change rendering.
|
|
145
|
+
Neither display positions nor peak/mean aggregates identify raw samples.
|
|
146
|
+
Apply native processing before log mapping, keeping the resulting arrays
|
|
147
|
+
aligned for snapping and selection statistics.
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
if line is None or not _line_is_snap_visible(line):
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
get_data = getattr(line, "getData", None)
|
|
154
|
+
if not callable(get_data):
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
view_data = get_data()
|
|
158
|
+
if view_data is None or view_data[0] is None or view_data[1] is None:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
get_original = getattr(line, "getOriginalDataset", None)
|
|
162
|
+
raw_data = get_original() if callable(get_original) else view_data
|
|
163
|
+
if raw_data is None or raw_data[0] is None or raw_data[1] is None:
|
|
164
|
+
raw_data = view_data
|
|
165
|
+
if callable(get_original):
|
|
166
|
+
opts = getattr(line, "opts", {})
|
|
167
|
+
processing = {
|
|
168
|
+
key: opts.get(key, False)
|
|
169
|
+
for key in ("subtractMeanMode", "fftMode", "derivativeMode", "phasemapMode")
|
|
170
|
+
}
|
|
171
|
+
if any(processing.values()):
|
|
172
|
+
# Share the rendering mapping, without display reduction.
|
|
173
|
+
analysis_line = NativePlotDataItem(
|
|
174
|
+
x=raw_data[0], y=raw_data[1], **processing,
|
|
175
|
+
)
|
|
176
|
+
raw_data = analysis_line.getData()
|
|
177
|
+
if raw_data[0] is None or raw_data[1] is None:
|
|
178
|
+
return None
|
|
179
|
+
# Native log-X drops DC within the FFT stage. Phase map runs later
|
|
180
|
+
# and replaces both coordinates with full input-derived samples,
|
|
181
|
+
# so that FFT slice must not remove a valid phase-map point here.
|
|
182
|
+
if (processing["fftMode"] and not processing["phasemapMode"]
|
|
183
|
+
and opts.get("logMode", (False, False))[0]):
|
|
184
|
+
raw_data = (raw_data[0][1:], raw_data[1][1:])
|
|
185
|
+
view_data = tuple(np.asarray(values, dtype=float) for values in raw_data)
|
|
186
|
+
log_mode = opts.get("logMode", (False, False))
|
|
187
|
+
mapped = []
|
|
188
|
+
for values, logarithmic in zip(view_data, log_mode, strict=True):
|
|
189
|
+
if logarithmic:
|
|
190
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
191
|
+
values = np.log10(values)
|
|
192
|
+
values[~np.isfinite(values)] = np.nan
|
|
193
|
+
mapped.append(values)
|
|
194
|
+
view_data = tuple(mapped)
|
|
195
|
+
return _LineData(
|
|
196
|
+
x_view=view_data[0],
|
|
197
|
+
y_view=view_data[1],
|
|
198
|
+
x_raw=raw_data[0],
|
|
199
|
+
y_raw=raw_data[1],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _scene_distance_squared(
|
|
204
|
+
first: QtCore.QPointF,
|
|
205
|
+
second: QtCore.QPointF,
|
|
206
|
+
) -> float:
|
|
207
|
+
"""
|
|
208
|
+
Return squared distance between two scene points.
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
return (first.x() - second.x()) ** 2 + (first.y() - second.y()) ** 2
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Plot1DSnapMixin(_Plot1DSnapBase):
|
|
215
|
+
"""Snap-to-trace cursor readout for 1D plot windows."""
|
|
216
|
+
|
|
217
|
+
def initLabels(self):
|
|
218
|
+
"""
|
|
219
|
+
Sets up coordinate labels and trace snapping command for 1d plots.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
super().initLabels()
|
|
223
|
+
|
|
224
|
+
self.trace_label = qtw.QLabel("")
|
|
225
|
+
self.trace_label.setMinimumWidth(0)
|
|
226
|
+
self.toolbarCo_ord.addWidget(self.trace_label)
|
|
227
|
+
self._update_coordinate_context()
|
|
228
|
+
|
|
229
|
+
self.snap_to_trace_action = create_action(
|
|
230
|
+
SNAP_TO_TRACE_COMMAND,
|
|
231
|
+
self,
|
|
232
|
+
text=f"Snap to Trace ({SNAP_TO_TRACE_SHORTCUT_LABEL})",
|
|
233
|
+
status_tip="Toggle snap-to-trace cursor readout",
|
|
234
|
+
checkable=True,
|
|
235
|
+
)
|
|
236
|
+
self.snap_to_trace_action.setToolTip(
|
|
237
|
+
"Lock the coordinate readout to the nearest plotted data point"
|
|
238
|
+
)
|
|
239
|
+
self.register_shortcut(
|
|
240
|
+
self.snap_to_trace_action,
|
|
241
|
+
command_with_status(
|
|
242
|
+
"plot.snap_to_trace",
|
|
243
|
+
"Toggle snap-to-trace cursor readout",
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
self.snap_to_trace_action.toggled.connect(self._snap_to_trace_toggled)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def initMenu(self):
|
|
250
|
+
"""
|
|
251
|
+
Adds 1d-specific commands to the plot window menu bar.
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
super().initMenu()
|
|
255
|
+
|
|
256
|
+
view_menu = self._menu_by_title("&View")
|
|
257
|
+
if view_menu is None or self.snap_to_trace_action is None:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
actions = view_menu.actions()
|
|
261
|
+
before = actions[0] if actions else None
|
|
262
|
+
view_menu.insertAction(before, self.snap_to_trace_action)
|
|
263
|
+
view_menu.insertSeparator(before)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _menu_by_title(self, title):
|
|
267
|
+
"""
|
|
268
|
+
Returns the menu matching a top-level menu title.
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
menu_bar = self.menuBar()
|
|
272
|
+
if menu_bar is None:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
for action in menu_bar.actions():
|
|
276
|
+
if action.text() == title:
|
|
277
|
+
return action.menu()
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@QtCore.pyqtSlot(bool)
|
|
282
|
+
def _snap_to_trace_toggled(self, enabled):
|
|
283
|
+
"""
|
|
284
|
+
Handles the snap-to-trace toggle state.
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
if not enabled:
|
|
288
|
+
self._hide_snap_marker()
|
|
289
|
+
self._clear_snap_report()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@QtCore.pyqtSlot(object)
|
|
293
|
+
def mouseMoved(self, pos):
|
|
294
|
+
"""
|
|
295
|
+
Updates the coordinate readout, optionally snapping to a 1d trace.
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
if not (
|
|
299
|
+
self.snap_to_trace_action is not None
|
|
300
|
+
and self.snap_to_trace_action.isChecked()
|
|
301
|
+
):
|
|
302
|
+
super().mouseMoved(pos)
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
if not self.plot.sceneBoundingRect().contains(pos):
|
|
306
|
+
self._hide_snap_marker()
|
|
307
|
+
self._clear_snap_report()
|
|
308
|
+
self._set_cursor_index_label("")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
nearest = self._nearest_trace_point(pos)
|
|
312
|
+
if nearest is None:
|
|
313
|
+
self._hide_snap_marker()
|
|
314
|
+
self._clear_snap_report()
|
|
315
|
+
self._set_cursor_index_label("")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
self._set_cursor_index_label(f"[{nearest.point_number - 1}]")
|
|
319
|
+
self.pos_labels["x"].setText(f"x = {self.formatNum(nearest.x_value)};")
|
|
320
|
+
self.pos_labels["y"].setText(f"y = {self.formatNum(nearest.y_value)}")
|
|
321
|
+
self._show_snap_report(nearest.label, nearest.point_number)
|
|
322
|
+
self._show_snap_marker(
|
|
323
|
+
nearest.x_view_value,
|
|
324
|
+
nearest.y_view_value,
|
|
325
|
+
nearest.viewbox,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _show_snap_report(self, label, point_number):
|
|
330
|
+
"""
|
|
331
|
+
Shows the active run, plotted axes, and snapped point.
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
self._update_coordinate_context(label, point_number)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _clear_snap_report(self):
|
|
338
|
+
"""
|
|
339
|
+
Restores the persistent run and axis relationship message.
|
|
340
|
+
|
|
341
|
+
"""
|
|
342
|
+
self._update_coordinate_context()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _update_coordinate_context(self, label=None, point_number=None):
|
|
346
|
+
"""Show ``Run N: measurement vs setpoint`` and optional snap detail."""
|
|
347
|
+
|
|
348
|
+
if self.trace_label is None:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
run_id, vertical, horizontal, tooltip = self._coordinate_context_parts(label)
|
|
352
|
+
message = f"Run {run_id}: {vertical} vs {horizontal}"
|
|
353
|
+
if point_number is not None:
|
|
354
|
+
message += f" (snapped to point {point_number - 1})"
|
|
355
|
+
self.trace_label.setText(message)
|
|
356
|
+
self.trace_label.setToolTip(tooltip)
|
|
357
|
+
self.trace_label.adjustSize()
|
|
358
|
+
self.trace_label.updateGeometry()
|
|
359
|
+
self.toolbarCo_ord.updateGeometry()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _coordinate_context_parts(self, label=None):
|
|
363
|
+
"""Return run and human-readable plotted-axis names for the message."""
|
|
364
|
+
|
|
365
|
+
line = self.lines.get(label) if label is not None else self.__dict__.get("line")
|
|
366
|
+
source = getattr(line, "from_win", self)
|
|
367
|
+
try:
|
|
368
|
+
dataset = source.ds
|
|
369
|
+
except (AttributeError, RuntimeError):
|
|
370
|
+
dataset = None
|
|
371
|
+
run_id = getattr(dataset, "run_id", "?")
|
|
372
|
+
|
|
373
|
+
host_options = self.__dict__.get("_axis_selection", {})
|
|
374
|
+
if not host_options:
|
|
375
|
+
try:
|
|
376
|
+
host_options = self.axis_options
|
|
377
|
+
except RuntimeError:
|
|
378
|
+
host_options = {}
|
|
379
|
+
horizontal_source = self
|
|
380
|
+
horizontal_axis = "x"
|
|
381
|
+
source_axis = "y"
|
|
382
|
+
if source is not self:
|
|
383
|
+
axis_order = getattr(line, "choose_from", None)
|
|
384
|
+
if axis_order is not None and len(axis_order) == 2:
|
|
385
|
+
horizontal_axis = axis_order[0]
|
|
386
|
+
source_axis = axis_order[1]
|
|
387
|
+
horizontal_source = source
|
|
388
|
+
source_options = (
|
|
389
|
+
host_options
|
|
390
|
+
if source is self
|
|
391
|
+
else getattr(source, "axis_options", {})
|
|
392
|
+
)
|
|
393
|
+
horizontal_name = source_options.get(horizontal_axis)
|
|
394
|
+
if horizontal_name is None:
|
|
395
|
+
horizontal_name = host_options.get("x", "x")
|
|
396
|
+
horizontal_source = self
|
|
397
|
+
horizontal_axis = "x"
|
|
398
|
+
horizontal = self._parameter_display_name(
|
|
399
|
+
horizontal_source,
|
|
400
|
+
horizontal_name,
|
|
401
|
+
horizontal_axis,
|
|
402
|
+
)
|
|
403
|
+
vertical_name = source_options.get(source_axis)
|
|
404
|
+
if vertical_name is None:
|
|
405
|
+
try:
|
|
406
|
+
source_param = source.param
|
|
407
|
+
except (AttributeError, RuntimeError):
|
|
408
|
+
source_param = None
|
|
409
|
+
vertical_name = getattr(source_param, "name", "y")
|
|
410
|
+
vertical = self._parameter_display_name(
|
|
411
|
+
source,
|
|
412
|
+
vertical_name,
|
|
413
|
+
source_axis,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
display_label = getattr(self, "_trace_display_label", None)
|
|
417
|
+
if label is not None and callable(display_label):
|
|
418
|
+
tooltip = str(display_label(label, line))
|
|
419
|
+
else:
|
|
420
|
+
tooltip = str(getattr(source, "label", ""))
|
|
421
|
+
return run_id, vertical, horizontal, tooltip
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _parameter_display_name(source, name, axis):
|
|
426
|
+
"""Resolve a parameter label without discarding a useful name."""
|
|
427
|
+
|
|
428
|
+
param = getattr(source, "param_dict", {}).get(name)
|
|
429
|
+
if param is None:
|
|
430
|
+
axis_param = getattr(source, "axis_param", {})
|
|
431
|
+
candidate = axis_param.get(axis) if isinstance(axis_param, dict) else None
|
|
432
|
+
if getattr(candidate, "name", name) == name:
|
|
433
|
+
param = candidate
|
|
434
|
+
if param is None:
|
|
435
|
+
candidate = getattr(source, "param", None)
|
|
436
|
+
if getattr(candidate, "name", None) == name:
|
|
437
|
+
param = candidate
|
|
438
|
+
return str(getattr(param, "label", None) or getattr(param, "name", None) or name)
|
|
439
|
+
|
|
440
|
+
def _nearest_trace_point(self, scene_pos):
|
|
441
|
+
"""
|
|
442
|
+
Finds the plotted data point nearest to the mouse position.
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
nearest = None
|
|
446
|
+
nearest_distance = None
|
|
447
|
+
|
|
448
|
+
for label, line in self.lines.items():
|
|
449
|
+
data = _line_snap_data(line)
|
|
450
|
+
if data is None:
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
viewbox = self._viewbox_for_line(line)
|
|
454
|
+
sample, distance = self._nearest_scene_trace_sample(
|
|
455
|
+
data.x_view,
|
|
456
|
+
data.y_view,
|
|
457
|
+
scene_pos,
|
|
458
|
+
viewbox,
|
|
459
|
+
data.x_raw,
|
|
460
|
+
data.y_raw,
|
|
461
|
+
)
|
|
462
|
+
if sample is None:
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
if nearest_distance is None or distance < nearest_distance:
|
|
466
|
+
nearest = _NearestTracePoint(
|
|
467
|
+
label=label,
|
|
468
|
+
x_value=sample.x_value,
|
|
469
|
+
y_value=sample.y_value,
|
|
470
|
+
viewbox=viewbox,
|
|
471
|
+
point_number=sample.point_number,
|
|
472
|
+
x_view_value=sample.x_view_value,
|
|
473
|
+
y_view_value=sample.y_view_value,
|
|
474
|
+
)
|
|
475
|
+
nearest_distance = distance
|
|
476
|
+
|
|
477
|
+
return nearest
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def _nearest_scene_trace_sample(
|
|
482
|
+
x_data,
|
|
483
|
+
y_data,
|
|
484
|
+
scene_pos,
|
|
485
|
+
viewbox,
|
|
486
|
+
raw_x_data=None,
|
|
487
|
+
raw_y_data=None,
|
|
488
|
+
):
|
|
489
|
+
"""Return the nearest full trace sample with aligned physical values."""
|
|
490
|
+
|
|
491
|
+
x_data = np.asarray(x_data, dtype=float)
|
|
492
|
+
y_data = np.asarray(y_data, dtype=float)
|
|
493
|
+
if raw_x_data is None:
|
|
494
|
+
raw_x_data = x_data
|
|
495
|
+
if raw_y_data is None:
|
|
496
|
+
raw_y_data = y_data
|
|
497
|
+
raw_x_data = np.asarray(raw_x_data, dtype=float)
|
|
498
|
+
raw_y_data = np.asarray(raw_y_data, dtype=float)
|
|
499
|
+
count = min(
|
|
500
|
+
x_data.size,
|
|
501
|
+
y_data.size,
|
|
502
|
+
raw_x_data.size,
|
|
503
|
+
raw_y_data.size,
|
|
504
|
+
)
|
|
505
|
+
if count == 0:
|
|
506
|
+
return None, None
|
|
507
|
+
|
|
508
|
+
x_data = x_data[:count]
|
|
509
|
+
y_data = y_data[:count]
|
|
510
|
+
raw_x_data = raw_x_data[:count]
|
|
511
|
+
raw_y_data = raw_y_data[:count]
|
|
512
|
+
finite = (
|
|
513
|
+
np.isfinite(x_data)
|
|
514
|
+
& np.isfinite(y_data)
|
|
515
|
+
& np.isfinite(raw_x_data)
|
|
516
|
+
& np.isfinite(raw_y_data)
|
|
517
|
+
)
|
|
518
|
+
if not np.any(finite):
|
|
519
|
+
return None, None
|
|
520
|
+
|
|
521
|
+
finite_indices = np.flatnonzero(finite)
|
|
522
|
+
x_values = x_data[finite]
|
|
523
|
+
y_values = y_data[finite]
|
|
524
|
+
raw_x_values = raw_x_data[finite]
|
|
525
|
+
raw_y_values = raw_y_data[finite]
|
|
526
|
+
# Read the affine coefficients directly: subtracting mapped unit
|
|
527
|
+
# points loses precision when a narrow sweep is far from zero.
|
|
528
|
+
# childTransform also updates the matrix after a range/geometry change.
|
|
529
|
+
transform = viewbox.childTransform() * viewbox.sceneTransform()
|
|
530
|
+
origin = viewbox.mapSceneToView(scene_pos)
|
|
531
|
+
scene_origin = viewbox.mapViewToScene(origin)
|
|
532
|
+
x_offsets = x_values - origin.x()
|
|
533
|
+
y_offsets = y_values - origin.y()
|
|
534
|
+
# Work near the pointer so neither the large absolute data coordinates
|
|
535
|
+
# nor the transform's large translation enter the vectorized mapping.
|
|
536
|
+
scene_dx = (
|
|
537
|
+
(scene_origin.x() - scene_pos.x())
|
|
538
|
+
+ x_offsets * transform.m11()
|
|
539
|
+
+ y_offsets * transform.m21()
|
|
540
|
+
)
|
|
541
|
+
scene_dy = (
|
|
542
|
+
(scene_origin.y() - scene_pos.y())
|
|
543
|
+
+ x_offsets * transform.m12()
|
|
544
|
+
+ y_offsets * transform.m22()
|
|
545
|
+
)
|
|
546
|
+
distances = np.square(scene_dx) + np.square(scene_dy)
|
|
547
|
+
index = int(np.argmin(distances))
|
|
548
|
+
sample = _SnapTraceSample(
|
|
549
|
+
x_value=float(raw_x_values[index]),
|
|
550
|
+
y_value=float(raw_y_values[index]),
|
|
551
|
+
point_number=int(finite_indices[index]) + 1,
|
|
552
|
+
x_view_value=float(x_values[index]),
|
|
553
|
+
y_view_value=float(y_values[index]),
|
|
554
|
+
)
|
|
555
|
+
return sample, float(distances[index])
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _viewbox_for_line(self, line):
|
|
559
|
+
"""
|
|
560
|
+
Returns the viewbox that owns a plotted line.
|
|
561
|
+
|
|
562
|
+
"""
|
|
563
|
+
get_viewbox = getattr(line, "getViewBox", None)
|
|
564
|
+
if callable(get_viewbox):
|
|
565
|
+
viewbox = get_viewbox()
|
|
566
|
+
if viewbox is not None:
|
|
567
|
+
return viewbox
|
|
568
|
+
if getattr(line, "side", "left") == "right" and self.right_vb is not None:
|
|
569
|
+
return self.right_vb
|
|
570
|
+
|
|
571
|
+
return self.plot.vb
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _show_snap_marker(self, x_value, y_value, viewbox):
|
|
575
|
+
"""
|
|
576
|
+
Places a small marker on the snapped data point.
|
|
577
|
+
|
|
578
|
+
"""
|
|
579
|
+
if self.snap_marker is None:
|
|
580
|
+
self.snap_marker = pg.ScatterPlotItem(
|
|
581
|
+
symbol="s",
|
|
582
|
+
size=3,
|
|
583
|
+
pen=pg.mkPen("k", width=1),
|
|
584
|
+
brush=pg.mkBrush("w"),
|
|
585
|
+
)
|
|
586
|
+
# The marker follows the pointer and is often directly over the
|
|
587
|
+
# trace. Keep it behind plot lines so the line receives a
|
|
588
|
+
# double-click before the marker can consume it.
|
|
589
|
+
self.snap_marker.setZValue(-1)
|
|
590
|
+
|
|
591
|
+
if self._snap_marker_view is not viewbox:
|
|
592
|
+
self._hide_snap_marker()
|
|
593
|
+
viewbox.addItem(self.snap_marker, ignoreBounds=True)
|
|
594
|
+
self._snap_marker_view = viewbox
|
|
595
|
+
|
|
596
|
+
self.snap_marker.setData([x_value], [y_value])
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _hide_snap_marker(self):
|
|
600
|
+
"""
|
|
601
|
+
Removes the snap marker from whichever viewbox currently owns it.
|
|
602
|
+
|
|
603
|
+
"""
|
|
604
|
+
if self.snap_marker is None or self._snap_marker_view is None:
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
self._snap_marker_view.removeItem(self.snap_marker)
|
|
608
|
+
self._snap_marker_view = None
|