"""Widget graphique bande passante RX/TX (QPainter, sans dépendance externe).""" from __future__ import annotations from collections import deque from PyQt6.QtWidgets import QWidget from PyQt6.QtGui import QPainter, QColor, QPen, QBrush, QFont, QPainterPath from PyQt6.QtCore import Qt, QRect, QPointF from app.ui import theme def _fmt(bps: float) -> str: if bps >= 1_048_576: return f"{bps/1_048_576:.1f} MB/s" if bps >= 1_024: return f"{bps/1_024:.0f} KB/s" return f"{bps:.0f} B/s" class BandwidthGraph(QWidget): """Affiche RX (bleu) et TX (vert) sur les N dernières secondes.""" POINTS = 30 # nombre de points conservés def __init__(self, parent=None): super().__init__(parent) self._rx: deque[float] = deque([0.0] * self.POINTS, maxlen=self.POINTS) self._tx: deque[float] = deque([0.0] * self.POINTS, maxlen=self.POINTS) self.setMinimumHeight(70) self.setStyleSheet("background: transparent;") def push(self, rx_bps: float, tx_bps: float) -> None: self._rx.append(max(0.0, rx_bps)) self._tx.append(max(0.0, tx_bps)) self.update() def reset(self) -> None: self._rx = deque([0.0] * self.POINTS, maxlen=self.POINTS) self._tx = deque([0.0] * self.POINTS, maxlen=self.POINTS) self.update() def paintEvent(self, _): p = QPainter(self) p.setRenderHint(QPainter.RenderHint.Antialiasing) w, h = self.width(), self.height() pad_l, pad_r, pad_t, pad_b = 42, 8, 6, 20 # Fond p.fillRect(0, 0, w, h, QColor(theme.GRAPH_BG)) plot_w = w - pad_l - pad_r plot_h = h - pad_t - pad_b max_val = max(max(self._rx), max(self._tx), 1.0) def _path(data: deque, color: str, fill: str): pts = list(data) path = QPainterPath() xs = [pad_l + i * plot_w / (len(pts) - 1) for i in range(len(pts))] ys = [pad_t + plot_h - (v / max_val) * plot_h for v in pts] path.moveTo(QPointF(xs[0], pad_t + plot_h)) path.lineTo(QPointF(xs[0], ys[0])) for x, y in zip(xs[1:], ys[1:]): path.lineTo(QPointF(x, y)) path.lineTo(QPointF(xs[-1], pad_t + plot_h)) path.closeSubpath() p.fillPath(path, QBrush(QColor(fill))) pen = QPen(QColor(color)) pen.setWidthF(1.5) p.setPen(pen) line = QPainterPath() line.moveTo(QPointF(xs[0], ys[0])) for x, y in zip(xs[1:], ys[1:]): line.lineTo(QPointF(x, y)) p.drawPath(line) _path(self._rx, theme.GRAPH_RX, theme.GRAPH_RX_FILL) # RX _path(self._tx, theme.GRAPH_TX, theme.GRAPH_TX_FILL) # TX # Axes p.setPen(QPen(QColor(theme.GRAPH_AXIS))) p.drawLine(pad_l, pad_t, pad_l, pad_t + plot_h) p.drawLine(pad_l, pad_t + plot_h, w - pad_r, pad_t + plot_h) # Labels Y font = QFont("Arial", 8) p.setFont(font) p.setPen(QPen(QColor(theme.GRAPH_LABEL))) for frac, label in [(0.0, _fmt(max_val)), (0.5, _fmt(max_val / 2)), (1.0, "0")]: y = int(pad_t + frac * plot_h) p.drawText(QRect(0, y - 8, pad_l - 2, 16), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, label) # Légende p.setPen(QPen(QColor(theme.GRAPH_RX))) p.drawText(pad_l + 4, pad_t + 12, f"↓ {_fmt(self._rx[-1])}") p.setPen(QPen(QColor(theme.GRAPH_TX))) p.drawText(pad_l + 90, pad_t + 12, f"↑ {_fmt(self._tx[-1])}") p.end()