Spaces:
Build error
Build error
File size: 10,232 Bytes
73ecabe e1707ed c6eaf85 e1707ed c6eaf85 70fa45e 75481e3 c6eaf85 e1707ed c6eaf85 73ecabe 3d61168 c6eaf85 75481e3 c6eaf85 75481e3 a3ea68d c6eaf85 73ecabe c6eaf85 e1707ed c6eaf85 e1707ed 73ecabe c6eaf85 75481e3 c6eaf85 e1707ed a3ea68d 3234c7a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
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() |