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,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