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,430 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomDivergingBarChart - a diverging (bipolar / up-down) bar chart.
|
|
9
|
+
##
|
|
10
|
+
## Each category is ONE column split across a zero axis: an UPWARD segment
|
|
11
|
+
## (e.g. income, `upColor`) and a DOWNWARD segment (e.g. expense/expenditure,
|
|
12
|
+
## `downColor`), so a single slot carries two colours and two signs. A
|
|
13
|
+
## configurable `zeroGap` leaves clear space between the + and - bars around
|
|
14
|
+
## the zero line (as in cash-flow dashboards). Painted (no QtCharts) so it is
|
|
15
|
+
## crisp at any size and needs no toolbar/legend.
|
|
16
|
+
##
|
|
17
|
+
## This is the diverging sibling of QCustomMiniBarChart. Give data via
|
|
18
|
+
## setData(up, down, labels) in code, or the upCsv / downCsv / labelsCsv
|
|
19
|
+
## properties in Qt Designer. Optional y-axis (gridlines + prefixed/suffixed
|
|
20
|
+
## value labels, e.g. "EUR 5K") and x labels underneath.
|
|
21
|
+
########################################################################
|
|
22
|
+
from qtpy.QtCore import Qt, Property, QRectF
|
|
23
|
+
from qtpy.QtGui import QColor, QPainter, QBrush, QPen, QPainterPath, QFont, QFontMetrics
|
|
24
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QCustomDivergingBarChart(QWidget):
|
|
28
|
+
|
|
29
|
+
WIDGET_ICON = "components/icons/bar_chart.png"
|
|
30
|
+
WIDGET_TOOLTIP = "A diverging (up/down) bar chart: income up, expense down, split across a zero axis"
|
|
31
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomDivergingBarChart"
|
|
32
|
+
WIDGET_DOM_XML = """
|
|
33
|
+
<ui language='c++'>
|
|
34
|
+
<widget class='QCustomDivergingBarChart' name='customDivergingBarChart'>
|
|
35
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>420</width><height>220</height></rect></property>
|
|
36
|
+
</widget>
|
|
37
|
+
</ui>
|
|
38
|
+
"""
|
|
39
|
+
__catalog__ = {
|
|
40
|
+
"name": "QCustomDivergingBarChart",
|
|
41
|
+
"props": {"upCsv": {"type": "string", "default": "1.2,3.1,2.4,0.9,1.1,4.2,3.0,2.0"},
|
|
42
|
+
"downCsv": {"type": "string", "default": "0.9,1.4,2.6,0.7,0.9,1.7,1.2,1.3"},
|
|
43
|
+
"labelsCsv": {"type": "string", "default": ""},
|
|
44
|
+
"upColor": {"type": "color", "default": "#123f39"},
|
|
45
|
+
"downColor": {"type": "color", "default": "#34d17a"},
|
|
46
|
+
"barWidth": {"type": "int", "default": 12},
|
|
47
|
+
"cornerRadius": {"type": "int", "default": 3},
|
|
48
|
+
"zeroGap": {"type": "int", "default": 8},
|
|
49
|
+
"showAxis": {"type": "bool", "default": True},
|
|
50
|
+
"showLabels": {"type": "bool", "default": True},
|
|
51
|
+
"axisPrefix": {"type": "string", "default": ""},
|
|
52
|
+
"axisSuffix": {"type": "string", "default": ""},
|
|
53
|
+
"gridColor": {"type": "color", "default": "#e6e9ec"},
|
|
54
|
+
"axisTextColor": {"type": "color", "default": "#8b93a1"}},
|
|
55
|
+
"signals": [],
|
|
56
|
+
# Intentionally no tokens_used: this widget resolves no token roles.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Rich editors for the Designer "Custom Properties" dock (see
|
|
60
|
+
# DesignerTools.CustomPropertiesDock).
|
|
61
|
+
DESIGNER_CUSTOM_PROPS = [
|
|
62
|
+
{"name": "upCsv", "kind": "str", "group": "General"},
|
|
63
|
+
{"name": "downCsv", "kind": "str", "group": "General"},
|
|
64
|
+
{"name": "labelsCsv", "kind": "str", "group": "General"},
|
|
65
|
+
{"name": "upColor", "kind": "color", "group": "Colors"},
|
|
66
|
+
{"name": "downColor", "kind": "color", "group": "Colors"},
|
|
67
|
+
{"name": "barWidth", "kind": "int", "group": "General"},
|
|
68
|
+
{"name": "cornerRadius", "kind": "int", "group": "General"},
|
|
69
|
+
{"name": "zeroGap", "kind": "int", "group": "General"},
|
|
70
|
+
{"name": "showAxis", "kind": "bool", "group": "General"},
|
|
71
|
+
{"name": "showLabels", "kind": "bool", "group": "General"},
|
|
72
|
+
{"name": "axisPrefix", "kind": "str", "group": "General"},
|
|
73
|
+
{"name": "axisSuffix", "kind": "str", "group": "General"},
|
|
74
|
+
{"name": "gridColor", "kind": "color", "group": "Colors"},
|
|
75
|
+
{"name": "axisTextColor", "kind": "color", "group": "Colors"},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
def __init__(self, parent=None, up=None, down=None, labels=None):
|
|
79
|
+
super().__init__(parent)
|
|
80
|
+
self.setObjectName("QCustomDivergingBarChart")
|
|
81
|
+
self._up = [float(v) for v in (up or [])]
|
|
82
|
+
self._down = [abs(float(v)) for v in (down or [])] # stored as magnitudes
|
|
83
|
+
self._labels = list(labels) if labels else []
|
|
84
|
+
self._up_color = QColor("#123f39")
|
|
85
|
+
self._down_color = QColor("#34d17a")
|
|
86
|
+
self._bar_width = 12
|
|
87
|
+
self._corner_radius = 3
|
|
88
|
+
self._zero_gap = 8 # px of clear space between + and - bars
|
|
89
|
+
self._show_axis = True
|
|
90
|
+
self._show_labels = True
|
|
91
|
+
self._axis_prefix = ""
|
|
92
|
+
self._axis_suffix = ""
|
|
93
|
+
self._grid_color = QColor("#e6e9ec")
|
|
94
|
+
self._axis_text_color = QColor("#8b93a1")
|
|
95
|
+
self._label_color = QColor("#8b93a1")
|
|
96
|
+
self._label_size = 11
|
|
97
|
+
self._axis_size = 10
|
|
98
|
+
self._tick_step = 0.0 # 0 = auto
|
|
99
|
+
self._baseline_gap = 10 # gap between chart and x labels
|
|
100
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
101
|
+
self.setMinimumSize(120, 100)
|
|
102
|
+
# seed demo data so the widget previews in Designer / render_widget
|
|
103
|
+
# (replaced the moment setData()/upCsv is called).
|
|
104
|
+
if not self._up and not self._down:
|
|
105
|
+
self._up = [1.2, 3.1, 2.4, 0.9, 1.1, 1.3, 4.2, 4.8, 3.0, 1.6, 2.1, 3.4]
|
|
106
|
+
self._down = [0.9, 1.4, 2.6, 0.7, 0.9, 1.1, 1.7, 2.0, 1.2, 1.0, 1.9, 1.5]
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------ #
|
|
109
|
+
## Data API
|
|
110
|
+
# ------------------------------------------------------------------ #
|
|
111
|
+
def setData(self, up, down, labels=None):
|
|
112
|
+
"""Set both series at once. `up` = upward (income) values, `down` =
|
|
113
|
+
downward (expense) values — either sign is accepted for `down`, its
|
|
114
|
+
magnitude is used."""
|
|
115
|
+
self._up = [float(v) for v in (up or [])]
|
|
116
|
+
self._down = [abs(float(v)) for v in (down or [])]
|
|
117
|
+
if labels is not None:
|
|
118
|
+
self._labels = list(labels)
|
|
119
|
+
self.update()
|
|
120
|
+
|
|
121
|
+
def setUpValues(self, values):
|
|
122
|
+
self._up = [float(v) for v in (values or [])]
|
|
123
|
+
self.update()
|
|
124
|
+
|
|
125
|
+
def setDownValues(self, values):
|
|
126
|
+
self._down = [abs(float(v)) for v in (values or [])]
|
|
127
|
+
self.update()
|
|
128
|
+
|
|
129
|
+
def setLabels(self, labels):
|
|
130
|
+
self._labels = list(labels or [])
|
|
131
|
+
self.update()
|
|
132
|
+
|
|
133
|
+
def setColors(self, up_color, down_color):
|
|
134
|
+
self._up_color = QColor(up_color)
|
|
135
|
+
self._down_color = QColor(down_color)
|
|
136
|
+
self.update()
|
|
137
|
+
|
|
138
|
+
def setTickStep(self, step):
|
|
139
|
+
"""Axis gridline/label step in data units (0 = auto)."""
|
|
140
|
+
self._tick_step = max(0.0, float(step))
|
|
141
|
+
self.update()
|
|
142
|
+
|
|
143
|
+
def upValues(self):
|
|
144
|
+
return list(self._up)
|
|
145
|
+
|
|
146
|
+
def downValues(self):
|
|
147
|
+
return list(self._down)
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------ #
|
|
150
|
+
## Painting
|
|
151
|
+
# ------------------------------------------------------------------ #
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _rounded_path(x, y, w, h, r, top=True):
|
|
154
|
+
"""Rounded on the top edge (top=True) or the bottom edge (top=False)."""
|
|
155
|
+
r = max(0.0, min(r, w / 2.0, h))
|
|
156
|
+
p = QPainterPath()
|
|
157
|
+
if top:
|
|
158
|
+
p.moveTo(x, y + h)
|
|
159
|
+
p.lineTo(x, y + r)
|
|
160
|
+
p.quadTo(x, y, x + r, y)
|
|
161
|
+
p.lineTo(x + w - r, y)
|
|
162
|
+
p.quadTo(x + w, y, x + w, y + r)
|
|
163
|
+
p.lineTo(x + w, y + h)
|
|
164
|
+
else:
|
|
165
|
+
p.moveTo(x, y)
|
|
166
|
+
p.lineTo(x, y + h - r)
|
|
167
|
+
p.quadTo(x, y + h, x + r, y + h)
|
|
168
|
+
p.lineTo(x + w - r, y + h)
|
|
169
|
+
p.quadTo(x + w, y + h, x + w, y + h - r)
|
|
170
|
+
p.lineTo(x + w, y)
|
|
171
|
+
p.closeSubpath()
|
|
172
|
+
return p
|
|
173
|
+
|
|
174
|
+
def _nice_step(self, span):
|
|
175
|
+
if self._tick_step > 0:
|
|
176
|
+
return self._tick_step
|
|
177
|
+
if span <= 0:
|
|
178
|
+
return 1.0
|
|
179
|
+
raw = span / 4.0
|
|
180
|
+
import math
|
|
181
|
+
mag = 10 ** math.floor(math.log10(raw))
|
|
182
|
+
for m in (1, 2, 2.5, 5, 10):
|
|
183
|
+
if raw <= m * mag:
|
|
184
|
+
return m * mag
|
|
185
|
+
return 10 * mag
|
|
186
|
+
|
|
187
|
+
def _fmt(self, v):
|
|
188
|
+
txt = ("%g" % round(v, 3))
|
|
189
|
+
return "%s%s%s" % (self._axis_prefix, txt, self._axis_suffix)
|
|
190
|
+
|
|
191
|
+
def paintEvent(self, e):
|
|
192
|
+
if not self._up and not self._down:
|
|
193
|
+
return
|
|
194
|
+
p = QPainter(self)
|
|
195
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
196
|
+
|
|
197
|
+
w, h = self.width(), self.height()
|
|
198
|
+
n = max(len(self._up), len(self._down))
|
|
199
|
+
if n == 0:
|
|
200
|
+
p.end(); return
|
|
201
|
+
|
|
202
|
+
max_up = max(self._up) if self._up else 0.0
|
|
203
|
+
max_dn = max(self._down) if self._down else 0.0
|
|
204
|
+
if max_up <= 0 and max_dn <= 0:
|
|
205
|
+
p.end(); return
|
|
206
|
+
|
|
207
|
+
# x-label room at the bottom
|
|
208
|
+
show_x = bool(self._show_labels and self._labels)
|
|
209
|
+
lbl_font = QFont(self.font()); lbl_font.setPointSize(self._label_size)
|
|
210
|
+
lbl_h = QFontMetrics(lbl_font).height() if show_x else 0
|
|
211
|
+
x_room = (lbl_h + self._baseline_gap) if show_x else 0
|
|
212
|
+
|
|
213
|
+
# y-axis room on the left (widest tick label)
|
|
214
|
+
axis_font = QFont(self.font()); axis_font.setPointSize(self._axis_size)
|
|
215
|
+
afm = QFontMetrics(axis_font)
|
|
216
|
+
left_pad = 0
|
|
217
|
+
step = self._nice_step(max_up + max_dn)
|
|
218
|
+
if self._show_axis:
|
|
219
|
+
widest = max(afm.horizontalAdvance(self._fmt(max_up)),
|
|
220
|
+
afm.horizontalAdvance(self._fmt(-max_dn)),
|
|
221
|
+
afm.horizontalAdvance(self._fmt(0)))
|
|
222
|
+
left_pad = widest + 10
|
|
223
|
+
|
|
224
|
+
chart_x0 = left_pad
|
|
225
|
+
chart_w = max(1.0, w - left_pad)
|
|
226
|
+
chart_h = max(1.0, h - x_room)
|
|
227
|
+
|
|
228
|
+
total = (max_up + max_dn) or 1.0
|
|
229
|
+
gap = float(self._zero_gap)
|
|
230
|
+
usable = max(1.0, chart_h - gap)
|
|
231
|
+
up_region = usable * (max_up / total)
|
|
232
|
+
dn_region = usable * (max_dn / total)
|
|
233
|
+
zero_y = up_region + gap / 2.0 # y of the zero axis (from top)
|
|
234
|
+
up_base = zero_y - gap / 2.0 # up bars grow from here, upward
|
|
235
|
+
dn_base = zero_y + gap / 2.0 # down bars grow from here, downward
|
|
236
|
+
|
|
237
|
+
# --- axis: gridlines + value labels ------------------------------- #
|
|
238
|
+
if self._show_axis:
|
|
239
|
+
p.setFont(axis_font)
|
|
240
|
+
def y_of(val):
|
|
241
|
+
if val >= 0:
|
|
242
|
+
return up_base - (val / max_up) * up_region if max_up > 0 else up_base
|
|
243
|
+
return dn_base + (abs(val) / max_dn) * dn_region if max_dn > 0 else dn_base
|
|
244
|
+
# collect tick values from -max_dn..max_up on `step`, plus zero
|
|
245
|
+
ticks = set([0.0])
|
|
246
|
+
v = step
|
|
247
|
+
while v <= max_up + 1e-9:
|
|
248
|
+
ticks.add(v); v += step
|
|
249
|
+
v = -step
|
|
250
|
+
while v >= -max_dn - 1e-9:
|
|
251
|
+
ticks.add(v); v -= step
|
|
252
|
+
for tv in sorted(ticks):
|
|
253
|
+
gy = y_of(tv)
|
|
254
|
+
pen = QPen(QColor(self._grid_color), 1)
|
|
255
|
+
p.setPen(pen)
|
|
256
|
+
p.drawLine(int(chart_x0), int(gy), int(w), int(gy))
|
|
257
|
+
p.setPen(self._axis_text_color)
|
|
258
|
+
rect = QRectF(0, gy - afm.height() / 2.0, left_pad - 8, afm.height())
|
|
259
|
+
p.drawText(rect, Qt.AlignRight | Qt.AlignVCenter, self._fmt(tv))
|
|
260
|
+
|
|
261
|
+
# --- bars ---------------------------------------------------------- #
|
|
262
|
+
p.setPen(Qt.NoPen)
|
|
263
|
+
col_w = chart_w / n
|
|
264
|
+
bw = float(min(self._bar_width, col_w * 0.7))
|
|
265
|
+
r = float(self._corner_radius)
|
|
266
|
+
for i in range(n):
|
|
267
|
+
cx = chart_x0 + col_w * (i + 0.5)
|
|
268
|
+
x = cx - bw / 2.0
|
|
269
|
+
if i < len(self._up) and self._up[i] > 0 and max_up > 0:
|
|
270
|
+
uh = max(2.0, (self._up[i] / max_up) * up_region)
|
|
271
|
+
p.setBrush(QBrush(self._up_color))
|
|
272
|
+
p.drawPath(self._rounded_path(x, up_base - uh, bw, uh, r, top=True))
|
|
273
|
+
if i < len(self._down) and self._down[i] > 0 and max_dn > 0:
|
|
274
|
+
dh = max(2.0, (self._down[i] / max_dn) * dn_region)
|
|
275
|
+
p.setBrush(QBrush(self._down_color))
|
|
276
|
+
p.drawPath(self._rounded_path(x, dn_base, bw, dh, r, top=False))
|
|
277
|
+
|
|
278
|
+
# --- x labels ------------------------------------------------------ #
|
|
279
|
+
if show_x:
|
|
280
|
+
p.setFont(lbl_font)
|
|
281
|
+
p.setPen(self._label_color)
|
|
282
|
+
ly = chart_h + self._baseline_gap
|
|
283
|
+
for i in range(n):
|
|
284
|
+
if i >= len(self._labels):
|
|
285
|
+
break
|
|
286
|
+
if not str(self._labels[i]):
|
|
287
|
+
continue
|
|
288
|
+
cx = chart_x0 + col_w * (i + 0.5)
|
|
289
|
+
rect = QRectF(cx - col_w / 2.0, ly, col_w, lbl_h)
|
|
290
|
+
p.drawText(rect, Qt.AlignHCenter | Qt.AlignVCenter, str(self._labels[i]))
|
|
291
|
+
p.end()
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------ #
|
|
294
|
+
## Designer properties
|
|
295
|
+
# ------------------------------------------------------------------ #
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _parse_floats(text):
|
|
298
|
+
out = []
|
|
299
|
+
for tok in str(text).replace(";", ",").split(","):
|
|
300
|
+
tok = tok.strip()
|
|
301
|
+
if tok:
|
|
302
|
+
try:
|
|
303
|
+
out.append(float(tok))
|
|
304
|
+
except ValueError:
|
|
305
|
+
pass
|
|
306
|
+
return out
|
|
307
|
+
|
|
308
|
+
@Property(str)
|
|
309
|
+
def upCsv(self):
|
|
310
|
+
return ",".join("%g" % v for v in self._up)
|
|
311
|
+
|
|
312
|
+
@upCsv.setter
|
|
313
|
+
def upCsv(self, text):
|
|
314
|
+
self.setUpValues(self._parse_floats(text))
|
|
315
|
+
|
|
316
|
+
@Property(str)
|
|
317
|
+
def downCsv(self):
|
|
318
|
+
return ",".join("%g" % v for v in self._down)
|
|
319
|
+
|
|
320
|
+
@downCsv.setter
|
|
321
|
+
def downCsv(self, text):
|
|
322
|
+
self.setDownValues(self._parse_floats(text))
|
|
323
|
+
|
|
324
|
+
@Property(str)
|
|
325
|
+
def labelsCsv(self):
|
|
326
|
+
return ",".join(str(x) for x in self._labels)
|
|
327
|
+
|
|
328
|
+
@labelsCsv.setter
|
|
329
|
+
def labelsCsv(self, text):
|
|
330
|
+
self.setLabels([t.strip() for t in str(text).replace(";", ",").split(",") if t.strip()])
|
|
331
|
+
|
|
332
|
+
@Property(QColor)
|
|
333
|
+
def upColor(self):
|
|
334
|
+
return self._up_color
|
|
335
|
+
|
|
336
|
+
@upColor.setter
|
|
337
|
+
def upColor(self, c):
|
|
338
|
+
self._up_color = QColor(c)
|
|
339
|
+
self.update()
|
|
340
|
+
|
|
341
|
+
@Property(QColor)
|
|
342
|
+
def downColor(self):
|
|
343
|
+
return self._down_color
|
|
344
|
+
|
|
345
|
+
@downColor.setter
|
|
346
|
+
def downColor(self, c):
|
|
347
|
+
self._down_color = QColor(c)
|
|
348
|
+
self.update()
|
|
349
|
+
|
|
350
|
+
@Property(int)
|
|
351
|
+
def barWidth(self):
|
|
352
|
+
return self._bar_width
|
|
353
|
+
|
|
354
|
+
@barWidth.setter
|
|
355
|
+
def barWidth(self, v):
|
|
356
|
+
self._bar_width = max(1, int(v))
|
|
357
|
+
self.update()
|
|
358
|
+
|
|
359
|
+
@Property(int)
|
|
360
|
+
def cornerRadius(self):
|
|
361
|
+
return self._corner_radius
|
|
362
|
+
|
|
363
|
+
@cornerRadius.setter
|
|
364
|
+
def cornerRadius(self, v):
|
|
365
|
+
self._corner_radius = max(0, int(v))
|
|
366
|
+
self.update()
|
|
367
|
+
|
|
368
|
+
@Property(int)
|
|
369
|
+
def zeroGap(self):
|
|
370
|
+
return self._zero_gap
|
|
371
|
+
|
|
372
|
+
@zeroGap.setter
|
|
373
|
+
def zeroGap(self, v):
|
|
374
|
+
self._zero_gap = max(0, int(v))
|
|
375
|
+
self.update()
|
|
376
|
+
|
|
377
|
+
@Property(bool)
|
|
378
|
+
def showAxis(self):
|
|
379
|
+
return self._show_axis
|
|
380
|
+
|
|
381
|
+
@showAxis.setter
|
|
382
|
+
def showAxis(self, v):
|
|
383
|
+
self._show_axis = bool(v)
|
|
384
|
+
self.update()
|
|
385
|
+
|
|
386
|
+
@Property(bool)
|
|
387
|
+
def showLabels(self):
|
|
388
|
+
return self._show_labels
|
|
389
|
+
|
|
390
|
+
@showLabels.setter
|
|
391
|
+
def showLabels(self, v):
|
|
392
|
+
self._show_labels = bool(v)
|
|
393
|
+
self.update()
|
|
394
|
+
|
|
395
|
+
@Property(str)
|
|
396
|
+
def axisPrefix(self):
|
|
397
|
+
return self._axis_prefix
|
|
398
|
+
|
|
399
|
+
@axisPrefix.setter
|
|
400
|
+
def axisPrefix(self, t):
|
|
401
|
+
self._axis_prefix = str(t)
|
|
402
|
+
self.update()
|
|
403
|
+
|
|
404
|
+
@Property(str)
|
|
405
|
+
def axisSuffix(self):
|
|
406
|
+
return self._axis_suffix
|
|
407
|
+
|
|
408
|
+
@axisSuffix.setter
|
|
409
|
+
def axisSuffix(self, t):
|
|
410
|
+
self._axis_suffix = str(t)
|
|
411
|
+
self.update()
|
|
412
|
+
|
|
413
|
+
@Property(QColor)
|
|
414
|
+
def gridColor(self):
|
|
415
|
+
return self._grid_color
|
|
416
|
+
|
|
417
|
+
@gridColor.setter
|
|
418
|
+
def gridColor(self, c):
|
|
419
|
+
self._grid_color = QColor(c)
|
|
420
|
+
self.update()
|
|
421
|
+
|
|
422
|
+
@Property(QColor)
|
|
423
|
+
def axisTextColor(self):
|
|
424
|
+
return self._axis_text_color
|
|
425
|
+
|
|
426
|
+
@axisTextColor.setter
|
|
427
|
+
def axisTextColor(self, c):
|
|
428
|
+
self._axis_text_color = QColor(c)
|
|
429
|
+
self._label_color = QColor(c)
|
|
430
|
+
self.update()
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomDotMatrix - a density / category dot grid.
|
|
9
|
+
##
|
|
10
|
+
## A grid of small dots where each cell carries a STATE (0..N). State 0 is
|
|
11
|
+
## the "empty" slot (drawn faintly with `emptyColor`); states 1..N pick a
|
|
12
|
+
## colour from `colorsCsv`. Use it for the dot-matrix density panels seen on
|
|
13
|
+
## modern dashboards (e.g. a "product" activity grid: valid / invalid / idle
|
|
14
|
+
## dots that thicken toward one corner). Painted (no QtCharts) so it is crisp
|
|
15
|
+
## at any size and needs no legend/toolbar.
|
|
16
|
+
##
|
|
17
|
+
## Give the grid in code with setData([[0,1,2,...], ...]) (row-major 2-D list
|
|
18
|
+
## of state ints), or in Qt Designer with the `dataCsv` property (rows joined
|
|
19
|
+
## by ';', cells by ','). `rows`/`cols` size an empty grid. Colours are
|
|
20
|
+
## qproperties so a theme/manager can tokenise them and they flip on theme
|
|
21
|
+
## switch.
|
|
22
|
+
########################################################################
|
|
23
|
+
from qtpy.QtCore import Qt, Property, QRectF
|
|
24
|
+
from qtpy.QtGui import QColor, QPainter, QBrush
|
|
25
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QCustomDotMatrix(QWidget):
|
|
29
|
+
|
|
30
|
+
WIDGET_ICON = "components/icons/bar_chart.png"
|
|
31
|
+
WIDGET_TOOLTIP = "A density / category dot grid (valid / invalid / idle dots)"
|
|
32
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomDotMatrix"
|
|
33
|
+
WIDGET_DOM_XML = """
|
|
34
|
+
<ui language='c++'>
|
|
35
|
+
<widget class='QCustomDotMatrix' name='customDotMatrix'>
|
|
36
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>260</width><height>120</height></rect></property>
|
|
37
|
+
</widget>
|
|
38
|
+
</ui>
|
|
39
|
+
"""
|
|
40
|
+
__catalog__ = {
|
|
41
|
+
"name": "QCustomDotMatrix",
|
|
42
|
+
"props": {"dataCsv": {"type": "string", "default": ""},
|
|
43
|
+
"rows": {"type": "int", "default": 6},
|
|
44
|
+
"cols": {"type": "int", "default": 14},
|
|
45
|
+
"colorsCsv": {"type": "string", "default": "#8fe36b,#ffffff,#f6912b"},
|
|
46
|
+
"emptyColor": {"type": "color", "default": "#2c2c30"},
|
|
47
|
+
"dotDiameter": {"type": "int", "default": 0},
|
|
48
|
+
"gapRatio": {"type": "float", "default": 0.55},
|
|
49
|
+
"emptyOpacity": {"type": "float", "default": 0.6},
|
|
50
|
+
"square": {"type": "bool", "default": False}},
|
|
51
|
+
"signals": [],
|
|
52
|
+
# Intentionally no tokens_used: this widget resolves no token roles.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Rich editors for the Designer "Custom Properties" dock (see
|
|
56
|
+
# DesignerTools.CustomPropertiesDock).
|
|
57
|
+
DESIGNER_CUSTOM_PROPS = [
|
|
58
|
+
{"name": "dataCsv", "kind": "str", "group": "General"},
|
|
59
|
+
{"name": "rows", "kind": "int", "group": "General"},
|
|
60
|
+
{"name": "cols", "kind": "int", "group": "General"},
|
|
61
|
+
{"name": "colorsCsv", "kind": "str", "group": "Colors"},
|
|
62
|
+
{"name": "emptyColor", "kind": "color", "group": "Colors"},
|
|
63
|
+
{"name": "dotDiameter", "kind": "int", "group": "General"},
|
|
64
|
+
{"name": "gapRatio", "kind": "float", "group": "General"},
|
|
65
|
+
{"name": "emptyOpacity", "kind": "float", "group": "General"},
|
|
66
|
+
{"name": "square", "kind": "bool", "group": "General"},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# a deterministic demo pattern (density thickens toward the bottom-right)
|
|
70
|
+
_DEMO = [
|
|
71
|
+
[0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 2, 1, 2],
|
|
72
|
+
[0, 0, 0, 1, 0, 2, 1, 2, 1, 1, 2, 1, 2, 1],
|
|
73
|
+
[0, 1, 0, 2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 2],
|
|
74
|
+
[1, 2, 1, 1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 3],
|
|
75
|
+
[2, 1, 2, 2, 1, 1, 2, 1, 3, 2, 1, 2, 3, 2],
|
|
76
|
+
[1, 2, 1, 2, 3, 2, 1, 3, 2, 1, 3, 2, 1, 3],
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def __init__(self, parent=None, data=None):
|
|
80
|
+
super().__init__(parent)
|
|
81
|
+
self.setObjectName("QCustomDotMatrix")
|
|
82
|
+
self._data = [list(r) for r in data] if data else []
|
|
83
|
+
self._rows = 6
|
|
84
|
+
self._cols = 14
|
|
85
|
+
self._colors = [QColor("#8fe36b"), QColor("#ffffff"), QColor("#f6912b")]
|
|
86
|
+
self._empty = QColor("#2c2c30")
|
|
87
|
+
self._dot_d = 0 # 0 = auto-fit
|
|
88
|
+
self._gap_ratio = 0.55 # gap as a fraction of the dot diameter
|
|
89
|
+
self._empty_opacity = 0.6
|
|
90
|
+
self._square = False
|
|
91
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
92
|
+
self.setMinimumSize(80, 48)
|
|
93
|
+
if not self._data:
|
|
94
|
+
self._data = [list(r) for r in self._DEMO]
|
|
95
|
+
self._rows = len(self._DEMO)
|
|
96
|
+
self._cols = len(self._DEMO[0])
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------ #
|
|
99
|
+
## Data API
|
|
100
|
+
# ------------------------------------------------------------------ #
|
|
101
|
+
def setData(self, data):
|
|
102
|
+
"""2-D row-major list of state ints (0 = empty, 1..N = colour index)."""
|
|
103
|
+
self._data = [list(r) for r in (data or [])]
|
|
104
|
+
if self._data:
|
|
105
|
+
self._rows = len(self._data)
|
|
106
|
+
self._cols = max(len(r) for r in self._data)
|
|
107
|
+
self.update()
|
|
108
|
+
|
|
109
|
+
def data(self):
|
|
110
|
+
return [list(r) for r in self._data]
|
|
111
|
+
|
|
112
|
+
def setColors(self, colors):
|
|
113
|
+
self._colors = [QColor(c) for c in colors if c]
|
|
114
|
+
self.update()
|
|
115
|
+
|
|
116
|
+
def _grid(self):
|
|
117
|
+
"""Normalised rows×cols grid (pads short rows with 0)."""
|
|
118
|
+
rows = self._data if self._data else [[0] * self._cols for _ in range(self._rows)]
|
|
119
|
+
cols = max((len(r) for r in rows), default=self._cols)
|
|
120
|
+
return [[(r[c] if c < len(r) else 0) for c in range(cols)] for r in rows], len(rows), cols
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------ #
|
|
123
|
+
## Painting
|
|
124
|
+
# ------------------------------------------------------------------ #
|
|
125
|
+
def paintEvent(self, e):
|
|
126
|
+
grid, nrows, ncols = self._grid()
|
|
127
|
+
if nrows == 0 or ncols == 0:
|
|
128
|
+
return
|
|
129
|
+
p = QPainter(self)
|
|
130
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
131
|
+
w, h = self.width(), self.height()
|
|
132
|
+
|
|
133
|
+
# cell pitch that fits both axes; dot = pitch / (1 + gapRatio)
|
|
134
|
+
g = max(0.0, float(self._gap_ratio))
|
|
135
|
+
pitch_x = w / ncols
|
|
136
|
+
pitch_y = h / nrows
|
|
137
|
+
pitch = min(pitch_x, pitch_y)
|
|
138
|
+
d = self._dot_d if self._dot_d > 0 else pitch / (1.0 + g)
|
|
139
|
+
d = max(2.0, d)
|
|
140
|
+
gap = pitch - d
|
|
141
|
+
|
|
142
|
+
grid_w = ncols * pitch
|
|
143
|
+
grid_h = nrows * pitch
|
|
144
|
+
ox = (w - grid_w) / 2.0
|
|
145
|
+
oy = (h - grid_h) / 2.0
|
|
146
|
+
|
|
147
|
+
p.setPen(Qt.NoPen)
|
|
148
|
+
for r in range(nrows):
|
|
149
|
+
for c in range(ncols):
|
|
150
|
+
state = grid[r][c]
|
|
151
|
+
if state <= 0:
|
|
152
|
+
col = QColor(self._empty)
|
|
153
|
+
col.setAlphaF(max(0.0, min(1.0, self._empty_opacity)))
|
|
154
|
+
else:
|
|
155
|
+
idx = (state - 1) % len(self._colors) if self._colors else 0
|
|
156
|
+
col = QColor(self._colors[idx]) if self._colors else QColor("#ffffff")
|
|
157
|
+
x = ox + c * pitch + gap / 2.0
|
|
158
|
+
y = oy + r * pitch + gap / 2.0
|
|
159
|
+
p.setBrush(QBrush(col))
|
|
160
|
+
rect = QRectF(x, y, d, d)
|
|
161
|
+
if self._square:
|
|
162
|
+
p.drawRoundedRect(rect, d * 0.28, d * 0.28)
|
|
163
|
+
else:
|
|
164
|
+
p.drawEllipse(rect)
|
|
165
|
+
p.end()
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------ #
|
|
168
|
+
## Designer properties
|
|
169
|
+
# ------------------------------------------------------------------ #
|
|
170
|
+
@Property(str)
|
|
171
|
+
def dataCsv(self):
|
|
172
|
+
return ";".join(",".join(str(int(v)) for v in row) for row in self._grid()[0])
|
|
173
|
+
|
|
174
|
+
@dataCsv.setter
|
|
175
|
+
def dataCsv(self, text):
|
|
176
|
+
rows = []
|
|
177
|
+
for line in str(text).replace("|", ";").split(";"):
|
|
178
|
+
line = line.strip()
|
|
179
|
+
if not line:
|
|
180
|
+
continue
|
|
181
|
+
row = []
|
|
182
|
+
for tok in line.split(","):
|
|
183
|
+
tok = tok.strip()
|
|
184
|
+
if not tok:
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
row.append(int(float(tok)))
|
|
188
|
+
except ValueError:
|
|
189
|
+
row.append(0)
|
|
190
|
+
rows.append(row)
|
|
191
|
+
if rows:
|
|
192
|
+
self.setData(rows)
|
|
193
|
+
|
|
194
|
+
@Property(int)
|
|
195
|
+
def rows(self):
|
|
196
|
+
return self._rows
|
|
197
|
+
|
|
198
|
+
@rows.setter
|
|
199
|
+
def rows(self, v):
|
|
200
|
+
self._rows = max(1, int(v))
|
|
201
|
+
if not self._data or len(self._data) != self._rows:
|
|
202
|
+
self.update()
|
|
203
|
+
|
|
204
|
+
@Property(int)
|
|
205
|
+
def cols(self):
|
|
206
|
+
return self._cols
|
|
207
|
+
|
|
208
|
+
@cols.setter
|
|
209
|
+
def cols(self, v):
|
|
210
|
+
self._cols = max(1, int(v))
|
|
211
|
+
self.update()
|
|
212
|
+
|
|
213
|
+
@Property(str)
|
|
214
|
+
def colorsCsv(self):
|
|
215
|
+
return ",".join(c.name() for c in self._colors)
|
|
216
|
+
|
|
217
|
+
@colorsCsv.setter
|
|
218
|
+
def colorsCsv(self, text):
|
|
219
|
+
cols = [t.strip() for t in str(text).replace(";", ",").split(",") if t.strip()]
|
|
220
|
+
if cols:
|
|
221
|
+
self.setColors(cols)
|
|
222
|
+
|
|
223
|
+
@Property(QColor)
|
|
224
|
+
def emptyColor(self):
|
|
225
|
+
return self._empty
|
|
226
|
+
|
|
227
|
+
@emptyColor.setter
|
|
228
|
+
def emptyColor(self, c):
|
|
229
|
+
self._empty = QColor(c)
|
|
230
|
+
self.update()
|
|
231
|
+
|
|
232
|
+
@Property(int)
|
|
233
|
+
def dotDiameter(self):
|
|
234
|
+
return self._dot_d
|
|
235
|
+
|
|
236
|
+
@dotDiameter.setter
|
|
237
|
+
def dotDiameter(self, v):
|
|
238
|
+
self._dot_d = max(0, int(v))
|
|
239
|
+
self.update()
|
|
240
|
+
|
|
241
|
+
@Property(float)
|
|
242
|
+
def gapRatio(self):
|
|
243
|
+
return self._gap_ratio
|
|
244
|
+
|
|
245
|
+
@gapRatio.setter
|
|
246
|
+
def gapRatio(self, v):
|
|
247
|
+
self._gap_ratio = max(0.0, float(v))
|
|
248
|
+
self.update()
|
|
249
|
+
|
|
250
|
+
@Property(float)
|
|
251
|
+
def emptyOpacity(self):
|
|
252
|
+
return self._empty_opacity
|
|
253
|
+
|
|
254
|
+
@emptyOpacity.setter
|
|
255
|
+
def emptyOpacity(self, v):
|
|
256
|
+
self._empty_opacity = max(0.0, min(1.0, float(v)))
|
|
257
|
+
self.update()
|
|
258
|
+
|
|
259
|
+
@Property(bool)
|
|
260
|
+
def square(self):
|
|
261
|
+
return self._square
|
|
262
|
+
|
|
263
|
+
@square.setter
|
|
264
|
+
def square(self, v):
|
|
265
|
+
self._square = bool(v)
|
|
266
|
+
self.update()
|