s880453 commited on
Commit
d021b9c
·
verified ·
1 Parent(s): 2887397

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -143
app.py CHANGED
@@ -1236,8 +1236,8 @@ def recommend_chart_settings(df):
1236
  """
1237
  Gradio 應用程式:進階數據可視化工具
1238
  作者:Gemini
1239
- 版本:5.1 (修正 TypeError + Y 軸加入無選項)
1240
- 描述:包含所有功能的完整程式碼,修正繪圖函數錯誤並改進 Y 軸選項。
1241
  """
1242
 
1243
  # =========================================
@@ -1251,14 +1251,12 @@ import plotly.graph_objects as go
1251
  import io
1252
  import base64
1253
  from PIL import Image
1254
- # import matplotlib.pyplot as plt # Matplotlib/Seaborn 在此版本中未使用,暫時註解
1255
- # import seaborn as sns # Matplotlib/Seaborn 在此版本中未使用,暫時註解
1256
  from plotly.subplots import make_subplots
1257
  import re
1258
  import json
1259
  import colorsys
1260
  import traceback # 用於更詳細的錯誤追蹤
1261
- import sys # 用於打印調試信息
1262
 
1263
  # =========================================
1264
  # == 常數定義 (Constants) ==
@@ -1281,7 +1279,7 @@ NONE_STR = "無" # 代表 '無' 選項的值
1281
  # =========================================
1282
  # == 輔助函數 (Helper Functions) ==
1283
  # =========================================
1284
- # --- 顏色處理相關 (與 V4 相同) ---
1285
  COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
1286
  COLOR_CARD_TEMPLATE = """<div title="{color_name} ({color_hex})" style="width: 20px; height: 20px; background-color: {color_hex}; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; transition: transform 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.1);" onclick="copyToClipboard('{color_hex}')" onmouseover="this.style.transform='scale(1.15)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.2)';" onmouseout="this.style.transform='scale(1)'; this.style.boxShadow='0 1px 2px rgba(0,0,0,0.1)';"></div>"""
1287
  COPY_SCRIPT = """
@@ -1351,7 +1349,6 @@ def update_patterns(*patterns_input):
1351
  # == 數據處理函數 (Data Processing Functions) ==
1352
  # =========================================
1353
  def process_upload(file):
1354
- # (與 V4 相同)
1355
  if file is None: return None, "❌ 未上傳任何文件。"
1356
  try:
1357
  file_path = file.name; file_type = file_path.split('.')[-1].lower()
@@ -1370,7 +1367,6 @@ def process_upload(file):
1370
  except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
1371
 
1372
  def parse_data(text_data):
1373
- # (與 V4 相同)
1374
  if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
1375
  try:
1376
  data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
@@ -1386,43 +1382,38 @@ def parse_data(text_data):
1386
  except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
1387
 
1388
  def update_columns_as_radio(df):
1389
- """修改:更新列選擇為 Radio 選項,並為 Y 軸添加 '無'"""
1390
  no_data_choices = [NO_DATA_STR]
1391
  no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
1392
 
1393
  if df is None or df.empty:
1394
- # 返回空的 Radio 更新
1395
  no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1396
  no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1397
- return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none # Y 軸包含 "無"
1398
 
1399
  try:
1400
  columns = df.columns.tolist()
1401
- valid_columns = [str(col) for col in columns if col is not None and str(col) != ""] # 確保列名是字符串
1402
- if not valid_columns: # 如果沒有有效列
1403
  no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1404
  no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1405
  return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
1406
 
1407
  x_default = valid_columns[0]
1408
- # Y 軸預設值:如果只有一列,預設選 '無';否則選第二列
1409
  y_default = NONE_STR if len(valid_columns) <= 1 else valid_columns[1]
1410
 
1411
- # Y, group, size 添加 "無" 選項
1412
- y_choices = [NONE_STR] + valid_columns # <--- 修改:Y 軸加入 "無"
1413
  group_choices = [NONE_STR] + valid_columns
1414
  size_choices = [NONE_STR] + valid_columns
1415
 
1416
- # 返回 Radio 更新對象
1417
  return (gr.Radio(choices=valid_columns, value=x_default, label="X�� / 類別"),
1418
- gr.Radio(choices=y_choices, value=y_default, label="Y軸 / 數值"), # <--- 修改:使用 y_choices
1419
  gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
1420
  gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
1421
  except Exception as e:
1422
  print(f"更新列選項 (Radio) 時出錯: {e}")
1423
  no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1424
  no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1425
- # 返回 Y 軸包含 "無" 的更新
1426
  return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
1427
 
1428
 
@@ -1433,27 +1424,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1433
  color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
1434
  show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
1435
  """
1436
- 根據用戶選擇創建 Plotly 圖表 (已加入 Null/空白 過濾)。
1437
-
1438
- Args:
1439
- df (pd.DataFrame): 輸入數據。
1440
- chart_type (str): 圖表類型。
1441
- x_column (str): X軸或類別列。
1442
- y_column (str): Y軸或數值列 (可能為 None)。
1443
- group_column (str, optional): 分組列 (可能為 "無" 或 None)。 Defaults to None.
1444
- size_column (str, optional): 大小列 (可能為 "無" 或 None)。 Defaults to None.
1445
- color_scheme_name (str, optional): 顏色方案名稱。 Defaults to "預設 (Plotly)".
1446
- patterns (list, optional): 圖案列表。 Defaults to [].
1447
- title (str, optional): 圖表標題。 Defaults to "".
1448
- width (int, optional): 圖表寬度。 Defaults to 800.
1449
- height (int, optional): 圖表高度。 Defaults to 500.
1450
- show_grid_str (str, optional): 是否顯示網格 ("是" 或 "否")。 Defaults to "是".
1451
- show_legend_str (str, optional): 是否顯示圖例 ("是" 或 "否")。 Defaults to "是".
1452
- agg_func_name (str, optional): 聚合函數名稱。 Defaults to "計數".
1453
- custom_colors_dict (dict, optional): 自定義顏色映射。 Defaults to {}.
1454
-
1455
- Returns:
1456
- go.Figure: Plotly 圖表對象。
1457
  """
1458
  # --- 添加調試信息 ---
1459
  print("-" * 20, file=sys.stderr)
@@ -1484,23 +1455,22 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1484
  raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
1485
  if not chart_type: raise ValueError("請選擇圖表類型。")
1486
  if not agg_func_name: raise ValueError("請選擇聚合函數。")
1487
- # NO_DATA_STR = "-- 無數據 --" # 確保此變數已定義或直接使用字符串
1488
  if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
1489
 
1490
  # 檢查列是否存在
1491
  if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
1492
 
1493
- # 判斷是否需要 Y 軸
 
1494
  y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
1495
 
1496
  if y_needed:
1497
- if not y_column or y_column == NO_DATA_STR: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列。")
1498
- if y_column not in df.columns: raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。可用列: {', '.join(df.columns)}")
1499
- else:
1500
- y_column = None # 如果不需要 Y 軸,明確設為 None
1501
 
1502
  # 處理可選列 (從 Radio 傳來的值可能是 NONE_STR)
1503
- # NONE_STR = "無" # 確保此變數已定義或直接使用字符串
1504
  group_col = None if group_column == NONE_STR or not group_column else group_column
1505
  size_col = None if size_column == NONE_STR or not size_column else size_column
1506
 
@@ -1513,12 +1483,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1513
 
1514
  # --- NEW: 過濾 Null/空白值 ---
1515
  columns_to_filter = [x_column]
1516
- if y_needed and y_column: # Filter Y only if it's needed and selected
1517
- columns_to_filter.append(y_column)
1518
  if group_col:
1519
  columns_to_filter.append(group_col)
1520
 
1521
- # 移除在關鍵列中有 Null (NaN, None) 值的行
1522
  valid_columns_to_filter = [col for col in columns_to_filter if col in df_processed.columns]
1523
  if valid_columns_to_filter:
1524
  original_rows = len(df_processed)
@@ -1527,11 +1496,9 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1527
  else:
1528
  print("警告: 沒有有效的列用於 Null 值過濾。", file=sys.stderr)
1529
 
1530
- # 對於 X 軸和分組列,額外移除空白字符串 (轉換為字符串後判斷)
1531
  if x_column in df_processed.columns:
1532
  try:
1533
  original_rows = len(df_processed)
1534
- # 僅移除完全是空白或空字符串的行
1535
  df_processed = df_processed[df_processed[x_column].astype(str).str.strip() != '']
1536
  print(f"移除 X 軸 '{x_column}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
1537
  except Exception as e:
@@ -1545,22 +1512,20 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1545
  except Exception as e:
1546
  print(f"警告: 過濾分組列 '{group_col}' 空白字符串時出錯: {e}", file=sys.stderr)
1547
 
1548
- # 檢查過濾後是否還有數據
1549
  if df_processed.empty:
1550
  raise ValueError("過濾掉 Null 或空白值後,沒有剩餘數據可供繪圖。")
1551
  # --- END NEW ---
1552
 
1553
 
1554
  # --- 2. 數據類型轉換與準備 ---
1555
- # 將 X 軸和分組列強制轉為字符串,以便正確分組
1556
  df_processed[x_column] = df_processed[x_column].astype(str)
1557
  if group_col:
1558
  df_processed[group_col] = df_processed[group_col].astype(str)
1559
 
1560
- # 嘗試將 Y 軸和大小列轉為數值
1561
- if y_column: # 這裡 y_column 可能為 None
1562
- try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='coerce')
1563
- except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column}' 為數值時出錯: {e}")
1564
  if size_col:
1565
  try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
1566
  except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
@@ -1569,49 +1534,48 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1569
  # --- 3. 數據聚合 (如果需要) ---
1570
  needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
1571
  agg_df = None
1572
- y_col_agg = y_column # 預設 Y 軸列名 (可能為 None)
1573
  if needs_aggregation:
1574
  grouping_cols = [x_column] + ([group_col] if group_col else [])
1575
- # 檢查分組列是否有效 (已在驗證部分完成)
1576
 
1577
  if agg_func_name == "計數":
1578
- # 使用 size() 計算每個組的行數
1579
- agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size() # dropna=False 包含 NaN 類別 (已被過濾?)
1580
- agg_df = agg_df.reset_index(name='__count__')
1581
- y_col_agg = '__count__' # 使用新生成的計數列
1582
  else:
1583
  agg_func_pd = agg_function_map(agg_func_name)
1584
- if not y_column: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
1585
  # 確保 Y 軸是數值類型 (除非 first/last)
1586
- if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_column]):
1587
- # 嘗試再次轉換,如果失敗則報錯
1588
- try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='raise')
1589
- except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
 
 
 
1590
 
1591
  try:
1592
  # 執行聚合
1593
- agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd)
1594
  agg_df = agg_df.reset_index()
1595
- y_col_agg = y_column # 保持原始列名
1596
  except Exception as agg_e:
1597
  raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
1598
  else:
1599
- # 不需要聚合,直接使用處理過的數據
1600
  agg_df = df_processed
1601
- y_col_agg = y_column # 保持原始列名 (可能為 None)
1602
 
1603
- # 再次檢查聚合後的 DataFrame
1604
  if agg_df is None or agg_df.empty:
1605
- raise ValueError("數據聚合後沒有產生有效結果。")
 
1606
  # 確保繪圖所需的列存在於 agg_df 中
1607
  required_cols_for_plot = [x_column]
1608
- # 只有在 y_col_agg 有效時才加入檢查
1609
- if y_col_agg: required_cols_for_plot.append(y_col_agg)
1610
  if group_col: required_cols_for_plot.append(group_col)
1611
  if size_col: required_cols_for_plot.append(size_col)
1612
  missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
1613
  if missing_cols:
1614
- raise ValueError(f"聚合後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
1615
 
1616
 
1617
  # --- 4. 獲取顏色方案 ---
@@ -1620,10 +1584,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1620
  # --- 5. 創建圖表 (核心邏輯) ---
1621
  fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
1622
  if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
1623
- # 確定實際用於繪圖的 Y 軸列名 (可能是 '__count__' 或原始 Y 列名)
1624
- effective_y = y_col_agg # 使用聚合後確定的 Y 軸列名
1625
 
1626
  # --- (繪圖邏輯開始) ---
 
 
1627
  if chart_type == "長條圖":
1628
  if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
1629
  fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
@@ -1670,29 +1635,26 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1670
  fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
1671
  if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(str(cat), colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
1672
  elif chart_type == "散點圖":
1673
- # 散點圖不需要聚合,使用原始 y_column
1674
- if not y_column: raise ValueError("散點圖需要選擇 Y 軸列。")
1675
- fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
1676
  elif chart_type == "氣泡圖":
1677
- if not y_column: raise ValueError("氣泡圖需要選擇 Y 軸列。")
1678
  if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
1679
  if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
1680
- fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, size_max=60, **fig_params)
1681
  elif chart_type == "直方圖":
1682
- # 直方圖使用原始數據的 x_column
1683
  if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
1684
  fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
1685
  elif chart_type == "箱型圖":
1686
- # 箱型圖使用原始 y_column
1687
- if not y_column: raise ValueError("箱型圖需要選擇 Y 軸列。")
1688
- if not pd.api.types.is_numeric_dtype(agg_df[y_column]): raise ValueError(f"箱型圖的 Y 軸列 '{y_column}' 必須是數值類型。")
1689
- fig = px.box(agg_df, x=group_col, y=y_column, color=group_col, **fig_params)
1690
- if not group_col: fig = px.box(agg_df, y=y_column, **fig_params)
1691
  elif chart_type == "小提琴圖":
1692
- if not y_column: raise ValueError("小提琴圖需要選擇 Y 軸列。")
1693
- if not pd.api.types.is_numeric_dtype(agg_df[y_column]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_column}' 必須是數值類型。")
1694
- fig = px.violin(agg_df, x=group_col, y=y_column, color=group_col, box=True, points="all", **fig_params)
1695
- if not group_col: fig = px.violin(agg_df, y=y_column, box=True, points="all", **fig_params)
1696
  elif chart_type == "熱力圖":
1697
  if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
1698
  if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
@@ -1731,7 +1693,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1731
  if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
1732
  fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
1733
  elif chart_type == "甘特圖":
1734
- start_col_gantt = y_column; end_col_gantt = group_col; task_col_gantt = x_column
1735
  if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
1736
  try:
1737
  df_gantt = df.copy() # 使用原始 df
@@ -1791,18 +1753,18 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1791
  if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
1792
  elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
1793
  elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
1794
- elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column) # 甘特圖軸標籤
1795
  else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
1796
 
1797
- except ValueError as ve:
1798
- print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
1799
- fig = go.Figure(); fig.add_annotation(text=f"⚠️ 創建圖表時出錯:<br>{ve}", align='left', showarrow=False, font=dict(size=14, color="red")); fig.update_layout(xaxis_visible=False, yaxis_visible=False)
1800
- except Exception as e:
1801
- error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
1802
- fig = go.Figure(); user_error_msg = f"⚠️ 創建圖表時發生內部錯誤。<br>請檢查數據和設置。<br>詳細錯誤: {str(e)[:100]}..."; fig.add_annotation(text=user_error_msg, align='left', showarrow=False, font=dict(size=14, color="red")); fig.update_layout(xaxis_visible=False, yaxis_visible=False)
1803
 
1804
- print("create_plot 函數執行完畢。", file=sys.stderr) # 調試信息
1805
- return fig
1806
 
1807
 
1808
  # =========================================
@@ -1810,52 +1772,52 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1810
  # =========================================
1811
  # (與 V4 相同)
1812
  def export_data(df, format_type):
1813
- if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
1814
- try:
1815
- if format_type == "CSV": filename = "exported_data.csv"; df.to_csv(filename, index=False, encoding='utf-8-sig')
1816
- elif format_type == "Excel": filename = "exported_data.xlsx"; df.to_excel(filename, index=False)
1817
- elif format_type == "JSON": filename = "exported_data.json"; df.to_json(filename, orient="records", indent=4, force_ascii=False)
1818
- else: return None, f"❌ 不支持的導出格式: {format_type}"
1819
- return filename, f"✅ 數據已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1820
- except Exception as e: print(f"導出數據時出錯: {e}"); traceback.print_exc(); return None, f"❌ 導出數據時出錯: {e}"
1821
 
1822
  def download_figure(fig, format_type="PNG"):
1823
- if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
1824
- try:
1825
- format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
1826
- import kaleido # 確保導入
1827
- fig.write_image(filename, format=format_lower)
1828
- return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1829
- except ImportError: error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
1830
- except ValueError as ve:
1831
- if "kaleido" in str(ve).lower(): error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"; print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
1832
- else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
1833
- except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
1834
 
1835
  # =========================================
1836
  # == 智能推薦函數 (Recommendation Function) ==
1837
  # =========================================
1838
  # (與 V4 相同)
1839
  def recommend_chart_settings(df):
1840
- recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
1841
- if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
1842
- columns = df.columns.tolist(); num_cols = df.select_dtypes(include=np.number).columns.tolist(); cat_cols = df.select_dtypes(include=['object', 'category', 'string']).columns.tolist()
1843
- date_cols = [col for col in columns if pd.api.types.is_datetime64_any_dtype(df[col]) or ('日期' in col or '時間' in col)]
1844
- try:
1845
- if date_cols and num_cols: recommendation.update({"chart_type": "折線圖", "x_column": date_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到時間列 '{date_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用折線圖顯示趨勢。"})
1846
- elif len(num_cols) >= 2: recommendation.update({"chart_type": "散點圖", "x_column": num_cols[0], "y_column": num_cols[1], "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}' 和 '{num_cols[1]}',推薦使用散點圖分析相關性。"})
1847
- elif cat_cols and num_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到類別列 '{cat_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用長條圖比較各類別的數值。"})
1848
- elif len(cat_cols) >= 2: recommendation.update({"chart_type": "堆疊長條圖", "x_column": cat_cols[0], "y_column": None, "group_column": cat_cols[1], "agg_function": "計數", "message": f"檢測到多個類別列 ('{cat_cols[0]}', '{cat_cols[1]}', ...),推薦使用堆疊長條圖顯示計數分佈。"})
1849
- elif cat_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": None, "agg_function": "計數", "message": f"檢測到類別列 '{cat_cols[0]}',推薦使用長條圖顯示其頻數分佈。"})
1850
- elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
1851
- else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
1852
- except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
1853
- if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
1854
- if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
1855
- if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
1856
- if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
1857
- if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
1858
- return recommendation
1859
 
1860
  # =========================================
1861
  # == CSS 樣式 (CSS Styling) ==
 
1236
  """
1237
  Gradio 應用程式:進階數據可視化工具
1238
  作者:Gemini
1239
+ 版本:5.2 (完整修正版 - 清理格式, 確保一致性)
1240
+ 描述:包含所有功能的完整程式碼,修正導入、CSS、格式問題。
1241
  """
1242
 
1243
  # =========================================
 
1251
  import io
1252
  import base64
1253
  from PIL import Image
 
 
1254
  from plotly.subplots import make_subplots
1255
  import re
1256
  import json
1257
  import colorsys
1258
  import traceback # 用於更詳細的錯誤追蹤
1259
+ import sys # 用於打印調試信息 - 已加入
1260
 
1261
  # =========================================
1262
  # == 常數定義 (Constants) ==
 
1279
  # =========================================
1280
  # == 輔助函數 (Helper Functions) ==
1281
  # =========================================
1282
+ # --- 顏色處理相關 ---
1283
  COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
1284
  COLOR_CARD_TEMPLATE = """<div title="{color_name} ({color_hex})" style="width: 20px; height: 20px; background-color: {color_hex}; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; transition: transform 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.1);" onclick="copyToClipboard('{color_hex}')" onmouseover="this.style.transform='scale(1.15)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.2)';" onmouseout="this.style.transform='scale(1)'; this.style.boxShadow='0 1px 2px rgba(0,0,0,0.1)';"></div>"""
1285
  COPY_SCRIPT = """
 
1349
  # == 數據處理函數 (Data Processing Functions) ==
1350
  # =========================================
1351
  def process_upload(file):
 
1352
  if file is None: return None, "❌ 未上傳任何文件。"
1353
  try:
1354
  file_path = file.name; file_type = file_path.split('.')[-1].lower()
 
1367
  except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
1368
 
1369
  def parse_data(text_data):
 
1370
  if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
1371
  try:
1372
  data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
 
1382
  except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
1383
 
1384
  def update_columns_as_radio(df):
1385
+ """更新列選擇為 Radio 選項,並為 Y/Group/Size 軸添加 '無'"""
1386
  no_data_choices = [NO_DATA_STR]
1387
  no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
1388
 
1389
  if df is None or df.empty:
 
1390
  no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1391
  no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1392
+ return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
1393
 
1394
  try:
1395
  columns = df.columns.tolist()
1396
+ valid_columns = [str(col) for col in columns if col is not None and str(col) != ""]
1397
+ if not valid_columns:
1398
  no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1399
  no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1400
  return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
1401
 
1402
  x_default = valid_columns[0]
 
1403
  y_default = NONE_STR if len(valid_columns) <= 1 else valid_columns[1]
1404
 
1405
+ y_choices = [NONE_STR] + valid_columns
 
1406
  group_choices = [NONE_STR] + valid_columns
1407
  size_choices = [NONE_STR] + valid_columns
1408
 
 
1409
  return (gr.Radio(choices=valid_columns, value=x_default, label="X�� / 類別"),
1410
+ gr.Radio(choices=y_choices, value=y_default, label="Y軸 / 數值"),
1411
  gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
1412
  gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
1413
  except Exception as e:
1414
  print(f"更新列選項 (Radio) 時出錯: {e}")
1415
  no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1416
  no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
 
1417
  return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
1418
 
1419
 
 
1424
  color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
1425
  show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
1426
  """
1427
+ 根據用戶選擇創建 Plotly 圖表 (已加入 Null/空白 過濾)。 V5.2 版
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1428
  """
1429
  # --- 添加調試信息 ---
1430
  print("-" * 20, file=sys.stderr)
 
1455
  raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
1456
  if not chart_type: raise ValueError("請選擇圖表類型。")
1457
  if not agg_func_name: raise ValueError("請選擇聚合函數。")
 
1458
  if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
1459
 
1460
  # 檢查列是否存在
1461
  if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
1462
 
1463
+ # 判斷是否需要 Y 軸 (修正 V5.1 錯誤: y_column 可能來自 Radio 且值為 NONE_STR)
1464
+ y_column_selected = None if y_column == NONE_STR or y_column == NO_DATA_STR or not y_column else y_column
1465
  y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
1466
 
1467
  if y_needed:
1468
+ if not y_column_selected: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列 (不能選 '無')。")
1469
+ if y_column_selected not in df.columns: raise ValueError(f"Y 軸列 '{y_column_selected}' 不在數據中。可用列: {', '.join(df.columns)}")
1470
+ # else:
1471
+ # y_column_selected 保持為 None
1472
 
1473
  # 處理可選列 (從 Radio 傳來的值可能是 NONE_STR)
 
1474
  group_col = None if group_column == NONE_STR or not group_column else group_column
1475
  size_col = None if size_column == NONE_STR or not size_column else size_column
1476
 
 
1483
 
1484
  # --- NEW: 過濾 Null/空白值 ---
1485
  columns_to_filter = [x_column]
1486
+ if y_needed and y_column_selected: # Filter Y only if it's needed and selected
1487
+ columns_to_filter.append(y_column_selected)
1488
  if group_col:
1489
  columns_to_filter.append(group_col)
1490
 
 
1491
  valid_columns_to_filter = [col for col in columns_to_filter if col in df_processed.columns]
1492
  if valid_columns_to_filter:
1493
  original_rows = len(df_processed)
 
1496
  else:
1497
  print("警告: 沒有有效的列用於 Null 值過濾。", file=sys.stderr)
1498
 
 
1499
  if x_column in df_processed.columns:
1500
  try:
1501
  original_rows = len(df_processed)
 
1502
  df_processed = df_processed[df_processed[x_column].astype(str).str.strip() != '']
1503
  print(f"移除 X 軸 '{x_column}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
1504
  except Exception as e:
 
1512
  except Exception as e:
1513
  print(f"警告: 過濾分組列 '{group_col}' 空白字符串時出錯: {e}", file=sys.stderr)
1514
 
 
1515
  if df_processed.empty:
1516
  raise ValueError("過濾掉 Null 或空白值後,沒有剩餘數據可供繪圖。")
1517
  # --- END NEW ---
1518
 
1519
 
1520
  # --- 2. 數據類型轉換與準備 ---
 
1521
  df_processed[x_column] = df_processed[x_column].astype(str)
1522
  if group_col:
1523
  df_processed[group_col] = df_processed[group_col].astype(str)
1524
 
1525
+ # 僅在需要時轉換 Y 軸和大小列
1526
+ if y_column_selected:
1527
+ try: df_processed[y_column_selected] = pd.to_numeric(df_processed[y_column_selected], errors='coerce')
1528
+ except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column_selected}' 為數值時出錯: {e}")
1529
  if size_col:
1530
  try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
1531
  except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
 
1534
  # --- 3. 數據聚合 (如果需要) ---
1535
  needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
1536
  agg_df = None
1537
+ y_col_agg = y_column_selected # 使用處理過的 Y 軸列名 (可能為 None)
1538
  if needs_aggregation:
1539
  grouping_cols = [x_column] + ([group_col] if group_col else [])
 
1540
 
1541
  if agg_func_name == "計數":
1542
+ agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
1543
+ y_col_agg = '__count__'
 
 
1544
  else:
1545
  agg_func_pd = agg_function_map(agg_func_name)
1546
+ if not y_column_selected: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列 (不能選 '無')。")
1547
  # 確保 Y 軸是數值類型 (除非 first/last)
1548
+ if agg_func_pd not in ['first', 'last']:
1549
+ if not pd.api.types.is_numeric_dtype(df_processed[y_column_selected]):
1550
+ try: df_processed[y_column_selected] = pd.to_numeric(df_processed[y_column_selected], errors='raise')
1551
+ except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column_selected}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
1552
+ # 檢查轉換後是否有非 NaN 值
1553
+ if df_processed[y_column_selected].isnull().all():
1554
+ raise ValueError(f"Y 軸列 '{y_column_selected}' 在轉換為數值後全為無效值 (NaN),無法執行聚合 '{agg_func_name}'。")
1555
 
1556
  try:
1557
  # 執行聚合
1558
+ agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column_selected].agg(agg_func_pd)
1559
  agg_df = agg_df.reset_index()
1560
+ y_col_agg = y_column_selected # 保持原始列名
1561
  except Exception as agg_e:
1562
  raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
1563
  else:
 
1564
  agg_df = df_processed
1565
+ y_col_agg = y_column_selected # 保持處理過的 Y 軸列名 (可能為 None)
1566
 
 
1567
  if agg_df is None or agg_df.empty:
1568
+ raise ValueError("數據聚合或處理後沒有產生有效結果。")
1569
+
1570
  # 確保繪圖所需的列存在於 agg_df 中
1571
  required_cols_for_plot = [x_column]
1572
+ # 修正:只有在 y_col_agg 實際有值(不是 None)時才加入檢查
1573
+ if y_col_agg is not None: required_cols_for_plot.append(y_col_agg)
1574
  if group_col: required_cols_for_plot.append(group_col)
1575
  if size_col: required_cols_for_plot.append(size_col)
1576
  missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
1577
  if missing_cols:
1578
+ raise ValueError(f"處理後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
1579
 
1580
 
1581
  # --- 4. 獲取顏色方案 ---
 
1584
  # --- 5. 創建圖表 (核心邏輯) ---
1585
  fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
1586
  if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
1587
+ effective_y = y_col_agg # 使用聚合後或處理過的 Y 軸列名
 
1588
 
1589
  # --- (繪圖邏輯開始) ---
1590
+ # 修正:繪圖時使用 y_column_selected (處理過的 Y 軸) 而不是原始 y_column
1591
+ # 修正:甘特圖需要原始 df,而不是 df_processed 或 agg_df
1592
  if chart_type == "長條圖":
1593
  if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
1594
  fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
 
1635
  fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
1636
  if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(str(cat), colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
1637
  elif chart_type == "散點圖":
1638
+ if not y_column_selected: raise ValueError("散點圖需要選擇 Y 軸列。")
1639
+ fig = px.scatter(agg_df, x=x_column, y=y_column_selected, color=group_col, size=size_col, **fig_params)
 
1640
  elif chart_type == "氣泡圖":
1641
+ if not y_column_selected: raise ValueError("氣泡圖需要選擇 Y 軸列。")
1642
  if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
1643
  if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
1644
+ fig = px.scatter(agg_df, x=x_column, y=y_column_selected, color=group_col, size=size_col, size_max=60, **fig_params)
1645
  elif chart_type == "直方圖":
 
1646
  if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
1647
  fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
1648
  elif chart_type == "箱型圖":
1649
+ if not y_column_selected: raise ValueError("箱型圖需要選擇 Y 軸列。")
1650
+ if not pd.api.types.is_numeric_dtype(agg_df[y_column_selected]): raise ValueError(f"箱型圖的 Y 軸列 '{y_column_selected}' 必須是數值類型。")
1651
+ fig = px.box(agg_df, x=group_col, y=y_column_selected, color=group_col, **fig_params)
1652
+ if not group_col: fig = px.box(agg_df, y=y_column_selected, **fig_params)
 
1653
  elif chart_type == "小提琴圖":
1654
+ if not y_column_selected: raise ValueError("小提琴圖需要選擇 Y 軸列。")
1655
+ if not pd.api.types.is_numeric_dtype(agg_df[y_column_selected]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_column_selected}' 必須是數值類型。")
1656
+ fig = px.violin(agg_df, x=group_col, y=y_column_selected, color=group_col, box=True, points="all", **fig_params)
1657
+ if not group_col: fig = px.violin(agg_df, y=y_column_selected, box=True, points="all", **fig_params)
1658
  elif chart_type == "熱力圖":
1659
  if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
1660
  if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
 
1693
  if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
1694
  fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
1695
  elif chart_type == "甘特圖":
1696
+ start_col_gantt = y_column_selected; end_col_gantt = group_col; task_col_gantt = x_column
1697
  if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
1698
  try:
1699
  df_gantt = df.copy() # 使用原始 df
 
1753
  if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
1754
  elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
1755
  elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
1756
+ elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=task_col_gantt) # 使用任務列作為 Y 軸標籤
1757
  else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
1758
 
1759
+     except ValueError as ve:
1760
+         print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
1761
+         fig = go.Figure(); fig.add_annotation(text=f"⚠️ 創建圖表時出錯:<br>{ve}", align='left', showarrow=False, font=dict(size=14, color="red")); fig.update_layout(xaxis_visible=False, yaxis_visible=False)
1762
+     except Exception as e:
1763
+         error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
1764
+         fig = go.Figure(); user_error_msg = f"⚠️ 創建圖表時發生內部錯誤。<br>請檢查數據和設置。<br>詳細錯誤: {str(e)[:100]}..."; fig.add_annotation(text=user_error_msg, align='left', showarrow=False, font=dict(size=14, color="red")); fig.update_layout(xaxis_visible=False, yaxis_visible=False)
1765
 
1766
+     print("create_plot 函數執行完畢。", file=sys.stderr) # 調試信息
1767
+     return fig
1768
 
1769
 
1770
  # =========================================
 
1772
  # =========================================
1773
  # (與 V4 相同)
1774
  def export_data(df, format_type):
1775
+     if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
1776
+     try:
1777
+         if format_type == "CSV": filename = "exported_data.csv"; df.to_csv(filename, index=False, encoding='utf-8-sig')
1778
+         elif format_type == "Excel": filename = "exported_data.xlsx"; df.to_excel(filename, index=False)
1779
+         elif format_type == "JSON": filename = "exported_data.json"; df.to_json(filename, orient="records", indent=4, force_ascii=False)
1780
+         else: return None, f"❌ 不支持的導出格式: {format_type}"
1781
+         return filename, f"✅ 數據已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1782
+     except Exception as e: print(f"導出數據時出錯: {e}"); traceback.print_exc(); return None, f"❌ 導出數據時出錯: {e}"
1783
 
1784
  def download_figure(fig, format_type="PNG"):
1785
+     if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
1786
+     try:
1787
+         format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
1788
+         import kaleido # 確保導入
1789
+         fig.write_image(filename, format=format_lower)
1790
+         return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1791
+     except ImportError: error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
1792
+     except ValueError as ve:
1793
+          if "kaleido" in str(ve).lower(): error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"; print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
1794
+          else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
1795
+     except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
1796
 
1797
  # =========================================
1798
  # == 智能推薦函數 (Recommendation Function) ==
1799
  # =========================================
1800
  # (與 V4 相同)
1801
  def recommend_chart_settings(df):
1802
+     recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
1803
+     if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
1804
+     columns = df.columns.tolist(); num_cols = df.select_dtypes(include=np.number).columns.tolist(); cat_cols = df.select_dtypes(include=['object', 'category', 'string']).columns.tolist()
1805
+     date_cols = [col for col in columns if pd.api.types.is_datetime64_any_dtype(df[col]) or ('日期' in col or '時間' in col)]
1806
+     try:
1807
+         if date_cols and num_cols: recommendation.update({"chart_type": "折線圖", "x_column": date_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到時間列 '{date_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用折線圖顯示趨勢。"})
1808
+         elif len(num_cols) >= 2: recommendation.update({"chart_type": "散點圖", "x_column": num_cols[0], "y_column": num_cols[1], "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}' 和 '{num_cols[1]}',推薦使用散點圖分析相關性。"})
1809
+         elif cat_cols and num_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到類別列 '{cat_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用長條圖比較各類別的��值。"})
1810
+         elif len(cat_cols) >= 2: recommendation.update({"chart_type": "堆疊長條圖", "x_column": cat_cols[0], "y_column": None, "group_column": cat_cols[1], "agg_function": "計數", "message": f"檢測到多個類別列 ('{cat_cols[0]}', '{cat_cols[1]}', ...),推薦使用堆疊長條圖顯示計數分佈。"})
1811
+         elif cat_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": None, "agg_function": "計數", "message": f"檢測到類別列 '{cat_cols[0]}',推薦使用長條圖顯示其頻數分佈。"})
1812
+         elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
1813
+         else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
1814
+     except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
1815
+     if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
1816
+     if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
1817
+     if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
1818
+     if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
1819
+     if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
1820
+     return recommendation
1821
 
1822
  # =========================================
1823
  # == CSS 樣式 (CSS Styling) ==