cwadayi commited on
Commit
a3a987c
·
verified ·
1 Parent(s): 2cc6f78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -47
app.py CHANGED
@@ -7,17 +7,20 @@ import gradio as gr
7
  import pandas as pd
8
  import matplotlib.pyplot as plt
9
 
10
- # === grafanalib: 定義 Dashboard 結構(不負責畫圖) ===
11
- # 注意:grafanalib 只產生 Grafana JSON;我們會把 JSON 顯示出來,並用 Matplotlib 實際畫圖
12
  from grafanalib.core import (
13
- Dashboard, Graph, Row, Target, YAxis, YAxes, TimeRange # ⬅️ 加入 TimeRange
14
  )
15
 
16
  TAIPEI = tz.gettz("Asia/Taipei")
17
 
18
 
19
- def make_demo_dataframe():
20
- """產生示範資料(1 小時,1 分鐘 1 點),欄位:time, amplitude, count"""
 
 
 
21
  t0 = datetime.now(tz=TAIPEI) - timedelta(minutes=60)
22
  times = [t0 + timedelta(minutes=i) for i in range(61)]
23
  amp = [0.5 + 0.4 * __import__("math").sin(i / 6.0) for i in range(61)]
@@ -27,7 +30,7 @@ def make_demo_dataframe():
27
 
28
 
29
  def load_csv(file: gr.File | None) -> pd.DataFrame:
30
- """支援使用者上傳 CSV,要求至少包含 time 欄位;其他欄位可自訂"""
31
  if file is None:
32
  return make_demo_dataframe()
33
 
@@ -39,38 +42,58 @@ def load_csv(file: gr.File | None) -> pd.DataFrame:
39
 
40
  df[time_col] = pd.to_datetime(df[time_col])
41
  df.rename(columns={time_col: "time"}, inplace=True)
42
- # 保證有時區(預設轉為台北)
 
43
  if getattr(df["time"].dt, "tz", None) is None:
44
  df["time"] = df["time"].dt.tz_localize(TAIPEI)
 
 
 
 
 
45
  return df
46
 
47
 
 
 
 
48
  def build_grafanalib_dashboard(series_columns: list[str]) -> dict:
49
  """
50
- grafanalib 定義一個簡單 Dashboard
51
- - Row 1:Time Series(Graph)展示第一個數值欄
52
- - Row 2:Event Count(Graph)展示第二個數值欄(如有)
53
- 注意:Target 只是示意;實際取數由我們在 Gradio 端處理。
54
  """
55
- targets = [Target(expr=f"{series_columns[0]}", legendFormat=series_columns[0])]
56
- panels = [
 
 
57
  Graph(
58
  title=f"Time Series - {series_columns[0]}",
59
  dataSource="(example)",
60
- targets=targets,
 
 
 
 
61
  yAxes=YAxes(
62
- left=YAxis(format="short"), # 用 Grafana 內建單位 key;避免不支援的 "none"
63
  right=YAxis(format="short"),
64
  ),
65
  )
66
- ]
67
 
 
68
  if len(series_columns) > 1:
69
  panels.append(
70
  Graph(
71
- title=f"Event Count - {series_columns[1]}",
72
  dataSource="(example)",
73
  targets=[Target(expr=f"{series_columns[1]}", legendFormat=series_columns[1])],
 
 
 
 
74
  yAxes=YAxes(
75
  left=YAxis(format="short"),
76
  right=YAxis(format="short"),
@@ -78,22 +101,45 @@ def build_grafanalib_dashboard(series_columns: list[str]) -> dict:
78
  )
79
  )
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  dash = Dashboard(
82
  title="Seismology Demo Dashboard (grafanalib + Gradio)",
83
  rows=[Row(panels=panels)],
84
  timezone="browser",
85
- time=TimeRange("now-1h", "now"), # ⬅️ 正確做法:用 TimeRange 取代 time_from/time_to
86
  )
87
  return dash.to_json_data()
88
 
89
 
 
 
 
90
  def render_matplotlib(df: pd.DataFrame, value_cols: list[str]):
91
  """
92
- 用 Matplotlib 依據 df 實際畫圖,對應 grafanalib 定義的欄位。
93
- 回傳多個 matplotlib Figure 物件(給 gr.Gallery 使用)
 
 
 
94
  """
95
  figs = []
96
 
 
97
  fig1 = plt.figure()
98
  plt.plot(df["time"], df[value_cols[0]])
99
  plt.title(f"Time Series - {value_cols[0]}")
@@ -103,97 +149,132 @@ def render_matplotlib(df: pd.DataFrame, value_cols: list[str]):
103
  plt.tight_layout()
104
  figs.append(fig1)
105
 
 
106
  if len(value_cols) > 1:
107
  fig2 = plt.figure()
108
- plt.plot(df["time"], df[value_cols[1]])
109
- plt.title(f"Event Count - {value_cols[1]}")
 
110
  plt.xlabel("Time")
111
  plt.ylabel(value_cols[1])
112
  plt.xticks(rotation=20)
113
  plt.tight_layout()
114
  figs.append(fig2)
115
 
116
- return figs
 
 
 
117
 
 
 
 
 
 
 
 
 
118
 
 
 
 
 
 
 
119
  def pipeline(file, series_choice):
120
  """
121
- 主要管線:
122
- 1) 讀 CSV(或用示範資料)
123
- 2) 決定要展示的欄位
124
- 3) grafanalib 產出 Dashboard JSON
125
- 4) Matplotlib 畫圖
126
  """
127
  df = load_csv(file)
128
 
 
129
  numeric_cols = [c for c in df.columns if c != "time" and pd.api.types.is_numeric_dtype(df[c])]
130
  if not numeric_cols:
131
- raise ValueError("未找到可繪圖的數值欄位。請在 CSV 中提供至少一個數值欄位(除了 time 以外)。")
132
 
 
133
  chosen = series_choice or numeric_cols[:2]
134
  chosen = [c for c in chosen if c in numeric_cols]
135
  if not chosen:
136
  chosen = numeric_cols[:2]
137
 
 
138
  dash_json = build_grafanalib_dashboard(chosen)
139
- figs = render_matplotlib(df, chosen)
140
 
141
- dash_json_str = json.dumps(dash_json, ensure_ascii=False, indent=2, default=str)
 
142
 
 
 
143
  json_bytes = io.BytesIO(dash_json_str.encode("utf-8"))
144
  json_bytes.name = "dashboard.json"
145
 
146
- return figs, dash_json_str, json_bytes, df
147
 
148
 
 
 
 
149
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
150
- gr.Markdown("# grafanalib 生成 Dashboard + Gradio 呈現\n"
151
- "上傳 CSV(需含 `time` 欄)或用示範資料,選擇要畫的數值欄位。\n\n"
152
- "**說明**:grafanalib 產生 Grafana 的 Dashboard JSON;下方 Matplotlib 圖是 Space 端實際呈現。")
 
 
 
 
153
 
154
  with gr.Row():
155
  file_in = gr.File(label="上傳 CSV(可空,會用示範資料)", file_types=[".csv"])
156
- series_multiselect = gr.CheckboxGroup(label="選擇要呈現的數值欄位(不選則自動挑前兩���)", choices=[])
 
 
 
157
 
158
  run_btn = gr.Button("產生 Dashboard 並繪圖")
159
 
160
  with gr.Row():
161
- gallery = gr.Gallery(label="圖表預覽(Matplotlib)", height=400)
 
162
  with gr.Row():
163
- json_box = gr.Code(label="grafanalib 輸出的 Dashboard JSON(可匯入真正的 Grafana)", language="json")
 
164
  with gr.Row():
165
  json_file = gr.File(label="下載 dashboard.json")
166
 
167
- df_view = gr.Dataframe(label="資料預覽(自動偵測 time + 數值欄位)", wrap=True)
168
 
 
169
  def probe_columns(file):
170
- """當檔案上傳時,更新欄位選項和資料預覽"""
171
  df = load_csv(file)
172
  numeric_cols = [c for c in df.columns if c != "time" and pd.api.types.is_numeric_dtype(df[c])]
173
- return gr.CheckboxGroup(choices=numeric_cols), df
 
 
174
 
 
175
  def initial_load():
176
- """在 App 載入時,用示範資料跑一次完整流程"""
177
  figs, dash_json, json_file_obj, df = pipeline(file=None, series_choice=[])
178
  numeric_cols = [c for c in df.columns if c != "time" and pd.api.types.is_numeric_dtype(df[c])]
179
- chosen = numeric_cols[:2]
180
- return figs, dash_json, json_file_obj, df, gr.CheckboxGroup(choices=numeric_cols, value=chosen)
181
 
182
- # 頁面載入:直接顯示範例圖與 JSON
183
  demo.load(
184
  initial_load,
185
  inputs=None,
186
  outputs=[gallery, json_box, json_file, df_view, series_multiselect]
187
  )
188
 
189
- # 上傳新檔:更新欄位清單與表格預覽
190
  file_in.change(
191
  probe_columns,
192
  inputs=[file_in],
193
  outputs=[series_multiselect, df_view],
194
  )
195
 
196
- # 產生與繪圖
197
  run_btn.click(
198
  pipeline,
199
  inputs=[file_in, series_multiselect],
 
7
  import pandas as pd
8
  import matplotlib.pyplot as plt
9
 
10
+ # === grafanalib: 只用來「定義」Grafana Dashboard JSON 結構 ===
11
+ # 注意:實際畫圖在 Space 端用 Matplotlib 完成
12
  from grafanalib.core import (
13
+ Dashboard, Graph, Row, Target, YAxis, YAxes, Time
14
  )
15
 
16
  TAIPEI = tz.gettz("Asia/Taipei")
17
 
18
 
19
+ # -----------------------------
20
+ # Demo / Data loading
21
+ # -----------------------------
22
+ def make_demo_dataframe() -> pd.DataFrame:
23
+ """產生示範資料(1 小時、每分鐘 1 點)"""
24
  t0 = datetime.now(tz=TAIPEI) - timedelta(minutes=60)
25
  times = [t0 + timedelta(minutes=i) for i in range(61)]
26
  amp = [0.5 + 0.4 * __import__("math").sin(i / 6.0) for i in range(61)]
 
30
 
31
 
32
  def load_csv(file: gr.File | None) -> pd.DataFrame:
33
+ """支援上傳 CSV(需含時間欄:time/timestamp/datetime/date),自動轉 tz=Asia/Taipei。"""
34
  if file is None:
35
  return make_demo_dataframe()
36
 
 
42
 
43
  df[time_col] = pd.to_datetime(df[time_col])
44
  df.rename(columns={time_col: "time"}, inplace=True)
45
+
46
+ # 轉換 / 補上時區
47
  if getattr(df["time"].dt, "tz", None) is None:
48
  df["time"] = df["time"].dt.tz_localize(TAIPEI)
49
+ else:
50
+ df["time"] = df["time"].dt.tz_convert(TAIPEI)
51
+
52
+ # 依時間排序,避免畫圖時軸亂序
53
+ df = df.sort_values("time").reset_index(drop=True)
54
  return df
55
 
56
 
57
+ # -----------------------------
58
+ # grafanalib JSON builder
59
+ # -----------------------------
60
  def build_grafanalib_dashboard(series_columns: list[str]) -> dict:
61
  """
62
+ 產生 Grafana Dashboard JSON
63
+ - Panel 1:Line(第一個數值欄位)
64
+ - Panel 2:Bar(第二個數值欄位;若不存在則忽略)
65
+ - Panel 3:Line(第一個數值欄位的 5 點 rolling mean)
66
  """
67
+ panels = []
68
+
69
+ # Panel 1: 第一個欄位(折線)
70
+ panels.append(
71
  Graph(
72
  title=f"Time Series - {series_columns[0]}",
73
  dataSource="(example)",
74
+ targets=[Target(expr=f"{series_columns[0]}", legendFormat=series_columns[0])],
75
+ # 折線圖
76
+ lines=True,
77
+ bars=False,
78
+ points=False,
79
  yAxes=YAxes(
80
+ left=YAxis(format="short"),
81
  right=YAxis(format="short"),
82
  ),
83
  )
84
+ )
85
 
86
+ # Panel 2: 第二個欄位(柱狀)- 若有第二個欄位
87
  if len(series_columns) > 1:
88
  panels.append(
89
  Graph(
90
+ title=f"Event Count (Bar) - {series_columns[1]}",
91
  dataSource="(example)",
92
  targets=[Target(expr=f"{series_columns[1]}", legendFormat=series_columns[1])],
93
+ # 柱狀圖
94
+ lines=False,
95
+ bars=True,
96
+ points=False,
97
  yAxes=YAxes(
98
  left=YAxis(format="short"),
99
  right=YAxis(format="short"),
 
101
  )
102
  )
103
 
104
+ # Panel 3: 第一個欄位 Rolling Mean(折線)
105
+ panels.append(
106
+ Graph(
107
+ title=f"Rolling Mean (5) - {series_columns[0]}",
108
+ dataSource="(example)",
109
+ targets=[Target(expr=f"{series_columns[0]}_rolling5", legendFormat=f"{series_columns[0]}_rolling5")],
110
+ lines=True,
111
+ bars=False,
112
+ points=False,
113
+ yAxes=YAxes(
114
+ left=YAxis(format="short"),
115
+ right=YAxis(format="short"),
116
+ ),
117
+ )
118
+ )
119
+
120
  dash = Dashboard(
121
  title="Seismology Demo Dashboard (grafanalib + Gradio)",
122
  rows=[Row(panels=panels)],
123
  timezone="browser",
124
+ time=Time("now-1h", "now"),
125
  )
126
  return dash.to_json_data()
127
 
128
 
129
+ # -----------------------------
130
+ # Matplotlib rendering
131
+ # -----------------------------
132
  def render_matplotlib(df: pd.DataFrame, value_cols: list[str]):
133
  """
134
+ 依據 df 實際畫圖:
135
+ - 圖1:第一個欄位(折線)
136
+ - 圖2:第二個欄位(柱狀;若不存在則略過)
137
+ - 圖3:第一個欄位的 5 點 rolling mean(折線)
138
+ 回傳 List[Figure],給 gr.Gallery 顯示。
139
  """
140
  figs = []
141
 
142
+ # 圖1:折線(第一欄)
143
  fig1 = plt.figure()
144
  plt.plot(df["time"], df[value_cols[0]])
145
  plt.title(f"Time Series - {value_cols[0]}")
 
149
  plt.tight_layout()
150
  figs.append(fig1)
151
 
152
+ # 圖2:柱狀(第二欄,若存在)
153
  if len(value_cols) > 1:
154
  fig2 = plt.figure()
155
+ # 使用條狀圖
156
+ plt.bar(df["time"], df[value_cols[1]])
157
+ plt.title(f"Event Count (Bar) - {value_cols[1]}")
158
  plt.xlabel("Time")
159
  plt.ylabel(value_cols[1])
160
  plt.xticks(rotation=20)
161
  plt.tight_layout()
162
  figs.append(fig2)
163
 
164
+ # 圖3:Rolling Mean(對第一欄做 5 點移動平均)
165
+ rolling_col = f"{value_cols[0]}_rolling5"
166
+ if rolling_col not in df.columns:
167
+ df[rolling_col] = df[value_cols[0]].rolling(window=5, min_periods=1, center=False).mean()
168
 
169
+ fig3 = plt.figure()
170
+ plt.plot(df["time"], df[rolling_col])
171
+ plt.title(f"Rolling Mean (5) - {value_cols[0]}")
172
+ plt.xlabel("Time")
173
+ plt.ylabel(rolling_col)
174
+ plt.xticks(rotation=20)
175
+ plt.tight_layout()
176
+ figs.append(fig3)
177
 
178
+ return figs, df # 回傳 df 讓上游可帶出 rolling 欄位
179
+
180
+
181
+ # -----------------------------
182
+ # Main pipeline
183
+ # -----------------------------
184
  def pipeline(file, series_choice):
185
  """
186
+ 主要流程:
187
+ 1) 讀 CSV(或示範資料)
188
+ 2) 選擇要展示的欄位(第一欄必定存在;第二欄選配)
189
+ 3) 建立 grafanalib Dashboard JSON(含第三個 rolling 面板)
190
+ 4) Matplotlib 繪圖(第2圖為柱狀;第3圖為 rolling mean)
191
  """
192
  df = load_csv(file)
193
 
194
+ # 取得數值欄位(排除 time)
195
  numeric_cols = [c for c in df.columns if c != "time" and pd.api.types.is_numeric_dtype(df[c])]
196
  if not numeric_cols:
197
+ raise ValueError("未找到可繪圖的數值欄位。請提供至少一個數值欄位(除了 time 以外)。")
198
 
199
+ # 使用者選擇優先;否則預設取前兩個
200
  chosen = series_choice or numeric_cols[:2]
201
  chosen = [c for c in chosen if c in numeric_cols]
202
  if not chosen:
203
  chosen = numeric_cols[:2]
204
 
205
+ # 建 JSON(第三面板會參照 f"{chosen[0]}_rolling5")
206
  dash_json = build_grafanalib_dashboard(chosen)
 
207
 
208
+ # 畫圖(第2圖為柱狀;第3圖為 rolling)
209
+ figs, df_with_rolling = render_matplotlib(df.copy(), chosen)
210
 
211
+ # JSON 輸出與下載檔
212
+ dash_json_str = json.dumps(dash_json, ensure_ascii=False, indent=2, default=str)
213
  json_bytes = io.BytesIO(dash_json_str.encode("utf-8"))
214
  json_bytes.name = "dashboard.json"
215
 
216
+ return figs, dash_json_str, json_bytes, df_with_rolling
217
 
218
 
219
+ # -----------------------------
220
+ # UI
221
+ # -----------------------------
222
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
223
+ gr.Markdown(
224
+ "# grafanalib 生成 Dashboard + Gradio 呈現\n"
225
+ "- 第一個面板:折線(第一個數值欄)\n"
226
+ "- 第二個面板:柱狀(第二個數值欄,若有)\n"
227
+ "- 第三個面板:第一個欄位的 5 點移動平均(折線)\n\n"
228
+ "**說明**:grafanalib 產生可匯入 Grafana 的 Dashboard JSON;下方 Matplotlib 圖為 Space 端即時呈現。"
229
+ )
230
 
231
  with gr.Row():
232
  file_in = gr.File(label="上傳 CSV(可空,會用示範資料)", file_types=[".csv"])
233
+ series_multiselect = gr.CheckboxGroup(
234
+ label="選擇要呈現的數值欄位(第一個欄位用於折線&rolling;第二個欄位用於柱狀,選配)",
235
+ choices=[]
236
+ )
237
 
238
  run_btn = gr.Button("產生 Dashboard 並繪圖")
239
 
240
  with gr.Row():
241
+ gallery = gr.Gallery(label="圖表預覽(1:Line, 2:Bar, 3:Rolling Line)", height=420)
242
+
243
  with gr.Row():
244
+ json_box = gr.Code(label="grafanalib Dashboard JSON(可匯入真正的 Grafana)", language="json")
245
+
246
  with gr.Row():
247
  json_file = gr.File(label="下載 dashboard.json")
248
 
249
+ df_view = gr.Dataframe(label="資料預覽(包含 rolling 欄位)", wrap=True)
250
 
251
+ # 依檔案動態更新欄位選單與表格
252
  def probe_columns(file):
 
253
  df = load_csv(file)
254
  numeric_cols = [c for c in df.columns if c != "time" and pd.api.types.is_numeric_dtype(df[c])]
255
+ # 預設勾選前兩個(若只有一個,就只勾一個)
256
+ default_select = numeric_cols[:2]
257
+ return gr.CheckboxGroup(choices=numeric_cols, value=default_select), df
258
 
259
+ # 初次載入:以 Demo 資料跑一次
260
  def initial_load():
 
261
  figs, dash_json, json_file_obj, df = pipeline(file=None, series_choice=[])
262
  numeric_cols = [c for c in df.columns if c != "time" and pd.api.types.is_numeric_dtype(df[c])]
263
+ default_select = numeric_cols[:2]
264
+ return figs, dash_json, json_file_obj, df, gr.CheckboxGroup(choices=numeric_cols, value=default_select)
265
 
 
266
  demo.load(
267
  initial_load,
268
  inputs=None,
269
  outputs=[gallery, json_box, json_file, df_view, series_multiselect]
270
  )
271
 
 
272
  file_in.change(
273
  probe_columns,
274
  inputs=[file_in],
275
  outputs=[series_multiselect, df_view],
276
  )
277
 
 
278
  run_btn.click(
279
  pipeline,
280
  inputs=[file_in, series_multiselect],