QT-PyQt-PySide-Custom-Widgets-Pro 1.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 (45) hide show
  1. custom_widgets_pro/__init__.py +73 -0
  2. custom_widgets_pro/_catalog.py +25 -0
  3. custom_widgets_pro/_cli.py +132 -0
  4. custom_widgets_pro/_license.py +419 -0
  5. custom_widgets_pro/_version.py +14 -0
  6. custom_widgets_pro/datatable/__init__.py +17 -0
  7. custom_widgets_pro/datatable/datatable_pro.py +319 -0
  8. custom_widgets_pro/datatable/export.py +143 -0
  9. custom_widgets_pro/datatable/frozen_view.py +112 -0
  10. custom_widgets_pro/datatable/grouping.py +309 -0
  11. custom_widgets_pro/datatable/pivot.py +100 -0
  12. custom_widgets_pro/datatable/provider.py +98 -0
  13. custom_widgets_pro/datatable/virtual_model.py +451 -0
  14. custom_widgets_pro/widgets/__init__.py +7 -0
  15. custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
  16. custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
  17. custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
  18. custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
  19. custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
  20. custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
  21. custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
  22. custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
  23. custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
  24. custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
  25. custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
  26. custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
  27. custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
  28. custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
  29. custom_widgets_pro/widgets/charts/__init__.py +1 -0
  30. custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
  31. custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
  32. custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
  33. custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
  34. custom_widgets_pro/widgets/data/__init__.py +1 -0
  35. custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
  36. custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
  37. custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
  38. custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
  39. custom_widgets_pro/widgets/media/__init__.py +1 -0
  40. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
  41. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
  42. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
  43. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
  44. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
  45. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,600 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
4
+ # WEBSITE: customwidgets.org
5
+ ########################################################################
6
+
7
+ ########################################################################
8
+ ## QCustomRadarChart - a painted radar / spider chart.
9
+ ##
10
+ ## N axes radiating from a centre, one filled polygon per series. Used to
11
+ ## compare several entities across the same set of measures - the shape of the
12
+ ## polygon is the comparison, which no cartesian chart gives you.
13
+ ##
14
+ ## This is the catalog's first polar chart of any kind. Rendered entirely with
15
+ ## QPainter and NO QtCharts: Qt Charts is GPLv3-or-commercial with no LGPL
16
+ ## option, so anything built on it cannot ship inside a proprietary wheel (see
17
+ ## docs/design/mui-charts-gap.md). A test asserts the import graph stays clean.
18
+ ##
19
+ ## Data goes in with setAxes([...]) + addSeries(name, values) in code, or the
20
+ ## axesCsv / seriesCsv properties in Qt Designer, following the same convention
21
+ ## as the other charts:
22
+ ##
23
+ ## axesCsv = "Speed,Power,Range,Agility"
24
+ ## seriesCsv = "Alpha=80,60,90,70;Beta=60,90,50,80"
25
+ ##
26
+ ## Emits seriesHovered(int) and axisClicked(int); -1 means "nothing".
27
+ ########################################################################
28
+ import math
29
+
30
+ from qtpy.QtCore import Qt, Signal, Property, QRectF, QSize, QPointF
31
+ from qtpy.QtGui import (QColor, QPainter, QPen, QBrush, QPolygonF, QFontMetrics)
32
+ from qtpy.QtWidgets import QWidget, QSizePolicy
33
+
34
+
35
+ class QCustomRadarChart(QWidget):
36
+ seriesHovered = Signal(int)
37
+ axisClicked = Signal(int)
38
+
39
+ WIDGET_ICON = "components/icons/radar.png"
40
+ WIDGET_TOOLTIP = "A painted radar / spider chart"
41
+ WIDGET_MODULE = "Custom_Widgets.QCustomRadarChart"
42
+ WIDGET_DOM_XML = """
43
+ <ui language='c++'>
44
+ <widget class='QCustomRadarChart' name='customRadarChart'>
45
+ <property name='geometry'><rect><x>0</x><y>0</y><width>320</width><height>300</height></rect></property>
46
+ <property name='axesCsv'><string>Speed,Power,Range,Agility,Cost</string></property>
47
+ <property name='seriesCsv'><string>Alpha=80,60,90,70,50</string></property>
48
+ </widget>
49
+ </ui>
50
+ """
51
+ __catalog__ = {
52
+ "name": "QCustomRadarChart",
53
+ "props": {"axesCsv": {"type": "string", "default": ""},
54
+ "seriesCsv": {"type": "string", "default": ""},
55
+ "seriesColorsCsv": {"type": "string", "default": ""},
56
+ "maxValue": {"type": "float", "default": 0.0},
57
+ "rings": {"type": "int", "default": 4},
58
+ "gridStyle": {"type": "enum", "values": ["polygon", "circle"],
59
+ "default": "polygon"},
60
+ "startAngle": {"type": "int", "default": 90},
61
+ "fillOpacity": {"type": "float", "default": 0.25},
62
+ "lineWidth": {"type": "float", "default": 2.0},
63
+ "showAxisLabels": {"type": "bool", "default": True},
64
+ "showRingLabels": {"type": "bool", "default": False},
65
+ "showMarkers": {"type": "bool", "default": True},
66
+ "showLegend": {"type": "bool", "default": True},
67
+ "gridColor": {"type": "color", "default": "#e2e8f0"},
68
+ "axisColor": {"type": "color", "default": "#64748b"},
69
+ "labelColor": {"type": "color", "default": "#0f172a"}},
70
+ "signals": ["seriesHovered", "axisClicked"],
71
+ "tokens_used": ["on-surface", "on-surface-muted", "surface-muted"],
72
+ }
73
+
74
+ # Rich editors for the Designer "Custom Properties" dock (see
75
+ # DesignerTools.CustomPropertiesDock).
76
+ DESIGNER_CUSTOM_PROPS = [
77
+ {"name": "axesCsv", "kind": "str", "group": "General"},
78
+ {"name": "seriesCsv", "kind": "str", "group": "General"},
79
+ {"name": "seriesColorsCsv", "kind": "str", "group": "Colors"},
80
+ {"name": "maxValue", "kind": "float", "group": "General"},
81
+ {"name": "rings", "kind": "int", "group": "General"},
82
+ {"name": "gridStyle", "kind": "choice", "group": "General"},
83
+ {"name": "startAngle", "kind": "int", "group": "General"},
84
+ {"name": "fillOpacity", "kind": "float", "group": "General"},
85
+ {"name": "lineWidth", "kind": "float", "group": "General"},
86
+ {"name": "showAxisLabels", "kind": "bool", "group": "General"},
87
+ {"name": "showRingLabels", "kind": "bool", "group": "General"},
88
+ {"name": "showMarkers", "kind": "bool", "group": "General"},
89
+ {"name": "showLegend", "kind": "bool", "group": "General"},
90
+ {"name": "gridColor", "kind": "color", "group": "Colors"},
91
+ {"name": "axisColor", "kind": "color", "group": "Colors"},
92
+ {"name": "labelColor", "kind": "color", "group": "Colors"},
93
+ ]
94
+
95
+ _DEFAULT_COLORS = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed",
96
+ "#0891b2"]
97
+ _LEGEND_SWATCH = 10
98
+
99
+ def __init__(self, parent=None, axes=None, series=None):
100
+ super().__init__(parent)
101
+ self.setObjectName("QCustomRadarChart")
102
+ self._axes = [] # axis labels
103
+ self._series = [] # list of (name, [values])
104
+ self._colors = [] # explicit per-series colours
105
+ self._maxValue = 0.0 # 0 = derive from the data
106
+ self._rings = 4
107
+ self._gridStyle = "polygon"
108
+ self._startAngle = 90 # degrees, counter-clockwise from +x
109
+ self._fillOpacity = 0.25
110
+ self._lineWidth = 2.0
111
+ self._showAxisLabels = True
112
+ self._showRingLabels = False
113
+ self._showMarkers = True
114
+ self._showLegend = True
115
+ self._hover = -1
116
+
117
+ self._gridColor = QColor("#e2e8f0")
118
+ # Was #cbd5e1 -- the light "outline" value, 1.48:1, so the spokes that
119
+ # give a radar its shape were invisible until a theme was applied. This
120
+ # is light-theme on-surface-muted, matching the token QSS. The web
121
+ # rings above stay faint.
122
+ self._axisColor = QColor("#64748b")
123
+ self._labelColor = QColor("#0f172a")
124
+
125
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
126
+ self.setMouseTracking(True)
127
+ if axes:
128
+ self.setAxes(axes)
129
+ for name, values in (series or []):
130
+ self.addSeries(name, values)
131
+
132
+ # ------------------------------------------------------------------ #
133
+ ## Data
134
+ # ------------------------------------------------------------------ #
135
+ def setAxes(self, labels):
136
+ """Replace the axis labels. Existing series are re-fitted to the new
137
+ count so a shortened axis list cannot leave a series over-long."""
138
+ self._axes = [str(x) for x in (labels or [])]
139
+ self._series = [(name, self._fit(values)) for name, values in self._series]
140
+ self.update()
141
+
142
+ def axes(self):
143
+ return list(self._axes)
144
+
145
+ def axisCount(self):
146
+ return len(self._axes)
147
+
148
+ def _fit(self, values):
149
+ """Pad with zeros / truncate so a series matches the axis count."""
150
+ count = len(self._axes)
151
+ values = [float(v) for v in values][:count]
152
+ return values + [0.0] * (count - len(values))
153
+
154
+ def addSeries(self, name, values, color=None):
155
+ self._series.append((str(name), self._fit(values)))
156
+ if color is not None:
157
+ while len(self._colors) < len(self._series) - 1:
158
+ self._colors.append(None)
159
+ self._colors.append(QColor(color))
160
+ self.update()
161
+ return len(self._series) - 1
162
+
163
+ def setSeries(self, series):
164
+ """Replace every series. Each item is (name, values)."""
165
+ self._series = []
166
+ for item in series or []:
167
+ if isinstance(item, dict):
168
+ self._series.append((str(item.get("name", "")),
169
+ self._fit(item.get("values", []))))
170
+ elif isinstance(item, (tuple, list)) and len(item) >= 2:
171
+ self._series.append((str(item[0]), self._fit(item[1])))
172
+ self.update()
173
+
174
+ def series(self):
175
+ return [(name, list(values)) for name, values in self._series]
176
+
177
+ def seriesCount(self):
178
+ return len(self._series)
179
+
180
+ def removeSeries(self, index):
181
+ if 0 <= index < len(self._series):
182
+ del self._series[index]
183
+ if index < len(self._colors):
184
+ del self._colors[index]
185
+ self._hover = -1
186
+ self.update()
187
+ return True
188
+ return False
189
+
190
+ def clearSeries(self):
191
+ self._series = []
192
+ self._colors = []
193
+ self._hover = -1
194
+ self.update()
195
+
196
+ def seriesColor(self, index):
197
+ if 0 <= index < len(self._colors) and self._colors[index] is not None:
198
+ return QColor(self._colors[index])
199
+ if not self._DEFAULT_COLORS:
200
+ return QColor("#2563eb")
201
+ return QColor(self._DEFAULT_COLORS[index % len(self._DEFAULT_COLORS)])
202
+
203
+ def maximum(self):
204
+ """The value the outermost ring represents."""
205
+ if self._maxValue > 0:
206
+ return self._maxValue
207
+ peak = 0.0
208
+ for _name, values in self._series:
209
+ for v in values:
210
+ peak = max(peak, v)
211
+ return peak or 1.0
212
+
213
+ # ------------------------------------------------------------------ #
214
+ ## Geometry
215
+ # ------------------------------------------------------------------ #
216
+ def sizeHint(self):
217
+ return QSize(320, 300)
218
+
219
+ def minimumSizeHint(self):
220
+ return QSize(120, 120)
221
+
222
+ def _legendHeight(self):
223
+ if not (self._showLegend and self._series):
224
+ return 0.0
225
+ return QFontMetrics(self.font()).height() + 10
226
+
227
+ def _plotRect(self):
228
+ pad = 8.0
229
+ bottom = self._legendHeight()
230
+ if self._showAxisLabels and self._axes:
231
+ pad += QFontMetrics(self.font()).height() + 6
232
+ side = min(self.width() - 2 * pad, self.height() - 2 * pad - bottom)
233
+ side = max(10.0, side)
234
+ return QRectF((self.width() - side) / 2.0,
235
+ (self.height() - bottom - side) / 2.0, side, side)
236
+
237
+ def _angleFor(self, index):
238
+ """Radians for axis `index`, running clockwise from startAngle."""
239
+ count = max(1, len(self._axes))
240
+ return math.radians(self._startAngle) - index * (2 * math.pi / count)
241
+
242
+ def _pointAt(self, index, fraction):
243
+ """Cartesian point `fraction` (0..1) of the way along an axis."""
244
+ rect = self._plotRect()
245
+ radius = rect.width() / 2.0 * max(0.0, min(1.0, fraction))
246
+ angle = self._angleFor(index)
247
+ return QPointF(rect.center().x() + radius * math.cos(angle),
248
+ rect.center().y() - radius * math.sin(angle))
249
+
250
+ def _ringLabelPoint(self, fraction):
251
+ """Where a ring's value label sits.
252
+
253
+ On the bisector between the first two axes, not along axis 0: an axis
254
+ already carries its own label at the rim, and the outermost ring label
255
+ collided with it.
256
+ """
257
+ rect = self._plotRect()
258
+ count = max(1, len(self._axes))
259
+ angle = self._angleFor(0) - (math.pi / count)
260
+ radius = rect.width() / 2.0 * max(0.0, min(1.0, fraction))
261
+ return QPointF(rect.center().x() + radius * math.cos(angle),
262
+ rect.center().y() - radius * math.sin(angle))
263
+
264
+ def _polygonFor(self, values):
265
+ peak = self.maximum()
266
+ poly = QPolygonF()
267
+ for i, value in enumerate(values):
268
+ poly.append(self._pointAt(i, value / peak if peak else 0.0))
269
+ return poly
270
+
271
+ # ------------------------------------------------------------------ #
272
+ ## Painting
273
+ # ------------------------------------------------------------------ #
274
+ def paintEvent(self, e):
275
+ p = QPainter(self)
276
+ p.setRenderHint(QPainter.Antialiasing)
277
+ p.setFont(self.font())
278
+ if not self._axes:
279
+ return
280
+
281
+ self._paintGrid(p)
282
+ self._paintAxes(p)
283
+ for index, (_name, values) in enumerate(self._series):
284
+ self._paintSeries(p, index, values)
285
+ if self._showAxisLabels:
286
+ self._paintAxisLabels(p)
287
+ if self._showLegend and self._series:
288
+ self._paintLegend(p)
289
+
290
+ def _paintGrid(self, p):
291
+ rect = self._plotRect()
292
+ p.setBrush(Qt.NoBrush)
293
+ p.setPen(QPen(self._gridColor, 1))
294
+ for ring in range(1, max(1, self._rings) + 1):
295
+ fraction = ring / float(max(1, self._rings))
296
+ if self._gridStyle == "circle":
297
+ radius = rect.width() / 2.0 * fraction
298
+ p.drawEllipse(rect.center(), radius, radius)
299
+ else:
300
+ poly = QPolygonF([self._pointAt(i, fraction)
301
+ for i in range(len(self._axes))])
302
+ p.drawPolygon(poly)
303
+ if self._showRingLabels:
304
+ value = self.maximum() * fraction
305
+ point = self._ringLabelPoint(fraction)
306
+ p.setPen(QPen(self._labelColor))
307
+ p.drawText(QPointF(point.x() + 3, point.y() - 2), "%g" % value)
308
+ p.setPen(QPen(self._gridColor, 1))
309
+
310
+ def _paintAxes(self, p):
311
+ p.setPen(QPen(self._axisColor, 1))
312
+ for i in range(len(self._axes)):
313
+ p.drawLine(self._plotRect().center(), self._pointAt(i, 1.0))
314
+
315
+ def _paintSeries(self, p, index, values):
316
+ colour = self.seriesColor(index)
317
+ hovered = index == self._hover
318
+ # A hovered series is drawn more opaque rather than moved, so the
319
+ # shapes stay comparable while one is emphasised.
320
+ fill = QColor(colour)
321
+ opacity = self._fillOpacity * (1.9 if hovered else 1.0)
322
+ fill.setAlphaF(max(0.0, min(1.0, opacity)))
323
+ poly = self._polygonFor(values)
324
+
325
+ p.setBrush(QBrush(fill))
326
+ p.setPen(QPen(colour, self._lineWidth + (1.0 if hovered else 0.0)))
327
+ p.drawPolygon(poly)
328
+
329
+ if self._showMarkers:
330
+ p.setBrush(QBrush(colour))
331
+ p.setPen(Qt.NoPen)
332
+ radius = 3.0 + (1.0 if hovered else 0.0)
333
+ for point in poly:
334
+ p.drawEllipse(point, radius, radius)
335
+
336
+ def _paintAxisLabels(self, p):
337
+ fm = QFontMetrics(self.font())
338
+ p.setPen(QPen(self._labelColor))
339
+ for i, label in enumerate(self._axes):
340
+ anchor = self._pointAt(i, 1.08)
341
+ width = fm.horizontalAdvance(label)
342
+ height = fm.height()
343
+ # Nudge the box so a label never overlaps its own axis tip: left of
344
+ # centre reads right-aligned, right of centre left-aligned.
345
+ centre = self._plotRect().center()
346
+ if anchor.x() < centre.x() - 1:
347
+ x = anchor.x() - width
348
+ elif anchor.x() > centre.x() + 1:
349
+ x = anchor.x()
350
+ else:
351
+ x = anchor.x() - width / 2.0
352
+ y = anchor.y() - height / 2.0
353
+ p.drawText(QRectF(x, y, width, height),
354
+ int(Qt.AlignCenter), label)
355
+
356
+ def _paintLegend(self, p):
357
+ fm = QFontMetrics(self.font())
358
+ swatch = self._LEGEND_SWATCH
359
+ gap = 14.0
360
+ widths = [swatch + 5 + fm.horizontalAdvance(name)
361
+ for name, _v in self._series]
362
+ total = sum(widths) + gap * (len(widths) - 1)
363
+ x = (self.width() - total) / 2.0
364
+ y = self.height() - self._legendHeight() + 4
365
+
366
+ for index, (name, _values) in enumerate(self._series):
367
+ colour = self.seriesColor(index)
368
+ p.setPen(Qt.NoPen)
369
+ p.setBrush(QBrush(colour))
370
+ p.drawEllipse(QRectF(x, y + (fm.height() - swatch) / 2.0,
371
+ swatch, swatch))
372
+ p.setPen(QPen(self._labelColor))
373
+ p.drawText(QRectF(x + swatch + 5, y, widths[index], fm.height()),
374
+ int(Qt.AlignLeft | Qt.AlignVCenter), name)
375
+ x += widths[index] + gap
376
+
377
+ # ------------------------------------------------------------------ #
378
+ ## Interaction
379
+ # ------------------------------------------------------------------ #
380
+ def seriesAt(self, pos):
381
+ """Index of the series whose polygon contains a point, or -1.
382
+
383
+ Later series are checked first so the one painted on top wins, which
384
+ is what the user sees.
385
+ """
386
+ point = QPointF(pos)
387
+ for index in range(len(self._series) - 1, -1, -1):
388
+ if self._polygonFor(self._series[index][1]).containsPoint(
389
+ point, Qt.OddEvenFill):
390
+ return index
391
+ return -1
392
+
393
+ def axisAt(self, pos):
394
+ """Index of the nearest axis to a point, or -1 when outside the plot."""
395
+ if not self._axes:
396
+ return -1
397
+ rect = self._plotRect()
398
+ centre = rect.center()
399
+ dx, dy = QPointF(pos).x() - centre.x(), QPointF(pos).y() - centre.y()
400
+ distance = math.hypot(dx, dy)
401
+ if distance > rect.width() / 2.0 or distance < 1e-6:
402
+ return -1
403
+ angle = math.atan2(-dy, dx)
404
+ step = 2 * math.pi / len(self._axes)
405
+ offset = (math.radians(self._startAngle) - angle) % (2 * math.pi)
406
+ return int(round(offset / step)) % len(self._axes)
407
+
408
+ def mouseMoveEvent(self, e):
409
+ index = self.seriesAt(e.pos())
410
+ if index != self._hover:
411
+ self._hover = index
412
+ self.seriesHovered.emit(index)
413
+ self.update()
414
+ super().mouseMoveEvent(e)
415
+
416
+ def leaveEvent(self, e):
417
+ if self._hover != -1:
418
+ self._hover = -1
419
+ self.seriesHovered.emit(-1)
420
+ self.update()
421
+ super().leaveEvent(e)
422
+
423
+ def mouseReleaseEvent(self, e):
424
+ if e.button() == Qt.LeftButton:
425
+ index = self.axisAt(e.pos())
426
+ if index >= 0:
427
+ self.axisClicked.emit(index)
428
+ super().mouseReleaseEvent(e)
429
+
430
+ # ------------------------------------------------------------------ #
431
+ ## Designer properties
432
+ # ------------------------------------------------------------------ #
433
+ @Property(str)
434
+ def axesCsv(self):
435
+ return ",".join(self._axes)
436
+
437
+ @axesCsv.setter
438
+ def axesCsv(self, text):
439
+ self.setAxes([t.strip() for t in str(text).replace(";", ",").split(",")
440
+ if t.strip()])
441
+
442
+ @Property(str)
443
+ def seriesCsv(self):
444
+ return ";".join("%s=%s" % (name, ",".join("%g" % v for v in values))
445
+ for name, values in self._series)
446
+
447
+ @seriesCsv.setter
448
+ def seriesCsv(self, text):
449
+ parsed = []
450
+ for index, chunk in enumerate(str(text).replace("|", ";").split(";")):
451
+ chunk = chunk.strip()
452
+ if not chunk:
453
+ continue
454
+ if "=" in chunk:
455
+ name, _, raw = chunk.partition("=")
456
+ name = name.strip() or "Series %d" % (len(parsed) + 1)
457
+ else:
458
+ name, raw = "Series %d" % (len(parsed) + 1), chunk
459
+ values = []
460
+ for token in raw.split(","):
461
+ token = token.strip()
462
+ if not token:
463
+ continue
464
+ try:
465
+ values.append(float(token))
466
+ except ValueError:
467
+ pass
468
+ if values:
469
+ parsed.append((name, values))
470
+ self.setSeries(parsed)
471
+
472
+ @Property(str)
473
+ def seriesColorsCsv(self):
474
+ return ",".join(c.name() if c is not None else "" for c in self._colors)
475
+
476
+ @seriesColorsCsv.setter
477
+ def seriesColorsCsv(self, text):
478
+ colors = []
479
+ for token in str(text).replace(";", ",").split(","):
480
+ token = token.strip()
481
+ colour = QColor(token) if token else QColor()
482
+ colors.append(colour if colour.isValid() else None)
483
+ self._colors = colors
484
+ self.update()
485
+
486
+ @Property(float)
487
+ def maxValue(self):
488
+ return self._maxValue
489
+
490
+ @maxValue.setter
491
+ def maxValue(self, value):
492
+ self._maxValue = max(0.0, float(value))
493
+ self.update()
494
+
495
+ @Property(int)
496
+ def rings(self):
497
+ return self._rings
498
+
499
+ @rings.setter
500
+ def rings(self, value):
501
+ self._rings = max(1, int(value))
502
+ self.update()
503
+
504
+ @Property(str)
505
+ def gridStyle(self):
506
+ return self._gridStyle
507
+
508
+ @gridStyle.setter
509
+ def gridStyle(self, value):
510
+ self._gridStyle = "circle" if str(value) == "circle" else "polygon"
511
+ self.update()
512
+
513
+ @Property(int)
514
+ def startAngle(self):
515
+ return self._startAngle
516
+
517
+ @startAngle.setter
518
+ def startAngle(self, value):
519
+ self._startAngle = int(value) % 360
520
+ self.update()
521
+
522
+ @Property(float)
523
+ def fillOpacity(self):
524
+ return self._fillOpacity
525
+
526
+ @fillOpacity.setter
527
+ def fillOpacity(self, value):
528
+ self._fillOpacity = max(0.0, min(1.0, float(value)))
529
+ self.update()
530
+
531
+ @Property(float)
532
+ def lineWidth(self):
533
+ return self._lineWidth
534
+
535
+ @lineWidth.setter
536
+ def lineWidth(self, value):
537
+ self._lineWidth = max(0.0, float(value))
538
+ self.update()
539
+
540
+ @Property(bool)
541
+ def showAxisLabels(self):
542
+ return self._showAxisLabels
543
+
544
+ @showAxisLabels.setter
545
+ def showAxisLabels(self, value):
546
+ self._showAxisLabels = bool(value)
547
+ self.update()
548
+
549
+ @Property(bool)
550
+ def showRingLabels(self):
551
+ return self._showRingLabels
552
+
553
+ @showRingLabels.setter
554
+ def showRingLabels(self, value):
555
+ self._showRingLabels = bool(value)
556
+ self.update()
557
+
558
+ @Property(bool)
559
+ def showMarkers(self):
560
+ return self._showMarkers
561
+
562
+ @showMarkers.setter
563
+ def showMarkers(self, value):
564
+ self._showMarkers = bool(value)
565
+ self.update()
566
+
567
+ @Property(bool)
568
+ def showLegend(self):
569
+ return self._showLegend
570
+
571
+ @showLegend.setter
572
+ def showLegend(self, value):
573
+ self._showLegend = bool(value)
574
+ self.updateGeometry()
575
+ self.update()
576
+
577
+ # -- colours (from tokens via qproperty) --
578
+ @Property(QColor)
579
+ def gridColor(self):
580
+ return self._gridColor
581
+
582
+ @gridColor.setter
583
+ def gridColor(self, c):
584
+ self._gridColor = QColor(c); self.update()
585
+
586
+ @Property(QColor)
587
+ def axisColor(self):
588
+ return self._axisColor
589
+
590
+ @axisColor.setter
591
+ def axisColor(self, c):
592
+ self._axisColor = QColor(c); self.update()
593
+
594
+ @Property(QColor)
595
+ def labelColor(self):
596
+ return self._labelColor
597
+
598
+ @labelColor.setter
599
+ def labelColor(self, c):
600
+ self._labelColor = QColor(c); self.update()