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.
Files changed (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. qplotter-1.6.0.dist-info/top_level.txt +1 -0
qplot/windows/_help.py ADDED
@@ -0,0 +1,210 @@
1
+ from html import escape
2
+ from typing import Any, cast
3
+
4
+ from PyQt6 import QtCore, QtGui
5
+ from PyQt6 import QtWidgets as qtw
6
+
7
+ from qplot._metadata import version_info
8
+ from qplot.diagnostics import default_log_file
9
+
10
+ from ._commands import create_action, shortcut_help_html
11
+
12
+ _OPEN_HELP_DIALOGS: list[qtw.QDialog] = []
13
+
14
+
15
+ QUICK_START_HTML = """
16
+ <h2>Quick Start</h2>
17
+ <ol>
18
+ <li><b>Load a database.</b> Drop a QCoDeS .db file onto the database field,
19
+ or use <b>File -&gt; Load Database...</b>.</li>
20
+ <li><b>Select a run.</b> Click a row in the run table to see details,
21
+ parameters, preview images, and metadata.</li>
22
+ <li><b>Open a plot.</b> Double-click a preview, right-click a run, or enter
23
+ a run ID and measurement number at the top of the main window.</li>
24
+ <li><b>Inspect the plot.</b> Use the mouse wheel to zoom, left-drag to pan,
25
+ and Shift-drag to constrain panning to one axis,
26
+ right-click for plot actions, and double-click axes for scale controls.</li>
27
+ <li><b>Export or print data and plots.</b> Use the CSV button or preview
28
+ context menu for measurement data. In plot windows, use
29
+ <b>File -&gt; Export Plot...</b> for images or data, or
30
+ <b>File -&gt; Save Plot as PDF...</b> for a plot-sized PDF. Use
31
+ <b>File -&gt; Print Plot...</b> for a printer or, when the system dialog
32
+ offers a concrete PDF destination, a page-formatted PDF. qPlot stages and
33
+ atomically publishes PDF file output.</li>
34
+ </ol>
35
+ <p>Plot windows may appear before their data has finished loading. Check the
36
+ status bar at the bottom of the window before assuming a load has failed.</p>
37
+ <p>Set the refresh interval to <b>0.0 s</b> for manual refresh only. Press
38
+ <b>R</b> to refresh the active window.</p>
39
+ """
40
+
41
+
42
+ KEYBOARD_SHORTCUTS_HTML = shortcut_help_html()
43
+
44
+
45
+ def add_help_menu(window: qtw.QMainWindow) -> qtw.QMenu:
46
+ """
47
+ Adds qPlot's shared Help menu to a main or plot window.
48
+
49
+ """
50
+ menu_bar = window.menuBar()
51
+ if menu_bar is None:
52
+ raise RuntimeError("Help menu requires a menu bar.")
53
+ help_menu = menu_bar.addMenu("&Help")
54
+ if help_menu is None:
55
+ raise RuntimeError("Help menu could not be created.")
56
+
57
+ quick_start_action = create_action("help.quick_start", window)
58
+ quick_start_action.triggered.connect(lambda: show_quick_start(window))
59
+ help_menu.addAction(quick_start_action)
60
+
61
+ shortcuts_action = QtGui.QAction("&Keyboard Shortcuts", window)
62
+ shortcuts_action.setObjectName("keyboardShortcutsHelpAction")
63
+ shortcuts_action.setStatusTip("Show qPlot keyboard shortcuts")
64
+ shortcuts_action.triggered.connect(lambda: show_keyboard_shortcuts(window))
65
+ help_menu.addAction(shortcuts_action)
66
+
67
+ help_menu.addSeparator()
68
+
69
+ copy_log_path_action = QtGui.QAction("Copy &Diagnostic Log Path", window)
70
+ copy_log_path_action.setObjectName("copyDiagnosticLogPathAction")
71
+ copy_log_path_action.setStatusTip("Copy the qPlot diagnostic log file path")
72
+ copy_log_path_action.triggered.connect(lambda: copy_diagnostic_log_path(window))
73
+ help_menu.addAction(copy_log_path_action)
74
+
75
+ help_menu.addSeparator()
76
+ about_action = QtGui.QAction("&About qPlot", window)
77
+ about_action.setObjectName("aboutQplotAction")
78
+ # Keep this window-owned action in Help. Cocoa's AboutRole rewrites its
79
+ # title to "About python", while ApplicationSpecificRole accumulates
80
+ # duplicate application-menu entries when switching between windows.
81
+ about_action.setMenuRole(QtGui.QAction.MenuRole.NoRole)
82
+ about_action.setStatusTip("Show qPlot version and build information")
83
+ about_action.triggered.connect(lambda: show_about(window))
84
+ help_menu.addAction(about_action)
85
+
86
+ return help_menu
87
+
88
+
89
+ def show_about(parent: qtw.QWidget | None = None) -> qtw.QDialog:
90
+ """Show selectable release and development provenance in an owned dialog."""
91
+ info = version_info()
92
+ rows = [f"<p><b>Version:</b> {escape(str(info['version']))}</p>"]
93
+ if "released" in info:
94
+ build = "Release" if info["released"] else "Unreleased development build"
95
+ if info.get("dirty"):
96
+ build += " (local changes)"
97
+ rows.append(f"<p><b>Build:</b> {build}</p>")
98
+ elif info.get("checkout"):
99
+ rows.append("<p><b>Build:</b> Source checkout</p>")
100
+ if info.get("commit"):
101
+ rows.append(f"<p><b>Commit:</b> {escape(str(info['commit']))}</p>")
102
+ if info.get("pull_request"):
103
+ rows.append(f"<p><b>Pull request:</b> #{escape(str(info['pull_request']))}</p>")
104
+ return _show_help_dialog(
105
+ parent, "About qPlot",
106
+ "<h2>qPlot</h2><p>A viewer for QCoDeS measurement data.</p>"
107
+ + "".join(rows)
108
+ + '<p><a href="https://github.com/lairdgrouplancaster/QCoDeS-Plotter">'
109
+ "Project on GitHub</a></p>",
110
+ "qplotAboutDialog",
111
+ size=(580, 320),
112
+ )
113
+
114
+
115
+ def show_quick_start(parent: qtw.QWidget | None = None) -> qtw.QDialog:
116
+ """
117
+ Opens the quick-start help dialog.
118
+
119
+ """
120
+ return _show_help_dialog(
121
+ parent,
122
+ "qPlot Quick Start",
123
+ QUICK_START_HTML,
124
+ "qplotQuickStartDialog",
125
+ )
126
+
127
+
128
+ def show_keyboard_shortcuts(parent: qtw.QWidget | None = None) -> qtw.QDialog:
129
+ """
130
+ Opens the keyboard-shortcuts help dialog.
131
+
132
+ """
133
+ return _show_help_dialog(
134
+ parent,
135
+ "qPlot Keyboard Shortcuts",
136
+ KEYBOARD_SHORTCUTS_HTML,
137
+ "qplotKeyboardShortcutsDialog",
138
+ )
139
+
140
+
141
+ def copy_diagnostic_log_path(parent: Any | None = None) -> str:
142
+ """
143
+ Copies qPlot's diagnostic log path to the clipboard.
144
+
145
+ """
146
+ path = str(default_log_file())
147
+ clipboard = qtw.QApplication.clipboard()
148
+ if clipboard is not None:
149
+ clipboard.setText(path)
150
+ if parent is not None and hasattr(parent, "show_status"):
151
+ parent.show_status(f"Copied diagnostic log path: {path}", 5000)
152
+ return path
153
+
154
+
155
+ def _show_help_dialog(
156
+ parent: qtw.QWidget | None,
157
+ title: str,
158
+ html: str,
159
+ object_name: str,
160
+ *,
161
+ size: tuple[int, int] = (640, 520),
162
+ ) -> qtw.QDialog:
163
+ dialog = qtw.QDialog(parent)
164
+ dialog.setObjectName(object_name)
165
+ dialog.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose, True)
166
+ dialog.setWindowTitle(title)
167
+ dialog.resize(*size)
168
+
169
+ layout = qtw.QVBoxLayout(dialog)
170
+ browser = qtw.QTextBrowser()
171
+ browser.setObjectName("qplotHelpBrowser")
172
+ browser.setOpenExternalLinks(True)
173
+ browser.setHtml(html)
174
+ browser.setMinimumSize(min(520, size[0] - 40), min(360, size[1] - 100))
175
+ layout.addWidget(browser)
176
+
177
+ buttons = qtw.QDialogButtonBox(qtw.QDialogButtonBox.StandardButton.Close)
178
+ buttons.rejected.connect(dialog.close)
179
+ layout.addWidget(buttons)
180
+
181
+ _remember_help_dialog(parent, dialog)
182
+ dialog.show()
183
+ dialog.raise_()
184
+ dialog.activateWindow()
185
+ return dialog
186
+
187
+
188
+ def _remember_help_dialog(
189
+ parent: qtw.QWidget | None,
190
+ dialog: qtw.QDialog,
191
+ ) -> None:
192
+ dialogs: list[qtw.QDialog] | None
193
+ if parent is None:
194
+ dialogs = _OPEN_HELP_DIALOGS
195
+ else:
196
+ owner = cast(Any, parent)
197
+ dialogs = getattr(owner, "_help_dialogs", None)
198
+ if dialogs is None:
199
+ dialogs = []
200
+ owner._help_dialogs = dialogs
201
+
202
+ if dialogs is None:
203
+ return
204
+ dialogs.append(dialog)
205
+
206
+ def forget_dialog(*_args: object) -> None:
207
+ if dialog in dialogs:
208
+ dialogs.remove(dialog)
209
+
210
+ dialog.destroyed.connect(forget_dialog)
@@ -0,0 +1,303 @@
1
+ """One appearance list with controls for the selected renderer type."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from PyQt6 import QtCore, QtGui
6
+ from PyQt6 import QtWidgets as qtw
7
+
8
+ from ._plot1d_traces import _TraceAppearanceDialog
9
+ from ._plot2d_layers import _HeatmapAppearanceControls
10
+ from ._plot_appearance import CenteredPreviewDelegate
11
+ from ._plot_drop_axes import dataset_axis_warnings
12
+
13
+ if TYPE_CHECKING:
14
+ from ._mixed_plots import MixedPlotMixin
15
+
16
+ _KIND_ROLE = int(QtCore.Qt.ItemDataRole.UserRole) + 1
17
+
18
+
19
+ class _HeatmapControls(_HeatmapAppearanceControls):
20
+ def __init__(self, dialog: "PlotAppearanceDialog"):
21
+ self.dialog = dialog
22
+ super().__init__(dialog.owner, table=dialog.table)
23
+ self.setParent(dialog)
24
+ layout = self.layout()
25
+ if layout is not None:
26
+ layout.setContentsMargins(0, 0, 0, 0)
27
+
28
+ def refresh_rows(self):
29
+ self.dialog.refresh_rows()
30
+
31
+
32
+ class PlotAppearanceDialog(_TraceAppearanceDialog):
33
+ """Reuse trace and heatmap editors behind a single dataset table."""
34
+
35
+ owner: "MixedPlotMixin"
36
+ _COL_WARNING = 3
37
+
38
+ def __init__(self, owner: "MixedPlotMixin"):
39
+ super().__init__(owner)
40
+ self.setWindowTitle("Plot Appearance")
41
+ self.table.setObjectName("plotAppearanceTable")
42
+ self.table.setColumnCount(4)
43
+ self.table.setHorizontalHeaderLabels(["ID", "Preview", "Measurement", ""])
44
+ self.table.setItemDelegateForColumn(
45
+ self._COL_WARNING,
46
+ CenteredPreviewDelegate(self.table, icon_size=QtCore.QSize(18, 18)),
47
+ )
48
+ header = self.table.horizontalHeader()
49
+ if header is not None:
50
+ header.setSectionResizeMode(self._COL_WARNING, qtw.QHeaderView.ResizeMode.Fixed)
51
+ header.resizeSection(self._COL_WARNING, 28)
52
+ self.table.setToolTip("Data at the top is drawn on top. Drag rows to change the drawing order.")
53
+ self.setStyleSheet(self.styleSheet().replace("traceAppearanceTable", "plotAppearanceTable"))
54
+ self.heatmap_controls = _HeatmapControls(self)
55
+ self.controls_stack = qtw.QStackedWidget(self)
56
+ self.controls_stack.setObjectName("plotAppearanceControls")
57
+ self.appearance_body.removeWidget(self.trace_controls_panel)
58
+ self.controls_stack.addWidget(self.trace_controls_panel)
59
+ self.controls_stack.addWidget(self.heatmap_controls)
60
+ self.controls_stack.setMinimumWidth(self.trace_controls_panel.minimumSizeHint().width())
61
+ self.appearance_body.addWidget(self.controls_stack, 4)
62
+ for widget in (self.add_trace_combo, self.add_trace_button, self.remove_trace_button):
63
+ widget.hide()
64
+ self.add_item_combo = qtw.QComboBox(self)
65
+ self.add_item_combo.setObjectName("plotAppearanceAddCombo")
66
+ self.add_item_combo.setMinimumContentsLength(18)
67
+ self.add_item_combo.setSizeAdjustPolicy(
68
+ qtw.QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon
69
+ )
70
+ self.add_item_combo.setToolTip("Choose a trace or heatmap to add.")
71
+ self.add_item_button = qtw.QPushButton("Add", self)
72
+ self.remove_item_button = qtw.QPushButton("Remove", self)
73
+ actions = qtw.QHBoxLayout()
74
+ actions.addWidget(self.add_item_combo, 1)
75
+ actions.addWidget(self.add_item_button)
76
+ actions.addWidget(self.remove_item_button)
77
+ self.items_layout.addLayout(actions)
78
+ self.add_item_combo.currentIndexChanged.connect(self._add_item_selection_changed)
79
+ self.add_item_button.clicked.connect(self._add_item)
80
+ self.remove_item_button.clicked.connect(self._remove_items)
81
+ self.table.currentCellChanged.connect(lambda *_args: self._sync_controls_from_selection())
82
+ self.setMinimumWidth(max(self.minimumWidth(), self.minimumSizeHint().width()))
83
+ self.refresh_rows()
84
+
85
+ def _row_entry(self, row: int) -> tuple[str, Any] | None:
86
+ item = self.table.item(row, self._COL_ID)
87
+ if item is None:
88
+ return None
89
+ return item.data(_KIND_ROLE), item.data(QtCore.Qt.ItemDataRole.UserRole)
90
+
91
+ def _selected_entries(self) -> list[tuple[str, Any]]:
92
+ return [entry for row in self._selected_rows() if (entry := self._row_entry(row)) is not None]
93
+
94
+ def _sync_controls_from_selection(self):
95
+ if self._building:
96
+ return
97
+ controls = self.__dict__.get("heatmap_controls")
98
+ if controls is None:
99
+ super()._sync_controls_from_selection()
100
+ return
101
+ selected = self._selected_entries()
102
+ current = self._row_entry(self.table.currentRow())
103
+ entry = current if current in selected else next(iter(selected), None)
104
+ if entry is not None and entry[0] == "heatmap":
105
+ self.controls_stack.setCurrentWidget(controls)
106
+ controls._sync_controls_from_selection()
107
+ else:
108
+ self.controls_stack.setCurrentWidget(self.trace_controls_panel)
109
+ super()._sync_controls_from_selection()
110
+ self.remove_item_button.setEnabled(self._can_remove_items(selected))
111
+ self.remove_item_button.setToolTip(self.owner.plot_item_removal_error(selected) or "")
112
+
113
+ def _can_remove_items(self, selected):
114
+ entries = set(self.owner._plot_item_order)
115
+ selected = set(selected)
116
+ return bool(selected) and selected < entries and not self.owner.plot_item_removal_error(selected)
117
+
118
+ def _is_primary(self, kind: str, key: Any) -> bool:
119
+ if kind == "trace":
120
+ return self.owner.lines.get(key) is self.owner.line
121
+ return self.owner.heatmaps.get(key) is self.owner
122
+
123
+ def sync_swap_axes_control(self):
124
+ super().sync_swap_axes_control()
125
+ controls = self.__dict__.get("heatmap_controls")
126
+ if controls is not None:
127
+ controls.sync_swap_axes_control()
128
+
129
+ def refresh_rows(self):
130
+ if "heatmap_controls" not in self.__dict__:
131
+ return
132
+ selected = set(self._selected_entries())
133
+ current = self._row_entry(self.table.currentRow())
134
+ self.owner._sync_plot_item_order()
135
+ pending = self.__dict__.get("_pending_added_entry")
136
+ if pending is not None and pending in self.owner._plot_item_order:
137
+ selected = {pending}
138
+ current = pending
139
+ self._pending_added_entry = None
140
+ self.sync_swap_axes_control()
141
+ self.heatmap_controls.refresh_color_scale(refresh_rows=False)
142
+ self._building = True
143
+ blocked = self.table.blockSignals(True)
144
+ try:
145
+ self.table.setRowCount(0)
146
+ for row, (kind, key) in enumerate(self.owner._plot_item_order):
147
+ self.table.insertRow(row)
148
+ if kind == "trace":
149
+ renderer = self.owner.lines[key]
150
+ label = self.owner._trace_display_label(key, renderer)
151
+ measurement = self.owner._trace_measurement_name(key, renderer)
152
+ icon = self._trace_icon(self.owner._ensure_trace_styles()[key])
153
+ else:
154
+ renderer = self.owner.heatmaps[key]
155
+ label = self.owner._heatmap_display_label(key, renderer)
156
+ measurement = self.owner._heatmap_measurement_name(key, renderer)
157
+ icon = QtGui.QIcon(self.heatmap_controls._heatmap_pixmap(renderer))
158
+ dataset_id = label.split()[0].replace("ID:", "") if label.startswith("ID:") else str(row + 1)
159
+ for column, value in ((self._COL_ID, dataset_id), (self._COL_MEASUREMENT, measurement)):
160
+ item = qtw.QTableWidgetItem(value)
161
+ item.setFlags(item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
162
+ item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignVCenter | (
163
+ QtCore.Qt.AlignmentFlag.AlignRight if column == self._COL_ID
164
+ else QtCore.Qt.AlignmentFlag.AlignLeft
165
+ ))
166
+ item.setToolTip(label if column == self._COL_ID else f"{measurement} ({kind})")
167
+ self.table.setItem(row, column, item)
168
+ id_item = self.table.item(row, self._COL_ID)
169
+ assert id_item is not None
170
+ id_item.setData(QtCore.Qt.ItemDataRole.UserRole, key)
171
+ id_item.setData(_KIND_ROLE, kind)
172
+ preview = qtw.QTableWidgetItem()
173
+ preview.setIcon(icon)
174
+ preview.setFlags(preview.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable
175
+ & ~QtCore.Qt.ItemFlag.ItemIsDragEnabled)
176
+ preview.setToolTip("Trace preview" if kind == "trace" else "Heatmap preview")
177
+ self.table.setItem(row, self._COL_PREVIEW, preview)
178
+ self.table.setRowHeight(row, 28)
179
+ if (kind, key) in selected:
180
+ selection = self.table.selectionModel()
181
+ model = self.table.model()
182
+ if selection is not None and model is not None:
183
+ selection.select(model.index(row, 0),
184
+ QtCore.QItemSelectionModel.SelectionFlag.Select
185
+ | QtCore.QItemSelectionModel.SelectionFlag.Rows)
186
+ if (kind, key) == current:
187
+ self.table.setCurrentItem(id_item, QtCore.QItemSelectionModel.SelectionFlag.NoUpdate)
188
+ finally:
189
+ self.table.blockSignals(blocked)
190
+ self._building = False
191
+ if self.table.rowCount() and not self._selected_entries():
192
+ self.table.selectRow(0)
193
+ self._sync_controls_from_selection()
194
+ self.refresh_available_items()
195
+ self.refresh_axis_warnings()
196
+
197
+ def refresh_axis_warnings(self):
198
+ if self._building or "heatmap_controls" not in self.__dict__:
199
+ return
200
+ style = self.style()
201
+ if style is None:
202
+ return
203
+ icon = style.standardIcon(qtw.QStyle.StandardPixmap.SP_MessageBoxWarning)
204
+ for row in range(self.table.rowCount()):
205
+ entry = self._row_entry(row)
206
+ if entry is None:
207
+ continue
208
+ kind, key = entry
209
+ registry = self.owner.lines if kind == "trace" else self.owner.heatmaps
210
+ if key not in registry:
211
+ continue
212
+ reasons = dataset_axis_warnings(self.owner, kind, key)
213
+ item = qtw.QTableWidgetItem()
214
+ item.setFlags(item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable
215
+ & ~QtCore.Qt.ItemFlag.ItemIsDragEnabled)
216
+ if reasons:
217
+ item.setIcon(icon)
218
+ item.setToolTip("Incompatible axes:\n" + "\n".join(reasons))
219
+ item.setData(QtCore.Qt.ItemDataRole.AccessibleTextRole, item.toolTip())
220
+ self.table.setItem(row, self._COL_WARNING, item)
221
+
222
+ def _sync_plot_order_from_rows(self):
223
+ self.owner._sync_plot_item_order()
224
+
225
+ def _move_rows_to_position(self, source_rows, destination_row):
226
+ entries = list(self.owner._plot_item_order)
227
+ source_rows = sorted({row for row in source_rows if 0 <= row < len(entries)})
228
+ selected = [entries[row] for row in source_rows]
229
+ destination_row = max(0, min(destination_row, len(entries)))
230
+ destination_row -= sum(row < destination_row for row in source_rows)
231
+ remaining = [entry for row, entry in enumerate(entries) if row not in source_rows]
232
+ self.owner._plot_item_order = remaining[:destination_row] + selected + remaining[destination_row:]
233
+ for kind, registry in (("trace", self.owner.lines), ("heatmap", self.owner.heatmaps)):
234
+ reordered = [(key, registry[key]) for entry_kind, key in self.owner._plot_item_order if entry_kind == kind]
235
+ registry.clear()
236
+ registry.update(reordered)
237
+ self.owner._sync_mixed_axis_labels()
238
+ self.owner._sync_heatmap_colorbar_items()
239
+ self.refresh_rows()
240
+
241
+ def select_heatmap(self, key: Any) -> bool:
242
+ return self.heatmap_controls.select_heatmap(key)
243
+
244
+ def refresh_color_scale(self, *, refresh_rows=True):
245
+ self.heatmap_controls.refresh_color_scale(refresh_rows=refresh_rows)
246
+
247
+ def refresh_available_traces(self):
248
+ super().refresh_available_traces()
249
+ if "add_item_combo" in self.__dict__:
250
+ self.refresh_available_items()
251
+
252
+ def refresh_available_items(self):
253
+ previous = self.add_item_combo.currentData()
254
+ blocked = self.add_item_combo.blockSignals(True)
255
+ try:
256
+ self.add_item_combo.clear()
257
+ self.add_item_combo.addItem("Select a measurement to add…", None)
258
+ for kind, provider, registry in (
259
+ ("trace", self.owner.available_trace_candidates, self.owner.lines),
260
+ ("heatmap", self.owner.available_heatmap_candidates, self.owner.heatmaps),
261
+ ):
262
+ for label, key in provider():
263
+ if key not in registry:
264
+ self.add_item_combo.addItem(f"{label} ({kind})", (kind, key, label))
265
+ self.add_item_combo.setCurrentIndex(max(0, self.add_item_combo.findData(previous)))
266
+ finally:
267
+ self.add_item_combo.blockSignals(blocked)
268
+ self._add_item_selection_changed()
269
+
270
+ def _add_item_selection_changed(self, *_args):
271
+ self.add_item_button.setEnabled(self.add_item_combo.currentData() is not None)
272
+
273
+ def _add_item(self, *_args):
274
+ entry = self.add_item_combo.currentData()
275
+ if entry is None:
276
+ return
277
+ kind, key, label = entry
278
+ self._pending_added_entry = (kind, key)
279
+ try:
280
+ if kind == "trace":
281
+ self.owner.add_trace_from_dialog(label, key)
282
+ else:
283
+ self.owner.add_heatmap_from_dialog(label, key)
284
+ except Exception as error:
285
+ self._pending_added_entry = None
286
+ qtw.QMessageBox.critical(self, "Could Not Add Dataset", f"The selected dataset could not be added:\n{error}")
287
+ self.refresh_rows()
288
+ if kind == "trace":
289
+ self.select_trace(key)
290
+ else:
291
+ self.select_heatmap(key)
292
+
293
+ def _remove_items(self, *_args):
294
+ selected = self._selected_entries()
295
+ if not self._can_remove_items(selected):
296
+ return
297
+ try:
298
+ owner = self.owner.remove_plot_items(selected)
299
+ except Exception as error:
300
+ qtw.QMessageBox.critical(self, "Could Not Remove Dataset", str(error))
301
+ return
302
+ if owner is self.owner:
303
+ self.refresh_rows()