Kung-Hsun commited on
Commit
53578f1
·
verified ·
1 Parent(s): f2b4c7d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -28
app.py CHANGED
@@ -6,16 +6,29 @@ import os
6
  from datetime import datetime, time
7
  from typing import Union, Optional, Tuple, Dict, List
8
  import plotly.graph_objects as go
 
9
 
10
  EXCEL_LETTERS = ["A", "B", "K", "L", "M", "V", "W", "X", "Y"]
11
  TARGET_NAMES = ["data", "time", "⊿Ptop", "⊿Pmid", "⊿Pbot", "H2%", "CO%", "CO2%", "CH4%"]
12
 
 
13
  DEFAULT_COLORS = [
14
  "#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd",
15
  "#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"
16
  ]
17
  DASH_OPTIONS = ["solid","dot","dash","longdash","dashdot","longdashdot"]
18
 
 
 
 
 
 
 
 
 
 
 
 
19
  def letters_to_index_zero_based(letter: str) -> int:
20
  idx = 0
21
  for ch in letter.upper():
@@ -198,7 +211,6 @@ def prepare_x_series(df: pd.DataFrame, x_col: str) -> Tuple[pd.Series, bool]:
198
  return num, False
199
 
200
  def build_default_style_rows(y_cols: List[str], existing: Optional[pd.DataFrame]=None) -> pd.DataFrame:
201
- """依照 Y 選擇產生/更新樣式表,盡量保留既有設定。"""
202
  rows = []
203
  for i, y in enumerate(y_cols):
204
  default = {
@@ -217,13 +229,12 @@ def build_default_style_rows(y_cols: List[str], existing: Optional[pd.DataFrame]
217
  return pd.DataFrame(rows, columns=["series","color","width","dash"])
218
 
219
  def styles_to_map(style_df: Optional[pd.DataFrame]) -> Dict[str, Dict]:
220
- """將樣式表轉成 {series: {color, width, dash}} 的對應表,含容錯。"""
221
  m: Dict[str, Dict] = {}
222
  if not isinstance(style_df, pd.DataFrame) or style_df.empty:
223
  return m
224
  for _, r in style_df.iterrows():
225
  name = str(r.get("series","")).strip()
226
- if not name: # 跳過空列
227
  continue
228
  col = str(r.get("color","")).strip() or None
229
  w = r.get("width", None)
@@ -277,7 +288,6 @@ def make_line_figure(df: pd.DataFrame, x_col: str, y_cols: list,
277
  )
278
  )
279
 
280
- # 佈局
281
  fig.update_layout(
282
  width=int(fig_w) if fig_w else None,
283
  height=int(fig_h) if fig_h else None,
@@ -328,8 +338,9 @@ def make_line_figure(df: pd.DataFrame, x_col: str, y_cols: list,
328
  fig.update_xaxes(showgrid=True)
329
  return fig
330
 
331
- with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動線圖+樣式)") as demo:
332
- gr.Markdown("### 指定欄位擷取(A,B,K,L,M,V,W,X,Y)→ data,time,⊿Ptop,⊿Pmid,⊿Pbot,H2%,CO%,CO2%,CH4% ;支援 **時間區段 (hh:mm:ss)** 過濾與 **互動線圖**(Y 可複選,並可設定顏色/線寬/線型")
 
333
 
334
  df_state = gr.State(value=None)
335
  style_state = gr.State(value=None) # 保存目前樣式表(DataFrame)
@@ -376,7 +387,7 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
376
  y_max = gr.Textbox(label="Y 最大(數值)", value="")
377
  y_dtick = gr.Textbox(label="Y 刻度間距 dtick(數值)", value="")
378
 
379
- gr.Markdown("### 樣式(每個 Y 一列 - `dash` 可選:solid / dot / dash / longdash / dashdot / longdashdot")
380
  style_df = gr.Dataframe(
381
  label="系列樣式表",
382
  headers=["series","color","width","dash"],
@@ -385,25 +396,36 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
385
  col_count=(4, "fixed"),
386
  wrap=True
387
  )
388
- sync_btn = gr.Button("同步 Y 選擇 → 樣式表(保留既有設定)")
 
 
 
 
 
 
 
 
389
 
390
  plot_btn = gr.Button("繪製線圖(互動)")
391
  plot_out = gr.Plot(label="互動線圖")
392
  plot_msg = gr.Markdown()
393
 
 
394
  def run_pipeline(file_path_str, sh_, sm_, ss_, eh_, em_, es_):
395
  if not file_path_str:
 
396
  return (gr.update(visible=False), "請先上傳檔案。", pd.DataFrame(),
397
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
398
- pd.DataFrame(), None)
399
 
400
  try:
401
  df = load_dataframe(file_path_str)
402
  out = extract_and_rename(df)
403
  except Exception as e:
 
404
  return (gr.update(visible=False), f"處理失敗:{e}", pd.DataFrame(),
405
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
406
- pd.DataFrame(), None)
407
 
408
  original_rows = len(out)
409
 
@@ -411,16 +433,18 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
411
  start_sec = parse_time_to_seconds(sh_, sm_, ss_)
412
  end_sec = parse_time_to_seconds(eh_, em_, es_)
413
  except Exception as e:
 
414
  return (gr.update(visible=False), f"時間輸入錯誤:{e}", pd.DataFrame(),
415
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
416
- pd.DataFrame(), None)
417
 
418
  parsed_ok = None
419
  if (start_sec is not None) and (end_sec is not None):
420
  if "time" not in out.columns:
 
421
  return (gr.update(visible=False), "找不到 'time' 欄,無法做時間過濾。", pd.DataFrame(),
422
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
423
- pd.DataFrame(), None)
424
  secs = series_time_to_seconds_of_day(out["time"])
425
  parsed_ok = int(secs.notna().sum())
426
  valid_mask = secs.notna()
@@ -436,15 +460,15 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
436
  try:
437
  out.to_excel(out_path, index=False, engine="openpyxl")
438
  except Exception as e:
 
439
  return (gr.update(visible=False), f"輸出 Excel 失敗:{e}", pd.DataFrame(),
440
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
441
- pd.DataFrame(), None)
442
 
443
  cols = out.columns.tolist()
444
  default_x = "time" if "time" in cols else (cols[0] if cols else None)
445
  default_y = [c for c in ["H2%", "CO%", "CO2%", "CH4%"] if c in cols] or ([cols[1]] if len(cols) > 1 else cols)
446
 
447
- # 初始化樣式表
448
  init_style_df = build_default_style_rows(default_y)
449
 
450
  note_lines = [f"完成!原始列數:**{original_rows}**",
@@ -452,9 +476,19 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
452
  if parsed_ok is not None:
453
  note_lines.insert(1, f"可解析時間列數:**{parsed_ok}**")
454
  note_lines.insert(2, f"時間區段:**{pad_time(sh_, sm_, ss_)} → {pad_time(eh_, em_, es_)}**")
455
- note_lines.append("下方預覽、右側可下載 Excel於下方欄位與樣式繪圖。")
456
  note = "|".join(note_lines)
457
 
 
 
 
 
 
 
 
 
 
 
458
  return (
459
  gr.update(value=out_path, visible=True),
460
  note,
@@ -463,21 +497,46 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
463
  gr.update(choices=cols, value=default_x),
464
  gr.update(choices=cols, value=default_y),
465
  init_style_df,
466
- init_style_df # style_state
 
467
  )
468
 
469
  def sync_styles(style_df_current, y_cols):
470
  y_cols = y_cols or []
471
  existing = style_df_current if isinstance(style_df_current, pd.DataFrame) else None
472
  new_df = build_default_style_rows(y_cols, existing=existing)
473
- return new_df, new_df
 
 
 
 
 
 
 
 
 
474
 
475
  def on_y_change(style_df_current, y_cols):
476
  # 自動同步(保留既有設定)
 
 
 
477
  y_cols = y_cols or []
478
- existing = style_df_current if isinstance(style_df_current, pd.DataFrame) else None
479
- new_df = build_default_style_rows(y_cols, existing=existing)
480
- return new_df, new_df
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
  def plot_handler(df, x_col, y_cols, fig_w_, fig_h_, auto_x_, x_min_, x_max_, x_dtick_, auto_y_, y_min_, y_max_, y_dtick_, style_df_current):
483
  if df is None:
@@ -495,26 +554,46 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(含時間過濾+互動
495
  except Exception as e:
496
  return None, f"繪圖失敗:{e}"
497
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  run_btn.click(
499
  run_pipeline,
500
  inputs=[inp, sh, sm, ss, eh, em, es],
501
- outputs=[file_out, msg, preview, df_state, x_sel, y_sel, style_df, style_state]
502
  )
503
 
504
- # 變更 Y 選擇時,自動同步樣式表
505
  y_sel.change(
506
  on_y_change,
507
  inputs=[style_df, y_sel],
508
- outputs=[style_df, style_state]
509
  )
510
 
511
- # 手動按鈕同步(保險)
512
- sync_btn.click(
513
- sync_styles,
514
- inputs=[style_df, y_sel],
515
- outputs=[style_df, style_state]
516
  )
517
 
 
 
 
 
 
 
 
518
  plot_btn.click(
519
  plot_handler,
520
  inputs=[df_state, x_sel, y_sel, fig_w, fig_h, auto_x, x_min, x_max, x_dtick, auto_y, y_min, y_max, y_dtick, style_state],
 
6
  from datetime import datetime, time
7
  from typing import Union, Optional, Tuple, Dict, List
8
  import plotly.graph_objects as go
9
+ from functools import partial
10
 
11
  EXCEL_LETTERS = ["A", "B", "K", "L", "M", "V", "W", "X", "Y"]
12
  TARGET_NAMES = ["data", "time", "⊿Ptop", "⊿Pmid", "⊿Pbot", "H2%", "CO%", "CO2%", "CH4%"]
13
 
14
+ # 預設顏色/線型
15
  DEFAULT_COLORS = [
16
  "#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd",
17
  "#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"
18
  ]
19
  DASH_OPTIONS = ["solid","dot","dash","longdash","dashdot","longdashdot"]
20
 
21
+ # 色盤(含色弱友善方案)
22
+ PALETTES = {
23
+ "Plotly10": DEFAULT_COLORS,
24
+ "Tableau10": ["#4E79A7","#F28E2B","#E15759","#76B7B2","#59A14F","#EDC948","#B07AA1","#FF9DA7","#9C755F","#BAB0AC"],
25
+ "Set2": ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"],
26
+ "Dark2": ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"],
27
+ "Okabe-Ito (CB-safe)": ["#0072B2","#E69F00","#009E73","#D55E00","#CC79A7","#F0E442","#56B4E9","#000000"]
28
+ }
29
+
30
+ MAX_SERIES_FOR_PICKER = 10 # 最多顯示 10 個顏色挑選器
31
+
32
  def letters_to_index_zero_based(letter: str) -> int:
33
  idx = 0
34
  for ch in letter.upper():
 
211
  return num, False
212
 
213
  def build_default_style_rows(y_cols: List[str], existing: Optional[pd.DataFrame]=None) -> pd.DataFrame:
 
214
  rows = []
215
  for i, y in enumerate(y_cols):
216
  default = {
 
229
  return pd.DataFrame(rows, columns=["series","color","width","dash"])
230
 
231
  def styles_to_map(style_df: Optional[pd.DataFrame]) -> Dict[str, Dict]:
 
232
  m: Dict[str, Dict] = {}
233
  if not isinstance(style_df, pd.DataFrame) or style_df.empty:
234
  return m
235
  for _, r in style_df.iterrows():
236
  name = str(r.get("series","")).strip()
237
+ if not name:
238
  continue
239
  col = str(r.get("color","")).strip() or None
240
  w = r.get("width", None)
 
288
  )
289
  )
290
 
 
291
  fig.update_layout(
292
  width=int(fig_w) if fig_w else None,
293
  height=int(fig_h) if fig_h else None,
 
338
  fig.update_xaxes(showgrid=True)
339
  return fig
340
 
341
+ # ===== Gradio UI =====
342
+ with gr.Blocks(title="Excel/CSV 指定欄位擷取(時間過濾互動線圖+樣式)") as demo:
343
+ gr.Markdown("### 指定欄位擷取(A,B,K,L,M,V,W,X,Y)→ data,time,⊿Ptop,⊿Pmid,⊿Pbot,H2%,CO%,CO2%,CH4% ;支援 **時間區段 (hh:mm:ss)** 過濾與 **互動線圖**(Y 可複選)。")
344
 
345
  df_state = gr.State(value=None)
346
  style_state = gr.State(value=None) # 保存目前樣式表(DataFrame)
 
387
  y_max = gr.Textbox(label="Y 最大(數值)", value="")
388
  y_dtick = gr.Textbox(label="Y 刻度間距 dtick(數值)", value="")
389
 
390
+ gr.Markdown("### 樣式(線寬與線型在表格中編輯;顏色改用 ColorPicker 或色盤)")
391
  style_df = gr.Dataframe(
392
  label="系列樣式表",
393
  headers=["series","color","width","dash"],
 
396
  col_count=(4, "fixed"),
397
  wrap=True
398
  )
399
+
400
+ with gr.Row():
401
+ palette_dd = gr.Dropdown(label="色盤", choices=list(PALETTES.keys()), value="Okabe-Ito (CB-safe)")
402
+ apply_palette_btn = gr.Button("套用色盤到 Y(依序)")
403
+
404
+ gr.Markdown("#### 顏色挑選(最多顯示前 10 條線)")
405
+ color_pickers = []
406
+ for i in range(MAX_SERIES_FOR_PICKER):
407
+ color_pickers.append(gr.ColorPicker(label=f"系列 {i+1}", value="#000000", visible=False))
408
 
409
  plot_btn = gr.Button("繪製線圖(互動)")
410
  plot_out = gr.Plot(label="互動線圖")
411
  plot_msg = gr.Markdown()
412
 
413
+ # --------- 回呼邏輯 ---------
414
  def run_pipeline(file_path_str, sh_, sm_, ss_, eh_, em_, es_):
415
  if not file_path_str:
416
+ empty_updates = [gr.update(visible=False) for _ in range(MAX_SERIES_FOR_PICKER)]
417
  return (gr.update(visible=False), "請先上傳檔案。", pd.DataFrame(),
418
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
419
+ pd.DataFrame(), None, *empty_updates)
420
 
421
  try:
422
  df = load_dataframe(file_path_str)
423
  out = extract_and_rename(df)
424
  except Exception as e:
425
+ empty_updates = [gr.update(visible=False) for _ in range(MAX_SERIES_FOR_PICKER)]
426
  return (gr.update(visible=False), f"處理失敗:{e}", pd.DataFrame(),
427
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
428
+ pd.DataFrame(), None, *empty_updates)
429
 
430
  original_rows = len(out)
431
 
 
433
  start_sec = parse_time_to_seconds(sh_, sm_, ss_)
434
  end_sec = parse_time_to_seconds(eh_, em_, es_)
435
  except Exception as e:
436
+ empty_updates = [gr.update(visible=False) for _ in range(MAX_SERIES_FOR_PICKER)]
437
  return (gr.update(visible=False), f"時間輸入錯誤:{e}", pd.DataFrame(),
438
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
439
+ pd.DataFrame(), None, *empty_updates)
440
 
441
  parsed_ok = None
442
  if (start_sec is not None) and (end_sec is not None):
443
  if "time" not in out.columns:
444
+ empty_updates = [gr.update(visible=False) for _ in range(MAX_SERIES_FOR_PICKER)]
445
  return (gr.update(visible=False), "找不到 'time' 欄,無法做時間過濾。", pd.DataFrame(),
446
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
447
+ pd.DataFrame(), None, *empty_updates)
448
  secs = series_time_to_seconds_of_day(out["time"])
449
  parsed_ok = int(secs.notna().sum())
450
  valid_mask = secs.notna()
 
460
  try:
461
  out.to_excel(out_path, index=False, engine="openpyxl")
462
  except Exception as e:
463
+ empty_updates = [gr.update(visible=False) for _ in range(MAX_SERIES_FOR_PICKER)]
464
  return (gr.update(visible=False), f"輸出 Excel 失敗:{e}", pd.DataFrame(),
465
  None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
466
+ pd.DataFrame(), None, *empty_updates)
467
 
468
  cols = out.columns.tolist()
469
  default_x = "time" if "time" in cols else (cols[0] if cols else None)
470
  default_y = [c for c in ["H2%", "CO%", "CO2%", "CH4%"] if c in cols] or ([cols[1]] if len(cols) > 1 else cols)
471
 
 
472
  init_style_df = build_default_style_rows(default_y)
473
 
474
  note_lines = [f"完成!原始列數:**{original_rows}**",
 
476
  if parsed_ok is not None:
477
  note_lines.insert(1, f"可解析時間列數:**{parsed_ok}**")
478
  note_lines.insert(2, f"時間區段:**{pad_time(sh_, sm_, ss_)} → {pad_time(eh_, em_, es_)}**")
479
+ note_lines.append("下方預覽、右側可下載;可選欄位與樣式繪圖。")
480
  note = "|".join(note_lines)
481
 
482
+ # 初始顏色挑選器狀態
483
+ picker_updates = []
484
+ for i in range(MAX_SERIES_FOR_PICKER):
485
+ if i < len(default_y):
486
+ series = default_y[i]
487
+ c = init_style_df.loc[init_style_df["series"]==series, "color"].iloc[0]
488
+ picker_updates.append(gr.update(visible=True, value=c, label=f"{series} 顏色"))
489
+ else:
490
+ picker_updates.append(gr.update(visible=False))
491
+
492
  return (
493
  gr.update(value=out_path, visible=True),
494
  note,
 
497
  gr.update(choices=cols, value=default_x),
498
  gr.update(choices=cols, value=default_y),
499
  init_style_df,
500
+ init_style_df, # style_state
501
+ *picker_updates
502
  )
503
 
504
  def sync_styles(style_df_current, y_cols):
505
  y_cols = y_cols or []
506
  existing = style_df_current if isinstance(style_df_current, pd.DataFrame) else None
507
  new_df = build_default_style_rows(y_cols, existing=existing)
508
+
509
+ picker_updates = []
510
+ for i in range(MAX_SERIES_FOR_PICKER):
511
+ if i < len(y_cols):
512
+ series = y_cols[i]
513
+ c = new_df.loc[new_df["series"]==series, "color"].iloc[0]
514
+ picker_updates.append(gr.update(visible=True, value=c, label=f"{series} 顏色"))
515
+ else:
516
+ picker_updates.append(gr.update(visible=False))
517
+ return new_df, new_df, *picker_updates
518
 
519
  def on_y_change(style_df_current, y_cols):
520
  # 自動同步(保留既有設定)
521
+ return sync_styles(style_df_current, y_cols)
522
+
523
+ def apply_palette(style_df_current, y_cols, palette_name):
524
  y_cols = y_cols or []
525
+ df = build_default_style_rows(y_cols, existing=style_df_current)
526
+ pal = PALETTES.get(palette_name, DEFAULT_COLORS)
527
+ # y_cols 順序覆蓋 color
528
+ for i, s in enumerate(y_cols):
529
+ df.loc[df["series"]==s, "color"] = pal[i % len(pal)]
530
+ # 更新挑選器
531
+ picker_updates = []
532
+ for i in range(MAX_SERIES_FOR_PICKER):
533
+ if i < len(y_cols):
534
+ series = y_cols[i]
535
+ c = df.loc[df["series"]==series, "color"].iloc[0]
536
+ picker_updates.append(gr.update(visible=True, value=c, label=f"{series} 顏色"))
537
+ else:
538
+ picker_updates.append(gr.update(visible=False))
539
+ return df, df, *picker_updates
540
 
541
  def plot_handler(df, x_col, y_cols, fig_w_, fig_h_, auto_x_, x_min_, x_max_, x_dtick_, auto_y_, y_min_, y_max_, y_dtick_, style_df_current):
542
  if df is None:
 
554
  except Exception as e:
555
  return None, f"繪圖失敗:{e}"
556
 
557
+ def make_color_change_handler(idx):
558
+ def handler(style_df_current, y_cols, new_color):
559
+ y_cols = y_cols or []
560
+ if idx >= len(y_cols):
561
+ return style_df_current, style_df_current # 無變更
562
+ series = y_cols[idx]
563
+ df = build_default_style_rows(y_cols, existing=style_df_current)
564
+ df.loc[df["series"]==series, "color"] = new_color
565
+ return df, df
566
+ return handler
567
+
568
+ # 事件綁定
569
+ outputs_base = [file_out, msg, preview, df_state, x_sel, y_sel, style_df, style_state]
570
+ outputs_with_pickers = outputs_base + color_pickers
571
+
572
  run_btn.click(
573
  run_pipeline,
574
  inputs=[inp, sh, sm, ss, eh, em, es],
575
+ outputs=outputs_with_pickers
576
  )
577
 
 
578
  y_sel.change(
579
  on_y_change,
580
  inputs=[style_df, y_sel],
581
+ outputs=[style_df, style_state, *color_pickers]
582
  )
583
 
584
+ apply_palette_btn.click(
585
+ apply_palette,
586
+ inputs=[style_df, y_sel, palette_dd],
587
+ outputs=[style_df, style_state, *color_pickers]
 
588
  )
589
 
590
+ for i, picker in enumerate(color_pickers):
591
+ picker.change(
592
+ make_color_change_handler(i),
593
+ inputs=[style_df, y_sel, picker],
594
+ outputs=[style_df, style_state]
595
+ )
596
+
597
  plot_btn.click(
598
  plot_handler,
599
  inputs=[df_state, x_sel, y_sel, fig_w, fig_h, auto_x, x_min, x_max, x_dtick, auto_y, y_min, y_max, y_dtick, style_state],