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,474 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomFunnelChart - a painted funnel / pyramid chart.
|
|
9
|
+
##
|
|
10
|
+
## Stacked trapezoid stages narrowing toward the end: a conversion funnel, a
|
|
11
|
+
## sales pipeline, a drop-off analysis. Each stage's width encodes its value
|
|
12
|
+
## and the taper between stages is the loss.
|
|
13
|
+
##
|
|
14
|
+
## Pyramid is the SAME chart inverted, so it is a `shape` property rather than
|
|
15
|
+
## a second widget - MUI ships them as two components, which duplicates every
|
|
16
|
+
## property for one flipped polygon.
|
|
17
|
+
##
|
|
18
|
+
## QPainter only, NO QtCharts (see docs/design/mui-charts-gap.md).
|
|
19
|
+
##
|
|
20
|
+
## Data goes in with setStages([...]) in code, or stagesCsv in Qt Designer:
|
|
21
|
+
##
|
|
22
|
+
## stagesCsv = "Visits=1000,Signups=420,Trials=180,Paid=64"
|
|
23
|
+
##
|
|
24
|
+
## Emits stageHovered(int) and stageClicked(int); -1 means "nothing".
|
|
25
|
+
########################################################################
|
|
26
|
+
from qtpy.QtCore import Qt, Signal, Property, QRectF, QSize, QPointF
|
|
27
|
+
from qtpy.QtGui import QColor, QPainter, QPen, QBrush, QPolygonF, QFontMetrics
|
|
28
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class QCustomFunnelChart(QWidget):
|
|
32
|
+
stageHovered = Signal(int)
|
|
33
|
+
stageClicked = Signal(int)
|
|
34
|
+
|
|
35
|
+
WIDGET_ICON = "components/icons/filter_alt.png"
|
|
36
|
+
WIDGET_TOOLTIP = "A painted funnel / pyramid chart"
|
|
37
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomFunnelChart"
|
|
38
|
+
WIDGET_DOM_XML = """
|
|
39
|
+
<ui language='c++'>
|
|
40
|
+
<widget class='QCustomFunnelChart' name='customFunnelChart'>
|
|
41
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>360</width><height>300</height></rect></property>
|
|
42
|
+
<property name='stagesCsv'><string>Visits=1000,Signups=420,Trials=180,Paid=64</string></property>
|
|
43
|
+
</widget>
|
|
44
|
+
</ui>
|
|
45
|
+
"""
|
|
46
|
+
__catalog__ = {
|
|
47
|
+
"name": "QCustomFunnelChart",
|
|
48
|
+
"props": {"stagesCsv": {"type": "string", "default": ""},
|
|
49
|
+
"colorsCsv": {"type": "string", "default": ""},
|
|
50
|
+
"shape": {"type": "enum", "values": ["funnel", "pyramid"],
|
|
51
|
+
"default": "funnel"},
|
|
52
|
+
"orientation": {"type": "enum", "values": ["vertical", "horizontal"],
|
|
53
|
+
"default": "vertical"},
|
|
54
|
+
"gapPx": {"type": "int", "default": 3},
|
|
55
|
+
"neckRatio": {"type": "float", "default": 0.0},
|
|
56
|
+
"showLabels": {"type": "bool", "default": True},
|
|
57
|
+
"showValues": {"type": "bool", "default": True},
|
|
58
|
+
"showPercent": {"type": "bool", "default": False},
|
|
59
|
+
"percentOf": {"type": "enum", "values": ["first", "previous"],
|
|
60
|
+
"default": "first"},
|
|
61
|
+
"labelColor": {"type": "color", "default": "#ffffff"},
|
|
62
|
+
"outsideLabelColor": {"type": "color", "default": "#0f172a"}},
|
|
63
|
+
"signals": ["stageHovered", "stageClicked"],
|
|
64
|
+
"tokens_used": ["on-primary", "on-surface"],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Rich editors for the Designer "Custom Properties" dock (see
|
|
68
|
+
# DesignerTools.CustomPropertiesDock).
|
|
69
|
+
DESIGNER_CUSTOM_PROPS = [
|
|
70
|
+
{"name": "stagesCsv", "kind": "str", "group": "General"},
|
|
71
|
+
{"name": "colorsCsv", "kind": "str", "group": "Colors"},
|
|
72
|
+
{"name": "shape", "kind": "choice", "group": "General"},
|
|
73
|
+
{"name": "orientation", "kind": "choice", "group": "General"},
|
|
74
|
+
{"name": "gapPx", "kind": "int", "group": "General"},
|
|
75
|
+
{"name": "neckRatio", "kind": "float", "group": "General"},
|
|
76
|
+
{"name": "showLabels", "kind": "bool", "group": "General"},
|
|
77
|
+
{"name": "showValues", "kind": "bool", "group": "General"},
|
|
78
|
+
{"name": "showPercent", "kind": "bool", "group": "General"},
|
|
79
|
+
{"name": "percentOf", "kind": "choice", "group": "General"},
|
|
80
|
+
{"name": "labelColor", "kind": "color", "group": "Colors"},
|
|
81
|
+
{"name": "outsideLabelColor", "kind": "color", "group": "Colors"},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
_DEFAULT_COLORS = ["#2563eb", "#3b82f6", "#60a5fa", "#93c5fd", "#bfdbfe",
|
|
85
|
+
"#dbeafe"]
|
|
86
|
+
|
|
87
|
+
def __init__(self, parent=None, stages=None):
|
|
88
|
+
super().__init__(parent)
|
|
89
|
+
self.setObjectName("QCustomFunnelChart")
|
|
90
|
+
self._stages = [] # list of (label, value)
|
|
91
|
+
self._colors = []
|
|
92
|
+
self._shape = "funnel"
|
|
93
|
+
self._orientation = "vertical"
|
|
94
|
+
self._gap = 3
|
|
95
|
+
self._neckRatio = 0.0 # 0 = taper to a point at the last stage
|
|
96
|
+
self._showLabels = True
|
|
97
|
+
self._showValues = True
|
|
98
|
+
self._showPercent = False
|
|
99
|
+
self._percentOf = "first"
|
|
100
|
+
self._hover = -1
|
|
101
|
+
self._bands = [] # per-stage polygon, parallel to _stages
|
|
102
|
+
|
|
103
|
+
self._labelColor = QColor("#ffffff")
|
|
104
|
+
self._outsideLabelColor = QColor("#0f172a")
|
|
105
|
+
|
|
106
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
107
|
+
self.setMouseTracking(True)
|
|
108
|
+
if stages:
|
|
109
|
+
self.setStages(stages)
|
|
110
|
+
|
|
111
|
+
# ------------------------------------------------------------------ #
|
|
112
|
+
## Data
|
|
113
|
+
# ------------------------------------------------------------------ #
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _coerce(stage):
|
|
116
|
+
if isinstance(stage, dict):
|
|
117
|
+
label, value = stage.get("label"), stage.get("value")
|
|
118
|
+
elif isinstance(stage, (tuple, list)) and len(stage) >= 2:
|
|
119
|
+
label, value = stage[0], stage[1]
|
|
120
|
+
else:
|
|
121
|
+
return None
|
|
122
|
+
try:
|
|
123
|
+
return (str(label), max(0.0, float(value)))
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def setStages(self, stages):
|
|
128
|
+
self._stages = [s for s in (self._coerce(x) for x in (stages or [])) if s]
|
|
129
|
+
self._hover = -1
|
|
130
|
+
self.update()
|
|
131
|
+
|
|
132
|
+
def stages(self):
|
|
133
|
+
return list(self._stages)
|
|
134
|
+
|
|
135
|
+
def stageCount(self):
|
|
136
|
+
return len(self._stages)
|
|
137
|
+
|
|
138
|
+
def clearStages(self):
|
|
139
|
+
self._stages = []
|
|
140
|
+
self._hover = -1
|
|
141
|
+
self.update()
|
|
142
|
+
|
|
143
|
+
def maximum(self):
|
|
144
|
+
"""The value the widest band represents; never zero."""
|
|
145
|
+
return max((v for _l, v in self._stages), default=0.0) or 1.0
|
|
146
|
+
|
|
147
|
+
def stageColor(self, index):
|
|
148
|
+
if 0 <= index < len(self._colors) and self._colors[index] is not None:
|
|
149
|
+
return QColor(self._colors[index])
|
|
150
|
+
return QColor(self._DEFAULT_COLORS[index % len(self._DEFAULT_COLORS)])
|
|
151
|
+
|
|
152
|
+
def percentFor(self, index):
|
|
153
|
+
"""Conversion percentage for a stage, per `percentOf`."""
|
|
154
|
+
if not (0 <= index < len(self._stages)):
|
|
155
|
+
return 0.0
|
|
156
|
+
value = self._stages[index][1]
|
|
157
|
+
if self._percentOf == "previous" and index > 0:
|
|
158
|
+
base = self._stages[index - 1][1]
|
|
159
|
+
else:
|
|
160
|
+
base = self._stages[0][1]
|
|
161
|
+
return (value / base * 100.0) if base else 0.0
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------ #
|
|
164
|
+
## Geometry
|
|
165
|
+
# ------------------------------------------------------------------ #
|
|
166
|
+
def sizeHint(self):
|
|
167
|
+
return QSize(360, 300)
|
|
168
|
+
|
|
169
|
+
def minimumSizeHint(self):
|
|
170
|
+
return QSize(120, 100)
|
|
171
|
+
|
|
172
|
+
def _plotRect(self):
|
|
173
|
+
return QRectF(8, 8, max(1.0, self.width() - 16),
|
|
174
|
+
max(1.0, self.height() - 16))
|
|
175
|
+
|
|
176
|
+
def _halfWidthAt(self, value):
|
|
177
|
+
"""Half-extent of a band, as a fraction of the plot's half-width."""
|
|
178
|
+
peak = self.maximum()
|
|
179
|
+
fraction = value / peak if peak else 0.0
|
|
180
|
+
# neckRatio floors the taper so a funnel can end in a spout rather
|
|
181
|
+
# than a point; 0 keeps the classic full taper.
|
|
182
|
+
floor = max(0.0, min(1.0, self._neckRatio))
|
|
183
|
+
return (floor + (1.0 - floor) * fraction) / 2.0
|
|
184
|
+
|
|
185
|
+
def _computeBands(self):
|
|
186
|
+
"""Trapezoid per stage. Each band's far edge matches the next band's
|
|
187
|
+
near edge, so the funnel reads as one continuous shape."""
|
|
188
|
+
self._bands = []
|
|
189
|
+
count = len(self._stages)
|
|
190
|
+
if not count:
|
|
191
|
+
return
|
|
192
|
+
rect = self._plotRect()
|
|
193
|
+
horizontal = self._orientation == "horizontal"
|
|
194
|
+
length = rect.width() if horizontal else rect.height()
|
|
195
|
+
extent = rect.height() if horizontal else rect.width()
|
|
196
|
+
gap = float(self._gap)
|
|
197
|
+
band = (length - gap * (count - 1)) / count
|
|
198
|
+
|
|
199
|
+
for index, (_label, value) in enumerate(self._stages):
|
|
200
|
+
nearValue = value
|
|
201
|
+
nextValue = self._stages[index + 1][1] if index + 1 < count else (
|
|
202
|
+
value if self._neckRatio > 0 else 0.0)
|
|
203
|
+
nearHalf = self._halfWidthAt(nearValue) * extent
|
|
204
|
+
farHalf = self._halfWidthAt(nextValue) * extent
|
|
205
|
+
|
|
206
|
+
start = (rect.top() if not horizontal else rect.left()) + index * (band + gap)
|
|
207
|
+
stop = start + band
|
|
208
|
+
centre = rect.center().y() if horizontal else rect.center().x()
|
|
209
|
+
|
|
210
|
+
if horizontal:
|
|
211
|
+
poly = QPolygonF([QPointF(start, centre - nearHalf),
|
|
212
|
+
QPointF(stop, centre - farHalf),
|
|
213
|
+
QPointF(stop, centre + farHalf),
|
|
214
|
+
QPointF(start, centre + nearHalf)])
|
|
215
|
+
else:
|
|
216
|
+
poly = QPolygonF([QPointF(centre - nearHalf, start),
|
|
217
|
+
QPointF(centre + nearHalf, start),
|
|
218
|
+
QPointF(centre + farHalf, stop),
|
|
219
|
+
QPointF(centre - farHalf, stop)])
|
|
220
|
+
self._bands.append(poly)
|
|
221
|
+
|
|
222
|
+
if self._shape == "pyramid":
|
|
223
|
+
# Mirror the finished chain rather than swapping each band's own
|
|
224
|
+
# edges: swapping per band leaves band i's far edge no longer
|
|
225
|
+
# matching band i+1's near edge, so the shape comes apart at every
|
|
226
|
+
# seam. Mirroring keeps it continuous by construction.
|
|
227
|
+
self._bands = [self._mirror(poly, rect, horizontal)
|
|
228
|
+
for poly in self._bands]
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _mirror(poly, rect, horizontal):
|
|
232
|
+
"""Flip a polygon across the plot's centre line, along the flow axis."""
|
|
233
|
+
out = QPolygonF()
|
|
234
|
+
for point in poly:
|
|
235
|
+
if horizontal:
|
|
236
|
+
out.append(QPointF(rect.left() + rect.right() - point.x(),
|
|
237
|
+
point.y()))
|
|
238
|
+
else:
|
|
239
|
+
out.append(QPointF(point.x(),
|
|
240
|
+
rect.top() + rect.bottom() - point.y()))
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
def bands(self):
|
|
244
|
+
if not self._bands:
|
|
245
|
+
self._computeBands()
|
|
246
|
+
return list(self._bands)
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------ #
|
|
249
|
+
## Painting
|
|
250
|
+
# ------------------------------------------------------------------ #
|
|
251
|
+
def paintEvent(self, e):
|
|
252
|
+
p = QPainter(self)
|
|
253
|
+
p.setRenderHint(QPainter.Antialiasing)
|
|
254
|
+
p.setFont(self.font())
|
|
255
|
+
self._computeBands()
|
|
256
|
+
if not self._stages:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
for index, poly in enumerate(self._bands):
|
|
260
|
+
colour = self.stageColor(index)
|
|
261
|
+
if index == self._hover:
|
|
262
|
+
colour = colour.lighter(112)
|
|
263
|
+
p.setPen(Qt.NoPen)
|
|
264
|
+
p.setBrush(QBrush(colour))
|
|
265
|
+
p.drawPolygon(poly)
|
|
266
|
+
|
|
267
|
+
if self._showLabels or self._showValues or self._showPercent:
|
|
268
|
+
self._paintLabels(p)
|
|
269
|
+
|
|
270
|
+
def _labelFor(self, index):
|
|
271
|
+
label, value = self._stages[index]
|
|
272
|
+
parts = []
|
|
273
|
+
if self._showLabels:
|
|
274
|
+
parts.append(label)
|
|
275
|
+
if self._showValues:
|
|
276
|
+
parts.append("%g" % value)
|
|
277
|
+
if self._showPercent:
|
|
278
|
+
parts.append("%.0f%%" % self.percentFor(index))
|
|
279
|
+
return " ".join(parts)
|
|
280
|
+
|
|
281
|
+
def _paintLabels(self, p):
|
|
282
|
+
fm = QFontMetrics(self.font())
|
|
283
|
+
for index, poly in enumerate(self._bands):
|
|
284
|
+
text = self._labelFor(index)
|
|
285
|
+
if not text:
|
|
286
|
+
continue
|
|
287
|
+
box = poly.boundingRect()
|
|
288
|
+
width = fm.horizontalAdvance(text)
|
|
289
|
+
# Narrow bands cannot hold their label: put it outside and use the
|
|
290
|
+
# on-surface colour, or the text disappears into the background.
|
|
291
|
+
inside = width + 12 <= box.width() and box.height() >= fm.height()
|
|
292
|
+
if inside:
|
|
293
|
+
p.setPen(QPen(self._labelColor))
|
|
294
|
+
p.drawText(box, int(Qt.AlignCenter), text)
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
p.setPen(QPen(self._outsideLabelColor))
|
|
298
|
+
# Outside labels go PERPENDICULAR to the flow. Pushing them along
|
|
299
|
+
# it (right, on a horizontal funnel) lands them inside the next
|
|
300
|
+
# band, so every label piles up on top of the others.
|
|
301
|
+
if self._orientation == "horizontal":
|
|
302
|
+
target = QRectF(box.center().x() - width / 2.0,
|
|
303
|
+
max(0.0, box.top() - fm.height() - 2),
|
|
304
|
+
width, fm.height())
|
|
305
|
+
if target.top() < 0:
|
|
306
|
+
target.moveTop(min(self.height() - fm.height(),
|
|
307
|
+
box.bottom() + 2))
|
|
308
|
+
p.drawText(target, int(Qt.AlignCenter), text)
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
target = QRectF(box.right() + 6, box.center().y() - fm.height() / 2.0,
|
|
312
|
+
max(0.0, self.width() - box.right() - 8), fm.height())
|
|
313
|
+
if target.width() < width:
|
|
314
|
+
target = QRectF(max(0.0, box.left() - width - 6),
|
|
315
|
+
box.center().y() - fm.height() / 2.0,
|
|
316
|
+
width, fm.height())
|
|
317
|
+
p.drawText(target, int(Qt.AlignRight | Qt.AlignVCenter), text)
|
|
318
|
+
else:
|
|
319
|
+
p.drawText(target, int(Qt.AlignLeft | Qt.AlignVCenter), text)
|
|
320
|
+
|
|
321
|
+
# ------------------------------------------------------------------ #
|
|
322
|
+
## Interaction
|
|
323
|
+
# ------------------------------------------------------------------ #
|
|
324
|
+
def stageAt(self, pos):
|
|
325
|
+
point = QPointF(pos)
|
|
326
|
+
for index, poly in enumerate(self.bands()):
|
|
327
|
+
if poly.containsPoint(point, Qt.OddEvenFill):
|
|
328
|
+
return index
|
|
329
|
+
return -1
|
|
330
|
+
|
|
331
|
+
def mouseMoveEvent(self, e):
|
|
332
|
+
index = self.stageAt(e.pos())
|
|
333
|
+
if index != self._hover:
|
|
334
|
+
self._hover = index
|
|
335
|
+
self.stageHovered.emit(index)
|
|
336
|
+
self.update()
|
|
337
|
+
super().mouseMoveEvent(e)
|
|
338
|
+
|
|
339
|
+
def leaveEvent(self, e):
|
|
340
|
+
if self._hover != -1:
|
|
341
|
+
self._hover = -1
|
|
342
|
+
self.stageHovered.emit(-1)
|
|
343
|
+
self.update()
|
|
344
|
+
super().leaveEvent(e)
|
|
345
|
+
|
|
346
|
+
def mouseReleaseEvent(self, e):
|
|
347
|
+
if e.button() == Qt.LeftButton:
|
|
348
|
+
index = self.stageAt(e.pos())
|
|
349
|
+
if index >= 0:
|
|
350
|
+
self.stageClicked.emit(index)
|
|
351
|
+
super().mouseReleaseEvent(e)
|
|
352
|
+
|
|
353
|
+
# ------------------------------------------------------------------ #
|
|
354
|
+
## Designer properties
|
|
355
|
+
# ------------------------------------------------------------------ #
|
|
356
|
+
@Property(str)
|
|
357
|
+
def stagesCsv(self):
|
|
358
|
+
return ",".join("%s=%g" % (label, value) for label, value in self._stages)
|
|
359
|
+
|
|
360
|
+
@stagesCsv.setter
|
|
361
|
+
def stagesCsv(self, text):
|
|
362
|
+
stages = []
|
|
363
|
+
for chunk in str(text).replace(";", ",").split(","):
|
|
364
|
+
chunk = chunk.strip()
|
|
365
|
+
if not chunk:
|
|
366
|
+
continue
|
|
367
|
+
if "=" in chunk:
|
|
368
|
+
label, _, raw = chunk.partition("=")
|
|
369
|
+
else:
|
|
370
|
+
label, raw = "Stage %d" % (len(stages) + 1), chunk
|
|
371
|
+
try:
|
|
372
|
+
stages.append((label.strip() or "Stage %d" % (len(stages) + 1),
|
|
373
|
+
max(0.0, float(raw.strip()))))
|
|
374
|
+
except ValueError:
|
|
375
|
+
continue
|
|
376
|
+
self.setStages(stages)
|
|
377
|
+
|
|
378
|
+
@Property(str)
|
|
379
|
+
def colorsCsv(self):
|
|
380
|
+
return ",".join(c.name() if c is not None else "" for c in self._colors)
|
|
381
|
+
|
|
382
|
+
@colorsCsv.setter
|
|
383
|
+
def colorsCsv(self, text):
|
|
384
|
+
colors = []
|
|
385
|
+
for token in str(text).replace(";", ",").split(","):
|
|
386
|
+
token = token.strip()
|
|
387
|
+
colour = QColor(token) if token else QColor()
|
|
388
|
+
colors.append(colour if colour.isValid() else None)
|
|
389
|
+
self._colors = colors
|
|
390
|
+
self.update()
|
|
391
|
+
|
|
392
|
+
@Property(str)
|
|
393
|
+
def shape(self):
|
|
394
|
+
return self._shape
|
|
395
|
+
|
|
396
|
+
@shape.setter
|
|
397
|
+
def shape(self, value):
|
|
398
|
+
self._shape = "pyramid" if str(value) == "pyramid" else "funnel"
|
|
399
|
+
self.update()
|
|
400
|
+
|
|
401
|
+
@Property(str)
|
|
402
|
+
def orientation(self):
|
|
403
|
+
return self._orientation
|
|
404
|
+
|
|
405
|
+
@orientation.setter
|
|
406
|
+
def orientation(self, value):
|
|
407
|
+
self._orientation = "horizontal" if str(value) == "horizontal" else "vertical"
|
|
408
|
+
self.update()
|
|
409
|
+
|
|
410
|
+
@Property(int)
|
|
411
|
+
def gapPx(self):
|
|
412
|
+
return self._gap
|
|
413
|
+
|
|
414
|
+
@gapPx.setter
|
|
415
|
+
def gapPx(self, value):
|
|
416
|
+
self._gap = max(0, int(value)); self.update()
|
|
417
|
+
|
|
418
|
+
@Property(float)
|
|
419
|
+
def neckRatio(self):
|
|
420
|
+
return self._neckRatio
|
|
421
|
+
|
|
422
|
+
@neckRatio.setter
|
|
423
|
+
def neckRatio(self, value):
|
|
424
|
+
self._neckRatio = max(0.0, min(1.0, float(value))); self.update()
|
|
425
|
+
|
|
426
|
+
@Property(bool)
|
|
427
|
+
def showLabels(self):
|
|
428
|
+
return self._showLabels
|
|
429
|
+
|
|
430
|
+
@showLabels.setter
|
|
431
|
+
def showLabels(self, value):
|
|
432
|
+
self._showLabels = bool(value); self.update()
|
|
433
|
+
|
|
434
|
+
@Property(bool)
|
|
435
|
+
def showValues(self):
|
|
436
|
+
return self._showValues
|
|
437
|
+
|
|
438
|
+
@showValues.setter
|
|
439
|
+
def showValues(self, value):
|
|
440
|
+
self._showValues = bool(value); self.update()
|
|
441
|
+
|
|
442
|
+
@Property(bool)
|
|
443
|
+
def showPercent(self):
|
|
444
|
+
return self._showPercent
|
|
445
|
+
|
|
446
|
+
@showPercent.setter
|
|
447
|
+
def showPercent(self, value):
|
|
448
|
+
self._showPercent = bool(value); self.update()
|
|
449
|
+
|
|
450
|
+
@Property(str)
|
|
451
|
+
def percentOf(self):
|
|
452
|
+
return self._percentOf
|
|
453
|
+
|
|
454
|
+
@percentOf.setter
|
|
455
|
+
def percentOf(self, value):
|
|
456
|
+
self._percentOf = "previous" if str(value) == "previous" else "first"
|
|
457
|
+
self.update()
|
|
458
|
+
|
|
459
|
+
# -- colours (from tokens via qproperty) --
|
|
460
|
+
@Property(QColor)
|
|
461
|
+
def labelColor(self):
|
|
462
|
+
return self._labelColor
|
|
463
|
+
|
|
464
|
+
@labelColor.setter
|
|
465
|
+
def labelColor(self, c):
|
|
466
|
+
self._labelColor = QColor(c); self.update()
|
|
467
|
+
|
|
468
|
+
@Property(QColor)
|
|
469
|
+
def outsideLabelColor(self):
|
|
470
|
+
return self._outsideLabelColor
|
|
471
|
+
|
|
472
|
+
@outsideLabelColor.setter
|
|
473
|
+
def outsideLabelColor(self, c):
|
|
474
|
+
self._outsideLabelColor = QColor(c); self.update()
|