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,94 @@
|
|
|
1
|
+
"""Drawing layers that share stacking order across independent axis pairs."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pyqtgraph as pg
|
|
6
|
+
from PyQt6 import QtCore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlotDataViewBox(pg.ViewBox):
|
|
10
|
+
"""Mirror an axis ViewBox while drawing one dataset among sibling layers.
|
|
11
|
+
|
|
12
|
+
Qt stacks an entire parent's descendants together. Renderer Z values alone
|
|
13
|
+
therefore cannot interleave datasets in different axis ViewBoxes. These
|
|
14
|
+
transparent, noninteractive siblings provide a common stacking parent.
|
|
15
|
+
Items stay registered with their axis owner for native automatic bounds.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, primary: Any, coordinate_owner: Any):
|
|
19
|
+
self.coordinate_owner = coordinate_owner
|
|
20
|
+
self.primary = primary
|
|
21
|
+
self._mirroring_view = True
|
|
22
|
+
super().__init__(enableMouse=False, enableMenu=False)
|
|
23
|
+
self.setParentItem(primary)
|
|
24
|
+
self.setAcceptedMouseButtons(QtCore.Qt.MouseButton.NoButton)
|
|
25
|
+
self.setFlag(self.GraphicsItemFlag.ItemIsFocusable, False)
|
|
26
|
+
self.disableAutoRange()
|
|
27
|
+
coordinate_owner.sigRangeChanged.connect(self.sync_view)
|
|
28
|
+
coordinate_owner.sigStateChanged.connect(self.sync_view)
|
|
29
|
+
coordinate_owner.sigResized.connect(self.sync_view)
|
|
30
|
+
if primary is not coordinate_owner:
|
|
31
|
+
primary.sigResized.connect(self.sync_view)
|
|
32
|
+
self._mirroring_view = False
|
|
33
|
+
self.sync_view()
|
|
34
|
+
|
|
35
|
+
def sync_view(self, *_args):
|
|
36
|
+
if self._mirroring_view:
|
|
37
|
+
return
|
|
38
|
+
self._mirroring_view = True
|
|
39
|
+
try:
|
|
40
|
+
self._sync_coordinate_view()
|
|
41
|
+
finally:
|
|
42
|
+
self._mirroring_view = False
|
|
43
|
+
|
|
44
|
+
def _sync_coordinate_view(self):
|
|
45
|
+
owner = self.coordinate_owner
|
|
46
|
+
self.setGeometry(self.primary.mapRectFromScene(owner.mapRectToScene(owner.rect())))
|
|
47
|
+
state = owner.getState(copy=False)
|
|
48
|
+
self.invertX(state["xInverted"])
|
|
49
|
+
self.invertY(state["yInverted"])
|
|
50
|
+
ranges = owner.viewRange()
|
|
51
|
+
self.setRange(xRange=ranges[0], yRange=ranges[1], padding=0, disableAutoRange=True)
|
|
52
|
+
# Bounds can be measured before the next paint. Keep both transforms
|
|
53
|
+
# current so native mapping between the two groups is an identity.
|
|
54
|
+
owner.updateMatrix()
|
|
55
|
+
self.updateMatrix()
|
|
56
|
+
|
|
57
|
+
def setRange(self, *args, **kwargs):
|
|
58
|
+
if self._mirroring_view:
|
|
59
|
+
return super().setRange(*args, **kwargs)
|
|
60
|
+
return self.coordinate_owner.setRange(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
def invertX(self, inverted=True):
|
|
63
|
+
if self._mirroring_view:
|
|
64
|
+
return super().invertX(inverted)
|
|
65
|
+
return self.coordinate_owner.invertX(inverted)
|
|
66
|
+
|
|
67
|
+
def invertY(self, inverted=True):
|
|
68
|
+
if self._mirroring_view:
|
|
69
|
+
return super().invertY(inverted)
|
|
70
|
+
return self.coordinate_owner.invertY(inverted)
|
|
71
|
+
|
|
72
|
+
def itemBoundsChanged(self, item):
|
|
73
|
+
self.coordinate_owner.itemBoundsChanged(item)
|
|
74
|
+
|
|
75
|
+
def removeItem(self, item):
|
|
76
|
+
self.coordinate_owner.removeItem(item)
|
|
77
|
+
|
|
78
|
+
def detach(self):
|
|
79
|
+
owner = self.coordinate_owner
|
|
80
|
+
owner.sigRangeChanged.disconnect(self.sync_view)
|
|
81
|
+
owner.sigStateChanged.disconnect(self.sync_view)
|
|
82
|
+
owner.sigResized.disconnect(self.sync_view)
|
|
83
|
+
if self.primary is not owner:
|
|
84
|
+
self.primary.sigResized.disconnect(self.sync_view)
|
|
85
|
+
self.setParentItem(None)
|
|
86
|
+
scene = self.scene()
|
|
87
|
+
if scene is not None:
|
|
88
|
+
scene.removeItem(self)
|
|
89
|
+
self.close()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def coordinate_viewbox(viewbox: Any) -> Any:
|
|
93
|
+
"""Return the axis owner of a drawing layer, or an ordinary ViewBox."""
|
|
94
|
+
return viewbox.coordinate_owner if isinstance(viewbox, PlotDataViewBox) else viewbox
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Choose physical axes for a dropped dataset before mutating the plot."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from PyQt6 import QtWidgets as qtw
|
|
7
|
+
|
|
8
|
+
from ._config_persistence import persist_config_value
|
|
9
|
+
from ._plot_transform_labels import native_axis_quantities
|
|
10
|
+
from ._window_controls import CONFIRM_INCOMPATIBLE_AXES_KEY
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def displayed_axis_parameter(window: Any, axis: str) -> Any:
|
|
14
|
+
parameters = getattr(window, "axis_param", {})
|
|
15
|
+
parameter = parameters.get(axis)
|
|
16
|
+
if parameter is not None:
|
|
17
|
+
return parameter
|
|
18
|
+
name = window.axis_options.get(axis)
|
|
19
|
+
if name == getattr(window.param, "name", None):
|
|
20
|
+
return getattr(window, "display_param", None) or window.param
|
|
21
|
+
return getattr(window, "param_dict", {}).get(name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _axis_identity(window: Any, axis: str, parameter: Any) -> tuple[Any, ...]:
|
|
25
|
+
"""Coordinates match by name and unit; measured values match by unit."""
|
|
26
|
+
unit = getattr(parameter, "unit", None)
|
|
27
|
+
name = window.axis_options.get(axis)
|
|
28
|
+
independent = name in getattr(window.param, "depends_on_", ())
|
|
29
|
+
return (
|
|
30
|
+
name,
|
|
31
|
+
None if unit is None else str(unit or "").strip(),
|
|
32
|
+
independent,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _axes_match(existing, incoming):
|
|
37
|
+
return (existing is not None and incoming[1] is not None
|
|
38
|
+
and existing[1] == incoming[1]
|
|
39
|
+
and (existing[0] == incoming[0] or not (existing[2] or incoming[2])))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def dataset_axis_entries(target: Any, kind: str, key: Any):
|
|
43
|
+
"""Yield the axes currently assigned to one dataset."""
|
|
44
|
+
if kind == "trace":
|
|
45
|
+
trace = target.lines[key]
|
|
46
|
+
style = target._ensure_trace_styles()[key]
|
|
47
|
+
source = target if trace is target.__dict__.get("line") else trace.from_win
|
|
48
|
+
order = getattr(trace, "choose_from", None) or ("x", "y")
|
|
49
|
+
for axis, side, source_axis in zip(
|
|
50
|
+
("x", "y"), (style.x_axis, style.y_axis), order, strict=True,
|
|
51
|
+
):
|
|
52
|
+
parameter = target._trace_axis_parameter(trace, axis)
|
|
53
|
+
yield side, _axis_identity(source, source_axis, parameter), parameter
|
|
54
|
+
else:
|
|
55
|
+
layer = target.heatmaps[key]
|
|
56
|
+
source = target if layer is target else layer.from_win
|
|
57
|
+
order = ("x", "y") if layer is target else layer.source_axis_order()
|
|
58
|
+
if order is not None:
|
|
59
|
+
for side, source_axis in zip(target._heatmap_axis_sides(layer), order, strict=True):
|
|
60
|
+
parameter = displayed_axis_parameter(source, source_axis)
|
|
61
|
+
yield side, _axis_identity(source, source_axis, parameter), parameter
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def plot_axis_entries(target: Any):
|
|
65
|
+
"""Yield each dataset's physical axis, identity and displayed metadata."""
|
|
66
|
+
def line_entries():
|
|
67
|
+
for key in target.__dict__.get("lines", {}):
|
|
68
|
+
yield from dataset_axis_entries(target, "trace", key)
|
|
69
|
+
|
|
70
|
+
def heatmap_entries():
|
|
71
|
+
for key in target.__dict__.get("heatmaps", {}):
|
|
72
|
+
yield from dataset_axis_entries(target, "heatmap", key)
|
|
73
|
+
|
|
74
|
+
# Follow the same renderer priority and registry order as the axis labels.
|
|
75
|
+
generators = (line_entries, heatmap_entries) if target.operation_kind == "plot1d" else (
|
|
76
|
+
heatmap_entries, line_entries,
|
|
77
|
+
)
|
|
78
|
+
for entries in generators:
|
|
79
|
+
yield from entries()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def dataset_axis_warnings(target: Any, kind: str, key: Any) -> tuple[str, ...]:
|
|
83
|
+
"""Explain mismatches with the quantities labelling the assigned plot axes."""
|
|
84
|
+
references: dict[str, tuple[tuple[Any, ...], Any]] = {}
|
|
85
|
+
for side, identity, parameter in plot_axis_entries(target):
|
|
86
|
+
references.setdefault(side, (identity, parameter))
|
|
87
|
+
|
|
88
|
+
def quantity(parameter):
|
|
89
|
+
name = getattr(parameter, "label", None) or getattr(parameter, "name", None) or "Unknown quantity"
|
|
90
|
+
unit = getattr(parameter, "unit", None)
|
|
91
|
+
return f"{name} ({'unknown units' if unit is None else str(unit).strip() or 'unitless'})"
|
|
92
|
+
|
|
93
|
+
reasons = []
|
|
94
|
+
for side, identity, parameter in dataset_axis_entries(target, kind, key):
|
|
95
|
+
reference, plot_parameter = references[side]
|
|
96
|
+
if not _axes_match(reference, identity):
|
|
97
|
+
reason = f"{side} axis: dataset {quantity(parameter)} does not match plot {quantity(plot_parameter)}."
|
|
98
|
+
if identity[0] != reference[0] and (identity[2] or reference[2]):
|
|
99
|
+
reason += f" Coordinate names differ (dataset: {identity[0]}; plot: {reference[0]})."
|
|
100
|
+
reasons.append(reason)
|
|
101
|
+
if kind == "heatmap":
|
|
102
|
+
layer = target.heatmaps[key]
|
|
103
|
+
source = target if layer is target else layer.from_win
|
|
104
|
+
parameter = getattr(source, "display_param", None) or source.param
|
|
105
|
+
plot_parameter = target._colorbar_display_parameter()
|
|
106
|
+
unit = getattr(parameter, "unit", None)
|
|
107
|
+
plot_unit = getattr(plot_parameter, "unit", None)
|
|
108
|
+
if unit is None or plot_unit is None or str(unit).strip() != str(plot_unit).strip():
|
|
109
|
+
reasons.append(f"Colorscale: dataset {quantity(parameter)} does not match plot {quantity(plot_parameter)}.")
|
|
110
|
+
return tuple(reasons)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _occupied_axes(target: Any) -> dict[str, tuple[Any, ...]]:
|
|
114
|
+
occupied: dict[str, tuple[Any, ...]] = {}
|
|
115
|
+
for side, identity, _parameter in plot_axis_entries(target):
|
|
116
|
+
occupied.setdefault(side, identity)
|
|
117
|
+
return occupied
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class DropAxisPlan:
|
|
122
|
+
axis_order: tuple[str, str]
|
|
123
|
+
x_side: str
|
|
124
|
+
y_side: str
|
|
125
|
+
incompatible: tuple[str, ...]
|
|
126
|
+
|
|
127
|
+
def approve(self, target: Any, source: Any) -> bool:
|
|
128
|
+
if not self.incompatible or not target.config.get(CONFIRM_INCOMPATIBLE_AXES_KEY):
|
|
129
|
+
return True
|
|
130
|
+
reasons = "\n".join(f"• {reason}" for reason in self.incompatible)
|
|
131
|
+
box = qtw.QMessageBox(
|
|
132
|
+
qtw.QMessageBox.Icon.Warning,
|
|
133
|
+
"Incompatible axes",
|
|
134
|
+
f"The axes are incompatible with {source.label}:\n\n{reasons}\n\n"
|
|
135
|
+
f"Add the dataset using the {self.x_side.lower()} and "
|
|
136
|
+
f"{self.y_side.lower()} axes?",
|
|
137
|
+
qtw.QMessageBox.StandardButton.Ok | qtw.QMessageBox.StandardButton.Cancel,
|
|
138
|
+
target,
|
|
139
|
+
)
|
|
140
|
+
box.setDefaultButton(qtw.QMessageBox.StandardButton.Cancel)
|
|
141
|
+
do_not_show_again = qtw.QCheckBox("Do not show again", box)
|
|
142
|
+
box.setCheckBox(do_not_show_again)
|
|
143
|
+
result = box.exec()
|
|
144
|
+
if result == qtw.QMessageBox.StandardButton.Ok and do_not_show_again.isChecked():
|
|
145
|
+
persist_config_value(
|
|
146
|
+
target, target.config, CONFIRM_INCOMPATIBLE_AXES_KEY, False,
|
|
147
|
+
"the incompatible-axis warning preference",
|
|
148
|
+
)
|
|
149
|
+
return result == qtw.QMessageBox.StandardButton.Ok
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def plan_drop_axes(target: Any, source: Any) -> DropAxisPlan:
|
|
153
|
+
occupied = _occupied_axes(target)
|
|
154
|
+
candidates = []
|
|
155
|
+
for order in (("x", "y"), ("y", "x")):
|
|
156
|
+
sides = []
|
|
157
|
+
incompatible = []
|
|
158
|
+
compatible_count = unused_count = 0
|
|
159
|
+
parameters = {
|
|
160
|
+
axis: displayed_axis_parameter(source, source_axis)
|
|
161
|
+
for axis, source_axis in zip(("x", "y"), order, strict=True)
|
|
162
|
+
}
|
|
163
|
+
if getattr(source, "operation_kind", None) == "plot1d" and target.__dict__.get("lines"):
|
|
164
|
+
# New traces inherit the host's native processing controls. Match
|
|
165
|
+
# their resulting quantities rather than the source's raw units.
|
|
166
|
+
parameters = native_axis_quantities(
|
|
167
|
+
parameters["x"], parameters["y"], next(iter(target.lines.values())).opts,
|
|
168
|
+
)
|
|
169
|
+
for axis, source_axis, choices in zip(
|
|
170
|
+
("x", "y"), order, (("Bottom", "Top"), ("Left", "Right")), strict=True,
|
|
171
|
+
):
|
|
172
|
+
parameter = parameters[axis]
|
|
173
|
+
identity = _axis_identity(source, source_axis, parameter)
|
|
174
|
+
matching = next((side for side in choices if _axes_match(occupied.get(side), identity)), None)
|
|
175
|
+
unused = next((side for side in choices if side not in occupied), None)
|
|
176
|
+
if matching is not None:
|
|
177
|
+
sides.append(matching)
|
|
178
|
+
compatible_count += 1
|
|
179
|
+
elif unused is not None:
|
|
180
|
+
sides.append(unused)
|
|
181
|
+
unused_count += 1
|
|
182
|
+
else:
|
|
183
|
+
sides.append(choices[0])
|
|
184
|
+
name = getattr(parameter, "label", None) or source.axis_options[source_axis]
|
|
185
|
+
incompatible.append(f"{name} does not match the {choices[0].lower()} axis.")
|
|
186
|
+
candidates.append((compatible_count, unused_count, DropAxisPlan(
|
|
187
|
+
order, sides[0], sides[1], tuple(incompatible),
|
|
188
|
+
)))
|
|
189
|
+
# Stable ties keep the source's existing orientation.
|
|
190
|
+
plan = max(candidates, key=lambda candidate: candidate[:2])[2]
|
|
191
|
+
heatmaps = target.__dict__.get("heatmaps", {})
|
|
192
|
+
if getattr(source, "operation_kind", None) == "plot2d" and heatmaps:
|
|
193
|
+
first = next(iter(heatmaps.values()))
|
|
194
|
+
color_source = target if first is target else first.from_win
|
|
195
|
+
target_parameter = getattr(color_source, "display_param", None) or color_source.param
|
|
196
|
+
source_parameter = getattr(source, "display_param", None) or source.param
|
|
197
|
+
target_unit = getattr(target_parameter, "unit", None)
|
|
198
|
+
source_unit = getattr(source_parameter, "unit", None)
|
|
199
|
+
if target_unit is None or source_unit is None or str(target_unit).strip() != str(source_unit).strip():
|
|
200
|
+
plan = DropAxisPlan(plan.axis_order, plan.x_side, plan.y_side, plan.incompatible + (
|
|
201
|
+
f"The colorscale units do not match (source: {source_unit or 'unitless'}; "
|
|
202
|
+
f"plot: {target_unit or 'unitless'}).",
|
|
203
|
+
))
|
|
204
|
+
return plan
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class DroppedAxisMapping:
|
|
208
|
+
"""Retain an approved mapping through source refreshes and X/Y swaps."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, target: Any, source: Any, order: tuple[str, str]):
|
|
211
|
+
self.target = target
|
|
212
|
+
self.source = source
|
|
213
|
+
self.target_names = tuple(target.axis_options[axis] for axis in ("x", "y"))
|
|
214
|
+
self.source_names = tuple(source.axis_options[axis] for axis in order)
|
|
215
|
+
|
|
216
|
+
def axis_order(self) -> tuple[str, str] | None:
|
|
217
|
+
options = self.source.axis_options
|
|
218
|
+
names = self.source_names
|
|
219
|
+
target_names = tuple(self.target.axis_options[axis] for axis in ("x", "y"))
|
|
220
|
+
if target_names == self.target_names[::-1]:
|
|
221
|
+
names = names[::-1]
|
|
222
|
+
axes = tuple(next((axis for axis in ("x", "y") if options[axis] == name), "")
|
|
223
|
+
for name in names)
|
|
224
|
+
return (axes[0], axes[1]) if all(axes) else None
|