Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,9 +14,11 @@ import folium
|
|
| 14 |
from matplotlib import cm
|
| 15 |
import branca.colormap as bcm
|
| 16 |
import folium.plugins as plugins
|
|
|
|
|
|
|
| 17 |
|
| 18 |
from grafanalib.core import (
|
| 19 |
-
Dashboard, Graph, Row, Target, YAxis, YAxes, Time
|
| 20 |
)
|
| 21 |
|
| 22 |
TAIPEI = tz.gettz("Asia/Taipei")
|
|
@@ -153,7 +155,7 @@ def filter_data(df: pd.DataFrame, start_time: str, end_time: str) -> pd.DataFram
|
|
| 153 |
|
| 154 |
|
| 155 |
# -----------------------------
|
| 156 |
-
# grafanalib JSON builder
|
| 157 |
# -----------------------------
|
| 158 |
def build_grafanalib_dashboard(series_columns: list[str], dual_axis: bool, rolling_window: int) -> dict:
|
| 159 |
panels = []
|
|
@@ -192,6 +194,14 @@ def build_grafanalib_dashboard(series_columns: list[str], dual_axis: bool, rolli
|
|
| 192 |
yAxes=YAxes(left=YAxis(format="short"), right=YAxis(format="short")),
|
| 193 |
)
|
| 194 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
return Dashboard(
|
| 196 |
title="Grafana-like Demo (grafanalib + Gradio)",
|
| 197 |
rows=[Row(panels=panels)],
|
|
@@ -270,6 +280,76 @@ def render_rolling(df, col, window=5):
|
|
| 270 |
return fig, df
|
| 271 |
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
# -----------------------------
|
| 274 |
# Folium helpers (map + legend + heatmap)
|
| 275 |
# -----------------------------
|
|
@@ -337,7 +417,7 @@ def make_point_choices(df: pd.DataFrame) -> list[str]:
|
|
| 337 |
labels = []
|
| 338 |
for _, r in df.iterrows():
|
| 339 |
t = pd.to_datetime(r["time"]).strftime("%H:%M:%S")
|
| 340 |
-
labels.append(f"#{int(r['pid'])} | {t} | amp={r
|
| 341 |
return labels
|
| 342 |
|
| 343 |
|
|
@@ -376,11 +456,12 @@ def pipeline(source, file, sheet_url, series_choice, dual_axis, rolling_window,
|
|
| 376 |
fig1 = render_line(df, chosen[0])
|
| 377 |
fig2 = render_bar_or_dual(df, chosen[1], chosen[0], bool(dual_axis)) if len(chosen) > 1 else plt.figure()
|
| 378 |
fig3, df_with_roll = render_rolling(df.copy(), chosen[0], int(rolling_window))
|
|
|
|
| 379 |
|
| 380 |
map_html = render_map_folium(df, value_col=chosen[0], size_col=chosen[1] if len(chosen) > 1 else "count",
|
| 381 |
cmap_name=cmap_choice, tiles=tiles_choice, show_heatmap=bool(show_heatmap))
|
| 382 |
|
| 383 |
-
point_choices = make_point_choices(df)
|
| 384 |
default_choice = point_choices[0] if point_choices else ""
|
| 385 |
detail_df = pick_detail(df, default_choice)
|
| 386 |
|
|
@@ -390,7 +471,7 @@ def pipeline(source, file, sheet_url, series_choice, dual_axis, rolling_window,
|
|
| 390 |
demo_csv_path = f.name
|
| 391 |
|
| 392 |
return (
|
| 393 |
-
fig1, fig2, fig3, map_html,
|
| 394 |
dash_json_str, json_path, df_with_roll,
|
| 395 |
demo_csv_path,
|
| 396 |
gr.Dropdown(choices=point_choices, value=default_choice),
|
|
@@ -399,7 +480,7 @@ def pipeline(source, file, sheet_url, series_choice, dual_axis, rolling_window,
|
|
| 399 |
)
|
| 400 |
except Exception as e:
|
| 401 |
return (
|
| 402 |
-
None, None, None, "<p>錯誤:無資料顯示</p>",
|
| 403 |
"", None, pd.DataFrame(),
|
| 404 |
None,
|
| 405 |
gr.Dropdown(choices=[], value=None),
|
|
@@ -417,10 +498,10 @@ def update_detail(df: pd.DataFrame, choice: str):
|
|
| 417 |
|
| 418 |
|
| 419 |
# -----------------------------
|
| 420 |
-
# UI 優化:使用 Tab
|
| 421 |
# -----------------------------
|
| 422 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 423 |
-
gr.Markdown("## 優化版 Grafana-like Demo + Folium Map(支援 Google Drive / Sheets
|
| 424 |
|
| 425 |
with gr.Row():
|
| 426 |
with gr.Column(scale=1):
|
|
@@ -433,7 +514,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 433 |
)
|
| 434 |
with gr.Row():
|
| 435 |
start_time_in = gr.Textbox(label="開始時間 (YYYY-MM-DD HH:MM:SS)", placeholder="2023-01-01 00:00:00")
|
| 436 |
-
end_time_in = gr.Textbox(label="結束時間 (YYYY-MM-DD HH:
|
| 437 |
|
| 438 |
with gr.Column(scale=1):
|
| 439 |
series_multiselect = gr.CheckboxGroup(label="數值欄位", choices=[])
|
|
@@ -459,7 +540,9 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 459 |
with gr.Row():
|
| 460 |
plot1 = gr.Plot(label="1:Line")
|
| 461 |
plot2 = gr.Plot(label="2:Bar / Dual Axis")
|
|
|
|
| 462 |
plot3 = gr.Plot(label="3:Rolling Mean")
|
|
|
|
| 463 |
|
| 464 |
with gr.Tab("地圖"):
|
| 465 |
map_out = gr.HTML(label="4:Geo Map (Interactive + Legend + Heatmap)")
|
|
@@ -473,7 +556,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 473 |
df_view = gr.Dataframe(label="資料預覽(含 rolling)", wrap=True)
|
| 474 |
|
| 475 |
with gr.Tab("點位詳情"):
|
| 476 |
-
gr.Markdown("### 🔎 點位詳情(對應地圖彈窗中的 #ID
|
| 477 |
point_selector = gr.Dropdown(label="選擇點位(#ID | 時間 | 值)", choices=[], value=None)
|
| 478 |
detail_view = gr.Dataframe(label="選取點詳細資料", wrap=True)
|
| 479 |
|
|
@@ -499,7 +582,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 499 |
lambda: pipeline("drive", None, DRIVE_PRESETS[0], [], False, "5", "viridis", "OpenStreetMap", "", "", False),
|
| 500 |
inputs=None,
|
| 501 |
outputs=[
|
| 502 |
-
plot1, plot2, plot3, map_out,
|
| 503 |
json_box, json_file, df_view,
|
| 504 |
demo_csv_file,
|
| 505 |
point_selector, detail_view,
|
|
@@ -512,7 +595,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 512 |
pipeline,
|
| 513 |
inputs=[source_radio, file_in, preset_dd, series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, start_time_in, end_time_in, heatmap_chk],
|
| 514 |
outputs=[
|
| 515 |
-
plot1, plot2, plot3, map_out,
|
| 516 |
json_box, json_file, df_view,
|
| 517 |
demo_csv_file,
|
| 518 |
point_selector, detail_view,
|
|
@@ -524,7 +607,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 524 |
regenerate_demo,
|
| 525 |
inputs=[series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, point_selector, start_time_in, end_time_in, heatmap_chk],
|
| 526 |
outputs=[
|
| 527 |
-
plot1, plot2, plot3, map_out,
|
| 528 |
json_box, json_file, df_view,
|
| 529 |
demo_csv_file,
|
| 530 |
point_selector, detail_view,
|
|
|
|
| 14 |
from matplotlib import cm
|
| 15 |
import branca.colormap as bcm
|
| 16 |
import folium.plugins as plugins
|
| 17 |
+
from matplotlib.patches import Wedge, Rectangle, FancyArrowPatch
|
| 18 |
+
from matplotlib.path import Path as mpath
|
| 19 |
|
| 20 |
from grafanalib.core import (
|
| 21 |
+
Dashboard, Graph, Row, Target, YAxis, YAxes, Time, BarGauge
|
| 22 |
)
|
| 23 |
|
| 24 |
TAIPEI = tz.gettz("Asia/Taipei")
|
|
|
|
| 155 |
|
| 156 |
|
| 157 |
# -----------------------------
|
| 158 |
+
# grafanalib JSON builder (新增 BarGauge)
|
| 159 |
# -----------------------------
|
| 160 |
def build_grafanalib_dashboard(series_columns: list[str], dual_axis: bool, rolling_window: int) -> dict:
|
| 161 |
panels = []
|
|
|
|
| 194 |
yAxes=YAxes(left=YAxis(format="short"), right=YAxis(format="short")),
|
| 195 |
)
|
| 196 |
)
|
| 197 |
+
# 新增 BarGauge 面板,顯示最新值
|
| 198 |
+
panels.append(
|
| 199 |
+
BarGauge(
|
| 200 |
+
title=f"Latest {series_columns[0]}",
|
| 201 |
+
dataSource="(example)",
|
| 202 |
+
targets=[Target(expr=f"last({series_columns[0]})", legendFormat=series_columns[0])],
|
| 203 |
+
)
|
| 204 |
+
)
|
| 205 |
return Dashboard(
|
| 206 |
title="Grafana-like Demo (grafanalib + Gradio)",
|
| 207 |
rows=[Row(panels=panels)],
|
|
|
|
| 280 |
return fig, df
|
| 281 |
|
| 282 |
|
| 283 |
+
# -----------------------------
|
| 284 |
+
# 新增 Gauge 渲染 (使用 Matplotlib patches)
|
| 285 |
+
# -----------------------------
|
| 286 |
+
def degree_range(n):
|
| 287 |
+
start = np.linspace(0, 180, n + 1, endpoint=True)[0:-1]
|
| 288 |
+
end = np.linspace(0, 180, n + 1, endpoint=True)[1:]
|
| 289 |
+
mid_points = start + ((end - start) / 2.)
|
| 290 |
+
return np.c_[start, end], mid_points
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def rot_text(ang):
|
| 294 |
+
rotation = np.degrees(np.radians(ang) * np.pi / np.pi - np.radians(90))
|
| 295 |
+
return rotation
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def render_gauge(df, col):
|
| 299 |
+
value = df[col].iloc[-1] if not df.empty else 0
|
| 300 |
+
min_val, max_val = df[col].min(), df[col].max()
|
| 301 |
+
normalized = (value - min_val) / (max_val - min_val + 1e-9) if max_val > min_val else 0
|
| 302 |
+
|
| 303 |
+
labels = ['LOW', 'MEDIUM', 'HIGH']
|
| 304 |
+
N = len(labels)
|
| 305 |
+
colors = ['#007A00', '#FFCC00', '#ED1C24'] # Green, Yellow, Red
|
| 306 |
+
|
| 307 |
+
if normalized < 0.33:
|
| 308 |
+
arrow = 1
|
| 309 |
+
elif normalized < 0.66:
|
| 310 |
+
arrow = 2
|
| 311 |
+
else:
|
| 312 |
+
arrow = 3
|
| 313 |
+
|
| 314 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 315 |
+
|
| 316 |
+
ang_range, mid_points = degree_range(N)
|
| 317 |
+
|
| 318 |
+
labels = labels[::-1]
|
| 319 |
+
|
| 320 |
+
patches = []
|
| 321 |
+
for ang, c in zip(ang_range, colors):
|
| 322 |
+
patches.append(Wedge((0., 0.), .4, *ang, facecolor='w', lw=2))
|
| 323 |
+
patches.append(Wedge((0., 0.), .4, *ang, width=0.10, facecolor=c, lw=2, alpha=0.5))
|
| 324 |
+
|
| 325 |
+
[ax.add_patch(p) for p in patches]
|
| 326 |
+
|
| 327 |
+
for mid, lab in zip(mid_points, labels):
|
| 328 |
+
ax.text(0.35 * np.cos(np.radians(mid)), 0.35 * np.sin(np.radians(mid)), lab,
|
| 329 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14,
|
| 330 |
+
fontweight='bold', rotation=rot_text(mid))
|
| 331 |
+
|
| 332 |
+
r = Rectangle((-0.4, -0.1), 0.8, 0.1, facecolor='w', lw=2)
|
| 333 |
+
ax.add_patch(r)
|
| 334 |
+
|
| 335 |
+
ax.text(0, -0.05, f"Latest {col}: {value:.2f}", horizontalalignment='center',
|
| 336 |
+
verticalalignment='center', fontsize=14, fontweight='bold')
|
| 337 |
+
|
| 338 |
+
pos = mid_points[abs(arrow - N)]
|
| 339 |
+
ax.arrow(0, 0, 0.225 * np.cos(np.radians(pos)), 0.225 * np.sin(np.radians(pos)),
|
| 340 |
+
width=0.04, head_width=0.09, head_length=0.1, fc='k', ec='k')
|
| 341 |
+
|
| 342 |
+
ax.add_patch(FancyArrowPatch((0, 0), (0.01 * np.cos(np.radians(pos)), 0.01 * np.sin(np.radians(pos))),
|
| 343 |
+
mutation_scale=10, fc='k', ec='k'))
|
| 344 |
+
|
| 345 |
+
ax.set_frame_on(False)
|
| 346 |
+
ax.axes.set_xticks([])
|
| 347 |
+
ax.axes.set_yticks([])
|
| 348 |
+
ax.axis('equal')
|
| 349 |
+
plt.tight_layout()
|
| 350 |
+
return fig
|
| 351 |
+
|
| 352 |
+
|
| 353 |
# -----------------------------
|
| 354 |
# Folium helpers (map + legend + heatmap)
|
| 355 |
# -----------------------------
|
|
|
|
| 417 |
labels = []
|
| 418 |
for _, r in df.iterrows():
|
| 419 |
t = pd.to_datetime(r["time"]).strftime("%H:%M:%S")
|
| 420 |
+
labels.append(f"#{int(r['pid'])} | {t} | amp={r.get('amplitude', 0):.3f} cnt={int(r.get('count', 0))}")
|
| 421 |
return labels
|
| 422 |
|
| 423 |
|
|
|
|
| 456 |
fig1 = render_line(df, chosen[0])
|
| 457 |
fig2 = render_bar_or_dual(df, chosen[1], chosen[0], bool(dual_axis)) if len(chosen) > 1 else plt.figure()
|
| 458 |
fig3, df_with_roll = render_rolling(df.copy(), chosen[0], int(rolling_window))
|
| 459 |
+
fig4 = render_gauge(df, chosen[0])
|
| 460 |
|
| 461 |
map_html = render_map_folium(df, value_col=chosen[0], size_col=chosen[1] if len(chosen) > 1 else "count",
|
| 462 |
cmap_name=cmap_choice, tiles=tiles_choice, show_heatmap=bool(show_heatmap))
|
| 463 |
|
| 464 |
+
point_choices = [] if show_heatmap else make_point_choices(df)
|
| 465 |
default_choice = point_choices[0] if point_choices else ""
|
| 466 |
detail_df = pick_detail(df, default_choice)
|
| 467 |
|
|
|
|
| 471 |
demo_csv_path = f.name
|
| 472 |
|
| 473 |
return (
|
| 474 |
+
fig1, fig2, fig3, fig4, map_html,
|
| 475 |
dash_json_str, json_path, df_with_roll,
|
| 476 |
demo_csv_path,
|
| 477 |
gr.Dropdown(choices=point_choices, value=default_choice),
|
|
|
|
| 480 |
)
|
| 481 |
except Exception as e:
|
| 482 |
return (
|
| 483 |
+
None, None, None, None, "<p>錯誤:無資料顯示</p>",
|
| 484 |
"", None, pd.DataFrame(),
|
| 485 |
None,
|
| 486 |
gr.Dropdown(choices=[], value=None),
|
|
|
|
| 498 |
|
| 499 |
|
| 500 |
# -----------------------------
|
| 501 |
+
# UI 優化:使用 Tab 分頁、添加錯誤顯示、時間過濾、熱圖選項、優化圖表佈局
|
| 502 |
# -----------------------------
|
| 503 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 504 |
+
gr.Markdown("## 優化版 Grafana-like Demo + Folium Map(支援 Google Drive / Sheets,新增熱圖與 Gauge)")
|
| 505 |
|
| 506 |
with gr.Row():
|
| 507 |
with gr.Column(scale=1):
|
|
|
|
| 514 |
)
|
| 515 |
with gr.Row():
|
| 516 |
start_time_in = gr.Textbox(label="開始時間 (YYYY-MM-DD HH:MM:SS)", placeholder="2023-01-01 00:00:00")
|
| 517 |
+
end_time_in = gr.Textbox(label="結束時間 (YYYY-MM-DD HH:MM:SS)", placeholder="2023-12-31 23:59:59")
|
| 518 |
|
| 519 |
with gr.Column(scale=1):
|
| 520 |
series_multiselect = gr.CheckboxGroup(label="數值欄位", choices=[])
|
|
|
|
| 540 |
with gr.Row():
|
| 541 |
plot1 = gr.Plot(label="1:Line")
|
| 542 |
plot2 = gr.Plot(label="2:Bar / Dual Axis")
|
| 543 |
+
with gr.Row():
|
| 544 |
plot3 = gr.Plot(label="3:Rolling Mean")
|
| 545 |
+
plot4 = gr.Plot(label="4:Gauge")
|
| 546 |
|
| 547 |
with gr.Tab("地圖"):
|
| 548 |
map_out = gr.HTML(label="4:Geo Map (Interactive + Legend + Heatmap)")
|
|
|
|
| 556 |
df_view = gr.Dataframe(label="資料預覽(含 rolling)", wrap=True)
|
| 557 |
|
| 558 |
with gr.Tab("點位詳情"):
|
| 559 |
+
gr.Markdown("### 🔎 點位詳情(對應地圖彈窗中的 #ID,熱圖模式下不可用)")
|
| 560 |
point_selector = gr.Dropdown(label="選擇點位(#ID | 時間 | 值)", choices=[], value=None)
|
| 561 |
detail_view = gr.Dataframe(label="選取點詳細資料", wrap=True)
|
| 562 |
|
|
|
|
| 582 |
lambda: pipeline("drive", None, DRIVE_PRESETS[0], [], False, "5", "viridis", "OpenStreetMap", "", "", False),
|
| 583 |
inputs=None,
|
| 584 |
outputs=[
|
| 585 |
+
plot1, plot2, plot3, plot4, map_out,
|
| 586 |
json_box, json_file, df_view,
|
| 587 |
demo_csv_file,
|
| 588 |
point_selector, detail_view,
|
|
|
|
| 595 |
pipeline,
|
| 596 |
inputs=[source_radio, file_in, preset_dd, series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, start_time_in, end_time_in, heatmap_chk],
|
| 597 |
outputs=[
|
| 598 |
+
plot1, plot2, plot3, plot4, map_out,
|
| 599 |
json_box, json_file, df_view,
|
| 600 |
demo_csv_file,
|
| 601 |
point_selector, detail_view,
|
|
|
|
| 607 |
regenerate_demo,
|
| 608 |
inputs=[series_multiselect, dual_axis_chk, rolling_dd, cmap_dd, tiles_dd, point_selector, start_time_in, end_time_in, heatmap_chk],
|
| 609 |
outputs=[
|
| 610 |
+
plot1, plot2, plot3, plot4, map_out,
|
| 611 |
json_box, json_file, df_view,
|
| 612 |
demo_csv_file,
|
| 613 |
point_selector, detail_view,
|