"""Biểu đồ mức dùng theo khoảng thời gian — R08-T13. Cắt khoảng đang chọn thành từng phần: TUẦN → 7 ngày (T2–CN) · THÁNG → các tuần W1…Wn · NĂM → 12 tháng. Đường nét đứt là mốc so sánh với khoảng liền trước cùng loại — "tuần trước" ở chế độ tuần, "tháng trước" ở chế độ tháng. Nó vẽ ở mức trung bình mỗi điểm của khoảng trước để nằm đúng thang đo, còn nhãn thì hiện % thay đổi của tổng. Các nút lật khoảng và hai ô chọn nằm ở đây nhưng được đặt lên hàng điều khiển của Dashboard — chúng thuộc về biểu đồ, chỉ hiển thị ở chỗ khác. """ from __future__ import annotations from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QComboBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget, ) from ...core import usage_tracker as ut from ...i18n import tr from ...theme import current_palette from ...ui.icons import icon from ...ui.spline_chart import SplineChart from ...ui.widgets import fmt_tokens as _fmt_tokens _REF_KEY = {"week": "dashboard.ref_last_week", "month": "dashboard.ref_last_month", "year": "dashboard.ref_last_year"} class UsageChartWidget(QWidget): #: Người dùng đổi khoảng/độ mịn/chỉ số — Dashboard nạp lại cả màn, không #: riêng biểu đồ, vì thẻ số liệu cũng đi theo bộ lọc. filter_changed = Signal() def __init__(self, parent=None): super().__init__(parent) self.offset = 0 # 0 = khoảng hiện tại; số âm = lùi về trước # --- các control, sẽ được Dashboard nhấc lên hàng điều khiển --- self.prev_btn = QPushButton() self.prev_btn.setIcon(icon("chevron-left")) self.prev_btn.setFixedWidth(30) self.prev_btn.clicked.connect(self._lui) self.period_lbl = QLabel() self.period_lbl.setObjectName("hint") self.period_lbl.setAlignment(Qt.AlignCenter) self.period_lbl.setMinimumWidth(170) self.next_btn = QPushButton() self.next_btn.setIcon(icon("chevron-right")) self.next_btn.setFixedWidth(30) self.next_btn.clicked.connect(self._toi) self.gran_combo = QComboBox() self.metric_combo = QComboBox() self.metric_combo.currentIndexChanged.connect(self.filter_changed) # --- thân widget: tiêu đề + biểu đồ --- v = QVBoxLayout(self) v.setContentsMargins(0, 0, 0, 0) head = QHBoxLayout() self.title = QLabel() self.title.setStyleSheet("font-weight:600;") head.addWidget(self.title, 1) v.addLayout(head) self.chart = SplineChart() v.addWidget(self.chart) # ---- lật khoảng ------------------------------------------------------ def _lui(self) -> None: self.offset -= 1 self.filter_changed.emit() def _toi(self) -> None: # Không cho đi quá khoảng hiện tại: tương lai thì chưa có dữ liệu. if self.offset < 0: self.offset += 1 self.filter_changed.emit() # ---- vẽ -------------------------------------------------------------- def refresh(self, events, pricing: dict) -> None: gran = self.gran_combo.currentData() or "week" metric = self.metric_combo.currentData() or "cost" parts = ut.period_breakdown(events, gran, pricing, offset=self.offset) mi = 0 if metric == "tokens" else 1 # (nhãn, token, tiền) pts = [(row[0], float(row[mi + 1])) for row in parts] # Dạng tiền rút gọn: hộp nhãn trục y hẹp, format_cost đầy đủ (tới 4 số # lẻ với USD) tràn ra ngoài và che mất con số. fmt = (_fmt_tokens if metric == "tokens" else (lambda v: ut.format_cost_compact(v, pricing))) cur = ut.period_totals(events, gran, pricing, self.offset) prev = ut.period_totals(events, gran, pricing, self.offset - 1) refs = [] if prev[mi] > 0: # Màu mờ có chủ ý: đây là mốc tham chiếu, không được tranh chấp # với đường dữ liệu màu nhấn. refs.append((prev[mi] / max(1, len(parts)), "%s %s" % (tr(_REF_KEY.get(gran, _REF_KEY["week"])), _delta(cur[mi], prev[mi])), current_palette().text_muted)) self.chart.set_reference_lines(refs) self.chart.set_data(pts, fmt, tr("dashboard.metric_%s" % metric)) self.period_lbl.setText(ut.period_range_label(gran, self.offset)) self.next_btn.setEnabled(self.offset < 0) def _delta(cur: float, prev: float) -> str: if prev <= 0: return "" pct = (cur - prev) / prev * 100.0 return "(%+.0f%%)" % pct