Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,19 +6,17 @@ 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 |
-
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"],
|
|
@@ -27,8 +25,9 @@ PALETTES = {
|
|
| 27 |
"Okabe-Ito (CB-safe)": ["#0072B2","#E69F00","#009E73","#D55E00","#CC79A7","#F0E442","#56B4E9","#000000"]
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
| 31 |
|
|
|
|
| 32 |
def letters_to_index_zero_based(letter: str) -> int:
|
| 33 |
idx = 0
|
| 34 |
for ch in letter.upper():
|
|
@@ -47,7 +46,6 @@ def get_lower_name(file_input: Union[str, os.PathLike, io.BytesIO, bytes, object
|
|
| 47 |
|
| 48 |
def load_dataframe(file_input) -> pd.DataFrame:
|
| 49 |
lower_name = get_lower_name(file_input)
|
| 50 |
-
|
| 51 |
if isinstance(file_input, (str, os.PathLike)):
|
| 52 |
path = str(file_input)
|
| 53 |
if lower_name.endswith((".xlsx", ".xls")):
|
|
@@ -138,10 +136,8 @@ def _hhmmss_int_to_seconds(n: int):
|
|
| 138 |
|
| 139 |
def series_time_to_seconds_of_day(series: pd.Series) -> pd.Series:
|
| 140 |
s = series.copy()
|
| 141 |
-
|
| 142 |
if pd.api.types.is_datetime64_any_dtype(s):
|
| 143 |
return (s.dt.hour*3600 + s.dt.minute*60 + s.dt.second).astype("float")
|
| 144 |
-
|
| 145 |
if pd.api.types.is_timedelta64_dtype(s):
|
| 146 |
total_sec = s.dt.total_seconds()
|
| 147 |
return (total_sec % 86400).astype("float")
|
|
@@ -188,11 +184,9 @@ def parse_hhmmss_or_number(s: Optional[str]) -> Optional[float]:
|
|
| 188 |
parts = text.split(":")
|
| 189 |
try:
|
| 190 |
if len(parts) == 2:
|
| 191 |
-
mm, ss = int(parts[0]), int(parts[1])
|
| 192 |
-
return mm*60 + ss
|
| 193 |
elif len(parts) == 3:
|
| 194 |
-
hh, mm, ss = int(parts[0]), int(parts[1]), int(parts[2])
|
| 195 |
-
return hh*3600 + mm*60 + ss
|
| 196 |
except Exception:
|
| 197 |
return None
|
| 198 |
try:
|
|
@@ -210,42 +204,30 @@ def prepare_x_series(df: pd.DataFrame, x_col: str) -> Tuple[pd.Series, bool]:
|
|
| 210 |
num = pd.to_numeric(x, errors="coerce")
|
| 211 |
return num, False
|
| 212 |
|
| 213 |
-
def
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
"width": float(existing.loc[existing["series"]==y, "width"].iloc[0])
|
| 222 |
-
if isinstance(existing, pd.DataFrame) and (existing["series"]==y).any()
|
| 223 |
-
else 2.0,
|
| 224 |
-
"dash": (existing.loc[existing["series"]==y, "dash"].iloc[0]
|
| 225 |
-
if isinstance(existing, pd.DataFrame) and (existing["series"]==y).any()
|
| 226 |
-
else "solid"),
|
| 227 |
-
}
|
| 228 |
-
rows.append(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 |
-
|
| 234 |
-
|
| 235 |
-
for _, r in
|
| 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)
|
| 241 |
try:
|
| 242 |
-
|
| 243 |
except Exception:
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
return m
|
| 250 |
|
| 251 |
def make_line_figure(df: pd.DataFrame, x_col: str, y_cols: list,
|
|
@@ -271,20 +253,15 @@ def make_line_figure(df: pd.DataFrame, x_col: str, y_cols: list,
|
|
| 271 |
mask = x_series.notna() & y.notna()
|
| 272 |
if mask.sum() < 2:
|
| 273 |
continue
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
line_kwargs = {
|
| 277 |
-
"color": style.get("color") or DEFAULT_COLORS[idx % len(DEFAULT_COLORS)],
|
| 278 |
-
"width": style.get("width", 2.0),
|
| 279 |
-
"dash": style.get("dash", "solid")
|
| 280 |
-
}
|
| 281 |
fig.add_trace(
|
| 282 |
go.Scatter(
|
| 283 |
x=x_series[mask],
|
| 284 |
y=y[mask],
|
| 285 |
mode="lines",
|
| 286 |
name=y_col,
|
| 287 |
-
line=
|
| 288 |
)
|
| 289 |
)
|
| 290 |
|
|
@@ -338,12 +315,11 @@ def make_line_figure(df: pd.DataFrame, x_col: str, y_cols: list,
|
|
| 338 |
fig.update_xaxes(showgrid=True)
|
| 339 |
return fig
|
| 340 |
|
| 341 |
-
# =====
|
| 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)
|
| 347 |
|
| 348 |
inp = gr.File(label="上傳 .xlsx 或 .csv 檔案", file_types=[".xlsx", ".csv"], type="filepath")
|
| 349 |
|
|
@@ -387,13 +363,14 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 387 |
y_max = gr.Textbox(label="Y 最大(數值)", value="")
|
| 388 |
y_dtick = gr.Textbox(label="Y 刻度間距 dtick(數值)", value="")
|
| 389 |
|
| 390 |
-
|
|
|
|
| 391 |
style_df = gr.Dataframe(
|
| 392 |
-
label="系列
|
| 393 |
-
headers=["series","
|
| 394 |
-
datatype=["str","
|
| 395 |
row_count=(0, "dynamic"),
|
| 396 |
-
col_count=(
|
| 397 |
wrap=True
|
| 398 |
)
|
| 399 |
|
|
@@ -401,10 +378,22 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 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("#### 顏色挑選(
|
|
|
|
| 405 |
color_pickers = []
|
| 406 |
-
for
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
|
| 409 |
plot_btn = gr.Button("繪製線圖(互動)")
|
| 410 |
plot_out = gr.Plot(label="互動線圖")
|
|
@@ -412,20 +401,22 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 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(),
|
| 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(),
|
| 429 |
|
| 430 |
original_rows = len(out)
|
| 431 |
|
|
@@ -433,18 +424,16 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 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(),
|
| 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(),
|
| 448 |
secs = series_time_to_seconds_of_day(out["time"])
|
| 449 |
parsed_ok = int(secs.notna().sum())
|
| 450 |
valid_mask = secs.notna()
|
|
@@ -460,16 +449,16 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 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(),
|
| 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 |
-
|
|
|
|
| 473 |
|
| 474 |
note_lines = [f"完成!原始列數:**{original_rows}**",
|
| 475 |
f"輸出列數:**{len(out)}**"]
|
|
@@ -479,15 +468,16 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 479 |
note_lines.append("下方預覽、右側可下載;可選欄位與樣式後繪圖。")
|
| 480 |
note = "|".join(note_lines)
|
| 481 |
|
| 482 |
-
# 初始
|
| 483 |
-
picker_updates = []
|
| 484 |
-
for i in range(
|
| 485 |
if i < len(default_y):
|
| 486 |
series = default_y[i]
|
| 487 |
-
|
| 488 |
-
|
| 489 |
else:
|
| 490 |
picker_updates.append(gr.update(visible=False))
|
|
|
|
| 491 |
|
| 492 |
return (
|
| 493 |
gr.update(value=out_path, visible=True),
|
|
@@ -497,52 +487,58 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 497 |
gr.update(choices=cols, value=default_x),
|
| 498 |
gr.update(choices=cols, value=default_y),
|
| 499 |
init_style_df,
|
| 500 |
-
|
| 501 |
-
*
|
| 502 |
)
|
| 503 |
|
| 504 |
-
def
|
| 505 |
y_cols = y_cols or []
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
| 508 |
|
| 509 |
-
picker_updates = []
|
| 510 |
-
for i in range(
|
| 511 |
if i < len(y_cols):
|
| 512 |
series = y_cols[i]
|
| 513 |
-
|
| 514 |
-
|
| 515 |
else:
|
| 516 |
picker_updates.append(gr.update(visible=False))
|
| 517 |
-
|
| 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(
|
| 533 |
if i < len(y_cols):
|
| 534 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_,
|
|
|
|
| 542 |
if df is None:
|
| 543 |
return None, "尚未有可用資料,請先完成上方處理。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
try:
|
| 545 |
-
s_map = styles_to_map(style_df_current)
|
| 546 |
fig = make_line_figure(
|
| 547 |
df, x_col, y_cols or [],
|
| 548 |
fig_w=int(fig_w_ or 900), fig_h=int(fig_h_ or 500),
|
|
@@ -554,49 +550,35 @@ with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線
|
|
| 554 |
except Exception as e:
|
| 555 |
return None, f"繪圖失敗:{e}"
|
| 556 |
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 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=
|
| 576 |
)
|
| 577 |
|
|
|
|
| 578 |
y_sel.change(
|
| 579 |
-
|
| 580 |
inputs=[style_df, y_sel],
|
| 581 |
-
outputs=[style_df
|
| 582 |
)
|
| 583 |
|
|
|
|
| 584 |
apply_palette_btn.click(
|
| 585 |
apply_palette,
|
| 586 |
inputs=[style_df, y_sel, palette_dd],
|
| 587 |
-
outputs=[style_df
|
| 588 |
)
|
| 589 |
|
| 590 |
-
|
| 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,
|
| 600 |
outputs=[plot_out, plot_msg]
|
| 601 |
)
|
| 602 |
|
|
|
|
| 6 |
from datetime import datetime, time
|
| 7 |
from typing import Union, Optional, Tuple, Dict, List
|
| 8 |
import plotly.graph_objects as go
|
|
|
|
| 9 |
|
| 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 |
DEFAULT_COLORS = [
|
| 15 |
"#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd",
|
| 16 |
"#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"
|
| 17 |
]
|
| 18 |
DASH_OPTIONS = ["solid","dot","dash","longdash","dashdot","longdashdot"]
|
| 19 |
|
|
|
|
| 20 |
PALETTES = {
|
| 21 |
"Plotly10": DEFAULT_COLORS,
|
| 22 |
"Tableau10": ["#4E79A7","#F28E2B","#E15759","#76B7B2","#59A14F","#EDC948","#B07AA1","#FF9DA7","#9C755F","#BAB0AC"],
|
|
|
|
| 25 |
"Okabe-Ito (CB-safe)": ["#0072B2","#E69F00","#009E73","#D55E00","#CC79A7","#F0E442","#56B4E9","#000000"]
|
| 26 |
}
|
| 27 |
|
| 28 |
+
MAX_SERIES = 10 # 最多顯示 10 組(顏色 + 線型)
|
| 29 |
|
| 30 |
+
# ====== 工具函式 ======
|
| 31 |
def letters_to_index_zero_based(letter: str) -> int:
|
| 32 |
idx = 0
|
| 33 |
for ch in letter.upper():
|
|
|
|
| 46 |
|
| 47 |
def load_dataframe(file_input) -> pd.DataFrame:
|
| 48 |
lower_name = get_lower_name(file_input)
|
|
|
|
| 49 |
if isinstance(file_input, (str, os.PathLike)):
|
| 50 |
path = str(file_input)
|
| 51 |
if lower_name.endswith((".xlsx", ".xls")):
|
|
|
|
| 136 |
|
| 137 |
def series_time_to_seconds_of_day(series: pd.Series) -> pd.Series:
|
| 138 |
s = series.copy()
|
|
|
|
| 139 |
if pd.api.types.is_datetime64_any_dtype(s):
|
| 140 |
return (s.dt.hour*3600 + s.dt.minute*60 + s.dt.second).astype("float")
|
|
|
|
| 141 |
if pd.api.types.is_timedelta64_dtype(s):
|
| 142 |
total_sec = s.dt.total_seconds()
|
| 143 |
return (total_sec % 86400).astype("float")
|
|
|
|
| 184 |
parts = text.split(":")
|
| 185 |
try:
|
| 186 |
if len(parts) == 2:
|
| 187 |
+
mm, ss = int(parts[0]), int(parts[1]); return mm*60 + ss
|
|
|
|
| 188 |
elif len(parts) == 3:
|
| 189 |
+
hh, mm, ss = int(parts[0]), int(parts[1]), int(parts[2]); return hh*3600 + mm*60 + ss
|
|
|
|
| 190 |
except Exception:
|
| 191 |
return None
|
| 192 |
try:
|
|
|
|
| 204 |
num = pd.to_numeric(x, errors="coerce")
|
| 205 |
return num, False
|
| 206 |
|
| 207 |
+
def styles_to_map(y_cols: List[str], style_df: Optional[pd.DataFrame],
|
| 208 |
+
colors: List[Optional[str]], dashes: List[Optional[str]]) -> Dict[str, Dict]:
|
| 209 |
+
"""
|
| 210 |
+
最終用於繪圖的樣式:{series: {color, width, dash}}
|
| 211 |
+
- width 來自 style_df(使用者可編輯)
|
| 212 |
+
- color 來自 ColorPicker
|
| 213 |
+
- dash 來自 Radio
|
| 214 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
m: Dict[str, Dict] = {}
|
| 216 |
+
df = style_df if isinstance(style_df, pd.DataFrame) else pd.DataFrame(columns=["series","width"])
|
| 217 |
+
width_map = {}
|
| 218 |
+
for _, r in df.iterrows():
|
| 219 |
name = str(r.get("series","")).strip()
|
| 220 |
+
if not name: continue
|
|
|
|
|
|
|
|
|
|
| 221 |
try:
|
| 222 |
+
width_map[name] = float(r.get("width", 2.0))
|
| 223 |
except Exception:
|
| 224 |
+
width_map[name] = 2.0
|
| 225 |
+
|
| 226 |
+
for i, s in enumerate(y_cols):
|
| 227 |
+
col = colors[i] if i < len(colors) and colors[i] else DEFAULT_COLORS[i % len(DEFAULT_COLORS)]
|
| 228 |
+
dash = (dashes[i] if i < len(dashes) and (dashes[i] in DASH_OPTIONS) else "solid")
|
| 229 |
+
w = width_map.get(s, 2.0)
|
| 230 |
+
m[s] = {"color": col, "width": w, "dash": dash}
|
| 231 |
return m
|
| 232 |
|
| 233 |
def make_line_figure(df: pd.DataFrame, x_col: str, y_cols: list,
|
|
|
|
| 253 |
mask = x_series.notna() & y.notna()
|
| 254 |
if mask.sum() < 2:
|
| 255 |
continue
|
| 256 |
+
st = style_map.get(y_col, {"color": DEFAULT_COLORS[idx % len(DEFAULT_COLORS)],
|
| 257 |
+
"width": 2.0, "dash": "solid"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
fig.add_trace(
|
| 259 |
go.Scatter(
|
| 260 |
x=x_series[mask],
|
| 261 |
y=y[mask],
|
| 262 |
mode="lines",
|
| 263 |
name=y_col,
|
| 264 |
+
line=dict(color=st["color"], width=st["width"], dash=st["dash"])
|
| 265 |
)
|
| 266 |
)
|
| 267 |
|
|
|
|
| 315 |
fig.update_xaxes(showgrid=True)
|
| 316 |
return fig
|
| 317 |
|
| 318 |
+
# ====== 介面 ======
|
| 319 |
+
with gr.Blocks(title="Excel/CSV 指定欄位擷取器(時間過濾+互動線圖+色彩樣式)") as demo:
|
| 320 |
gr.Markdown("### 指定欄位擷取(A,B,K,L,M,V,W,X,Y)→ data,time,⊿Ptop,⊿Pmid,⊿Pbot,H2%,CO%,CO2%,CH4% ;支援 **時間區段 (hh:mm:ss)** 過濾與 **互動線圖**(Y 可複選)。")
|
| 321 |
|
| 322 |
df_state = gr.State(value=None)
|
|
|
|
| 323 |
|
| 324 |
inp = gr.File(label="上傳 .xlsx 或 .csv 檔案", file_types=[".xlsx", ".csv"], type="filepath")
|
| 325 |
|
|
|
|
| 363 |
y_max = gr.Textbox(label="Y 最大(數值)", value="")
|
| 364 |
y_dtick = gr.Textbox(label="Y 刻度間距 dtick(數值)", value="")
|
| 365 |
|
| 366 |
+
# ---- 樣式:寬度表(已移除 color 欄),顏色用 ColorPicker,線型用 Radio ----
|
| 367 |
+
gr.Markdown("### 樣式設定(寬度表 + ColorPicker + 線型 Radio)")
|
| 368 |
style_df = gr.Dataframe(
|
| 369 |
+
label="系列寬度(每條線一列)",
|
| 370 |
+
headers=["series","width"],
|
| 371 |
+
datatype=["str","number"],
|
| 372 |
row_count=(0, "dynamic"),
|
| 373 |
+
col_count=(2, "fixed"),
|
| 374 |
wrap=True
|
| 375 |
)
|
| 376 |
|
|
|
|
| 378 |
palette_dd = gr.Dropdown(label="色盤", choices=list(PALETTES.keys()), value="Okabe-Ito (CB-safe)")
|
| 379 |
apply_palette_btn = gr.Button("套用色盤到 Y(依序)")
|
| 380 |
|
| 381 |
+
gr.Markdown("#### 顏色挑選(前 10 條;縮短標籤以節省寬度)")
|
| 382 |
+
# 兩排 × 5 組 ColorPicker,縮短標籤(僅顯示系列名)
|
| 383 |
color_pickers = []
|
| 384 |
+
for row in range(2):
|
| 385 |
+
with gr.Row():
|
| 386 |
+
for i in range(5):
|
| 387 |
+
idx = row*5 + i
|
| 388 |
+
color_pickers.append(gr.ColorPicker(label=f"系列 {idx+1}", value="#000000", visible=False))
|
| 389 |
+
|
| 390 |
+
gr.Markdown("#### 線型(Radio 勾選;前 10 條)")
|
| 391 |
+
dash_radios = []
|
| 392 |
+
for row in range(2):
|
| 393 |
+
with gr.Row():
|
| 394 |
+
for i in range(5):
|
| 395 |
+
idx = row*5 + i
|
| 396 |
+
dash_radios.append(gr.Radio(label=f"系列 {idx+1}", choices=DASH_OPTIONS, value="solid", visible=False))
|
| 397 |
|
| 398 |
plot_btn = gr.Button("繪製線圖(互動)")
|
| 399 |
plot_out = gr.Plot(label="互動線圖")
|
|
|
|
| 401 |
|
| 402 |
# --------- 回呼邏輯 ---------
|
| 403 |
def run_pipeline(file_path_str, sh_, sm_, ss_, eh_, em_, es_):
|
| 404 |
+
# 基本輸出佔位
|
| 405 |
+
empty_color = [gr.update(visible=False) for _ in range(MAX_SERIES)]
|
| 406 |
+
empty_dash = [gr.update(visible=False) for _ in range(MAX_SERIES)]
|
| 407 |
+
|
| 408 |
if not file_path_str:
|
|
|
|
| 409 |
return (gr.update(visible=False), "請先上傳檔��。", pd.DataFrame(),
|
| 410 |
None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 411 |
+
pd.DataFrame(), *empty_color, *empty_dash)
|
| 412 |
|
| 413 |
try:
|
| 414 |
df = load_dataframe(file_path_str)
|
| 415 |
out = extract_and_rename(df)
|
| 416 |
except Exception as e:
|
|
|
|
| 417 |
return (gr.update(visible=False), f"處理失敗:{e}", pd.DataFrame(),
|
| 418 |
None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 419 |
+
pd.DataFrame(), *empty_color, *empty_dash)
|
| 420 |
|
| 421 |
original_rows = len(out)
|
| 422 |
|
|
|
|
| 424 |
start_sec = parse_time_to_seconds(sh_, sm_, ss_)
|
| 425 |
end_sec = parse_time_to_seconds(eh_, em_, es_)
|
| 426 |
except Exception as e:
|
|
|
|
| 427 |
return (gr.update(visible=False), f"時間輸入錯誤:{e}", pd.DataFrame(),
|
| 428 |
None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 429 |
+
pd.DataFrame(), *empty_color, *empty_dash)
|
| 430 |
|
| 431 |
parsed_ok = None
|
| 432 |
if (start_sec is not None) and (end_sec is not None):
|
| 433 |
if "time" not in out.columns:
|
|
|
|
| 434 |
return (gr.update(visible=False), "找不到 'time' 欄,無法做時間過濾。", pd.DataFrame(),
|
| 435 |
None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 436 |
+
pd.DataFrame(), *empty_color, *empty_dash)
|
| 437 |
secs = series_time_to_seconds_of_day(out["time"])
|
| 438 |
parsed_ok = int(secs.notna().sum())
|
| 439 |
valid_mask = secs.notna()
|
|
|
|
| 449 |
try:
|
| 450 |
out.to_excel(out_path, index=False, engine="openpyxl")
|
| 451 |
except Exception as e:
|
|
|
|
| 452 |
return (gr.update(visible=False), f"輸出 Excel 失敗:{e}", pd.DataFrame(),
|
| 453 |
None, gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 454 |
+
pd.DataFrame(), *empty_color, *empty_dash)
|
| 455 |
|
| 456 |
cols = out.columns.tolist()
|
| 457 |
default_x = "time" if "time" in cols else (cols[0] if cols else None)
|
| 458 |
default_y = [c for c in ["H2%", "CO%", "CO2%", "CH4%"] if c in cols] or ([cols[1]] if len(cols) > 1 else cols)
|
| 459 |
|
| 460 |
+
# 初始化寬度表(無 color 欄)
|
| 461 |
+
init_style_df = pd.DataFrame({"series": default_y, "width": [2.0]*len(default_y)})
|
| 462 |
|
| 463 |
note_lines = [f"完成!原始列數:**{original_rows}**",
|
| 464 |
f"輸出列數:**{len(out)}**"]
|
|
|
|
| 468 |
note_lines.append("下方預覽、右側可下載;可選欄位與樣式後繪圖。")
|
| 469 |
note = "|".join(note_lines)
|
| 470 |
|
| 471 |
+
# 初始 ColorPicker 與 Radio 顯示(兩排 × 5)
|
| 472 |
+
picker_updates, radio_updates = [], []
|
| 473 |
+
for i in range(MAX_SERIES):
|
| 474 |
if i < len(default_y):
|
| 475 |
series = default_y[i]
|
| 476 |
+
picker_updates.append(gr.update(visible=True, value=DEFAULT_COLORS[i % len(DEFAULT_COLORS)], label=series))
|
| 477 |
+
radio_updates.append(gr.update(visible=True, value="solid", label=series))
|
| 478 |
else:
|
| 479 |
picker_updates.append(gr.update(visible=False))
|
| 480 |
+
radio_updates.append(gr.update(visible=False))
|
| 481 |
|
| 482 |
return (
|
| 483 |
gr.update(value=out_path, visible=True),
|
|
|
|
| 487 |
gr.update(choices=cols, value=default_x),
|
| 488 |
gr.update(choices=cols, value=default_y),
|
| 489 |
init_style_df,
|
| 490 |
+
*picker_updates,
|
| 491 |
+
*radio_updates
|
| 492 |
)
|
| 493 |
|
| 494 |
+
def sync_on_y_change(style_df_current, y_cols):
|
| 495 |
y_cols = y_cols or []
|
| 496 |
+
# 寬度表刷新(保留既有 width)
|
| 497 |
+
exist = style_df_current if isinstance(style_df_current, pd.DataFrame) else pd.DataFrame(columns=["series","width"])
|
| 498 |
+
width_map = {str(r["series"]): r["width"] for _, r in exist.iterrows()} if not exist.empty else {}
|
| 499 |
+
new_df = pd.DataFrame({"series": y_cols, "width": [width_map.get(s, 2.0) for s in y_cols]})
|
| 500 |
|
| 501 |
+
picker_updates, radio_updates = [], []
|
| 502 |
+
for i in range(MAX_SERIES):
|
| 503 |
if i < len(y_cols):
|
| 504 |
series = y_cols[i]
|
| 505 |
+
picker_updates.append(gr.update(visible=True, value=DEFAULT_COLORS[i % len(DEFAULT_COLORS)], label=series))
|
| 506 |
+
radio_updates.append(gr.update(visible=True, value="solid", label=series))
|
| 507 |
else:
|
| 508 |
picker_updates.append(gr.update(visible=False))
|
| 509 |
+
radio_updates.append(gr.update(visible=False))
|
| 510 |
+
return new_df, *picker_updates, *radio_updates
|
|
|
|
|
|
|
|
|
|
| 511 |
|
| 512 |
def apply_palette(style_df_current, y_cols, palette_name):
|
| 513 |
y_cols = y_cols or []
|
|
|
|
| 514 |
pal = PALETTES.get(palette_name, DEFAULT_COLORS)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
picker_updates = []
|
| 516 |
+
for i in range(MAX_SERIES):
|
| 517 |
if i < len(y_cols):
|
| 518 |
+
picker_updates.append(gr.update(visible=True, value=pal[i % len(pal)], label=y_cols[i]))
|
|
|
|
|
|
|
| 519 |
else:
|
| 520 |
picker_updates.append(gr.update(visible=False))
|
| 521 |
+
# Radio 不變,只更新顏色
|
| 522 |
+
radio_updates = []
|
| 523 |
+
for i in range(MAX_SERIES):
|
| 524 |
+
if i < len(y_cols):
|
| 525 |
+
radio_updates.append(gr.update(visible=True, value="solid", label=y_cols[i]))
|
| 526 |
+
else:
|
| 527 |
+
radio_updates.append(gr.update(visible=False))
|
| 528 |
+
# 寬度表保持不變
|
| 529 |
+
return style_df_current, *picker_updates, *radio_updates
|
| 530 |
|
| 531 |
+
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_,
|
| 532 |
+
style_df_current, *color_and_dash):
|
| 533 |
if df is None:
|
| 534 |
return None, "尚未有可用資料,請先完成上方處理。"
|
| 535 |
+
|
| 536 |
+
# 拆分 color 與 dash(各 MAX_SERIES 個)
|
| 537 |
+
colors = list(color_and_dash[:MAX_SERIES])
|
| 538 |
+
dashes = list(color_and_dash[MAX_SERIES:MAX_SERIES*2])
|
| 539 |
+
|
| 540 |
try:
|
| 541 |
+
s_map = styles_to_map(y_cols or [], style_df_current, colors, dashes)
|
| 542 |
fig = make_line_figure(
|
| 543 |
df, x_col, y_cols or [],
|
| 544 |
fig_w=int(fig_w_ or 900), fig_h=int(fig_h_ or 500),
|
|
|
|
| 550 |
except Exception as e:
|
| 551 |
return None, f"繪圖失敗:{e}"
|
| 552 |
|
| 553 |
+
# 綁定事件
|
| 554 |
+
base_outputs = [file_out, msg, preview, df_state, x_sel, y_sel, style_df]
|
| 555 |
+
color_outputs = color_pickers
|
| 556 |
+
dash_outputs = dash_radios
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
|
| 558 |
run_btn.click(
|
| 559 |
run_pipeline,
|
| 560 |
inputs=[inp, sh, sm, ss, eh, em, es],
|
| 561 |
+
outputs=base_outputs + color_outputs + dash_outputs
|
| 562 |
)
|
| 563 |
|
| 564 |
+
# Y 變更時同��(更新寬度表、ColorPicker 與 Radio)
|
| 565 |
y_sel.change(
|
| 566 |
+
sync_on_y_change,
|
| 567 |
inputs=[style_df, y_sel],
|
| 568 |
+
outputs=[style_df] + color_outputs + dash_outputs
|
| 569 |
)
|
| 570 |
|
| 571 |
+
# 套用色盤(只變更顏色挑選器;Radio 與寬度表不動)
|
| 572 |
apply_palette_btn.click(
|
| 573 |
apply_palette,
|
| 574 |
inputs=[style_df, y_sel, palette_dd],
|
| 575 |
+
outputs=[style_df] + color_outputs + dash_outputs
|
| 576 |
)
|
| 577 |
|
| 578 |
+
# 繪圖(把所有 ColorPicker 與 Radio 的值都丟進 handler)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
plot_btn.click(
|
| 580 |
plot_handler,
|
| 581 |
+
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_df] + color_pickers + dash_radios,
|
| 582 |
outputs=[plot_out, plot_msg]
|
| 583 |
)
|
| 584 |
|