Files
cowork-local/presentation/dashboard/usage_chart_widget.py
T
Nam Pham Dinh ThanhandClaude Opus 5 062ea4ba21 refactor(dashboard): R08-T13 — dashboard_tab.py 438 -> 215, tách 3 widget
token_usage_card_widget.py    93   5 thẻ số liệu + thẻ Ngân sách
    usage_chart_widget.py        119   biểu đồ tuần/tháng/năm + đường so sánh
    habits_widget.py             171   thói quen dùng token + nhận xét của AI

Ba widget THẬT, không phải mixin — khác với shell và Co4E, ba mảng này tách
bạch trên màn hình và không đọc state của nhau. Giao tiếp bằng signal:
budget_applied, filter_changed, status_message.

Điểm cần biết: các nút lật khoảng và hai ô chọn thuộc về UsageChartWidget
nhưng được Dashboard nhấc lên hàng điều khiển ở trên. Chúng là control của
biểu đồ, chỉ hiển thị ở chỗ khác.

Giữ 18 cầu tương thích cho tên cũ vì check_dashboard, check_design_parity và
check_controls_alive đọc thẳng self.card_total, self._chart_period_lbl...

756 test xanh. check_dashboard, check_design_parity, check_controls_alive qua.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:05:48 +09:00

120 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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