laboard 1.0.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.
laboard/charts.py ADDED
@@ -0,0 +1,631 @@
1
+ from PyQt6.QtCore import QPointF, QRectF, Qt
2
+ from PyQt6.QtGui import QPainter, QPainterPath, QPen, QPixmap
3
+ from PyQt6.QtWidgets import QSizePolicy, QWidget
4
+
5
+ from . import theme
6
+ from .theme import T, font, qc
7
+ from .widgets import col
8
+
9
+
10
+ def _aa(w):
11
+ p = QPainter(w)
12
+ p.setRenderHint(QPainter.RenderHint.Antialiasing)
13
+ p.setRenderHint(QPainter.RenderHint.TextAntialiasing)
14
+ return p
15
+
16
+
17
+ class StackedBar(QWidget):
18
+ def __init__(self, segments, height=12, gap=2, min_px=3):
19
+ super().__init__()
20
+ self.segments, self.gap, self.min_px = segments, gap, min_px
21
+ self.setFixedHeight(height)
22
+
23
+ def paintEvent(self, e):
24
+ p = _aa(self)
25
+ clip = QPainterPath()
26
+ clip.addRoundedRect(QRectF(self.rect()), self.height() / 2, self.height() / 2)
27
+ p.setClipPath(clip)
28
+ avail = self.width() - self.gap * (len(self.segments) - 1)
29
+ x = 0.0
30
+ for frac, c in self.segments:
31
+ w = max(self.min_px, avail * frac)
32
+ p.fillRect(QRectF(x, 0, w, self.height()), qc(col(c)))
33
+ x += w + self.gap
34
+
35
+
36
+ class Meter(QWidget):
37
+ def __init__(self, pct, height=6, fill="ink", track="surf2"):
38
+ super().__init__()
39
+ self.pct, self.fill, self.track = pct, fill, track
40
+ self.setFixedHeight(height)
41
+
42
+ def paintEvent(self, e):
43
+ p = _aa(self)
44
+ r = self.height() / 2
45
+ p.setPen(Qt.PenStyle.NoPen)
46
+ p.setBrush(qc(col(self.track)))
47
+ p.drawRoundedRect(QRectF(self.rect()), r, r)
48
+ w = self.width() * self.pct / 100
49
+ if w > 0:
50
+ p.setBrush(qc(col(self.fill)))
51
+ p.drawRoundedRect(QRectF(0, 0, max(w, self.height()), self.height()), r, r)
52
+
53
+
54
+ class DataChangeChart(QWidget):
55
+ def __init__(self, changes, vmax):
56
+ super().__init__()
57
+ self.changes, self.vmax = changes, vmax
58
+ self.setFixedHeight(76 + 1 + 52 + 4 + 28)
59
+
60
+ def paintEvent(self, e):
61
+ p = _aa(self)
62
+ n = len(self.changes)
63
+ gap = 8
64
+ cw = (self.width() - gap * (n - 1)) / n
65
+ pos, neg = qc("#2ead4b"), qc("#d03238")
66
+ lab = font(11, 600)
67
+ for i, (hsh, date, add, rem) in enumerate(self.changes):
68
+ x = i * (cw + gap)
69
+ bw = cw * 0.6
70
+
71
+ bx = x + (cw - bw) / 2
72
+ ah = round(add / self.vmax * 56)
73
+ dh = round(rem / self.vmax * 56)
74
+ p.setPen(Qt.PenStyle.NoPen)
75
+ if ah:
76
+ path = QPainterPath()
77
+ path.addRoundedRect(QRectF(bx, 76 - ah, bw, ah + 4), 4, 4)
78
+ p.save()
79
+ p.setClipRect(QRectF(bx, 0, bw, 76))
80
+ p.fillPath(path, pos)
81
+ p.restore()
82
+ p.setFont(lab)
83
+ p.setPen(pos)
84
+ p.drawText(QRectF(x, 76 - ah - 17, cw, 14), Qt.AlignmentFlag.AlignCenter, f"+{add:,}")
85
+ p.setPen(Qt.PenStyle.NoPen)
86
+ p.fillRect(QRectF(x, 76, cw + (gap if i < n - 1 else 0), 1), qc(T.mute))
87
+ if dh:
88
+ path = QPainterPath()
89
+ path.addRoundedRect(QRectF(bx, 73, bw, dh + 4), 4, 4)
90
+ p.save()
91
+ p.setClipRect(QRectF(bx, 77, bw, 52))
92
+ p.fillPath(path, neg)
93
+ p.restore()
94
+ p.setFont(lab)
95
+ p.setPen(neg)
96
+ p.drawText(QRectF(x, 77 + dh + 3, cw, 14), Qt.AlignmentFlag.AlignCenter, f"−{rem:,}")
97
+ p.setFont(font(11, 500, mono=True))
98
+ p.setPen(qc(T.body))
99
+ p.drawText(QRectF(x, 133, cw, 14), Qt.AlignmentFlag.AlignCenter, hsh)
100
+ p.setFont(font(11))
101
+ p.setPen(qc(T.mute))
102
+ p.drawText(QRectF(x, 147, cw, 14), Qt.AlignmentFlag.AlignCenter, date)
103
+
104
+
105
+ class AccuracyChart(QWidget):
106
+ PLOT_H = 200
107
+
108
+ def __init__(self, rows):
109
+ super().__init__()
110
+ self.rows = rows
111
+ self.loss_max = max(max(r[2], r[3]) for r in rows) * 1.1 or 1
112
+ self.ms_max = max(r[4] for r in rows) * 1.1 or 1
113
+ self.setFixedHeight(self.PLOT_H + 8 + 28 + 16)
114
+
115
+ def paintEvent(self, e):
116
+ p = _aa(self)
117
+ p.translate(0, 16)
118
+ H = self.PLOT_H
119
+ left, right = 34, 42
120
+ pw = self.width() - left - right
121
+ n = len(self.rows)
122
+ p.setFont(font(11))
123
+ p.setPen(qc(T.mute))
124
+ lm, mm = self.loss_max, self.ms_max
125
+ for i, (l, r) in enumerate([(f"{lm:.2g}", f"{mm:.2g} ms"), (f"{lm / 2:.2g}", f"{mm / 2:.2g} ms"), ("0", "0")]):
126
+ y = i * H / 2
127
+ p.drawText(QRectF(0, y - 6, 26, 12), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, l)
128
+ p.drawText(QRectF(left + pw + 8, y - 6, 40, 12), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, r)
129
+ cw = pw / n
130
+ for i, (_, _, _, _, ms) in enumerate(self.rows):
131
+ bh = ms / self.ms_max * H
132
+ bw = cw * 0.46
133
+ bx = left + i * cw + (cw - bw) / 2
134
+ path = QPainterPath()
135
+ path.addRoundedRect(QRectF(bx, H - bh, bw, bh + 4), 4, 4)
136
+ p.save()
137
+ p.setClipRect(QRectF(bx, 0, bw, H))
138
+ p.fillPath(path, qc(T.grid))
139
+ p.restore()
140
+ p.setFont(font(10, 600))
141
+ p.setPen(qc(T.mute))
142
+ p.drawText(QRectF(left + i * cw, H - bh - 15, cw, 12), Qt.AlignmentFlag.AlignCenter, f"{ms:.1f}")
143
+ dash = QPen(qc(T.grid), 1)
144
+ dash.setDashPattern([3, 4])
145
+ p.setPen(dash)
146
+ p.drawLine(QPointF(left, H / 2), QPointF(left + pw, H / 2))
147
+ p.setPen(QPen(qc(T.mute), 1))
148
+ p.drawLine(QPointF(left, H - 0.5), QPointF(left + pw, H - 0.5))
149
+ for idx, color in ((3, T.c2), (2, T.ink)):
150
+ pts = [QPointF(left + (i + 0.5) * cw, H - r[idx] / self.loss_max * H) for i, r in enumerate(self.rows)]
151
+ p.setPen(QPen(qc(color), 2.5, cap=Qt.PenCapStyle.RoundCap, join=Qt.PenJoinStyle.RoundJoin))
152
+ p.drawPolyline(pts)
153
+ for i, (hsh, model, *_rest) in enumerate(self.rows):
154
+ x = left + i * cw
155
+ p.setFont(font(11, 500, mono=True))
156
+ p.setPen(qc(T.body))
157
+ p.drawText(QRectF(x, H + 8, cw, 14), Qt.AlignmentFlag.AlignCenter, hsh)
158
+ p.setFont(font(11))
159
+ p.setPen(qc(T.mute))
160
+ p.drawText(QRectF(x, H + 22, cw, 14), Qt.AlignmentFlag.AlignCenter, model)
161
+
162
+
163
+ SERIES = dict(light=["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"],
164
+ dark=["#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"])
165
+
166
+
167
+ def series_style(slot):
168
+ colors = SERIES[theme.current]
169
+ return colors[slot % len(colors)], slot >= len(colors)
170
+
171
+
172
+ class Swatch(QWidget):
173
+ def __init__(self, color, dashed=False):
174
+ super().__init__()
175
+ self.color, self.dashed = color, dashed
176
+ self.setFixedSize(22, 12)
177
+
178
+ def paintEvent(self, e):
179
+ p = _aa(self)
180
+ pen = QPen(qc(self.color), 3, cap=Qt.PenCapStyle.RoundCap)
181
+ if self.dashed:
182
+ pen.setDashPattern([1.5, 1.5])
183
+ p.setPen(pen)
184
+ p.drawLine(QPointF(2, 6), QPointF(20, 6))
185
+
186
+
187
+ class LineChart(QWidget):
188
+ PLOT_H = 200
189
+ LEFT = 44
190
+ MAX_ZOOM = 50
191
+ Y_STEP, X_STEP = 100, 180
192
+
193
+ def __init__(self, y_labels, x_labels, plot_h=PLOT_H):
194
+ super().__init__()
195
+ self.y_labels, self.x_labels, self.plot_h = y_labels, x_labels, plot_h
196
+ self.series = []
197
+ self.hover_rows, self.x_name, self.pointer = [], "", None
198
+ self.x_value = None
199
+ self.y_range = None
200
+ self.view = (0.0, 1.0)
201
+ self.pan = None
202
+ self.moved = False
203
+ self.setFixedHeight(plot_h + 8 + 12 + 8)
204
+ self.setMouseTracking(True)
205
+
206
+ def set_series(self, series):
207
+ self.series = series
208
+ self.update()
209
+
210
+ def set_plot_h(self, h):
211
+ self.plot_h = h
212
+ self.setFixedHeight(h + 8 + 12 + 8)
213
+ self.update()
214
+
215
+ def set_y_range(self, lo, hi, fmt):
216
+ self.y_range = (lo, hi, fmt)
217
+ self.update()
218
+
219
+ def set_axis(self, x_value, x_name):
220
+ self.x_value, self.x_name = x_value, x_name
221
+ self.update()
222
+
223
+ def set_hover(self, rows):
224
+ self.hover_rows = rows
225
+ self.update()
226
+
227
+ def _sx(self, f):
228
+ v0, v1 = self.view
229
+ return self.LEFT + (f - v0) / (v1 - v0) * (self.width() - self.LEFT)
230
+
231
+ def _fx(self, x):
232
+ v0, v1 = self.view
233
+ return v0 + (x - self.LEFT) / (self.width() - self.LEFT) * (v1 - v0)
234
+
235
+ def wheelEvent(self, e):
236
+ if not e.modifiers() & Qt.KeyboardModifier.ControlModifier or not e.angleDelta().y():
237
+ return super().wheelEvent(e)
238
+ v0, v1 = self.view
239
+ at = min(max(self._fx(e.position().x()), v0), v1)
240
+ span = min(max((v1 - v0) / 1.25 ** (e.angleDelta().y() / 120), 1 / self.MAX_ZOOM), 1.0)
241
+ lo = at - (at - v0) / (v1 - v0) * span
242
+ lo = min(max(lo, 0.0), 1.0 - span)
243
+ self.view = (lo, lo + span)
244
+ self._cursor()
245
+ self.update()
246
+ e.accept()
247
+
248
+ def _cursor(self):
249
+ if self.pan and self.moved:
250
+ self.setCursor(Qt.CursorShape.ClosedHandCursor)
251
+ elif self.view != (0.0, 1.0):
252
+ self.setCursor(Qt.CursorShape.OpenHandCursor)
253
+ else:
254
+ self.unsetCursor()
255
+
256
+ def mousePressEvent(self, e):
257
+ if e.button() == Qt.MouseButton.LeftButton:
258
+ self.pan, self.moved = (e.position().x(), self.view), False
259
+ e.ignore()
260
+
261
+ def mouseMoveEvent(self, e):
262
+ self.pointer = e.position()
263
+ if self.pan and e.buttons() & Qt.MouseButton.LeftButton:
264
+ x0, (v0, v1) = self.pan
265
+ dx = e.position().x() - x0
266
+ if abs(dx) > 3 or self.moved:
267
+ self.moved = True
268
+ shift = -dx / (self.width() - self.LEFT) * (v1 - v0)
269
+ lo = min(max(v0 + shift, 0.0), 1.0 - (v1 - v0))
270
+ self.view = (lo, lo + (v1 - v0))
271
+ self._cursor()
272
+ self.update()
273
+ super().mouseMoveEvent(e)
274
+
275
+ def mouseReleaseEvent(self, e):
276
+ dragged = self.pan is not None and self.moved
277
+ self.pan, self.moved = None, False
278
+ self._cursor()
279
+ if dragged:
280
+ e.accept()
281
+ else:
282
+ e.ignore()
283
+
284
+ def leaveEvent(self, e):
285
+ self.pointer = None
286
+ self.update()
287
+
288
+ def paintEvent(self, e):
289
+ p = _aa(self)
290
+ p.translate(0, 6)
291
+ H, left = self.plot_h, self.LEFT
292
+ pw = self.width() - left
293
+ p.setFont(font(11))
294
+ p.setPen(qc(T.mute))
295
+ y_labels = self.y_labels
296
+ if self.y_range is not None:
297
+ lo, hi, fmt = self.y_range
298
+ n = max(2, round(H / self.Y_STEP))
299
+ y_labels = [fmt(hi - i * (hi - lo) / n) for i in range(n + 1)]
300
+ k = max(len(y_labels) - 1, 1)
301
+ for i, t in enumerate(y_labels):
302
+ p.drawText(QRectF(0, i * H / k - 6, 34, 12), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, t)
303
+ p.setPen(QPen(qc(T.grid), 1))
304
+ for i in range(k):
305
+ y = 0.5 if i == 0 else i * H / k
306
+ p.drawLine(QPointF(left, y), QPointF(left + pw, y))
307
+ p.setPen(QPen(qc(T.mute), 1))
308
+ p.drawLine(QPointF(left, H - 0.5), QPointF(left + pw, H - 0.5))
309
+ p.save()
310
+ p.setClipRect(QRectF(left, -6, pw, H + 12))
311
+ for vals, color, width, *rest in reversed(self.series):
312
+ if len(vals) < 2:
313
+ continue
314
+ xs = rest[0] if rest else [i / (len(vals) - 1) for i in range(len(vals))]
315
+ pts = [QPointF(self._sx(x), H - min(max(v, -0.1), 1) * H) for x, v in zip(xs, vals)]
316
+ pen = QPen(qc(col(color)), width, join=Qt.PenJoinStyle.RoundJoin)
317
+ if len(rest) > 1 and rest[1]:
318
+ pen.setDashPattern([3, 2])
319
+ p.setPen(pen)
320
+ p.drawPolyline(pts)
321
+ p.restore()
322
+ p.setPen(qc(T.mute))
323
+ labels = self.x_labels
324
+ if self.x_value is not None:
325
+ v0, v1 = self.view
326
+ n = max(2, round(pw / self.X_STEP))
327
+ while True:
328
+ labels = [_num(self.x_value(v0 + j * (v1 - v0) / n)) for j in range(n + 1)]
329
+ labels[-1] += f" {self.x_name}"
330
+ if n == 1 or sum(map(p.fontMetrics().horizontalAdvance, labels)) + 12 * n <= pw:
331
+ break
332
+ n -= 1
333
+ n = len(labels)
334
+ for i, t in enumerate(labels):
335
+ fm = p.fontMetrics()
336
+ tw = fm.horizontalAdvance(t)
337
+ x = left + i * (pw - tw) / (n - 1) if n > 1 else left
338
+ p.drawText(QPointF(x, H + 8 + fm.ascent()), t)
339
+ if self.view != (0.0, 1.0):
340
+ zoom = f"{1 / (self.view[1] - self.view[0]):.1f}×"
341
+ p.drawText(QRectF(left, 2, pw - 4, 14), Qt.AlignmentFlag.AlignRight, zoom)
342
+ self._paint_readout(p, H)
343
+
344
+ def _paint_readout(self, p, H):
345
+ if self.pointer is None or not self.hover_rows:
346
+ return
347
+ fx = self._fx(self.pointer.x())
348
+ if not self.view[0] <= fx <= self.view[1] or not 0 <= self.pointer.y() - 6 <= H:
349
+ return
350
+ near = lambda r, f: min(range(len(r["x01"])), key=lambda i: abs(r["x01"][i] - f))
351
+ snap = min((r["x01"][near(r, fx)] for r in self.hover_rows), key=lambda x: abs(x - fx))
352
+ hits = [(r, near(r, snap)) for r in self.hover_rows]
353
+ x = self._sx(snap)
354
+ p.setPen(QPen(qc(T.mute), 1))
355
+ p.drawLine(QPointF(x, 0), QPointF(x, H))
356
+ for r, i in hits:
357
+ p.setPen(QPen(qc(T.surf), 2))
358
+ p.setBrush(qc(col(r["color"])))
359
+ p.drawEllipse(QPointF(self._sx(r["x01"][i]), H - min(max(r["y01"][i], -0.1), 1) * H), 4, 4)
360
+ head = f"{self.x_name} {_num(hits[0][0]['xs'][hits[0][1]])}".strip()
361
+ rows = []
362
+ for r, i in hits:
363
+ v, sm = _num(r["ys"][i]), r["smooth"]
364
+ extra = f"smoothed {_num(sm[i])}" if sm is not None and _num(sm[i]) != v else ""
365
+ rows.append((r, v, " · ".join(t for t in (r["name"], extra) if t)))
366
+ strong, small = font(12, 600), font(11)
367
+ key = 22 if len(rows) > 1 else 0
368
+ p.setFont(strong)
369
+ vw = max(p.fontMetrics().horizontalAdvance(v) for _, v, _ in rows)
370
+ p.setFont(small)
371
+ fm = p.fontMetrics()
372
+ room = self.width() - 24 - key - vw - 8
373
+ names = [fm.elidedText(n, Qt.TextElideMode.ElideMiddle, max(room, 40)) for _, _, n in rows]
374
+ nw = max(fm.horizontalAdvance(n) for n in names)
375
+ w = min(12 + max(key + vw + (8 + nw if nw else 0), fm.horizontalAdvance(head)) + 12, self.width())
376
+ h = 8 + 16 + len(rows) * 18 + 6
377
+ bx = self.pointer.x() + 14
378
+ if bx + w > self.width():
379
+ bx = self.pointer.x() - 14 - w
380
+ bx = min(max(bx, 0), self.width() - w)
381
+ by = min(max(self.pointer.y() - 6 + 14, -6), self.height() - 6 - h)
382
+ p.setPen(Qt.PenStyle.NoPen)
383
+ p.setBrush(qc(T.ink, 0.92))
384
+ p.drawRoundedRect(QRectF(bx, by, w, h), 8, 8)
385
+ p.setPen(qc(T.mute))
386
+ p.drawText(QRectF(bx + 12, by + 8, w - 24, 14), Qt.AlignmentFlag.AlignVCenter, head)
387
+ for k, ((r, v, _), name) in enumerate(zip(rows, names)):
388
+ y = by + 8 + 16 + k * 18
389
+ if key:
390
+ pen = QPen(qc(col(r["color"])), 3, cap=Qt.PenCapStyle.RoundCap)
391
+ if r["dashed"]:
392
+ pen.setDashPattern([1.5, 1.5])
393
+ p.setPen(pen)
394
+ p.drawLine(QPointF(bx + 12, y + 9), QPointF(bx + 12 + 14, y + 9))
395
+ p.setFont(strong)
396
+ p.setPen(qc(T.surf))
397
+ p.drawText(QRectF(bx + 12 + key, y, vw + 2, 18), Qt.AlignmentFlag.AlignVCenter, v)
398
+ if name:
399
+ p.setFont(small)
400
+ p.setPen(qc(T.mute))
401
+ p.drawText(QRectF(bx + 12 + key + vw + 8, y, nw + 2, 18), Qt.AlignmentFlag.AlignVCenter, name)
402
+
403
+
404
+ def _num(v):
405
+ if isinstance(v, float) and v.is_integer():
406
+ v = int(v)
407
+ if isinstance(v, int) or abs(v) >= 1e4:
408
+ return f"{v:,.0f}"
409
+ return f"{v:.4g}"
410
+
411
+
412
+ class ZoomView:
413
+ MAX = 40.0
414
+ MIN_SCALE = 0.1
415
+
416
+ def __init__(self):
417
+ self.zoom, self.cx, self.cy = 1.0, 0.5, 0.5
418
+ self.panes = []
419
+
420
+ def reset(self):
421
+ self.zoom, self.cx, self.cy = 1.0, 0.5, 0.5
422
+ self.update()
423
+
424
+ def update(self):
425
+ for pane in self.panes:
426
+ pane.update()
427
+
428
+
429
+ class Stage(QWidget):
430
+ PAN_STEP = 60
431
+ KEEP = 48
432
+
433
+ def __init__(self, name, meta, kind, label=None, pred=None, video=False, path=None, radius=4, view=None, overlay=None):
434
+ super().__init__()
435
+ self.name, self.meta, self.kind, self.label, self.pred = name, meta, kind, label, pred
436
+ self.overlay = overlay
437
+ self.video, self.radius = video, radius
438
+ self.pix = QPixmap(path) if path else QPixmap()
439
+ self.view = view or ZoomView()
440
+ self.view.panes.append(self)
441
+ self._drag = None
442
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
443
+ if not self.pix.isNull():
444
+ self.setCursor(Qt.CursorShape.OpenHandCursor)
445
+
446
+ def _scale(self):
447
+ fit = min(self.width() / self.pix.width(), self.height() / self.pix.height())
448
+ return max(fit, 1e-6) * self.view.zoom
449
+
450
+ def _min_zoom(self):
451
+ fit = min(self.width() / self.pix.width(), self.height() / self.pix.height())
452
+ return min(1.0, ZoomView.MIN_SCALE / max(fit, 1e-6))
453
+
454
+ def _clamp(self):
455
+ v, s = self.view, self._scale()
456
+ for axis, side, size in (("cx", self.width(), self.pix.width()), ("cy", self.height(), self.pix.height())):
457
+ w = size * s
458
+ keep = min(self.KEEP, w)
459
+ lo, hi = (keep - side / 2) / w, (side / 2 + w - keep) / w
460
+ setattr(v, axis, min(max(getattr(v, axis), lo), hi))
461
+
462
+ def _image_rect(self):
463
+ s = self._scale()
464
+ w, h = self.pix.width() * s, self.pix.height() * s
465
+ return QRectF(self.width() / 2 - self.view.cx * w, self.height() / 2 - self.view.cy * h, w, h)
466
+
467
+ def _pan(self, dx, dy):
468
+ s = self._scale()
469
+ self.view.cx -= dx / (self.pix.width() * s)
470
+ self.view.cy -= dy / (self.pix.height() * s)
471
+ self._clamp()
472
+ self.view.update()
473
+
474
+ def wheelEvent(self, e):
475
+ if self.pix.isNull():
476
+ return super().wheelEvent(e)
477
+ d = e.angleDelta()
478
+ if e.modifiers() & Qt.KeyboardModifier.ShiftModifier:
479
+ self._pan((d.y() or d.x()) / 120 * self.PAN_STEP, 0)
480
+ elif d.y():
481
+ v, pos, r = self.view, e.position(), self._image_rect()
482
+ ix, iy = (pos.x() - r.x()) / r.width(), (pos.y() - r.y()) / r.height()
483
+ v.zoom = min(max(v.zoom * 1.25 ** (d.y() / 120), self._min_zoom()), ZoomView.MAX)
484
+ s = self._scale()
485
+ v.cx = ix - (pos.x() - self.width() / 2) / (self.pix.width() * s)
486
+ v.cy = iy - (pos.y() - self.height() / 2) / (self.pix.height() * s)
487
+ self._clamp()
488
+ v.update()
489
+ e.accept()
490
+
491
+ def mousePressEvent(self, e):
492
+ if not self.pix.isNull() and e.button() == Qt.MouseButton.LeftButton:
493
+ self._drag = e.position()
494
+ self.setCursor(Qt.CursorShape.ClosedHandCursor)
495
+
496
+ def mouseMoveEvent(self, e):
497
+ if self._drag is not None:
498
+ d = e.position() - self._drag
499
+ self._drag = e.position()
500
+ self._pan(d.x(), d.y())
501
+
502
+ def mouseReleaseEvent(self, e):
503
+ if self._drag is not None:
504
+ self._drag = None
505
+ self.setCursor(Qt.CursorShape.OpenHandCursor)
506
+
507
+ def mouseDoubleClickEvent(self, e):
508
+ if not self.pix.isNull():
509
+ self.view.reset()
510
+
511
+ def resizeEvent(self, e):
512
+ if not self.pix.isNull():
513
+ self._clamp()
514
+
515
+ def _chip(self, p, x, y, text, bg, fg="#e8ebe6"):
516
+ p.setFont(font(12, 600))
517
+ w = p.fontMetrics().horizontalAdvance(text) + 20
518
+ p.setPen(Qt.PenStyle.NoPen)
519
+ p.setBrush(bg)
520
+ p.drawRoundedRect(QRectF(x, y, w, 24), 12, 12)
521
+ p.setPen(qc(fg))
522
+ p.drawText(QRectF(x, y, w, 24), Qt.AlignmentFlag.AlignCenter, text)
523
+ return x + w + 6
524
+
525
+ def _pred_box(self, p, x, y, rows, bg):
526
+ key_font, val_font = font(12), font(12, 600)
527
+ p.setFont(key_font)
528
+ kw = max(p.fontMetrics().horizontalAdvance(k) for k, _, _ in rows)
529
+ sep = p.fontMetrics().horizontalAdvance(" : ") if kw else 0
530
+ p.setFont(val_font)
531
+ vw = max(p.fontMetrics().horizontalAdvance(v) for _, v, _ in rows)
532
+ dot = 14 if any(t for _, _, t in rows) else 0
533
+ box = QRectF(x, y, 12 + dot + kw + sep + vw + 14, 8 + 20 * len(rows) + 6)
534
+ p.setPen(Qt.PenStyle.NoPen)
535
+ p.setBrush(bg)
536
+ p.drawRoundedRect(box, 12, 12)
537
+ for i, (k, v, tone) in enumerate(rows):
538
+ line = QRectF(box.x() + 12, box.y() + 8 + 20 * i, box.width() - 24, 20)
539
+ if tone:
540
+ p.setPen(Qt.PenStyle.NoPen)
541
+ p.setBrush(qc(tone))
542
+ p.drawEllipse(QPointF(line.x() + 4, line.center().y()), 4, 4)
543
+ tx = line.x() + dot
544
+ if k:
545
+ p.setFont(key_font)
546
+ p.setPen(qc("#b4b8b1"))
547
+ p.drawText(QRectF(tx, line.y(), kw + sep, 20), Qt.AlignmentFlag.AlignVCenter, f"{k}")
548
+ p.drawText(QRectF(tx + kw, line.y(), sep, 20), Qt.AlignmentFlag.AlignVCenter, " : ")
549
+ tx += kw + sep
550
+ p.setFont(val_font)
551
+ p.setPen(qc("#ffffff"))
552
+ p.drawText(QRectF(tx, line.y(), vw + 2, 20), Qt.AlignmentFlag.AlignVCenter, v)
553
+ return box.bottom()
554
+
555
+ def paintEvent(self, e):
556
+ p = _aa(self)
557
+ r = QRectF(self.rect())
558
+ clip = QPainterPath()
559
+ area = QRectF(self.parentWidget().rect()).translated(-QPointF(self.pos())) if self.parentWidget() else r
560
+ clip.addRoundedRect(area, self.radius, self.radius)
561
+ p.setClipPath(clip)
562
+ if not self.pix.isNull():
563
+ p.fillRect(r, qc(T.stage))
564
+ p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, self._scale() < 2)
565
+ p.drawPixmap(self._image_rect(), self.pix, QRectF(self.pix.rect()))
566
+ if self.overlay:
567
+ self.overlay.paint(p, self._image_rect(), self._scale())
568
+ if self.view.zoom > 1:
569
+ text = f"{self.view.zoom * 100:.0f}%"
570
+ p.setFont(font(12, 600))
571
+ self._chip(p, r.right() - 12 - (p.fontMetrics().horizontalAdvance(text) + 20), 12, text, qc("#0e0f0c", 0.7))
572
+ else:
573
+ p.fillRect(r, qc("#262724"))
574
+ p.save()
575
+ p.translate(r.center())
576
+ p.rotate(-45)
577
+ span = int((r.width() + r.height()))
578
+ p.setPen(Qt.PenStyle.NoPen)
579
+ p.setBrush(qc("#2e2f2c"))
580
+ k = -span
581
+ while k < span:
582
+ p.drawRect(QRectF(k + 14, -span, 14, span * 2))
583
+ k += 28
584
+ p.restore()
585
+ cy = r.center().y() - (30 if self.video else 0)
586
+ p.setFont(font(14, 600, mono=True))
587
+ p.setPen(qc("#b4b8b1"))
588
+ p.drawText(QRectF(0, cy - 24, r.width(), 20), Qt.AlignmentFlag.AlignCenter, self.name)
589
+ p.setFont(font(13))
590
+ p.setPen(qc("#868685"))
591
+ p.drawText(QRectF(0, cy + 4, r.width(), 18), Qt.AlignmentFlag.AlignCenter, self.meta)
592
+ if self.video:
593
+ p.setPen(Qt.PenStyle.NoPen)
594
+ p.setBrush(qc("#9fe870"))
595
+ c = QPointF(r.center().x(), cy + 58)
596
+ p.drawEllipse(c, 28, 28)
597
+ tri = QPainterPath()
598
+ tri.moveTo(c.x() - 5, c.y() - 9)
599
+ tri.lineTo(c.x() + 9, c.y())
600
+ tri.lineTo(c.x() - 5, c.y() + 9)
601
+ tri.closeSubpath()
602
+ p.fillPath(tri, qc("#0e0f0c"))
603
+ chip = qc("#0e0f0c", 0.7)
604
+ if self.kind == "raw":
605
+ self._chip(p, 12, 12, f"label: {self.label}", chip)
606
+ elif self.kind == "inference":
607
+ pr = self.pred
608
+ below = self._pred_box(p, 12, 12, pr["chips"], chip) if pr["chips"] else 12
609
+ if not pr["top"]:
610
+ return
611
+ pw = min(300, r.width() - 24)
612
+ box = QRectF(12, below + 8, pw, 10 + 14 + 6 + 3 * 22 + 4)
613
+ p.setPen(Qt.PenStyle.NoPen)
614
+ p.setBrush(qc("#0e0f0c", 0.82))
615
+ p.drawRoundedRect(box, 10, 10)
616
+ p.setFont(font(11))
617
+ p.setPen(qc("#868685"))
618
+ p.drawText(QRectF(box.x() + 12, box.y() + 10, pw - 24, 14), Qt.AlignmentFlag.AlignLeft, pr.get("source", "Top-3"))
619
+ p.drawText(QRectF(box.x() + 12, box.y() + 10, pw - 24, 14), Qt.AlignmentFlag.AlignRight, pr["ms"])
620
+ for i, (c, prob) in enumerate(pr["top"]):
621
+ y = box.y() + 30 + i * 22
622
+ p.setFont(font(12))
623
+ p.setPen(qc("#e8ebe6"))
624
+ p.drawText(QRectF(box.x() + 12, y, 84, 16), Qt.AlignmentFlag.AlignVCenter, c)
625
+ p.drawText(QRectF(box.right() - 52, y, 40, 16), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, f"{prob:.2f}")
626
+ bx, bw = box.x() + 12 + 92, pw - 24 - 92 - 48
627
+ p.setPen(Qt.PenStyle.NoPen)
628
+ p.setBrush(qc("#30312e"))
629
+ p.drawRoundedRect(QRectF(bx, y + 5, bw, 6), 3, 3)
630
+ p.setBrush(qc("#e8ebe6"))
631
+ p.drawRoundedRect(QRectF(bx, y + 5, max(6, bw * prob), 6), 3, 3)