cwadayi commited on
Commit
769f92b
·
verified ·
1 Parent(s): 659a78d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -78
app.py CHANGED
@@ -10,37 +10,13 @@ USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
10
 
11
  # 自訂 CSS 來美化介面
12
  CUSTOM_CSS = """
13
- /* 美化主標題 */
14
- #main-title {
15
- text-align: center;
16
- font-family: 'Arial Black', sans-serif;
17
- color: #2c3e50;
18
- font-size: 2.5em;
19
- padding-top: 10px;
20
- }
21
- /* 讓控制面板的卡片有陰影和圓角 */
22
- #control-panel {
23
- border-radius: 15px;
24
- background-color: #f8f9fa;
25
- padding: 20px;
26
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
27
- }
28
- /* 結果統計卡片的樣式 */
29
- .stats-card {
30
- border-radius: 10px;
31
- padding: 15px;
32
- text-align: center;
33
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
34
- }
35
  .stats-card h3 { font-size: 1.2em; color: #34495e; }
36
  .stats-card p { font-size: 1.5em; font-weight: bold; color: #e74c3c; }
37
- /* 頁腳樣式 */
38
- #footer {
39
- text-align: center;
40
- color: #7f8c8d;
41
- font-size: 0.9em;
42
- padding: 15px;
43
- }
44
  """
45
 
46
  # --- 核心資料處理函式 ---
@@ -70,7 +46,6 @@ def process_earthquake_data(data):
70
  if df.empty:
71
  return None, "在指定的條件下,找不到任何地震資料。", None
72
 
73
- # --- 製作統計摘要 ---
74
  strongest_quake = df.loc[df['magnitude'].idxmax()]
75
  deepest_quake = df.loc[df['depth'].idxmax()]
76
  stats = {
@@ -82,45 +57,83 @@ def process_earthquake_data(data):
82
  "deepest_loc": deepest_quake['place']
83
  }
84
 
85
- # --- 製作 Markdown 表格 ---
86
  df_display = df.copy()
87
  df_display['time_utc'] = df_display['time_utc'].dt.strftime('%Y-%m-%d %H:%M:%S')
88
  md_table = df_display[['time_utc', 'place', 'magnitude', 'depth']].to_markdown(index=False)
89
 
90
  return df, md_table, stats
91
 
92
- def create_earthquake_map(df):
93
- """使用 Plotly 繪製互動式地震分佈圖"""
 
94
  if df is None or df.empty:
95
  return None
96
 
97
- fig = px.scatter_geo(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  df,
99
- lat='latitude',
100
- lon='longitude',
101
- color='magnitude',
102
- size=df['magnitude'].apply(lambda x: x**2), # 讓規模大小差異更明顯
103
- hover_name='place',
104
- hover_data={'magnitude': ':.2f', 'depth': ':.2f km', 'time_utc': True, 'latitude':False, 'longitude':False},
105
- projection="natural earth",
106
- title="地震分佈圖",
107
- color_continuous_scale=px.colors.sequential.Inferno
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  )
109
- fig.update_layout(margin={"r":0, "t":40, "l":0, "b":0})
110
  return fig
111
 
112
  # --- Gradio 主要��行函式 ---
113
-
114
  def query_and_visualize(
115
  starttime_str: str,
116
  endtime_str: str,
117
  min_magnitude: float,
118
  use_geo: bool,
119
  min_lat: float, max_lat: float,
120
- min_lon: float, max_lon: float
 
121
  ):
122
  """主函式,接收 UI 輸入,回傳所有 UI 更新"""
123
- # 1. 顯示載入狀態
124
  yield {
125
  submit_btn: gr.update(value="⏳ 查詢中...", interactive=False),
126
  map_plot: None,
@@ -135,9 +148,7 @@ def query_and_visualize(
135
  }
136
 
137
  if use_geo:
138
- if not all([min_lat is not None, max_lat is not None, min_lon is not None, max_lon is not None]):
139
- yield { submit_btn: gr.update(value="開始查詢", interactive=True), md_output: "❌ 錯誤:地理範圍四個欄位皆須填寫。" }
140
- return
141
  params.update({"minlatitude": min_lat, "maxlatitude": max_lat, "minlongitude": min_lon, "maxlongitude": max_lon})
142
 
143
  try:
@@ -146,57 +157,52 @@ def query_and_visualize(
146
  df, md_table, stats = process_earthquake_data(response.json())
147
 
148
  if df is not None:
149
- map_figure = create_earthquake_map(df)
150
- # DataFrame 存為 CSV 格式的暫存檔
151
  csv_path = "earthquake_data.csv"
152
  df.to_csv(csv_path, index=False)
153
 
154
  yield {
 
155
  submit_btn: gr.update(value="查詢完成", interactive=True),
156
  map_plot: map_figure,
157
  md_output: md_table,
158
  stats_group: gr.update(visible=True),
159
  download_group: gr.update(visible=True),
160
  file_download: gr.update(value=csv_path, visible=True),
161
- # 更新統計數據
162
  stats_count_val: str(stats['count']),
163
  stats_avg_mag_val: stats['avg_mag'],
164
  stats_strongest_val: f"{stats['strongest_mag']} @ {stats['strongest_loc']}",
165
  stats_deepest_val: f"{stats['deepest_depth']} @ {stats['deepest_loc']}"
166
  }
167
  else:
168
- yield {
169
- submit_btn: gr.update(value="開始查詢", interactive=True),
170
- md_output: "在指定的條件下,找不到任何地震資料。"
171
- }
172
  except Exception as e:
 
173
  yield { submit_btn: gr.update(value="查詢失敗", interactive=True), md_output: f"❌ 發生錯誤: {e}" }
174
 
175
- # 短暫延遲後恢復按鈕文字
176
  time.sleep(2)
177
  yield { submit_btn: gr.update(value="開始查詢", interactive=True) }
178
 
179
  # --- Gradio UI 介面設計 ---
180
-
181
- # --- 💡 修正點:將 'light' 改為 'neutral' ---
182
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="neutral"), css=CUSTOM_CSS) as demo:
183
- # 標題
184
  gr.Markdown("# 🌋 地震視覺化儀 (Quake Visualizer)", elem_id="main-title")
185
 
186
  with gr.Row():
187
- # 左側控制面板
188
  with gr.Column(scale=2, elem_id="control-panel"):
189
  gr.Markdown("### ⚙️ 控制面板")
190
 
191
  with gr.Accordion("時間與規模", open=True):
192
- # 預設查詢時間為過去 24 小時
193
  now = datetime.now()
194
  yesterday = now - timedelta(days=1)
195
  starttime_input = gr.Textbox(label="開始日期 (YYYY-MM-DD)", value=yesterday.strftime('%Y-%m-%d'))
196
  endtime_input = gr.Textbox(label="結束日期 (YYYY-MM-DD)", value=now.strftime('%Y-%m-%d'))
197
  magnitude_slider = gr.Slider(minimum=0, maximum=10, step=0.1, label="最小芮氏規模", value=2.5)
198
-
199
  with gr.Accordion("地理範圍 (可選)", open=False):
 
200
  use_geo_checkbox = gr.Checkbox(label="啟用地理範圍篩選", value=False)
201
  with gr.Row():
202
  min_lat_input = gr.Number(label="最小緯度", value=21.5, interactive=False)
@@ -205,12 +211,27 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="neutral"),
205
  min_lon_input = gr.Number(label="最小經度", value=120.0, interactive=False)
206
  max_lon_input = gr.Number(label="最大經度", value=122.5, interactive=False)
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  submit_btn = gr.Button("開始查詢", variant="primary")
209
 
210
- # 右側結果顯示區
211
  with gr.Column(scale=5):
212
  with gr.Tabs():
213
  with gr.TabItem("📊 視覺化儀表板"):
 
214
  with gr.Row(visible=False) as stats_group:
215
  with gr.Column(elem_classes="stats-card"):
216
  gr.Markdown("<h3>總計地震數</h3>")
@@ -224,7 +245,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="neutral"),
224
  with gr.Column(elem_classes="stats-card"):
225
  gr.Markdown("<h3>最深地震</h3>")
226
  stats_deepest_val = gr.Markdown("<p>-</p>")
227
-
228
  map_plot = gr.Plot()
229
 
230
  with gr.Row(visible=False) as download_group:
@@ -233,30 +254,28 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="neutral"),
233
  with gr.TabItem("📄 詳細數據"):
234
  md_output = gr.Markdown("點擊「開始查詢」後,結果將顯示於此。")
235
 
 
236
  gr.Markdown("---", elem_id="footer")
237
- gr.Markdown("由 Gradio 與 Plotly 強力驅動 | 資料來源:美國地質調查局 (USGS)", elem_id="footer")
238
 
239
  # --- 事件監聽與互動邏輯 ---
 
240
  def toggle_geo_inputs(enabled):
241
  return {
242
  min_lat_input: gr.update(interactive=enabled), max_lat_input: gr.update(interactive=enabled),
243
  min_lon_input: gr.update(interactive=enabled), max_lon_input: gr.update(interactive=enabled)
244
  }
245
  use_geo_checkbox.change(fn=toggle_geo_inputs, inputs=use_geo_checkbox, outputs=[min_lat_input, max_lat_input, min_lon_input, max_lon_input])
246
-
 
 
 
 
247
  # 綁定主查詢函式
248
- submit_btn.click(
249
- fn=query_and_visualize,
250
- inputs=[starttime_input, endtime_input, magnitude_slider, use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input],
251
- outputs=[submit_btn, map_plot, md_output, stats_group, download_group, file_download, stats_count_val, stats_avg_mag_val, stats_strongest_val, stats_deepest_val]
252
- )
253
 
254
  # 頁面載入時自動觸發一次查詢
255
- demo.load(
256
- fn=query_and_visualize,
257
- inputs=[starttime_input, endtime_input, magnitude_slider, use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input],
258
- outputs=[submit_btn, map_plot, md_output, stats_group, download_group, file_download, stats_count_val, stats_avg_mag_val, stats_strongest_val, stats_deepest_val]
259
- )
260
 
261
  if __name__ == "__main__":
262
  demo.launch()
 
10
 
11
  # 自訂 CSS 來美化介面
12
  CUSTOM_CSS = """
13
+ /* ... (CSS 樣式與之前相同,為節省空間此處省略) ... */
14
+ #main-title { text-align: center; font-family: 'Arial Black', sans-serif; color: #2c3e50; font-size: 2.5em; padding-top: 10px; }
15
+ #control-panel { border-radius: 15px; background-color: #f8f9fa; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
16
+ .stats-card { border-radius: 10px; padding: 15px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  .stats-card h3 { font-size: 1.2em; color: #34495e; }
18
  .stats-card p { font-size: 1.5em; font-weight: bold; color: #e74c3c; }
19
+ #footer { text-align: center; color: #7f8c8d; font-size: 0.9em; padding: 15px; }
 
 
 
 
 
 
20
  """
21
 
22
  # --- 核心資料處理函式 ---
 
46
  if df.empty:
47
  return None, "在指定的條件下,找不到任何地震資料。", None
48
 
 
49
  strongest_quake = df.loc[df['magnitude'].idxmax()]
50
  deepest_quake = df.loc[df['depth'].idxmax()]
51
  stats = {
 
57
  "deepest_loc": deepest_quake['place']
58
  }
59
 
 
60
  df_display = df.copy()
61
  df_display['time_utc'] = df_display['time_utc'].dt.strftime('%Y-%m-%d %H:%M:%S')
62
  md_table = df_display[['time_utc', 'place', 'magnitude', 'depth']].to_markdown(index=False)
63
 
64
  return df, md_table, stats
65
 
66
+ # --- 🎨 全新升級的地圖函式 ---
67
+ def create_earthquake_map(df, map_style):
68
+ """使用 Plotly 繪製更 Fancy 的互動式地震分佈圖"""
69
  if df is None or df.empty:
70
  return None
71
 
72
+ # 建立一個包含豐富 HTML 格式的懸停資訊欄位
73
+ df['hover_text'] = df.apply(
74
+ lambda row: (
75
+ f"<b>📍 {row['place']}</b><br><br>"
76
+ f"<b>時間:</b> {row['time_utc'].strftime('%Y-%m-%d %H:%M:%S')} UTC<br>"
77
+ f"<b>芮氏規模:</b> {row['magnitude']:.2f}<br>"
78
+ f"<b>深度:</b> {row['depth']:.2f} km<br>"
79
+ f"<a href='{row['url']}' target='_blank'>查看 USGS 詳細報告</a>"
80
+ ),
81
+ axis=1
82
+ )
83
+
84
+ # 設置地圖中心點
85
+ center_lat = df['latitude'].mean()
86
+ center_lon = df['longitude'].mean()
87
+
88
+ # 使用 px.scatter_mapbox 來獲得更豐富的底圖
89
+ fig = px.scatter_mapbox(
90
  df,
91
+ lat="latitude",
92
+ lon="longitude",
93
+ size="magnitude", # 點的大小與規模相關
94
+ color="magnitude", # 點的顏色也與規模相關
95
+ hover_name="hover_text", # 使用我們自訂的 HTML 懸停文字
96
+ hover_data={"hover_text": False, "latitude": False, "longitude": False, "magnitude": False}, # 隱藏預設的懸停資料
97
+ color_continuous_scale=px.colors.sequential.YlOrRd, # 使用黃-橙-紅的火焰色階
98
+ size_max=50, # 最大的點的尺寸
99
+ zoom=3, # 初始縮放等級
100
+ center={"lat": center_lat, "lon": center_lon},
101
+ mapbox_style=map_style # 從 UI 傳入的地圖樣式!
102
+ )
103
+
104
+ fig.update_layout(
105
+ title={
106
+ 'text': "<b>地震分佈視覺化儀表板</b>",
107
+ 'y':0.95,
108
+ 'x':0.5,
109
+ 'xanchor': 'center',
110
+ 'yanchor': 'top'
111
+ },
112
+ margin={"r": 0, "t": 40, "l": 0, "b": 0},
113
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
114
+ mapbox=dict(
115
+ bearing=0,
116
+ pitch=0
117
+ ),
118
+ # 移除 colorbar 的標題,讓介面更乾淨
119
+ coloraxis_colorbar=dict(
120
+ title=""
121
+ )
122
  )
123
+
124
  return fig
125
 
126
  # --- Gradio 主要��行函式 ---
 
127
  def query_and_visualize(
128
  starttime_str: str,
129
  endtime_str: str,
130
  min_magnitude: float,
131
  use_geo: bool,
132
  min_lat: float, max_lat: float,
133
+ min_lon: float, max_lon: float,
134
+ map_style: str # 新增地圖樣式參數
135
  ):
136
  """主函式,接收 UI 輸入,回傳所有 UI 更新"""
 
137
  yield {
138
  submit_btn: gr.update(value="⏳ 查詢中...", interactive=False),
139
  map_plot: None,
 
148
  }
149
 
150
  if use_geo:
151
+ # ... (驗證邏輯與之前相同) ...
 
 
152
  params.update({"minlatitude": min_lat, "maxlatitude": max_lat, "minlongitude": min_lon, "maxlongitude": max_lon})
153
 
154
  try:
 
157
  df, md_table, stats = process_earthquake_data(response.json())
158
 
159
  if df is not None:
160
+ # 傳入 map_style
161
+ map_figure = create_earthquake_map(df, map_style)
162
  csv_path = "earthquake_data.csv"
163
  df.to_csv(csv_path, index=False)
164
 
165
  yield {
166
+ # ... (其他元件更新與之前相同) ...
167
  submit_btn: gr.update(value="查詢完成", interactive=True),
168
  map_plot: map_figure,
169
  md_output: md_table,
170
  stats_group: gr.update(visible=True),
171
  download_group: gr.update(visible=True),
172
  file_download: gr.update(value=csv_path, visible=True),
 
173
  stats_count_val: str(stats['count']),
174
  stats_avg_mag_val: stats['avg_mag'],
175
  stats_strongest_val: f"{stats['strongest_mag']} @ {stats['strongest_loc']}",
176
  stats_deepest_val: f"{stats['deepest_depth']} @ {stats['deepest_loc']}"
177
  }
178
  else:
179
+ # ... (錯誤處理與之前相同) ...
180
+ yield { submit_btn: gr.update(value="開始查詢", interactive=True), md_output: "在指定的條件下,找不到任何地震資料。" }
 
 
181
  except Exception as e:
182
+ # ... (錯誤處理與之前相同) ...
183
  yield { submit_btn: gr.update(value="查詢失敗", interactive=True), md_output: f"❌ 發生錯誤: {e}" }
184
 
 
185
  time.sleep(2)
186
  yield { submit_btn: gr.update(value="開始查詢", interactive=True) }
187
 
188
  # --- Gradio UI 介面設計 ---
 
 
189
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="neutral"), css=CUSTOM_CSS) as demo:
 
190
  gr.Markdown("# 🌋 地震視覺化儀 (Quake Visualizer)", elem_id="main-title")
191
 
192
  with gr.Row():
 
193
  with gr.Column(scale=2, elem_id="control-panel"):
194
  gr.Markdown("### ⚙️ 控制面板")
195
 
196
  with gr.Accordion("時間與規模", open=True):
197
+ # ... (時間與規模元件與之前相同) ...
198
  now = datetime.now()
199
  yesterday = now - timedelta(days=1)
200
  starttime_input = gr.Textbox(label="開始日期 (YYYY-MM-DD)", value=yesterday.strftime('%Y-%m-%d'))
201
  endtime_input = gr.Textbox(label="結束日期 (YYYY-MM-DD)", value=now.strftime('%Y-%m-%d'))
202
  magnitude_slider = gr.Slider(minimum=0, maximum=10, step=0.1, label="最小芮氏規模", value=2.5)
203
+
204
  with gr.Accordion("地理範圍 (可選)", open=False):
205
+ # ... (地理範圍元件與之前相同) ...
206
  use_geo_checkbox = gr.Checkbox(label="啟用地理範圍篩選", value=False)
207
  with gr.Row():
208
  min_lat_input = gr.Number(label="最小緯度", value=21.5, interactive=False)
 
211
  min_lon_input = gr.Number(label="最小經度", value=120.0, interactive=False)
212
  max_lon_input = gr.Number(label="最大經度", value=122.5, interactive=False)
213
 
214
+ # --- ✨ 新增地圖樣式選擇器 ---
215
+ with gr.Accordion("地圖樣式", open=True):
216
+ map_style_dropdown = gr.Dropdown(
217
+ label="選擇地圖底圖",
218
+ choices=[
219
+ "open-street-map",
220
+ "carto-darkmatter",
221
+ "carto-positron",
222
+ "stamen-terrain",
223
+ "stamen-toner",
224
+ "stamen-watercolor"
225
+ ],
226
+ value="carto-darkmatter" # 預設使用深色模式
227
+ )
228
+
229
  submit_btn = gr.Button("開始查詢", variant="primary")
230
 
 
231
  with gr.Column(scale=5):
232
  with gr.Tabs():
233
  with gr.TabItem("📊 視覺化儀表板"):
234
+ # ... (統計卡片與下載元件與之前相同) ...
235
  with gr.Row(visible=False) as stats_group:
236
  with gr.Column(elem_classes="stats-card"):
237
  gr.Markdown("<h3>總計地震數</h3>")
 
245
  with gr.Column(elem_classes="stats-card"):
246
  gr.Markdown("<h3>最深地震</h3>")
247
  stats_deepest_val = gr.Markdown("<p>-</p>")
248
+
249
  map_plot = gr.Plot()
250
 
251
  with gr.Row(visible=False) as download_group:
 
254
  with gr.TabItem("📄 詳細數據"):
255
  md_output = gr.Markdown("點擊「開始查詢」後,結果將顯示於此。")
256
 
257
+ # ... (頁腳與之前相同) ...
258
  gr.Markdown("---", elem_id="footer")
259
+ gr.Markdown("由 Gradio 與 Plotly 強力驅動 | 資料來源:美國地制調查局 (USGS)", elem_id="footer")
260
 
261
  # --- 事件監聽與互動邏輯 ---
262
+ # ... (地理範圍鎖定邏輯與之前相同) ...
263
  def toggle_geo_inputs(enabled):
264
  return {
265
  min_lat_input: gr.update(interactive=enabled), max_lat_input: gr.update(interactive=enabled),
266
  min_lon_input: gr.update(interactive=enabled), max_lon_input: gr.update(interactive=enabled)
267
  }
268
  use_geo_checkbox.change(fn=toggle_geo_inputs, inputs=use_geo_checkbox, outputs=[min_lat_input, max_lat_input, min_lon_input, max_lon_input])
269
+
270
+ # 組合所有輸入元件
271
+ all_inputs = [starttime_input, endtime_input, magnitude_slider, use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input, map_style_dropdown]
272
+ all_outputs = [submit_btn, map_plot, md_output, stats_group, download_group, file_download, stats_count_val, stats_avg_mag_val, stats_strongest_val, stats_deepest_val]
273
+
274
  # 綁定主查詢函式
275
+ submit_btn.click(fn=query_and_visualize, inputs=all_inputs, outputs=all_outputs)
 
 
 
 
276
 
277
  # 頁面載入時自動觸發一次查詢
278
+ demo.load(fn=query_and_visualize, inputs=all_inputs, outputs=all_outputs)
 
 
 
 
279
 
280
  if __name__ == "__main__":
281
  demo.launch()