""" 数据可视化组件 """ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QPushButton, QComboBox ) from PyQt5.QtCore import Qt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import matplotlib.pyplot as plt from frontend.utils.api_client import APIClient # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False class VisualizationWidget(QWidget): """数据可视化界面""" def __init__(self, api_client: APIClient, parent=None): super().__init__(parent) self.api_client = api_client self.init_ui() self.load_ranking() def init_ui(self): """初始化界面""" layout = QVBoxLayout() # 标题 title = QLabel("积分排名可视化") title.setStyleSheet("font-size: 18px; font-weight: bold; margin: 10px;") layout.addWidget(title) # 控制栏 control_layout = QHBoxLayout() control_layout.addWidget(QLabel("显示前")) self.top_n_spin = QSpinBox() self.top_n_spin.setRange(5, 50) self.top_n_spin.setValue(10) control_layout.addWidget(self.top_n_spin) control_layout.addWidget(QLabel("名")) self.chart_type_combo = QComboBox() self.chart_type_combo.addItems(["柱形图", "折线图"]) control_layout.addWidget(self.chart_type_combo) self.refresh_btn = QPushButton("刷新") self.refresh_btn.clicked.connect(self.load_ranking) control_layout.addWidget(self.refresh_btn) control_layout.addStretch() layout.addLayout(control_layout) # 图表区域 self.figure = Figure(figsize=(10, 6)) self.canvas = FigureCanvas(self.figure) layout.addWidget(self.canvas) self.setLayout(layout) def load_ranking(self): """加载排名数据并绘制图表""" try: top_n = self.top_n_spin.value() data = self.api_client.get_ranking(top_n=top_n) if not data['rankings']: self.figure.clear() ax = self.figure.add_subplot(111) ax.text(0.5, 0.5, '暂无数据', ha='center', va='center', fontsize=16) self.canvas.draw() return rankings = data['rankings'] names = [f"{r['name']}\n({r['student_id']})" for r in rankings] scores = [r['total_score'] for r in rankings] rollcall_counts = [r['random_rollcall_count'] for r in rankings] self.figure.clear() chart_type = self.chart_type_combo.currentText() if chart_type == "柱形图": # 创建双Y轴 ax1 = self.figure.add_subplot(111) ax2 = ax1.twinx() # 绘制积分柱形图 bars = ax1.bar(range(len(names)), scores, color='skyblue', alpha=0.7, label='总积分') ax1.set_xlabel('学生') ax1.set_ylabel('总积分', color='blue') ax1.set_xticks(range(len(names))) ax1.set_xticklabels(names, rotation=45, ha='right') ax1.tick_params(axis='y', labelcolor='blue') ax1.grid(True, alpha=0.3) # 绘制点名次数折线图 line = ax2.plot(range(len(names)), rollcall_counts, color='red', marker='o', linewidth=2, label='随机点名次数') ax2.set_ylabel('随机点名次数', color='red') ax2.tick_params(axis='y', labelcolor='red') # 添加图例 ax1.legend(loc='upper left') ax2.legend(loc='upper right') self.figure.suptitle(f'积分排名前{top_n}名(柱形图:积分,折线图:点名次数)', fontsize=14) else: # 折线图 ax = self.figure.add_subplot(111) # 绘制积分折线 line1 = ax.plot(range(len(names)), scores, color='blue', marker='o', linewidth=2, label='总积分') ax.set_xlabel('学生') ax.set_ylabel('总积分', color='blue') ax.tick_params(axis='y', labelcolor='blue') ax.set_xticks(range(len(names))) ax.set_xticklabels(names, rotation=45, ha='right') # 创建第二个Y轴用于点名次数 ax2 = ax.twinx() line2 = ax2.plot(range(len(names)), rollcall_counts, color='red', marker='s', linewidth=2, label='随机点名次数') ax2.set_ylabel('随机点名次数', color='red') ax2.tick_params(axis='y', labelcolor='red') # 添加图例 ax.legend(loc='upper left') ax2.legend(loc='upper right') ax.grid(True, alpha=0.3) self.figure.suptitle(f'积分排名前{top_n}名(折线图)', fontsize=14) self.figure.tight_layout() self.canvas.draw() except Exception as e: import traceback error_msg = f"加载排名数据失败: {str(e)}\n{traceback.format_exc()}" print(error_msg) self.figure.clear() ax = self.figure.add_subplot(111) ax.text(0.5, 0.5, f'加载失败\n{str(e)}', ha='center', va='center', fontsize=12) self.canvas.draw()