cwadayi commited on
Commit
970159f
·
verified ·
1 Parent(s): f161bb9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -108
app.py CHANGED
@@ -93,30 +93,36 @@ def _finalize_time(df: pd.DataFrame) -> pd.DataFrame:
93
 
94
  def load_csv(file: gr.File | None) -> pd.DataFrame:
95
  """讀上傳 CSV"""
96
- df = pd.read_csv(file.name)
97
- df = _finalize_time(df)
98
- # 若無 lat/lon,補隨機(避免地圖空白)
99
- if "lat" not in df.columns or "lon" not in df.columns:
100
- n = len(df)
101
- df["lat"] = np.random.uniform(21.8, 25.3, size=n)
102
- df["lon"] = np.random.uniform(120.0, 122.0, size=n)
103
- if "pid" not in df.columns:
104
- df["pid"] = np.arange(len(df))
105
- return df
 
 
 
106
 
107
 
108
  def load_drive_csv(sheet_or_file_url: str) -> pd.DataFrame:
109
  """從 Google Sheets 或 Google Drive File 讀 CSV"""
110
- url = normalize_drive_url(sheet_or_file_url)
111
- df = pd.read_csv(url)
112
- df = _finalize_time(df)
113
- if "lat" not in df.columns or "lon" not in df.columns:
114
- n = len(df)
115
- df["lat"] = np.random.uniform(21.8, 25.3, size=n)
116
- df["lon"] = np.random.uniform(120.0, 122.0, size=n)
117
- if "pid" not in df.columns:
118
- df["pid"] = np.arange(len(df))
119
- return df
 
 
 
120
 
121
 
122
  def load_data(source: str, file: gr.File | None = None, sheet_url: str = "") -> pd.DataFrame:
@@ -133,6 +139,18 @@ def load_data(source: str, file: gr.File | None = None, sheet_url: str = "") ->
133
  return make_demo_dataframe()
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  # -----------------------------
137
  # grafanalib JSON builder
138
  # -----------------------------
@@ -266,6 +284,8 @@ def render_map_folium(
266
  cmap_name: str = "viridis",
267
  tiles: str = "OpenStreetMap",
268
  ) -> str:
 
 
269
  center_lat, center_lon = df["lat"].mean(), df["lon"].mean()
270
  m = folium.Map(location=[center_lat, center_lon], zoom_start=7, tiles=tiles)
271
 
@@ -328,46 +348,60 @@ def pick_detail(df: pd.DataFrame, choice: str) -> pd.DataFrame:
328
  # -----------------------------
329
  # Main pipeline
330
  # -----------------------------
331
- def pipeline(source, file, sheet_url, series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice):
332
- df = load_data(source, file, sheet_url)
333
- numeric_cols = [c for c in df.columns if c not in ["time", "lat", "lon", "pid"] and pd.api.types.is_numeric_dtype(df[c])]
334
- chosen = [c for c in (series_choice or numeric_cols[:2]) if c in numeric_cols]
335
- if not chosen:
336
- chosen = numeric_cols[:2]
337
-
338
- dash_json = build_grafanalib_dashboard(chosen, bool(dual_axis), int(rolling_window))
339
- dash_json_str = json.dumps(dash_json, ensure_ascii=False, indent=2, default=str)
340
- with tempfile.NamedTemporaryFile(delete=False, suffix=".json", mode="w", encoding="utf-8") as f:
341
- f.write(dash_json_str)
342
- json_path = f.name
343
-
344
- fig1 = render_line(df, chosen[0])
345
- fig2 = render_bar_or_dual(df, chosen[1], chosen[0], bool(dual_axis)) if len(chosen) > 1 else plt.figure()
346
- fig3, df_with_roll = render_rolling(df.copy(), chosen[0], int(rolling_window))
347
-
348
- map_html = render_map_folium(df, value_col=chosen[0], size_col="count",
349
- cmap_name=cmap_choice, tiles=tiles_choice)
350
-
351
- point_choices = make_point_choices(df)
352
- default_choice = point_choices[0] if point_choices else ""
353
- detail_df = pick_detail(df, default_choice)
354
-
355
- demo_df = make_demo_dataframe()
356
- with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="w", encoding="utf-8") as f:
357
- demo_df.to_csv(f, index=False)
358
- demo_csv_path = f.name
359
-
360
- return (
361
- fig1, fig2, fig3, map_html,
362
- dash_json_str, json_path, df_with_roll,
363
- demo_csv_path,
364
- gr.Dropdown(choices=point_choices, value=default_choice),
365
- detail_df,
366
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
 
369
- def regenerate_demo(series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice, current_choice):
370
- return pipeline("demo", None, "", series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice)
371
 
372
 
373
  def update_detail(df: pd.DataFrame, choice: str):
@@ -375,93 +409,117 @@ def update_detail(df: pd.DataFrame, choice: str):
375
 
376
 
377
  # -----------------------------
378
- # UI(將 Google 來源改成只有下拉選單)
379
  # -----------------------------
380
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
381
- gr.Markdown("## Grafana-like Demo + Folium Map(支援 Google Drive / Sheets,下拉選單選擇來源)")
382
-
383
- source_radio = gr.Radio(["upload", "drive", "demo"], label="資料來源", value="demo")
384
- file_in = gr.File(label="上傳 CSV(選 upload 時使用)", file_types=[".csv"])
385
 
386
- # 只保留下拉選單,不再顯示可編輯的文字框
387
- preset_dd = gr.Dropdown(
388
- label="Google 預設來源(3 個連結)",
389
- choices=DRIVE_PRESETS,
390
- value=DRIVE_PRESETS[0]
391
- )
392
-
393
- series_multiselect = gr.CheckboxGroup(label="數值欄位", choices=[])
394
- dual_axis_chk = gr.Checkbox(label="第二面板啟用雙軸", value=False)
395
- rolling_dd = gr.Dropdown(label="Rolling window", choices=["3", "5", "10", "20"], value="5")
396
- cmap_dd = gr.Dropdown(label="地圖配色 (colormap)",
397
- choices=["viridis", "plasma", "inferno", "magma", "cividis", "coolwarm"],
398
- value="viridis")
399
- tiles_dd = gr.Dropdown(label="地圖底圖 (tiles)",
400
- choices=["OpenStreetMap", "Stamen Terrain", "Stamen Toner",
401
- "CartoDB positron", "CartoDB dark_matter"],
402
- value="OpenStreetMap")
 
 
 
 
 
 
 
403
 
404
  with gr.Row():
405
  run_btn = gr.Button("產生 Dashboard", scale=1)
406
  regen_btn = gr.Button("🔁 重新產生示範資料", scale=1)
407
 
408
- plot1 = gr.Plot(label="1:Line")
409
- plot2 = gr.Plot(label="2:Bar / Dual Axis")
410
- plot3 = gr.Plot(label="3:Rolling Mean")
411
- map_out = gr.HTML(label="4:Geo Map (Interactive + Legend)")
412
 
413
- json_box = gr.Code(label="grafanalib Dashboard JSON", language="json")
414
- json_file = gr.File(label="下載 dashboard.json")
415
- demo_csv_file = gr.File(label="下載示範資料 demo.csv")
416
- df_view = gr.Dataframe(label="資料預覽(含 rolling)", wrap=True)
 
 
417
 
418
- gr.Markdown("### 🔎 點位詳情(對應地圖彈窗中的 #ID)")
419
- point_selector = gr.Dropdown(label="選擇點位(#ID | 時間 | 值)", choices=[], value=None)
420
- detail_view = gr.Dataframe(label="選取點詳細資料", wrap=True)
421
 
422
- # 根據來源探勘欄位(drive 時讀取下拉的 URL)
423
- def probe_columns(source, file, preset_url):
424
- sheet_url = preset_url if source == "drive" else ""
425
- df = load_data(source, file, sheet_url)
426
- numeric_cols = [c for c in df.columns if c not in ["time", "lat", "lon", "pid"] and pd.api.types.is_numeric_dtype(df[c])]
427
- return gr.CheckboxGroup(choices=numeric_cols, value=numeric_cols[:2]), df
 
428
 
429
- source_radio.change(probe_columns, inputs=[source_radio, file_in, preset_dd], outputs=[series_multiselect, df_view])
430
- file_in.change(probe_columns, inputs=[source_radio, file_in, preset_dd], outputs=[series_multiselect, df_view])
431
- preset_dd.change(probe_columns, inputs=[source_radio, file_in, preset_dd], outputs=[series_multiselect, df_view])
 
432
 
433
- # 初次載入:預設用第一個 Google 連結
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  demo.load(
435
- lambda: pipeline("drive", None, DRIVE_PRESETS[0], [], False, "5", "viridis", "OpenStreetMap"),
436
  inputs=None,
437
  outputs=[
438
  plot1, plot2, plot3, map_out,
439
  json_box, json_file, df_view,
440
  demo_csv_file,
441
- point_selector, detail_view
 
442
  ]
443
  )
444
 
445
  # 產生 / 重新產生
446
  run_btn.click(
447
  pipeline,
448
- inputs=[source_radio, file_in, preset_dd, series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd],
449
  outputs=[
450
  plot1, plot2, plot3, map_out,
451
  json_box, json_file, df_view,
452
  demo_csv_file,
453
- point_selector, detail_view
 
454
  ]
455
  )
456
 
457
  regen_btn.click(
458
  regenerate_demo,
459
- inputs=[series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, point_selector],
460
  outputs=[
461
  plot1, plot2, plot3, map_out,
462
  json_box, json_file, df_view,
463
  demo_csv_file,
464
- point_selector, detail_view
 
465
  ]
466
  )
467
 
@@ -472,4 +530,4 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
472
  )
473
 
474
  if __name__ == "__main__":
475
- demo.launch()
 
93
 
94
  def load_csv(file: gr.File | None) -> pd.DataFrame:
95
  """讀上傳 CSV"""
96
+ try:
97
+ df = pd.read_csv(file.name)
98
+ df = _finalize_time(df)
99
+ # 若無 lat/lon,補隨機(避免地圖空白)
100
+ if "lat" not in df.columns or "lon" not in df.columns:
101
+ n = len(df)
102
+ df["lat"] = np.random.uniform(21.8, 25.3, size=n)
103
+ df["lon"] = np.random.uniform(120.0, 122.0, size=n)
104
+ if "pid" not in df.columns:
105
+ df["pid"] = np.arange(len(df))
106
+ return df
107
+ except Exception as e:
108
+ raise ValueError(f"CSV 載入失敗:{str(e)}")
109
 
110
 
111
  def load_drive_csv(sheet_or_file_url: str) -> pd.DataFrame:
112
  """從 Google Sheets 或 Google Drive File 讀 CSV"""
113
+ try:
114
+ url = normalize_drive_url(sheet_or_file_url)
115
+ df = pd.read_csv(url)
116
+ df = _finalize_time(df)
117
+ if "lat" not in df.columns or "lon" not in df.columns:
118
+ n = len(df)
119
+ df["lat"] = np.random.uniform(21.8, 25.3, size=n)
120
+ df["lon"] = np.random.uniform(120.0, 122.0, size=n)
121
+ if "pid" not in df.columns:
122
+ df["pid"] = np.arange(len(df))
123
+ return df
124
+ except Exception as e:
125
+ raise ValueError(f"Google 連結載入失敗:{str(e)}")
126
 
127
 
128
  def load_data(source: str, file: gr.File | None = None, sheet_url: str = "") -> pd.DataFrame:
 
139
  return make_demo_dataframe()
140
 
141
 
142
+ # -----------------------------
143
+ # 新功能:資料過濾
144
+ # -----------------------------
145
+ def filter_data(df: pd.DataFrame, start_time: str, end_time: str) -> pd.DataFrame:
146
+ """根據時間範圍過濾資料"""
147
+ if start_time:
148
+ df = df[df["time"] >= pd.to_datetime(start_time).tz_localize(TAIPEI)]
149
+ if end_time:
150
+ df = df[df["time"] <= pd.to_datetime(end_time).tz_localize(TAIPEI)]
151
+ return df
152
+
153
+
154
  # -----------------------------
155
  # grafanalib JSON builder
156
  # -----------------------------
 
284
  cmap_name: str = "viridis",
285
  tiles: str = "OpenStreetMap",
286
  ) -> str:
287
+ if df.empty:
288
+ return "<p>無資料可顯示地圖</p>"
289
  center_lat, center_lon = df["lat"].mean(), df["lon"].mean()
290
  m = folium.Map(location=[center_lat, center_lon], zoom_start=7, tiles=tiles)
291
 
 
348
  # -----------------------------
349
  # Main pipeline
350
  # -----------------------------
351
+ def pipeline(source, file, sheet_url, series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice, start_time, end_time):
352
+ try:
353
+ df = load_data(source, file, sheet_url)
354
+ df = filter_data(df, start_time, end_time) # 新增過濾
355
+ numeric_cols = [c for c in df.columns if c not in ["time", "lat", "lon", "pid"] and pd.api.types.is_numeric_dtype(df[c])]
356
+ chosen = [c for c in (series_choice or numeric_cols[:2]) if c in numeric_cols]
357
+ if not chosen:
358
+ chosen = numeric_cols[:2]
359
+ if not chosen:
360
+ raise ValueError("無有效數值欄位可視覺化")
361
+
362
+ dash_json = build_grafanalib_dashboard(chosen, bool(dual_axis), int(rolling_window))
363
+ dash_json_str = json.dumps(dash_json, ensure_ascii=False, indent=2, default=str)
364
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".json", mode="w", encoding="utf-8") as f:
365
+ f.write(dash_json_str)
366
+ json_path = f.name
367
+
368
+ fig1 = render_line(df, chosen[0])
369
+ fig2 = render_bar_or_dual(df, chosen[1], chosen[0], bool(dual_axis)) if len(chosen) > 1 else plt.figure()
370
+ fig3, df_with_roll = render_rolling(df.copy(), chosen[0], int(rolling_window))
371
+
372
+ map_html = render_map_folium(df, value_col=chosen[0], size_col=chosen[1] if len(chosen) > 1 else "count",
373
+ cmap_name=cmap_choice, tiles=tiles_choice)
374
+
375
+ point_choices = make_point_choices(df)
376
+ default_choice = point_choices[0] if point_choices else ""
377
+ detail_df = pick_detail(df, default_choice)
378
+
379
+ demo_df = make_demo_dataframe()
380
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="w", encoding="utf-8") as f:
381
+ demo_df.to_csv(f, index=False)
382
+ demo_csv_path = f.name
383
+
384
+ return (
385
+ fig1, fig2, fig3, map_html,
386
+ dash_json_str, json_path, df_with_roll,
387
+ demo_csv_path,
388
+ gr.Dropdown(choices=point_choices, value=default_choice),
389
+ detail_df,
390
+ "" # 錯誤訊息清空
391
+ )
392
+ except Exception as e:
393
+ return (
394
+ None, None, None, "<p>錯誤:無資料顯示</p>",
395
+ "", None, pd.DataFrame(),
396
+ None,
397
+ gr.Dropdown(choices=[], value=None),
398
+ pd.DataFrame(),
399
+ str(e)
400
+ )
401
 
402
 
403
+ def regenerate_demo(series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice, current_choice, start_time, end_time):
404
+ return pipeline("demo", None, "", series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice, start_time, end_time)
405
 
406
 
407
  def update_detail(df: pd.DataFrame, choice: str):
 
409
 
410
 
411
  # -----------------------------
412
+ # UI 優化:使用 Tab 分頁、添加錯誤顯示、時間過濾
413
  # -----------------------------
414
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
415
+ gr.Markdown("## 優化版 Grafana-like Demo + Folium Map(支援 Google Drive / Sheets")
 
 
 
416
 
417
+ with gr.Row():
418
+ with gr.Column(scale=1):
419
+ source_radio = gr.Radio(["upload", "drive", "demo"], label="資料來源", value="demo")
420
+ file_in = gr.File(label="上傳 CSV(選 upload 時使用)", file_types=[".csv"])
421
+ preset_dd = gr.Dropdown(
422
+ label="Google 預設來源(3 個連結)",
423
+ choices=DRIVE_PRESETS,
424
+ value=DRIVE_PRESETS[0]
425
+ )
426
+ with gr.Row():
427
+ start_time_in = gr.Textbox(label="開始時間 (YYYY-MM-DD HH:MM:SS)", placeholder="2023-01-01 00:00:00")
428
+ end_time_in = gr.Textbox(label="結束時間 (YYYY-MM-DD HH:MM:SS)", placeholder="2023-12-31 23:59:59")
429
+
430
+ with gr.Column(scale=1):
431
+ series_multiselect = gr.CheckboxGroup(label="數值欄位", choices=[])
432
+ dual_axis_chk = gr.Checkbox(label="第二面板啟用雙軸", value=False)
433
+ rolling_dd = gr.Dropdown(label="Rolling window", choices=["3", "5", "10", "20"], value="5")
434
+ cmap_dd = gr.Dropdown(label="地圖配色 (colormap)",
435
+ choices=["viridis", "plasma", "inferno", "magma", "cividis", "coolwarm"],
436
+ value="viridis")
437
+ tiles_dd = gr.Dropdown(label="地圖底圖 (tiles)",
438
+ choices=["OpenStreetMap", "Stamen Terrain", "Stamen Toner",
439
+ "CartoDB positron", "CartoDB dark_matter"],
440
+ value="OpenStreetMap")
441
 
442
  with gr.Row():
443
  run_btn = gr.Button("產生 Dashboard", scale=1)
444
  regen_btn = gr.Button("🔁 重新產生示範資料", scale=1)
445
 
446
+ error_msg = gr.Markdown(value="", label="錯誤訊息", visible=True)
 
 
 
447
 
448
+ with gr.Tabs():
449
+ with gr.Tab("圖表"):
450
+ with gr.Row():
451
+ plot1 = gr.Plot(label="1:Line")
452
+ plot2 = gr.Plot(label="2:Bar / Dual Axis")
453
+ plot3 = gr.Plot(label="3:Rolling Mean")
454
 
455
+ with gr.Tab("地圖"):
456
+ map_out = gr.HTML(label="4:Geo Map (Interactive + Legend)")
 
457
 
458
+ with gr.Tab("JSON & 檔案"):
459
+ json_box = gr.Code(label="grafanalib Dashboard JSON", language="json")
460
+ json_file = gr.File(label="下載 dashboard.json")
461
+ demo_csv_file = gr.File(label="下載示範資料 demo.csv")
462
+
463
+ with gr.Tab("資料預覽"):
464
+ df_view = gr.Dataframe(label="資料預覽(含 rolling)", wrap=True)
465
 
466
+ with gr.Tab("點位詳情"):
467
+ gr.Markdown("### 🔎 點位詳情(對應地圖彈窗中的 #ID)")
468
+ point_selector = gr.Dropdown(label="選擇點位(#ID | 時間 | 值)", choices=[], value=None)
469
+ detail_view = gr.Dataframe(label="選取點詳細資料", wrap=True)
470
 
471
+ # 探勘欄位並更新 UI
472
+ def probe_columns(source, file, preset_url, start_time, end_time):
473
+ sheet_url = preset_url if source == "drive" else ""
474
+ try:
475
+ df = load_data(source, file, sheet_url)
476
+ df = filter_data(df, start_time, end_time)
477
+ numeric_cols = [c for c in df.columns if c not in ["time", "lat", "lon", "pid"] and pd.api.types.is_numeric_dtype(df[c])]
478
+ return gr.CheckboxGroup(choices=numeric_cols, value=numeric_cols[:2]), df, ""
479
+ except Exception as e:
480
+ return gr.CheckboxGroup(choices=[]), pd.DataFrame(), str(e)
481
+
482
+ source_radio.change(probe_columns, inputs=[source_radio, file_in, preset_dd, start_time_in, end_time_in], outputs=[series_multiselect, df_view, error_msg])
483
+ file_in.change(probe_columns, inputs=[source_radio, file_in, preset_dd, start_time_in, end_time_in], outputs=[series_multiselect, df_view, error_msg])
484
+ preset_dd.change(probe_columns, inputs=[source_radio, file_in, preset_dd, start_time_in, end_time_in], outputs=[series_multiselect, df_view, error_msg])
485
+ start_time_in.change(probe_columns, inputs=[source_radio, file_in, preset_dd, start_time_in, end_time_in], outputs=[series_multiselect, df_view, error_msg])
486
+ end_time_in.change(probe_columns, inputs=[source_radio, file_in, preset_dd, start_time_in, end_time_in], outputs=[series_multiselect, df_view, error_msg])
487
+
488
+ # 初次載入
489
  demo.load(
490
+ lambda: pipeline("drive", None, DRIVE_PRESETS[0], [], False, "5", "viridis", "OpenStreetMap", "", ""),
491
  inputs=None,
492
  outputs=[
493
  plot1, plot2, plot3, map_out,
494
  json_box, json_file, df_view,
495
  demo_csv_file,
496
+ point_selector, detail_view,
497
+ error_msg
498
  ]
499
  )
500
 
501
  # 產生 / 重新產生
502
  run_btn.click(
503
  pipeline,
504
+ inputs=[source_radio, file_in, preset_dd, series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, start_time_in, end_time_in],
505
  outputs=[
506
  plot1, plot2, plot3, map_out,
507
  json_box, json_file, df_view,
508
  demo_csv_file,
509
+ point_selector, detail_view,
510
+ error_msg
511
  ]
512
  )
513
 
514
  regen_btn.click(
515
  regenerate_demo,
516
+ inputs=[series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, point_selector, start_time_in, end_time_in],
517
  outputs=[
518
  plot1, plot2, plot3, map_out,
519
  json_box, json_file, df_view,
520
  demo_csv_file,
521
+ point_selector, detail_view,
522
+ error_msg
523
  ]
524
  )
525
 
 
530
  )
531
 
532
  if __name__ == "__main__":
533
+ demo.launch(server_name="0.0.0.0", server_port=7860) # 方便部署,綁定所有 IP