Lashtw commited on
Commit
73ecabe
·
verified ·
1 Parent(s): 3234c7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -251
app.py CHANGED
@@ -1,279 +1,142 @@
1
- import streamlit as st
 
2
  import pandas as pd
3
- import plotly.graph_objects as go
4
- import plotly.io as pio
5
- from zipfile import ZipFile
6
  from io import BytesIO
7
- import matplotlib.pyplot as plt
8
  import os
9
- import matplotlib.font_manager as fm
10
 
11
- # ============================
12
- # 載入本地字型檔與設定
13
- # ============================
14
-
15
- font_path = "NotoSansTC-Regular.ttf" # 確認此字型檔路徑正確
16
- if os.path.exists(font_path):
17
- fm.fontManager.addfont(font_path)
18
- font_prop = fm.FontProperties(fname=font_path)
19
- CHINESE_FONT = font_prop.get_name() # 例如 "Noto Sans TC"
20
- else:
21
- st.error(f"找不到字型檔: {font_path}")
22
- CHINESE_FONT = "Noto Sans TC" # fallback
23
-
24
- # 配置 Kaleido 的預設參數與額外字型設定
25
- pio.kaleido.scope.default_font = CHINESE_FONT
26
- pio.kaleido.scope.default_format = "png"
27
- pio.kaleido.scope.default_width = 1200
28
- pio.kaleido.scope.default_height = 900
29
- pio.kaleido.scope.default_scale = 2
30
- pio.kaleido.scope.extra_fonts = {CHINESE_FONT: font_path}
31
-
32
- # ============================
33
- # 系統字型檢查與安裝
34
- # ============================
35
 
36
  def setup_chinese_font():
37
- """檢查系統中文字型是否可用,若無則嘗試安裝"""
38
- try:
39
- test_fig, ax = plt.subplots()
40
- ax.text(0.5, 0.5, "中文測試", fontfamily=CHINESE_FONT)
41
- test_fig.savefig("font_test.png")
42
- plt.close(test_fig)
43
- except Exception as e:
44
- st.warning("正在安裝中文字型...")
45
- os.system('apt-get update && apt-get install -y --force-yes fonts-noto-cjk')
46
- st.experimental_rerun()
47
 
48
- # ============================
49
- # 資料讀取
50
- # ============================
51
 
52
- def load_data(uploaded_file):
53
- """從 CSV 檔案中讀取資料"""
54
- try:
55
- df = pd.read_csv(uploaded_file, encoding='utf-8').dropna(how='all')
56
- if '姓名' not in df.columns:
57
- raise ValueError("CSV檔案中缺少必要欄位:姓名")
58
- numeric_columns = []
59
- potential_numeric = ['平均', '總分', '國文', '英文', '數學', '自科',
60
- '社會', '地理', '歷史', '公民', '物理', '化學', '生物']
61
- for col in potential_numeric:
62
- if col in df.columns:
63
- try:
64
- df[col] = pd.to_numeric(df[col], errors='coerce')
65
- numeric_columns.append(col)
66
- except:
67
- pass
68
- if not numeric_columns:
69
- raise ValueError("CSV檔案中未找到有效的數值欄位")
70
- return df, numeric_columns
71
- except Exception as e:
72
- st.error(f"數據加載錯誤:{str(e)}")
73
- return None, None
74
 
75
- # ============================
76
- # 生成雷達圖
77
- # ============================
 
 
 
78
 
79
- def create_radar_chart(df, selected_rows, selected_columns, student_name=""):
80
- """生成雷達圖 (包含圖例、極座標軸等設定)"""
81
- fig = go.Figure()
82
- line_styles = ['solid', 'dot', 'dash', 'longdash', 'dashdot']
83
- colors = ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD']
84
-
85
- for i, row_name in enumerate(selected_rows):
86
- row_data = df[df['姓名'] == row_name][selected_columns].iloc[0]
87
- fig.add_trace(go.Scatterpolar(
88
- r=row_data.values,
89
- theta=selected_columns,
90
- fill='toself',
91
- name=row_name,
92
- line=dict(
93
- color=colors[i % len(colors)],
94
- dash=line_styles[i % len(line_styles)],
95
- width=2
96
- ),
97
- marker=dict(opacity=0.5)
98
- ))
99
-
100
- max_value = df[selected_columns].max().max() * 1.1 if selected_columns else 100
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  fig.update_layout(
103
  polar=dict(
104
- radialaxis=dict(
105
- visible=True,
106
- range=[0, max_value],
107
- tickfont=dict(size=14, color='black', family=CHINESE_FONT)
108
- ),
109
- angularaxis=dict(
110
- tickfont=dict(size=16, color='black', family=CHINESE_FONT),
111
- rotation=20
112
- )
113
- ),
114
- showlegend=True,
115
- legend=dict(
116
- font=dict(size=14, color='black', family=CHINESE_FONT),
117
- orientation="v",
118
- yanchor="middle",
119
- xanchor="left",
120
- x=1.2,
121
- y=0.5
122
  ),
123
- title=dict(
124
- text=f'{student_name} 成績比較圖' if student_name else '學生成績雷達圖',
125
- font=dict(size=24, family=CHINESE_FONT),
126
- x=0.5,
127
- xanchor='center'
128
- ),
129
- margin=dict(t=80, b=120, r=200),
130
- plot_bgcolor='white',
131
- paper_bgcolor='white',
132
- font=dict(family=CHINESE_FONT, size=14)
133
  )
134
- return fig
135
 
136
- # ============================
137
- # 生成圖片
138
- # ============================
139
 
140
  def generate_radar_image(fig):
141
- """生成圖片 (重設 Kaleido 配置與更新字型設定)"""
142
- try:
143
- # 重設 kaleido 配置(含 extra_fonts)
144
- pio.kaleido.scope.default_font = CHINESE_FONT
145
- pio.kaleido.scope.default_format = "png"
146
- pio.kaleido.scope.default_width = 1200
147
- pio.kaleido.scope.default_height = 900
148
- pio.kaleido.scope.default_scale = 2
149
- pio.kaleido.scope.extra_fonts = {CHINESE_FONT: font_path}
150
 
151
- # 強制更新圖表內所有文字字型設定
152
- fig.update_layout(
153
- font=dict(
154
- family=CHINESE_FONT,
155
- size=14
156
- ),
157
- title=dict(
158
- font=dict(
159
- family=CHINESE_FONT,
160
- size=24
161
- )
162
- ),
163
- legend=dict(
164
- font=dict(
165
- family=CHINESE_FONT,
166
- size=14
167
- )
168
- ),
169
- polar=dict(
170
- radialaxis=dict(
171
- tickfont=dict(
172
- family=CHINESE_FONT,
173
- size=14
174
- )
175
- ),
176
- angularaxis=dict(
177
- tickfont=dict(
178
- family=CHINESE_FONT,
179
- size=16
180
- )
181
- )
182
- )
183
- )
184
 
185
- return pio.to_image(fig, format="png", engine="kaleido")
 
 
 
 
 
 
186
  except Exception as e:
187
- st.error(f"圖片生成失敗: {str(e)}")
188
  return None
189
 
190
- # ============================
191
- # 主程式
192
- # ============================
193
-
194
  def main():
195
- st.title('學生成績雷達圖產生器')
196
- st.markdown("""
197
- <style>
198
- .stDownloadButton>button {
199
- background-color: #4CAF50 !important;
200
- color: white !important;
201
- }
202
- .stProgress > div > div > div {
203
- background-color: #4CAF50;
204
- }
205
- </style>
206
- """, unsafe_allow_html=True)
207
-
208
- setup_chinese_font() # 檢查中文字型
209
-
210
- uploaded_file = st.file_uploader("上傳CSV檔案", type=['csv'])
211
- if uploaded_file is not None:
212
- df, numeric_columns = load_data(uploaded_file)
213
- if df is not None and numeric_columns:
214
- st.write("### 選擇要比較的欄位")
215
- preset_columns = ['平均', '國文', '英文', '數學', '自科', '社會']
216
- use_preset = st.checkbox("使用五科分數與平均比較")
217
- available_preset = [col for col in preset_columns if col in numeric_columns]
218
- selected_columns = st.multiselect(
219
- '選擇科目',
220
- numeric_columns,
221
- default=available_preset if use_preset else numeric_columns[:5]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  )
223
 
224
- st.write("### 選擇要比較的對象")
225
- all_students = df['姓名'].tolist()
226
- selected_rows = st.multiselect('選擇學生', all_students)
227
-
228
- if selected_columns and selected_rows:
229
- try:
230
- fig = create_radar_chart(df, selected_rows, selected_columns)
231
- st.plotly_chart(fig, use_container_width=True)
232
- except Exception as e:
233
- st.error(f"生成雷達圖錯誤:{str(e)}")
234
-
235
- st.write("### 批次下載全班學生雷達圖")
236
- comparison_items = st.multiselect("選擇比較基準", all_students)
237
- if comparison_items and selected_columns:
238
- progress_bar = st.progress(0)
239
- image_bytes = {}
240
- current_columns = selected_columns.copy()
241
- current_comparison = comparison_items.copy()
242
- target_students = [s for s in all_students if s not in current_comparison]
243
- for idx, student in enumerate(target_students):
244
- try:
245
- # 動態組合比較組(比較基準加上當前學生)
246
- fig = create_radar_chart(
247
- df,
248
- current_comparison + [student],
249
- current_columns,
250
- student_name=student
251
- )
252
- img = generate_radar_image(fig)
253
- if img:
254
- image_bytes[student] = img
255
- except Exception as e:
256
- st.error(f"生成 {student} 圖表失敗:{str(e)}")
257
- progress_bar.progress((idx + 1) / len(target_students))
258
- if image_bytes:
259
- st.write("### 圖表預覽")
260
- cols = st.columns(3)
261
- for i, (student, img) in enumerate(image_bytes.items()):
262
- with cols[i % 3]:
263
- st.image(img, use_container_width=True)
264
- st.caption(f"{student} 比較圖")
265
- zip_buffer = BytesIO()
266
- with ZipFile(zip_buffer, "w") as zip_file:
267
- for student, img in image_bytes.items():
268
- zip_file.writestr(f"{student}_成績比較圖.png", img)
269
- zip_buffer.seek(0)
270
- st.download_button(
271
- label="⬇️ 下載全部圖表 (ZIP)",
272
- data=zip_buffer,
273
- file_name="學生成績比較圖.zip",
274
- mime="application/zip",
275
- use_container_width=True
276
- )
277
-
278
  if __name__ == "__main__":
279
  main()
 
1
+ import plotly.express as px
2
+ from plotly import graph_objects as go
3
  import pandas as pd
4
+ import streamlit as st
 
 
5
  from io import BytesIO
6
+ from zipfile import ZipFile
7
  import os
 
8
 
9
+ CHINESE_FONT = "SimHei"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  def setup_chinese_font():
12
+ """设置中文字体,确保图表中的中文显示正常"""
13
+ px.defaults.template = "plotly_white"
14
+ go.FigureWidget._default_font = {"family": CHINESE_FONT}
 
 
 
 
 
 
 
15
 
16
+ def create_radar_chart(df, students, subjects):
17
+ """
18
+ 创建雷达图
19
 
20
+ Args:
21
+ df: 数据框
22
+ students: 学生列表
23
+ subjects: 科目列表
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ Returns:
26
+ plotly figure 对象
27
+ """
28
+ # 筛选指定学生和科目
29
+ filtered_df = df[df['姓名'].isin(students)]
30
+ filtered_df = filtered_df[subjects]
31
 
32
+ # 计算每个学生的平均分
33
+ avg_scores = filtered_df.mean()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ fig = go.Figure()
36
+ for student in students:
37
+ student_data = df[df['姓名'] == student][subjects]
38
+ if not student_data.empty:
39
+ scores = student_data.values.flatten().tolist()
40
+ fig.add_trace(go.Scatterpolar(
41
+ r=scores,
42
+ theta=subjects,
43
+ name=f"{student} ({avg_scores[student]:.1f})",
44
+ mode='lines+markers'
45
+ ))
46
+
47
+ # 设置图表样式
48
  fig.update_layout(
49
  polar=dict(
50
+ radialaxis=dict(range=[0, 100], showline=True),
51
+ angularaxis=dict(direction="clockwise")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  ),
53
+ title=f"学生成绩对比 - {', '.join(students)}",
54
+ font=dict(family=CHINESE_FONT)
 
 
 
 
 
 
 
 
55
  )
 
56
 
57
+ return fig
 
 
58
 
59
  def generate_radar_image(fig):
60
+ """
61
+ 生成图表的图像数据
 
 
 
 
 
 
 
62
 
63
+ Args:
64
+ fig: plotly figure 对象
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ Returns:
67
+ 图像数据 bytes
68
+ """
69
+ try:
70
+ # 使用 kaleido 转换为 PNG 格式
71
+ img_bytes = fig.to_image(format="png", engine="kaleido")
72
+ return img_bytes
73
  except Exception as e:
74
+ st.error(f"生成图表时出错: {str(e)}")
75
  return None
76
 
 
 
 
 
77
  def main():
78
+ # 设置页面标题和宽屏模式
79
+ st.set_page_config(page_title="学生成绩分析工具", layout="wide")
80
+
81
+ # 设置中文字体
82
+ setup_chinese_font()
83
+
84
+ # 上传数据
85
+ with st.expander("上传成绩数据"):
86
+ uploaded_file = st.file_uploader("上传Excel文件", type=['xlsx', 'xls'])
87
+ if uploaded_file is not None:
88
+ try:
89
+ df = pd.read_excel(uploaded_file)
90
+ st.success("数据上传成功!")
91
+ except Exception as e:
92
+ st.error(f"数据读取失败: {str(e)}")
93
+ return
94
+ else:
95
+ st.info("请上传包含学生成绩的Excel文件。")
96
+ return
97
+
98
+ # 选择学生和科目
99
+ with st.expander("选择学生和科目"):
100
+ students = df['姓名'].unique().tolist()
101
+ selected_students = st.multiselect("选择学生", students)
102
+ subjects = [col for col in df.columns if col != '姓名']
103
+ selected_subjects = st.multiselect("选择科目", subjects, default=subjects)
104
+
105
+ # 生成图表
106
+ with st.expander("生成雷达图"):
107
+ if not selected_students or not selected_subjects:
108
+ st.warning("请选择至少一个学生和一个科目。")
109
+ return
110
+
111
+ fig = create_radar_chart(df, selected_students, selected_subjects)
112
+ st.plotly_chart(fig)
113
+
114
+ # 批量下载功能
115
+ with st.expander("批量下载图表"):
116
+ if st.button("生成并下载所有图表"):
117
+ # 创建临时文件夹存储图表
118
+ temp_dir = "temp_charts"
119
+ os.makedirs(temp_dir, exist_ok=True)
120
+
121
+ # 为每个学生生成图表并保存
122
+ zip_buffer = BytesIO()
123
+ with ZipFile(zip_buffer, mode="w") as zf:
124
+ for student in selected_students:
125
+ fig_student = create_radar_chart(df, [student], selected_subjects)
126
+ img_bytes = generate_radar_image(fig_student)
127
+ if img_bytes is not None:
128
+ file_path = os.path.join(temp_dir, f"{student}.png")
129
+ with open(file_path, "wb") as f:
130
+ f.write(img_bytes)
131
+ zf.write(file_path, os.path.basename(file_path))
132
+
133
+ # 下载Zip文件
134
+ st.download_button(
135
+ label="下载所有图表",
136
+ data=zip_buffer.getvalue(),
137
+ file_name="student_charts.zip",
138
+ mime="application/zip"
139
  )
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  if __name__ == "__main__":
142
  main()