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
@@ -0,0 +1,461 @@
1
+ """Shared trace and heatmap ownership for plots containing both data types."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from PyQt6 import QtGui
6
+
7
+ from ._mixed_appearance import PlotAppearanceDialog
8
+ from ._plot1d_traces import Plot1DTraceMixin
9
+ from ._plot2d_colorbar import Plot2DColorbarMixin
10
+ from ._plot2d_layers import Plot2DLayerMixin
11
+ from ._plot_data_viewbox import PlotDataViewBox, coordinate_viewbox
12
+ from ._plot_drop_axes import plot_axis_entries
13
+
14
+ if TYPE_CHECKING:
15
+ from ._plot_axis_scaling import PlotAxisScalingMixin
16
+
17
+ class _MixedPlotBase(PlotAxisScalingMixin):
18
+ operation_kind: str
19
+
20
+ def apply_axis_major_tick_count_preference(self) -> None: ...
21
+ def _update_plot_annotation(self) -> None: ...
22
+ def _add_plot_overlay(self, item: Any, viewbox: Any = None) -> None: ...
23
+ def _marquee_viewbox(self) -> Any: ...
24
+ else:
25
+ class _MixedPlotBase:
26
+ pass
27
+
28
+
29
+ class MixedPlotMixin(Plot1DTraceMixin, Plot2DLayerMixin, Plot2DColorbarMixin, _MixedPlotBase):
30
+ def initMenu(self):
31
+ super().initMenu()
32
+ menu_bar = self.menuBar()
33
+ for menu_action in menu_bar.actions() if menu_bar is not None else ():
34
+ if menu_action.text().replace("&", "") == "View":
35
+ menu = menu_action.menu()
36
+ if menu is not None:
37
+ menu.addSeparator()
38
+ action = QtGui.QAction("Appearance…", self)
39
+ action.triggered.connect(lambda _checked: self.open_plot_appearance_dialog())
40
+ menu.addAction(action)
41
+ self.appearance_action = action
42
+ break
43
+
44
+ def open_plot_appearance_dialog(self, key=None, *, kind=None):
45
+ dialog = self.__dict__.get("_plot_appearance_dialog")
46
+ if dialog is None:
47
+ if "heatmaps" not in self.__dict__:
48
+ self._init_heatmap_layers()
49
+ dialog = PlotAppearanceDialog(self)
50
+ self._plot_appearance_dialog = dialog
51
+ self._trace_appearance_dialog = dialog
52
+ self._heatmap_appearance_dialog = dialog
53
+ self.get_mergables.emit()
54
+ dialog.refresh_rows()
55
+ if key is not None:
56
+ if kind == "heatmap":
57
+ dialog.select_heatmap(key)
58
+ else:
59
+ dialog.select_trace(key)
60
+ dialog.show()
61
+ dialog.raise_()
62
+ dialog.activateWindow()
63
+
64
+ def open_trace_appearance_dialog(self, trace_key=None):
65
+ self.open_plot_appearance_dialog(trace_key, kind="trace")
66
+
67
+ def open_heatmap_appearance_dialog(self, heatmap_key=None):
68
+ self.open_plot_appearance_dialog(heatmap_key, kind="heatmap")
69
+
70
+ def add_line(self, label, trace_key=None, *, axis_plan=None):
71
+ super().add_line(label, trace_key, axis_plan=axis_plan)
72
+ self._sync_plot_item_order()
73
+ dialog = self.__dict__.get("_plot_appearance_dialog")
74
+ if dialog is not None:
75
+ dialog.refresh_rows()
76
+
77
+ def remove_line(self, label, trace_key=None):
78
+ key = trace_key if trace_key is not None else label
79
+ line = self.__dict__.get("lines", {}).get(key)
80
+ if line is self.__dict__.get("line") and line is not None:
81
+ self.remove_plot_items([("trace", key)])
82
+ return
83
+ if line is not None:
84
+ self._restore_data_coordinate_parent(line)
85
+ super().remove_line(label, trace_key)
86
+ self._sync_plot_item_order()
87
+ self._sync_mixed_axis_labels()
88
+
89
+ def remove_heatmap(self, label="", trace_key=None):
90
+ key = trace_key
91
+ if key is None and label == self.__dict__.get("label"):
92
+ key = self.__dict__.get("_primary_heatmap_key")
93
+ if self.__dict__.get("heatmaps", {}).get(key) is self:
94
+ return self.remove_plot_items([("heatmap", key)]) is not self
95
+ return super().remove_heatmap(label, trace_key)
96
+
97
+ def remove_plot_items(self, selected):
98
+ self._sync_plot_item_order()
99
+ entries = set(self._plot_item_order)
100
+ if not set(selected) or not set(selected) < entries:
101
+ return self
102
+ removal_error = self.plot_item_removal_error(selected)
103
+ if removal_error:
104
+ self.show_status(removal_error, 5000)
105
+ return self
106
+ primary_selected = any(
107
+ (self.lines.get(key) is self.__dict__.get("line") if kind == "trace"
108
+ else self.heatmaps.get(key) is self)
109
+ for kind, key in selected
110
+ )
111
+ if primary_selected:
112
+ return self.__dict__["_plot_source_replacement_request"](selected)
113
+ for kind, key in selected:
114
+ if kind == "trace":
115
+ self.remove_line(self._trace_display_label(key, self.lines[key]), key)
116
+ else:
117
+ self.remove_heatmap(trace_key=key)
118
+ return self
119
+
120
+ def plot_item_removal_error(self, selected):
121
+ """Cuts may remain overlays, but cannot own a mixed plot's controls."""
122
+ selected = set(selected)
123
+ primary_selected = any(
124
+ (self.lines.get(key) is self.__dict__.get("line") if kind == "trace"
125
+ else self.heatmaps.get(key) is self)
126
+ for kind, key in selected
127
+ )
128
+ if primary_selected:
129
+ for kind, key in self._plot_item_order:
130
+ if (kind, key) in selected:
131
+ continue
132
+ item = self.lines[key] if kind == "trace" else self.heatmaps[key]
133
+ source = item.from_win
134
+ if source.operation_kind in {"plot1d", "plot2d"}:
135
+ return None
136
+ return "Keep a full line or heatmap dataset in this plot; cuts cannot be its primary source."
137
+ return None
138
+
139
+ def _add_plot_overlay(self, item, viewbox=None):
140
+ if viewbox is None:
141
+ viewbox = self._marquee_viewbox()
142
+ super()._add_plot_overlay(item, coordinate_viewbox(viewbox))
143
+
144
+ def _sync_plot_item_order(self):
145
+ lines = self.__dict__.get("lines", {})
146
+ heatmaps = self.__dict__.get("heatmaps", {})
147
+ existing = [("trace", key) for key in lines] + [("heatmap", key) for key in heatmaps]
148
+ order = [entry for entry in self.__dict__.get("_plot_item_order", []) if entry in existing]
149
+ for entry in existing:
150
+ if entry in order:
151
+ continue
152
+ order.insert(0, entry)
153
+ self._plot_item_order = order
154
+ styles = self._ensure_trace_styles()
155
+ renderers = {}
156
+ for index, (kind, key) in enumerate(order):
157
+ z = len(order) - index - 1
158
+ if kind == "trace":
159
+ styles.setdefault(key, self._initial_trace_style()).order = z
160
+ line = lines[key]
161
+ set_z = getattr(line, "setZValue", None)
162
+ if callable(set_z):
163
+ set_z(z)
164
+ if "vb" in self.__dict__ and line is not None:
165
+ renderers[(kind, key)] = (self._trace_axis_viewbox(styles[key]), [line])
166
+ else:
167
+ layer = heatmaps[key]
168
+ items = self._heatmap_render_items(layer)
169
+ for item in items:
170
+ item.setZValue(z)
171
+ renderers[(kind, key)] = (self._heatmap_axis_viewbox(layer), items)
172
+ self._sync_data_drawing_layers(renderers)
173
+ self._update_plot_annotation()
174
+
175
+ def _sync_data_drawing_layers(self, renderers):
176
+ layers = self.__dict__.get("_plot_data_viewboxes", {})
177
+ # A single axis pair already has a common graphics parent. Allocate
178
+ # drawing layers only when independent axis pairs need to interleave.
179
+ if not layers and len({owner for owner, _items in renderers.values()}) < 2:
180
+ return
181
+ self._plot_data_viewboxes = layers
182
+ for entry in list(layers):
183
+ if entry not in renderers or layers[entry].coordinate_owner is not renderers[entry][0]:
184
+ layer = layers.pop(entry)
185
+ # Axis reassignment may already have moved these items. Keep
186
+ # any remaining renderers alive while replacing their layer.
187
+ for item in list(layer.childGroup.childItems()):
188
+ item.setParentItem(layer.coordinate_owner.childGroup)
189
+ layer.detach()
190
+ count = len(renderers)
191
+ for index, (entry, (owner, items)) in enumerate(renderers.items()):
192
+ layer = layers.get(entry)
193
+ if layer is None:
194
+ layer = layers[entry] = PlotDataViewBox(self.vb, owner)
195
+ layer.sync_view()
196
+ # Negative Z keeps cursor/marquee decorations in the primary
197
+ # childGroup above every data layer, regardless of item count.
198
+ layer.setZValue(-1 + (count - index) / (count + 1))
199
+ for item in items:
200
+ if item.parentItem() is not layer.childGroup:
201
+ item.setParentItem(layer.childGroup)
202
+
203
+ @staticmethod
204
+ def _restore_data_coordinate_parent(item):
205
+ get_viewbox = getattr(item, "getViewBox", None)
206
+ current = get_viewbox() if callable(get_viewbox) else None
207
+ if isinstance(current, PlotDataViewBox):
208
+ item.setParentItem(current.coordinate_owner.childGroup)
209
+
210
+ def _move_trace_to_axis_viewbox(self, line, target):
211
+ current = line.getViewBox()
212
+ if coordinate_viewbox(current) is target:
213
+ return
214
+ self._restore_data_coordinate_parent(line)
215
+ super()._move_trace_to_axis_viewbox(line, target)
216
+
217
+ def _move_heatmap_item_to_viewbox(self, item, target):
218
+ get_viewbox = getattr(item, "getViewBox", None)
219
+ current = get_viewbox() if callable(get_viewbox) else None
220
+ if coordinate_viewbox(current) is target:
221
+ return
222
+ self._restore_data_coordinate_parent(item)
223
+ super()._move_heatmap_item_to_viewbox(item, target)
224
+
225
+ def _remove_heatmap_render_item(self, item):
226
+ self._restore_data_coordinate_parent(item)
227
+ super()._remove_heatmap_render_item(item)
228
+
229
+ def _init_heatmap_layers(self):
230
+ super()._init_heatmap_layers()
231
+ self._sync_plot_item_order()
232
+
233
+ def _sync_heatmap_layer_order(self):
234
+ super()._sync_heatmap_layer_order()
235
+ self._sync_plot_item_order()
236
+
237
+ def _set_layer_visibility(self, layer, visible):
238
+ super()._set_layer_visibility(layer, visible)
239
+ self._update_plot_annotation()
240
+
241
+ def _set_layer_opacity(self, layer, opacity):
242
+ super()._set_layer_opacity(layer, opacity)
243
+ self._update_plot_annotation()
244
+
245
+ def can_swap_plot_axes(self):
246
+ mixin = Plot1DTraceMixin if self.operation_kind == "plot1d" else Plot2DLayerMixin
247
+ return mixin.can_swap_plot_axes(self)
248
+
249
+ def plot_axes_swapped(self):
250
+ mixin = Plot1DTraceMixin if self.operation_kind == "plot1d" else Plot2DLayerMixin
251
+ return mixin.plot_axes_swapped(self)
252
+
253
+ def set_plot_axes_swapped(self, swapped):
254
+ before = self.axis_options
255
+ mixin = Plot1DTraceMixin if self.operation_kind == "plot1d" else Plot2DLayerMixin
256
+ result = mixin.set_plot_axes_swapped(self, swapped)
257
+ if result and before != self.axis_options:
258
+ if self.operation_kind == "plot1d":
259
+ Plot2DLayerMixin._transpose_heatmap_axis_assignments(self)
260
+ else:
261
+ Plot1DTraceMixin._transpose_trace_axis_assignments(self)
262
+ self.refresh_secondary_lines()
263
+ self.refresh_secondary_heatmaps()
264
+ return result
265
+
266
+ def _sync_mixed_axis_labels(self):
267
+ self._update_plot_annotation()
268
+ plot = self.__dict__.get("plot")
269
+ if plot is None:
270
+ return
271
+ if self.operation_kind == "plot1d" and not self.__dict__.get("heatmaps"):
272
+ Plot1DTraceMixin._sync_trace_axis_labels(self)
273
+ self._sync_unused_plot_axes()
274
+ self._refresh_appearance_axis_warnings()
275
+ return
276
+ parameters: dict[str, Any] = {}
277
+ for side, _identity, parameter in plot_axis_entries(self):
278
+ parameters.setdefault(side, parameter)
279
+ for side in ("Bottom", "Left", "Top", "Right"):
280
+ parameter = parameters.get(side)
281
+ axis = plot.getAxis(side.lower())
282
+ axis.setStyle(showValues=side in parameters)
283
+ label = getattr(parameter, "label", None) or getattr(parameter, "name", "")
284
+ axis.setLabel(text=str(label), units=str(getattr(parameter, "unit", "") or ""))
285
+ self._sync_unused_plot_axes()
286
+ sync_tabs = getattr(self, "_sync_axis_scale_tab_states", None)
287
+ if callable(sync_tabs):
288
+ sync_tabs()
289
+ self._refresh_appearance_axis_warnings()
290
+
291
+ def _sync_unused_plot_axes(self):
292
+ """Keep unassigned axes linked to the opposite assigned coordinate range."""
293
+ plot = self.__dict__.get("plot")
294
+ primary = self.__dict__.get("vb")
295
+ if plot is None or primary is None:
296
+ return
297
+ occupied = {side for side, _identity, _parameter in plot_axis_entries(self)}
298
+ owners = {
299
+ "Bottom": primary,
300
+ "Left": primary,
301
+ "Top": self.__dict__.get("top_vb") or primary,
302
+ "Right": self.__dict__.get("right_vb") or primary,
303
+ }
304
+ for side, opposite in (("Bottom", "Top"), ("Top", "Bottom"),
305
+ ("Left", "Right"), ("Right", "Left")):
306
+ axis = plot.getAxis(side.lower())
307
+ owner = owners[side] if side in occupied else owners[opposite]
308
+ if axis.linkedView() is not owner:
309
+ axis.linkToView(owner)
310
+ axis.linkedViewChanged(owner)
311
+ axis.setStyle(showValues=side in occupied)
312
+ if side not in occupied:
313
+ axis.setLabel(text="", units="")
314
+
315
+ def _refresh_appearance_axis_warnings(self):
316
+ dialog = self.__dict__.get("_plot_appearance_dialog")
317
+ if dialog is not None:
318
+ dialog.refresh_axis_warnings()
319
+
320
+ def _sync_trace_axis_labels(self):
321
+ self._sync_mixed_axis_labels()
322
+
323
+ def _sync_heatmap_axis_visibility(self):
324
+ self._sync_mixed_axis_labels()
325
+
326
+ def _sync_vertical_axis_visibility(self, side):
327
+ if self.operation_kind == "plot1d" and not self.__dict__.get("heatmaps"):
328
+ Plot1DTraceMixin._sync_vertical_axis_visibility(self, side)
329
+ self._sync_unused_plot_axes()
330
+ else:
331
+ self._sync_mixed_axis_labels()
332
+
333
+ def _sync_top_axis_visibility(self):
334
+ if self.operation_kind == "plot1d" and not self.__dict__.get("heatmaps"):
335
+ Plot1DTraceMixin._sync_top_axis_visibility(self)
336
+ self._sync_unused_plot_axes()
337
+ else:
338
+ self._sync_mixed_axis_labels()
339
+
340
+ def _apply_trace_style(self, key, line):
341
+ super()._apply_trace_style(key, line)
342
+ self._sync_plot_item_order()
343
+
344
+ def _ensure_trace_axis_viewboxes(self, *, top=False, right=False):
345
+ if self.operation_kind == "plot2d":
346
+ Plot2DLayerMixin._ensure_heatmap_axis_viewboxes(self, top=top, right=right)
347
+ else:
348
+ Plot1DTraceMixin._ensure_trace_axis_viewboxes(self, top=top, right=right)
349
+
350
+ def _ensure_heatmap_axis_viewboxes(self, *, top=False, right=False):
351
+ self._ensure_trace_axis_viewboxes(top=top, right=right)
352
+ if self.operation_kind == "plot1d":
353
+ for name in ("right_vb", "top_vb", "top_right_vb"):
354
+ viewbox = self.__dict__.get(name)
355
+ if viewbox is not None and not getattr(viewbox, "_qplot_heatmap_range_connected", False):
356
+ viewbox.sigRangeChanged.connect(self._heatmap_overlay_range_changed)
357
+ viewbox._qplot_heatmap_range_connected = True
358
+
359
+ def _axis_scale_bound_item_groups(self, axis):
360
+ groups: dict[Any, list[Any]] = {}
361
+ for viewbox, items in super()._axis_scale_bound_item_groups(axis):
362
+ if isinstance(viewbox, PlotDataViewBox):
363
+ viewbox.sync_view()
364
+ if items is not None:
365
+ groups.setdefault(viewbox, []).extend(items)
366
+ dimension = self._axis_scale_dimension(axis)
367
+ side = {"x": "Bottom", "x2": "Top", "y": "Left", "y2": "Right"}[axis]
368
+ # The base implementation chooses traces when that registry exists.
369
+ # Include heatmaps as well so axis autoscaling sees all assigned data.
370
+ if isinstance(self.__dict__.get("lines"), dict):
371
+ for layer in self.__dict__.get("heatmaps", {}).values():
372
+ x_side, y_side = self._heatmap_axis_sides(layer)
373
+ if (x_side if dimension == "x" else y_side) == side:
374
+ for item in self._heatmap_render_items(layer):
375
+ viewbox = item.getViewBox() or self._heatmap_axis_viewbox(layer)
376
+ if isinstance(viewbox, PlotDataViewBox):
377
+ viewbox.sync_view()
378
+ groups.setdefault(viewbox, []).append(item)
379
+ return list(groups.items())
380
+
381
+ def _axis_scale_axis_is_used(self, axis):
382
+ if super()._axis_scale_axis_is_used(axis):
383
+ return True
384
+ dimension = "x" if axis in ("x", "x2") else "y"
385
+ side = {"x": "Bottom", "x2": "Top", "y": "Left", "y2": "Right"}[axis]
386
+ return any(assignment.get(dimension) == side for assignment in
387
+ self.__dict__.get("_heatmap_axis_assignments", {}).values())
388
+
389
+ def _apply_heatmap_axis_assignment(self, layer, *, auto_range=True):
390
+ super()._apply_heatmap_axis_assignment(layer, auto_range=auto_range)
391
+ x_side, y_side = self._heatmap_axis_sides(layer)
392
+ for axis in ("x2" if x_side == "Top" else "x", "y2" if y_side == "Right" else "y"):
393
+ # Heatmap geometry uses physical coordinates. A shared axis must
394
+ # stay linear, including when a heatmap is added to a log plot.
395
+ if self._axis_scale_log_mode(axis):
396
+ self._axis_scale_log_toggled(axis, False)
397
+ self._sync_plot_item_order()
398
+
399
+ def _colorbar_display_parameter(self):
400
+ if self.operation_kind == "plot2d":
401
+ return self.__dict__.get("display_param", self.__dict__.get("param"))
402
+ layer = next(iter(self.__dict__.get("heatmaps", {}).values()), None)
403
+ if layer is None:
404
+ return None
405
+ source = layer.from_win
406
+ return getattr(source, "display_param", None) or source.param
407
+
408
+ def _sync_heatmap_colorbar_items(self, *, rescale=False):
409
+ self._update_plot_annotation()
410
+ if self.operation_kind == "plot1d":
411
+ layers = self.__dict__.get("heatmaps", {})
412
+ if not layers:
413
+ bar = self.__dict__.get("bar")
414
+ if bar is not None:
415
+ bar.hide()
416
+ return
417
+ if "bar" not in self.__dict__ and self._data_colorbar_levels() is not None:
418
+ self._colorbar_manual_levels = None
419
+ self._init_colorbar_scale_controls()
420
+ items = self._heatmap_colorbar_items()
421
+ self.bar = self.plot.addColorBar(
422
+ items, colorMap=self._colorbar_colormap(),
423
+ rounding=self._data_colorbar_rounding(), width=self._configured_colorbar_width(),
424
+ colorMapMenu=False,
425
+ )
426
+ self._heatmap_colorbar_item_ids = tuple(id(item) for item in items)
427
+ self._set_colorbar_tick_formatter()
428
+ for key, line in self.__dict__.get("lines", {}).items():
429
+ self._apply_trace_style(key, line)
430
+ self.config.theme.style_plotItem(self)
431
+ self.apply_axis_major_tick_count_preference()
432
+ action = QtGui.QAction("Color scale…", self)
433
+ action.triggered.connect(self.open_colorbar_scale_dialog)
434
+ menu_bar = self.menuBar()
435
+ for menu_action in menu_bar.actions() if menu_bar is not None else ():
436
+ if menu_action.text().replace("&", "") == "View":
437
+ menu = menu_action.menu()
438
+ if menu is not None:
439
+ menu.addAction(action)
440
+ break
441
+ bar = self.__dict__.get("bar")
442
+ if bar is not None:
443
+ bar.show()
444
+ super()._sync_heatmap_colorbar_items(rescale=rescale)
445
+ self._refresh_appearance_axis_warnings()
446
+
447
+ def _reset_heatmap_hover(self):
448
+ # A line-primary plot has no heatmap hover/cut decorations.
449
+ pass
450
+
451
+ def _set_sweep_lines_visible(self, visible):
452
+ pass
453
+
454
+ def closeEvent(self, event: Any):
455
+ self.close_secondary_heatmaps()
456
+ super().closeEvent(event)
457
+ for layer in self.__dict__.get("_plot_data_viewboxes", {}).values():
458
+ for item in list(layer.childGroup.childItems()):
459
+ item.setParentItem(layer.coordinate_owner.childGroup)
460
+ layer.detach()
461
+ self._plot_data_viewboxes = {}