s880453 commited on
Commit
ddfbd0d
·
verified ·
1 Parent(s): 0a5a399

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -368
app.py CHANGED
@@ -1273,8 +1273,8 @@ def recommend_chart_settings(df):
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
- 版本:4.0 (改用 Radio + 簡化佈局)
1277
- 描述:包含所有功能的完整程式碼,大量使用 Radio Button 替代 Dropdown,期望解決 UI 問題。
1278
  """
1279
 
1280
  # =========================================
@@ -1295,67 +1295,30 @@ import re
1295
  import json
1296
  import colorsys
1297
  import traceback # 用於更詳細的錯誤追蹤
 
1298
 
1299
  # =========================================
1300
  # == 常數定義 (Constants) ==
1301
  # =========================================
1302
-
1303
- # 圖表類型選項 (Chart Type Options) - 保持列表
1304
- CHART_TYPES = [
1305
- "長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖",
1306
- "折線圖", "多重折線圖", "階梯折線圖",
1307
- "區域圖", "堆疊區域圖", "百分比堆疊區域圖",
1308
- "圓餅圖", "環形圖",
1309
- "散點圖", "氣泡圖",
1310
- "直方圖", "箱型圖", "小提琴圖",
1311
- "熱力圖", "樹狀圖",
1312
- "雷達圖", "漏斗圖", "極座標圖", "甘特圖"
1313
- ]
1314
-
1315
- # 顏色方案選項 (Color Scheme Options) - 保持字典,用於 Dropdown
1316
  COLOR_SCHEMES = {
1317
- "預設 (Plotly)": px.colors.qualitative.Plotly,
1318
- "分類 - D3": px.colors.qualitative.D3, "分類 - G10": px.colors.qualitative.G10,
1319
- "分類 - T10": px.colors.qualitative.T10, "分類 - Alphabet": px.colors.qualitative.Alphabet,
1320
- "分類 - Dark24": px.colors.qualitative.Dark24, "分類 - Light24": px.colors.qualitative.Light24,
1321
- "分類 - Set1": px.colors.qualitative.Set1, "分類 - Set2": px.colors.qualitative.Set2,
1322
- "分類 - Set3": px.colors.qualitative.Set3, "分類 - Pastel": px.colors.qualitative.Pastel,
1323
- "分類 - Pastel1": px.colors.qualitative.Pastel1, "分類 - Pastel2": px.colors.qualitative.Pastel2,
1324
- "分類 - Antique": px.colors.qualitative.Antique, "分類 - Bold": px.colors.qualitative.Bold,
1325
- "分類 - Prism": px.colors.qualitative.Prism, "分類 - Safe": px.colors.qualitative.Safe,
1326
- "分類 - Vivid": px.colors.qualitative.Vivid,
1327
- "連續 - Viridis": px.colors.sequential.Viridis, "連續 - Plasma": px.colors.sequential.Plasma,
1328
- "連續 - Inferno": px.colors.sequential.Inferno, "連續 - Magma": px.colors.sequential.Magma,
1329
- "連續 - Cividis": px.colors.sequential.Cividis, "連續 - Blues": px.colors.sequential.Blues,
1330
- "連續 - Reds": px.colors.sequential.Reds, "連續 - Greens": px.colors.sequential.Greens,
1331
- "連續 - Purples": px.colors.sequential.Purples, "連續 - Oranges": px.colors.sequential.Oranges,
1332
- "連續 - Greys": px.colors.sequential.Greys, "連續 - Rainbow": px.colors.sequential.Rainbow,
1333
- "連續 - Turbo": px.colors.sequential.Turbo, "連續 - Electric": px.colors.sequential.Electric,
1334
- "連續 - Hot": px.colors.sequential.Hot, "連續 - Teal": px.colors.sequential.Teal,
1335
- "發散 - Spectral": px.colors.diverging.Spectral, "發散 - RdBu": px.colors.diverging.RdBu,
1336
- "發散 - PRGn": px.colors.diverging.PRGn, "發散 - PiYG": px.colors.diverging.PiYG,
1337
- "發散 - BrBG": px.colors.diverging.BrBG, "發散 - Geyser": px.colors.diverging.Geyser,
1338
  "循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
1339
  }
1340
-
1341
- # 圖案填充選項 (Pattern Fill Options) - 保持列表,用於 Radio
1342
  PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
1343
-
1344
- # 聚合函數選項 (Aggregation Function Options) - 保持列表,用於 Radio
1345
- AGGREGATION_FUNCTIONS = [
1346
- "計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"
1347
- ]
1348
-
1349
- # 導出格式選項
1350
  EXPORT_FORMATS_DATA = ["CSV", "Excel", "JSON"]
1351
  EXPORT_FORMATS_IMG = ["PNG", "SVG", "PDF", "JPEG"]
1352
  YES_NO_CHOICES = ["是", "否"]
 
 
1353
 
1354
  # =========================================
1355
  # == 輔助函數 (Helper Functions) ==
1356
  # =========================================
1357
-
1358
- # --- 顏色處理相關 ---
1359
  COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
1360
  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>"""
1361
  COPY_SCRIPT = """
@@ -1382,35 +1345,18 @@ function copyToClipboard(text) {
1382
  </script>
1383
  """
1384
  COMMON_COLORS = {
1385
- "紅色": "#FF0000", "亮紅": "#FF5733", "深紅": "#C70039", "橙色": "#FFA500", "亮橙": "#FFC300", "深橙": "#D35400",
1386
- "黃色": "#FFFF00", "亮黃": "#F1C40F", "金色": "#FFD700", "綠色": "#008000", "亮綠": "#2ECC71", "深綠": "#1E8449",
1387
- "橄欖綠": "#808000", "藍色": "#0000FF", "亮藍": "#3498DB", "深藍": "#2874A6", "天藍": "#87CEEB", "紫色": "#800080",
1388
- "亮紫": "#9B59B6", "深紫": "#6C3483", "薰衣草紫": "#E6E6FA", "粉紅色": "#FFC0CB", "亮粉": "#FF69B4", "深粉": "#C71585",
1389
- "棕色": "#A52A2A", "亮棕": "#E59866", "深棕": "#6E2C00", "青色": "#00FFFF", "藍綠色": "#008080", "綠松石色": "#40E0D0",
1390
- "洋紅": "#FF00FF", "紫紅色": "#DC143C", "灰色": "#808080", "淺灰": "#D3D3D3", "深灰": "#696969", "石板灰": "#708090",
1391
- "黑色": "#000000", "白色": "#FFFFFF", "米色": "#F5F5DC",
1392
  }
1393
  def generate_gradient_colors(start_color, end_color, steps=10):
1394
- def hex_to_rgb(hex_color):
1395
- hex_color = hex_color.lstrip('#')
1396
- return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
1397
- def rgb_to_hex(rgb):
1398
- r, g, b = [max(0, min(255, int(c))) for c in rgb]
1399
- return '#{:02x}{:02x}{:02x}'.format(r, g, b)
1400
  try:
1401
- start_rgb, end_rgb = hex_to_rgb(start_color), hex_to_rgb(end_color)
1402
  if steps <= 1: return [start_color] if steps == 1 else []
1403
  r_step, g_step, b_step = [(end_rgb[i] - start_rgb[i]) / (steps - 1) for i in range(3)]
1404
  return [rgb_to_hex((start_rgb[0] + r_step * i, start_rgb[1] + g_step * i, start_rgb[2] + b_step * i)) for i in range(steps)]
1405
- except Exception as e:
1406
- print(f"生成漸變色時出錯: {e}"); return [start_color, end_color]
1407
- GRADIENTS = {
1408
- "紅→黃": generate_gradient_colors("#FF0000", "#FFFF00"), "藍→綠": generate_gradient_colors("#0000FF", "#00FF00"),
1409
- "紫→粉": generate_gradient_colors("#800080", "#FFC0CB"), "紅→藍": generate_gradient_colors("#FF0000", "#0000FF"),
1410
- "黑→白": generate_gradient_colors("#000000", "#FFFFFF"), "藍→紅 (發散)": generate_gradient_colors("#0000FF", "#FF0000"),
1411
- "綠→紫 (發散)": generate_gradient_colors("#00FF00", "#800080"),
1412
- "彩虹 (簡易)": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"]
1413
- }
1414
  def generate_color_cards():
1415
  common_cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code) for name, hex_code in COMMON_COLORS.items()])
1416
  gradient_cards_html = ""
@@ -1436,14 +1382,13 @@ def parse_custom_colors(color_text):
1436
  except Exception as e: print(f"解析自定義顏色時出錯: {e}"); return {}
1437
  return custom_colors
1438
  def update_patterns(*patterns_input):
1439
- # 確保只返回有效圖案
1440
  return [p for p in patterns_input if p in PATTERN_TYPES and p != "無"]
1441
 
1442
-
1443
  # =========================================
1444
  # == 數據處理函數 (Data Processing Functions) ==
1445
  # =========================================
1446
  def process_upload(file):
 
1447
  if file is None: return None, "❌ 未上傳任何文件。"
1448
  try:
1449
  file_path = file.name; file_type = file_path.split('.')[-1].lower()
@@ -1458,53 +1403,62 @@ def process_upload(file):
1458
  except Exception as e: return None, f"❌ 讀取 Excel 文件時出錯: {e}"
1459
  else: return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"
1460
  df.columns = df.columns.str.strip()
1461
- # 清理數據中的潛在空格(可選,但有助於分組)
1462
- # for col in df.select_dtypes(include=['object', 'string']).columns:
1463
- # df[col] = df[col].str.strip()
1464
  return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"
1465
  except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
1466
 
1467
  def parse_data(text_data):
 
1468
  if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
1469
  try:
1470
  data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
1471
  if ',' in first_line: separator = ','
1472
  elif '\t' in first_line: separator = '\t'
1473
  elif ' ' in first_line: separator = r'\s+'
1474
- else: separator = ',' # 默認
1475
- try:
1476
- df = pd.read_csv(data_io, sep=separator, skipinitialspace=True) # skipinitialspace 處理分隔符後的空格
1477
  except pd.errors.ParserError as pe: return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
1478
  except Exception as e: return None, f"❌ 解析數據時出錯: {e}"
1479
  df.columns = df.columns.str.strip()
1480
- # 清理數據中的潛在空格
1481
- # for col in df.select_dtypes(include=['object', 'string']).columns:
1482
- # df[col] = df[col].str.strip()
1483
  return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"
1484
  except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
1485
 
1486
- def update_columns(df):
1487
- default_choices = ["-- 無數據 --"]
 
 
 
1488
  if df is None or df.empty:
1489
- # 返回空的 Dropdown 更新,避免錯誤
1490
- no_data_update = gr.Dropdown(choices=default_choices, value=default_choices[0])
1491
- no_data_update_with_none = gr.Dropdown(choices=["無"] + default_choices, value="無")
1492
  return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
1493
 
1494
  try:
1495
  columns = df.columns.tolist()
1496
- x_default = columns[0] if columns else None
1497
- y_default = columns[1] if len(columns) > 1 else (columns[0] if columns else None)
1498
  valid_columns = [str(col) for col in columns if col is not None and str(col) != ""] # 確保列名是字符串
1499
- group_choices, size_choices = ["無"] + valid_columns, ["無"] + valid_columns
1500
- return (gr.Dropdown(choices=valid_columns, value=x_default, label="X軸 / 類別"),
1501
- gr.Dropdown(choices=valid_columns, value=y_default, label="Y軸 / 數值"),
1502
- gr.Dropdown(choices=group_choices, value="無", label="分組列"),
1503
- gr.Dropdown(choices=size_choices, value="無", label="大小列"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1504
  except Exception as e:
1505
- print(f"更新列選項時出錯: {e}")
1506
- no_data_update = gr.Dropdown(choices=default_choices, value=default_choices[0])
1507
- no_data_update_with_none = gr.Dropdown(choices=["無"] + default_choices, value="無")
1508
  return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
1509
 
1510
 
@@ -1514,6 +1468,23 @@ def update_columns(df):
1514
  def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
1515
  color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
1516
  show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
  fig = go.Figure()
1518
  try:
1519
  # --- 0. 將 "是"/"否" 轉換為布林值 ---
@@ -1521,40 +1492,37 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1521
  show_legend = True if show_legend_str == "是" else False
1522
 
1523
  # --- 1. 輸入驗證 (更嚴格) ---
1524
- if df is None or df.empty: raise ValueError("沒有有效的數據可供繪圖。")
 
1525
  if not chart_type: raise ValueError("請選擇圖表類型。")
1526
  if not agg_func_name: raise ValueError("請選擇聚合函數。")
1527
- if not x_column or x_column == "-- 無數據 --": raise ValueError("請選擇有效的 X 軸或類別列。")
1528
 
1529
  # 檢查列是否存在
1530
- if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。")
1531
 
1532
  # 只有在非計數且非直方圖時才嚴格要求 Y 軸
1533
  y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
1534
  if y_needed:
1535
- if not y_column or y_column == "-- 無數據 --": raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列。")
1536
- if y_column not in df.columns: raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。")
1537
  else:
1538
  y_column = None # 如果不需要 Y 軸,明確設為 None
1539
 
1540
  # 處理可選列
1541
- group_col = None if group_column == "無" or not group_column else group_column
1542
- size_col = None if size_column == "無" or not size_column else size_column
1543
 
1544
- if group_col and group_col not in df.columns: raise ValueError(f"分組列 '{group_col}' 不在數據中。")
1545
- if size_col and size_col not in df.columns: raise ValueError(f"大小列 '{size_col}' 不在數據中。")
1546
  if group_col == x_column: raise ValueError("分組列不能與 X 軸列相同。")
1547
- # if y_needed and y_column == x_column: raise ValueError("Y 軸列不能與 X 軸列相同(除非特殊情況)。") # 允許某些情況相同
1548
 
1549
  df_processed = df.copy()
1550
 
1551
  # --- 2. 數據類型轉換與準備 ---
1552
- # X 軸和分組列強制轉為字符串,以便正確分組
1553
  df_processed[x_column] = df_processed[x_column].astype(str)
1554
- if group_col:
1555
- df_processed[group_col] = df_processed[group_col].astype(str)
1556
-
1557
- # 嘗試將 Y 軸和大小列轉為數值
1558
  if y_column:
1559
  try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='coerce')
1560
  except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column}' 為數值時出錯: {e}")
@@ -1562,48 +1530,39 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1562
  try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
1563
  except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
1564
 
1565
-
1566
  # --- 3. 數據聚合 (如果需要) ---
 
1567
  needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
1568
  agg_df = None
1569
- y_col_agg = y_column # 預設 Y 軸列名
1570
  if needs_aggregation:
1571
  grouping_cols = [x_column] + ([group_col] if group_col else [])
1572
-
 
1573
  if agg_func_name == "計數":
1574
  agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
1575
  y_col_agg = '__count__'
1576
  else:
1577
  agg_func_pd = agg_function_map(agg_func_name)
1578
  if not y_column: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
1579
- # 確保 Y 軸是數值類型 (除非 first/last)
1580
  if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_column]):
1581
- # 嘗試再次轉換,如果失敗則報錯
1582
  try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='raise')
1583
  except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
1584
-
1585
  try:
1586
- # 執行聚合
1587
  agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd).reset_index()
1588
- y_col_agg = y_column # 保持原始列名
1589
- except Exception as agg_e:
1590
- raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
1591
  else:
1592
  agg_df = df_processed
1593
- y_col_agg = y_column # 保持原始列名 (可能為 None)
1594
 
1595
- # 再次檢查聚合後的 DataFrame
1596
- if agg_df is None or agg_df.empty:
1597
- raise ValueError("數據聚合後沒有產生有效結果。")
1598
- # 確保繪圖所需的列存在於 agg_df 中
1599
  required_cols_for_plot = [x_column]
1600
  if y_col_agg: required_cols_for_plot.append(y_col_agg)
1601
  if group_col: required_cols_for_plot.append(group_col)
1602
  if size_col: required_cols_for_plot.append(size_col)
1603
  missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
1604
- if missing_cols:
1605
- raise ValueError(f"聚合後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
1606
-
1607
 
1608
  # --- 4. 獲取顏色方案 ---
1609
  colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)
@@ -1611,12 +1570,9 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1611
  # --- 5. 創建圖表 (核心邏輯) ---
1612
  fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
1613
  if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
1614
-
1615
- # --- (繪圖邏輯開始) ---
1616
- # (與 V3 版本相同,此處省略以保持簡潔,實際代碼應包含完整的 if/elif 結構)
1617
- # 確保 y_col_agg 在不需要時為 None
1618
  effective_y = y_col_agg if y_needed or agg_func_name == "計數" else None
1619
 
 
1620
  if chart_type == "長條圖":
1621
  if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
1622
  fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
@@ -1663,7 +1619,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1663
  fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
1664
  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])]))
1665
  elif chart_type == "散點圖":
1666
- if not y_column: raise ValueError("散點圖需要選擇 Y 軸列。") # 散點圖必須有 Y
1667
  fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
1668
  elif chart_type == "氣泡圖":
1669
  if not y_column: raise ValueError("氣泡圖需要選擇 Y 軸列。")
@@ -1721,13 +1677,10 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1721
  if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
1722
  fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
1723
  elif chart_type == "甘特圖":
1724
- # 甘特圖使用原始數據,需要開始和結束列
1725
- start_col_gantt = y_column # Y 軸被用作開始列
1726
- end_col_gantt = group_col # 分組列被用作結束列
1727
- task_col_gantt = x_column # X 軸被用作任務列
1728
  if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
1729
  try:
1730
- df_gantt = df.copy() # 使用原始 df
1731
  if start_col_gantt not in df_gantt.columns: raise ValueError(f"開始列 '{start_col_gantt}' 不在數據中。")
1732
  if end_col_gantt not in df_gantt.columns: raise ValueError(f"結束列 '{end_col_gantt}' 不在數據中。")
1733
  if task_col_gantt not in df_gantt.columns: raise ValueError(f"任務列 '{task_col_gantt}' 不在數據中。")
@@ -1746,6 +1699,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1746
 
1747
 
1748
  # --- 6. 應用圖案 (如果支持) ---
 
1749
  if patterns:
1750
  try:
1751
  num_traces = len(fig.data)
@@ -1768,10 +1722,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1768
  dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
1769
  for i, trace in enumerate(fig.data):
1770
  pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
1771
- if dash: trace.line.dash = dash; trace.fill = 'tonexty' if hasattr(trace, 'stackgroup') and trace.stackgroup else 'tozeroy' # 修正 stackgroup 判斷
1772
  except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")
1773
 
1774
  # --- 7. 更新佈局 ---
 
1775
  fig.update_layout(
1776
  showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
1777
  template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
@@ -1779,25 +1734,25 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1779
  hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
1780
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
1781
  )
1782
- # 根據圖表類型更新軸標籤
1783
  final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
1784
  if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
1785
  elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
1786
  elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
1787
- elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column) # 甘特圖軸標籤
1788
  else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
1789
 
1790
  except ValueError as ve:
1791
- print(f"圖表創建錯誤 (ValueError): {ve}"); traceback.print_exc()
1792
  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)
1793
  except Exception as e:
1794
- error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message)
1795
  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)
1796
  return fig
1797
 
1798
  # =========================================
1799
  # == 導出與下載函數 (Export & Download Functions) ==
1800
  # =========================================
 
1801
  def export_data(df, format_type):
1802
  if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
1803
  try:
@@ -1812,26 +1767,19 @@ def download_figure(fig, format_type="PNG"):
1812
  if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
1813
  try:
1814
  format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
1815
- # 確保有 kaleido
1816
- import kaleido
1817
  fig.write_image(filename, format=format_lower)
1818
  return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1819
- except ImportError:
1820
- error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"
1821
- print(error_msg); return None, error_msg
1822
  except ValueError as ve:
1823
- # kaleido 可能安裝但無法運行
1824
- if "kaleido" in str(ve).lower():
1825
- error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"
1826
- print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
1827
- else:
1828
- print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
1829
- except Exception as e:
1830
- print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
1831
 
1832
  # =========================================
1833
  # == 智能推薦函數 (Recommendation Function) ==
1834
  # =========================================
 
1835
  def recommend_chart_settings(df):
1836
  recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
1837
  if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
@@ -1846,18 +1794,17 @@ def recommend_chart_settings(df):
1846
  elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
1847
  else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
1848
  except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
1849
- # 驗證推薦的列名
1850
  if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
1851
  if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
1852
  if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
1853
- # 清理無效聚合
1854
  if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
1855
- if recommendation["agg_function"] == "計數": recommendation["y_column"] = None # 計數時 Y 軸應為空
1856
  return recommendation
1857
 
1858
  # =========================================
1859
  # == CSS 樣式 (CSS Styling) ==
1860
  # =========================================
 
1861
  CUSTOM_CSS = """
1862
  /* --- 全局和容器 --- */
1863
  .gradio-container { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
@@ -1907,23 +1854,21 @@ CUSTOM_CSS = """
1907
  # =========================================
1908
  # == Gradio UI 介面定義 (Gradio UI Definition) ==
1909
  # =========================================
1910
- with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.themes.Soft()) as demo:
1911
 
1912
  # --- 應用程式標頭 ---
1913
  gr.HTML("""
1914
  <div class="app-header">
1915
- <h1 class="app-title">📊 進階數據可視化工具 V4</h1>
1916
- <p class="app-subtitle">上傳或貼上數據,輕鬆創建和比較多種專業圖表 (改用 Radio 選項)</p>
1917
  </div>
1918
  """)
1919
 
1920
  # --- 狀態變量 ---
1921
  data_state = gr.State(None)
1922
- custom_colors_state_1 = gr.State({})
1923
- patterns_state_1 = gr.State([])
1924
- custom_colors_state_2 = gr.State({})
1925
- patterns_state_2 = gr.State([])
1926
- recommendation_state = gr.State({}) # 用於存儲推薦結果
1927
 
1928
  # --- 主頁籤佈局 ---
1929
  with gr.Tabs() as tabs:
@@ -1948,116 +1893,79 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
1948
  gr.Markdown("下方將顯示載入或解析後的數據預覽。")
1949
  data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
1950
  with gr.Row():
1951
- export_format = gr.Radio(EXPORT_FORMATS_DATA, label="選擇導出格式", value="CSV") # 改用 Radio
1952
  export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
1953
  export_result = gr.File(label="導出文件下載", interactive=False)
1954
  export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1955
 
1956
- # --- 圖表創建頁籤 (左右佈局, 移除 Accordion, 使用 Radio) ---
1957
- with gr.TabItem("📈 圖表創建與比較", id=1):
1958
- gr.HTML('<div class="section-title">創建與比較圖表 (左右並列)</div>')
1959
- gr.Markdown("在這裡,您可以分別設置並生成兩張���表,方便進行對比分析。")
1960
-
1961
- with gr.Row(): # 主 Row,包含左右兩個圖表區塊
1962
- # --- 圖表一 (左側 Column) ---
1963
- with gr.Column(scale=1):
1964
- gr.Markdown("### 📊 圖表一")
1965
- with gr.Group(elem_classes=["card"]): # 將所有設定放在一個卡片中
1966
- gr.Markdown("**基本設置**")
1967
- chart_type_1 = gr.Radio(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True) # 改用 Radio
1968
- recommend_button_1 = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm")
1969
- chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一標題")
1970
- agg_function_1 = gr.Radio(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數") # 改用 Radio
1971
-
1972
- gr.Markdown("**數據映射**")
1973
- x_column_1 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸") # 保留 Dropdown
1974
- y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)") # 保留 Dropdown
1975
- group_column_1 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊") # 保留 Dropdown
1976
- size_column_1 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小") # 保留 Dropdown
1977
-
1978
- gr.Markdown("**顯示選項**")
1979
- chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1980
- chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1981
- show_grid_1 = gr.Radio(YES_NO_CHOICES, label="顯示網格", value="是") # 改用 Radio
1982
- show_legend_1 = gr.Radio(YES_NO_CHOICES, label="顯示圖例", value="是") # 改用 Radio
1983
- color_scheme_1 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)") # 保留 Dropdown
1984
- gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
1985
- gr.HTML(generate_color_cards(), elem_id="color_display_1")
1986
-
1987
- gr.Markdown("**圖案與自定義顏色**")
1988
- pattern1_1 = gr.Radio(PATTERN_TYPES, label="圖案1", value="無") # 改用 Radio
1989
- pattern2_1 = gr.Radio(PATTERN_TYPES, label="圖案2", value="無") # 改用 Radio
1990
- pattern3_1 = gr.Radio(PATTERN_TYPES, label="圖案3", value="無") # 改用 Radio
1991
- color_customization_1 = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
1992
-
1993
- # 圖表一:操作按鈕
1994
- with gr.Row():
1995
- update_button_1 = gr.Button("🔄 更新圖表一", variant="primary", elem_classes=["primary-button"])
1996
- export_img_format_1 = gr.Radio(EXPORT_FORMATS_IMG, label="導出格式", value="PNG", scale=1) # 改用 Radio
1997
- download_button_1 = gr.Button("💾 導出圖表一", elem_classes=["secondary-button"], scale=1)
1998
- export_chart_1 = gr.File(label="圖表一文件下載", interactive=False)
1999
- export_chart_status_1 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
2000
-
2001
- # 圖表一:預覽區域
2002
- with gr.Group(elem_classes=["chart-previewer"]):
2003
- chart_output_1 = gr.Plot(label="", elem_id="chart_preview_1")
2004
-
2005
 
2006
- # --- 圖表二 (右側 Column) ---
 
2007
  with gr.Column(scale=1):
2008
- gr.Markdown("### 📊 圖表二")
2009
  with gr.Group(elem_classes=["card"]):
2010
  gr.Markdown("**基本設置**")
2011
- chart_type_2 = gr.Radio(CHART_TYPES, label="圖表類型", value="折線圖", interactive=True) # 改用 Radio
2012
- chart_title_2 = gr.Textbox(label="圖表標題", placeholder="圖表二標題")
2013
- agg_function_2 = gr.Radio(AGGREGATION_FUNCTIONS, label="聚合函數", value="平均值") # 改用 Radio
2014
-
2015
- gr.Markdown("**數據映射**")
2016
- x_column_2 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選��圖表主要分類或 X 軸") # 保留 Dropdown
2017
- y_column_2 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)") # 保留 Dropdown
2018
- group_column_2 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊") # 保留 Dropdown
2019
- size_column_2 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小") # 保留 Dropdown
 
 
2020
 
2021
  gr.Markdown("**顯示選項**")
2022
- chart_width_2 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
2023
- chart_height_2 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
2024
- show_grid_2 = gr.Radio(YES_NO_CHOICES, label="顯示網格", value="是") # 改用 Radio
2025
- show_legend_2 = gr.Radio(YES_NO_CHOICES, label="顯示圖例", value="是") # 改用 Radio
2026
- color_scheme_2 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="分類 - Set2") # 保留 Dropdown
2027
- # 顏色參考共用
 
2028
 
2029
  gr.Markdown("**圖案與自定義顏色**")
2030
- pattern1_2 = gr.Radio(PATTERN_TYPES, label="圖案1", value="無") # 改用 Radio
2031
- pattern2_2 = gr.Radio(PATTERN_TYPES, label="圖案2", value="無") # 改用 Radio
2032
- pattern3_2 = gr.Radio(PATTERN_TYPES, label="圖案3", value="無") # 改用 Radio
2033
- color_customization_2 = gr.Textbox(label="自定義顏色", placeholder="類別C:#FFC300, 類別D:#C70039", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
2034
-
2035
- # 圖表二:操作按鈕
 
 
 
 
2036
  with gr.Row():
2037
- update_button_2 = gr.Button("🔄 更新圖表二", variant="primary", elem_classes=["primary-button"])
2038
- export_img_format_2 = gr.Radio(EXPORT_FORMATS_IMG, label="導出格式", value="PNG", scale=1) # 改用 Radio
2039
- download_button_2 = gr.Button("💾 導出圖表二", elem_classes=["secondary-button"], scale=1)
2040
- export_chart_2 = gr.File(label="圖表二文件下載", interactive=False)
2041
- export_chart_status_2 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
2042
 
2043
- # 圖表二:預覽區域
 
2044
  with gr.Group(elem_classes=["chart-previewer"]):
2045
- chart_output_2 = gr.Plot(label="", elem_id="chart_preview_2")
2046
 
2047
 
2048
  # --- 使用說明頁籤 ---
2049
  with gr.TabItem("❓ 使用說明", id=2):
2050
  with gr.Group(elem_classes=["card"]):
2051
  gr.HTML("""
2052
- <div class="section-title">使用說明</div>
2053
  <h3>數據輸入</h3>
2054
  <ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
2055
- <h3>圖表創建與比較</h3>
2056
- <ul><li>此頁面提供左右兩個獨立的圖表設置和預覽區域(圖表一、圖表二)。</li><li><strong>智能推薦:</strong>點擊圖表一的 "智能推薦" 按鈕,系統會根據數據結構嘗試為圖表一推薦合適的設置。</li><li><strong>圖表類型:</strong>選擇您想創建的圖表樣式 (使用點選按鈕)。</li><li><strong>聚合函數:</strong>決定如何匯總 Y 軸數據 (使用點選按鈕)。
2057
- <ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong>若要統計某一欄位中各個項目出現的次數(例如,統計不同產品的銷售筆數),請在 <strong>X軸/類別</strong> 選擇該欄位,並將 <strong>聚合函數</strong> 設為 <strong>計數</strong>,此時 <strong>無需選擇 Y軸/數值</strong>。然後選擇「長條圖」或「圓餅圖」。</li></ul>
2058
- </li><li><strong>數據映射 (下拉選單):</strong><ul><li><strong>X軸/類別:</strong>圖表的主要分類軸。</li><li><strong>Y軸/數值:</strong>圖表的數值軸。若聚合函數為 "計數",此項可忽略。</li><li><strong>分組列:</strong>用於創建堆疊、分組或多系列圖表。</li><li><strong>大小列:</strong>主要用於氣泡圖,控制點的大小。</li></ul></li><li><strong>顯示選項:</strong>調整圖表的外觀,如寬度、高度、顏色方案、是否顯示網格和圖例。</li><li><strong>圖案與自定義顏色:</strong><ul><li>為圖表系列添加不同的填充圖案(適用於部分圖表類型,如條形圖)。</li><li>通過 "類別名:顏色代碼" 的格式為特定類別指定顏色 (例如 <code>正面:#2ca02c, 負面:#d62728</code>)。</li></ul></li><li>點擊 "更新圖表" 按鈕生成或刷新對應的圖表預覽。</li><li>使用 "導出圖表" 功能將生成的圖表保存為圖片文件。</li></ul>
2059
  <h3>提示</h3>
2060
- <ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>確保數值列確實包含數字,日期列包含有效的日期格式。</li><li>部分圖表類型對數據結構有特定要求(例如,熱力圖、甘特圖)。</li><li>如果欄位選擇的下拉選單仍然有問題,可能是由於數據欄位過多或 Gradio 的限制,嘗試簡化數據或在本地環境運行。</li></ul>
2061
  """)
2062
 
2063
  # =========================================
@@ -2065,80 +1973,37 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
2065
  # =========================================
2066
 
2067
  # --- 數據載入與更新 ---
2068
- def load_data_and_update_ui(df, status_msg):
2069
- """輔助函數:更新數據狀態、預覽和所有下拉列表 (暫時不繪製初始圖表)"""
 
2070
  preview_df = df if df is not None else pd.DataFrame()
2071
- # 更新列下拉選單
2072
- col_updates = update_columns(df)
2073
  if col_updates is None or len(col_updates) != 4:
2074
- print("警告: update_columns 未返回預期的 4 個組件更新。")
2075
- # 返回空更新,避免錯誤
2076
- updates = [df, status_msg, preview_df] + [gr.update()] * 8 # 8 個下拉選單更新
2077
- # 額外返回兩個空的 Plot 更新以匹配輸出數量
2078
- updates += [gr.Plot(value=None), gr.Plot(value=None)]
2079
- return updates
2080
-
2081
- # 準備所有更新 (狀態、狀態消息、預覽表格、8 個下拉選單)
2082
- updates = [df, status_msg, preview_df] + list(col_updates) * 2
2083
- # 添加兩個空的 Plot 更新以匹配輸出數量
2084
- updates += [gr.Plot(value=None), gr.Plot(value=None)] # 讓圖表區域保持空白
2085
-
2086
- # 檢查返回的列表長度是否為 13
2087
- if len(updates) != 13:
2088
- print(f"錯誤:load_data_and_update_ui 返回了 {len(updates)} 個元素,預期 13 個。")
2089
- # 嘗試返回一個包含 13 個 None 的列表以避免崩潰
2090
- return [None] * 13
2091
 
2092
- return updates
2093
-
2094
- # 準備所有更新
2095
- updates = [df, status_msg, preview_df] + list(col_updates) * 2
2096
-
2097
- # 準備觸發圖表一初始繪圖的輸入
2098
- # 使用更新後的下拉列表的值(如果有效)或預設值
2099
- x_col_val = col_updates[0].value if col_updates[0].value else None
2100
- y_col_val = col_updates[1].value if col_updates[1].value else None
2101
- # 獲取圖表一的其他預設值
2102
- chart_type_val = "長條圖" # 來自 chart_type_1 的預設值
2103
- agg_func_val = "計數" # 來自 agg_function_1 的預設值
2104
-
2105
- # 只有在數據有效且 X 軸有效時才嘗試繪圖
2106
- initial_plot = go.Figure() # 默認空圖
2107
- if df is not None and not df.empty and x_col_val and x_col_val != "-- 無數據 --":
2108
- try:
2109
- # 使用預設值嘗試繪製圖表一
2110
- initial_plot = create_plot(df, chart_type_val, x_col_val, y_col_val,
2111
- None, None, # group, size
2112
- "預設 (Plotly)", [], "", 700, 450, # color, patterns, title, w, h
2113
- "是", "是", agg_func_val, {}) # grid, legend, agg, custom_colors
2114
- except Exception as initial_plot_e:
2115
- print(f"載入數據後嘗試初始繪圖時出錯: {initial_plot_e}")
2116
- initial_plot = go.Figure()
2117
- initial_plot.add_annotation(text=f"⚠️ 無法生成初始圖表:<br>{initial_plot_e}", align='left', showarrow=False, font=dict(size=14, color="orange"))
2118
- initial_plot.update_layout(xaxis_visible=False, yaxis_visible=False)
2119
-
2120
- # 將初始圖表添加到更新列表中
2121
- updates.append(initial_plot)
2122
- # 圖表二保持空白
2123
- updates.append(gr.Plot(value=None)) # 更新圖表二為空
2124
 
 
2125
  return updates
2126
 
2127
-
2128
- # 綁定數據載入事件
2129
  upload_button.click(
2130
  process_upload,
2131
  inputs=[file_upload],
2132
  outputs=[data_state, upload_status]
2133
  ).then(
2134
- load_data_and_update_ui,
2135
  inputs=[data_state, upload_status],
2136
  outputs=[
2137
  data_state, upload_status, data_preview,
2138
- x_column_1, y_column_1, group_column_1, size_column_1, # 圖表一 Dropdowns
2139
- x_column_2, y_column_2, group_column_2, size_column_2, # 圖表二 Dropdowns
2140
- chart_output_1, # 圖表一初始繪圖
2141
- chart_output_2 # 圖表二初始空白
2142
  ]
2143
  )
2144
  parse_button.click(
@@ -2146,13 +2011,12 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
2146
  inputs=[csv_input],
2147
  outputs=[data_state, parse_status]
2148
  ).then(
2149
- load_data_and_update_ui,
2150
  inputs=[data_state, parse_status],
2151
  outputs=[
2152
  data_state, parse_status, data_preview,
2153
- x_column_1, y_column_1, group_column_1, size_column_1,
2154
- x_column_2, y_column_2, group_column_2, size_column_2,
2155
- chart_output_1, chart_output_2
2156
  ]
2157
  )
2158
 
@@ -2160,63 +2024,56 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
2160
  # --- 數據導出 ---
2161
  export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
2162
 
2163
- # --- 圖表一:顏色和圖案狀態 ---
2164
- color_customization_1.change(parse_custom_colors, inputs=[color_customization_1], outputs=[custom_colors_state_1])
2165
- patterns_inputs_1 = [pattern1_1, pattern2_1, pattern3_1]
2166
- for pattern_dd in patterns_inputs_1: pattern_dd.change(update_patterns, inputs=patterns_inputs_1, outputs=[patterns_state_1])
2167
-
2168
- # --- 圖表一:更新圖表 ---
2169
- chart_inputs_1 = [data_state, chart_type_1, x_column_1, y_column_1, group_column_1, size_column_1, color_scheme_1, patterns_state_1, chart_title_1, chart_width_1, chart_height_1, show_grid_1, show_legend_1, agg_function_1, custom_colors_state_1]
2170
- update_button_1.click(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
2171
- def auto_update_chart_1(*inputs): return create_plot(*inputs)
2172
- # 綁定 change 事件到會影響圖表的 UI 組件
2173
- for input_component in [chart_type_1, x_column_1, y_column_1, group_column_1, size_column_1, color_scheme_1, chart_title_1, chart_width_1, chart_height_1, show_grid_1, show_legend_1, agg_function_1, color_customization_1, pattern1_1, pattern2_1, pattern3_1]:
2174
- if input_component is not None and hasattr(input_component, 'change'):
2175
- input_component.change(auto_update_chart_1, inputs=chart_inputs_1, outputs=[chart_output_1])
 
 
 
 
 
 
 
 
 
2176
 
 
2177
 
2178
- # --- 圖表一:導出圖表 ---
2179
- download_button_1.click(download_figure, inputs=[chart_output_1, export_img_format_1], outputs=[export_chart_1, export_chart_status_1])
2180
 
2181
- # --- 圖表一:智能推薦 ---
2182
- def apply_recommendation(rec_dict):
2183
  if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
2184
  chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
2185
- y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", "無")
2186
  # 返回 Radio 和 Dropdown 的更新
2187
  return [
2188
  gr.Radio(value=chart_type_val), # 更新 Chart Type Radio
2189
- gr.Dropdown(value=x_col_val), # 更新 X Column Dropdown
2190
- gr.Dropdown(value=y_col_val), # 更新 Y Column Dropdown
2191
- gr.Dropdown(value=group_col_val),# 更新 Group Column Dropdown
2192
- gr.Radio(value=agg_func_val) # 更新 Agg Function Radio
2193
  ]
2194
 
2195
- recommend_button_1.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
2196
- apply_recommendation, inputs=[recommendation_state], outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
2197
- ).then(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1]) # 應用推薦後觸發更新
2198
-
2199
- # --- 圖表二:顏色和圖案狀態 ---
2200
- color_customization_2.change(parse_custom_colors, inputs=[color_customization_2], outputs=[custom_colors_state_2])
2201
- patterns_inputs_2 = [pattern1_2, pattern2_2, pattern3_2]
2202
- for pattern_dd in patterns_inputs_2: pattern_dd.change(update_patterns, inputs=patterns_inputs_2, outputs=[patterns_state_2])
2203
-
2204
- # --- 圖表二:更新圖表 ---
2205
- chart_inputs_2 = [data_state, chart_type_2, x_column_2, y_column_2, group_column_2, size_column_2, color_scheme_2, patterns_state_2, chart_title_2, chart_width_2, chart_height_2, show_grid_2, show_legend_2, agg_function_2, custom_colors_state_2]
2206
- update_button_2.click(create_plot, inputs=chart_inputs_2, outputs=[chart_output_2])
2207
- def auto_update_chart_2(*inputs): return create_plot(*inputs)
2208
- # 綁定 change 事件到會影響圖表的 UI 組件
2209
- for input_component in [chart_type_2, x_column_2, y_column_2, group_column_2, size_column_2, color_scheme_2, chart_title_2, chart_width_2, chart_height_2, show_grid_2, show_legend_2, agg_function_2, color_customization_2, pattern1_2, pattern2_2, pattern3_2]:
2210
- if input_component is not None and hasattr(input_component, 'change'):
2211
- input_component.change(auto_update_chart_2, inputs=chart_inputs_2, outputs=[chart_output_2])
2212
-
2213
- # --- 圖表二:導出圖表 ---
2214
- download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
2215
-
2216
- # --- 圖表類型改變時更新 UI 元素可見性 (保持不變) ---
2217
- def update_element_visibility(chart_type):
2218
  try:
2219
- # (與 V3 相同的邏輯)
2220
  is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
2221
  is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
2222
  is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
@@ -2231,13 +2088,12 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
2231
  elif is_heatmap: group_label, group_needed = "行/列 分組", True
2232
  size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
2233
  if is_gantt: size_label, size_needed = "顏色列 (可選)", True
2234
- # 返回 Dropdown 的更新對象
2235
  return (gr.update(label=y_label, visible=y_needed), gr.update(label=group_label, visible=group_needed), gr.update(label=size_label, visible=size_needed))
2236
  except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
2237
 
2238
  # 綁定到 Radio chart_type 的 change 事件
2239
- chart_type_1.change(update_element_visibility, inputs=[chart_type_1], outputs=[y_column_1, group_column_1, size_column_1])
2240
- chart_type_2.change(update_element_visibility, inputs=[chart_type_2], outputs=[y_column_2, group_column_2, size_column_2])
2241
 
2242
  # =========================================
2243
  # == 應用程式啟動 (Launch Application) ==
 
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
+ 版本:5.0 (極簡化 - 單圖表 + 全 Radio)
1277
+ 描述:包含所有功能的完整程式碼,極度簡化 UI,使用 Radio 替代 Dropdown,移除自動更新。
1278
  """
1279
 
1280
  # =========================================
 
1295
  import json
1296
  import colorsys
1297
  import traceback # 用於更詳細的錯誤追蹤
1298
+ import sys # 用於打印調試信息
1299
 
1300
  # =========================================
1301
  # == 常數定義 (Constants) ==
1302
  # =========================================
1303
+ CHART_TYPES = ["長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "折線圖", "多重折線圖", "階梯折線圖", "區域圖", "堆疊區域圖", "百分比堆疊區域圖", "圓餅圖", "環形圖", "散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "熱力圖", "樹狀圖", "雷達圖", "漏斗圖", "極座標圖", "甘特圖"]
 
 
 
 
 
 
 
 
 
 
 
 
 
1304
  COLOR_SCHEMES = {
1305
+ "預設 (Plotly)": px.colors.qualitative.Plotly, "分類 - D3": px.colors.qualitative.D3, "分類 - G10": px.colors.qualitative.G10, "分類 - T10": px.colors.qualitative.T10, "分類 - Alphabet": px.colors.qualitative.Alphabet, "分類 - Dark24": px.colors.qualitative.Dark24, "分類 - Light24": px.colors.qualitative.Light24, "分類 - Set1": px.colors.qualitative.Set1, "分類 - Set2": px.colors.qualitative.Set2, "分類 - Set3": px.colors.qualitative.Set3, "分類 - Pastel": px.colors.qualitative.Pastel, "分類 - Pastel1": px.colors.qualitative.Pastel1, "分類 - Pastel2": px.colors.qualitative.Pastel2, "分類 - Antique": px.colors.qualitative.Antique, "分類 - Bold": px.colors.qualitative.Bold, "分類 - Prism": px.colors.qualitative.Prism, "分類 - Safe": px.colors.qualitative.Safe, "分類 - Vivid": px.colors.qualitative.Vivid,
1306
+ "連續 - Viridis": px.colors.sequential.Viridis, "連續 - Plasma": px.colors.sequential.Plasma, "連續 - Inferno": px.colors.sequential.Inferno, "連續 - Magma": px.colors.sequential.Magma, "連續 - Cividis": px.colors.sequential.Cividis, "連續 - Blues": px.colors.sequential.Blues, "連續 - Reds": px.colors.sequential.Reds, "連續 - Greens": px.colors.sequential.Greens, "連續 - Purples": px.colors.sequential.Purples, "連續 - Oranges": px.colors.sequential.Oranges, "連續 - Greys": px.colors.sequential.Greys, "連續 - Rainbow": px.colors.sequential.Rainbow, "連續 - Turbo": px.colors.sequential.Turbo, "連續 - Electric": px.colors.sequential.Electric, "連續 - Hot": px.colors.sequential.Hot, "連續 - Teal": px.colors.sequential.Teal,
1307
+ "發散 - Spectral": px.colors.diverging.Spectral, "發散 - RdBu": px.colors.diverging.RdBu, "發散 - PRGn": px.colors.diverging.PRGn, "發散 - PiYG": px.colors.diverging.PiYG, "發散 - BrBG": px.colors.diverging.BrBG, "發散 - Geyser": px.colors.diverging.Geyser,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1308
  "循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
1309
  }
 
 
1310
  PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
1311
+ AGGREGATION_FUNCTIONS = ["計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"]
 
 
 
 
 
 
1312
  EXPORT_FORMATS_DATA = ["CSV", "Excel", "JSON"]
1313
  EXPORT_FORMATS_IMG = ["PNG", "SVG", "PDF", "JPEG"]
1314
  YES_NO_CHOICES = ["是", "否"]
1315
+ NO_DATA_STR = "-- 無數據 --"
1316
+ NONE_STR = "無" # 代表 '無' 選項的值
1317
 
1318
  # =========================================
1319
  # == 輔助函數 (Helper Functions) ==
1320
  # =========================================
1321
+ # --- 顏色處理相關 (與 V4 相同) ---
 
1322
  COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
1323
  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>"""
1324
  COPY_SCRIPT = """
 
1345
  </script>
1346
  """
1347
  COMMON_COLORS = {
1348
+ "紅色": "#FF0000", "亮紅": "#FF5733", "深紅": "#C70039", "橙色": "#FFA500", "亮橙": "#FFC300", "深橙": "#D35400", "黃色": "#FFFF00", "亮黃": "#F1C40F", "金色": "#FFD700", "綠色": "#008000", "亮綠": "#2ECC71", "深綠": "#1E8449", "橄欖綠": "#808000", "藍色": "#0000FF", "亮藍": "#3498DB", "深藍": "#2874A6", "天藍": "#87CEEB", "紫色": "#800080", "亮紫": "#9B59B6", "深紫": "#6C3483", "薰衣草紫": "#E6E6FA", "粉紅色": "#FFC0CB", "亮粉": "#FF69B4", "深粉": "#C71585", "棕色": "#A52A2A", "亮棕": "#E59866", "深棕": "#6E2C00", "青色": "#00FFFF", "藍綠色": "#008080", "綠松石色": "#40E0D0", "洋紅": "#FF00FF", "紫紅色": "#DC143C", "灰色": "#808080", "淺灰": "#D3D3D3", "深灰": "#696969", "石板灰": "#708090", "黑色": "#000000", "白色": "#FFFFFF", "米色": "#F5F5DC",
 
 
 
 
 
 
1349
  }
1350
  def generate_gradient_colors(start_color, end_color, steps=10):
1351
+ def hex_to_rgb(hex_color): hex_color = hex_color.lstrip('#'); return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
1352
+ def rgb_to_hex(rgb): r, g, b = [max(0, min(255, int(c))) for c in rgb]; return '#{:02x}{:02x}{:02x}'.format(r, g, b)
 
 
 
 
1353
  try:
1354
+ start_rgb, end_rgb = hex_to_rgb(start_color), hex_to_rgb(end_color);
1355
  if steps <= 1: return [start_color] if steps == 1 else []
1356
  r_step, g_step, b_step = [(end_rgb[i] - start_rgb[i]) / (steps - 1) for i in range(3)]
1357
  return [rgb_to_hex((start_rgb[0] + r_step * i, start_rgb[1] + g_step * i, start_rgb[2] + b_step * i)) for i in range(steps)]
1358
+ except Exception as e: print(f"生成漸變色時出錯: {e}"); return [start_color, end_color]
1359
+ GRADIENTS = {"紅→黃": generate_gradient_colors("#FF0000", "#FFFF00"), "藍→綠": generate_gradient_colors("#0000FF", "#00FF00"), "紫→粉": generate_gradient_colors("#800080", "#FFC0CB"), "紅→藍": generate_gradient_colors("#FF0000", "#0000FF"), "黑→白": generate_gradient_colors("#000000", "#FFFFFF"), "藍→紅 (發散)": generate_gradient_colors("#0000FF", "#FF0000"), "綠→紫 (發散)": generate_gradient_colors("#00FF00", "#800080"), "彩虹 (簡易)": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"]}
 
 
 
 
 
 
 
1360
  def generate_color_cards():
1361
  common_cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code) for name, hex_code in COMMON_COLORS.items()])
1362
  gradient_cards_html = ""
 
1382
  except Exception as e: print(f"解析自定義顏色時出錯: {e}"); return {}
1383
  return custom_colors
1384
  def update_patterns(*patterns_input):
 
1385
  return [p for p in patterns_input if p in PATTERN_TYPES and p != "無"]
1386
 
 
1387
  # =========================================
1388
  # == 數據處理函數 (Data Processing Functions) ==
1389
  # =========================================
1390
  def process_upload(file):
1391
+ # (與 V4 相同)
1392
  if file is None: return None, "❌ 未上傳任何文件。"
1393
  try:
1394
  file_path = file.name; file_type = file_path.split('.')[-1].lower()
 
1403
  except Exception as e: return None, f"❌ 讀取 Excel 文件時出錯: {e}"
1404
  else: return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"
1405
  df.columns = df.columns.str.strip()
 
 
 
1406
  return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"
1407
  except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
1408
 
1409
  def parse_data(text_data):
1410
+ # (與 V4 相同)
1411
  if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
1412
  try:
1413
  data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
1414
  if ',' in first_line: separator = ','
1415
  elif '\t' in first_line: separator = '\t'
1416
  elif ' ' in first_line: separator = r'\s+'
1417
+ else: separator = ','
1418
+ try: df = pd.read_csv(data_io, sep=separator, skipinitialspace=True)
 
1419
  except pd.errors.ParserError as pe: return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
1420
  except Exception as e: return None, f"❌ 解析數據時出錯: {e}"
1421
  df.columns = df.columns.str.strip()
 
 
 
1422
  return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"
1423
  except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
1424
 
1425
+ def update_columns_as_radio(df):
1426
+ """修改:更新列選擇為 Radio 選項"""
1427
+ no_data_choices = [NO_DATA_STR]
1428
+ no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
1429
+
1430
  if df is None or df.empty:
1431
+ # 返回空的 Radio 更新
1432
+ no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1433
+ no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1434
  return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
1435
 
1436
  try:
1437
  columns = df.columns.tolist()
 
 
1438
  valid_columns = [str(col) for col in columns if col is not None and str(col) != ""] # 確保列名是字符串
1439
+ if not valid_columns: # 如果沒有有效列
1440
+ no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1441
+ no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1442
+ return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
1443
+
1444
+ x_default = valid_columns[0]
1445
+ # Y 軸預設選第二個(如果存在),否則選第一個
1446
+ y_default = valid_columns[1] if len(valid_columns) > 1 else valid_columns[0]
1447
+
1448
+ # 為 group 和 size 添加 "無" 選項
1449
+ group_choices = [NONE_STR] + valid_columns
1450
+ size_choices = [NONE_STR] + valid_columns
1451
+
1452
+ # 返回 Radio 更新對象
1453
+ # 注意:如果列過多,Radio 會很長,但這是為了測試功能性
1454
+ return (gr.Radio(choices=valid_columns, value=x_default, label="X軸 / 類別"),
1455
+ gr.Radio(choices=valid_columns, value=y_default, label="Y軸 / 數值"),
1456
+ gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
1457
+ gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
1458
  except Exception as e:
1459
+ print(f"更新列選項 (Radio) 時出錯: {e}")
1460
+ no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
1461
+ no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
1462
  return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
1463
 
1464
 
 
1468
  def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
1469
  color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
1470
  show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
1471
+ # --- 添加調試信息 ---
1472
+ print("-" * 20, file=sys.stderr)
1473
+ print(f"調用 create_plot:", file=sys.stderr)
1474
+ print(f" - df type: {type(df)}", file=sys.stderr)
1475
+ if isinstance(df, pd.DataFrame):
1476
+ print(f" - df empty: {df.empty}", file=sys.stderr)
1477
+ print(f" - df shape: {df.shape}", file=sys.stderr)
1478
+ # print(f" - df head:\n{df.head()}", file=sys.stderr) # 打印頭部數據幫助調試
1479
+ print(f" - chart_type: {chart_type}", file=sys.stderr)
1480
+ print(f" - x_column: {x_column}", file=sys.stderr)
1481
+ print(f" - y_column: {y_column}", file=sys.stderr)
1482
+ print(f" - group_column: {group_column}", file=sys.stderr)
1483
+ print(f" - size_column: {size_column}", file=sys.stderr)
1484
+ print(f" - agg_func_name: {agg_func_name}", file=sys.stderr)
1485
+ print("-" * 20, file=sys.stderr)
1486
+ # --- 結束調試信息 ---
1487
+
1488
  fig = go.Figure()
1489
  try:
1490
  # --- 0. 將 "是"/"否" 轉換為布林值 ---
 
1492
  show_legend = True if show_legend_str == "是" else False
1493
 
1494
  # --- 1. 輸入驗證 (更嚴格) ---
1495
+ if df is None or not isinstance(df, pd.DataFrame) or df.empty: # 嚴格檢查 df
1496
+ raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
1497
  if not chart_type: raise ValueError("請選擇圖表類型。")
1498
  if not agg_func_name: raise ValueError("請選擇聚合函數。")
1499
+ if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
1500
 
1501
  # 檢查列是否存在
1502
+ if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
1503
 
1504
  # 只有在非計數且非直方圖時才嚴格要求 Y 軸
1505
  y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
1506
  if y_needed:
1507
+ if not y_column or y_column == NO_DATA_STR: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列。")
1508
+ if y_column not in df.columns: raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。可用列: {', '.join(df.columns)}")
1509
  else:
1510
  y_column = None # 如果不需要 Y 軸,明確設為 None
1511
 
1512
  # 處理可選列
1513
+ group_col = None if group_column == NONE_STR or not group_column else group_column
1514
+ size_col = None if size_column == NONE_STR or not size_column else size_column
1515
 
1516
+ if group_col and group_col not in df.columns: raise ValueError(f"分組列 '{group_col}' 不在數據中。可用列: {', '.join(df.columns)}")
1517
+ if size_col and size_col not in df.columns: raise ValueError(f"大小列 '{size_col}' 不在數據中。可用列: {', '.join(df.columns)}")
1518
  if group_col == x_column: raise ValueError("分組列不能與 X 軸列相同。")
 
1519
 
1520
  df_processed = df.copy()
1521
 
1522
  # --- 2. 數據類型轉換與準備 ---
1523
+ # (與 V4 相同)
1524
  df_processed[x_column] = df_processed[x_column].astype(str)
1525
+ if group_col: df_processed[group_col] = df_processed[group_col].astype(str)
 
 
 
1526
  if y_column:
1527
  try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='coerce')
1528
  except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column}' 為數值時出錯: {e}")
 
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}")
1532
 
 
1533
  # --- 3. 數據聚合 (如果需要) ---
1534
+ # (與 V4 相同)
1535
  needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
1536
  agg_df = None
1537
+ y_col_agg = y_column
1538
  if needs_aggregation:
1539
  grouping_cols = [x_column] + ([group_col] if group_col else [])
1540
+ invalid_grouping_cols = [col for col in grouping_cols if col not in df_processed.columns]
1541
+ if invalid_grouping_cols: raise ValueError(f"以下分組/X軸列不在數據中: {', '.join(invalid_grouping_cols)}")
1542
  if agg_func_name == "計數":
1543
  agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
1544
  y_col_agg = '__count__'
1545
  else:
1546
  agg_func_pd = agg_function_map(agg_func_name)
1547
  if not y_column: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
 
1548
  if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_column]):
 
1549
  try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='raise')
1550
  except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
 
1551
  try:
 
1552
  agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd).reset_index()
1553
+ y_col_agg = y_column
1554
+ except Exception as agg_e: raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
 
1555
  else:
1556
  agg_df = df_processed
1557
+ y_col_agg = y_column
1558
 
1559
+ if agg_df is None or agg_df.empty: raise ValueError("數據聚合後沒有產生有效結果。")
 
 
 
1560
  required_cols_for_plot = [x_column]
1561
  if y_col_agg: required_cols_for_plot.append(y_col_agg)
1562
  if group_col: required_cols_for_plot.append(group_col)
1563
  if size_col: required_cols_for_plot.append(size_col)
1564
  missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
1565
+ if missing_cols: raise ValueError(f"聚合後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
 
 
1566
 
1567
  # --- 4. 獲取顏色方案 ---
1568
  colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)
 
1570
  # --- 5. 創建圖表 (核心邏輯) ---
1571
  fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
1572
  if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
 
 
 
 
1573
  effective_y = y_col_agg if y_needed or agg_func_name == "計數" else None
1574
 
1575
+ # --- (繪圖邏輯 - 與 V4 相同,省略) ---
1576
  if chart_type == "長條圖":
1577
  if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
1578
  fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
 
1619
  fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
1620
  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])]))
1621
  elif chart_type == "散點圖":
1622
+ if not y_column: raise ValueError("散點圖需要選擇 Y 軸列。")
1623
  fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
1624
  elif chart_type == "氣泡圖":
1625
  if not y_column: raise ValueError("氣泡圖需要選擇 Y 軸列。")
 
1677
  if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
1678
  fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
1679
  elif chart_type == "甘特圖":
1680
+ start_col_gantt = y_column; end_col_gantt = group_col; task_col_gantt = x_column
 
 
 
1681
  if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
1682
  try:
1683
+ df_gantt = df.copy()
1684
  if start_col_gantt not in df_gantt.columns: raise ValueError(f"開始列 '{start_col_gantt}' 不在數據中。")
1685
  if end_col_gantt not in df_gantt.columns: raise ValueError(f"結束列 '{end_col_gantt}' 不在數據中。")
1686
  if task_col_gantt not in df_gantt.columns: raise ValueError(f"任務列 '{task_col_gantt}' 不在數據中。")
 
1699
 
1700
 
1701
  # --- 6. 應用圖案 (如果支持) ---
1702
+ # (與 V4 相同,省略)
1703
  if patterns:
1704
  try:
1705
  num_traces = len(fig.data)
 
1722
  dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
1723
  for i, trace in enumerate(fig.data):
1724
  pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
1725
+ if dash: trace.line.dash = dash; trace.fill = 'tonexty' if hasattr(trace, 'stackgroup') and trace.stackgroup else 'tozeroy'
1726
  except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")
1727
 
1728
  # --- 7. 更新佈局 ---
1729
+ # (與 V4 相同)
1730
  fig.update_layout(
1731
  showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
1732
  template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
 
1734
  hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
1735
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
1736
  )
 
1737
  final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
1738
  if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
1739
  elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
1740
  elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
1741
+ elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column)
1742
  else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
1743
 
1744
  except ValueError as ve:
1745
+ print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
1746
  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)
1747
  except Exception as e:
1748
+ error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
1749
  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)
1750
  return fig
1751
 
1752
  # =========================================
1753
  # == 導出與下載函數 (Export & Download Functions) ==
1754
  # =========================================
1755
+ # (與 V4 相同)
1756
  def export_data(df, format_type):
1757
  if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
1758
  try:
 
1767
  if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
1768
  try:
1769
  format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
1770
+ import kaleido # 確保導入
 
1771
  fig.write_image(filename, format=format_lower)
1772
  return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1773
+ except ImportError: error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
 
 
1774
  except ValueError as ve:
1775
+ if "kaleido" in str(ve).lower(): error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"; print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
1776
+ else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
1777
+ except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"導出圖表時發生未預期錯誤: {e}"
 
 
 
 
 
1778
 
1779
  # =========================================
1780
  # == 智能推薦函數 (Recommendation Function) ==
1781
  # =========================================
1782
+ # (與 V4 相同)
1783
  def recommend_chart_settings(df):
1784
  recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
1785
  if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
 
1794
  elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
1795
  else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
1796
  except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
 
1797
  if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
1798
  if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
1799
  if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
 
1800
  if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
1801
+ if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
1802
  return recommendation
1803
 
1804
  # =========================================
1805
  # == CSS 樣式 (CSS Styling) ==
1806
  # =========================================
1807
+ # (與 V4 相同)
1808
  CUSTOM_CSS = """
1809
  /* --- 全局和容器 --- */
1810
  .gradio-container { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
 
1854
  # =========================================
1855
  # == Gradio UI 介面定義 (Gradio UI Definition) ==
1856
  # =========================================
1857
+ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V5", theme=gr.themes.Soft()) as demo:
1858
 
1859
  # --- 應用程式標頭 ---
1860
  gr.HTML("""
1861
  <div class="app-header">
1862
+ <h1 class="app-title">📊 進階數據可視化工具 V5</h1>
1863
+ <p class="app-subtitle">上傳或貼上數據,創建專業圖表 (極簡化測試版)</p>
1864
  </div>
1865
  """)
1866
 
1867
  # --- 狀態變量 ---
1868
  data_state = gr.State(None)
1869
+ custom_colors_state = gr.State({}) # 只保留一組狀態
1870
+ patterns_state = gr.State([])
1871
+ recommendation_state = gr.State({})
 
 
1872
 
1873
  # --- 主頁籤佈局 ---
1874
  with gr.Tabs() as tabs:
 
1893
  gr.Markdown("下方將顯示載入或解析後的數據預覽。")
1894
  data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
1895
  with gr.Row():
1896
+ export_format = gr.Radio(EXPORT_FORMATS_DATA, label="選擇導出格式", value="CSV") # Radio
1897
  export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
1898
  export_result = gr.File(label="導出文件下載", interactive=False)
1899
  export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1900
 
1901
+ # --- 圖表創建頁籤 (單圖表, 移除 Accordion, 使用 Radio) ---
1902
+ with gr.TabItem("📈 圖表創建", id=1):
1903
+ gr.HTML('<div class="section-title">創建圖表</div>')
1904
+ gr.Markdown("在此設置並生成圖表。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1905
 
1906
+ with gr.Row(): # Row
1907
+ # --- 設定欄 (左側 Column) ---
1908
  with gr.Column(scale=1):
1909
+ gr.Markdown("### 📊 圖表設置")
1910
  with gr.Group(elem_classes=["card"]):
1911
  gr.Markdown("**基本設置**")
1912
+ chart_type = gr.Radio(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True) # Radio
1913
+ recommend_button = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm")
1914
+ chart_title = gr.Textbox(label="圖表標題", placeholder="我的圖表")
1915
+ agg_function = gr.Radio(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數") # Radio
1916
+
1917
+ gr.Markdown("**數據映射 (請選擇)**")
1918
+ # 使用 Radio 進行欄位選擇 - 如果欄位過多會很長!
1919
+ x_column = gr.Radio([NO_DATA_STR], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
1920
+ y_column = gr.Radio([NO_DATA_STR], label="Y軸 / 數值", info="選擇圖表數值或 Y (計數時可忽略)")
1921
+ group_column = gr.Radio([NONE_STR, NO_DATA_STR], label="分組列", info="用於生成多系列或堆疊", value=NONE_STR)
1922
+ size_column = gr.Radio([NONE_STR, NO_DATA_STR], label="大小列", info="用於氣泡圖等控制點的大小", value=NONE_STR)
1923
 
1924
  gr.Markdown("**顯示選項**")
1925
+ chart_width = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1926
+ chart_height = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1927
+ show_grid = gr.Radio(YES_NO_CHOICES, label="顯示網格", value="是") # Radio
1928
+ show_legend = gr.Radio(YES_NO_CHOICES, label="顯示圖例", value="是") # Radio
1929
+ color_scheme = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)") # 保留 Dropdown
1930
+ gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
1931
+ gr.HTML(generate_color_cards(), elem_id="color_display")
1932
 
1933
  gr.Markdown("**圖案與自定義顏色**")
1934
+ pattern1 = gr.Radio(PATTERN_TYPES, label="圖案1", value="無") # Radio
1935
+ pattern2 = gr.Radio(PATTERN_TYPES, label="圖案2", value="無") # Radio
1936
+ pattern3 = gr.Radio(PATTERN_TYPES, label="圖案3", value="無") # Radio
1937
+ color_customization = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
1938
+
1939
+ # --- 預覽與操作欄 (右側 Column) ---
1940
+ with gr.Column(scale=2):
1941
+ # 操作按鈕 (預覽上方)
1942
+ gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">操作</div>')
1943
+ update_button = gr.Button("🔄 更新圖表", variant="primary", elem_classes=["primary-button"])
1944
  with gr.Row():
1945
+ export_img_format = gr.Radio(EXPORT_FORMATS_IMG, label="導出格式", value="PNG", scale=1) # Radio
1946
+ download_button = gr.Button("💾 導出圖表", elem_classes=["secondary-button"], scale=1)
1947
+ export_chart = gr.File(label="圖表文件下載", interactive=False)
1948
+ export_chart_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
 
1949
 
1950
+ # 預覽區域
1951
+ gr.HTML('<div class="section-title" style="margin-top:20px; margin-bottom:10px;">圖表預覽</div>')
1952
  with gr.Group(elem_classes=["chart-previewer"]):
1953
+ chart_output = gr.Plot(label="", elem_id="chart_preview")
1954
 
1955
 
1956
  # --- 使用說明頁籤 ---
1957
  with gr.TabItem("❓ 使用說明", id=2):
1958
  with gr.Group(elem_classes=["card"]):
1959
  gr.HTML("""
1960
+ <div class="section-title">使用說明 (V5 - 極簡測試版)</div>
1961
  <h3>數據輸入</h3>
1962
  <ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
1963
+ <h3>圖表創建</h3>
1964
+ <ul><li>此頁面僅提供一個圖表進行測試。</li><li><strong>智能推薦:</strong>點擊 "智能推薦" 按鈕,系統會根據數據結構嘗試推薦合適的設置。</li><li><strong>圖表類型/聚合函數/圖案等:</strong>使用點選按鈕 (Radio) 進行選擇。</li>
1965
+ <ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong>若要統計某一欄位中各個項目出現的次數,請在 <strong>X軸/類別</strong> 選擇該欄位,並將 <strong>聚合函數</strong> 設為 <strong>計數</strong>,此時 <strong>無需選擇 Y軸/數值</strong>。然後選擇「長條圖」或「圓餅圖」。</li></ul>
1966
+ </li><li><strong>數據映射 (欄位選擇):</strong>使用點選按鈕 (Radio) 選擇 X軸、Y軸、分組列、大小列。<strong>注意:如果數據欄位過多,這裡會顯示得很長。</strong></li><li><strong>顯示選項/顏色/自定義:</strong>調整圖表外觀。顏色方案仍為下拉選單。</li><li>點擊 <strong>"更新圖表"</strong> 按鈕生成或刷新圖表預覽 (已移除自動更新)。</li><li>使用 "導出圖表" 功能將生成的圖表保存為圖片文件。</li></ul>
1967
  <h3>提示</h3>
1968
+ <ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>如果欄位選擇的 Radio 按鈕區域過長或無法使用,表示此方法不適用於您的數據,且 Gradio 可能存在根本的元件衝突問題。</li></ul>
1969
  """)
1970
 
1971
  # =========================================
 
1973
  # =========================================
1974
 
1975
  # --- 數據載入與更新 ---
1976
+ def load_data_and_update_ui_v5(df, status_msg):
1977
+ """輔助函數:更新數據狀態、預覽和所有列選擇 Radio"""
1978
+ print("調用 load_data_and_update_ui_v5...", file=sys.stderr)
1979
  preview_df = df if df is not None else pd.DataFrame()
1980
+ # 更新列選擇 Radio
1981
+ col_updates = update_columns_as_radio(df)
1982
  if col_updates is None or len(col_updates) != 4:
1983
+ print("警告: update_columns_as_radio 未返回預期的 4 個組件更新。", file=sys.stderr)
1984
+ # 返回空更新,避免錯誤 (狀態, 消息, 預覽, 4個Radio, 1個Plot)
1985
+ return [df, status_msg, preview_df] + [gr.update()] * 4 + [gr.Plot(value=None)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1986
 
1987
+ # 準備所有更新 (狀態, 消息, 預覽表格, 4 個 Radio)
1988
+ updates = [df, status_msg, preview_df] + list(col_updates)
1989
+ # 添加一個空的 Plot 更新
1990
+ updates.append(gr.Plot(value=None)) # 初始不繪圖
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1991
 
1992
+ print(f"load_data_and_update_ui_v5 返回 {len(updates)} 個更新。", file=sys.stderr)
1993
  return updates
1994
 
1995
+ # 綁定數據載入事件 - 移除初始繪圖
 
1996
  upload_button.click(
1997
  process_upload,
1998
  inputs=[file_upload],
1999
  outputs=[data_state, upload_status]
2000
  ).then(
2001
+ load_data_and_update_ui_v5,
2002
  inputs=[data_state, upload_status],
2003
  outputs=[
2004
  data_state, upload_status, data_preview,
2005
+ x_column, y_column, group_column, size_column, # 更新 Radio
2006
+ chart_output # 更新 Plot 為空
 
 
2007
  ]
2008
  )
2009
  parse_button.click(
 
2011
  inputs=[csv_input],
2012
  outputs=[data_state, parse_status]
2013
  ).then(
2014
+ load_data_and_update_ui_v5,
2015
  inputs=[data_state, parse_status],
2016
  outputs=[
2017
  data_state, parse_status, data_preview,
2018
+ x_column, y_column, group_column, size_column,
2019
+ chart_output
 
2020
  ]
2021
  )
2022
 
 
2024
  # --- 數據導出 ---
2025
  export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
2026
 
2027
+ # --- 顏色和圖案狀態 ---
2028
+ color_customization.change(parse_custom_colors, inputs=[color_customization], outputs=[custom_colors_state])
2029
+ patterns_inputs = [pattern1, pattern2, pattern3]
2030
+ for pattern_radio in patterns_inputs: # 改為 Radio
2031
+ pattern_radio.change(update_patterns, inputs=patterns_inputs, outputs=[patterns_state])
2032
+
2033
+ # --- 更新圖表 (僅通過按鈕) ---
2034
+ chart_inputs = [data_state, chart_type, x_column, y_column, group_column, size_column, color_scheme, patterns_state, chart_title, chart_width, chart_height, show_grid, show_legend, agg_function, custom_colors_state]
2035
+
2036
+ def update_chart_action(*inputs):
2037
+ """按鈕點擊時的處理函數,包含調試信息"""
2038
+ print("="*30, file=sys.stderr)
2039
+ print("更新圖表按鈕點擊!", file=sys.stderr)
2040
+ # 打印傳入 create_plot 的數據狀態
2041
+ df_input = inputs[0] # data_state 在列表第一個
2042
+ print(f" - data_state type in handler: {type(df_input)}", file=sys.stderr)
2043
+ if isinstance(df_input, pd.DataFrame):
2044
+ print(f" - data_state empty in handler: {df_input.empty}", file=sys.stderr)
2045
+ else:
2046
+ print(f" - data_state is not a DataFrame!", file=sys.stderr)
2047
+ print("="*30, file=sys.stderr)
2048
+ return create_plot(*inputs)
2049
 
2050
+ update_button.click(update_chart_action, inputs=chart_inputs, outputs=[chart_output])
2051
 
2052
+ # --- 導出圖表 ---
2053
+ download_button.click(download_figure, inputs=[chart_output, export_img_format], outputs=[export_chart, export_chart_status])
2054
 
2055
+ # --- 智能推薦 ---
2056
+ def apply_recommendation_v5(rec_dict):
2057
  if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
2058
  chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
2059
+ y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", NONE_STR) # 默認為 "無"
2060
  # 返回 Radio 和 Dropdown 的更新
2061
  return [
2062
  gr.Radio(value=chart_type_val), # 更新 Chart Type Radio
2063
+ gr.Radio(value=x_col_val), # 更新 X Column Radio
2064
+ gr.Radio(value=y_col_val), # 更新 Y Column Radio
2065
+ gr.Radio(value=group_col_val), # 更新 Group Column Radio
2066
+ gr.Radio(value=agg_func_val) # 更新 Agg Function Radio
2067
  ]
2068
 
2069
+ recommend_button.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
2070
+ apply_recommendation_v5, inputs=[recommendation_state], outputs=[chart_type, x_column, y_column, group_column, agg_function]
2071
+ ) # 應用推薦後不自動更新圖表,讓用戶點擊按鈕
2072
+
2073
+ # --- 圖表類型改變時更新 UI 元素可見性 ---
2074
+ def update_element_visibility_v5(chart_type):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2075
  try:
2076
+ # (與 V4 相同的邏輯)
2077
  is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
2078
  is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
2079
  is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
 
2088
  elif is_heatmap: group_label, group_needed = "行/列 分組", True
2089
  size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
2090
  if is_gantt: size_label, size_needed = "顏色列 (可選)", True
2091
+ # 返回 Radio 的更新對象
2092
  return (gr.update(label=y_label, visible=y_needed), gr.update(label=group_label, visible=group_needed), gr.update(label=size_label, visible=size_needed))
2093
  except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
2094
 
2095
  # 綁定到 Radio chart_type 的 change 事件
2096
+ chart_type.change(update_element_visibility_v5, inputs=[chart_type], outputs=[y_column, group_column, size_column])
 
2097
 
2098
  # =========================================
2099
  # == 應用程式啟動 (Launch Application) ==