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.
Files changed (43) hide show
  1. qmlmathplot/__init__.py +84 -0
  2. qmlmathplot/app.py +79 -0
  3. qmlmathplot/camera.py +366 -0
  4. qmlmathplot/curve.py +296 -0
  5. qmlmathplot/model.py +541 -0
  6. qmlmathplot/plot.py +213 -0
  7. qmlmathplot/qml/PlotView.qml +247 -0
  8. qmlmathplot/qsb.py +96 -0
  9. qmlmathplot/themes/LICENSE.matplotlib +99 -0
  10. qmlmathplot/themes/README.md +15 -0
  11. qmlmathplot/themes/Solarize_Light2.mplstyle +53 -0
  12. qmlmathplot/themes/bmh.mplstyle +27 -0
  13. qmlmathplot/themes/classic.mplstyle +491 -0
  14. qmlmathplot/themes/dark_background.mplstyle +26 -0
  15. qmlmathplot/themes/fast.mplstyle +11 -0
  16. qmlmathplot/themes/fivethirtyeight.mplstyle +37 -0
  17. qmlmathplot/themes/ggplot.mplstyle +39 -0
  18. qmlmathplot/themes/grayscale.mplstyle +29 -0
  19. qmlmathplot/themes/petroff10.mplstyle +5 -0
  20. qmlmathplot/themes/seaborn-v0_8-bright.mplstyle +3 -0
  21. qmlmathplot/themes/seaborn-v0_8-colorblind.mplstyle +3 -0
  22. qmlmathplot/themes/seaborn-v0_8-dark-palette.mplstyle +3 -0
  23. qmlmathplot/themes/seaborn-v0_8-dark.mplstyle +30 -0
  24. qmlmathplot/themes/seaborn-v0_8-darkgrid.mplstyle +30 -0
  25. qmlmathplot/themes/seaborn-v0_8-deep.mplstyle +3 -0
  26. qmlmathplot/themes/seaborn-v0_8-muted.mplstyle +3 -0
  27. qmlmathplot/themes/seaborn-v0_8-notebook.mplstyle +21 -0
  28. qmlmathplot/themes/seaborn-v0_8-paper.mplstyle +21 -0
  29. qmlmathplot/themes/seaborn-v0_8-pastel.mplstyle +3 -0
  30. qmlmathplot/themes/seaborn-v0_8-poster.mplstyle +21 -0
  31. qmlmathplot/themes/seaborn-v0_8-talk.mplstyle +21 -0
  32. qmlmathplot/themes/seaborn-v0_8-ticks.mplstyle +30 -0
  33. qmlmathplot/themes/seaborn-v0_8-white.mplstyle +30 -0
  34. qmlmathplot/themes/seaborn-v0_8-whitegrid.mplstyle +30 -0
  35. qmlmathplot/themes/seaborn-v0_8.mplstyle +57 -0
  36. qmlmathplot/themes/tableau-colorblind10.mplstyle +3 -0
  37. qmlmathplot/themes.py +236 -0
  38. qmlmathplot/view.py +55 -0
  39. qmlmathplot/widget.py +183 -0
  40. qmlmathplot-0.1.0.dist-info/METADATA +383 -0
  41. qmlmathplot-0.1.0.dist-info/RECORD +43 -0
  42. qmlmathplot-0.1.0.dist-info/WHEEL +4 -0
  43. 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)