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,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()