s880453 commited on
Commit
2f05ed2
·
verified ·
1 Parent(s): f4427e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +350 -233
app.py CHANGED
@@ -1273,8 +1273,8 @@ def recommend_chart_settings(df):
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
- 版本:3.0 (完整修復版 - 左右佈局, Accordion, 移除下拉選單 CSS)
1277
- 描述:包含所有功能的完整程式碼,旨在解決下拉選單問題並改進佈局。
1278
  """
1279
 
1280
  # =========================================
@@ -1300,7 +1300,7 @@ import traceback # 用於更詳細的錯誤追蹤
1300
  # == 常數定義 (Constants) ==
1301
  # =========================================
1302
 
1303
- # 圖表類型選項 (Chart Type Options)
1304
  CHART_TYPES = [
1305
  "長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖",
1306
  "折線圖", "多重折線圖", "階梯折線圖",
@@ -1312,7 +1312,7 @@ CHART_TYPES = [
1312
  "雷達圖", "漏斗圖", "極座標圖", "甘特圖"
1313
  ]
1314
 
1315
- # 顏色方案選項 (Color Scheme Options)
1316
  COLOR_SCHEMES = {
1317
  "預設 (Plotly)": px.colors.qualitative.Plotly,
1318
  "分類 - D3": px.colors.qualitative.D3, "分類 - G10": px.colors.qualitative.G10,
@@ -1338,14 +1338,19 @@ COLOR_SCHEMES = {
1338
  "循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
1339
  }
1340
 
1341
- # 圖案填充選項 (Pattern Fill Options)
1342
  PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
1343
 
1344
- # 聚合函數選項 (Aggregation Function Options)
1345
  AGGREGATION_FUNCTIONS = [
1346
  "計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"
1347
  ]
1348
 
 
 
 
 
 
1349
  # =========================================
1350
  # == 輔助函數 (Helper Functions) ==
1351
  # =========================================
@@ -1431,7 +1436,9 @@ def parse_custom_colors(color_text):
1431
  except Exception as e: print(f"解析自定義顏色時出錯: {e}"); return {}
1432
  return custom_colors
1433
  def update_patterns(*patterns_input):
1434
- return [p for p in patterns_input if p and p != "無"]
 
 
1435
 
1436
  # =========================================
1437
  # == 數據處理函數 (Data Processing Functions) ==
@@ -1451,6 +1458,9 @@ def process_upload(file):
1451
  except Exception as e: return None, f"❌ 讀取 Excel 文件時出錯: {e}"
1452
  else: return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"
1453
  df.columns = df.columns.str.strip()
 
 
 
1454
  return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"
1455
  except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
1456
 
@@ -1461,66 +1471,97 @@ def parse_data(text_data):
1461
  if ',' in first_line: separator = ','
1462
  elif '\t' in first_line: separator = '\t'
1463
  elif ' ' in first_line: separator = r'\s+'
1464
- else: separator = ','
1465
- try: df = pd.read_csv(data_io, sep=separator)
 
1466
  except pd.errors.ParserError as pe: return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
1467
  except Exception as e: return None, f"❌ 解析數據時出錯: {e}"
1468
  df.columns = df.columns.str.strip()
 
 
 
1469
  return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"
1470
  except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
1471
 
1472
  def update_columns(df):
1473
  default_choices = ["-- 無數據 --"]
1474
  if df is None or df.empty:
1475
- return (gr.Dropdown(choices=default_choices, value=default_choices[0], label="X軸 / 類別"),
1476
- gr.Dropdown(choices=default_choices, value=default_choices[0], label="Y軸 / 數值"),
1477
- gr.Dropdown(choices=["無"] + default_choices, value="無", label="分組列"),
1478
- gr.Dropdown(choices=["無"] + default_choices, value="無", label="大小列"))
1479
- columns = df.columns.tolist()
1480
- x_default = columns[0] if columns else None
1481
- y_default = columns[1] if len(columns) > 1 else (columns[0] if columns else None)
1482
- valid_columns = [col for col in columns if col is not None and col != ""]
1483
- group_choices, size_choices = ["無"] + valid_columns, ["無"] + valid_columns
1484
- return (gr.Dropdown(choices=valid_columns, value=x_default, label="X軸 / 類別"),
1485
- gr.Dropdown(choices=valid_columns, value=y_default, label="Y軸 / 數值"),
1486
- gr.Dropdown(choices=group_choices, value="無", label="分組列"),
1487
- gr.Dropdown(choices=size_choices, value="無", label="大小列"))
 
 
 
 
 
 
 
 
1488
 
1489
  # =========================================
1490
  # == 圖表創建核心函數 (Core Plotting Function) ==
1491
  # =========================================
1492
  def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
1493
  color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
1494
- show_grid=True, show_legend=True, agg_func_name="計數", custom_colors_dict={}):
1495
  fig = go.Figure()
1496
  try:
1497
- # --- 1. 輸入驗證 ---
 
 
 
 
1498
  if df is None or df.empty: raise ValueError("沒有有效的數據可供繪圖。")
 
 
1499
  if not x_column or x_column == "-- 無數據 --": raise ValueError("請選擇有效的 X 軸或類別列。")
1500
- # 只有在非計數且非直方圖時才嚴格要求 Y 軸
1501
- if agg_func_name != "計數" and chart_type not in ["直方圖"] and (not y_column or y_column == "-- 無數據 --"):
1502
- raise ValueError("請選擇有效的 Y 軸或數值列。")
1503
  if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。")
1504
- # 只有在需要 Y 軸時才檢查 Y 軸是否存在
1505
- if agg_func_name != "計數" and chart_type not in ["直方圖"] and y_column not in df.columns:
1506
- raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。")
1507
- if group_column and group_column != "無" and group_column not in df.columns: raise ValueError(f"分組列 '{group_column}' 不在數據中。")
1508
- if size_column and size_column != "無" and size_column not in df.columns: raise ValueError(f"大小列 '{size_column}' 不在數據中。")
1509
 
1510
- group_col, size_col = (None if col == "無" else col for col in [group_column, size_column])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1511
  df_processed = df.copy()
1512
 
1513
  # --- 2. 數據類型轉換與準備 ---
1514
- for col in [x_column, y_column, group_col, size_col]:
1515
- if col:
1516
- try:
1517
- # 嘗試將 Y 軸和大小列轉為數值 (如果需要聚合或用於大小)
1518
- if (agg_func_name != "計數" and col == y_column) or (col == size_col):
1519
- df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
1520
- # X 軸和分組列通常視為分類 (字符串)
1521
- elif col in [x_column, group_col]:
1522
- df_processed[col] = df_processed[col].astype(str)
1523
- except Exception as e: print(f"警告:轉換列 '{col}' 類型時出錯: {e}")
 
 
 
1524
 
1525
  # --- 3. 數據聚合 (如果需要) ---
1526
  needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
@@ -1528,36 +1569,41 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1528
  y_col_agg = y_column # 預設 Y 軸列名
1529
  if needs_aggregation:
1530
  grouping_cols = [x_column] + ([group_col] if group_col else [])
1531
- # 檢查分組列是否有效
1532
- invalid_grouping_cols = [col for col in grouping_cols if col not in df_processed.columns]
1533
- if invalid_grouping_cols:
1534
- raise ValueError(f"以下分組/X軸列不在數據中: {', '.join(invalid_grouping_cols)}")
1535
 
1536
  if agg_func_name == "計數":
1537
- # 使用 size() 計算每個組的行數
1538
- agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size() # dropna=False 包含 NaN 類別
1539
- agg_df = agg_df.reset_index(name='__count__')
1540
- y_col_agg = '__count__' # 使用新生成的計數列
1541
  else:
1542
  agg_func_pd = agg_function_map(agg_func_name)
1543
- if not y_column or y_column not in df_processed.columns: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
1544
- # 再次檢查 Y 軸是否為數值
1545
- if not pd.api.types.is_numeric_dtype(df_processed[y_column]):
1546
- # 如果聚合函數是 first 或 last,允許非數值
1547
- if agg_func_pd not in ['first', 'last']:
1548
- raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
1549
 
1550
  try:
1551
  # 執行聚合
1552
- agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd)
1553
- agg_df = agg_df.reset_index()
1554
- # y_col_agg 保持為 y_column
1555
  except Exception as agg_e:
1556
  raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
1557
  else:
1558
- # 不需要聚合,直接使用處理過的數據
1559
  agg_df = df_processed
1560
- # y_col_agg 保持為 y_column
 
 
 
 
 
 
 
 
 
 
 
 
 
1561
 
1562
  # --- 4. 獲取顏色方案 ---
1563
  colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)
@@ -1567,136 +1613,138 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1567
  if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
1568
 
1569
  # --- (繪圖邏輯開始) ---
 
 
 
 
1570
  if chart_type == "長條圖":
1571
- fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
 
1572
  elif chart_type == "堆疊長條圖":
1573
- fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, barmode='stack', **fig_params)
 
1574
  elif chart_type == "百分比堆疊長條圖":
1575
- fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, barmode='relative', text_auto='.1%', **fig_params)
 
1576
  fig.update_layout(yaxis_title="百分比 (%)")
1577
  elif chart_type == "群組長條圖":
1578
- fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, barmode='group', **fig_params)
 
1579
  elif chart_type == "水平長條圖":
1580
- fig = px.bar(agg_df, y=x_column, x=y_col_agg, color=group_col, orientation='h', **fig_params)
 
1581
  elif chart_type == "折線圖":
1582
- fig = px.line(agg_df, x=x_column, y=y_col_agg, color=group_col, markers=True, **fig_params)
 
1583
  elif chart_type == "多重折線圖":
1584
- fig = px.line(agg_df, x=x_column, y=y_col_agg, color=group_col, markers=True, **fig_params)
 
1585
  elif chart_type == "階梯折線圖":
1586
- fig = px.line(agg_df, x=x_column, y=y_col_agg, color=group_col, line_shape='hv', **fig_params)
 
1587
  elif chart_type == "區域圖":
1588
- fig = px.area(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
 
1589
  elif chart_type == "堆疊區域圖":
1590
- fig = px.area(agg_df, x=x_column, y=y_col_agg, color=group_col, groupnorm=None, **fig_params)
 
1591
  elif chart_type == "百分比堆疊區域圖":
1592
- fig = px.area(agg_df, x=x_column, y=y_col_agg, color=group_col, groupnorm='percent', **fig_params)
 
1593
  fig.update_layout(yaxis_title="百分比 (%)")
1594
  elif chart_type == "圓餅圖":
 
1595
  if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
1596
- fig = px.pie(agg_df, names=x_column, values=y_col_agg, **fig_params)
1597
  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])]))
1598
  elif chart_type == "環形圖":
 
1599
  if group_col: print("警告:環形圖不支持分組列,已忽略。")
1600
- fig = px.pie(agg_df, names=x_column, values=y_col_agg, hole=0.4, **fig_params)
1601
  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])]))
1602
  elif chart_type == "散點圖":
1603
- fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, **fig_params)
 
1604
  elif chart_type == "氣泡圖":
 
1605
  if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
1606
- # 確保大小列是數值
1607
- if not pd.api.types.is_numeric_dtype(agg_df[size_col]):
1608
- raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
1609
- fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, size_max=60, **fig_params)
1610
  elif chart_type == "直方圖":
1611
- # 直方圖分析單一變量分佈,Y 軸通常是計數
1612
- # 確保 x_column 是數值類型
1613
- if not pd.api.types.is_numeric_dtype(agg_df[x_column]):
1614
- raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
1615
  fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
1616
  elif chart_type == "箱型圖":
1617
- # Y 軸必須是數值
1618
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1619
- raise ValueError(f"箱型圖的 Y 軸列 '{y_col_agg}' 必須是數值類型。")
1620
- fig = px.box(agg_df, x=group_col, y=y_col_agg, color=group_col, **fig_params)
1621
- if not group_col: fig = px.box(agg_df, y=y_col_agg, **fig_params) # 無分組
1622
  elif chart_type == "小提琴圖":
1623
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1624
- raise ValueError(f"小提琴圖的 Y 軸列 '{y_col_agg}' 必須是數值類型。")
1625
- fig = px.violin(agg_df, x=group_col, y=y_col_agg, color=group_col, box=True, points="all", **fig_params)
1626
- if not group_col: fig = px.violin(agg_df, y=y_col_agg, box=True, points="all", **fig_params)
1627
  elif chart_type == "熱力圖":
1628
- if not group_col: raise ValueError("熱力圖需要 X 軸、Y 和一個 分組列 (用於顏色或數值)。")
 
1629
  try:
1630
- # 確保值列是數值
1631
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1632
- raise ValueError(f"熱力圖的值列 '{y_col_agg}' 必須是數值類型。")
1633
- # 創建數據透視表
1634
- pivot_df = pd.pivot_table(agg_df, values=y_col_agg, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
1635
- fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params); # text_auto 顯示數值
1636
  fig.update_layout(coloraxis_showscale=True)
1637
  except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
1638
  elif chart_type == "樹狀圖":
 
1639
  path = [group_col, x_column] if group_col else [x_column]
1640
- # 確保值列是數值
1641
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1642
- raise ValueError(f"樹狀圖的值列 '{y_col_agg}' 必須是數值類型。")
1643
- fig = px.treemap(agg_df, path=path, values=y_col_agg, color=group_col if group_col else x_column, **fig_params)
1644
  elif chart_type == "雷達圖":
1645
- fig = go.Figure() # 使用 go.Figure 創建
1646
- # 確保 R 值是數值
1647
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1648
- raise ValueError(f"雷達圖的徑向值列 '{y_col_agg}' 必須是數值類型。")
1649
- if not group_col: # 單系列
1650
- theta = agg_df[x_column].tolist(); r = agg_df[y_col_agg].tolist(); theta.append(theta[0]); r.append(r[0]) # 封閉圖形
1651
- fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=y_col_agg, line_color=colors[0]))
1652
- else: # 多系列
1653
  categories = agg_df[group_col].unique()
1654
  for i, category in enumerate(categories):
1655
- subset = agg_df[agg_df[group_col] == category]; theta = subset[x_column].tolist(); r = subset[y_col_agg].tolist(); theta.append(theta[0]); r.append(r[0])
1656
  color = custom_colors_dict.get(str(category), colors[i % len(colors)])
1657
  fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
1658
  fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
1659
  elif chart_type == "漏斗圖":
1660
- # 確保值列是數值
1661
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1662
- raise ValueError(f"漏斗圖的值列 '{y_col_agg}' 必須是數值類型。")
1663
- sorted_df = agg_df.sort_values(by=y_col_agg, ascending=False)
1664
- fig = px.funnel(sorted_df, x=y_col_agg, y=x_column, color=group_col, **fig_params)
1665
  elif chart_type == "極座標圖":
1666
- # 確保 R 值是數值
1667
- if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
1668
- raise ValueError(f"極座標圖的徑向值列 '{y_col_agg}' 必須是數值類型。")
1669
- fig = px.bar_polar(agg_df, r=y_col_agg, theta=x_column, color=group_col if group_col else x_column, **fig_params)
1670
  elif chart_type == "甘特圖":
1671
  # 甘特圖使用原始數據,需要開始和結束列
1672
- if not y_column or not group_col: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
 
 
 
1673
  try:
1674
  df_gantt = df.copy() # 使用原始 df
1675
- start_col, end_col = y_column, group_col # 根據用戶選擇
1676
- task_col = x_column # 任務列
1677
- # 檢查列是否存在
1678
- if start_col not in df_gantt.columns: raise ValueError(f"開始列 '{start_col}' 不在數據中。")
1679
- if end_col not in df_gantt.columns: raise ValueError(f"結束列 '{end_col}' 不在數據中。")
1680
- if task_col not in df_gantt.columns: raise ValueError(f"任務列 '{task_col}' 不在數據中。")
1681
-
1682
- # 嘗試轉換為日期時間
1683
- df_gantt['_start_'] = pd.to_datetime(df_gantt[start_col], errors='coerce')
1684
- df_gantt['_end_'] = pd.to_datetime(df_gantt[end_col], errors='coerce')
1685
- # 檢查是否有無效日期
1686
- if df_gantt['_start_'].isnull().any(): raise ValueError(f"開始列 '{start_col}' 包含無效或無法解析的日期時間格式。")
1687
- if df_gantt['_end_'].isnull().any(): raise ValueError(f"結束列 '{end_col}' 包含無效或無法解析的日期時間格式。")
1688
-
1689
- # 繪製甘特圖
1690
- fig = px.timeline(df_gantt, x_start='_start_', x_end='_end_', y=task_col,
1691
- color=size_col if size_col else None, # 可以用大小列來區分顏色
1692
- title=title, color_discrete_sequence=colors, width=width, height=height)
1693
  fig.update_layout(xaxis_type="date")
1694
  except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
1695
  else:
1696
  print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
1697
- fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
 
1698
  # --- (繪圖邏輯結束) ---
1699
 
 
1700
  # --- 6. 應用圖案 (如果支持) ---
1701
  if patterns:
1702
  try:
@@ -1720,7 +1768,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1720
  dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
1721
  for i, trace in enumerate(fig.data):
1722
  pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
1723
- if dash: trace.line.dash = dash; trace.fill = 'tonexty' if 'stackgroup' in trace else 'tozeroy'
1724
  except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")
1725
 
1726
  # --- 7. 更新佈局 ---
@@ -1736,6 +1784,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1736
  if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
1737
  elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
1738
  elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
 
1739
  else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
1740
 
1741
  except ValueError as ve:
@@ -1819,19 +1868,19 @@ CUSTOM_CSS = """
1819
  /* --- 區塊標題 --- */
1820
  .section-title { font-size: 1.4em; font-weight: 600; color: #343a40; border-bottom: 3px solid #7367f0; padding-bottom: 8px; margin-top: 25px; margin-bottom: 20px; }
1821
  /* --- 卡片樣式 --- */
1822
- .card { background-color: white; border-radius: 10px; padding: 20px; /* 稍微減少 padding */ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); margin-bottom: 20px; transition: transform 0.25s ease-out, box-shadow 0.25s ease-out; border: 1px solid #e0e0e0; }
1823
  .card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); }
1824
  /* --- 按鈕樣式 --- */
1825
- .primary-button { background: linear-gradient(to right, #667eea, #764ba2) !important; border: none !important; color: white !important; font-weight: 600 !important; padding: 10px 20px !important; /* 調整 padding */ border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
1826
  .primary-button:hover { background: linear-gradient(to right, #764ba2, #667eea) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
1827
- .secondary-button { background: linear-gradient(to right, #89f7fe, #66a6ff) !important; border: none !important; color: #333 !important; font-weight: 600 !important; padding: 8px 16px !important; /* 調整 padding */ border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
1828
  .secondary-button:hover { background: linear-gradient(to right, #66a6ff, #89f7fe) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
1829
  /* --- 下拉選單修正 (Dropdown Fix) --- */
1830
  /* 移除自定義下拉選單樣式,使用 Gradio 預設 */
1831
  /* --- 其他 UI 元素 --- */
1832
  .tips-box { background-color: #e7f3ff; border-left: 5px solid #66a6ff; padding: 15px 20px; border-radius: 8px; margin: 20px 0; font-size: 0.95em; color: #333; }
1833
  .tips-box code { background-color: #d1e7fd; padding: 2px 5px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; }
1834
- .chart-previewer { border: 2px dashed #ced4da; border-radius: 10px; padding: 15px; /* 減少 padding */ min-height: 400px; /* 調整高度 */ display: flex; justify-content: center; align-items: center; background-color: #ffffff; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); margin-top: 10px; /* 減少 margin */ }
1835
  .gradio-dataframe table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
1836
  .gradio-dataframe th, .gradio-dataframe td { border: 1px solid #dee2e6; padding: 8px 10px; text-align: left; }
1837
  .gradio-dataframe th { background-color: #f8f9fa; font-weight: 600; }
@@ -1841,26 +1890,30 @@ CUSTOM_CSS = """
1841
  .gradio-tabs .tab-nav button.selected { background-color: #667eea !important; color: white !important; border-bottom: none !important; }
1842
  .gradio-slider label { margin-bottom: 5px !important; }
1843
  .gradio-slider input[type="range"] { cursor: pointer !important; }
1844
- .gradio-checkboxgroup label, .gradio-radio label { padding: 8px 0 !important; }
 
 
 
 
1845
  .gradio-textbox textarea, .gradio-textbox input { border-radius: 6px !important; border: 1px solid #ced4da !important; padding: 10px !important; }
1846
  .gradio-textbox textarea:focus, .gradio-textbox input:focus { border-color: #80bdff !important; box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25) !important; }
1847
  .gradio-file .hidden-upload, .gradio-file .download-button { border-radius: 6px !important; }
1848
  .gradio-file .upload-button { border-radius: 6px !important; background: #6c757d !important; color: white !important; padding: 8px 15px !important; }
1849
  .gradio-file .upload-button:hover { background: #5a6268 !important; }
1850
- /* Accordion 樣式微調 */
1851
- .gradio-accordion > .label { font-weight: 600 !important; font-size: 1.1em !important; padding: 10px 0 !important; }
1852
  """
1853
 
1854
  # =========================================
1855
  # == Gradio UI 介面定義 (Gradio UI Definition) ==
1856
  # =========================================
1857
- with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.themes.Soft()) as demo:
1858
 
1859
  # --- 應用程式標頭 ---
1860
  gr.HTML("""
1861
  <div class="app-header">
1862
- <h1 class="app-title">📊 進階數據可視化工具 V3</h1>
1863
- <p class="app-subtitle">上傳或貼上數據,輕鬆創建和比較多種專業圖表 (左右佈局)</p>
1864
  </div>
1865
  """)
1866
 
@@ -1893,14 +1946,14 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
1893
  gr.HTML('<div class="section-title">2. 數據預覽與導出</div>')
1894
  with gr.Group(elem_classes=["card"]):
1895
  gr.Markdown("下方將顯示載入或解析後的數據預覽。")
1896
- data_preview = gr.Dataframe(label="數據表格預覽", interactive=False) # 移除 height
1897
  with gr.Row():
1898
- export_format = gr.Dropdown(["CSV", "Excel", "JSON"], label="選擇導出格式", value="CSV")
1899
  export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
1900
  export_result = gr.File(label="導出文件下載", interactive=False)
1901
  export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1902
 
1903
- # --- 圖表創建頁籤 (左右佈局) ---
1904
  with gr.TabItem("📈 圖表創建與比較", id=1):
1905
  gr.HTML('<div class="section-title">創建與比較圖表 (左右並列)</div>')
1906
  gr.Markdown("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
@@ -1910,39 +1963,37 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
1910
  with gr.Column(scale=1):
1911
  gr.Markdown("### 📊 圖表一")
1912
  with gr.Group(elem_classes=["card"]): # 將所有設定放在一個卡片中
1913
- with gr.Accordion("基本設置", open=True): # 使用 Accordion
1914
- chart_type_1 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True)
1915
- recommend_button_1 = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm") # 縮小按鈕
1916
- chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一標題")
1917
- agg_function_1 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數", info="選擇如何彙總 Y 軸數據")
1918
-
1919
- with gr.Accordion("數據映射", open=True): # 使用 Accordion
1920
- x_column_1 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
1921
- y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
1922
- group_column_1 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊")
1923
- size_column_1 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小")
1924
-
1925
- with gr.Accordion("顯示選項", open=False): # 預設關閉
1926
- chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1927
- chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1928
- with gr.Row():
1929
- show_grid_1 = gr.Checkbox(label="顯示網格", value=True)
1930
- show_legend_1 = gr.Checkbox(label="顯示圖例", value=True)
1931
- color_scheme_1 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)")
1932
- gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
1933
- gr.HTML(generate_color_cards(), elem_id="color_display_1")
1934
-
1935
- with gr.Accordion("圖案與自定義顏色", open=False): # 預設關閉
1936
- with gr.Row():
1937
- pattern1_1 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
1938
- pattern2_1 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="")
1939
- pattern3_1 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1940
- color_customization_1 = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
1941
-
1942
- # 圖表一:操作按鈕 (放在設定下方,預覽上方)
1943
  with gr.Row():
1944
  update_button_1 = gr.Button("🔄 更新圖表一", variant="primary", elem_classes=["primary-button"])
1945
- export_img_format_1 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG", scale=1)
1946
  download_button_1 = gr.Button("💾 導出圖表一", elem_classes=["secondary-button"], scale=1)
1947
  export_chart_1 = gr.File(label="圖表一文件下載", interactive=False)
1948
  export_chart_status_1 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
@@ -1955,39 +2006,36 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
1955
  # --- 圖表二 (右側 Column) ---
1956
  with gr.Column(scale=1):
1957
  gr.Markdown("### 📊 圖表二")
1958
- with gr.Group(elem_classes=["card"]): # 將所有設定放在一個卡片中
1959
- with gr.Accordion("基本設置", open=True): # 使用 Accordion
1960
- chart_type_2 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="折線圖", interactive=True)
1961
- # 智能推薦只放在圖表一
1962
- chart_title_2 = gr.Textbox(label="圖表標題", placeholder="圖表二標題")
1963
- agg_function_2 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="平均值", info="選擇如何彙總 Y 軸數據")
1964
-
1965
- with gr.Accordion("數據映射", open=True): # 使用 Accordion
1966
- x_column_2 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
1967
- y_column_2 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y (計數時可忽略)")
1968
- group_column_2 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊")
1969
- size_column_2 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小")
1970
-
1971
- with gr.Accordion("顯示選項", open=False): # 預設關閉
1972
- chart_width_2 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1973
- chart_height_2 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1974
- with gr.Row():
1975
- show_grid_2 = gr.Checkbox(label="顯示網格", value=True)
1976
- show_legend_2 = gr.Checkbox(label="顯示圖例", value=True)
1977
- color_scheme_2 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="分類 - Set2")
1978
- # 顏色參考共用
1979
-
1980
- with gr.Accordion("圖案與自定義顏色", open=False): # 預設關閉
1981
- with gr.Row():
1982
- pattern1_2 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="")
1983
- pattern2_2 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
1984
- pattern3_2 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1985
- color_customization_2 = gr.Textbox(label="自定義顏色", placeholder="類別C:#FFC300, 類別D:#C70039", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
1986
 
1987
  # 圖表二:操作按鈕
1988
  with gr.Row():
1989
  update_button_2 = gr.Button("🔄 更新圖表二", variant="primary", elem_classes=["primary-button"])
1990
- export_img_format_2 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG", scale=1)
1991
  download_button_2 = gr.Button("💾 導出圖表二", elem_classes=["secondary-button"], scale=1)
1992
  export_chart_2 = gr.File(label="圖表二文件下載", interactive=False)
1993
  export_chart_status_2 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
@@ -1999,17 +2047,17 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
1999
 
2000
  # --- 使用說明頁籤 ---
2001
  with gr.TabItem("❓ 使用說明", id=2):
2002
- with gr.Group(elem_classes=["card"]): # 使用說明也放在卡片裡
2003
  gr.HTML("""
2004
  <div class="section-title">使用說明</div>
2005
  <h3>數據輸入</h3>
2006
  <ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
2007
  <h3>圖表創建與比較</h3>
2008
- <ul><li>此頁面提供左右兩個獨立的圖表設置和預覽區域(圖表一、圖表二)。</li><li><strong>智能推薦:</strong>點擊圖表一的 "智能推薦" 按鈕,系統會根據數據結構嘗試為圖表一推薦合適的設置。</li><li><strong>圖表類型:</strong>選擇您想創建的圖表樣式。</li><li><strong>聚合函數:</strong>決定如何匯總 Y 軸數據。
2009
  <ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong>若要統計某一欄位中各個項目出現的次數(例如,統計不同產品的銷售筆數),請在 <strong>X軸/類別</strong> 選擇該欄位,並將 <strong>聚合函數</strong> 設為 <strong>計數</strong>,此時 <strong>無需選擇 Y軸/數值</strong>。然後選擇「長條圖」或「圓餅圖」。</li></ul>
2010
- </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>
2011
  <h3>提示</h3>
2012
- <ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>確保數值列確實包含數字,日期列包含有效的日期格式。</li><li>部分圖表類型對數據結構有特定要求(例如,熱力圖、甘特圖)。</li><li>如果下拉選單仍然有問題,嘗試刷新頁面或在不同的瀏覽器中打開。</li></ul>
2013
  """)
2014
 
2015
  # =========================================
@@ -2019,21 +2067,80 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
2019
  # --- 數據載入與更新 ---
2020
  def load_data_and_update_ui(df, status_msg):
2021
  preview_df = df if df is not None else pd.DataFrame()
 
2022
  col_updates = update_columns(df)
2023
  if col_updates is None or len(col_updates) != 4:
2024
  print("警告: update_columns 未返回預期的 4 個組件更新。")
2025
- return [df, status_msg, preview_df] + [gr.update()] * 8
2026
- return [df, status_msg, preview_df] + list(col_updates) * 2 # 更新兩組下拉列表
2027
-
2028
- upload_button.click(process_upload, inputs=[file_upload], outputs=[data_state, upload_status]).then(
2029
- load_data_and_update_ui, inputs=[data_state, upload_status],
2030
- outputs=[data_state, upload_status, data_preview, x_column_1, y_column_1, group_column_1, size_column_1, x_column_2, y_column_2, group_column_2, size_column_2]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2031
  )
2032
- parse_button.click(parse_data, inputs=[csv_input], outputs=[data_state, parse_status]).then(
2033
- load_data_and_update_ui, inputs=[data_state, parse_status],
2034
- outputs=[data_state, parse_status, data_preview, x_column_1, y_column_1, group_column_1, size_column_1, x_column_2, y_column_2, group_column_2, size_column_2]
 
 
 
 
 
 
 
 
 
 
2035
  )
2036
 
 
2037
  # --- 數據導出 ---
2038
  export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
2039
 
@@ -2048,7 +2155,6 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
2048
  def auto_update_chart_1(*inputs): return create_plot(*inputs)
2049
  # 綁定 change 事件到會影響圖表的 UI 組件
2050
  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]:
2051
- # 確保組件不是 None 且有 change 方法
2052
  if input_component is not None and hasattr(input_component, 'change'):
2053
  input_component.change(auto_update_chart_1, inputs=chart_inputs_1, outputs=[chart_output_1])
2054
 
@@ -2061,7 +2167,14 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
2061
  if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
2062
  chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
2063
  y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", "無")
2064
- return [gr.Dropdown(value=chart_type_val), gr.Dropdown(value=x_col_val), gr.Dropdown(value=y_col_val), gr.Dropdown(value=group_col_val), gr.Dropdown(value=agg_func_val)]
 
 
 
 
 
 
 
2065
 
2066
  recommend_button_1.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
2067
  apply_recommendation, inputs=[recommendation_state], outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
@@ -2084,9 +2197,10 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
2084
  # --- 圖表二:導出圖表 ---
2085
  download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
2086
 
2087
- # --- 圖表類型改變時更新 UI 元素可見性 ---
2088
  def update_element_visibility(chart_type):
2089
  try:
 
2090
  is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
2091
  is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
2092
  is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
@@ -2101,9 +2215,11 @@ with gr.Blocks(css=CUSTOM_CSS, title="��階數據可視化工具 V3", theme=gr.
2101
  elif is_heatmap: group_label, group_needed = "行/列 分組", True
2102
  size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
2103
  if is_gantt: size_label, size_needed = "顏色列 (可選)", True
 
2104
  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))
2105
  except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
2106
 
 
2107
  chart_type_1.change(update_element_visibility, inputs=[chart_type_1], outputs=[y_column_1, group_column_1, size_column_1])
2108
  chart_type_2.change(update_element_visibility, inputs=[chart_type_2], outputs=[y_column_2, group_column_2, size_column_2])
2109
 
@@ -2111,4 +2227,5 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.
2111
  # == 應用程式啟動 (Launch Application) ==
2112
  # =========================================
2113
  if __name__ == "__main__":
2114
- demo.launch(debug=True)
 
 
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
+ 版本:4.0 (改用 Radio + 簡化佈局)
1277
+ 描述:包含所有功能的完整程式碼,大量使用 Radio Button 替代 Dropdown,期望解決 UI 問題。
1278
  """
1279
 
1280
  # =========================================
 
1300
  # == 常數定義 (Constants) ==
1301
  # =========================================
1302
 
1303
+ # 圖表類型選項 (Chart Type Options) - 保持列表
1304
  CHART_TYPES = [
1305
  "長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖",
1306
  "折線圖", "多重折線圖", "階梯折線圖",
 
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,
 
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
  # =========================================
 
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) ==
 
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
 
 
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
 
1511
  # =========================================
1512
  # == 圖表創建核心函數 (Core Plotting Function) ==
1513
  # =========================================
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. "是"/"否" 轉換為布林值 ---
1520
+ show_grid = True if show_grid_str == "是" else False
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}")
1561
+ if size_col:
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 ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
 
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)
 
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)
1623
  elif chart_type == "堆疊長條圖":
1624
+ if not effective_y: raise ValueError("堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
1625
+ fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='stack', **fig_params)
1626
  elif chart_type == "百分比堆疊長條圖":
1627
+ if not effective_y: raise ValueError("百分比堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
1628
+ fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='relative', text_auto='.1%', **fig_params)
1629
  fig.update_layout(yaxis_title="百分比 (%)")
1630
  elif chart_type == "群組長條圖":
1631
+ if not effective_y: raise ValueError("群組長條圖需要 Y 軸數值或 '計數' 聚合。")
1632
+ fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='group', **fig_params)
1633
  elif chart_type == "水平長條圖":
1634
+ if not effective_y: raise ValueError("水平長條圖需要 Y 軸數值或 '計數' 聚合。")
1635
+ fig = px.bar(agg_df, y=x_column, x=effective_y, color=group_col, orientation='h', **fig_params)
1636
  elif chart_type == "折線圖":
1637
+ if not effective_y: raise ValueError("折線圖需要 Y 軸數值或 '計數' 聚合。")
1638
+ fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
1639
  elif chart_type == "多重折線圖":
1640
+ if not effective_y: raise ValueError("多重折線圖需要 Y 軸數值或 '計數' 聚合。")
1641
+ fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
1642
  elif chart_type == "階梯折線圖":
1643
+ if not effective_y: raise ValueError("階梯折線圖需要 Y 軸數值或 '計數' 聚合。")
1644
+ fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, line_shape='hv', **fig_params)
1645
  elif chart_type == "區域圖":
1646
+ if not effective_y: raise ValueError("區域圖需要 Y 軸數值或 '計數' 聚合。")
1647
+ fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
1648
  elif chart_type == "堆疊區域圖":
1649
+ if not effective_y: raise ValueError("堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
1650
+ fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, groupnorm=None, **fig_params)
1651
  elif chart_type == "百分比堆疊區域圖":
1652
+ if not effective_y: raise ValueError("百分比堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
1653
+ fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, groupnorm='percent', **fig_params)
1654
  fig.update_layout(yaxis_title="百分比 (%)")
1655
  elif chart_type == "圓餅圖":
1656
+ if not effective_y: raise ValueError("圓餅圖需要 Y 軸數值或 '計數' 聚合。")
1657
  if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
1658
+ fig = px.pie(agg_df, names=x_column, values=effective_y, **fig_params)
1659
  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])]))
1660
  elif chart_type == "環形圖":
1661
+ if not effective_y: raise ValueError("環形圖需要 Y 軸數值或 '計數' 聚合。")
1662
  if group_col: print("警告:環形圖不支持分組列,已忽略。")
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 軸列。")
1670
  if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
1671
+ if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
1672
+ fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, size_max=60, **fig_params)
 
 
1673
  elif chart_type == "直方圖":
1674
+ if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
 
 
 
1675
  fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
1676
  elif chart_type == "箱型圖":
1677
+ if not y_column: raise ValueError("箱型圖需要選擇 Y 軸列。")
1678
+ if not pd.api.types.is_numeric_dtype(agg_df[y_column]): raise ValueError(f"箱型圖的 Y 軸列 '{y_column}' 必須是數值類型。")
1679
+ fig = px.box(agg_df, x=group_col, y=y_column, color=group_col, **fig_params)
1680
+ if not group_col: fig = px.box(agg_df, y=y_column, **fig_params)
 
1681
  elif chart_type == "小提琴圖":
1682
+ if not y_column: raise ValueError("小提琴圖需要選擇 Y 軸列。")
1683
+ if not pd.api.types.is_numeric_dtype(agg_df[y_column]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_column}' 必須是數值類型。")
1684
+ fig = px.violin(agg_df, x=group_col, y=y_column, color=group_col, box=True, points="all", **fig_params)
1685
+ if not group_col: fig = px.violin(agg_df, y=y_column, box=True, points="all", **fig_params)
1686
  elif chart_type == "熱力圖":
1687
+ if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
1688
+ if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
1689
  try:
1690
+ if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"熱力圖的值列 '{effective_y}' 必須是數值類型。")
1691
+ pivot_df = pd.pivot_table(agg_df, values=effective_y, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
1692
+ fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params);
 
 
 
1693
  fig.update_layout(coloraxis_showscale=True)
1694
  except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
1695
  elif chart_type == "樹狀圖":
1696
+ if not effective_y: raise ValueError("樹狀圖需要 Y 軸數值或 '計數' 聚合。")
1697
  path = [group_col, x_column] if group_col else [x_column]
1698
+ if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"樹狀圖的值列 '{effective_y}' 必須是數值類型。")
1699
+ fig = px.treemap(agg_df, path=path, values=effective_y, color=group_col if group_col else x_column, **fig_params)
 
 
1700
  elif chart_type == "雷達圖":
1701
+ if not effective_y: raise ValueError("雷達圖需要 Y 軸數值或 '計數' 聚合。")
1702
+ fig = go.Figure()
1703
+ if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"雷達圖的徑向值列 '{effective_y}' 必須是數值類型。")
1704
+ if not group_col:
1705
+ theta = agg_df[x_column].tolist(); r = agg_df[effective_y].tolist(); theta.append(theta[0]); r.append(r[0])
1706
+ fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=effective_y if effective_y != '__count__' else '計數', line_color=colors[0]))
1707
+ else:
 
1708
  categories = agg_df[group_col].unique()
1709
  for i, category in enumerate(categories):
1710
+ subset = agg_df[agg_df[group_col] == category]; theta = subset[x_column].tolist(); r = subset[effective_y].tolist(); theta.append(theta[0]); r.append(r[0])
1711
  color = custom_colors_dict.get(str(category), colors[i % len(colors)])
1712
  fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
1713
  fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
1714
  elif chart_type == "漏斗圖":
1715
+ if not effective_y: raise ValueError("漏斗圖需要 Y 軸數值或 '計數' 聚合。")
1716
+ if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"漏斗圖的值列 '{effective_y}' 必須是數值類型。")
1717
+ sorted_df = agg_df.sort_values(by=effective_y, ascending=False)
1718
+ fig = px.funnel(sorted_df, x=effective_y, y=x_column, color=group_col, **fig_params)
 
1719
  elif chart_type == "極座標圖":
1720
+ if not effective_y: raise ValueError("極座標圖需要 Y 軸數值或 '計數' 聚合。")
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}' 不在數據中。")
1734
+ df_gantt['_start_'] = pd.to_datetime(df_gantt[start_col_gantt], errors='coerce')
1735
+ df_gantt['_end_'] = pd.to_datetime(df_gantt[end_col_gantt], errors='coerce')
1736
+ if df_gantt['_start_'].isnull().any(): raise ValueError(f"開始列 '{start_col_gantt}' 包含無效或無法解析的日期時間格式。")
1737
+ if df_gantt['_end_'].isnull().any(): raise ValueError(f"結束列 '{end_col_gantt}' 包含無效或無法解析的日期時間格式。")
1738
+ fig = px.timeline(df_gantt, x_start='_start_', x_end='_end_', y=task_col_gantt, color=size_col if size_col else None, title=title, color_discrete_sequence=colors, width=width, height=height)
 
 
 
 
 
 
 
 
 
 
1739
  fig.update_layout(xaxis_type="date")
1740
  except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
1741
  else:
1742
  print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
1743
+ if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
1744
+ fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
1745
  # --- (繪圖邏輯結束) ---
1746
 
1747
+
1748
  # --- 6. 應用圖案 (如果支持) ---
1749
  if patterns:
1750
  try:
 
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. 更新佈局 ---
 
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:
 
1868
  /* --- 區塊標題 --- */
1869
  .section-title { font-size: 1.4em; font-weight: 600; color: #343a40; border-bottom: 3px solid #7367f0; padding-bottom: 8px; margin-top: 25px; margin-bottom: 20px; }
1870
  /* --- 卡片樣式 --- */
1871
+ .card { background-color: white; border-radius: 10px; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); margin-bottom: 20px; transition: transform 0.25s ease-out, box-shadow 0.25s ease-out; border: 1px solid #e0e0e0; }
1872
  .card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); }
1873
  /* --- 按鈕樣式 --- */
1874
+ .primary-button { background: linear-gradient(to right, #667eea, #764ba2) !important; border: none !important; color: white !important; font-weight: 600 !important; padding: 10px 20px !important; border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
1875
  .primary-button:hover { background: linear-gradient(to right, #764ba2, #667eea) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
1876
+ .secondary-button { background: linear-gradient(to right, #89f7fe, #66a6ff) !important; border: none !important; color: #333 !important; font-weight: 600 !important; padding: 8px 16px !important; border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
1877
  .secondary-button:hover { background: linear-gradient(to right, #66a6ff, #89f7fe) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
1878
  /* --- 下拉選單修正 (Dropdown Fix) --- */
1879
  /* 移除自定義下拉選單樣式,使用 Gradio 預設 */
1880
  /* --- 其他 UI 元素 --- */
1881
  .tips-box { background-color: #e7f3ff; border-left: 5px solid #66a6ff; padding: 15px 20px; border-radius: 8px; margin: 20px 0; font-size: 0.95em; color: #333; }
1882
  .tips-box code { background-color: #d1e7fd; padding: 2px 5px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; }
1883
+ .chart-previewer { border: 2px dashed #ced4da; border-radius: 10px; padding: 15px; min-height: 400px; display: flex; justify-content: center; align-items: center; background-color: #ffffff; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); margin-top: 10px; }
1884
  .gradio-dataframe table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
1885
  .gradio-dataframe th, .gradio-dataframe td { border: 1px solid #dee2e6; padding: 8px 10px; text-align: left; }
1886
  .gradio-dataframe th { background-color: #f8f9fa; font-weight: 600; }
 
1890
  .gradio-tabs .tab-nav button.selected { background-color: #667eea !important; color: white !important; border-bottom: none !important; }
1891
  .gradio-slider label { margin-bottom: 5px !important; }
1892
  .gradio-slider input[type="range"] { cursor: pointer !important; }
1893
+ /* Radio Button 樣式調整 */
1894
+ .gradio-radio fieldset { display: flex; flex-wrap: wrap; gap: 5px 15px; } /* 嘗試讓選項水平排列並換行 */
1895
+ .gradio-radio label { margin-bottom: 0 !important; padding: 5px 0 !important; } /* 調整標籤間距 */
1896
+ .gradio-radio input[type="radio"] { margin-right: 5px !important; }
1897
+
1898
  .gradio-textbox textarea, .gradio-textbox input { border-radius: 6px !important; border: 1px solid #ced4da !important; padding: 10px !important; }
1899
  .gradio-textbox textarea:focus, .gradio-textbox input:focus { border-color: #80bdff !important; box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25) !important; }
1900
  .gradio-file .hidden-upload, .gradio-file .download-button { border-radius: 6px !important; }
1901
  .gradio-file .upload-button { border-radius: 6px !important; background: #6c757d !important; color: white !important; padding: 8px 15px !important; }
1902
  .gradio-file .upload-button:hover { background: #5a6268 !important; }
1903
+ /* Accordion 樣式微調 (如果重新啟用) */
1904
+ /* .gradio-accordion > .label { font-weight: 600 !important; font-size: 1.1em !important; padding: 10px 0 !important; } */
1905
  """
1906
 
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
 
 
1946
  gr.HTML('<div class="section-title">2. 數據預覽與導出</div>')
1947
  with gr.Group(elem_classes=["card"]):
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("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
 
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)
 
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)
 
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
  # =========================================
 
2067
  # --- 數據載入與更新 ---
2068
  def load_data_and_update_ui(df, status_msg):
2069
  preview_df = df if df is not None else pd.DataFrame()
2070
+ # 更新列下拉選單
2071
  col_updates = update_columns(df)
2072
  if col_updates is None or len(col_updates) != 4:
2073
  print("警告: update_columns 未返回預期的 4 個組件更新。")
2074
+ # 返回空更新,避免錯誤
2075
+ updates = [df, status_msg, preview_df] + [gr.update()] * 8
2076
+ return updates
2077
+
2078
+ # 準備所有更新
2079
+ updates = [df, status_msg, preview_df] + list(col_updates) * 2
2080
+
2081
+ # 準備觸發圖表一初始繪圖的輸入
2082
+ # 使用更新後的下拉列表的值(如果有效)或預設值
2083
+ x_col_val = col_updates[0].value if col_updates[0].value else None
2084
+ y_col_val = col_updates[1].value if col_updates[1].value else None
2085
+ # 獲取圖表一的其他預設值
2086
+ chart_type_val = "長條圖" # 來自 chart_type_1 的預設值
2087
+ agg_func_val = "計數" # 來自 agg_function_1 的預設值
2088
+
2089
+ # 只有在數據有效且 X 軸有效時才嘗試繪圖
2090
+ initial_plot = go.Figure() # 默認空圖
2091
+ if df is not None and not df.empty and x_col_val and x_col_val != "-- 無數據 --":
2092
+ try:
2093
+ # 使用預設值嘗試繪製圖表一
2094
+ initial_plot = create_plot(df, chart_type_val, x_col_val, y_col_val,
2095
+ None, None, # group, size
2096
+ "預設 (Plotly)", [], "", 700, 450, # color, patterns, title, w, h
2097
+ "是", "是", agg_func_val, {}) # grid, legend, agg, custom_colors
2098
+ except Exception as initial_plot_e:
2099
+ print(f"載入數據後嘗試初始繪圖時出錯: {initial_plot_e}")
2100
+ initial_plot = go.Figure()
2101
+ initial_plot.add_annotation(text=f"⚠️ 無法生成初始圖表:<br>{initial_plot_e}", align='left', showarrow=False, font=dict(size=14, color="orange"))
2102
+ initial_plot.update_layout(xaxis_visible=False, yaxis_visible=False)
2103
+
2104
+ # 將初始圖表添加到更新列表中
2105
+ updates.append(initial_plot)
2106
+ # 圖表二保持空白
2107
+ updates.append(gr.Plot(value=None)) # 更新圖表二為空
2108
+
2109
+ return updates
2110
+
2111
+
2112
+ # 綁定數據載入事件
2113
+ upload_button.click(
2114
+ process_upload,
2115
+ inputs=[file_upload],
2116
+ outputs=[data_state, upload_status]
2117
+ ).then(
2118
+ load_data_and_update_ui,
2119
+ inputs=[data_state, upload_status],
2120
+ outputs=[
2121
+ data_state, upload_status, data_preview,
2122
+ x_column_1, y_column_1, group_column_1, size_column_1, # 圖表一 Dropdowns
2123
+ x_column_2, y_column_2, group_column_2, size_column_2, # 圖表二 Dropdowns
2124
+ chart_output_1, # 圖表一初始繪圖
2125
+ chart_output_2 # 圖表二初始空白
2126
+ ]
2127
  )
2128
+ parse_button.click(
2129
+ parse_data,
2130
+ inputs=[csv_input],
2131
+ outputs=[data_state, parse_status]
2132
+ ).then(
2133
+ load_data_and_update_ui,
2134
+ inputs=[data_state, parse_status],
2135
+ outputs=[
2136
+ data_state, parse_status, data_preview,
2137
+ x_column_1, y_column_1, group_column_1, size_column_1,
2138
+ x_column_2, y_column_2, group_column_2, size_column_2,
2139
+ chart_output_1, chart_output_2
2140
+ ]
2141
  )
2142
 
2143
+
2144
  # --- 數據導出 ---
2145
  export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
2146
 
 
2155
  def auto_update_chart_1(*inputs): return create_plot(*inputs)
2156
  # 綁定 change 事件到會影響圖表的 UI 組件
2157
  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]:
 
2158
  if input_component is not None and hasattr(input_component, 'change'):
2159
  input_component.change(auto_update_chart_1, inputs=chart_inputs_1, outputs=[chart_output_1])
2160
 
 
2167
  if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
2168
  chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
2169
  y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", "無")
2170
+ # 返回 Radio Dropdown 的更新
2171
+ return [
2172
+ gr.Radio(value=chart_type_val), # 更新 Chart Type Radio
2173
+ gr.Dropdown(value=x_col_val), # 更新 X Column Dropdown
2174
+ gr.Dropdown(value=y_col_val), # 更新 Y Column Dropdown
2175
+ gr.Dropdown(value=group_col_val),# 更新 Group Column Dropdown
2176
+ gr.Radio(value=agg_func_val) # 更新 Agg Function Radio
2177
+ ]
2178
 
2179
  recommend_button_1.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
2180
  apply_recommendation, inputs=[recommendation_state], outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
 
2197
  # --- 圖表二:導出圖表 ---
2198
  download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
2199
 
2200
+ # --- 圖表類型改變時更新 UI 元素可見性 (保持不變) ---
2201
  def update_element_visibility(chart_type):
2202
  try:
2203
+ # (與 V3 相同的邏輯)
2204
  is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
2205
  is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
2206
  is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
 
2215
  elif is_heatmap: group_label, group_needed = "行/列 分組", True
2216
  size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
2217
  if is_gantt: size_label, size_needed = "顏色列 (可選)", True
2218
+ # 返回 Dropdown 的更新對象
2219
  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))
2220
  except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
2221
 
2222
+ # 綁定到 Radio chart_type 的 change 事件
2223
  chart_type_1.change(update_element_visibility, inputs=[chart_type_1], outputs=[y_column_1, group_column_1, size_column_1])
2224
  chart_type_2.change(update_element_visibility, inputs=[chart_type_2], outputs=[y_column_2, group_column_2, size_column_2])
2225
 
 
2227
  # == 應用程式啟動 (Launch Application) ==
2228
  # =========================================
2229
  if __name__ == "__main__":
2230
+ demo.launch(debug=True)
2231
+