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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +67 -0
- qplot/_build_info.json +1 -0
- qplot/_metadata.py +75 -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 +578 -0
- qplot/configuration/config_schema.json +357 -0
- qplot/configuration/fonts.py +3 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +592 -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 +2340 -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 +2566 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +148 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +876 -0
- qplot/datahandling/trusted_heatmap.py +246 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1919 -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 +1804 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +123 -0
- qplot/tools/general.py +139 -0
- qplot/tools/heatmap_geometry.py +391 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +650 -0
- qplot/tools/sample_statistics.py +111 -0
- qplot/tools/worker.py +2838 -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 +4933 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_display_precision.py +48 -0
- qplot/windows/_dragdrop.py +70 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +210 -0
- qplot/windows/_mixed_appearance.py +303 -0
- qplot/windows/_mixed_plots.py +461 -0
- qplot/windows/_native_averaging.py +323 -0
- qplot/windows/_native_transforms.py +551 -0
- qplot/windows/_plot1d_snap.py +612 -0
- qplot/windows/_plot1d_traces.py +2236 -0
- qplot/windows/_plot2d_colorbar.py +1127 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2291 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +2082 -0
- qplot/windows/_plot_actions.py +2703 -0
- qplot/windows/_plot_annotation.py +186 -0
- qplot/windows/_plot_appearance.py +283 -0
- qplot/windows/_plot_axis_scaling.py +1521 -0
- qplot/windows/_plot_data_viewbox.py +94 -0
- qplot/windows/_plot_drop_axes.py +224 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +112 -0
- qplot/windows/_plot_marquee.py +659 -0
- qplot/windows/_plot_refresh.py +950 -0
- qplot/windows/_plot_source_promotion.py +178 -0
- qplot/windows/_plot_state.py +237 -0
- qplot/windows/_plot_status.py +115 -0
- qplot/windows/_plot_tiling.py +35 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +724 -0
- qplot/windows/_refresh_interval.py +172 -0
- qplot/windows/_run_controls.py +789 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +532 -0
- qplot/windows/_subplots/subplot2d.py +919 -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 +414 -0
- qplot/windows/main.py +1632 -0
- qplot/windows/plot1d.py +313 -0
- qplot/windows/plot2d.py +1935 -0
- qplotter-1.6.0.dist-info/METADATA +322 -0
- qplotter-1.6.0.dist-info/RECORD +130 -0
- qplotter-1.6.0.dist-info/WHEEL +5 -0
- qplotter-1.6.0.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
- 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 = {}
|