qmlmathplot 0.1.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.
- qmlmathplot/__init__.py +84 -0
- qmlmathplot/app.py +79 -0
- qmlmathplot/camera.py +366 -0
- qmlmathplot/curve.py +296 -0
- qmlmathplot/model.py +541 -0
- qmlmathplot/plot.py +213 -0
- qmlmathplot/qml/PlotView.qml +247 -0
- qmlmathplot/qsb.py +96 -0
- qmlmathplot/themes/LICENSE.matplotlib +99 -0
- qmlmathplot/themes/README.md +15 -0
- qmlmathplot/themes/Solarize_Light2.mplstyle +53 -0
- qmlmathplot/themes/bmh.mplstyle +27 -0
- qmlmathplot/themes/classic.mplstyle +491 -0
- qmlmathplot/themes/dark_background.mplstyle +26 -0
- qmlmathplot/themes/fast.mplstyle +11 -0
- qmlmathplot/themes/fivethirtyeight.mplstyle +37 -0
- qmlmathplot/themes/ggplot.mplstyle +39 -0
- qmlmathplot/themes/grayscale.mplstyle +29 -0
- qmlmathplot/themes/petroff10.mplstyle +5 -0
- qmlmathplot/themes/seaborn-v0_8-bright.mplstyle +3 -0
- qmlmathplot/themes/seaborn-v0_8-colorblind.mplstyle +3 -0
- qmlmathplot/themes/seaborn-v0_8-dark-palette.mplstyle +3 -0
- qmlmathplot/themes/seaborn-v0_8-dark.mplstyle +30 -0
- qmlmathplot/themes/seaborn-v0_8-darkgrid.mplstyle +30 -0
- qmlmathplot/themes/seaborn-v0_8-deep.mplstyle +3 -0
- qmlmathplot/themes/seaborn-v0_8-muted.mplstyle +3 -0
- qmlmathplot/themes/seaborn-v0_8-notebook.mplstyle +21 -0
- qmlmathplot/themes/seaborn-v0_8-paper.mplstyle +21 -0
- qmlmathplot/themes/seaborn-v0_8-pastel.mplstyle +3 -0
- qmlmathplot/themes/seaborn-v0_8-poster.mplstyle +21 -0
- qmlmathplot/themes/seaborn-v0_8-talk.mplstyle +21 -0
- qmlmathplot/themes/seaborn-v0_8-ticks.mplstyle +30 -0
- qmlmathplot/themes/seaborn-v0_8-white.mplstyle +30 -0
- qmlmathplot/themes/seaborn-v0_8-whitegrid.mplstyle +30 -0
- qmlmathplot/themes/seaborn-v0_8.mplstyle +57 -0
- qmlmathplot/themes/tableau-colorblind10.mplstyle +3 -0
- qmlmathplot/themes.py +236 -0
- qmlmathplot/view.py +55 -0
- qmlmathplot/widget.py +183 -0
- qmlmathplot-0.1.0.dist-info/METADATA +383 -0
- qmlmathplot-0.1.0.dist-info/RECORD +43 -0
- qmlmathplot-0.1.0.dist-info/WHEEL +4 -0
- qmlmathplot-0.1.0.dist-info/entry_points.txt +3 -0
qmlmathplot/curve.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Curve layer: one expression -> GLSL -> .qsb plus its style, and the list model QML repeats.
|
|
2
|
+
|
|
3
|
+
A curve knows nothing about the view: the camera decides where we look, the curve decides
|
|
4
|
+
what is drawn. One curve = one fragment shader pass (see ``model.FRAGMENT_TEMPLATE``), so
|
|
5
|
+
``visible`` is the lever that costs nothing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sympy as sp
|
|
11
|
+
from PySide6.QtCore import (
|
|
12
|
+
Property,
|
|
13
|
+
QAbstractListModel,
|
|
14
|
+
QModelIndex,
|
|
15
|
+
QObject,
|
|
16
|
+
Qt,
|
|
17
|
+
QUrl,
|
|
18
|
+
Signal,
|
|
19
|
+
Slot,
|
|
20
|
+
)
|
|
21
|
+
from PySide6.QtGui import QColor
|
|
22
|
+
|
|
23
|
+
from .model import shader_sources
|
|
24
|
+
from .qsb import bake
|
|
25
|
+
|
|
26
|
+
__all__ = ["Curve", "CurveListModel"]
|
|
27
|
+
|
|
28
|
+
#: First colour of the default cycle (also the classic single-curve colour).
|
|
29
|
+
DEFAULT_COLOR = "#33ccff"
|
|
30
|
+
DEFAULT_COLOR_CYCLE = ("#33ccff", "#ff8866", "#7ddc7d", "#e0c341", "#c98bff")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _color(value: object) -> QColor:
|
|
34
|
+
"""A QColor from a QColor or anything QColor() understands ("#rrggbb", a name, …)."""
|
|
35
|
+
return QColor(value) if not isinstance(value, QColor) else QColor(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Curve(QObject):
|
|
39
|
+
"""One curve: an expression, its baked shaders, its style and its error text."""
|
|
40
|
+
|
|
41
|
+
expressionChanged = Signal()
|
|
42
|
+
styleChanged = Signal()
|
|
43
|
+
visibleChanged = Signal()
|
|
44
|
+
labelChanged = Signal()
|
|
45
|
+
errorChanged = Signal()
|
|
46
|
+
shadersChanged = Signal()
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
expression: str = "sin(x)",
|
|
51
|
+
color: str | QColor = DEFAULT_COLOR,
|
|
52
|
+
line_width: float = 1.5,
|
|
53
|
+
label: str = "",
|
|
54
|
+
parent: QObject | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(parent)
|
|
57
|
+
self._symbol = sp.Symbol("x")
|
|
58
|
+
self._expression = str(expression)
|
|
59
|
+
self._color = _color(color)
|
|
60
|
+
self._line_width = float(line_width)
|
|
61
|
+
self._label = str(label)
|
|
62
|
+
self._visible = True
|
|
63
|
+
self._error = ""
|
|
64
|
+
self._vertex = QUrl()
|
|
65
|
+
self._fragment = QUrl()
|
|
66
|
+
self._compile(self._expression)
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------- Expression
|
|
69
|
+
def _get_expression(self) -> str:
|
|
70
|
+
return self._expression
|
|
71
|
+
|
|
72
|
+
def _set_expression(self, source: str) -> None:
|
|
73
|
+
source = str(source)
|
|
74
|
+
if source == self._expression:
|
|
75
|
+
return
|
|
76
|
+
self._expression = source
|
|
77
|
+
self.expressionChanged.emit()
|
|
78
|
+
self._compile(source)
|
|
79
|
+
|
|
80
|
+
# Annotate the value type (PySide6 idiom): Property is a descriptor, so without the
|
|
81
|
+
# annotation a type checker only sees the Property object and flags every read and write.
|
|
82
|
+
expression: str = Property(str, _get_expression, _set_expression, notify=expressionChanged)
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------ Style
|
|
85
|
+
def _get_color(self) -> QColor:
|
|
86
|
+
return QColor(self._color)
|
|
87
|
+
|
|
88
|
+
def _set_color(self, value: object) -> None:
|
|
89
|
+
color = _color(value)
|
|
90
|
+
if color == self._color:
|
|
91
|
+
return
|
|
92
|
+
self._color = color
|
|
93
|
+
self.styleChanged.emit()
|
|
94
|
+
|
|
95
|
+
color: QColor = Property(QColor, _get_color, _set_color, notify=styleChanged)
|
|
96
|
+
|
|
97
|
+
def _get_line_width(self) -> float:
|
|
98
|
+
return self._line_width
|
|
99
|
+
|
|
100
|
+
def _set_line_width(self, value: float) -> None:
|
|
101
|
+
value = float(value)
|
|
102
|
+
if value == self._line_width:
|
|
103
|
+
return
|
|
104
|
+
self._line_width = value
|
|
105
|
+
self.styleChanged.emit()
|
|
106
|
+
|
|
107
|
+
lineWidth: float = Property(float, _get_line_width, _set_line_width, notify=styleChanged)
|
|
108
|
+
|
|
109
|
+
def _get_visible(self) -> bool:
|
|
110
|
+
return self._visible
|
|
111
|
+
|
|
112
|
+
def _set_visible(self, value: bool) -> None:
|
|
113
|
+
value = bool(value)
|
|
114
|
+
if value == self._visible:
|
|
115
|
+
return
|
|
116
|
+
self._visible = value
|
|
117
|
+
self.visibleChanged.emit()
|
|
118
|
+
|
|
119
|
+
visible: bool = Property(bool, _get_visible, _set_visible, notify=visibleChanged)
|
|
120
|
+
|
|
121
|
+
def _get_label(self) -> str:
|
|
122
|
+
return self._label
|
|
123
|
+
|
|
124
|
+
def _set_label(self, value: str) -> None:
|
|
125
|
+
value = str(value)
|
|
126
|
+
if value == self._label:
|
|
127
|
+
return
|
|
128
|
+
self._label = value
|
|
129
|
+
self.labelChanged.emit()
|
|
130
|
+
|
|
131
|
+
label: str = Property(str, _get_label, _set_label, notify=labelChanged)
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------- Shader URLs
|
|
134
|
+
def _get_vertex_shader(self) -> QUrl:
|
|
135
|
+
return self._vertex
|
|
136
|
+
|
|
137
|
+
def _get_fragment_shader(self) -> QUrl:
|
|
138
|
+
return self._fragment
|
|
139
|
+
|
|
140
|
+
vertexShader: QUrl = Property(QUrl, _get_vertex_shader, notify=shadersChanged)
|
|
141
|
+
fragmentShader: QUrl = Property(QUrl, _get_fragment_shader, notify=shadersChanged)
|
|
142
|
+
|
|
143
|
+
def _get_error(self) -> str:
|
|
144
|
+
return self._error
|
|
145
|
+
|
|
146
|
+
error: str = Property(str, _get_error, notify=errorChanged)
|
|
147
|
+
|
|
148
|
+
def _compile(self, source: str) -> None:
|
|
149
|
+
"""expression -> GLSL -> .qsb; on failure keep the last working shaders and write the
|
|
150
|
+
reason into ``error``."""
|
|
151
|
+
try:
|
|
152
|
+
expr = sp.sympify(source, locals={"x": self._symbol})
|
|
153
|
+
vert_src, frag_src = shader_sources(expr)
|
|
154
|
+
vertex = bake(vert_src, "vert")
|
|
155
|
+
fragment = bake(frag_src, "frag")
|
|
156
|
+
except Exception as exc: # noqa: BLE001 — expression or bake can fail; both reach the UI
|
|
157
|
+
self._error = f"{type(exc).__name__}: {exc}"
|
|
158
|
+
self.errorChanged.emit()
|
|
159
|
+
return
|
|
160
|
+
self._error = ""
|
|
161
|
+
self._vertex = QUrl.fromLocalFile(str(vertex))
|
|
162
|
+
self._fragment = QUrl.fromLocalFile(str(fragment))
|
|
163
|
+
self.errorChanged.emit()
|
|
164
|
+
self.shadersChanged.emit()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class CurveListModel(QAbstractListModel):
|
|
168
|
+
"""The plot's curves, as a model so QML can put one ShaderEffect behind each of them."""
|
|
169
|
+
|
|
170
|
+
CurveRole = int(Qt.ItemDataRole.UserRole) + 1
|
|
171
|
+
ExpressionRole = CurveRole + 1
|
|
172
|
+
ColorRole = CurveRole + 2
|
|
173
|
+
LineWidthRole = CurveRole + 3
|
|
174
|
+
VisibleRole = CurveRole + 4
|
|
175
|
+
VertexShaderRole = CurveRole + 5
|
|
176
|
+
FragmentShaderRole = CurveRole + 6
|
|
177
|
+
|
|
178
|
+
_ROLES = {
|
|
179
|
+
CurveRole: b"curve",
|
|
180
|
+
ExpressionRole: b"expression",
|
|
181
|
+
ColorRole: b"color",
|
|
182
|
+
LineWidthRole: b"lineWidth",
|
|
183
|
+
VisibleRole: b"visible",
|
|
184
|
+
VertexShaderRole: b"vertexShader",
|
|
185
|
+
FragmentShaderRole: b"fragmentShader",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
color_cycle: tuple[str, ...] = DEFAULT_COLOR_CYCLE,
|
|
191
|
+
default_line_width: float = 1.5,
|
|
192
|
+
parent: QObject | None = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
super().__init__(parent)
|
|
195
|
+
self._curves: list[Curve] = []
|
|
196
|
+
self._cycle = [str(entry) for entry in color_cycle] or [DEFAULT_COLOR]
|
|
197
|
+
self._default_line_width = float(default_line_width)
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------ model glue
|
|
200
|
+
def roleNames(self) -> dict[int, bytes]: # noqa: N802 (Qt naming)
|
|
201
|
+
return dict(self._ROLES)
|
|
202
|
+
|
|
203
|
+
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: # noqa: N802
|
|
204
|
+
return 0 if parent.isValid() else len(self._curves)
|
|
205
|
+
|
|
206
|
+
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> object:
|
|
207
|
+
if not index.isValid() or not 0 <= index.row() < len(self._curves):
|
|
208
|
+
return None
|
|
209
|
+
curve = self._curves[index.row()]
|
|
210
|
+
if role == self.CurveRole:
|
|
211
|
+
return curve
|
|
212
|
+
if role == self.ExpressionRole:
|
|
213
|
+
return curve.expression
|
|
214
|
+
if role == self.ColorRole:
|
|
215
|
+
return curve.color
|
|
216
|
+
if role == self.LineWidthRole:
|
|
217
|
+
return curve.lineWidth
|
|
218
|
+
if role == self.VisibleRole:
|
|
219
|
+
return curve.visible
|
|
220
|
+
if role == self.VertexShaderRole:
|
|
221
|
+
return curve.vertexShader
|
|
222
|
+
if role == self.FragmentShaderRole:
|
|
223
|
+
return curve.fragmentShader
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def _notify(self, curve: Curve, roles: tuple[int, ...]) -> None:
|
|
227
|
+
row = self.index_of(curve)
|
|
228
|
+
if row < 0:
|
|
229
|
+
return
|
|
230
|
+
index = self.index(row, 0)
|
|
231
|
+
self.dataChanged.emit(index, index, list(roles))
|
|
232
|
+
|
|
233
|
+
def _watch(self, curve: Curve) -> None:
|
|
234
|
+
curve.expressionChanged.connect(
|
|
235
|
+
lambda c=curve: self._notify(c, (self.ExpressionRole,))
|
|
236
|
+
)
|
|
237
|
+
curve.shadersChanged.connect(
|
|
238
|
+
lambda c=curve: self._notify(c, (self.VertexShaderRole, self.FragmentShaderRole))
|
|
239
|
+
)
|
|
240
|
+
curve.styleChanged.connect(
|
|
241
|
+
lambda c=curve: self._notify(c, (self.ColorRole, self.LineWidthRole))
|
|
242
|
+
)
|
|
243
|
+
curve.visibleChanged.connect(lambda c=curve: self._notify(c, (self.VisibleRole,)))
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------- public
|
|
246
|
+
@Slot(str, "QVariant", "QVariant", str, result=QObject)
|
|
247
|
+
@Slot(str, result=QObject)
|
|
248
|
+
def add_curve(
|
|
249
|
+
self,
|
|
250
|
+
expression: str,
|
|
251
|
+
color: object = None,
|
|
252
|
+
line_width: object = None,
|
|
253
|
+
label: str = "",
|
|
254
|
+
) -> Curve:
|
|
255
|
+
"""Append a curve. Colour and line width default to the plot's cycle and width."""
|
|
256
|
+
if color is None:
|
|
257
|
+
color = self._cycle[len(self._curves) % len(self._cycle)]
|
|
258
|
+
if line_width is None:
|
|
259
|
+
line_width = self._default_line_width
|
|
260
|
+
curve = Curve(expression, color, float(line_width), label, parent=self) # type: ignore[arg-type]
|
|
261
|
+
row = len(self._curves)
|
|
262
|
+
self.beginInsertRows(QModelIndex(), row, row)
|
|
263
|
+
self._curves.append(curve)
|
|
264
|
+
self.endInsertRows()
|
|
265
|
+
self._watch(curve)
|
|
266
|
+
return curve
|
|
267
|
+
|
|
268
|
+
@Slot(QObject)
|
|
269
|
+
def remove_curve(self, curve: Curve) -> None:
|
|
270
|
+
row = self.index_of(curve)
|
|
271
|
+
if row < 0:
|
|
272
|
+
return
|
|
273
|
+
self.beginRemoveRows(QModelIndex(), row, row)
|
|
274
|
+
del self._curves[row]
|
|
275
|
+
self.endRemoveRows()
|
|
276
|
+
|
|
277
|
+
@Slot(int, result=QObject)
|
|
278
|
+
def at(self, row: int) -> Curve:
|
|
279
|
+
return self._curves[row]
|
|
280
|
+
|
|
281
|
+
def index_of(self, curve: Curve) -> int:
|
|
282
|
+
"""Row of ``curve``, or -1 (QML calls go through ``at``; this one is for Python)."""
|
|
283
|
+
for row, candidate in enumerate(self._curves):
|
|
284
|
+
if candidate is curve:
|
|
285
|
+
return row
|
|
286
|
+
return -1
|
|
287
|
+
|
|
288
|
+
def set_color_cycle(self, color_cycle: tuple[str, ...]) -> None:
|
|
289
|
+
"""Colours handed out by ``add_curve`` (the plot's ``colorCycle``)."""
|
|
290
|
+
self._cycle = [str(entry) for entry in color_cycle] or [DEFAULT_COLOR]
|
|
291
|
+
|
|
292
|
+
def set_default_line_width(self, width: float) -> None:
|
|
293
|
+
self._default_line_width = float(width)
|
|
294
|
+
|
|
295
|
+
def __len__(self) -> int:
|
|
296
|
+
return len(self._curves)
|