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,623 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomScatterChart - a painted x/y scatter plot.
|
|
9
|
+
##
|
|
10
|
+
## Points in a cartesian plane, one colour per series, optionally sized by a
|
|
11
|
+
## third value (a bubble plot). The most conspicuous absence for a general
|
|
12
|
+
## chart library: nothing in the catalog plotted two continuous variables
|
|
13
|
+
## against each other.
|
|
14
|
+
##
|
|
15
|
+
## QPainter only, NO QtCharts - Qt Charts is GPLv3-or-commercial with no LGPL
|
|
16
|
+
## option, so anything built on it cannot ship in a proprietary wheel (see
|
|
17
|
+
## docs/design/mui-charts-gap.md).
|
|
18
|
+
##
|
|
19
|
+
## Axis ticks come from _chart_axis, shared with the other cartesian charts so
|
|
20
|
+
## they agree about where round numbers fall.
|
|
21
|
+
##
|
|
22
|
+
## Data goes in with addSeries(name, points) in code, or pointsCsv in Qt
|
|
23
|
+
## Designer:
|
|
24
|
+
##
|
|
25
|
+
## pointsCsv = "Alpha=1,2;2,4;3,9|Beta=1,5;2,3"
|
|
26
|
+
## name x,y pairs separated by ";", series by "|"
|
|
27
|
+
##
|
|
28
|
+
## Emits pointHovered(int, int) and pointClicked(int, int) - series index and
|
|
29
|
+
## point index, or (-1, -1) for nothing.
|
|
30
|
+
########################################################################
|
|
31
|
+
from qtpy.QtCore import Qt, Signal, Property, QRectF, QSize, QPointF
|
|
32
|
+
from qtpy.QtGui import QColor, QPainter, QPen, QBrush, QFontMetrics
|
|
33
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
34
|
+
|
|
35
|
+
# _chart_axis stays in the free package; Pro depends on it, so import
|
|
36
|
+
# it absolutely rather than duplicating the helper here.
|
|
37
|
+
from Custom_Widgets.widgets.charts._chart_axis import formatTick, niceTicks, tickValues
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QCustomScatterChart(QWidget):
|
|
41
|
+
pointHovered = Signal(int, int)
|
|
42
|
+
pointClicked = Signal(int, int)
|
|
43
|
+
|
|
44
|
+
WIDGET_ICON = "components/icons/scatter_plot.png"
|
|
45
|
+
WIDGET_TOOLTIP = "A painted x/y scatter plot"
|
|
46
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomScatterChart"
|
|
47
|
+
WIDGET_DOM_XML = """
|
|
48
|
+
<ui language='c++'>
|
|
49
|
+
<widget class='QCustomScatterChart' name='customScatterChart'>
|
|
50
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>420</width><height>300</height></rect></property>
|
|
51
|
+
<property name='pointsCsv'><string>Alpha=1,2;2,4;3,9;4,7</string></property>
|
|
52
|
+
</widget>
|
|
53
|
+
</ui>
|
|
54
|
+
"""
|
|
55
|
+
__catalog__ = {
|
|
56
|
+
"name": "QCustomScatterChart",
|
|
57
|
+
"props": {"pointsCsv": {"type": "string", "default": ""},
|
|
58
|
+
"seriesColorsCsv": {"type": "string", "default": ""},
|
|
59
|
+
"xAxisTitle": {"type": "string", "default": ""},
|
|
60
|
+
"yAxisTitle": {"type": "string", "default": ""},
|
|
61
|
+
"markerSize": {"type": "float", "default": 7.0},
|
|
62
|
+
"markerShape": {"type": "enum",
|
|
63
|
+
"values": ["circle", "square", "diamond", "triangle"],
|
|
64
|
+
"default": "circle"},
|
|
65
|
+
"markerOpacity": {"type": "float", "default": 0.85},
|
|
66
|
+
"tickCount": {"type": "int", "default": 5},
|
|
67
|
+
"showGrid": {"type": "bool", "default": True},
|
|
68
|
+
"showAxes": {"type": "bool", "default": True},
|
|
69
|
+
"showLegend": {"type": "bool", "default": True},
|
|
70
|
+
"showTooltip": {"type": "bool", "default": True},
|
|
71
|
+
"gridColor": {"type": "color", "default": "#e2e8f0"},
|
|
72
|
+
"axisColor": {"type": "color", "default": "#64748b"},
|
|
73
|
+
"labelColor": {"type": "color", "default": "#64748b"}},
|
|
74
|
+
"signals": ["pointHovered", "pointClicked"],
|
|
75
|
+
"tokens_used": ["on-surface", "on-surface-muted", "surface-muted"],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Rich editors for the Designer "Custom Properties" dock (see
|
|
79
|
+
# DesignerTools.CustomPropertiesDock).
|
|
80
|
+
DESIGNER_CUSTOM_PROPS = [
|
|
81
|
+
{"name": "pointsCsv", "kind": "str", "group": "General"},
|
|
82
|
+
{"name": "seriesColorsCsv", "kind": "str", "group": "Colors"},
|
|
83
|
+
{"name": "xAxisTitle", "kind": "str", "group": "General"},
|
|
84
|
+
{"name": "yAxisTitle", "kind": "str", "group": "General"},
|
|
85
|
+
{"name": "markerSize", "kind": "float", "group": "General"},
|
|
86
|
+
{"name": "markerShape", "kind": "choice", "group": "General"},
|
|
87
|
+
{"name": "markerOpacity", "kind": "float", "group": "General"},
|
|
88
|
+
{"name": "tickCount", "kind": "int", "group": "General"},
|
|
89
|
+
{"name": "showGrid", "kind": "bool", "group": "General"},
|
|
90
|
+
{"name": "showAxes", "kind": "bool", "group": "General"},
|
|
91
|
+
{"name": "showLegend", "kind": "bool", "group": "General"},
|
|
92
|
+
{"name": "showTooltip", "kind": "bool", "group": "General"},
|
|
93
|
+
{"name": "gridColor", "kind": "color", "group": "Colors"},
|
|
94
|
+
{"name": "axisColor", "kind": "color", "group": "Colors"},
|
|
95
|
+
{"name": "labelColor", "kind": "color", "group": "Colors"},
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
_DEFAULT_COLORS = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed",
|
|
99
|
+
"#0891b2"]
|
|
100
|
+
_PAD = 10.0
|
|
101
|
+
|
|
102
|
+
def __init__(self, parent=None, series=None):
|
|
103
|
+
super().__init__(parent)
|
|
104
|
+
self.setObjectName("QCustomScatterChart")
|
|
105
|
+
self._series = [] # list of (name, [(x, y, size|None), ...])
|
|
106
|
+
self._colors = []
|
|
107
|
+
self._xTitle = ""
|
|
108
|
+
self._yTitle = ""
|
|
109
|
+
self._markerSize = 7.0
|
|
110
|
+
self._markerShape = "circle"
|
|
111
|
+
self._markerOpacity = 0.85
|
|
112
|
+
self._tickCount = 5
|
|
113
|
+
self._showGrid = True
|
|
114
|
+
self._showAxes = True
|
|
115
|
+
self._showLegend = True
|
|
116
|
+
self._showTooltip = True
|
|
117
|
+
self._hover = (-1, -1)
|
|
118
|
+
|
|
119
|
+
self._gridColor = QColor("#e2e8f0")
|
|
120
|
+
# Was #cbd5e1 -- the light "outline" value, 1.48:1 on a white plot, so
|
|
121
|
+
# an untokenised chart had no visible axes. This is light-theme
|
|
122
|
+
# on-surface-muted, matching what the token QSS sets. The grid above
|
|
123
|
+
# stays faint on purpose: it sits behind the data, the axes frame it.
|
|
124
|
+
self._axisColor = QColor("#64748b")
|
|
125
|
+
self._labelColor = QColor("#64748b")
|
|
126
|
+
self._tooltipBg = QColor("#0f172a")
|
|
127
|
+
self._tooltipText = QColor("#f8fafc")
|
|
128
|
+
|
|
129
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
130
|
+
self.setMouseTracking(True)
|
|
131
|
+
for name, points in (series or []):
|
|
132
|
+
self.addSeries(name, points)
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------ #
|
|
135
|
+
## Data
|
|
136
|
+
# ------------------------------------------------------------------ #
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _coercePoint(point):
|
|
139
|
+
"""(x, y) or (x, y, size); returns (x, y, size|None) or None."""
|
|
140
|
+
if isinstance(point, dict):
|
|
141
|
+
values = (point.get("x"), point.get("y"), point.get("size"))
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
values = tuple(point)
|
|
145
|
+
except TypeError:
|
|
146
|
+
return None
|
|
147
|
+
if len(values) < 2:
|
|
148
|
+
return None
|
|
149
|
+
try:
|
|
150
|
+
x, y = float(values[0]), float(values[1])
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
return None
|
|
153
|
+
size = None
|
|
154
|
+
if len(values) > 2 and values[2] is not None:
|
|
155
|
+
try:
|
|
156
|
+
size = max(0.0, float(values[2]))
|
|
157
|
+
except (TypeError, ValueError):
|
|
158
|
+
size = None
|
|
159
|
+
return (x, y, size)
|
|
160
|
+
|
|
161
|
+
def addSeries(self, name, points, color=None):
|
|
162
|
+
cleaned = [p for p in (self._coercePoint(pt) for pt in (points or [])) if p]
|
|
163
|
+
self._series.append((str(name), cleaned))
|
|
164
|
+
if color is not None:
|
|
165
|
+
while len(self._colors) < len(self._series) - 1:
|
|
166
|
+
self._colors.append(None)
|
|
167
|
+
self._colors.append(QColor(color))
|
|
168
|
+
self.update()
|
|
169
|
+
return len(self._series) - 1
|
|
170
|
+
|
|
171
|
+
def setSeries(self, series):
|
|
172
|
+
self._series = []
|
|
173
|
+
for item in series or []:
|
|
174
|
+
if isinstance(item, dict):
|
|
175
|
+
self.addSeries(item.get("name", ""), item.get("points", []))
|
|
176
|
+
elif isinstance(item, (tuple, list)) and len(item) >= 2:
|
|
177
|
+
self.addSeries(item[0], item[1])
|
|
178
|
+
self.update()
|
|
179
|
+
|
|
180
|
+
def series(self):
|
|
181
|
+
return [(name, list(points)) for name, points in self._series]
|
|
182
|
+
|
|
183
|
+
def seriesCount(self):
|
|
184
|
+
return len(self._series)
|
|
185
|
+
|
|
186
|
+
def pointCount(self):
|
|
187
|
+
return sum(len(points) for _name, points in self._series)
|
|
188
|
+
|
|
189
|
+
def clearSeries(self):
|
|
190
|
+
self._series = []
|
|
191
|
+
self._colors = []
|
|
192
|
+
self._hover = (-1, -1)
|
|
193
|
+
self.update()
|
|
194
|
+
|
|
195
|
+
def removeSeries(self, index):
|
|
196
|
+
if 0 <= index < len(self._series):
|
|
197
|
+
del self._series[index]
|
|
198
|
+
if index < len(self._colors):
|
|
199
|
+
del self._colors[index]
|
|
200
|
+
self._hover = (-1, -1)
|
|
201
|
+
self.update()
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def seriesColor(self, index):
|
|
206
|
+
if 0 <= index < len(self._colors) and self._colors[index] is not None:
|
|
207
|
+
return QColor(self._colors[index])
|
|
208
|
+
return QColor(self._DEFAULT_COLORS[index % len(self._DEFAULT_COLORS)])
|
|
209
|
+
|
|
210
|
+
def dataBounds(self):
|
|
211
|
+
"""(xmin, xmax, ymin, ymax) across every series."""
|
|
212
|
+
xs = [p[0] for _n, pts in self._series for p in pts]
|
|
213
|
+
ys = [p[1] for _n, pts in self._series for p in pts]
|
|
214
|
+
if not xs:
|
|
215
|
+
return (0.0, 1.0, 0.0, 1.0)
|
|
216
|
+
return (min(xs), max(xs), min(ys), max(ys))
|
|
217
|
+
|
|
218
|
+
def xRange(self):
|
|
219
|
+
xmin, xmax, _ymin, _ymax = self.dataBounds()
|
|
220
|
+
start, stop, _step = niceTicks(xmin, xmax, self._tickCount)
|
|
221
|
+
return (start, stop)
|
|
222
|
+
|
|
223
|
+
def yRange(self):
|
|
224
|
+
_xmin, _xmax, ymin, ymax = self.dataBounds()
|
|
225
|
+
start, stop, _step = niceTicks(ymin, ymax, self._tickCount)
|
|
226
|
+
return (start, stop)
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------ #
|
|
229
|
+
## Geometry
|
|
230
|
+
# ------------------------------------------------------------------ #
|
|
231
|
+
def sizeHint(self):
|
|
232
|
+
return QSize(420, 300)
|
|
233
|
+
|
|
234
|
+
def minimumSizeHint(self):
|
|
235
|
+
return QSize(140, 110)
|
|
236
|
+
|
|
237
|
+
def _legendHeight(self):
|
|
238
|
+
if not (self._showLegend and self._series):
|
|
239
|
+
return 0.0
|
|
240
|
+
return QFontMetrics(self.font()).height() + 8
|
|
241
|
+
|
|
242
|
+
def _plotRect(self):
|
|
243
|
+
fm = QFontMetrics(self.font())
|
|
244
|
+
left = self._PAD
|
|
245
|
+
bottom = self._PAD + self._legendHeight()
|
|
246
|
+
if self._showAxes:
|
|
247
|
+
ylo, yhi = self.yRange()
|
|
248
|
+
_s, _e, step = niceTicks(*self.dataBounds()[2:], count=self._tickCount)
|
|
249
|
+
widest = max(fm.horizontalAdvance(formatTick(v, step))
|
|
250
|
+
for v in (ylo, yhi))
|
|
251
|
+
left += widest + 8
|
|
252
|
+
bottom += fm.height() + 4
|
|
253
|
+
if self._yTitle:
|
|
254
|
+
left += fm.height() + 2
|
|
255
|
+
if self._xTitle:
|
|
256
|
+
bottom += fm.height() + 2
|
|
257
|
+
return QRectF(left, self._PAD,
|
|
258
|
+
max(1.0, self.width() - left - self._PAD),
|
|
259
|
+
max(1.0, self.height() - self._PAD - bottom))
|
|
260
|
+
|
|
261
|
+
def mapPoint(self, x, y):
|
|
262
|
+
"""Data coordinates -> widget coordinates."""
|
|
263
|
+
rect = self._plotRect()
|
|
264
|
+
xlo, xhi = self.xRange()
|
|
265
|
+
ylo, yhi = self.yRange()
|
|
266
|
+
xspan = (xhi - xlo) or 1.0
|
|
267
|
+
yspan = (yhi - ylo) or 1.0
|
|
268
|
+
return QPointF(rect.left() + (x - xlo) / xspan * rect.width(),
|
|
269
|
+
rect.bottom() - (y - ylo) / yspan * rect.height())
|
|
270
|
+
|
|
271
|
+
# ------------------------------------------------------------------ #
|
|
272
|
+
## Painting
|
|
273
|
+
# ------------------------------------------------------------------ #
|
|
274
|
+
def paintEvent(self, e):
|
|
275
|
+
p = QPainter(self)
|
|
276
|
+
p.setRenderHint(QPainter.Antialiasing)
|
|
277
|
+
p.setFont(self.font())
|
|
278
|
+
rect = self._plotRect()
|
|
279
|
+
|
|
280
|
+
if self._showGrid or self._showAxes:
|
|
281
|
+
self._paintGridAndTicks(p, rect)
|
|
282
|
+
if self._showAxes:
|
|
283
|
+
p.setPen(QPen(self._axisColor, 1))
|
|
284
|
+
p.drawLine(rect.bottomLeft(), rect.bottomRight())
|
|
285
|
+
p.drawLine(rect.topLeft(), rect.bottomLeft())
|
|
286
|
+
self._paintTitles(p, rect)
|
|
287
|
+
|
|
288
|
+
for si, (_name, points) in enumerate(self._series):
|
|
289
|
+
self._paintSeries(p, si, points)
|
|
290
|
+
|
|
291
|
+
if self._showLegend and self._series:
|
|
292
|
+
self._paintLegend(p)
|
|
293
|
+
if self._showTooltip and self._hover != (-1, -1):
|
|
294
|
+
self._paintTooltip(p, rect)
|
|
295
|
+
|
|
296
|
+
def _paintGridAndTicks(self, p, rect):
|
|
297
|
+
fm = QFontMetrics(self.font())
|
|
298
|
+
xmin, xmax, ymin, ymax = self.dataBounds()
|
|
299
|
+
_xs, _xe, xstep = niceTicks(xmin, xmax, self._tickCount)
|
|
300
|
+
_ys, _ye, ystep = niceTicks(ymin, ymax, self._tickCount)
|
|
301
|
+
|
|
302
|
+
for value in tickValues(ymin, ymax, self._tickCount):
|
|
303
|
+
point = self.mapPoint(self.xRange()[0], value)
|
|
304
|
+
if self._showGrid:
|
|
305
|
+
p.setPen(QPen(self._gridColor, 1))
|
|
306
|
+
p.drawLine(QPointF(rect.left(), point.y()),
|
|
307
|
+
QPointF(rect.right(), point.y()))
|
|
308
|
+
if self._showAxes:
|
|
309
|
+
p.setPen(QPen(self._labelColor))
|
|
310
|
+
text = formatTick(value, ystep)
|
|
311
|
+
p.drawText(QRectF(0, point.y() - fm.height() / 2.0,
|
|
312
|
+
rect.left() - 6, fm.height()),
|
|
313
|
+
int(Qt.AlignRight | Qt.AlignVCenter), text)
|
|
314
|
+
|
|
315
|
+
for value in tickValues(xmin, xmax, self._tickCount):
|
|
316
|
+
point = self.mapPoint(value, self.yRange()[0])
|
|
317
|
+
if self._showGrid:
|
|
318
|
+
p.setPen(QPen(self._gridColor, 1))
|
|
319
|
+
p.drawLine(QPointF(point.x(), rect.top()),
|
|
320
|
+
QPointF(point.x(), rect.bottom()))
|
|
321
|
+
if self._showAxes:
|
|
322
|
+
p.setPen(QPen(self._labelColor))
|
|
323
|
+
text = formatTick(value, xstep)
|
|
324
|
+
width = fm.horizontalAdvance(text) + 8
|
|
325
|
+
p.drawText(QRectF(point.x() - width / 2.0, rect.bottom() + 3,
|
|
326
|
+
width, fm.height()),
|
|
327
|
+
int(Qt.AlignHCenter | Qt.AlignVCenter), text)
|
|
328
|
+
|
|
329
|
+
def _paintTitles(self, p, rect):
|
|
330
|
+
fm = QFontMetrics(self.font())
|
|
331
|
+
p.setPen(QPen(self._labelColor))
|
|
332
|
+
if self._xTitle:
|
|
333
|
+
p.drawText(QRectF(rect.left(),
|
|
334
|
+
rect.bottom() + fm.height() + 5,
|
|
335
|
+
rect.width(), fm.height()),
|
|
336
|
+
int(Qt.AlignHCenter | Qt.AlignVCenter), self._xTitle)
|
|
337
|
+
if self._yTitle:
|
|
338
|
+
p.save()
|
|
339
|
+
p.translate(fm.height(), rect.center().y())
|
|
340
|
+
p.rotate(-90)
|
|
341
|
+
p.drawText(QRectF(-rect.height() / 2.0, -fm.height() / 2.0,
|
|
342
|
+
rect.height(), fm.height()),
|
|
343
|
+
int(Qt.AlignCenter), self._yTitle)
|
|
344
|
+
p.restore()
|
|
345
|
+
|
|
346
|
+
def _markerPath(self, p, centre, radius):
|
|
347
|
+
shape = self._markerShape
|
|
348
|
+
if shape == "square":
|
|
349
|
+
p.drawRect(QRectF(centre.x() - radius, centre.y() - radius,
|
|
350
|
+
radius * 2, radius * 2))
|
|
351
|
+
elif shape == "diamond":
|
|
352
|
+
from qtpy.QtGui import QPolygonF
|
|
353
|
+
p.drawPolygon(QPolygonF([
|
|
354
|
+
QPointF(centre.x(), centre.y() - radius),
|
|
355
|
+
QPointF(centre.x() + radius, centre.y()),
|
|
356
|
+
QPointF(centre.x(), centre.y() + radius),
|
|
357
|
+
QPointF(centre.x() - radius, centre.y())]))
|
|
358
|
+
elif shape == "triangle":
|
|
359
|
+
from qtpy.QtGui import QPolygonF
|
|
360
|
+
p.drawPolygon(QPolygonF([
|
|
361
|
+
QPointF(centre.x(), centre.y() - radius),
|
|
362
|
+
QPointF(centre.x() + radius, centre.y() + radius),
|
|
363
|
+
QPointF(centre.x() - radius, centre.y() + radius)]))
|
|
364
|
+
else:
|
|
365
|
+
p.drawEllipse(centre, radius, radius)
|
|
366
|
+
|
|
367
|
+
def _paintSeries(self, p, index, points):
|
|
368
|
+
colour = QColor(self.seriesColor(index))
|
|
369
|
+
colour.setAlphaF(max(0.0, min(1.0, self._markerOpacity)))
|
|
370
|
+
p.setPen(QPen(self.seriesColor(index), 1))
|
|
371
|
+
p.setBrush(QBrush(colour))
|
|
372
|
+
for pi, (x, y, size) in enumerate(points):
|
|
373
|
+
radius = (size if size is not None else self._markerSize) / 2.0
|
|
374
|
+
if (index, pi) == self._hover:
|
|
375
|
+
radius += 2.0
|
|
376
|
+
self._markerPath(p, self.mapPoint(x, y), max(1.0, radius))
|
|
377
|
+
|
|
378
|
+
def _paintLegend(self, p):
|
|
379
|
+
fm = QFontMetrics(self.font())
|
|
380
|
+
swatch, gap = 9.0, 14.0
|
|
381
|
+
widths = [swatch + 5 + fm.horizontalAdvance(name)
|
|
382
|
+
for name, _pts in self._series]
|
|
383
|
+
x = (self.width() - (sum(widths) + gap * (len(widths) - 1))) / 2.0
|
|
384
|
+
y = self.height() - self._legendHeight() + 2
|
|
385
|
+
for index, (name, _points) in enumerate(self._series):
|
|
386
|
+
p.setPen(Qt.NoPen)
|
|
387
|
+
p.setBrush(QBrush(self.seriesColor(index)))
|
|
388
|
+
p.drawEllipse(QRectF(x, y + (fm.height() - swatch) / 2.0,
|
|
389
|
+
swatch, swatch))
|
|
390
|
+
p.setPen(QPen(self._labelColor))
|
|
391
|
+
p.drawText(QRectF(x + swatch + 5, y, widths[index], fm.height()),
|
|
392
|
+
int(Qt.AlignLeft | Qt.AlignVCenter), name)
|
|
393
|
+
x += widths[index] + gap
|
|
394
|
+
|
|
395
|
+
def _paintTooltip(self, p, rect):
|
|
396
|
+
si, pi = self._hover
|
|
397
|
+
name, points = self._series[si]
|
|
398
|
+
x, y, _size = points[pi]
|
|
399
|
+
rows = [name, "x %g" % x, "y %g" % y]
|
|
400
|
+
fm = QFontMetrics(self.font())
|
|
401
|
+
width = max(fm.horizontalAdvance(r) for r in rows) + 16
|
|
402
|
+
height = fm.height() * len(rows) + 10
|
|
403
|
+
anchor = self.mapPoint(x, y)
|
|
404
|
+
px = anchor.x() + 12
|
|
405
|
+
if px + width > rect.right():
|
|
406
|
+
px = anchor.x() - 12 - width
|
|
407
|
+
px = max(rect.left(), min(px, rect.right() - width))
|
|
408
|
+
py = max(rect.top(), min(anchor.y() - height / 2.0, rect.bottom() - height))
|
|
409
|
+
|
|
410
|
+
p.setPen(Qt.NoPen)
|
|
411
|
+
p.setBrush(QBrush(QColor(self._tooltipBg.red(), self._tooltipBg.green(),
|
|
412
|
+
self._tooltipBg.blue(), 235)))
|
|
413
|
+
p.drawRoundedRect(QRectF(px, py, width, height), 6, 6)
|
|
414
|
+
p.setPen(QPen(self._tooltipText))
|
|
415
|
+
for i, text in enumerate(rows):
|
|
416
|
+
p.drawText(QRectF(px + 8, py + 5 + i * fm.height(),
|
|
417
|
+
width - 16, fm.height()),
|
|
418
|
+
int(Qt.AlignLeft | Qt.AlignVCenter), text)
|
|
419
|
+
|
|
420
|
+
# ------------------------------------------------------------------ #
|
|
421
|
+
## Interaction
|
|
422
|
+
# ------------------------------------------------------------------ #
|
|
423
|
+
def pointAt(self, pos, tolerance=None):
|
|
424
|
+
"""(series, point) nearest a position within tolerance, else (-1, -1).
|
|
425
|
+
|
|
426
|
+
Later series win ties so hit-testing agrees with what is painted on top.
|
|
427
|
+
"""
|
|
428
|
+
point = QPointF(pos)
|
|
429
|
+
limit = tolerance if tolerance is not None else max(6.0, self._markerSize)
|
|
430
|
+
best, bestDistance = (-1, -1), None
|
|
431
|
+
for si in range(len(self._series) - 1, -1, -1):
|
|
432
|
+
for pi, (x, y, _size) in enumerate(self._series[si][1]):
|
|
433
|
+
mapped = self.mapPoint(x, y)
|
|
434
|
+
dx, dy = point.x() - mapped.x(), point.y() - mapped.y()
|
|
435
|
+
distance = (dx * dx + dy * dy) ** 0.5
|
|
436
|
+
if distance <= limit and (bestDistance is None or distance < bestDistance):
|
|
437
|
+
best, bestDistance = (si, pi), distance
|
|
438
|
+
return best
|
|
439
|
+
|
|
440
|
+
def mouseMoveEvent(self, e):
|
|
441
|
+
hit = self.pointAt(e.pos())
|
|
442
|
+
if hit != self._hover:
|
|
443
|
+
self._hover = hit
|
|
444
|
+
self.pointHovered.emit(hit[0], hit[1])
|
|
445
|
+
self.update()
|
|
446
|
+
super().mouseMoveEvent(e)
|
|
447
|
+
|
|
448
|
+
def leaveEvent(self, e):
|
|
449
|
+
if self._hover != (-1, -1):
|
|
450
|
+
self._hover = (-1, -1)
|
|
451
|
+
self.pointHovered.emit(-1, -1)
|
|
452
|
+
self.update()
|
|
453
|
+
super().leaveEvent(e)
|
|
454
|
+
|
|
455
|
+
def mouseReleaseEvent(self, e):
|
|
456
|
+
if e.button() == Qt.LeftButton:
|
|
457
|
+
si, pi = self.pointAt(e.pos())
|
|
458
|
+
if si >= 0:
|
|
459
|
+
self.pointClicked.emit(si, pi)
|
|
460
|
+
super().mouseReleaseEvent(e)
|
|
461
|
+
|
|
462
|
+
# ------------------------------------------------------------------ #
|
|
463
|
+
## Designer properties
|
|
464
|
+
# ------------------------------------------------------------------ #
|
|
465
|
+
@Property(str)
|
|
466
|
+
def pointsCsv(self):
|
|
467
|
+
chunks = []
|
|
468
|
+
for name, points in self._series:
|
|
469
|
+
pairs = ";".join("%g,%g" % (x, y) if size is None
|
|
470
|
+
else "%g,%g,%g" % (x, y, size)
|
|
471
|
+
for x, y, size in points)
|
|
472
|
+
chunks.append("%s=%s" % (name, pairs))
|
|
473
|
+
return "|".join(chunks)
|
|
474
|
+
|
|
475
|
+
@pointsCsv.setter
|
|
476
|
+
def pointsCsv(self, text):
|
|
477
|
+
series = []
|
|
478
|
+
for index, chunk in enumerate(str(text).split("|")):
|
|
479
|
+
chunk = chunk.strip()
|
|
480
|
+
if not chunk:
|
|
481
|
+
continue
|
|
482
|
+
if "=" in chunk:
|
|
483
|
+
name, _, raw = chunk.partition("=")
|
|
484
|
+
name = name.strip() or "Series %d" % (len(series) + 1)
|
|
485
|
+
else:
|
|
486
|
+
name, raw = "Series %d" % (len(series) + 1), chunk
|
|
487
|
+
points = []
|
|
488
|
+
for pair in raw.split(";"):
|
|
489
|
+
pair = pair.strip()
|
|
490
|
+
if not pair:
|
|
491
|
+
continue
|
|
492
|
+
parts = [t.strip() for t in pair.split(",") if t.strip()]
|
|
493
|
+
coerced = self._coercePoint(parts) if len(parts) >= 2 else None
|
|
494
|
+
if coerced:
|
|
495
|
+
points.append(coerced)
|
|
496
|
+
if points:
|
|
497
|
+
series.append((name, points))
|
|
498
|
+
self._series = []
|
|
499
|
+
for name, points in series:
|
|
500
|
+
self.addSeries(name, points)
|
|
501
|
+
self.update()
|
|
502
|
+
|
|
503
|
+
@Property(str)
|
|
504
|
+
def seriesColorsCsv(self):
|
|
505
|
+
return ",".join(c.name() if c is not None else "" for c in self._colors)
|
|
506
|
+
|
|
507
|
+
@seriesColorsCsv.setter
|
|
508
|
+
def seriesColorsCsv(self, text):
|
|
509
|
+
colors = []
|
|
510
|
+
for token in str(text).replace(";", ",").split(","):
|
|
511
|
+
token = token.strip()
|
|
512
|
+
colour = QColor(token) if token else QColor()
|
|
513
|
+
colors.append(colour if colour.isValid() else None)
|
|
514
|
+
self._colors = colors
|
|
515
|
+
self.update()
|
|
516
|
+
|
|
517
|
+
@Property(str)
|
|
518
|
+
def xAxisTitle(self):
|
|
519
|
+
return self._xTitle
|
|
520
|
+
|
|
521
|
+
@xAxisTitle.setter
|
|
522
|
+
def xAxisTitle(self, text):
|
|
523
|
+
self._xTitle = str(text); self.update()
|
|
524
|
+
|
|
525
|
+
@Property(str)
|
|
526
|
+
def yAxisTitle(self):
|
|
527
|
+
return self._yTitle
|
|
528
|
+
|
|
529
|
+
@yAxisTitle.setter
|
|
530
|
+
def yAxisTitle(self, text):
|
|
531
|
+
self._yTitle = str(text); self.update()
|
|
532
|
+
|
|
533
|
+
@Property(float)
|
|
534
|
+
def markerSize(self):
|
|
535
|
+
return self._markerSize
|
|
536
|
+
|
|
537
|
+
@markerSize.setter
|
|
538
|
+
def markerSize(self, value):
|
|
539
|
+
self._markerSize = max(1.0, float(value)); self.update()
|
|
540
|
+
|
|
541
|
+
@Property(str)
|
|
542
|
+
def markerShape(self):
|
|
543
|
+
return self._markerShape
|
|
544
|
+
|
|
545
|
+
@markerShape.setter
|
|
546
|
+
def markerShape(self, value):
|
|
547
|
+
value = str(value)
|
|
548
|
+
self._markerShape = value if value in ("circle", "square", "diamond",
|
|
549
|
+
"triangle") else "circle"
|
|
550
|
+
self.update()
|
|
551
|
+
|
|
552
|
+
@Property(float)
|
|
553
|
+
def markerOpacity(self):
|
|
554
|
+
return self._markerOpacity
|
|
555
|
+
|
|
556
|
+
@markerOpacity.setter
|
|
557
|
+
def markerOpacity(self, value):
|
|
558
|
+
self._markerOpacity = max(0.0, min(1.0, float(value))); self.update()
|
|
559
|
+
|
|
560
|
+
@Property(int)
|
|
561
|
+
def tickCount(self):
|
|
562
|
+
return self._tickCount
|
|
563
|
+
|
|
564
|
+
@tickCount.setter
|
|
565
|
+
def tickCount(self, value):
|
|
566
|
+
self._tickCount = max(1, int(value)); self.update()
|
|
567
|
+
|
|
568
|
+
@Property(bool)
|
|
569
|
+
def showGrid(self):
|
|
570
|
+
return self._showGrid
|
|
571
|
+
|
|
572
|
+
@showGrid.setter
|
|
573
|
+
def showGrid(self, value):
|
|
574
|
+
self._showGrid = bool(value); self.update()
|
|
575
|
+
|
|
576
|
+
@Property(bool)
|
|
577
|
+
def showAxes(self):
|
|
578
|
+
return self._showAxes
|
|
579
|
+
|
|
580
|
+
@showAxes.setter
|
|
581
|
+
def showAxes(self, value):
|
|
582
|
+
self._showAxes = bool(value); self.update()
|
|
583
|
+
|
|
584
|
+
@Property(bool)
|
|
585
|
+
def showLegend(self):
|
|
586
|
+
return self._showLegend
|
|
587
|
+
|
|
588
|
+
@showLegend.setter
|
|
589
|
+
def showLegend(self, value):
|
|
590
|
+
self._showLegend = bool(value); self.updateGeometry(); self.update()
|
|
591
|
+
|
|
592
|
+
@Property(bool)
|
|
593
|
+
def showTooltip(self):
|
|
594
|
+
return self._showTooltip
|
|
595
|
+
|
|
596
|
+
@showTooltip.setter
|
|
597
|
+
def showTooltip(self, value):
|
|
598
|
+
self._showTooltip = bool(value); self.update()
|
|
599
|
+
|
|
600
|
+
# -- colours (from tokens via qproperty) --
|
|
601
|
+
@Property(QColor)
|
|
602
|
+
def gridColor(self):
|
|
603
|
+
return self._gridColor
|
|
604
|
+
|
|
605
|
+
@gridColor.setter
|
|
606
|
+
def gridColor(self, c):
|
|
607
|
+
self._gridColor = QColor(c); self.update()
|
|
608
|
+
|
|
609
|
+
@Property(QColor)
|
|
610
|
+
def axisColor(self):
|
|
611
|
+
return self._axisColor
|
|
612
|
+
|
|
613
|
+
@axisColor.setter
|
|
614
|
+
def axisColor(self, c):
|
|
615
|
+
self._axisColor = QColor(c); self.update()
|
|
616
|
+
|
|
617
|
+
@Property(QColor)
|
|
618
|
+
def labelColor(self):
|
|
619
|
+
return self._labelColor
|
|
620
|
+
|
|
621
|
+
@labelColor.setter
|
|
622
|
+
def labelColor(self, c):
|
|
623
|
+
self._labelColor = QColor(c); self.update()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pro widgets: charts."""
|