Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 332 |
-
|
|
|
|
| 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("### 樣式(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("下方預覽、右側可下載
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
def on_y_change(style_df_current, y_cols):
|
| 476 |
# 自動同步(保留既有設定)
|
|
|
|
|
|
|
|
|
|
| 477 |
y_cols = y_cols or []
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
|
| 513 |
-
|
| 514 |
-
|
| 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],
|