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,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."""