Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
|
|
| 1 |
import json
|
|
|
|
| 2 |
import tempfile
|
| 3 |
from datetime import datetime, timedelta
|
| 4 |
from dateutil import tz
|
|
@@ -18,20 +20,55 @@ from grafanalib.core import (
|
|
| 18 |
|
| 19 |
TAIPEI = tz.gettz("Asia/Taipei")
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
# -----------------------------
|
| 22 |
# Demo / Data loading
|
| 23 |
# -----------------------------
|
| 24 |
def make_demo_dataframe() -> pd.DataFrame:
|
| 25 |
-
"""隨機示範資料:含經緯度"""
|
| 26 |
t0 = datetime.now(tz=TAIPEI) - timedelta(minutes=60)
|
| 27 |
times = [t0 + timedelta(minutes=i) for i in range(61)]
|
| 28 |
amp = np.random.rand(len(times))
|
| 29 |
cnt = np.random.randint(0, 11, size=len(times))
|
| 30 |
-
|
| 31 |
-
# 台灣範圍大致經緯度
|
| 32 |
lats = np.random.uniform(21.8, 25.3, size=len(times))
|
| 33 |
lons = np.random.uniform(120.0, 122.0, size=len(times))
|
| 34 |
-
|
| 35 |
df = pd.DataFrame({
|
| 36 |
"time": times,
|
| 37 |
"amplitude": amp,
|
|
@@ -39,72 +76,61 @@ def make_demo_dataframe() -> pd.DataFrame:
|
|
| 39 |
"lat": lats,
|
| 40 |
"lon": lons
|
| 41 |
})
|
| 42 |
-
df["pid"] = np.arange(len(df))
|
| 43 |
return df
|
| 44 |
|
| 45 |
|
| 46 |
-
def
|
| 47 |
-
"""
|
| 48 |
-
df = pd.read_csv(file.name)
|
| 49 |
time_col = next((c for c in ["time", "timestamp", "datetime", "date"] if c in df.columns), None)
|
| 50 |
if time_col is None:
|
| 51 |
-
raise ValueError("
|
| 52 |
df[time_col] = pd.to_datetime(df[time_col])
|
| 53 |
df = df.rename(columns={time_col: "time"})
|
| 54 |
if getattr(df["time"].dt, "tz", None) is None:
|
| 55 |
df["time"] = df["time"].dt.tz_localize(TAIPEI)
|
| 56 |
else:
|
| 57 |
df["time"] = df["time"].dt.tz_convert(TAIPEI)
|
|
|
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
if "lat" not in df.columns or "lon" not in df.columns:
|
| 61 |
n = len(df)
|
| 62 |
df["lat"] = np.random.uniform(21.8, 25.3, size=n)
|
| 63 |
df["lon"] = np.random.uniform(120.0, 122.0, size=n)
|
| 64 |
-
|
| 65 |
if "pid" not in df.columns:
|
| 66 |
df["pid"] = np.arange(len(df))
|
|
|
|
| 67 |
|
| 68 |
-
return df.sort_values("time").reset_index(drop=True)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def load_drive_csv(sheet_url: str) -> pd.DataFrame:
|
| 72 |
-
"""從 Google Drive 試算表讀資料"""
|
| 73 |
-
if not sheet_url:
|
| 74 |
-
raise ValueError("請提供 Google Sheets 連結")
|
| 75 |
-
if "export?format=csv" not in sheet_url:
|
| 76 |
-
sheet_url = sheet_url.split("/edit")[0] + "/export?format=csv"
|
| 77 |
-
|
| 78 |
-
df = pd.read_csv(sheet_url)
|
| 79 |
-
time_col = next((c for c in ["time", "timestamp", "datetime", "date"] if c in df.columns), None)
|
| 80 |
-
if time_col is None:
|
| 81 |
-
raise ValueError("Google Drive CSV 檔需包含時間欄位")
|
| 82 |
-
df[time_col] = pd.to_datetime(df[time_col])
|
| 83 |
-
df = df.rename(columns={time_col: "time"})
|
| 84 |
-
if getattr(df["time"].dt, "tz", None) is None:
|
| 85 |
-
df["time"] = df["time"].dt.tz_localize(TAIPEI)
|
| 86 |
-
else:
|
| 87 |
-
df["time"] = df["time"].dt.tz_convert(TAIPEI)
|
| 88 |
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
if "lat" not in df.columns or "lon" not in df.columns:
|
| 91 |
n = len(df)
|
| 92 |
df["lat"] = np.random.uniform(21.8, 25.3, size=n)
|
| 93 |
df["lon"] = np.random.uniform(120.0, 122.0, size=n)
|
| 94 |
-
|
| 95 |
if "pid" not in df.columns:
|
| 96 |
df["pid"] = np.arange(len(df))
|
| 97 |
-
|
| 98 |
-
return df.sort_values("time").reset_index(drop=True)
|
| 99 |
|
| 100 |
|
| 101 |
def load_data(source: str, file: gr.File | None = None, sheet_url: str = "") -> pd.DataFrame:
|
| 102 |
-
"""
|
| 103 |
if source == "drive":
|
|
|
|
|
|
|
| 104 |
return load_drive_csv(sheet_url)
|
| 105 |
elif source == "upload":
|
| 106 |
if file is None:
|
| 107 |
-
raise ValueError("
|
| 108 |
return load_csv(file)
|
| 109 |
else:
|
| 110 |
return make_demo_dataframe()
|
|
@@ -232,7 +258,6 @@ def render_rolling(df, col, window=5):
|
|
| 232 |
# Folium helpers (map + legend)
|
| 233 |
# -----------------------------
|
| 234 |
def _to_hex_color(value: float, cmap=cm.viridis) -> str:
|
| 235 |
-
"""把 0~1 數值轉成 hex 顏色"""
|
| 236 |
rgba = cmap(value)
|
| 237 |
return "#{:02x}{:02x}{:02x}".format(int(rgba[0]*255), int(rgba[1]*255), int(rgba[2]*255))
|
| 238 |
|
|
@@ -244,23 +269,19 @@ def render_map_folium(
|
|
| 244 |
cmap_name: str = "viridis",
|
| 245 |
tiles: str = "OpenStreetMap",
|
| 246 |
) -> str:
|
| 247 |
-
"""用 folium 建立互動地圖,帶顏色圖例與 #ID 提示"""
|
| 248 |
center_lat, center_lon = df["lat"].mean(), df["lon"].mean()
|
| 249 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=7, tiles=tiles)
|
| 250 |
|
| 251 |
vmin, vmax = df[value_col].min(), df[value_col].max()
|
| 252 |
cmap = getattr(cm, cmap_name)
|
| 253 |
|
| 254 |
-
# 建立 folium colormap (legend)
|
| 255 |
colormap = bcm.LinearColormap(
|
| 256 |
[_to_hex_color(i, cmap) for i in np.linspace(0, 1, 256)],
|
| 257 |
-
vmin=vmin,
|
| 258 |
-
vmax=vmax,
|
| 259 |
)
|
| 260 |
colormap.caption = f"{value_col} (color scale)"
|
| 261 |
colormap.add_to(m)
|
| 262 |
|
| 263 |
-
# 加點資料(彈窗含 #ID,方便對應右側選擇器)
|
| 264 |
for _, row in df.iterrows():
|
| 265 |
norm_val = (row[value_col] - vmin) / (vmax - vmin + 1e-9)
|
| 266 |
popup_html = (
|
|
@@ -288,7 +309,6 @@ def render_map_folium(
|
|
| 288 |
# Detail helpers
|
| 289 |
# -----------------------------
|
| 290 |
def make_point_choices(df: pd.DataFrame) -> list[str]:
|
| 291 |
-
"""建立下拉清單文字,例如: '#23 | 12:34:56 | amp=0.42'"""
|
| 292 |
labels = []
|
| 293 |
for _, r in df.iterrows():
|
| 294 |
t = pd.to_datetime(r["time"]).strftime("%H:%M:%S")
|
|
@@ -297,7 +317,6 @@ def make_point_choices(df: pd.DataFrame) -> list[str]:
|
|
| 297 |
|
| 298 |
|
| 299 |
def pick_detail(df: pd.DataFrame, choice: str) -> pd.DataFrame:
|
| 300 |
-
"""依下拉選擇的 '#ID | ...' 找出對應列"""
|
| 301 |
if not choice:
|
| 302 |
return pd.DataFrame()
|
| 303 |
try:
|
|
@@ -332,35 +351,29 @@ def pipeline(source, file, sheet_url, series_choice, dual_axis, rolling_window,
|
|
| 332 |
map_html = render_map_folium(df, value_col=chosen[0], size_col="count",
|
| 333 |
cmap_name=cmap_choice, tiles=tiles_choice)
|
| 334 |
|
| 335 |
-
# 準備點位下拉與預設詳細資料
|
| 336 |
point_choices = make_point_choices(df)
|
| 337 |
default_choice = point_choices[0] if point_choices else ""
|
| 338 |
detail_df = pick_detail(df, default_choice)
|
| 339 |
|
| 340 |
-
# 也輸出 demo.csv
|
| 341 |
demo_df = make_demo_dataframe()
|
| 342 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="w", encoding="utf-8") as f:
|
| 343 |
demo_df.to_csv(f, index=False)
|
| 344 |
demo_csv_path = f.name
|
| 345 |
|
| 346 |
return (
|
| 347 |
-
fig1, fig2, fig3, map_html,
|
| 348 |
-
dash_json_str, json_path, df_with_roll,
|
| 349 |
-
demo_csv_path,
|
| 350 |
-
gr.Dropdown(choices=point_choices, value=default_choice),
|
| 351 |
-
detail_df,
|
| 352 |
)
|
| 353 |
|
| 354 |
|
| 355 |
def regenerate_demo(series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice, current_choice):
|
| 356 |
-
|
| 357 |
-
result = pipeline("demo", None, "", series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice)
|
| 358 |
-
# result 的倒數二個回傳是 Dropdown 與 detail_df,我們已在 pipeline 設好預設
|
| 359 |
-
return result
|
| 360 |
|
| 361 |
|
| 362 |
def update_detail(df: pd.DataFrame, choice: str):
|
| 363 |
-
"""使用目前資料與下拉選擇更新詳細資料表"""
|
| 364 |
return pick_detail(df, choice)
|
| 365 |
|
| 366 |
|
|
@@ -368,11 +381,14 @@ def update_detail(df: pd.DataFrame, choice: str):
|
|
| 368 |
# UI
|
| 369 |
# -----------------------------
|
| 370 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 371 |
-
gr.Markdown("## Grafana-like Demo + Folium Map
|
| 372 |
|
| 373 |
source_radio = gr.Radio(["upload", "drive", "demo"], label="資料來源", value="demo")
|
| 374 |
-
file_in = gr.File(label="上傳 CSV
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
series_multiselect = gr.CheckboxGroup(label="數值欄位", choices=[])
|
| 378 |
dual_axis_chk = gr.Checkbox(label="第二面板啟用雙軸", value=False)
|
|
@@ -399,23 +415,28 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 399 |
demo_csv_file = gr.File(label="下載示範資料 demo.csv")
|
| 400 |
df_view = gr.Dataframe(label="資料預覽(含 rolling)", wrap=True)
|
| 401 |
|
| 402 |
-
gr.Markdown("### 🔎
|
| 403 |
point_selector = gr.Dropdown(label="選擇點位(#ID | 時間 | 值)", choices=[], value=None)
|
| 404 |
detail_view = gr.Dataframe(label="選取點詳細資料", wrap=True)
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
def probe_columns(source, file, sheet_url):
|
| 407 |
df = load_data(source, file, sheet_url)
|
| 408 |
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])]
|
| 409 |
-
# 初步資料預覽也回給前端
|
| 410 |
return gr.CheckboxGroup(choices=numeric_cols, value=numeric_cols[:2]), df
|
| 411 |
|
| 412 |
source_radio.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
| 413 |
file_in.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
| 414 |
sheet_url_box.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
|
|
|
| 415 |
|
| 416 |
-
# 初始載入 demo
|
| 417 |
demo.load(
|
| 418 |
-
lambda: pipeline("
|
| 419 |
inputs=None,
|
| 420 |
outputs=[
|
| 421 |
plot1, plot2, plot3, map_out,
|
|
@@ -425,7 +446,6 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 425 |
]
|
| 426 |
)
|
| 427 |
|
| 428 |
-
# 產生與重新產生
|
| 429 |
run_btn.click(
|
| 430 |
pipeline,
|
| 431 |
inputs=[source_radio, file_in, sheet_url_box, series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd],
|
|
@@ -448,10 +468,9 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 448 |
]
|
| 449 |
)
|
| 450 |
|
| 451 |
-
# 當使用者從下拉選擇點位 → 更新詳細資料
|
| 452 |
point_selector.change(
|
| 453 |
update_detail,
|
| 454 |
-
inputs=[df_view, point_selector],
|
| 455 |
outputs=[detail_view]
|
| 456 |
)
|
| 457 |
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
import json
|
| 3 |
+
import re
|
| 4 |
import tempfile
|
| 5 |
from datetime import datetime, timedelta
|
| 6 |
from dateutil import tz
|
|
|
|
| 20 |
|
| 21 |
TAIPEI = tz.gettz("Asia/Taipei")
|
| 22 |
|
| 23 |
+
# -----------------------------
|
| 24 |
+
# Google Drive 連結處理
|
| 25 |
+
# -----------------------------
|
| 26 |
+
DRIVE_PRESETS = [
|
| 27 |
+
"https://drive.google.com/file/d/15yZ4QicICKZCnX6vjcD9JNXjnJmMFJD4/view?usp=drivesdk",
|
| 28 |
+
"https://drive.google.com/file/d/1dqazYh_YzNNMbkUpgLRKSE9Y3ioPhtFu/view?usp=drivesdk",
|
| 29 |
+
"https://drive.google.com/file/d/1A23f4q8DXHpoRIN5UQsDd6eM8jJ_Ruf8/view?usp=drivesdk",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
def normalize_drive_url(url: str) -> str:
|
| 33 |
+
"""
|
| 34 |
+
接受 Google Drive / Google Sheets 各式分享連結,回傳可直接給 pandas 讀取 CSV 的 URL。
|
| 35 |
+
規則:
|
| 36 |
+
- Sheets: .../spreadsheets/d/<ID>/edit → .../export?format=csv
|
| 37 |
+
- Drive File: .../file/d/<ID>/view → https://drive.google.com/uc?export=download&id=<ID>
|
| 38 |
+
其他 http(s) 連結原樣返回。
|
| 39 |
+
"""
|
| 40 |
+
if not isinstance(url, str) or not url.strip():
|
| 41 |
+
raise ValueError("請提供有效的 Google Drive / Google Sheets 連結")
|
| 42 |
+
|
| 43 |
+
url = url.strip()
|
| 44 |
+
|
| 45 |
+
# Sheets
|
| 46 |
+
m = re.search(r"https://docs\.google\.com/spreadsheets/d/([a-zA-Z0-9-_]+)", url)
|
| 47 |
+
if m:
|
| 48 |
+
sheet_id = m.group(1)
|
| 49 |
+
return f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv"
|
| 50 |
+
|
| 51 |
+
# Drive file
|
| 52 |
+
m = re.search(r"https://drive\.google\.com/file/d/([a-zA-Z0-9-_]+)/", url)
|
| 53 |
+
if m:
|
| 54 |
+
file_id = m.group(1)
|
| 55 |
+
return f"https://drive.google.com/uc?export=download&id={file_id}"
|
| 56 |
+
|
| 57 |
+
# 其他:若已經是 export?format=csv 或一般 http(s) 連結,原樣回傳
|
| 58 |
+
return url
|
| 59 |
+
|
| 60 |
+
|
| 61 |
# -----------------------------
|
| 62 |
# Demo / Data loading
|
| 63 |
# -----------------------------
|
| 64 |
def make_demo_dataframe() -> pd.DataFrame:
|
| 65 |
+
"""隨機示範資料:含經緯度 + pid"""
|
| 66 |
t0 = datetime.now(tz=TAIPEI) - timedelta(minutes=60)
|
| 67 |
times = [t0 + timedelta(minutes=i) for i in range(61)]
|
| 68 |
amp = np.random.rand(len(times))
|
| 69 |
cnt = np.random.randint(0, 11, size=len(times))
|
|
|
|
|
|
|
| 70 |
lats = np.random.uniform(21.8, 25.3, size=len(times))
|
| 71 |
lons = np.random.uniform(120.0, 122.0, size=len(times))
|
|
|
|
| 72 |
df = pd.DataFrame({
|
| 73 |
"time": times,
|
| 74 |
"amplitude": amp,
|
|
|
|
| 76 |
"lat": lats,
|
| 77 |
"lon": lons
|
| 78 |
})
|
| 79 |
+
df["pid"] = np.arange(len(df))
|
| 80 |
return df
|
| 81 |
|
| 82 |
|
| 83 |
+
def _finalize_time(df: pd.DataFrame) -> pd.DataFrame:
|
| 84 |
+
"""確保 time 欄位有時區、排序"""
|
|
|
|
| 85 |
time_col = next((c for c in ["time", "timestamp", "datetime", "date"] if c in df.columns), None)
|
| 86 |
if time_col is None:
|
| 87 |
+
raise ValueError("資料需包含時間欄位(time/timestamp/datetime/date 其一)")
|
| 88 |
df[time_col] = pd.to_datetime(df[time_col])
|
| 89 |
df = df.rename(columns={time_col: "time"})
|
| 90 |
if getattr(df["time"].dt, "tz", None) is None:
|
| 91 |
df["time"] = df["time"].dt.tz_localize(TAIPEI)
|
| 92 |
else:
|
| 93 |
df["time"] = df["time"].dt.tz_convert(TAIPEI)
|
| 94 |
+
return df.sort_values("time").reset_index(drop=True)
|
| 95 |
|
| 96 |
+
|
| 97 |
+
def load_csv(file: gr.File | None) -> pd.DataFrame:
|
| 98 |
+
"""讀上傳 CSV"""
|
| 99 |
+
df = pd.read_csv(file.name)
|
| 100 |
+
df = _finalize_time(df)
|
| 101 |
+
# 若無 lat/lon,補隨機(避免地圖空白)
|
| 102 |
if "lat" not in df.columns or "lon" not in df.columns:
|
| 103 |
n = len(df)
|
| 104 |
df["lat"] = np.random.uniform(21.8, 25.3, size=n)
|
| 105 |
df["lon"] = np.random.uniform(120.0, 122.0, size=n)
|
|
|
|
| 106 |
if "pid" not in df.columns:
|
| 107 |
df["pid"] = np.arange(len(df))
|
| 108 |
+
return df
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
def load_drive_csv(sheet_or_file_url: str) -> pd.DataFrame:
|
| 112 |
+
"""從 Google Sheets 或 Google Drive File 讀 CSV"""
|
| 113 |
+
url = normalize_drive_url(sheet_or_file_url)
|
| 114 |
+
df = pd.read_csv(url)
|
| 115 |
+
df = _finalize_time(df)
|
| 116 |
if "lat" not in df.columns or "lon" not in df.columns:
|
| 117 |
n = len(df)
|
| 118 |
df["lat"] = np.random.uniform(21.8, 25.3, size=n)
|
| 119 |
df["lon"] = np.random.uniform(120.0, 122.0, size=n)
|
|
|
|
| 120 |
if "pid" not in df.columns:
|
| 121 |
df["pid"] = np.arange(len(df))
|
| 122 |
+
return df
|
|
|
|
| 123 |
|
| 124 |
|
| 125 |
def load_data(source: str, file: gr.File | None = None, sheet_url: str = "") -> pd.DataFrame:
|
| 126 |
+
"""依來源載入資料:demo / upload / drive"""
|
| 127 |
if source == "drive":
|
| 128 |
+
if not sheet_url:
|
| 129 |
+
raise ValueError("請選擇或輸入 Google 連結")
|
| 130 |
return load_drive_csv(sheet_url)
|
| 131 |
elif source == "upload":
|
| 132 |
if file is None:
|
| 133 |
+
raise ValueError("請上傳 CSV 檔")
|
| 134 |
return load_csv(file)
|
| 135 |
else:
|
| 136 |
return make_demo_dataframe()
|
|
|
|
| 258 |
# Folium helpers (map + legend)
|
| 259 |
# -----------------------------
|
| 260 |
def _to_hex_color(value: float, cmap=cm.viridis) -> str:
|
|
|
|
| 261 |
rgba = cmap(value)
|
| 262 |
return "#{:02x}{:02x}{:02x}".format(int(rgba[0]*255), int(rgba[1]*255), int(rgba[2]*255))
|
| 263 |
|
|
|
|
| 269 |
cmap_name: str = "viridis",
|
| 270 |
tiles: str = "OpenStreetMap",
|
| 271 |
) -> str:
|
|
|
|
| 272 |
center_lat, center_lon = df["lat"].mean(), df["lon"].mean()
|
| 273 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=7, tiles=tiles)
|
| 274 |
|
| 275 |
vmin, vmax = df[value_col].min(), df[value_col].max()
|
| 276 |
cmap = getattr(cm, cmap_name)
|
| 277 |
|
|
|
|
| 278 |
colormap = bcm.LinearColormap(
|
| 279 |
[_to_hex_color(i, cmap) for i in np.linspace(0, 1, 256)],
|
| 280 |
+
vmin=vmin, vmax=vmax
|
|
|
|
| 281 |
)
|
| 282 |
colormap.caption = f"{value_col} (color scale)"
|
| 283 |
colormap.add_to(m)
|
| 284 |
|
|
|
|
| 285 |
for _, row in df.iterrows():
|
| 286 |
norm_val = (row[value_col] - vmin) / (vmax - vmin + 1e-9)
|
| 287 |
popup_html = (
|
|
|
|
| 309 |
# Detail helpers
|
| 310 |
# -----------------------------
|
| 311 |
def make_point_choices(df: pd.DataFrame) -> list[str]:
|
|
|
|
| 312 |
labels = []
|
| 313 |
for _, r in df.iterrows():
|
| 314 |
t = pd.to_datetime(r["time"]).strftime("%H:%M:%S")
|
|
|
|
| 317 |
|
| 318 |
|
| 319 |
def pick_detail(df: pd.DataFrame, choice: str) -> pd.DataFrame:
|
|
|
|
| 320 |
if not choice:
|
| 321 |
return pd.DataFrame()
|
| 322 |
try:
|
|
|
|
| 351 |
map_html = render_map_folium(df, value_col=chosen[0], size_col="count",
|
| 352 |
cmap_name=cmap_choice, tiles=tiles_choice)
|
| 353 |
|
|
|
|
| 354 |
point_choices = make_point_choices(df)
|
| 355 |
default_choice = point_choices[0] if point_choices else ""
|
| 356 |
detail_df = pick_detail(df, default_choice)
|
| 357 |
|
|
|
|
| 358 |
demo_df = make_demo_dataframe()
|
| 359 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="w", encoding="utf-8") as f:
|
| 360 |
demo_df.to_csv(f, index=False)
|
| 361 |
demo_csv_path = f.name
|
| 362 |
|
| 363 |
return (
|
| 364 |
+
fig1, fig2, fig3, map_html,
|
| 365 |
+
dash_json_str, json_path, df_with_roll,
|
| 366 |
+
demo_csv_path,
|
| 367 |
+
gr.Dropdown(choices=point_choices, value=default_choice),
|
| 368 |
+
detail_df,
|
| 369 |
)
|
| 370 |
|
| 371 |
|
| 372 |
def regenerate_demo(series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice, current_choice):
|
| 373 |
+
return pipeline("demo", None, "", series_choice, dual_axis, rolling_window, cmap_choice, tiles_choice)
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
|
| 376 |
def update_detail(df: pd.DataFrame, choice: str):
|
|
|
|
| 377 |
return pick_detail(df, choice)
|
| 378 |
|
| 379 |
|
|
|
|
| 381 |
# UI
|
| 382 |
# -----------------------------
|
| 383 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 384 |
+
gr.Markdown("## Grafana-like Demo + Folium Map(支援 Google Drive / Sheets 連結與預設來源)")
|
| 385 |
|
| 386 |
source_radio = gr.Radio(["upload", "drive", "demo"], label="資料來源", value="demo")
|
| 387 |
+
file_in = gr.File(label="上傳 CSV(選 upload 時使用)", file_types=[".csv"])
|
| 388 |
+
|
| 389 |
+
with gr.Row():
|
| 390 |
+
preset_dd = gr.Dropdown(label="Google 預設來源(你提供的 3 個連結)", choices=DRIVE_PRESETS, value=DRIVE_PRESETS[0])
|
| 391 |
+
sheet_url_box = gr.Textbox(label="Google 連結(可自貼)", value=DRIVE_PRESETS[0])
|
| 392 |
|
| 393 |
series_multiselect = gr.CheckboxGroup(label="數值欄位", choices=[])
|
| 394 |
dual_axis_chk = gr.Checkbox(label="第二面板啟用雙軸", value=False)
|
|
|
|
| 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 |
+
# 預設下拉 → 同步到文字框,方便改
|
| 423 |
+
def use_preset(preset_url):
|
| 424 |
+
return normalize_drive_url(preset_url) if preset_url else ""
|
| 425 |
+
|
| 426 |
+
preset_dd.change(use_preset, inputs=[preset_dd], outputs=[sheet_url_box])
|
| 427 |
+
|
| 428 |
def probe_columns(source, file, sheet_url):
|
| 429 |
df = load_data(source, file, sheet_url)
|
| 430 |
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])]
|
|
|
|
| 431 |
return gr.CheckboxGroup(choices=numeric_cols, value=numeric_cols[:2]), df
|
| 432 |
|
| 433 |
source_radio.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
| 434 |
file_in.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
| 435 |
sheet_url_box.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
| 436 |
+
preset_dd.change(probe_columns, inputs=[source_radio, file_in, sheet_url_box], outputs=[series_multiselect, df_view])
|
| 437 |
|
|
|
|
| 438 |
demo.load(
|
| 439 |
+
lambda: pipeline("drive", None, DRIVE_PRESETS[0], [], False, "5", "viridis", "OpenStreetMap"),
|
| 440 |
inputs=None,
|
| 441 |
outputs=[
|
| 442 |
plot1, plot2, plot3, map_out,
|
|
|
|
| 446 |
]
|
| 447 |
)
|
| 448 |
|
|
|
|
| 449 |
run_btn.click(
|
| 450 |
pipeline,
|
| 451 |
inputs=[source_radio, file_in, sheet_url_box, series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd],
|
|
|
|
| 468 |
]
|
| 469 |
)
|
| 470 |
|
|
|
|
| 471 |
point_selector.change(
|
| 472 |
update_detail,
|
| 473 |
+
inputs=[df_view, point_selector],
|
| 474 |
outputs=[detail_view]
|
| 475 |
)
|
| 476 |
|