radar_chart / app.py
Lashtw's picture
Update app.py
c6eaf85 verified
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import plotly.io as pio
from zipfile import ZipFile
from io import BytesIO
import matplotlib.pyplot as plt
import os
import matplotlib.font_manager as fm
# ============================
# 載入本地字型檔與設定
# ============================
font_path = "NotoSansTC-Regular.ttf" # 確認此字型檔路徑正確
if os.path.exists(font_path):
fm.fontManager.addfont(font_path)
font_prop = fm.FontProperties(fname=font_path)
CHINESE_FONT = font_prop.get_name() # 例如 "Noto Sans TC"
else:
st.error(f"找不到字型檔: {font_path}")
CHINESE_FONT = "Noto Sans TC" # fallback
# 配置 Kaleido 的預設參數與額外字型設定
pio.kaleido.scope.default_font = CHINESE_FONT
pio.kaleido.scope.default_format = "png"
pio.kaleido.scope.default_width = 1200
pio.kaleido.scope.default_height = 900
pio.kaleido.scope.default_scale = 2
pio.kaleido.scope.extra_fonts = {CHINESE_FONT: font_path}
# ============================
# 系統字型檢查與安裝
# ============================
def setup_chinese_font():
"""檢查系統中文字型是否可用,若無則嘗試安裝"""
try:
test_fig, ax = plt.subplots()
ax.text(0.5, 0.5, "中文測試", fontfamily=CHINESE_FONT)
test_fig.savefig("font_test.png")
plt.close(test_fig)
except Exception as e:
st.warning("正在安裝中文字型...")
os.system('apt-get update && apt-get install -y --force-yes fonts-noto-cjk')
st.experimental_rerun()
# ============================
# 資料讀取
# ============================
def load_data(uploaded_file):
"""從 CSV 檔案中讀取資料"""
try:
df = pd.read_csv(uploaded_file, encoding='utf-8').dropna(how='all')
if '姓名' not in df.columns:
raise ValueError("CSV檔案中缺少必要欄位:姓名")
numeric_columns = []
potential_numeric = ['平均', '總分', '國文', '英文', '數學', '自科',
'社會', '地理', '歷史', '公民', '物理', '化學', '生物']
for col in potential_numeric:
if col in df.columns:
try:
df[col] = pd.to_numeric(df[col], errors='coerce')
numeric_columns.append(col)
except:
pass
if not numeric_columns:
raise ValueError("CSV檔案中未找到有效的數值欄位")
return df, numeric_columns
except Exception as e:
st.error(f"數據加載錯誤:{str(e)}")
return None, None
# ============================
# 生成雷達圖
# ============================
def create_radar_chart(df, selected_rows, selected_columns, student_name=""):
"""生成雷達圖 (包含圖例、極座標軸等設定)"""
fig = go.Figure()
line_styles = ['solid', 'dot', 'dash', 'longdash', 'dashdot']
colors = ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD']
for i, row_name in enumerate(selected_rows):
row_data = df[df['姓名'] == row_name][selected_columns].iloc[0]
fig.add_trace(go.Scatterpolar(
r=row_data.values,
theta=selected_columns,
fill='toself',
name=row_name,
line=dict(
color=colors[i % len(colors)],
dash=line_styles[i % len(line_styles)],
width=2
),
marker=dict(opacity=0.5)
))
max_value = df[selected_columns].max().max() * 1.1 if selected_columns else 100
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, max_value],
tickfont=dict(size=14, color='black', family=CHINESE_FONT)
),
angularaxis=dict(
tickfont=dict(size=16, color='black', family=CHINESE_FONT),
rotation=20
)
),
showlegend=True,
legend=dict(
font=dict(size=14, color='black', family=CHINESE_FONT),
orientation="v",
yanchor="middle",
xanchor="left",
x=1.2,
y=0.5
),
title=dict(
text=f'{student_name} 成績比較圖' if student_name else '學生成績雷達圖',
font=dict(size=24, family=CHINESE_FONT),
x=0.5,
xanchor='center'
),
margin=dict(t=80, b=120, r=200),
plot_bgcolor='white',
paper_bgcolor='white',
font=dict(family=CHINESE_FONT, size=14)
)
return fig
# ============================
# 生成圖片
# ============================
def generate_radar_image(fig):
"""生成圖片 (重設 Kaleido 配置與更新字型設定)"""
try:
# 重設 kaleido 配置(含 extra_fonts)
pio.kaleido.scope.default_font = CHINESE_FONT
pio.kaleido.scope.default_format = "png"
pio.kaleido.scope.default_width = 1200
pio.kaleido.scope.default_height = 900
pio.kaleido.scope.default_scale = 2
pio.kaleido.scope.extra_fonts = {CHINESE_FONT: font_path}
# 強制更新圖表內所有文字字型設定
fig.update_layout(
font=dict(
family=CHINESE_FONT,
size=14
),
title=dict(
font=dict(
family=CHINESE_FONT,
size=24
)
),
legend=dict(
font=dict(
family=CHINESE_FONT,
size=14
)
),
polar=dict(
radialaxis=dict(
tickfont=dict(
family=CHINESE_FONT,
size=14
)
),
angularaxis=dict(
tickfont=dict(
family=CHINESE_FONT,
size=16
)
)
)
)
return pio.to_image(fig, format="png", engine="kaleido")
except Exception as e:
st.error(f"圖片生成失敗: {str(e)}")
return None
# ============================
# 主程式
# ============================
def main():
st.title('學生成績雷達圖產生器')
st.markdown("""
<style>
.stDownloadButton>button {
background-color: #4CAF50 !important;
color: white !important;
}
.stProgress > div > div > div {
background-color: #4CAF50;
}
</style>
""", unsafe_allow_html=True)
setup_chinese_font() # 檢查中文字型
uploaded_file = st.file_uploader("上傳CSV檔案", type=['csv'])
if uploaded_file is not None:
df, numeric_columns = load_data(uploaded_file)
if df is not None and numeric_columns:
st.write("### 選擇要比較的欄位")
preset_columns = ['平均', '國文', '英文', '數學', '自科', '社會']
use_preset = st.checkbox("使用五科分數與平均比較")
available_preset = [col for col in preset_columns if col in numeric_columns]
selected_columns = st.multiselect(
'選擇科目',
numeric_columns,
default=available_preset if use_preset else numeric_columns[:5]
)
st.write("### 選擇要比較的對象")
all_students = df['姓名'].tolist()
selected_rows = st.multiselect('選擇學生', all_students)
if selected_columns and selected_rows:
try:
fig = create_radar_chart(df, selected_rows, selected_columns)
st.plotly_chart(fig, use_container_width=True)
except Exception as e:
st.error(f"生成雷達圖錯誤:{str(e)}")
st.write("### 批次下載全班學生雷達圖")
comparison_items = st.multiselect("選擇比較基準", all_students)
if comparison_items and selected_columns:
progress_bar = st.progress(0)
image_bytes = {}
current_columns = selected_columns.copy()
current_comparison = comparison_items.copy()
target_students = [s for s in all_students if s not in current_comparison]
for idx, student in enumerate(target_students):
try:
# 動態組合比較組(比較基準加上當前學生)
fig = create_radar_chart(
df,
current_comparison + [student],
current_columns,
student_name=student
)
img = generate_radar_image(fig)
if img:
image_bytes[student] = img
except Exception as e:
st.error(f"生成 {student} 圖表失敗:{str(e)}")
progress_bar.progress((idx + 1) / len(target_students))
if image_bytes:
st.write("### 圖表預覽")
cols = st.columns(3)
for i, (student, img) in enumerate(image_bytes.items()):
with cols[i % 3]:
st.image(img, use_container_width=True)
st.caption(f"{student} 比較圖")
zip_buffer = BytesIO()
with ZipFile(zip_buffer, "w") as zip_file:
for student, img in image_bytes.items():
zip_file.writestr(f"{student}_成績比較圖.png", img)
zip_buffer.seek(0)
st.download_button(
label="⬇️ 下載全部圖表 (ZIP)",
data=zip_buffer,
file_name="學生成績比較圖.zip",
mime="application/zip",
use_container_width=True
)
if __name__ == "__main__":
main()