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.
- custom_widgets_pro/__init__.py +73 -0
- custom_widgets_pro/_catalog.py +25 -0
- custom_widgets_pro/_cli.py +132 -0
- custom_widgets_pro/_license.py +419 -0
- custom_widgets_pro/_version.py +14 -0
- custom_widgets_pro/datatable/__init__.py +17 -0
- custom_widgets_pro/datatable/datatable_pro.py +319 -0
- custom_widgets_pro/datatable/export.py +143 -0
- custom_widgets_pro/datatable/frozen_view.py +112 -0
- custom_widgets_pro/datatable/grouping.py +309 -0
- custom_widgets_pro/datatable/pivot.py +100 -0
- custom_widgets_pro/datatable/provider.py +98 -0
- custom_widgets_pro/datatable/virtual_model.py +451 -0
- custom_widgets_pro/widgets/__init__.py +7 -0
- custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
- custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
- custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
- custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
- custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
- custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
- custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
- custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
- custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
- custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
- custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
- custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
- custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
- custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
- custom_widgets_pro/widgets/charts/__init__.py +1 -0
- custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
- custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
- custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
- custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
- custom_widgets_pro/widgets/data/__init__.py +1 -0
- custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
- custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
- custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
- custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
- custom_widgets_pro/widgets/media/__init__.py +1 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomRadialLines - a painted polar line chart.
|
|
9
|
+
##
|
|
10
|
+
## A line chart wrapped onto a circle: the x axis is angular, so the series
|
|
11
|
+
## closes back on itself. The right form for cyclical data - hours of a day,
|
|
12
|
+
## months of a year, compass bearings - where a cartesian line chart puts an
|
|
13
|
+
## artificial break between the last point and the first.
|
|
14
|
+
##
|
|
15
|
+
## Distinct from QCustomRadarChart: radar compares a handful of NAMED axes as
|
|
16
|
+
## a shape; this plots a continuous series around a circle and does not label
|
|
17
|
+
## every sample.
|
|
18
|
+
##
|
|
19
|
+
## QPainter only, NO QtCharts (see docs/design/mui-charts-gap.md). Angles come
|
|
20
|
+
## from the shared _chart_axis polar helpers so it winds the same way as the
|
|
21
|
+
## other radial charts.
|
|
22
|
+
##
|
|
23
|
+
## Data goes in with addSeries(name, values) in code, or seriesCsv in Qt
|
|
24
|
+
## Designer:
|
|
25
|
+
##
|
|
26
|
+
## seriesCsv = "Weekday=30,45,60,52,48,70,64;Weekend=20,25,40,38,30,35,28"
|
|
27
|
+
##
|
|
28
|
+
## Emits seriesHovered(int) and pointClicked(int, int); -1 means "nothing".
|
|
29
|
+
########################################################################
|
|
30
|
+
import math
|
|
31
|
+
|
|
32
|
+
from qtpy.QtCore import Qt, Signal, Property, QRectF, QSize, QPointF
|
|
33
|
+
from qtpy.QtGui import QColor, QPainter, QPen, QBrush, QPolygonF, QFontMetrics
|
|
34
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
35
|
+
|
|
36
|
+
# _chart_axis stays in the free package; Pro depends on it, so import
|
|
37
|
+
# it absolutely rather than duplicating the helper here.
|
|
38
|
+
from Custom_Widgets.widgets.charts._chart_axis import polarAngle, polarPoint
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class QCustomRadialLines(QWidget):
|
|
42
|
+
seriesHovered = Signal(int)
|
|
43
|
+
pointClicked = Signal(int, int)
|
|
44
|
+
|
|
45
|
+
WIDGET_ICON = "components/icons/track_changes.png"
|
|
46
|
+
WIDGET_TOOLTIP = "A painted polar line chart for cyclical data"
|
|
47
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomRadialLines"
|
|
48
|
+
WIDGET_DOM_XML = """
|
|
49
|
+
<ui language='c++'>
|
|
50
|
+
<widget class='QCustomRadialLines' name='customRadialLines'>
|
|
51
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>300</width><height>300</height></rect></property>
|
|
52
|
+
<property name='seriesCsv'><string>Weekday=30,45,60,52,48,70,64</string></property>
|
|
53
|
+
</widget>
|
|
54
|
+
</ui>
|
|
55
|
+
"""
|
|
56
|
+
__catalog__ = {
|
|
57
|
+
"name": "QCustomRadialLines",
|
|
58
|
+
"props": {"seriesCsv": {"type": "string", "default": ""},
|
|
59
|
+
"labelsCsv": {"type": "string", "default": ""},
|
|
60
|
+
"colorsCsv": {"type": "string", "default": ""},
|
|
61
|
+
"maxValue": {"type": "float", "default": 0.0},
|
|
62
|
+
"startAngle": {"type": "int", "default": 90},
|
|
63
|
+
"clockwise": {"type": "bool", "default": True},
|
|
64
|
+
"rings": {"type": "int", "default": 4},
|
|
65
|
+
"lineWidth": {"type": "float", "default": 2.0},
|
|
66
|
+
"fillOpacity": {"type": "float", "default": 0.15},
|
|
67
|
+
"closed": {"type": "bool", "default": True},
|
|
68
|
+
"smooth": {"type": "bool", "default": False},
|
|
69
|
+
"showGrid": {"type": "bool", "default": True},
|
|
70
|
+
"showMarkers": {"type": "bool", "default": False},
|
|
71
|
+
"showLabels": {"type": "bool", "default": True},
|
|
72
|
+
"showLegend": {"type": "bool", "default": True},
|
|
73
|
+
"gridColor": {"type": "color", "default": "#e2e8f0"},
|
|
74
|
+
"labelColor": {"type": "color", "default": "#64748b"}},
|
|
75
|
+
"signals": ["seriesHovered", "pointClicked"],
|
|
76
|
+
"tokens_used": ["surface-muted", "on-surface"],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Rich editors for the Designer "Custom Properties" dock (see
|
|
80
|
+
# DesignerTools.CustomPropertiesDock).
|
|
81
|
+
DESIGNER_CUSTOM_PROPS = [
|
|
82
|
+
{"name": "seriesCsv", "kind": "str", "group": "General"},
|
|
83
|
+
{"name": "labelsCsv", "kind": "str", "group": "General"},
|
|
84
|
+
{"name": "colorsCsv", "kind": "str", "group": "Colors"},
|
|
85
|
+
{"name": "maxValue", "kind": "float", "group": "General"},
|
|
86
|
+
{"name": "startAngle", "kind": "int", "group": "General"},
|
|
87
|
+
{"name": "clockwise", "kind": "bool", "group": "General"},
|
|
88
|
+
{"name": "rings", "kind": "int", "group": "General"},
|
|
89
|
+
{"name": "lineWidth", "kind": "float", "group": "General"},
|
|
90
|
+
{"name": "fillOpacity", "kind": "float", "group": "General"},
|
|
91
|
+
{"name": "closed", "kind": "bool", "group": "General"},
|
|
92
|
+
{"name": "smooth", "kind": "bool", "group": "General"},
|
|
93
|
+
{"name": "showGrid", "kind": "bool", "group": "General"},
|
|
94
|
+
{"name": "showMarkers", "kind": "bool", "group": "General"},
|
|
95
|
+
{"name": "showLabels", "kind": "bool", "group": "General"},
|
|
96
|
+
{"name": "showLegend", "kind": "bool", "group": "General"},
|
|
97
|
+
{"name": "gridColor", "kind": "color", "group": "Colors"},
|
|
98
|
+
{"name": "labelColor", "kind": "color", "group": "Colors"},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
_DEFAULT_COLORS = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed",
|
|
102
|
+
"#0891b2"]
|
|
103
|
+
|
|
104
|
+
def __init__(self, parent=None, series=None, labels=None):
|
|
105
|
+
super().__init__(parent)
|
|
106
|
+
self.setObjectName("QCustomRadialLines")
|
|
107
|
+
self._series = [] # list of (name, [values])
|
|
108
|
+
self._labels = []
|
|
109
|
+
self._colors = []
|
|
110
|
+
self._maxValue = 0.0
|
|
111
|
+
self._startAngle = 90
|
|
112
|
+
self._clockwise = True
|
|
113
|
+
self._rings = 4
|
|
114
|
+
self._lineWidth = 2.0
|
|
115
|
+
self._fillOpacity = 0.15
|
|
116
|
+
self._closed = True
|
|
117
|
+
self._smooth = False
|
|
118
|
+
self._showGrid = True
|
|
119
|
+
self._showMarkers = False
|
|
120
|
+
self._showLabels = True
|
|
121
|
+
self._showLegend = True
|
|
122
|
+
self._hover = -1
|
|
123
|
+
|
|
124
|
+
self._gridColor = QColor("#e2e8f0")
|
|
125
|
+
self._labelColor = QColor("#64748b")
|
|
126
|
+
|
|
127
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
128
|
+
self.setMouseTracking(True)
|
|
129
|
+
if labels:
|
|
130
|
+
self._labels = [str(x) for x in labels]
|
|
131
|
+
for name, values in (series or []):
|
|
132
|
+
self.addSeries(name, values)
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------ #
|
|
135
|
+
## Data
|
|
136
|
+
# ------------------------------------------------------------------ #
|
|
137
|
+
def addSeries(self, name, values, color=None):
|
|
138
|
+
cleaned = []
|
|
139
|
+
for value in values or []:
|
|
140
|
+
try:
|
|
141
|
+
cleaned.append(float(value))
|
|
142
|
+
except (TypeError, ValueError):
|
|
143
|
+
continue
|
|
144
|
+
self._series.append((str(name), cleaned))
|
|
145
|
+
if color is not None:
|
|
146
|
+
while len(self._colors) < len(self._series) - 1:
|
|
147
|
+
self._colors.append(None)
|
|
148
|
+
self._colors.append(QColor(color))
|
|
149
|
+
self.update()
|
|
150
|
+
return len(self._series) - 1
|
|
151
|
+
|
|
152
|
+
def setSeries(self, series):
|
|
153
|
+
self._series = []
|
|
154
|
+
for item in series or []:
|
|
155
|
+
if isinstance(item, dict):
|
|
156
|
+
self.addSeries(item.get("name", ""), item.get("values", []))
|
|
157
|
+
elif isinstance(item, (tuple, list)) and len(item) >= 2:
|
|
158
|
+
self.addSeries(item[0], item[1])
|
|
159
|
+
self.update()
|
|
160
|
+
|
|
161
|
+
def series(self):
|
|
162
|
+
return [(name, list(values)) for name, values in self._series]
|
|
163
|
+
|
|
164
|
+
def seriesCount(self):
|
|
165
|
+
return len(self._series)
|
|
166
|
+
|
|
167
|
+
def clearSeries(self):
|
|
168
|
+
self._series = []
|
|
169
|
+
self._colors = []
|
|
170
|
+
self._hover = -1
|
|
171
|
+
self.update()
|
|
172
|
+
|
|
173
|
+
def setLabels(self, labels):
|
|
174
|
+
self._labels = [str(x) for x in (labels or [])]
|
|
175
|
+
self.update()
|
|
176
|
+
|
|
177
|
+
def labels(self):
|
|
178
|
+
return list(self._labels)
|
|
179
|
+
|
|
180
|
+
def sampleCount(self):
|
|
181
|
+
"""Samples around the circle — the longest series wins."""
|
|
182
|
+
return max((len(v) for _n, v in self._series), default=0)
|
|
183
|
+
|
|
184
|
+
def seriesColor(self, index):
|
|
185
|
+
if 0 <= index < len(self._colors) and self._colors[index] is not None:
|
|
186
|
+
return QColor(self._colors[index])
|
|
187
|
+
return QColor(self._DEFAULT_COLORS[index % len(self._DEFAULT_COLORS)])
|
|
188
|
+
|
|
189
|
+
def maximum(self):
|
|
190
|
+
if self._maxValue > 0:
|
|
191
|
+
return self._maxValue
|
|
192
|
+
peak = max((v for _n, values in self._series for v in values), default=0.0)
|
|
193
|
+
return peak or 1.0
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------ #
|
|
196
|
+
## Geometry
|
|
197
|
+
# ------------------------------------------------------------------ #
|
|
198
|
+
def sizeHint(self):
|
|
199
|
+
return QSize(300, 300)
|
|
200
|
+
|
|
201
|
+
def minimumSizeHint(self):
|
|
202
|
+
return QSize(110, 110)
|
|
203
|
+
|
|
204
|
+
def _legendHeight(self):
|
|
205
|
+
if not (self._showLegend and self._series):
|
|
206
|
+
return 0.0
|
|
207
|
+
return QFontMetrics(self.font()).height() + 8
|
|
208
|
+
|
|
209
|
+
def _plotRect(self):
|
|
210
|
+
pad = 10.0
|
|
211
|
+
if self._showLabels and self._labels:
|
|
212
|
+
pad += QFontMetrics(self.font()).height() + 4
|
|
213
|
+
bottom = self._legendHeight()
|
|
214
|
+
side = max(10.0, min(self.width() - 2 * pad,
|
|
215
|
+
self.height() - 2 * pad - bottom))
|
|
216
|
+
return QRectF((self.width() - side) / 2.0,
|
|
217
|
+
(self.height() - bottom - side) / 2.0, side, side)
|
|
218
|
+
|
|
219
|
+
def _angleFor(self, index):
|
|
220
|
+
return polarAngle(index, max(1, self.sampleCount()),
|
|
221
|
+
self._startAngle, self._clockwise)
|
|
222
|
+
|
|
223
|
+
def pointFor(self, seriesIndex, sampleIndex):
|
|
224
|
+
"""Widget-space point for one sample of one series."""
|
|
225
|
+
rect = self._plotRect()
|
|
226
|
+
values = self._series[seriesIndex][1]
|
|
227
|
+
if not values:
|
|
228
|
+
return QPointF(rect.center())
|
|
229
|
+
value = values[sampleIndex % len(values)]
|
|
230
|
+
radius = rect.width() / 2.0 * max(0.0, min(1.0, value / self.maximum()))
|
|
231
|
+
x, y = polarPoint(rect.center().x(), rect.center().y(), radius,
|
|
232
|
+
self._angleFor(sampleIndex))
|
|
233
|
+
return QPointF(x, y)
|
|
234
|
+
|
|
235
|
+
def _polygonFor(self, seriesIndex):
|
|
236
|
+
values = self._series[seriesIndex][1]
|
|
237
|
+
return QPolygonF([self.pointFor(seriesIndex, i)
|
|
238
|
+
for i in range(len(values))])
|
|
239
|
+
|
|
240
|
+
# ------------------------------------------------------------------ #
|
|
241
|
+
## Painting
|
|
242
|
+
# ------------------------------------------------------------------ #
|
|
243
|
+
def paintEvent(self, e):
|
|
244
|
+
p = QPainter(self)
|
|
245
|
+
p.setRenderHint(QPainter.Antialiasing)
|
|
246
|
+
p.setFont(self.font())
|
|
247
|
+
if not self._series or not self.sampleCount():
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if self._showGrid:
|
|
251
|
+
self._paintGrid(p)
|
|
252
|
+
for index in range(len(self._series)):
|
|
253
|
+
self._paintSeries(p, index)
|
|
254
|
+
if self._showLabels and self._labels:
|
|
255
|
+
self._paintLabels(p)
|
|
256
|
+
if self._showLegend:
|
|
257
|
+
self._paintLegend(p)
|
|
258
|
+
|
|
259
|
+
def _paintGrid(self, p):
|
|
260
|
+
rect = self._plotRect()
|
|
261
|
+
p.setBrush(Qt.NoBrush)
|
|
262
|
+
p.setPen(QPen(self._gridColor, 1))
|
|
263
|
+
for ring in range(1, max(1, self._rings) + 1):
|
|
264
|
+
radius = rect.width() / 2.0 * ring / float(max(1, self._rings))
|
|
265
|
+
p.drawEllipse(rect.center(), radius, radius)
|
|
266
|
+
# spokes only where a label exists, so a 60-sample series does not
|
|
267
|
+
# bury the data under 60 grid lines
|
|
268
|
+
count = len(self._labels) if self._labels else min(self.sampleCount(), 12)
|
|
269
|
+
for i in range(count):
|
|
270
|
+
step = max(1, self.sampleCount() // max(1, count))
|
|
271
|
+
angle = self._angleFor(i * step)
|
|
272
|
+
x, y = polarPoint(rect.center().x(), rect.center().y(),
|
|
273
|
+
rect.width() / 2.0, angle)
|
|
274
|
+
p.drawLine(rect.center(), QPointF(x, y))
|
|
275
|
+
|
|
276
|
+
def _paintSeries(self, p, index):
|
|
277
|
+
colour = self.seriesColor(index)
|
|
278
|
+
hovered = index == self._hover
|
|
279
|
+
poly = self._polygonFor(index)
|
|
280
|
+
if poly.count() < 2:
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
if self._fillOpacity > 0 and self._closed:
|
|
284
|
+
fill = QColor(colour)
|
|
285
|
+
fill.setAlphaF(max(0.0, min(1.0, self._fillOpacity *
|
|
286
|
+
(1.8 if hovered else 1.0))))
|
|
287
|
+
p.setPen(Qt.NoPen)
|
|
288
|
+
p.setBrush(QBrush(fill))
|
|
289
|
+
p.drawPolygon(poly)
|
|
290
|
+
|
|
291
|
+
p.setBrush(Qt.NoBrush)
|
|
292
|
+
p.setPen(QPen(colour, self._lineWidth + (1.0 if hovered else 0.0)))
|
|
293
|
+
if self._closed:
|
|
294
|
+
p.drawPolygon(poly)
|
|
295
|
+
else:
|
|
296
|
+
p.drawPolyline(poly)
|
|
297
|
+
|
|
298
|
+
if self._showMarkers:
|
|
299
|
+
p.setPen(Qt.NoPen)
|
|
300
|
+
p.setBrush(QBrush(colour))
|
|
301
|
+
radius = 3.0 + (1.0 if hovered else 0.0)
|
|
302
|
+
for point in poly:
|
|
303
|
+
p.drawEllipse(point, radius, radius)
|
|
304
|
+
|
|
305
|
+
def _paintLabels(self, p):
|
|
306
|
+
rect = self._plotRect()
|
|
307
|
+
fm = QFontMetrics(self.font())
|
|
308
|
+
p.setPen(QPen(self._labelColor))
|
|
309
|
+
count = self.sampleCount()
|
|
310
|
+
for i, label in enumerate(self._labels[:count]):
|
|
311
|
+
step = max(1, count // max(1, len(self._labels)))
|
|
312
|
+
angle = self._angleFor(i * step)
|
|
313
|
+
x, y = polarPoint(rect.center().x(), rect.center().y(),
|
|
314
|
+
rect.width() / 2.0 + 6, angle)
|
|
315
|
+
width = fm.horizontalAdvance(label)
|
|
316
|
+
if x < rect.center().x() - 1:
|
|
317
|
+
x -= width
|
|
318
|
+
elif abs(x - rect.center().x()) <= 1:
|
|
319
|
+
x -= width / 2.0
|
|
320
|
+
p.drawText(QRectF(x, y - fm.height() / 2.0, width, fm.height()),
|
|
321
|
+
int(Qt.AlignCenter), label)
|
|
322
|
+
|
|
323
|
+
def _paintLegend(self, p):
|
|
324
|
+
fm = QFontMetrics(self.font())
|
|
325
|
+
swatch, gap = 9.0, 14.0
|
|
326
|
+
widths = [swatch + 5 + fm.horizontalAdvance(name)
|
|
327
|
+
for name, _v in self._series]
|
|
328
|
+
x = (self.width() - (sum(widths) + gap * (len(widths) - 1))) / 2.0
|
|
329
|
+
y = self.height() - self._legendHeight() + 2
|
|
330
|
+
for index, (name, _values) in enumerate(self._series):
|
|
331
|
+
p.setPen(Qt.NoPen)
|
|
332
|
+
p.setBrush(QBrush(self.seriesColor(index)))
|
|
333
|
+
p.drawEllipse(QRectF(x, y + (fm.height() - swatch) / 2.0,
|
|
334
|
+
swatch, swatch))
|
|
335
|
+
p.setPen(QPen(self._labelColor))
|
|
336
|
+
p.drawText(QRectF(x + swatch + 5, y, widths[index], fm.height()),
|
|
337
|
+
int(Qt.AlignLeft | Qt.AlignVCenter), name)
|
|
338
|
+
x += widths[index] + gap
|
|
339
|
+
|
|
340
|
+
# ------------------------------------------------------------------ #
|
|
341
|
+
## Interaction
|
|
342
|
+
# ------------------------------------------------------------------ #
|
|
343
|
+
def sampleAt(self, pos):
|
|
344
|
+
"""(series, sample) nearest a point, or (-1, -1)."""
|
|
345
|
+
point = QPointF(pos)
|
|
346
|
+
best, bestDistance = (-1, -1), None
|
|
347
|
+
for si in range(len(self._series) - 1, -1, -1):
|
|
348
|
+
for i in range(len(self._series[si][1])):
|
|
349
|
+
mapped = self.pointFor(si, i)
|
|
350
|
+
distance = math.hypot(point.x() - mapped.x(),
|
|
351
|
+
point.y() - mapped.y())
|
|
352
|
+
if distance <= 10.0 and (bestDistance is None or distance < bestDistance):
|
|
353
|
+
best, bestDistance = (si, i), distance
|
|
354
|
+
return best
|
|
355
|
+
|
|
356
|
+
def seriesAt(self, pos):
|
|
357
|
+
"""Index of the series whose closed shape contains a point, else -1."""
|
|
358
|
+
point = QPointF(pos)
|
|
359
|
+
for index in range(len(self._series) - 1, -1, -1):
|
|
360
|
+
poly = self._polygonFor(index)
|
|
361
|
+
if poly.count() >= 3 and poly.containsPoint(point, Qt.OddEvenFill):
|
|
362
|
+
return index
|
|
363
|
+
return -1
|
|
364
|
+
|
|
365
|
+
def mouseMoveEvent(self, e):
|
|
366
|
+
index = self.seriesAt(e.pos())
|
|
367
|
+
if index != self._hover:
|
|
368
|
+
self._hover = index
|
|
369
|
+
self.seriesHovered.emit(index)
|
|
370
|
+
self.update()
|
|
371
|
+
super().mouseMoveEvent(e)
|
|
372
|
+
|
|
373
|
+
def leaveEvent(self, e):
|
|
374
|
+
if self._hover != -1:
|
|
375
|
+
self._hover = -1
|
|
376
|
+
self.seriesHovered.emit(-1)
|
|
377
|
+
self.update()
|
|
378
|
+
super().leaveEvent(e)
|
|
379
|
+
|
|
380
|
+
def mouseReleaseEvent(self, e):
|
|
381
|
+
if e.button() == Qt.LeftButton:
|
|
382
|
+
si, i = self.sampleAt(e.pos())
|
|
383
|
+
if si >= 0:
|
|
384
|
+
self.pointClicked.emit(si, i)
|
|
385
|
+
super().mouseReleaseEvent(e)
|
|
386
|
+
|
|
387
|
+
# ------------------------------------------------------------------ #
|
|
388
|
+
## Designer properties
|
|
389
|
+
# ------------------------------------------------------------------ #
|
|
390
|
+
@Property(str)
|
|
391
|
+
def seriesCsv(self):
|
|
392
|
+
return ";".join("%s=%s" % (name, ",".join("%g" % v for v in values))
|
|
393
|
+
for name, values in self._series)
|
|
394
|
+
|
|
395
|
+
@seriesCsv.setter
|
|
396
|
+
def seriesCsv(self, text):
|
|
397
|
+
parsed = []
|
|
398
|
+
for chunk in str(text).replace("|", ";").split(";"):
|
|
399
|
+
chunk = chunk.strip()
|
|
400
|
+
if not chunk:
|
|
401
|
+
continue
|
|
402
|
+
if "=" in chunk:
|
|
403
|
+
name, _, raw = chunk.partition("=")
|
|
404
|
+
name = name.strip() or "Series %d" % (len(parsed) + 1)
|
|
405
|
+
else:
|
|
406
|
+
name, raw = "Series %d" % (len(parsed) + 1), chunk
|
|
407
|
+
values = []
|
|
408
|
+
for token in raw.split(","):
|
|
409
|
+
token = token.strip()
|
|
410
|
+
if not token:
|
|
411
|
+
continue
|
|
412
|
+
try:
|
|
413
|
+
values.append(float(token))
|
|
414
|
+
except ValueError:
|
|
415
|
+
pass
|
|
416
|
+
if values:
|
|
417
|
+
parsed.append((name, values))
|
|
418
|
+
self.setSeries(parsed)
|
|
419
|
+
|
|
420
|
+
@Property(str)
|
|
421
|
+
def labelsCsv(self):
|
|
422
|
+
return ",".join(self._labels)
|
|
423
|
+
|
|
424
|
+
@labelsCsv.setter
|
|
425
|
+
def labelsCsv(self, text):
|
|
426
|
+
self.setLabels([t.strip() for t in str(text).replace(";", ",").split(",")
|
|
427
|
+
if t.strip()])
|
|
428
|
+
|
|
429
|
+
@Property(str)
|
|
430
|
+
def colorsCsv(self):
|
|
431
|
+
return ",".join(c.name() if c is not None else "" for c in self._colors)
|
|
432
|
+
|
|
433
|
+
@colorsCsv.setter
|
|
434
|
+
def colorsCsv(self, text):
|
|
435
|
+
colors = []
|
|
436
|
+
for token in str(text).replace(";", ",").split(","):
|
|
437
|
+
token = token.strip()
|
|
438
|
+
colour = QColor(token) if token else QColor()
|
|
439
|
+
colors.append(colour if colour.isValid() else None)
|
|
440
|
+
self._colors = colors
|
|
441
|
+
self.update()
|
|
442
|
+
|
|
443
|
+
@Property(float)
|
|
444
|
+
def maxValue(self):
|
|
445
|
+
return self._maxValue
|
|
446
|
+
|
|
447
|
+
@maxValue.setter
|
|
448
|
+
def maxValue(self, value):
|
|
449
|
+
self._maxValue = max(0.0, float(value)); self.update()
|
|
450
|
+
|
|
451
|
+
@Property(int)
|
|
452
|
+
def startAngle(self):
|
|
453
|
+
return self._startAngle
|
|
454
|
+
|
|
455
|
+
@startAngle.setter
|
|
456
|
+
def startAngle(self, value):
|
|
457
|
+
self._startAngle = int(value) % 360; self.update()
|
|
458
|
+
|
|
459
|
+
@Property(bool)
|
|
460
|
+
def clockwise(self):
|
|
461
|
+
return self._clockwise
|
|
462
|
+
|
|
463
|
+
@clockwise.setter
|
|
464
|
+
def clockwise(self, value):
|
|
465
|
+
self._clockwise = bool(value); self.update()
|
|
466
|
+
|
|
467
|
+
@Property(int)
|
|
468
|
+
def rings(self):
|
|
469
|
+
return self._rings
|
|
470
|
+
|
|
471
|
+
@rings.setter
|
|
472
|
+
def rings(self, value):
|
|
473
|
+
self._rings = max(1, int(value)); self.update()
|
|
474
|
+
|
|
475
|
+
@Property(float)
|
|
476
|
+
def lineWidth(self):
|
|
477
|
+
return self._lineWidth
|
|
478
|
+
|
|
479
|
+
@lineWidth.setter
|
|
480
|
+
def lineWidth(self, value):
|
|
481
|
+
self._lineWidth = max(0.0, float(value)); self.update()
|
|
482
|
+
|
|
483
|
+
@Property(float)
|
|
484
|
+
def fillOpacity(self):
|
|
485
|
+
return self._fillOpacity
|
|
486
|
+
|
|
487
|
+
@fillOpacity.setter
|
|
488
|
+
def fillOpacity(self, value):
|
|
489
|
+
self._fillOpacity = max(0.0, min(1.0, float(value))); self.update()
|
|
490
|
+
|
|
491
|
+
@Property(bool)
|
|
492
|
+
def closed(self):
|
|
493
|
+
return self._closed
|
|
494
|
+
|
|
495
|
+
@closed.setter
|
|
496
|
+
def closed(self, value):
|
|
497
|
+
self._closed = bool(value); self.update()
|
|
498
|
+
|
|
499
|
+
@Property(bool)
|
|
500
|
+
def smooth(self):
|
|
501
|
+
return self._smooth
|
|
502
|
+
|
|
503
|
+
@smooth.setter
|
|
504
|
+
def smooth(self, value):
|
|
505
|
+
self._smooth = bool(value); self.update()
|
|
506
|
+
|
|
507
|
+
@Property(bool)
|
|
508
|
+
def showGrid(self):
|
|
509
|
+
return self._showGrid
|
|
510
|
+
|
|
511
|
+
@showGrid.setter
|
|
512
|
+
def showGrid(self, value):
|
|
513
|
+
self._showGrid = bool(value); self.update()
|
|
514
|
+
|
|
515
|
+
@Property(bool)
|
|
516
|
+
def showMarkers(self):
|
|
517
|
+
return self._showMarkers
|
|
518
|
+
|
|
519
|
+
@showMarkers.setter
|
|
520
|
+
def showMarkers(self, value):
|
|
521
|
+
self._showMarkers = bool(value); self.update()
|
|
522
|
+
|
|
523
|
+
@Property(bool)
|
|
524
|
+
def showLabels(self):
|
|
525
|
+
return self._showLabels
|
|
526
|
+
|
|
527
|
+
@showLabels.setter
|
|
528
|
+
def showLabels(self, value):
|
|
529
|
+
self._showLabels = bool(value); self.updateGeometry(); self.update()
|
|
530
|
+
|
|
531
|
+
@Property(bool)
|
|
532
|
+
def showLegend(self):
|
|
533
|
+
return self._showLegend
|
|
534
|
+
|
|
535
|
+
@showLegend.setter
|
|
536
|
+
def showLegend(self, value):
|
|
537
|
+
self._showLegend = bool(value); self.updateGeometry(); self.update()
|
|
538
|
+
|
|
539
|
+
# -- colours (from tokens via qproperty) --
|
|
540
|
+
@Property(QColor)
|
|
541
|
+
def gridColor(self):
|
|
542
|
+
return self._gridColor
|
|
543
|
+
|
|
544
|
+
@gridColor.setter
|
|
545
|
+
def gridColor(self, c):
|
|
546
|
+
self._gridColor = QColor(c); self.update()
|
|
547
|
+
|
|
548
|
+
@Property(QColor)
|
|
549
|
+
def labelColor(self):
|
|
550
|
+
return self._labelColor
|
|
551
|
+
|
|
552
|
+
@labelColor.setter
|
|
553
|
+
def labelColor(self, c):
|
|
554
|
+
self._labelColor = QColor(c); self.update()
|