qplotter 1.6.0b3__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 +63 -0
- qplot/_metadata.py +12 -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 +573 -0
- qplot/configuration/config_schema.json +319 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +579 -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 +2330 -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 +2561 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +127 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +868 -0
- qplot/datahandling/trusted_heatmap.py +241 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1894 -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 +1733 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +114 -0
- qplot/tools/general.py +134 -0
- qplot/tools/heatmap_geometry.py +381 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +625 -0
- qplot/tools/sample_statistics.py +56 -0
- qplot/tools/worker.py +2787 -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 +4904 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_dragdrop.py +71 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +169 -0
- qplot/windows/_native_averaging.py +173 -0
- qplot/windows/_native_transforms.py +389 -0
- qplot/windows/_plot1d_snap.py +608 -0
- qplot/windows/_plot1d_traces.py +2183 -0
- qplot/windows/_plot2d_colorbar.py +1126 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2197 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +1993 -0
- qplot/windows/_plot_actions.py +2695 -0
- qplot/windows/_plot_appearance.py +276 -0
- qplot/windows/_plot_axis_scaling.py +1485 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +110 -0
- qplot/windows/_plot_marquee.py +656 -0
- qplot/windows/_plot_refresh.py +942 -0
- qplot/windows/_plot_state.py +239 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +635 -0
- qplot/windows/_refresh_interval.py +70 -0
- qplot/windows/_run_controls.py +828 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +531 -0
- qplot/windows/_subplots/subplot2d.py +918 -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 +413 -0
- qplot/windows/main.py +1635 -0
- qplot/windows/plot1d.py +311 -0
- qplot/windows/plot2d.py +1901 -0
- qplotter-1.6.0b3.dist-info/METADATA +318 -0
- qplotter-1.6.0b3.dist-info/RECORD +119 -0
- qplotter-1.6.0b3.dist-info/WHEEL +5 -0
- qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""SVG export using Qt's SVG 1.1 paint engine, including native clipping.
|
|
2
|
+
|
|
3
|
+
PyQtGraph 0.14's SVG coordinate rewriter cannot read Qt 6.11's closed
|
|
4
|
+
paths. Keep Qt's geometry, transforms, clip paths and gradient definitions
|
|
5
|
+
instead of reconstructing the scene with that rewriter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import re
|
|
10
|
+
import xml.dom.minidom as xml
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from PyQt6 import QtCore, QtGui, QtSvg
|
|
14
|
+
from pyqtgraph.exporters import SVGExporter
|
|
15
|
+
|
|
16
|
+
_NUMBER = r"[-+]?(?:\d*\.\d+|\d+\.?\d*)(?:[eE][-+]?\d+)?"
|
|
17
|
+
_TOKEN = re.compile(rf"[A-Za-z]|{_NUMBER}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def svg_bytes(exporter: SVGExporter) -> bytes:
|
|
21
|
+
"""Render the selected scene rectangle with all SVG dialog options."""
|
|
22
|
+
width, height = (float(exporter.params[name]) for name in ("width", "height"))
|
|
23
|
+
if not all(math.isfinite(v) and 0 < v <= 2_147_483_647 for v in (width, height)):
|
|
24
|
+
raise ValueError("SVG dimensions must be finite, positive Qt device dimensions.")
|
|
25
|
+
source = QtCore.QRectF(exporter.getSourceRect())
|
|
26
|
+
if source.isEmpty():
|
|
27
|
+
raise ValueError("The selected SVG source rectangle is empty.")
|
|
28
|
+
target = QtCore.QRectF(0, 0, width, height)
|
|
29
|
+
data = QtCore.QByteArray()
|
|
30
|
+
buffer = QtCore.QBuffer(data)
|
|
31
|
+
if not buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly):
|
|
32
|
+
raise RuntimeError("Could not open the SVG rendering buffer.")
|
|
33
|
+
# SVG Tiny (the generator default) does not support clipping. SVG 1.1
|
|
34
|
+
# does, including ViewBoxes, secondary axes and image/colorbar items.
|
|
35
|
+
generator = QtSvg.QSvgGenerator(QtSvg.QSvgGenerator.SvgVersion.Svg11)
|
|
36
|
+
generator.setOutputDevice(buffer)
|
|
37
|
+
generator.setSize(QtCore.QSize(math.ceil(width), math.ceil(height)))
|
|
38
|
+
generator.setViewBox(target)
|
|
39
|
+
generator.setResolution(96) # one SVG user unit is one CSS pixel
|
|
40
|
+
generator.setTitle("qPlot plot")
|
|
41
|
+
generator.setDescription("Exported from qPlot")
|
|
42
|
+
painter = QtGui.QPainter()
|
|
43
|
+
try:
|
|
44
|
+
if not painter.begin(generator):
|
|
45
|
+
raise RuntimeError("Could not start the SVG painter.")
|
|
46
|
+
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
|
47
|
+
painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing)
|
|
48
|
+
painter.fillRect(target, exporter.params["background"])
|
|
49
|
+
try:
|
|
50
|
+
exporter.setExportMode(True, {
|
|
51
|
+
"painter": painter,
|
|
52
|
+
"antialias": True,
|
|
53
|
+
"background": exporter.params["background"],
|
|
54
|
+
"resolutionScale": width / exporter.getTargetRect().width(),
|
|
55
|
+
})
|
|
56
|
+
exporter.getScene().render(
|
|
57
|
+
painter, target, source,
|
|
58
|
+
QtCore.Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
59
|
+
)
|
|
60
|
+
finally:
|
|
61
|
+
exporter.setExportMode(False)
|
|
62
|
+
if not painter.end():
|
|
63
|
+
raise RuntimeError("Could not finish the SVG painter.")
|
|
64
|
+
finally:
|
|
65
|
+
if painter.isActive():
|
|
66
|
+
painter.end()
|
|
67
|
+
buffer.close()
|
|
68
|
+
result = bytes(data.data())
|
|
69
|
+
if exporter.params["scaling stroke"]:
|
|
70
|
+
result = _scaling_strokes(result)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_svg_stage(filename: str, exporter: SVGExporter) -> bool:
|
|
75
|
+
"""Write only the private staging path supplied by qPlot's transaction."""
|
|
76
|
+
data = svg_bytes(exporter)
|
|
77
|
+
with open(filename, "wb") as output:
|
|
78
|
+
output.write(data)
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _matrix(transform: QtGui.QTransform) -> str:
|
|
83
|
+
return "matrix(" + ",".join(format(v, ".17g") for v in (
|
|
84
|
+
transform.m11(), transform.m12(), transform.m21(), transform.m22(),
|
|
85
|
+
transform.dx(), transform.dy(),
|
|
86
|
+
)) + ")"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _transform(value: str) -> QtGui.QTransform:
|
|
90
|
+
if not value:
|
|
91
|
+
return QtGui.QTransform()
|
|
92
|
+
# Qt emits only matrix transforms. Reject unexpected output rather than
|
|
93
|
+
# publishing geometry with a partially interpreted transform.
|
|
94
|
+
match = re.fullmatch(r"matrix\(([^()]*)\)", value.strip())
|
|
95
|
+
if match is None:
|
|
96
|
+
raise ValueError("Unexpected Qt SVG transform.")
|
|
97
|
+
values = [float(v) for v in re.split(r"[\s,]+", match[1].strip())]
|
|
98
|
+
if len(values) != 6 or not all(map(math.isfinite, values)):
|
|
99
|
+
raise ValueError("Invalid Qt SVG transform.")
|
|
100
|
+
return QtGui.QTransform(*values)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _map_pairs(values: list[str], transform: QtGui.QTransform) -> list[str]:
|
|
104
|
+
if len(values) % 2:
|
|
105
|
+
raise ValueError("Invalid Qt SVG coordinate pair.")
|
|
106
|
+
result = []
|
|
107
|
+
for index in range(0, len(values), 2):
|
|
108
|
+
point = transform.map(QtCore.QPointF(float(values[index]), float(values[index + 1])))
|
|
109
|
+
result.append(f"{point.x():.17g},{point.y():.17g}")
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _map_path(value: str, transform: QtGui.QTransform) -> str:
|
|
114
|
+
"""Map Qt's absolute M/L/C paths, preserving curves and closing Z commands.
|
|
115
|
+
|
|
116
|
+
Qt serializes QPainterPath with this subset of SVG commands (quadratics
|
|
117
|
+
become cubics). Tokenize SVG numbers, including exponents and adjacent
|
|
118
|
+
signed numbers; never assume whitespace-delimited tokens are pairs.
|
|
119
|
+
"""
|
|
120
|
+
tokens = _TOKEN.findall(value)
|
|
121
|
+
if re.sub(r"[\s,]", "", _TOKEN.sub("", value)):
|
|
122
|
+
raise ValueError("Invalid Qt SVG path.")
|
|
123
|
+
result: list[str] = []
|
|
124
|
+
index = 0
|
|
125
|
+
while index < len(tokens):
|
|
126
|
+
command = tokens[index]
|
|
127
|
+
index += 1
|
|
128
|
+
if command == "Z":
|
|
129
|
+
result.append("Z")
|
|
130
|
+
continue
|
|
131
|
+
arity = {"M": 2, "L": 2, "C": 6}.get(command)
|
|
132
|
+
if arity is None:
|
|
133
|
+
raise ValueError(f"Unexpected Qt SVG path command: {command}")
|
|
134
|
+
end = index
|
|
135
|
+
while end < len(tokens) and not tokens[end].isalpha():
|
|
136
|
+
end += 1
|
|
137
|
+
values = tokens[index:end]
|
|
138
|
+
if not values or len(values) % arity:
|
|
139
|
+
raise ValueError("Invalid Qt SVG path coordinates.")
|
|
140
|
+
result.append(command + " ".join(_map_pairs(values, transform)))
|
|
141
|
+
index = end
|
|
142
|
+
return " ".join(result)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _shape_path(node: Any) -> str:
|
|
146
|
+
"""Convert Qt's rect/ellipse primitives to the same absolute path subset."""
|
|
147
|
+
path = QtGui.QPainterPath()
|
|
148
|
+
if node.tagName == "rect":
|
|
149
|
+
path.addRoundedRect(QtCore.QRectF(*(
|
|
150
|
+
float(node.getAttribute(key) or 0) for key in ("x", "y", "width", "height")
|
|
151
|
+
)), float(node.getAttribute("rx") or 0), float(node.getAttribute("ry") or 0))
|
|
152
|
+
else:
|
|
153
|
+
cx, cy = (float(node.getAttribute(key) or 0) for key in ("cx", "cy"))
|
|
154
|
+
rx = float(node.getAttribute("rx") or node.getAttribute("r"))
|
|
155
|
+
ry = float(node.getAttribute("ry") or node.getAttribute("r"))
|
|
156
|
+
path.addEllipse(QtCore.QRectF(cx - rx, cy - ry, 2 * rx, 2 * ry))
|
|
157
|
+
parts = []
|
|
158
|
+
for i in range(path.elementCount()):
|
|
159
|
+
element = path.elementAt(i)
|
|
160
|
+
command = {
|
|
161
|
+
QtGui.QPainterPath.ElementType.MoveToElement: "M",
|
|
162
|
+
QtGui.QPainterPath.ElementType.LineToElement: "L",
|
|
163
|
+
QtGui.QPainterPath.ElementType.CurveToElement: "C",
|
|
164
|
+
QtGui.QPainterPath.ElementType.CurveToDataElement: "",
|
|
165
|
+
}[element.type]
|
|
166
|
+
parts.append(f"{command}{element.x:.17g},{element.y:.17g}")
|
|
167
|
+
return " ".join(parts) + " Z"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _scaling_strokes(data: bytes) -> bytes:
|
|
171
|
+
"""Make cosmetic strokes scale on SVG resize, without changing geometry.
|
|
172
|
+
|
|
173
|
+
Simply removing vector-effect would multiply widths by the data-to-view
|
|
174
|
+
transform (often anisotropic). Move those primitives to document coordinates
|
|
175
|
+
first, cancelling their inherited transform. Leave noncosmetic pens, text,
|
|
176
|
+
images, gradients and clipping definitions as Qt generated them.
|
|
177
|
+
"""
|
|
178
|
+
document = xml.parseString(data)
|
|
179
|
+
|
|
180
|
+
def visit(node: Any, parent_transform: QtGui.QTransform) -> None:
|
|
181
|
+
transform = _transform(node.getAttribute("transform")) * parent_transform
|
|
182
|
+
if node.getAttribute("vector-effect") == "non-scaling-stroke":
|
|
183
|
+
inverse, invertible = parent_transform.inverted()
|
|
184
|
+
if not invertible:
|
|
185
|
+
raise ValueError("Cannot scale strokes under a singular SVG transform.")
|
|
186
|
+
if node.tagName in ("polyline", "polygon"):
|
|
187
|
+
node.setAttribute("points", " ".join(_map_pairs(
|
|
188
|
+
re.findall(_NUMBER, node.getAttribute("points")), transform,
|
|
189
|
+
)))
|
|
190
|
+
elif node.tagName in ("path", "rect", "ellipse", "circle"):
|
|
191
|
+
value = node.getAttribute("d") if node.tagName == "path" else _shape_path(node)
|
|
192
|
+
node.tagName = "path"
|
|
193
|
+
node.setAttribute("d", _map_path(value, transform))
|
|
194
|
+
for name in ("x", "y", "width", "height", "rx", "ry", "cx", "cy", "r"):
|
|
195
|
+
if node.hasAttribute(name):
|
|
196
|
+
node.removeAttribute(name)
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError(f"Unexpected Qt SVG cosmetic primitive: {node.tagName}")
|
|
199
|
+
node.removeAttribute("vector-effect")
|
|
200
|
+
node.setAttribute("transform", _matrix(inverse))
|
|
201
|
+
for child in node.childNodes:
|
|
202
|
+
if isinstance(child, xml.Element) and child.tagName != "defs":
|
|
203
|
+
visit(child, transform)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
visit(document.documentElement, QtGui.QTransform())
|
|
207
|
+
return document.toxml(encoding="utf-8")
|
|
208
|
+
finally:
|
|
209
|
+
document.unlink()
|