qmlmathplot 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/PKG-INFO +58 -115
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/README.md +52 -113
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/pyproject.toml +5 -2
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/pyproject.toml.orig +8 -2
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/__init__.py +48 -27
- qmlmathplot-0.6.0/src/qmlmathplot/calc/__init__.py +11 -0
- qmlmathplot-0.6.0/src/qmlmathplot/calc/__main__.py +61 -0
- qmlmathplot-0.6.0/src/qmlmathplot/calc/build.py +103 -0
- qmlmathplot-0.6.0/src/qmlmathplot/client.py +243 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/curve.py +267 -172
- qmlmathplot-0.6.0/src/qmlmathplot/curveplotter.py +638 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/ggb_compute.py +1 -9
- qmlmathplot-0.6.0/src/qmlmathplot/ggb_eligibility.py +69 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/ggb_path.py +61 -29
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/interval.py +10 -65
- qmlmathplot-0.6.0/src/qmlmathplot/interval_reference.py +526 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/model.py +1 -158
- qmlmathplot-0.6.0/src/qmlmathplot/pathcurve.py +214 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/plot.py +36 -3
- qmlmathplot-0.6.0/src/qmlmathplot/program.py +32 -0
- qmlmathplot-0.6.0/src/qmlmathplot/protocol.py +216 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/CpuCurves.qml +2 -1
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotCurves.qml +4 -4
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotView.qml +4 -3
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/rhi.py +145 -31
- qmlmathplot-0.6.0/src/qmlmathplot/shaders/interval_path.comp +105 -0
- qmlmathplot-0.6.0/src/qmlmathplot/shaders/path.frag +10 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/path.vert +3 -0
- qmlmathplot-0.6.0/src/qmlmathplot/shaders/path_state.glsl +222 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/view.py +2 -2
- qmlmathplot-0.5.0/src/qmlmathplot/canvas.py +0 -107
- qmlmathplot-0.5.0/src/qmlmathplot/figure.py +0 -516
- qmlmathplot-0.5.0/src/qmlmathplot/pathcurve.py +0 -167
- qmlmathplot-0.5.0/src/qmlmathplot/pyplot.py +0 -370
- qmlmathplot-0.5.0/src/qmlmathplot/shaders/interval_path.comp +0 -332
- qmlmathplot-0.5.0/src/qmlmathplot/shaders/oned_sampled.frag +0 -490
- qmlmathplot-0.5.0/src/qmlmathplot/shaders/path.frag +0 -4
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/LICENSE +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/LICENSES/EUPL-1.2.txt +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/__main__.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/animation.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/app.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/camera.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/export.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/ggb_reference.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/input.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/mpl.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/overlay.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotFurniture.qml +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotInput.qml +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotLegend.qml +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotOverlay.qml +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qsb.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaderbuild.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/__init__.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/arg_reduction.glsl +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/real_pow.glsl +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/seg_dist.glsl +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/span_cov.glsl +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/uniforms.glsl +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/fullscreen.vert +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/implicit.frag +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/interval_lib.glsl +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/LICENSE.matplotlib +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/README.md +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/Solarize_Light2.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/bmh.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/classic.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/dark_background.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/default.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/fast.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/fivethirtyeight.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/ggplot.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/grayscale.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/petroff10.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-bright.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-colorblind.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-dark-palette.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-dark.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-darkgrid.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-deep.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-muted.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-notebook.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-paper.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-pastel.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-poster.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-talk.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-ticks.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-white.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-whitegrid.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/tableau-colorblind10.mplstyle +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes.py +0 -0
- {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/widget.py +0 -0
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qmlmathplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Embeddable function plotter for Qt Quick / QtWidgets (implicit per-pixel drawing, MVVM)
|
|
5
5
|
Author: I-AM-A-NOOB
|
|
6
6
|
Author-email: I-AM-A-NOOB <suzc08@outlook.com>
|
|
7
7
|
License-Expression: GPL-3.0-only
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
License-File: LICENSES/EUPL-1.2.txt
|
|
10
|
-
Requires-Dist: sympy>=1.14
|
|
11
10
|
Requires-Dist: pyside6>=6.12
|
|
11
|
+
Requires-Dist: numpy>=1.26
|
|
12
|
+
Requires-Dist: sympy>=1.14 ; extra == 'calc'
|
|
12
13
|
Requires-Dist: matplotlib>=3.8 ; extra == 'matplotlib'
|
|
14
|
+
Requires-Dist: matplotlib>=3.8 ; extra == 'mpl'
|
|
13
15
|
Requires-Python: >=3.11
|
|
16
|
+
Provides-Extra: calc
|
|
14
17
|
Provides-Extra: matplotlib
|
|
18
|
+
Provides-Extra: mpl
|
|
15
19
|
Description-Content-Type: text/markdown
|
|
16
20
|
|
|
17
21
|
# QMLMathPlot
|
|
@@ -28,6 +32,24 @@ The state is a **camera on an infinite canvas**: a `Plot` holds a `Camera` (wher
|
|
|
28
32
|
and a list of `Curve`s (what is drawn), and the QML component `PlotView` binds to it. Several
|
|
29
33
|
panels are several `Plot`s in a Qt layout — there is no figure, no axes box and no page.
|
|
30
34
|
|
|
35
|
+
## Breaking changes in 0.6
|
|
36
|
+
|
|
37
|
+
- **The model no longer parses expressions.** A calculator turns text into a `Program`
|
|
38
|
+
(`qmlmathplot.protocol`) and the model consumes it; the UI process does not import sympy.
|
|
39
|
+
The in-process default (`InlineCalculator`) needs the `calc` extra
|
|
40
|
+
(`pip install qmlmathplot[calc]`); a host that isolates the calculator injects
|
|
41
|
+
`ProcessCalculator` — see `examples/process_calculator.py`.
|
|
42
|
+
- `Curve` and `add_curve` are **asynchronous**: shaders arrive with the program. Wait for
|
|
43
|
+
`shadersChanged`, and read `curve.error` for a failing edit (the previous drawing stays).
|
|
44
|
+
`Curve.expr` is a deprecated, lazily imported sympy view.
|
|
45
|
+
- Routing is `Program.kind`: `intervals` (compute shader), `adaptive` (CPU `CurvePlotter`),
|
|
46
|
+
`implicit` (level set). `add_curve("a*x")` reports the parameter error on the curve
|
|
47
|
+
instead of raising.
|
|
48
|
+
- **Removed:** `Figure`, `Axes`, `FigureCanvasQt`, `qmlmathplot.pyplot`. `MathPlotWidget`
|
|
49
|
+
stays.
|
|
50
|
+
- **QML:** `import QmlMathPlot 2.0`; `cpuRenderer: true` became `renderMode: "cpu"`
|
|
51
|
+
(`"auto"` / `"gpu"` / `"cpu"`).
|
|
52
|
+
|
|
31
53
|
## Quick start
|
|
32
54
|
|
|
33
55
|
One line in a QtWidgets layout (least effort):
|
|
@@ -49,16 +71,15 @@ An un-themed plot looks like matplotlib (white background, tab10 colours, grey g
|
|
|
49
71
|
axes through the origin with the tick labels riding them) — the default style sheet is applied
|
|
50
72
|
at construction, so `plot.theme = "default"` is the reset.
|
|
51
73
|
|
|
52
|
-
`add_curve()` (and `Curve.expression`)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
curve can be composed without string interpolation:
|
|
74
|
+
`add_curve()` (and `Curve.expression`) take the expression as text; a sympy object is also
|
|
75
|
+
accepted and converted to its canonical string once. The model never parses it: a calculator
|
|
76
|
+
turns the text into a `Program` (`kind`, shader sources, `math`/`numpy` programs), so the UI
|
|
77
|
+
process can stay sympy-free. `Curve.expr` still exists as a deprecated, lazily imported sympy
|
|
78
|
+
view for the compatibility layer, and the matplotlib hand-off uses it.
|
|
58
79
|
|
|
59
80
|
```python
|
|
60
81
|
curve = plot.add_curve("sin(x)")
|
|
61
|
-
plot.add_curve(sp.diff(sp.sympify(curve.expression
|
|
82
|
+
plot.add_curve(sp.diff(sp.sympify(curve.expression)), label="f'") # needs the [calc] extra
|
|
62
83
|
```
|
|
63
84
|
|
|
64
85
|
`MathPlotWidget` keeps one curve's worth of convenience (`expression`, `error`,
|
|
@@ -77,14 +98,14 @@ Standalone window / command line:
|
|
|
77
98
|
```
|
|
78
99
|
uv run qmlmathplot "sin(1/x)" # equivalent to python examples/minimal.py …
|
|
79
100
|
uv run qmlmathplot --backend d3d11 "sin(1/x)" # force a specific backend
|
|
80
|
-
python examples/
|
|
101
|
+
python examples/widget_minimal.py # QtWidgets host
|
|
102
|
+
python examples/process_calculator.py # calculator in its own process
|
|
81
103
|
```
|
|
82
104
|
|
|
83
105
|
The examples live in `examples/`: `minimal.py` is the smallest window;
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
the pure Qt Quick demo.
|
|
106
|
+
`explorer_qtquick.py` (plus `explorer_qtquick.qml`) is the pure Qt Quick demo;
|
|
107
|
+
`process_calculator.py` runs the calculator in its own process and can prove the isolation
|
|
108
|
+
with `--check-isolation`; `widget_minimal.py` embeds `MathPlotWidget` in a QtWidgets layout.
|
|
88
109
|
|
|
89
110
|
## Embedding into an app
|
|
90
111
|
|
|
@@ -116,7 +137,7 @@ app.exec()
|
|
|
116
137
|
```
|
|
117
138
|
|
|
118
139
|
```qml
|
|
119
|
-
import QmlMathPlot
|
|
140
|
+
import QmlMathPlot 2.0
|
|
120
141
|
|
|
121
142
|
PlotView { // inject a Plot, or let the component create its own
|
|
122
143
|
anchors.fill: parent
|
|
@@ -171,74 +192,8 @@ Matplotlib *vocabulary* is kept where it is vocabulary (limits, grid, title, tic
|
|
|
171
192
|
*mechanism* is refused — no `Figure`/`Axes`/`subplots` in the core (one view per widget, no
|
|
172
193
|
bounded page), no redraw loop (the scene is live), no toolbar and no legend (host chrome), and
|
|
173
194
|
an export is a canvas region at a pixel size rather than a re-rendered page (§9, implemented —
|
|
174
|
-
see *Export* below).
|
|
175
|
-
`
|
|
176
|
-
model and refuses the mechanisms that do not fit (see *Matplotlib-style facade* below).
|
|
177
|
-
|
|
178
|
-
## Matplotlib-style facade
|
|
179
|
-
|
|
180
|
-
For porting scripts and for code that expects Matplotlib's vocabulary, the package ships a
|
|
181
|
-
**thin** compatibility layer. It maps onto the same `Plot`/`Camera` model and refuses the
|
|
182
|
-
mechanisms that do not fit an infinite, live canvas.
|
|
183
|
-
|
|
184
|
-
```python
|
|
185
|
-
from qmlmathplot import Figure, FigureCanvasQt
|
|
186
|
-
|
|
187
|
-
fig = Figure(figsize=(6.4, 4.8)) # stored for the window size only; no page is implied
|
|
188
|
-
ax = fig.gca() # exactly one axes per figure
|
|
189
|
-
ax.plot("sin(1/x)", label="sin(1/x)")
|
|
190
|
-
ax.set_xlim(-1, 1)
|
|
191
|
-
ax.set_ylim(-2, 2)
|
|
192
|
-
ax.set_title("sin(1/x)")
|
|
193
|
-
ax.set_xlabel("x")
|
|
194
|
-
ax.set_ylabel("y")
|
|
195
|
-
ax.grid(True)
|
|
196
|
-
ax.set_aspect("equal")
|
|
197
|
-
|
|
198
|
-
canvas = FigureCanvasQt(fig) # a QWidget, like MathPlotWidget
|
|
199
|
-
layout.addWidget(canvas)
|
|
200
|
-
canvas.draw() # no-op: the Qt scene is already live
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
The same model through a pyplot-shaped module (global current figure, for scripts and
|
|
204
|
-
notebooks; applications should keep the object API):
|
|
205
|
-
|
|
206
|
-
```python
|
|
207
|
-
import qmlmathplot.pyplot as plt
|
|
208
|
-
|
|
209
|
-
plt.figure()
|
|
210
|
-
plt.plot("sin(1/x)")
|
|
211
|
-
plt.xlim(-1, 1)
|
|
212
|
-
plt.title("sin(1/x)")
|
|
213
|
-
plt.xlabel("x")
|
|
214
|
-
plt.grid(True)
|
|
215
|
-
plt.savefig("out.png") # Qt raster export
|
|
216
|
-
plt.show() # blocks in a script; use block=False in a host
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
What maps: one figure/one axes (`subplots()` and `subplot(111)`), `plot(expression)`,
|
|
220
|
-
`xlim`/`ylim`, `title`/`xlabel`/`ylabel`, `grid(axis=...)`, `aspect`, `savefig`/`to_image`,
|
|
221
|
-
themes, `legend`, `annotate`/`text`, `axhline`/`axvline`, `set_xticks`/`set_yticks` and
|
|
222
|
-
`tick_params`. `plt.gcf()`/`gca()`/`sca()`/`close()`/`show()` follow Matplotlib's
|
|
223
|
-
current-figure shape.
|
|
224
|
-
|
|
225
|
-
What is deliberately different:
|
|
226
|
-
|
|
227
|
-
- **One view per figure.** `subplots(2, 1)`/`add_subplot(211)` raise `NotImplementedError`;
|
|
228
|
-
several panels are several `Figure` objects in a Qt layout, not a `GridSpec`.
|
|
229
|
-
- **Expressions, not x/y arrays.** `ax.plot("sin(x)")` is native; `ax.plot(x, y)` is refused
|
|
230
|
-
because the Qt renderer evaluates an expression per pixel. For sampled data use
|
|
231
|
-
`Plot.to_matplotlib()` (the hand-off), or add a data-series artist.
|
|
232
|
-
- **`draw()` / `draw_idle()` are no-ops.** The scene repaints from model signals; there is no
|
|
233
|
-
deferred renderer or blitting.
|
|
234
|
-
- **`savefig` is a region at a pixel size**, not a page at a dpi. `width`/`height` are device
|
|
235
|
-
pixels and `dpi` is a multiplier; `backend="matplotlib"` is the vector route.
|
|
236
|
-
- **Log axes and full annotation styling are not implemented.** `set_xscale()`/`set_yscale()`
|
|
237
|
-
raise with the reason; annotations support text + a straight arrow + pixel offset, not
|
|
238
|
-
Matplotlib's bbox/connectionstyle engine.
|
|
239
|
-
- **Themes are per figure**, not `rcParams`; `plt` has no global style state beyond the current
|
|
240
|
-
figure.
|
|
241
|
-
- **`mpl_connect`/`blit` are absent.** Qt signals are the event system.
|
|
195
|
+
see *Export* below). 0.6 removed the Matplotlib-shaped facade entirely: the model is `Plot` /
|
|
196
|
+
`Camera` / `Curve`, and the optional matplotlib hand-off is `Plot.to_matplotlib()`.
|
|
242
197
|
|
|
243
198
|
## Implicit curves
|
|
244
199
|
|
|
@@ -541,20 +496,17 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
|
|
|
541
496
|
|
|
542
497
|
| Layer | File | Responsibility |
|
|
543
498
|
|---|---|---|
|
|
544
|
-
|
|
|
499
|
+
| Calculator | `src/qmlmathplot/calc/` | text → `Program` (GLSL, `math` and `numpy` sources); the only code that imports sympy. Qt-free, runs in-process or as `python -m qmlmathplot.calc` |
|
|
545
500
|
| Shaders | `src/qmlmathplot/shaders/` + `shaderbuild.py` | GLSL resources, `@NAME@` substitution, shared `@INCLUDE` blocks, validation (no template strings in Python) |
|
|
546
501
|
| Camera | `src/qmlmathplot/camera.py` | `Camera`: centre / zoom / aspect, the derived ranges, pan/zoom/reset, the nice-number tick algorithm |
|
|
547
502
|
| Input | `src/qmlmathplot/input.py` | `ViewInput`: keyboard switch, pan step, rebindable shortcuts, key matching |
|
|
548
503
|
| Animation | `src/qmlmathplot/animation.py` | `CameraAnimator`: the one eased interpolator behind wheel/keyboard moves |
|
|
549
|
-
| Curves | `src/qmlmathplot/curve.py` | `Curve` (
|
|
504
|
+
| Curves | `src/qmlmathplot/curve.py` | `Curve` (requests a `Program`, bakes its GLSL on the thread pool, keeps the last drawing on failure) and `CurveListModel` |
|
|
550
505
|
| Render | `src/qmlmathplot/rhi.py` + `ggb_compute.py` | `CurveRhiItem`: one `QQuickRhiItem` draws every curve in one pass; interval-supported 1-D curves run the GGB path state machine in a compute shader |
|
|
551
506
|
| Path spec | `src/qmlmathplot/ggb_path.py` | The GGB path state machine (topology in, segments out), shared by the CPU sampler and the tests; no interval arithmetic |
|
|
552
|
-
| Reference | `src/qmlmathplot/ggb_reference.py` | CPU sampler (`mpmath.iv`) feeding the shared state machine
|
|
553
|
-
| CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.
|
|
507
|
+
| Reference | `src/qmlmathplot/ggb_reference.py` | test oracle: CPU sampler (`mpmath.iv`) feeding the shared state machine |
|
|
508
|
+
| CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.renderMode = "cpu"`: one QPainterPath per curve when RHI is unavailable |
|
|
554
509
|
| Plot | `src/qmlmathplot/plot.py` | `Plot`: the camera, the curves and the styling the view binds to |
|
|
555
|
-
| Facade | `src/qmlmathplot/figure.py` | `Figure` / `Axes`: Matplotlib-shaped names over one `Plot` (no Qt beyond the model) |
|
|
556
|
-
| Facade host | `src/qmlmathplot/canvas.py` | `FigureCanvasQt`: a `MathPlotWidget` attached to a `Figure` (QtWidgets, lazy) |
|
|
557
|
-
| Facade scripting | `src/qmlmathplot/pyplot.py` | `qmlmathplot.pyplot`: current figure, `plot`/`xlim`/`show`/`savefig` for scripts |
|
|
558
510
|
| View | `src/qmlmathplot/qml/PlotView.qml` (+ `PlotCurves.qml`, `PlotFurniture.qml`, `PlotInput.qml`, `view.py`) | composition: model injection, camera mapping and keyboard focus; curves, furniture and pointer input live in the three child components (one `QQuickRhiItem` pass for all curves) |
|
|
559
511
|
| Themes | `src/qmlmathplot/themes.py` + `themes/` | the vendored Matplotlib style sheets, resolved onto the `Plot`'s properties |
|
|
560
512
|
| Export | `src/qmlmathplot/export.py` | `Plot.savefig()` / `to_image()`: the canvas region rendered offscreen at a pixel size (QtWidgets is imported lazily) |
|
|
@@ -562,7 +514,7 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
|
|
|
562
514
|
| Bake | `src/qmlmathplot/qsb.py` | GLSL → `.qsb` (PySide6 ships `qsb.exe`), cached by source hash |
|
|
563
515
|
| Widget | `src/qmlmathplot/widget.py` | `MathPlotWidget`: QQuickWidget bridge, drops straight into QtWidgets layouts |
|
|
564
516
|
| Entry | `src/qmlmathplot/app.py` | command line / standalone window (the `qmlmathplot` script, `examples/minimal.py`) |
|
|
565
|
-
| Examples | `examples/` | `minimal.py` (
|
|
517
|
+
| Examples | `examples/` | `minimal.py` (standalone window), `explorer_qtquick.py` (Qt Quick), `process_calculator.py` (the process split + `--check-isolation`), `widget_minimal.py` (QtWidgets) |
|
|
566
518
|
|
|
567
519
|
## Algorithm (GeoGebra's interval path on the GPU)
|
|
568
520
|
|
|
@@ -596,20 +548,15 @@ same state machine (topology in, screen-space segments out).
|
|
|
596
548
|
the compute path from the expression). Expressions
|
|
597
549
|
GGB's `IntervalFunctionSupport` rejects — `floor`, `sign`, `Min/Max`, `Piecewise`, and
|
|
598
550
|
dependent quotients such as `x/sqrt(x**2)` (GGB's algebra simplifies it to `sgn`) — use
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
uniformly instead of leaving a moiré of spike tops. GGB's conservative connector would
|
|
609
|
-
violate the "no line across a pole" invariant. The CPU sampler (`ggb_reference.py`,
|
|
610
|
-
`mpmath.iv`) cannot express a gap, so its pole columns are path breaks; it feeds the same
|
|
611
|
-
state machine. The deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus
|
|
612
|
-
the whole-column rule, as before.
|
|
551
|
+
GGB's classic `CurvePlotter`, ported to `curveplotter.py`: adaptive sampling, continuity
|
|
552
|
+
checks and a world-space path, stroked by the RHI path pipeline (the CPU owns the path,
|
|
553
|
+
the GPU owns the stroke). Implicit curves always use their own level-set shader.
|
|
554
|
+
|
|
555
|
+
Deliberate deviation: `exp(tan(x))` follows GGB's own wrapped-pole behaviour; no special
|
|
556
|
+
fill compensates for it. The CPU sampler (`ggb_reference.py`, `mpmath.iv`) cannot express a
|
|
557
|
+
gap, so its pole columns are path breaks; it feeds the same `ggb_path` state machine. The
|
|
558
|
+
deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus the whole-column
|
|
559
|
+
rule.
|
|
613
560
|
|
|
614
561
|
|
|
615
562
|
## Tests
|
|
@@ -622,11 +569,11 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
|
|
|
622
569
|
```
|
|
623
570
|
|
|
624
571
|
- `test_model.py`: GLSL generation (small integer powers as repeated
|
|
625
|
-
multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0),
|
|
626
|
-
|
|
572
|
+
multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0), domain analysis,
|
|
573
|
+
and the camera math
|
|
627
574
|
(the home view at the first size, resize keeping the scale, an anchored zoom that
|
|
628
575
|
is exactly reversible, the aspect expanding but never cropping, ticks).
|
|
629
|
-
- `test_curve.py`: a failed expression keeps the last working
|
|
576
|
+
- `test_curve.py`: a failed expression keeps the last working expression, the style
|
|
630
577
|
setters are idempotent, and the list model reports the right roles — plus the
|
|
631
578
|
`Expr`/string round-trip accounting (a string is parsed once, an expression never).
|
|
632
579
|
- `test_implicit.py`: the routing by free symbols (and the `ValueError` for a third symbol), the
|
|
@@ -641,8 +588,8 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
|
|
|
641
588
|
matplotlib's default style, the limits are forwarded to the camera, and a theme applies
|
|
642
589
|
(and re-applies as a reset) while an unknown name is rejected.
|
|
643
590
|
- `test_themes.py`: the vendored style sheets resolve to this library's properties.
|
|
644
|
-
- `test_overlay.py`: annotations
|
|
645
|
-
|
|
591
|
+
- `test_overlay.py`: annotations, horizontal/vertical guides, legend and custom ticks, and
|
|
592
|
+
the matplotlib hand-off mapping.
|
|
646
593
|
- `test_qsb_cache.py`: the cache location/info/clear API and the age + LRU prune; unrelated
|
|
647
594
|
files in the temp directory are not touched.
|
|
648
595
|
- `test_shader_bake.py`: the stage of a baked `.qsb` must match its purpose, and
|
|
@@ -670,10 +617,6 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
|
|
|
670
617
|
extra.
|
|
671
618
|
- `test_async_bake.py`: a later expression assignment bakes on the thread pool, a stale bake
|
|
672
619
|
is discarded, and a failed bake keeps the drawn curve.
|
|
673
|
-
- `test_compat.py`: the Matplotlib-shaped facade — expressions (and the refusal of x/y arrays),
|
|
674
|
-
limits/title/labels/grid/aspect, the single-axes rule, `pyplot` current-figure semantics,
|
|
675
|
-
`FigureCanvasQt` rendering and export, and that importing `qmlmathplot` does not load
|
|
676
|
-
QtWidgets.
|
|
677
620
|
- `test_export.py`: `to_image()`/`savefig()` — the image is exactly the requested device size
|
|
678
621
|
(`dpi` multiplies it), the no-argument form reproduces the live centre and scale, the three
|
|
679
622
|
`adjustable` modes reconcile range and size as documented (a world 45-degree line stays at 45
|
|
@@ -750,7 +693,7 @@ Startup to the first frame is about **230 ms**, attributed as follows
|
|
|
750
693
|
| Qt graphics device init (RHI/D3D11) | **~160 ms** | exactly what the first window's `show→expose` costs; a **bare window (no ShaderEffect) is the same**, so it is not the shader's fault |
|
|
751
694
|
| Window creation + QML load + first frame | ~80 ms | from the second window on (device already created) this is the order of magnitude |
|
|
752
695
|
| `qsb` bake | 0 / ~126 ms | cached by source hash: 0.15 ms on a hit; later `expression` assignments bake on Qt's thread pool, so only the first curve construction blocks (tests force the synchronous path) |
|
|
753
|
-
| Expression analysis (sympy) | 0.6~3 ms |
|
|
696
|
+
| Expression analysis (sympy) | 0.6~3 ms | domain/pole analysis for the interval and implicit renderers; unsupported 1-D expressions go to the CPU CurvePlotter |
|
|
754
697
|
|
|
755
698
|
Per-frame grab cycles (900×600, `grabToImage` — frame + readback, so an upper bound on
|
|
756
699
|
the GPU cost), steady state on the same window: `sin(1/x)` default **13.6 ms**, `tan(x)`
|
|
@@ -12,6 +12,24 @@ The state is a **camera on an infinite canvas**: a `Plot` holds a `Camera` (wher
|
|
|
12
12
|
and a list of `Curve`s (what is drawn), and the QML component `PlotView` binds to it. Several
|
|
13
13
|
panels are several `Plot`s in a Qt layout — there is no figure, no axes box and no page.
|
|
14
14
|
|
|
15
|
+
## Breaking changes in 0.6
|
|
16
|
+
|
|
17
|
+
- **The model no longer parses expressions.** A calculator turns text into a `Program`
|
|
18
|
+
(`qmlmathplot.protocol`) and the model consumes it; the UI process does not import sympy.
|
|
19
|
+
The in-process default (`InlineCalculator`) needs the `calc` extra
|
|
20
|
+
(`pip install qmlmathplot[calc]`); a host that isolates the calculator injects
|
|
21
|
+
`ProcessCalculator` — see `examples/process_calculator.py`.
|
|
22
|
+
- `Curve` and `add_curve` are **asynchronous**: shaders arrive with the program. Wait for
|
|
23
|
+
`shadersChanged`, and read `curve.error` for a failing edit (the previous drawing stays).
|
|
24
|
+
`Curve.expr` is a deprecated, lazily imported sympy view.
|
|
25
|
+
- Routing is `Program.kind`: `intervals` (compute shader), `adaptive` (CPU `CurvePlotter`),
|
|
26
|
+
`implicit` (level set). `add_curve("a*x")` reports the parameter error on the curve
|
|
27
|
+
instead of raising.
|
|
28
|
+
- **Removed:** `Figure`, `Axes`, `FigureCanvasQt`, `qmlmathplot.pyplot`. `MathPlotWidget`
|
|
29
|
+
stays.
|
|
30
|
+
- **QML:** `import QmlMathPlot 2.0`; `cpuRenderer: true` became `renderMode: "cpu"`
|
|
31
|
+
(`"auto"` / `"gpu"` / `"cpu"`).
|
|
32
|
+
|
|
15
33
|
## Quick start
|
|
16
34
|
|
|
17
35
|
One line in a QtWidgets layout (least effort):
|
|
@@ -33,16 +51,15 @@ An un-themed plot looks like matplotlib (white background, tab10 colours, grey g
|
|
|
33
51
|
axes through the origin with the tick labels riding them) — the default style sheet is applied
|
|
34
52
|
at construction, so `plot.theme = "default"` is the reset.
|
|
35
53
|
|
|
36
|
-
`add_curve()` (and `Curve.expression`)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
curve can be composed without string interpolation:
|
|
54
|
+
`add_curve()` (and `Curve.expression`) take the expression as text; a sympy object is also
|
|
55
|
+
accepted and converted to its canonical string once. The model never parses it: a calculator
|
|
56
|
+
turns the text into a `Program` (`kind`, shader sources, `math`/`numpy` programs), so the UI
|
|
57
|
+
process can stay sympy-free. `Curve.expr` still exists as a deprecated, lazily imported sympy
|
|
58
|
+
view for the compatibility layer, and the matplotlib hand-off uses it.
|
|
42
59
|
|
|
43
60
|
```python
|
|
44
61
|
curve = plot.add_curve("sin(x)")
|
|
45
|
-
plot.add_curve(sp.diff(sp.sympify(curve.expression
|
|
62
|
+
plot.add_curve(sp.diff(sp.sympify(curve.expression)), label="f'") # needs the [calc] extra
|
|
46
63
|
```
|
|
47
64
|
|
|
48
65
|
`MathPlotWidget` keeps one curve's worth of convenience (`expression`, `error`,
|
|
@@ -61,14 +78,14 @@ Standalone window / command line:
|
|
|
61
78
|
```
|
|
62
79
|
uv run qmlmathplot "sin(1/x)" # equivalent to python examples/minimal.py …
|
|
63
80
|
uv run qmlmathplot --backend d3d11 "sin(1/x)" # force a specific backend
|
|
64
|
-
python examples/
|
|
81
|
+
python examples/widget_minimal.py # QtWidgets host
|
|
82
|
+
python examples/process_calculator.py # calculator in its own process
|
|
65
83
|
```
|
|
66
84
|
|
|
67
85
|
The examples live in `examples/`: `minimal.py` is the smallest window;
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
the pure Qt Quick demo.
|
|
86
|
+
`explorer_qtquick.py` (plus `explorer_qtquick.qml`) is the pure Qt Quick demo;
|
|
87
|
+
`process_calculator.py` runs the calculator in its own process and can prove the isolation
|
|
88
|
+
with `--check-isolation`; `widget_minimal.py` embeds `MathPlotWidget` in a QtWidgets layout.
|
|
72
89
|
|
|
73
90
|
## Embedding into an app
|
|
74
91
|
|
|
@@ -100,7 +117,7 @@ app.exec()
|
|
|
100
117
|
```
|
|
101
118
|
|
|
102
119
|
```qml
|
|
103
|
-
import QmlMathPlot
|
|
120
|
+
import QmlMathPlot 2.0
|
|
104
121
|
|
|
105
122
|
PlotView { // inject a Plot, or let the component create its own
|
|
106
123
|
anchors.fill: parent
|
|
@@ -155,74 +172,8 @@ Matplotlib *vocabulary* is kept where it is vocabulary (limits, grid, title, tic
|
|
|
155
172
|
*mechanism* is refused — no `Figure`/`Axes`/`subplots` in the core (one view per widget, no
|
|
156
173
|
bounded page), no redraw loop (the scene is live), no toolbar and no legend (host chrome), and
|
|
157
174
|
an export is a canvas region at a pixel size rather than a re-rendered page (§9, implemented —
|
|
158
|
-
see *Export* below).
|
|
159
|
-
`
|
|
160
|
-
model and refuses the mechanisms that do not fit (see *Matplotlib-style facade* below).
|
|
161
|
-
|
|
162
|
-
## Matplotlib-style facade
|
|
163
|
-
|
|
164
|
-
For porting scripts and for code that expects Matplotlib's vocabulary, the package ships a
|
|
165
|
-
**thin** compatibility layer. It maps onto the same `Plot`/`Camera` model and refuses the
|
|
166
|
-
mechanisms that do not fit an infinite, live canvas.
|
|
167
|
-
|
|
168
|
-
```python
|
|
169
|
-
from qmlmathplot import Figure, FigureCanvasQt
|
|
170
|
-
|
|
171
|
-
fig = Figure(figsize=(6.4, 4.8)) # stored for the window size only; no page is implied
|
|
172
|
-
ax = fig.gca() # exactly one axes per figure
|
|
173
|
-
ax.plot("sin(1/x)", label="sin(1/x)")
|
|
174
|
-
ax.set_xlim(-1, 1)
|
|
175
|
-
ax.set_ylim(-2, 2)
|
|
176
|
-
ax.set_title("sin(1/x)")
|
|
177
|
-
ax.set_xlabel("x")
|
|
178
|
-
ax.set_ylabel("y")
|
|
179
|
-
ax.grid(True)
|
|
180
|
-
ax.set_aspect("equal")
|
|
181
|
-
|
|
182
|
-
canvas = FigureCanvasQt(fig) # a QWidget, like MathPlotWidget
|
|
183
|
-
layout.addWidget(canvas)
|
|
184
|
-
canvas.draw() # no-op: the Qt scene is already live
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
The same model through a pyplot-shaped module (global current figure, for scripts and
|
|
188
|
-
notebooks; applications should keep the object API):
|
|
189
|
-
|
|
190
|
-
```python
|
|
191
|
-
import qmlmathplot.pyplot as plt
|
|
192
|
-
|
|
193
|
-
plt.figure()
|
|
194
|
-
plt.plot("sin(1/x)")
|
|
195
|
-
plt.xlim(-1, 1)
|
|
196
|
-
plt.title("sin(1/x)")
|
|
197
|
-
plt.xlabel("x")
|
|
198
|
-
plt.grid(True)
|
|
199
|
-
plt.savefig("out.png") # Qt raster export
|
|
200
|
-
plt.show() # blocks in a script; use block=False in a host
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
What maps: one figure/one axes (`subplots()` and `subplot(111)`), `plot(expression)`,
|
|
204
|
-
`xlim`/`ylim`, `title`/`xlabel`/`ylabel`, `grid(axis=...)`, `aspect`, `savefig`/`to_image`,
|
|
205
|
-
themes, `legend`, `annotate`/`text`, `axhline`/`axvline`, `set_xticks`/`set_yticks` and
|
|
206
|
-
`tick_params`. `plt.gcf()`/`gca()`/`sca()`/`close()`/`show()` follow Matplotlib's
|
|
207
|
-
current-figure shape.
|
|
208
|
-
|
|
209
|
-
What is deliberately different:
|
|
210
|
-
|
|
211
|
-
- **One view per figure.** `subplots(2, 1)`/`add_subplot(211)` raise `NotImplementedError`;
|
|
212
|
-
several panels are several `Figure` objects in a Qt layout, not a `GridSpec`.
|
|
213
|
-
- **Expressions, not x/y arrays.** `ax.plot("sin(x)")` is native; `ax.plot(x, y)` is refused
|
|
214
|
-
because the Qt renderer evaluates an expression per pixel. For sampled data use
|
|
215
|
-
`Plot.to_matplotlib()` (the hand-off), or add a data-series artist.
|
|
216
|
-
- **`draw()` / `draw_idle()` are no-ops.** The scene repaints from model signals; there is no
|
|
217
|
-
deferred renderer or blitting.
|
|
218
|
-
- **`savefig` is a region at a pixel size**, not a page at a dpi. `width`/`height` are device
|
|
219
|
-
pixels and `dpi` is a multiplier; `backend="matplotlib"` is the vector route.
|
|
220
|
-
- **Log axes and full annotation styling are not implemented.** `set_xscale()`/`set_yscale()`
|
|
221
|
-
raise with the reason; annotations support text + a straight arrow + pixel offset, not
|
|
222
|
-
Matplotlib's bbox/connectionstyle engine.
|
|
223
|
-
- **Themes are per figure**, not `rcParams`; `plt` has no global style state beyond the current
|
|
224
|
-
figure.
|
|
225
|
-
- **`mpl_connect`/`blit` are absent.** Qt signals are the event system.
|
|
175
|
+
see *Export* below). 0.6 removed the Matplotlib-shaped facade entirely: the model is `Plot` /
|
|
176
|
+
`Camera` / `Curve`, and the optional matplotlib hand-off is `Plot.to_matplotlib()`.
|
|
226
177
|
|
|
227
178
|
## Implicit curves
|
|
228
179
|
|
|
@@ -525,20 +476,17 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
|
|
|
525
476
|
|
|
526
477
|
| Layer | File | Responsibility |
|
|
527
478
|
|---|---|---|
|
|
528
|
-
|
|
|
479
|
+
| Calculator | `src/qmlmathplot/calc/` | text → `Program` (GLSL, `math` and `numpy` sources); the only code that imports sympy. Qt-free, runs in-process or as `python -m qmlmathplot.calc` |
|
|
529
480
|
| Shaders | `src/qmlmathplot/shaders/` + `shaderbuild.py` | GLSL resources, `@NAME@` substitution, shared `@INCLUDE` blocks, validation (no template strings in Python) |
|
|
530
481
|
| Camera | `src/qmlmathplot/camera.py` | `Camera`: centre / zoom / aspect, the derived ranges, pan/zoom/reset, the nice-number tick algorithm |
|
|
531
482
|
| Input | `src/qmlmathplot/input.py` | `ViewInput`: keyboard switch, pan step, rebindable shortcuts, key matching |
|
|
532
483
|
| Animation | `src/qmlmathplot/animation.py` | `CameraAnimator`: the one eased interpolator behind wheel/keyboard moves |
|
|
533
|
-
| Curves | `src/qmlmathplot/curve.py` | `Curve` (
|
|
484
|
+
| Curves | `src/qmlmathplot/curve.py` | `Curve` (requests a `Program`, bakes its GLSL on the thread pool, keeps the last drawing on failure) and `CurveListModel` |
|
|
534
485
|
| Render | `src/qmlmathplot/rhi.py` + `ggb_compute.py` | `CurveRhiItem`: one `QQuickRhiItem` draws every curve in one pass; interval-supported 1-D curves run the GGB path state machine in a compute shader |
|
|
535
486
|
| Path spec | `src/qmlmathplot/ggb_path.py` | The GGB path state machine (topology in, segments out), shared by the CPU sampler and the tests; no interval arithmetic |
|
|
536
|
-
| Reference | `src/qmlmathplot/ggb_reference.py` | CPU sampler (`mpmath.iv`) feeding the shared state machine
|
|
537
|
-
| CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.
|
|
487
|
+
| Reference | `src/qmlmathplot/ggb_reference.py` | test oracle: CPU sampler (`mpmath.iv`) feeding the shared state machine |
|
|
488
|
+
| CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.renderMode = "cpu"`: one QPainterPath per curve when RHI is unavailable |
|
|
538
489
|
| Plot | `src/qmlmathplot/plot.py` | `Plot`: the camera, the curves and the styling the view binds to |
|
|
539
|
-
| Facade | `src/qmlmathplot/figure.py` | `Figure` / `Axes`: Matplotlib-shaped names over one `Plot` (no Qt beyond the model) |
|
|
540
|
-
| Facade host | `src/qmlmathplot/canvas.py` | `FigureCanvasQt`: a `MathPlotWidget` attached to a `Figure` (QtWidgets, lazy) |
|
|
541
|
-
| Facade scripting | `src/qmlmathplot/pyplot.py` | `qmlmathplot.pyplot`: current figure, `plot`/`xlim`/`show`/`savefig` for scripts |
|
|
542
490
|
| View | `src/qmlmathplot/qml/PlotView.qml` (+ `PlotCurves.qml`, `PlotFurniture.qml`, `PlotInput.qml`, `view.py`) | composition: model injection, camera mapping and keyboard focus; curves, furniture and pointer input live in the three child components (one `QQuickRhiItem` pass for all curves) |
|
|
543
491
|
| Themes | `src/qmlmathplot/themes.py` + `themes/` | the vendored Matplotlib style sheets, resolved onto the `Plot`'s properties |
|
|
544
492
|
| Export | `src/qmlmathplot/export.py` | `Plot.savefig()` / `to_image()`: the canvas region rendered offscreen at a pixel size (QtWidgets is imported lazily) |
|
|
@@ -546,7 +494,7 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
|
|
|
546
494
|
| Bake | `src/qmlmathplot/qsb.py` | GLSL → `.qsb` (PySide6 ships `qsb.exe`), cached by source hash |
|
|
547
495
|
| Widget | `src/qmlmathplot/widget.py` | `MathPlotWidget`: QQuickWidget bridge, drops straight into QtWidgets layouts |
|
|
548
496
|
| Entry | `src/qmlmathplot/app.py` | command line / standalone window (the `qmlmathplot` script, `examples/minimal.py`) |
|
|
549
|
-
| Examples | `examples/` | `minimal.py` (
|
|
497
|
+
| Examples | `examples/` | `minimal.py` (standalone window), `explorer_qtquick.py` (Qt Quick), `process_calculator.py` (the process split + `--check-isolation`), `widget_minimal.py` (QtWidgets) |
|
|
550
498
|
|
|
551
499
|
## Algorithm (GeoGebra's interval path on the GPU)
|
|
552
500
|
|
|
@@ -580,20 +528,15 @@ same state machine (topology in, screen-space segments out).
|
|
|
580
528
|
the compute path from the expression). Expressions
|
|
581
529
|
GGB's `IntervalFunctionSupport` rejects — `floor`, `sign`, `Min/Max`, `Piecewise`, and
|
|
582
530
|
dependent quotients such as `x/sqrt(x**2)` (GGB's algebra simplifies it to `sgn`) — use
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
uniformly instead of leaving a moiré of spike tops. GGB's conservative connector would
|
|
593
|
-
violate the "no line across a pole" invariant. The CPU sampler (`ggb_reference.py`,
|
|
594
|
-
`mpmath.iv`) cannot express a gap, so its pole columns are path breaks; it feeds the same
|
|
595
|
-
state machine. The deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus
|
|
596
|
-
the whole-column rule, as before.
|
|
531
|
+
GGB's classic `CurvePlotter`, ported to `curveplotter.py`: adaptive sampling, continuity
|
|
532
|
+
checks and a world-space path, stroked by the RHI path pipeline (the CPU owns the path,
|
|
533
|
+
the GPU owns the stroke). Implicit curves always use their own level-set shader.
|
|
534
|
+
|
|
535
|
+
Deliberate deviation: `exp(tan(x))` follows GGB's own wrapped-pole behaviour; no special
|
|
536
|
+
fill compensates for it. The CPU sampler (`ggb_reference.py`, `mpmath.iv`) cannot express a
|
|
537
|
+
gap, so its pole columns are path breaks; it feeds the same `ggb_path` state machine. The
|
|
538
|
+
deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus the whole-column
|
|
539
|
+
rule.
|
|
597
540
|
|
|
598
541
|
|
|
599
542
|
## Tests
|
|
@@ -606,11 +549,11 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
|
|
|
606
549
|
```
|
|
607
550
|
|
|
608
551
|
- `test_model.py`: GLSL generation (small integer powers as repeated
|
|
609
|
-
multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0),
|
|
610
|
-
|
|
552
|
+
multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0), domain analysis,
|
|
553
|
+
and the camera math
|
|
611
554
|
(the home view at the first size, resize keeping the scale, an anchored zoom that
|
|
612
555
|
is exactly reversible, the aspect expanding but never cropping, ticks).
|
|
613
|
-
- `test_curve.py`: a failed expression keeps the last working
|
|
556
|
+
- `test_curve.py`: a failed expression keeps the last working expression, the style
|
|
614
557
|
setters are idempotent, and the list model reports the right roles — plus the
|
|
615
558
|
`Expr`/string round-trip accounting (a string is parsed once, an expression never).
|
|
616
559
|
- `test_implicit.py`: the routing by free symbols (and the `ValueError` for a third symbol), the
|
|
@@ -625,8 +568,8 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
|
|
|
625
568
|
matplotlib's default style, the limits are forwarded to the camera, and a theme applies
|
|
626
569
|
(and re-applies as a reset) while an unknown name is rejected.
|
|
627
570
|
- `test_themes.py`: the vendored style sheets resolve to this library's properties.
|
|
628
|
-
- `test_overlay.py`: annotations
|
|
629
|
-
|
|
571
|
+
- `test_overlay.py`: annotations, horizontal/vertical guides, legend and custom ticks, and
|
|
572
|
+
the matplotlib hand-off mapping.
|
|
630
573
|
- `test_qsb_cache.py`: the cache location/info/clear API and the age + LRU prune; unrelated
|
|
631
574
|
files in the temp directory are not touched.
|
|
632
575
|
- `test_shader_bake.py`: the stage of a baked `.qsb` must match its purpose, and
|
|
@@ -654,10 +597,6 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
|
|
|
654
597
|
extra.
|
|
655
598
|
- `test_async_bake.py`: a later expression assignment bakes on the thread pool, a stale bake
|
|
656
599
|
is discarded, and a failed bake keeps the drawn curve.
|
|
657
|
-
- `test_compat.py`: the Matplotlib-shaped facade — expressions (and the refusal of x/y arrays),
|
|
658
|
-
limits/title/labels/grid/aspect, the single-axes rule, `pyplot` current-figure semantics,
|
|
659
|
-
`FigureCanvasQt` rendering and export, and that importing `qmlmathplot` does not load
|
|
660
|
-
QtWidgets.
|
|
661
600
|
- `test_export.py`: `to_image()`/`savefig()` — the image is exactly the requested device size
|
|
662
601
|
(`dpi` multiplies it), the no-argument form reproduces the live centre and scale, the three
|
|
663
602
|
`adjustable` modes reconcile range and size as documented (a world 45-degree line stays at 45
|
|
@@ -734,7 +673,7 @@ Startup to the first frame is about **230 ms**, attributed as follows
|
|
|
734
673
|
| Qt graphics device init (RHI/D3D11) | **~160 ms** | exactly what the first window's `show→expose` costs; a **bare window (no ShaderEffect) is the same**, so it is not the shader's fault |
|
|
735
674
|
| Window creation + QML load + first frame | ~80 ms | from the second window on (device already created) this is the order of magnitude |
|
|
736
675
|
| `qsb` bake | 0 / ~126 ms | cached by source hash: 0.15 ms on a hit; later `expression` assignments bake on Qt's thread pool, so only the first curve construction blocks (tests force the synchronous path) |
|
|
737
|
-
| Expression analysis (sympy) | 0.6~3 ms |
|
|
676
|
+
| Expression analysis (sympy) | 0.6~3 ms | domain/pole analysis for the interval and implicit renderers; unsupported 1-D expressions go to the CPU CurvePlotter |
|
|
738
677
|
|
|
739
678
|
Per-frame grab cycles (900×600, `grabToImage` — frame + readback, so an upper bound on
|
|
740
679
|
the GPU cost), steady state on the same window: `sin(1/x)` default **13.6 ms**, `tan(x)`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qmlmathplot"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Embeddable function plotter for Qt Quick / QtWidgets (implicit per-pixel drawing, MVVM)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "GPL-3.0-only"
|
|
@@ -10,8 +10,8 @@ license-files = [
|
|
|
10
10
|
]
|
|
11
11
|
requires-python = ">=3.11"
|
|
12
12
|
dependencies = [
|
|
13
|
-
"sympy>=1.14",
|
|
14
13
|
"PySide6>=6.12",
|
|
14
|
+
"numpy>=1.26",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
[[project.authors]]
|
|
@@ -19,7 +19,9 @@ name = "I-AM-A-NOOB"
|
|
|
19
19
|
email = "suzc08@outlook.com"
|
|
20
20
|
|
|
21
21
|
[project.optional-dependencies]
|
|
22
|
+
calc = ["sympy>=1.14"]
|
|
22
23
|
matplotlib = ["matplotlib>=3.8"]
|
|
24
|
+
mpl = ["matplotlib>=3.8"]
|
|
23
25
|
|
|
24
26
|
[project.scripts]
|
|
25
27
|
qmlmathplot = "qmlmathplot.app:main"
|
|
@@ -32,6 +34,7 @@ build-backend = "uv_build"
|
|
|
32
34
|
dev = [
|
|
33
35
|
"pytest>=9.1.1",
|
|
34
36
|
"matplotlib>=3.8",
|
|
37
|
+
"sympy>=1.14",
|
|
35
38
|
"ruff>=0.9",
|
|
36
39
|
]
|
|
37
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qmlmathplot"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Embeddable function plotter for Qt Quick / QtWidgets (implicit per-pixel drawing, MVVM)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "GPL-3.0-only"
|
|
@@ -10,14 +10,19 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
requires-python = ">=3.11"
|
|
12
12
|
dependencies = [
|
|
13
|
-
"sympy>=1.14",
|
|
14
13
|
"PySide6>=6.12",
|
|
14
|
+
"numpy>=1.26",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
[project.optional-dependencies]
|
|
18
|
+
# The calculator is the only part that needs sympy; install it for the in-process
|
|
19
|
+
# calculator and the worker (`python -m qmlmathplot.calc`). A host that isolates the
|
|
20
|
+
# calculator in another process does not need it in the UI environment.
|
|
21
|
+
calc = ["sympy>=1.14"]
|
|
18
22
|
# The matplotlib hand-off (Plot.to_matplotlib / savefig(backend="matplotlib")) is optional:
|
|
19
23
|
# nothing in the package imports matplotlib until that path is used.
|
|
20
24
|
matplotlib = ["matplotlib>=3.8"]
|
|
25
|
+
mpl = ["matplotlib>=3.8"]
|
|
21
26
|
|
|
22
27
|
[project.scripts]
|
|
23
28
|
qmlmathplot = "qmlmathplot.app:main"
|
|
@@ -30,6 +35,7 @@ build-backend = "uv_build"
|
|
|
30
35
|
dev = [
|
|
31
36
|
"pytest>=9.1.1",
|
|
32
37
|
"matplotlib>=3.8", # the hand-off tests need it (the feature itself stays optional)
|
|
38
|
+
"sympy>=1.14", # the calculator implementation and its oracles
|
|
33
39
|
"ruff>=0.9",
|
|
34
40
|
]
|
|
35
41
|
|