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,555 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomHeatmap - a painted colour-intensity grid.
|
|
9
|
+
##
|
|
10
|
+
## Two modes (`mode`):
|
|
11
|
+
## "grid" (default) - a rows x cols matrix (e.g. hours x weekdays, the
|
|
12
|
+
## "Activity by time" heatmap): each cell's colour is its value mapped on a
|
|
13
|
+
## low->high ramp, with row/col labels and an optional Less->More legend.
|
|
14
|
+
## "calendar" - a GitHub-style contributions calendar: a flat list of daily
|
|
15
|
+
## values wrapped into 7 rows (weekdays) x N columns (weeks).
|
|
16
|
+
##
|
|
17
|
+
## Values come in via setValues(list[list[float]]) / a flat list (calendar), or
|
|
18
|
+
## the `valuesCsv` Designer property (rows separated by ';', cells by ','). Cells
|
|
19
|
+
## auto-normalise across the data (min->max) unless you setRange(...). Painted
|
|
20
|
+
## with QPainter so it stays crisp at any size; the grid FLEX-fits the box left
|
|
21
|
+
## after labels + legend, so nothing clips when the widget grows or shrinks.
|
|
22
|
+
##
|
|
23
|
+
## Signals: cellClicked(row, col, value); a per-cell tooltip on hover.
|
|
24
|
+
########################################################################
|
|
25
|
+
import math
|
|
26
|
+
|
|
27
|
+
from qtpy.QtCore import Qt, Property, Signal, QRectF, QPointF
|
|
28
|
+
from qtpy.QtGui import QColor, QPainter, QPen, QBrush, QFont, QFontMetrics
|
|
29
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QCustomHeatmap(QWidget):
|
|
33
|
+
|
|
34
|
+
cellClicked = Signal(int, int, float)
|
|
35
|
+
|
|
36
|
+
WIDGET_ICON = "components/icons/grid_on.png"
|
|
37
|
+
WIDGET_TOOLTIP = "A painted colour-intensity grid (activity / calendar heatmap)"
|
|
38
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomHeatmap"
|
|
39
|
+
WIDGET_DOM_XML = """
|
|
40
|
+
<ui language='c++'>
|
|
41
|
+
<widget class='QCustomHeatmap' name='customHeatmap'>
|
|
42
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>340</width><height>230</height></rect></property>
|
|
43
|
+
</widget>
|
|
44
|
+
</ui>
|
|
45
|
+
"""
|
|
46
|
+
__catalog__ = {
|
|
47
|
+
"name": "QCustomHeatmap",
|
|
48
|
+
"props": {
|
|
49
|
+
"mode": {"type": "enum", "values": ["grid", "calendar"], "default": "grid"},
|
|
50
|
+
"valuesCsv": {"type": "string", "default": ""},
|
|
51
|
+
"lowColor": {"type": "color", "default": "#1e1b3a"},
|
|
52
|
+
"highColor": {"type": "color", "default": "#b3a4ff"},
|
|
53
|
+
"emptyColor": {"type": "color", "default": "#17152b"},
|
|
54
|
+
"rowLabelsCsv": {"type": "string", "default": ""},
|
|
55
|
+
"colLabelsCsv": {"type": "string", "default": ""},
|
|
56
|
+
"cellSize": {"type": "int", "default": 0},
|
|
57
|
+
"cellGap": {"type": "int", "default": 4},
|
|
58
|
+
"cornerRadius": {"type": "int", "default": 4},
|
|
59
|
+
"showLabels": {"type": "bool", "default": True},
|
|
60
|
+
"showLegend": {"type": "bool", "default": True},
|
|
61
|
+
"labelColor": {"type": "color", "default": "#8b90a0"},
|
|
62
|
+
"autoNormalize": {"type": "bool", "default": True},
|
|
63
|
+
"minValue": {"type": "float", "default": 0.0},
|
|
64
|
+
"maxValue": {"type": "float", "default": 1.0},
|
|
65
|
+
},
|
|
66
|
+
"signals": ["cellClicked"],
|
|
67
|
+
# Intentionally no tokens_used: this widget resolves no token roles.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Rich editors for the Designer "Custom Properties" dock (see
|
|
71
|
+
# DesignerTools.CustomPropertiesDock).
|
|
72
|
+
DESIGNER_CUSTOM_PROPS = [
|
|
73
|
+
{"name": "mode", "kind": "choice", "group": "General"},
|
|
74
|
+
{"name": "valuesCsv", "kind": "str", "group": "General"},
|
|
75
|
+
{"name": "lowColor", "kind": "color", "group": "Colors"},
|
|
76
|
+
{"name": "highColor", "kind": "color", "group": "Colors"},
|
|
77
|
+
{"name": "emptyColor", "kind": "color", "group": "Colors"},
|
|
78
|
+
{"name": "rowLabelsCsv", "kind": "str", "group": "General"},
|
|
79
|
+
{"name": "colLabelsCsv", "kind": "str", "group": "General"},
|
|
80
|
+
{"name": "cellSize", "kind": "int", "group": "General"},
|
|
81
|
+
{"name": "cellGap", "kind": "int", "group": "General"},
|
|
82
|
+
{"name": "cornerRadius", "kind": "int", "group": "General"},
|
|
83
|
+
{"name": "showLabels", "kind": "bool", "group": "General"},
|
|
84
|
+
{"name": "showLegend", "kind": "bool", "group": "General"},
|
|
85
|
+
{"name": "labelColor", "kind": "color", "group": "Colors"},
|
|
86
|
+
{"name": "autoNormalize", "kind": "bool", "group": "General"},
|
|
87
|
+
{"name": "minValue", "kind": "float", "group": "General"},
|
|
88
|
+
{"name": "maxValue", "kind": "float", "group": "General"},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def __init__(self, parent=None, values=None, mode="grid"):
|
|
92
|
+
super().__init__(parent)
|
|
93
|
+
self.setObjectName("QCustomHeatmap")
|
|
94
|
+
self._mode = "calendar" if str(mode) == "calendar" else "grid"
|
|
95
|
+
self._values = self._coerce(values) if values is not None else self._seed()
|
|
96
|
+
self._low = QColor("#1e1b3a")
|
|
97
|
+
self._high = QColor("#b3a4ff")
|
|
98
|
+
self._empty = QColor("#17152b")
|
|
99
|
+
self._row_labels = ["1pm", "2pm", "3pm", "4pm", "5pm", "6pm"]
|
|
100
|
+
self._col_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
101
|
+
self._cell_size = 0 # 0 -> auto flex-fit
|
|
102
|
+
self._cell_gap = 4
|
|
103
|
+
self._radius = 4
|
|
104
|
+
self._show_labels = True
|
|
105
|
+
self._show_legend = True
|
|
106
|
+
self._label_color = QColor("#8b90a0")
|
|
107
|
+
self._auto = True
|
|
108
|
+
self._min = 0.0
|
|
109
|
+
self._max = 1.0
|
|
110
|
+
self._hover = (-1, -1)
|
|
111
|
+
self._rects = {} # (r,c) -> QRectF (last layout, for hit-test)
|
|
112
|
+
self.setMouseTracking(True)
|
|
113
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
114
|
+
self.setMinimumSize(120, 90)
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------ #
|
|
117
|
+
## Data
|
|
118
|
+
# ------------------------------------------------------------------ #
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _seed():
|
|
121
|
+
# a small activity-by-time grid so it previews in Designer / render_widget
|
|
122
|
+
import itertools
|
|
123
|
+
rng = [3, 8, 2, 6, 9, 1, 4, 7, 5, 2, 8, 6, 3, 9, 4, 1, 7, 5, 6, 2, 8]
|
|
124
|
+
it = itertools.cycle(rng)
|
|
125
|
+
return [[float(next(it)) for _ in range(7)] for _ in range(6)]
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _coerce(values):
|
|
129
|
+
"""Accept list[list] or a flat list (wrapped later for calendar)."""
|
|
130
|
+
if not values:
|
|
131
|
+
return []
|
|
132
|
+
if isinstance(values[0], (list, tuple)):
|
|
133
|
+
return [[(None if v is None else float(v)) for v in row] for row in values]
|
|
134
|
+
return [float(v) for v in values] # flat -> arranged per mode
|
|
135
|
+
|
|
136
|
+
def setValues(self, values):
|
|
137
|
+
self._values = self._coerce(values)
|
|
138
|
+
self.update()
|
|
139
|
+
|
|
140
|
+
def setData(self, values):
|
|
141
|
+
"""Backward-compatible alias for setValues().
|
|
142
|
+
|
|
143
|
+
Early releases shipped setData(); keep it so existing demos and user
|
|
144
|
+
code keep working after the rename.
|
|
145
|
+
"""
|
|
146
|
+
self.setValues(values)
|
|
147
|
+
|
|
148
|
+
def setRange(self, minimum, maximum):
|
|
149
|
+
self._auto = False
|
|
150
|
+
self._min, self._max = float(minimum), float(maximum)
|
|
151
|
+
self.update()
|
|
152
|
+
|
|
153
|
+
def setColors(self, low, high, empty=None):
|
|
154
|
+
self._low, self._high = QColor(low), QColor(high)
|
|
155
|
+
if empty is not None:
|
|
156
|
+
self._empty = QColor(empty)
|
|
157
|
+
self.update()
|
|
158
|
+
|
|
159
|
+
def setLabels(self, row_labels=None, col_labels=None):
|
|
160
|
+
if row_labels is not None:
|
|
161
|
+
self._row_labels = [str(s) for s in row_labels]
|
|
162
|
+
if col_labels is not None:
|
|
163
|
+
self._col_labels = [str(s) for s in col_labels]
|
|
164
|
+
self.update()
|
|
165
|
+
|
|
166
|
+
def setMode(self, mode):
|
|
167
|
+
self._mode = "calendar" if str(mode) == "calendar" else "grid"
|
|
168
|
+
self.update()
|
|
169
|
+
|
|
170
|
+
def values(self):
|
|
171
|
+
return self._values
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------ #
|
|
174
|
+
## Grid shape (mode-aware): returns (rows, cols, get(r,c)->value|None)
|
|
175
|
+
# ------------------------------------------------------------------ #
|
|
176
|
+
def _matrix(self):
|
|
177
|
+
vals = self._values
|
|
178
|
+
if not vals:
|
|
179
|
+
return 0, 0, (lambda r, c: None)
|
|
180
|
+
if self._mode == "calendar":
|
|
181
|
+
flat = vals
|
|
182
|
+
if flat and isinstance(flat[0], (list, tuple)):
|
|
183
|
+
flat = [v for row in flat for v in row]
|
|
184
|
+
weeks = max(1, math.ceil(len(flat) / 7.0))
|
|
185
|
+
|
|
186
|
+
def get(r, c):
|
|
187
|
+
i = c * 7 + r
|
|
188
|
+
return flat[i] if 0 <= i < len(flat) else None
|
|
189
|
+
return 7, weeks, get
|
|
190
|
+
# grid
|
|
191
|
+
rows = len(vals)
|
|
192
|
+
cols = max((len(row) for row in vals), default=0)
|
|
193
|
+
|
|
194
|
+
def get(r, c):
|
|
195
|
+
row = vals[r]
|
|
196
|
+
return row[c] if c < len(row) else None
|
|
197
|
+
return rows, cols, get
|
|
198
|
+
|
|
199
|
+
def _norm_range(self):
|
|
200
|
+
if not self._auto:
|
|
201
|
+
return self._min, self._max
|
|
202
|
+
lo, hi = None, None
|
|
203
|
+
for row in self._values:
|
|
204
|
+
row = row if isinstance(row, (list, tuple)) else [row]
|
|
205
|
+
for v in row:
|
|
206
|
+
if v is None:
|
|
207
|
+
continue
|
|
208
|
+
lo = v if lo is None else min(lo, v)
|
|
209
|
+
hi = v if hi is None else max(hi, v)
|
|
210
|
+
if lo is None:
|
|
211
|
+
return 0.0, 1.0
|
|
212
|
+
return lo, hi
|
|
213
|
+
|
|
214
|
+
def _cell_color(self, v, lo, hi):
|
|
215
|
+
if v is None:
|
|
216
|
+
return QColor(self._empty)
|
|
217
|
+
t = 0.5 if hi <= lo else (v - lo) / (hi - lo)
|
|
218
|
+
t = max(0.0, min(1.0, t))
|
|
219
|
+
return self._lerp(self._low, self._high, t)
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _lerp(c0, c1, t):
|
|
223
|
+
return QColor(
|
|
224
|
+
int(c0.red() + (c1.red() - c0.red()) * t),
|
|
225
|
+
int(c0.green() + (c1.green() - c0.green()) * t),
|
|
226
|
+
int(c0.blue() + (c1.blue() - c0.blue()) * t),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------------ #
|
|
230
|
+
## Flex layout — size the cells to the box left after labels + legend
|
|
231
|
+
# ------------------------------------------------------------------ #
|
|
232
|
+
def _label_font(self):
|
|
233
|
+
"""One label font used for BOTH measuring the gutters and painting the
|
|
234
|
+
labels — a size mismatch here clips the row labels."""
|
|
235
|
+
f = QFont(self.font())
|
|
236
|
+
side = min(self.width(), self.height())
|
|
237
|
+
f.setPointSizeF(max(7.5, min(11.0, side * 0.034)))
|
|
238
|
+
return f
|
|
239
|
+
|
|
240
|
+
def _layout(self, rows, cols):
|
|
241
|
+
w, h = self.width(), self.height()
|
|
242
|
+
m = 8.0
|
|
243
|
+
fm = QFontMetrics(self._label_font())
|
|
244
|
+
show_lab = self._show_labels
|
|
245
|
+
# left gutter for row labels, top strip for column labels
|
|
246
|
+
left = 0.0
|
|
247
|
+
if show_lab and self._row_labels and self._mode == "grid":
|
|
248
|
+
left = max((fm.horizontalAdvance(s) for s in self._row_labels), default=0) + 12.0
|
|
249
|
+
top = 0.0
|
|
250
|
+
if show_lab and self._col_labels and self._mode == "grid":
|
|
251
|
+
top = fm.height() + 4.0
|
|
252
|
+
legend_h = (fm.height() + 10.0) if self._show_legend else 0.0
|
|
253
|
+
|
|
254
|
+
avail_w = max(1.0, w - 2 * m - left)
|
|
255
|
+
avail_h = max(1.0, h - 2 * m - top - legend_h)
|
|
256
|
+
gap = float(self._cell_gap)
|
|
257
|
+
if self._cell_size > 0:
|
|
258
|
+
cell = float(self._cell_size)
|
|
259
|
+
else:
|
|
260
|
+
cell = min((avail_w - (cols - 1) * gap) / max(cols, 1),
|
|
261
|
+
(avail_h - (rows - 1) * gap) / max(rows, 1))
|
|
262
|
+
cell = max(cell, 2.0)
|
|
263
|
+
grid_w = cols * cell + (cols - 1) * gap
|
|
264
|
+
grid_h = rows * cell + (rows - 1) * gap
|
|
265
|
+
x0 = m + left + max(0.0, (avail_w - grid_w) / 2.0)
|
|
266
|
+
y0 = m + top + max(0.0, (avail_h - grid_h) / 2.0)
|
|
267
|
+
return x0, y0, cell, gap, left, top, legend_h
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------ #
|
|
270
|
+
## Painting
|
|
271
|
+
# ------------------------------------------------------------------ #
|
|
272
|
+
def paintEvent(self, e):
|
|
273
|
+
rows, cols, get = self._matrix()
|
|
274
|
+
if rows == 0 or cols == 0:
|
|
275
|
+
return
|
|
276
|
+
p = QPainter(self)
|
|
277
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
278
|
+
lo, hi = self._norm_range()
|
|
279
|
+
x0, y0, cell, gap, left, top, legend_h = self._layout(rows, cols)
|
|
280
|
+
self._rects = {}
|
|
281
|
+
|
|
282
|
+
# labels (grid mode) — same font used to measure the gutters
|
|
283
|
+
if self._show_labels and self._mode == "grid":
|
|
284
|
+
p.setFont(self._label_font())
|
|
285
|
+
p.setPen(QPen(self._label_color))
|
|
286
|
+
if top:
|
|
287
|
+
for c in range(cols):
|
|
288
|
+
if c < len(self._col_labels):
|
|
289
|
+
cx = x0 + c * (cell + gap)
|
|
290
|
+
p.drawText(QRectF(cx, y0 - top, cell, top - 2),
|
|
291
|
+
Qt.AlignCenter, self._col_labels[c])
|
|
292
|
+
if left:
|
|
293
|
+
for r in range(rows):
|
|
294
|
+
if r < len(self._row_labels):
|
|
295
|
+
ry = y0 + r * (cell + gap)
|
|
296
|
+
p.drawText(QRectF(4, ry, left - 8, cell),
|
|
297
|
+
Qt.AlignVCenter | Qt.AlignRight, self._row_labels[r])
|
|
298
|
+
|
|
299
|
+
# cells
|
|
300
|
+
for r in range(rows):
|
|
301
|
+
for c in range(cols):
|
|
302
|
+
v = get(r, c)
|
|
303
|
+
rect = QRectF(x0 + c * (cell + gap), y0 + r * (cell + gap), cell, cell)
|
|
304
|
+
self._rects[(r, c)] = rect
|
|
305
|
+
p.setPen(Qt.NoPen)
|
|
306
|
+
p.setBrush(QBrush(self._cell_color(v, lo, hi)))
|
|
307
|
+
p.drawRoundedRect(rect, self._radius, self._radius)
|
|
308
|
+
if (r, c) == self._hover and v is not None:
|
|
309
|
+
pen = QPen(QColor(255, 255, 255, 150), max(1.0, cell * 0.06))
|
|
310
|
+
p.setPen(pen)
|
|
311
|
+
p.setBrush(Qt.NoBrush)
|
|
312
|
+
p.drawRoundedRect(rect, self._radius, self._radius)
|
|
313
|
+
|
|
314
|
+
if self._show_legend:
|
|
315
|
+
self._paint_legend(p, x0, self.height() - 8.0 - (legend_h - 10.0),
|
|
316
|
+
x0 + cols * (cell + gap) - gap)
|
|
317
|
+
p.end()
|
|
318
|
+
|
|
319
|
+
def _paint_legend(self, p, x_left, y, x_right):
|
|
320
|
+
fm = p.fontMetrics()
|
|
321
|
+
f = QFont(self.font())
|
|
322
|
+
f.setPointSizeF(max(7.0, self.font().pointSizeF()))
|
|
323
|
+
p.setFont(f)
|
|
324
|
+
sw = max(9.0, fm.height() - 2) # swatch size
|
|
325
|
+
gap = 3.0
|
|
326
|
+
steps = 4
|
|
327
|
+
block = steps * sw + (steps - 1) * gap
|
|
328
|
+
more_w = fm.horizontalAdvance("More") + 3
|
|
329
|
+
less_w = fm.horizontalAdvance("Less") + 3
|
|
330
|
+
total = less_w + 6 + block + 6 + more_w
|
|
331
|
+
x = max(x_left, x_right - total)
|
|
332
|
+
p.setPen(QPen(self._label_color))
|
|
333
|
+
p.drawText(QRectF(x, y - fm.height() / 2.0 + sw / 2.0, less_w, fm.height()),
|
|
334
|
+
Qt.AlignVCenter | Qt.AlignLeft, "Less")
|
|
335
|
+
x += less_w + 6
|
|
336
|
+
for i in range(steps):
|
|
337
|
+
t = i / (steps - 1)
|
|
338
|
+
p.setPen(Qt.NoPen)
|
|
339
|
+
p.setBrush(QBrush(self._lerp(self._low, self._high, t)))
|
|
340
|
+
p.drawRoundedRect(QRectF(x, y, sw, sw), 2, 2)
|
|
341
|
+
x += sw + gap
|
|
342
|
+
x += 6 - gap
|
|
343
|
+
p.setPen(QPen(self._label_color))
|
|
344
|
+
p.drawText(QRectF(x, y - fm.height() / 2.0 + sw / 2.0, more_w, fm.height()),
|
|
345
|
+
Qt.AlignVCenter | Qt.AlignLeft, "More")
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------ #
|
|
348
|
+
## Interaction
|
|
349
|
+
# ------------------------------------------------------------------ #
|
|
350
|
+
def _cell_at(self, pos):
|
|
351
|
+
for (r, c), rect in self._rects.items():
|
|
352
|
+
if rect.contains(pos):
|
|
353
|
+
return r, c
|
|
354
|
+
return -1, -1
|
|
355
|
+
|
|
356
|
+
def mouseMoveEvent(self, e):
|
|
357
|
+
pos = e.position() if hasattr(e, "position") else QPointF(e.pos())
|
|
358
|
+
rc = self._cell_at(pos)
|
|
359
|
+
if rc != self._hover:
|
|
360
|
+
self._hover = rc
|
|
361
|
+
r, c = rc
|
|
362
|
+
if r >= 0:
|
|
363
|
+
_, _, get = self._matrix()
|
|
364
|
+
v = get(r, c)
|
|
365
|
+
if v is not None:
|
|
366
|
+
rl = self._row_labels[r] if r < len(self._row_labels) else "%d" % r
|
|
367
|
+
cl = self._col_labels[c] if c < len(self._col_labels) else "%d" % c
|
|
368
|
+
self.setToolTip("%s · %s: %g" % (cl, rl, v))
|
|
369
|
+
else:
|
|
370
|
+
self.setToolTip("")
|
|
371
|
+
else:
|
|
372
|
+
self.setToolTip("")
|
|
373
|
+
self.update()
|
|
374
|
+
super().mouseMoveEvent(e)
|
|
375
|
+
|
|
376
|
+
def leaveEvent(self, e):
|
|
377
|
+
if self._hover != (-1, -1):
|
|
378
|
+
self._hover = (-1, -1)
|
|
379
|
+
self.update()
|
|
380
|
+
super().leaveEvent(e)
|
|
381
|
+
|
|
382
|
+
def mousePressEvent(self, e):
|
|
383
|
+
pos = e.position() if hasattr(e, "position") else QPointF(e.pos())
|
|
384
|
+
r, c = self._cell_at(pos)
|
|
385
|
+
if r >= 0:
|
|
386
|
+
_, _, get = self._matrix()
|
|
387
|
+
v = get(r, c)
|
|
388
|
+
self.cellClicked.emit(r, c, float(v) if v is not None else float("nan"))
|
|
389
|
+
super().mousePressEvent(e)
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------ #
|
|
392
|
+
## Designer properties
|
|
393
|
+
# ------------------------------------------------------------------ #
|
|
394
|
+
@Property(str)
|
|
395
|
+
def mode(self):
|
|
396
|
+
return self._mode
|
|
397
|
+
|
|
398
|
+
@mode.setter
|
|
399
|
+
def mode(self, m):
|
|
400
|
+
self.setMode(m)
|
|
401
|
+
|
|
402
|
+
@Property(str)
|
|
403
|
+
def valuesCsv(self):
|
|
404
|
+
rows = self._values if self._values and isinstance(self._values[0], (list, tuple)) \
|
|
405
|
+
else [self._values]
|
|
406
|
+
return ";".join(",".join("" if v is None else "%g" % v for v in row) for row in rows)
|
|
407
|
+
|
|
408
|
+
@valuesCsv.setter
|
|
409
|
+
def valuesCsv(self, text):
|
|
410
|
+
out = []
|
|
411
|
+
for rowtok in str(text).split(";"):
|
|
412
|
+
rowtok = rowtok.strip()
|
|
413
|
+
if not rowtok:
|
|
414
|
+
continue
|
|
415
|
+
row = []
|
|
416
|
+
for tok in rowtok.split(","):
|
|
417
|
+
tok = tok.strip()
|
|
418
|
+
row.append(None if tok == "" else float(tok) if _isnum(tok) else None)
|
|
419
|
+
out.append(row)
|
|
420
|
+
if out:
|
|
421
|
+
self.setValues(out)
|
|
422
|
+
|
|
423
|
+
@Property(str)
|
|
424
|
+
def rowLabelsCsv(self):
|
|
425
|
+
return ",".join(self._row_labels)
|
|
426
|
+
|
|
427
|
+
@rowLabelsCsv.setter
|
|
428
|
+
def rowLabelsCsv(self, text):
|
|
429
|
+
self.setLabels(row_labels=[s.strip() for s in str(text).split(",") if s.strip()])
|
|
430
|
+
|
|
431
|
+
@Property(str)
|
|
432
|
+
def colLabelsCsv(self):
|
|
433
|
+
return ",".join(self._col_labels)
|
|
434
|
+
|
|
435
|
+
@colLabelsCsv.setter
|
|
436
|
+
def colLabelsCsv(self, text):
|
|
437
|
+
self.setLabels(col_labels=[s.strip() for s in str(text).split(",") if s.strip()])
|
|
438
|
+
|
|
439
|
+
@Property(QColor)
|
|
440
|
+
def lowColor(self):
|
|
441
|
+
return self._low
|
|
442
|
+
|
|
443
|
+
@lowColor.setter
|
|
444
|
+
def lowColor(self, c):
|
|
445
|
+
self._low = QColor(c)
|
|
446
|
+
self.update()
|
|
447
|
+
|
|
448
|
+
@Property(QColor)
|
|
449
|
+
def highColor(self):
|
|
450
|
+
return self._high
|
|
451
|
+
|
|
452
|
+
@highColor.setter
|
|
453
|
+
def highColor(self, c):
|
|
454
|
+
self._high = QColor(c)
|
|
455
|
+
self.update()
|
|
456
|
+
|
|
457
|
+
@Property(QColor)
|
|
458
|
+
def emptyColor(self):
|
|
459
|
+
return self._empty
|
|
460
|
+
|
|
461
|
+
@emptyColor.setter
|
|
462
|
+
def emptyColor(self, c):
|
|
463
|
+
self._empty = QColor(c)
|
|
464
|
+
self.update()
|
|
465
|
+
|
|
466
|
+
@Property(int)
|
|
467
|
+
def cellSize(self):
|
|
468
|
+
return self._cell_size
|
|
469
|
+
|
|
470
|
+
@cellSize.setter
|
|
471
|
+
def cellSize(self, v):
|
|
472
|
+
self._cell_size = max(0, int(v))
|
|
473
|
+
self.update()
|
|
474
|
+
|
|
475
|
+
@Property(int)
|
|
476
|
+
def cellGap(self):
|
|
477
|
+
return self._cell_gap
|
|
478
|
+
|
|
479
|
+
@cellGap.setter
|
|
480
|
+
def cellGap(self, v):
|
|
481
|
+
self._cell_gap = max(0, int(v))
|
|
482
|
+
self.update()
|
|
483
|
+
|
|
484
|
+
@Property(int)
|
|
485
|
+
def cornerRadius(self):
|
|
486
|
+
return self._radius
|
|
487
|
+
|
|
488
|
+
@cornerRadius.setter
|
|
489
|
+
def cornerRadius(self, v):
|
|
490
|
+
self._radius = max(0, int(v))
|
|
491
|
+
self.update()
|
|
492
|
+
|
|
493
|
+
@Property(bool)
|
|
494
|
+
def showLabels(self):
|
|
495
|
+
return self._show_labels
|
|
496
|
+
|
|
497
|
+
@showLabels.setter
|
|
498
|
+
def showLabels(self, v):
|
|
499
|
+
self._show_labels = bool(v)
|
|
500
|
+
self.update()
|
|
501
|
+
|
|
502
|
+
@Property(bool)
|
|
503
|
+
def showLegend(self):
|
|
504
|
+
return self._show_legend
|
|
505
|
+
|
|
506
|
+
@showLegend.setter
|
|
507
|
+
def showLegend(self, v):
|
|
508
|
+
self._show_legend = bool(v)
|
|
509
|
+
self.update()
|
|
510
|
+
|
|
511
|
+
@Property(QColor)
|
|
512
|
+
def labelColor(self):
|
|
513
|
+
return self._label_color
|
|
514
|
+
|
|
515
|
+
@labelColor.setter
|
|
516
|
+
def labelColor(self, c):
|
|
517
|
+
self._label_color = QColor(c)
|
|
518
|
+
self.update()
|
|
519
|
+
|
|
520
|
+
@Property(bool)
|
|
521
|
+
def autoNormalize(self):
|
|
522
|
+
return self._auto
|
|
523
|
+
|
|
524
|
+
@autoNormalize.setter
|
|
525
|
+
def autoNormalize(self, v):
|
|
526
|
+
self._auto = bool(v)
|
|
527
|
+
self.update()
|
|
528
|
+
|
|
529
|
+
@Property(float)
|
|
530
|
+
def minValue(self):
|
|
531
|
+
return self._min
|
|
532
|
+
|
|
533
|
+
@minValue.setter
|
|
534
|
+
def minValue(self, v):
|
|
535
|
+
self._min = float(v)
|
|
536
|
+
self._auto = False
|
|
537
|
+
self.update()
|
|
538
|
+
|
|
539
|
+
@Property(float)
|
|
540
|
+
def maxValue(self):
|
|
541
|
+
return self._max
|
|
542
|
+
|
|
543
|
+
@maxValue.setter
|
|
544
|
+
def maxValue(self, v):
|
|
545
|
+
self._max = float(v)
|
|
546
|
+
self._auto = False
|
|
547
|
+
self.update()
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _isnum(s):
|
|
551
|
+
try:
|
|
552
|
+
float(s)
|
|
553
|
+
return True
|
|
554
|
+
except ValueError:
|
|
555
|
+
return False
|