s880453 commited on
Commit
f4427e4
·
verified ·
1 Parent(s): 8ebb2b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +178 -99
app.py CHANGED
@@ -1273,8 +1273,8 @@ def recommend_chart_settings(df):
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
- 版本:2.0 (完整修復版 - 簡化佈局移除下拉選單 CSS)
1277
- 描述:包含所有功能的完整程式碼,旨在解決下拉選單問題。
1278
  """
1279
 
1280
  # =========================================
@@ -1497,10 +1497,13 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1497
  # --- 1. 輸入驗證 ---
1498
  if df is None or df.empty: raise ValueError("沒有有效的數據可供繪圖。")
1499
  if not x_column or x_column == "-- 無數據 --": raise ValueError("請選擇有效的 X 軸或類別列。")
1500
- if not y_column or y_column == "-- 無 --":
1501
- if agg_func_name != "計數" and chart_type not in ["直方圖"]: raise ValueError("請選擇有效的 Y 軸或值列。")
 
1502
  if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。")
1503
- if agg_func_name != "計數" and chart_type not in ["直方圖"] and y_column not in df.columns: raise ValueError(f"Y 軸 '{y_column}' 數據中。")
 
 
1504
  if group_column and group_column != "無" and group_column not in df.columns: raise ValueError(f"分組列 '{group_column}' 不在數據中。")
1505
  if size_column and size_column != "無" and size_column not in df.columns: raise ValueError(f"大小列 '{size_column}' 不在數據中。")
1506
 
@@ -1511,8 +1514,10 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1511
  for col in [x_column, y_column, group_col, size_col]:
1512
  if col:
1513
  try:
1514
- if agg_func_name not in ["計"] and col in [y_column, size_col]:
 
1515
  df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
 
1516
  elif col in [x_column, group_col]:
1517
  df_processed[col] = df_processed[col].astype(str)
1518
  except Exception as e: print(f"警告:轉換列 '{col}' 類型時出錯: {e}")
@@ -1523,18 +1528,34 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1523
  y_col_agg = y_column # 預設 Y 軸列名
1524
  if needs_aggregation:
1525
  grouping_cols = [x_column] + ([group_col] if group_col else [])
 
 
 
 
 
1526
  if agg_func_name == "計數":
1527
- agg_df = df_processed.groupby(grouping_cols, observed=False).size().reset_index(name='__count__')
1528
- y_col_agg = '__count__'
 
 
1529
  else:
1530
  agg_func_pd = agg_function_map(agg_func_name)
1531
  if not y_column or y_column not in df_processed.columns: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
1532
- if not pd.api.types.is_numeric_dtype(df_processed[y_column]): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
 
 
 
 
 
1533
  try:
1534
- agg_df = df_processed.groupby(grouping_cols, observed=False)[y_column].agg(agg_func_pd).reset_index()
 
 
1535
  # y_col_agg 保持為 y_column
1536
- except Exception as agg_e: raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
 
1537
  else:
 
1538
  agg_df = df_processed
1539
  # y_col_agg 保持為 y_column
1540
 
@@ -1545,8 +1566,6 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1545
  fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
1546
  if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
1547
 
1548
- # 繪圖邏輯... (與 Part 2 相同,此處省略以縮短篇幅,實際代碼中應包含)
1549
- # ... (省略 chart_type 判斷和 px/go 繪圖代碼) ...
1550
  # --- (繪圖邏輯開始) ---
1551
  if chart_type == "長條圖":
1552
  fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
@@ -1575,37 +1594,60 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1575
  elif chart_type == "圓餅圖":
1576
  if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
1577
  fig = px.pie(agg_df, names=x_column, values=y_col_agg, **fig_params)
1578
- if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(cat, colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
1579
  elif chart_type == "環形圖":
1580
  if group_col: print("警告:環形圖不支持分組列,已忽略。")
1581
  fig = px.pie(agg_df, names=x_column, values=y_col_agg, hole=0.4, **fig_params)
1582
- if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(cat, colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
1583
  elif chart_type == "散點圖":
1584
  fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, **fig_params)
1585
  elif chart_type == "氣泡圖":
1586
  if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
 
 
 
1587
  fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, size_max=60, **fig_params)
1588
  elif chart_type == "直方圖":
 
 
 
 
1589
  fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
1590
  elif chart_type == "箱型圖":
 
 
 
1591
  fig = px.box(agg_df, x=group_col, y=y_col_agg, color=group_col, **fig_params)
1592
- if not group_col: fig = px.box(agg_df, y=y_col_agg, **fig_params)
1593
  elif chart_type == "小提琴圖":
 
 
1594
  fig = px.violin(agg_df, x=group_col, y=y_col_agg, color=group_col, box=True, points="all", **fig_params)
1595
  if not group_col: fig = px.violin(agg_df, y=y_col_agg, box=True, points="all", **fig_params)
1596
  elif chart_type == "熱力圖":
1597
  if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列 (用於顏色或數值)。")
1598
  try:
 
 
 
 
1599
  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')
1600
- fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", **fig_params); fig.update_layout(coloraxis_showscale=True)
 
1601
  except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
1602
  elif chart_type == "樹狀圖":
1603
  path = [group_col, x_column] if group_col else [x_column]
 
 
 
1604
  fig = px.treemap(agg_df, path=path, values=y_col_agg, color=group_col if group_col else x_column, **fig_params)
1605
  elif chart_type == "雷達圖":
1606
  fig = go.Figure() # 使用 go.Figure 創建
 
 
 
1607
  if not group_col: # 單系列
1608
- theta = agg_df[x_column].tolist(); r = agg_df[y_col_agg].tolist(); theta.append(theta[0]); r.append(r[0])
1609
  fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=y_col_agg, line_color=colors[0]))
1610
  else: # 多系列
1611
  categories = agg_df[group_col].unique()
@@ -1615,16 +1657,39 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1615
  fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
1616
  fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
1617
  elif chart_type == "漏斗圖":
 
 
 
1618
  sorted_df = agg_df.sort_values(by=y_col_agg, ascending=False)
1619
  fig = px.funnel(sorted_df, x=y_col_agg, y=x_column, color=group_col, **fig_params)
1620
  elif chart_type == "極座標圖":
 
 
 
1621
  fig = px.bar_polar(agg_df, r=y_col_agg, theta=x_column, color=group_col if group_col else x_column, **fig_params)
1622
  elif chart_type == "甘特圖":
 
1623
  if not y_column or not group_col: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
1624
  try:
1625
- df_gantt = df.copy(); df_gantt['_start_'] = pd.to_datetime(df_gantt[y_column], errors='coerce'); df_gantt['_end_'] = pd.to_datetime(df_gantt[group_col], errors='coerce')
1626
- if df_gantt['_start_'].isnull().any() or df_gantt['_end_'].isnull().any(): raise ValueError("開始列或結束列包含無效的日期時間格式。")
1627
- fig = px.timeline(df_gantt, x_start='_start_', x_end='_end_', y=x_column, color=size_col if size_col else None, title=title, color_discrete_sequence=colors, width=width, height=height)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1628
  fig.update_layout(xaxis_type="date")
1629
  except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
1630
  else:
@@ -1666,9 +1731,12 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
1666
  hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
1667
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
1668
  )
 
 
1669
  if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
1670
- elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=y_col_agg, yaxis_title=x_column)
1671
- else: fig.update_layout(xaxis_title=x_column, yaxis_title=y_col_agg)
 
1672
 
1673
  except ValueError as ve:
1674
  print(f"圖表創建錯誤 (ValueError): {ve}"); traceback.print_exc()
@@ -1695,12 +1763,22 @@ def download_figure(fig, format_type="PNG"):
1695
  if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
1696
  try:
1697
  format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
 
 
1698
  fig.write_image(filename, format=format_lower)
1699
  return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
 
 
 
1700
  except ValueError as ve:
1701
- if "kaleido" in str(ve): error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
1702
- else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
1703
- except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
 
 
 
 
 
1704
 
1705
  # =========================================
1706
  # == 智能推薦函數 (Recommendation Function) ==
@@ -1719,11 +1797,13 @@ def recommend_chart_settings(df):
1719
  elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
1720
  else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
1721
  except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
 
1722
  if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
1723
  if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
1724
  if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
 
1725
  if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
1726
- if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
1727
  return recommendation
1728
 
1729
  # =========================================
@@ -1739,19 +1819,19 @@ CUSTOM_CSS = """
1739
  /* --- 區塊標題 --- */
1740
  .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; }
1741
  /* --- 卡片樣式 --- */
1742
- .card { background-color: white; border-radius: 10px; padding: 25px; 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; }
1743
  .card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); }
1744
  /* --- 按鈕樣式 --- */
1745
- .primary-button { background: linear-gradient(to right, #667eea, #764ba2) !important; border: none !important; color: white !important; font-weight: 600 !important; padding: 12px 24px !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; }
1746
  .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; }
1747
- .secondary-button { background: linear-gradient(to right, #89f7fe, #66a6ff) !important; border: none !important; color: #333 !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; }
1748
  .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; }
1749
  /* --- 下拉選單修正 (Dropdown Fix) --- */
1750
  /* 移除自定義下拉選單樣式,使用 Gradio 預設 */
1751
  /* --- 其他 UI 元素 --- */
1752
  .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; }
1753
  .tips-box code { background-color: #d1e7fd; padding: 2px 5px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; }
1754
- .chart-previewer { border: 2px dashed #ced4da; border-radius: 10px; padding: 20px; min-height: 450px; 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: 15px; }
1755
  .gradio-dataframe table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
1756
  .gradio-dataframe th, .gradio-dataframe td { border: 1px solid #dee2e6; padding: 8px 10px; text-align: left; }
1757
  .gradio-dataframe th { background-color: #f8f9fa; font-weight: 600; }
@@ -1767,18 +1847,20 @@ CUSTOM_CSS = """
1767
  .gradio-file .hidden-upload, .gradio-file .download-button { border-radius: 6px !important; }
1768
  .gradio-file .upload-button { border-radius: 6px !important; background: #6c757d !important; color: white !important; padding: 8px 15px !important; }
1769
  .gradio-file .upload-button:hover { background: #5a6268 !important; }
 
 
1770
  """
1771
 
1772
  # =========================================
1773
  # == Gradio UI 介面定義 (Gradio UI Definition) ==
1774
  # =========================================
1775
- with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.themes.Soft()) as demo:
1776
 
1777
  # --- 應用程式標頭 ---
1778
  gr.HTML("""
1779
  <div class="app-header">
1780
- <h1 class="app-title">📊 進階數據可視化工具 V2</h1>
1781
- <p class="app-subtitle">上傳或貼上數據,輕鬆創建和比較多種專業圖表</p>
1782
  </div>
1783
  """)
1784
 
@@ -1818,32 +1900,29 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1818
  export_result = gr.File(label="導出文件下載", interactive=False)
1819
  export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1820
 
1821
- # --- 圖表創建頁籤 ---
1822
  with gr.TabItem("📈 圖表創建與比較", id=1):
1823
- gr.HTML('<div class="section-title">創建與比較圖表</div>')
1824
  gr.Markdown("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
1825
 
1826
- # --- 圖表一 ---
1827
- with gr.Group():
1828
- gr.Markdown("### 📊 圖表一設置")
1829
- with gr.Row():
1830
- # 圖表一:設定欄 (左側)
1831
- with gr.Column(scale=1): # 調整比例
1832
- with gr.Group(elem_classes=["card"]):
1833
- gr.Markdown("**基本設置**")
1834
  chart_type_1 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True)
1835
- recommend_button_1 = gr.Button("🧠 智能推薦 (圖表一)", elem_classes=["secondary-button"])
1836
- chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一:我的數據分析")
1837
  agg_function_1 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數", info="選擇如何彙總 Y 軸數據")
1838
 
1839
- gr.Markdown("**數據映射**")
1840
  x_column_1 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
1841
  y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
1842
  group_column_1 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊")
1843
  size_column_1 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小")
1844
 
1845
- with gr.Group(elem_classes=["card"]):
1846
- gr.Markdown("**顯示選項**")
1847
  chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1848
  chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1849
  with gr.Row():
@@ -1853,50 +1932,43 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1853
  gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
1854
  gr.HTML(generate_color_cards(), elem_id="color_display_1")
1855
 
1856
- with gr.Group(elem_classes=["card"]):
1857
- gr.Markdown("**圖案與自定義顏色**")
1858
  with gr.Row():
1859
  pattern1_1 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
1860
  pattern2_1 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
1861
  pattern3_1 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1862
  color_customization_1 = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
1863
 
1864
- # 圖表一:預覽與操作 (右側)
1865
- with gr.Column(scale=2): # 調整比例
1866
- gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">圖表一預覽</div>')
1867
- with gr.Group(elem_classes=["chart-previewer"]):
1868
- chart_output_1 = gr.Plot(label="", elem_id="chart_preview_1")
1869
- gr.HTML('<div class="section-title" style="margin-top:20px; margin-bottom:10px;">操作</div>')
1870
  update_button_1 = gr.Button("🔄 更新圖表一", variant="primary", elem_classes=["primary-button"])
1871
- with gr.Row():
1872
- export_img_format_1 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG")
1873
- download_button_1 = gr.Button("💾 導出圖表一", elem_classes=["secondary-button"])
1874
- export_chart_1 = gr.File(label="圖表一文件下載", interactive=False)
1875
- export_chart_status_1 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1876
-
1877
- # --- 分隔線 ---
1878
- gr.Markdown("---")
1879
-
1880
- # --- 圖表二 ---
1881
- with gr.Group():
1882
- gr.Markdown("### 📊 圖表二設置")
1883
- with gr.Row():
1884
- # 圖表二:設定欄 (左側)
1885
- with gr.Column(scale=1): # 調整比例
1886
- with gr.Group(elem_classes=["card"]):
1887
- gr.Markdown("**基本設置**")
1888
  chart_type_2 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="折線圖", interactive=True)
1889
- chart_title_2 = gr.Textbox(label="圖表標題", placeholder="圖表二:另種視角")
 
1890
  agg_function_2 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="平均值", info="選擇如何彙總 Y 軸數據")
1891
 
1892
- gr.Markdown("**數據映射**")
1893
  x_column_2 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
1894
  y_column_2 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
1895
  group_column_2 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊")
1896
  size_column_2 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小")
1897
 
1898
- with gr.Group(elem_classes=["card"]):
1899
- gr.Markdown("**顯示選項**")
1900
  chart_width_2 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1901
  chart_height_2 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1902
  with gr.Row():
@@ -1905,26 +1977,25 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1905
  color_scheme_2 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="分類 - Set2")
1906
  # 顏色參考共用
1907
 
1908
- with gr.Group(elem_classes=["card"]):
1909
- gr.Markdown("**圖案與自定義顏色**")
1910
  with gr.Row():
1911
  pattern1_2 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
1912
  pattern2_2 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
1913
  pattern3_2 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1914
  color_customization_2 = gr.Textbox(label="自定義顏色", placeholder="���別C:#FFC300, 類別D:#C70039", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
1915
 
1916
- # 圖表二:預覽與操作欄 (右側)
1917
- with gr.Column(scale=2): # 調整比例
1918
- gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">圖表二預覽</div>')
1919
- with gr.Group(elem_classes=["chart-previewer"]):
1920
- chart_output_2 = gr.Plot(label="", elem_id="chart_preview_2")
1921
- gr.HTML('<div class="section-title" style="margin-top:20px; margin-bottom:10px;">操作</div>')
1922
  update_button_2 = gr.Button("🔄 更新圖表二", variant="primary", elem_classes=["primary-button"])
1923
- with gr.Row():
1924
- export_img_format_2 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG")
1925
- download_button_2 = gr.Button("💾 導出圖表二", elem_classes=["secondary-button"])
1926
- export_chart_2 = gr.File(label="圖表二文件下載", interactive=False)
1927
- export_chart_status_2 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
 
 
 
 
1928
 
1929
  # --- 使用說明頁籤 ---
1930
  with gr.TabItem("❓ 使用說明", id=2):
@@ -1934,9 +2005,11 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1934
  <h3>數據輸入</h3>
1935
  <ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
1936
  <h3>圖表創建與比較</h3>
1937
- <ul><li>此頁面提供兩個獨立的圖表設置和預覽區域(圖表一、圖表二)。</li><li><strong>智能推薦:</strong>點擊 "智能推薦 (圖表一)" 按鈕,系統會根據數據結構嘗試為圖表一推薦合適的設置。</li><li><strong>圖表類型:</strong>選擇您想創建的圖表樣式。</li><li><strong>聚合函數:</strong>決定如何匯總 Y 軸數據。選擇 "計數" 時,系統會計算 X 軸(和分組列)組合的出現次數,此時無需選擇 Y 軸列。</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>
 
 
1938
  <h3>提示</h3>
1939
- <ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>確保數值列確實包含數字,日期列包含有效的日期格式。</li><li>部分圖表類型對數據結構有特定要求(例如,熱力圖、甘特圖)。</li></ul>
1940
  """)
1941
 
1942
  # =========================================
@@ -1950,7 +2023,7 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1950
  if col_updates is None or len(col_updates) != 4:
1951
  print("警告: update_columns 未返回預期的 4 個組件更新。")
1952
  return [df, status_msg, preview_df] + [gr.update()] * 8
1953
- return [df, status_msg, preview_df] + list(col_updates) * 2
1954
 
1955
  upload_button.click(process_upload, inputs=[file_upload], outputs=[data_state, upload_status]).then(
1956
  load_data_and_update_ui, inputs=[data_state, upload_status],
@@ -1973,8 +2046,12 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1973
  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]
1974
  update_button_1.click(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
1975
  def auto_update_chart_1(*inputs): return create_plot(*inputs)
1976
- for input_component in chart_inputs_1:
1977
- if not isinstance(input_component, gr.State): input_component.change(auto_update_chart_1, inputs=chart_inputs_1, outputs=[chart_output_1])
 
 
 
 
1978
 
1979
  # --- 圖表一:導出圖表 ---
1980
  download_button_1.click(download_figure, inputs=[chart_output_1, export_img_format_1], outputs=[export_chart_1, export_chart_status_1])
@@ -1988,7 +2065,7 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1988
 
1989
  recommend_button_1.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
1990
  apply_recommendation, inputs=[recommendation_state], outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
1991
- ).then(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
1992
 
1993
  # --- 圖表二:顏色和圖案狀態 ---
1994
  color_customization_2.change(parse_custom_colors, inputs=[color_customization_2], outputs=[custom_colors_state_2])
@@ -1999,8 +2076,10 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1999
  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]
2000
  update_button_2.click(create_plot, inputs=chart_inputs_2, outputs=[chart_output_2])
2001
  def auto_update_chart_2(*inputs): return create_plot(*inputs)
2002
- for input_component in chart_inputs_2:
2003
- if not isinstance(input_component, gr.State): input_component.change(auto_update_chart_2, inputs=chart_inputs_2, outputs=[chart_output_2])
 
 
2004
 
2005
  # --- 圖表二:導出圖表 ---
2006
  download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
 
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
+ 版本:3.0 (完整修復版 - 左右佈局, Accordion, 移除下拉選單 CSS)
1277
+ 描述:包含所有功能的完整程式碼,旨在解決下拉選單問題並改進佈局
1278
  """
1279
 
1280
  # =========================================
 
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
 
 
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}")
 
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
 
 
1566
  fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
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)
 
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()
 
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:
 
1731
  hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
1732
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
1733
  )
1734
+ # 根據圖表類型更新軸標籤
1735
+ final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
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:
1742
  print(f"圖表創建錯誤 (ValueError): {ve}"); traceback.print_exc()
 
1763
  if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
1764
  try:
1765
  format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
1766
+ # 確保有 kaleido
1767
+ import kaleido
1768
  fig.write_image(filename, format=format_lower)
1769
  return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1770
+ except ImportError:
1771
+ error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"
1772
+ print(error_msg); return None, error_msg
1773
  except ValueError as ve:
1774
+ # kaleido 可能安裝但無法運行
1775
+ if "kaleido" in str(ve).lower():
1776
+ error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"
1777
+ print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
1778
+ else:
1779
+ print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
1780
+ except Exception as e:
1781
+ print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
1782
 
1783
  # =========================================
1784
  # == 智能推薦函數 (Recommendation Function) ==
 
1797
  elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
1798
  else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
1799
  except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
1800
+ # 驗證推薦的列名
1801
  if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
1802
  if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
1803
  if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
1804
+ # 清理無效聚合
1805
  if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
1806
+ if recommendation["agg_function"] == "計數": recommendation["y_column"] = None # 計數時 Y 軸應為空
1807
  return recommendation
1808
 
1809
  # =========================================
 
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; }
 
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
 
 
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("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
1907
 
1908
+ with gr.Row(): # Row,包含左右兩個圖表區塊
1909
+ # --- 圖表一 (左側 Column) ---
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():
 
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)
1949
+
1950
+ # 圖表一:預覽區域
1951
+ with gr.Group(elem_classes=["chart-previewer"]):
1952
+ chart_output_1 = gr.Plot(label="", elem_id="chart_preview_1")
1953
+
1954
+
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():
 
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)
1994
+
1995
+ # 圖表二:預覽區域
1996
+ with gr.Group(elem_classes=["chart-previewer"]):
1997
+ chart_output_2 = gr.Plot(label="", elem_id="chart_preview_2")
1998
+
1999
 
2000
  # --- 使用說明頁籤 ---
2001
  with gr.TabItem("❓ 使用說明", id=2):
 
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
  # =========================================
 
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],
 
2046
  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]
2047
  update_button_1.click(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
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
+
2055
 
2056
  # --- 圖表一:導出圖表 ---
2057
  download_button_1.click(download_figure, inputs=[chart_output_1, export_img_format_1], outputs=[export_chart_1, export_chart_status_1])
 
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]
2068
+ ).then(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1]) # 應用推薦後觸發更新
2069
 
2070
  # --- 圖表二:顏色和圖案狀態 ---
2071
  color_customization_2.change(parse_custom_colors, inputs=[color_customization_2], outputs=[custom_colors_state_2])
 
2076
  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]
2077
  update_button_2.click(create_plot, inputs=chart_inputs_2, outputs=[chart_output_2])
2078
  def auto_update_chart_2(*inputs): return create_plot(*inputs)
2079
+ # 綁定 change 事件到會影響圖表的 UI 組件
2080
+ 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]:
2081
+ if input_component is not None and hasattr(input_component, 'change'):
2082
+ input_component.change(auto_update_chart_2, inputs=chart_inputs_2, outputs=[chart_output_2])
2083
 
2084
  # --- 圖表二:導出圖表 ---
2085
  download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])