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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +612 -396
app.py CHANGED
@@ -1273,8 +1273,500 @@ def recommend_chart_settings(df):
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
- 版本:1.1 (修正 Dataframe height 參數錯誤)
1277
- 描述:此部分包含 Gradio UI 介面定義、事件處理和應用程式啟動。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1278
  """
1279
 
1280
  # =========================================
@@ -1290,14 +1782,13 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1290
  </div>
1291
  """)
1292
 
1293
- # --- 狀態變量 (State Variables) ---
1294
- # data_state 用於存儲載入的 DataFrame
1295
  data_state = gr.State(None)
1296
- # 分別為兩張圖表存儲自定義顏色和圖案
1297
  custom_colors_state_1 = gr.State({})
1298
  patterns_state_1 = gr.State([])
1299
  custom_colors_state_2 = gr.State({})
1300
  patterns_state_2 = gr.State([])
 
1301
 
1302
  # --- 主頁籤佈局 ---
1303
  with gr.Tabs() as tabs:
@@ -1305,41 +1796,25 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1305
  # --- 數據輸入頁籤 ---
1306
  with gr.TabItem("📁 數據輸入與管理", id=0):
1307
  with gr.Row():
1308
- # 左側:數據輸入
1309
- with gr.Column(scale=2):
1310
  gr.HTML('<div class="section-title">1. 上傳或輸入數據</div>')
1311
  with gr.Group(elem_classes=["card"]):
1312
  gr.Markdown("您可以上傳本地的 CSV 或 Excel 文件,或��接在下方的文本框中貼上數據。")
1313
- file_upload = gr.File(label="上傳 CSV / Excel 文件", type="filepath") # 使用 filepath 更穩定
1314
  upload_button = gr.Button("⬆️ 載入文件數據", elem_classes=["primary-button"])
1315
  upload_status = gr.Textbox(label="載入狀態", lines=1, interactive=False)
1316
-
1317
  with gr.Group(elem_classes=["card"]):
1318
- csv_input = gr.Textbox(
1319
- label="或者,在此貼上數據 (逗號、Tab 或空格分隔)",
1320
- placeholder="例如:\n類別,數值\nA,10\nB,20\nC,15\n\n或\n類別\t數值\nA\t10\nB\t20\nC\t15",
1321
- lines=8,
1322
- elem_classes=["data-input-textbox"]
1323
- )
1324
  parse_button = gr.Button("📝 解析貼上數據", elem_classes=["primary-button"])
1325
  parse_status = gr.Textbox(label="解析狀態", lines=1, interactive=False)
1326
-
1327
- # 右側:數據預覽與導出
1328
- with gr.Column(scale=3):
1329
  gr.HTML('<div class="section-title">2. 數據預覽與導出</div>')
1330
  with gr.Group(elem_classes=["card"]):
1331
  gr.Markdown("下方將顯示載入或解析後的數據預覽。")
1332
- # 修正:移除 height 參數
1333
- data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
1334
-
1335
  with gr.Row():
1336
- export_format = gr.Dropdown(
1337
- ["CSV", "Excel", "JSON"],
1338
- label="選擇導出格式",
1339
- value="CSV"
1340
- )
1341
  export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
1342
- # 使用 File 組件顯示導出結果,以便下載
1343
  export_result = gr.File(label="導出文件下載", interactive=False)
1344
  export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1345
 
@@ -1349,40 +1824,34 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1349
  gr.Markdown("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
1350
 
1351
  # --- 圖表一 ---
1352
- with gr.Group(): # 使用 Group 包裹每個圖表區塊
1353
  gr.Markdown("### 📊 圖表一設置")
1354
  with gr.Row():
1355
- # 圖表一:左側設置
1356
- with gr.Column(scale=2):
1357
  with gr.Group(elem_classes=["card"]):
1358
  gr.Markdown("**基本設置**")
1359
  chart_type_1 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True)
1360
- with gr.Row():
1361
- recommend_button_1 = gr.Button("🧠 智能推薦 (圖表一)", elem_classes=["secondary-button"])
1362
- # recommendation_result_1 = gr.Textbox(label="推薦結果", lines=1, interactive=False) # 暫時隱藏推薦結果文本框
1363
  chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一:我的數據分析")
1364
  agg_function_1 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數", info="選擇如何彙總 Y 軸數據")
1365
 
1366
  gr.Markdown("**數據映射**")
1367
- x_column_1 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇作為圖表主要分類或 X 軸的列")
1368
- y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇作為圖表數值或 Y 軸的列 (計數聚合時可忽略)")
1369
- group_column_1 = gr.Dropdown(["無"], label="分組列", info="選擇用於生成多系列或堆疊的列")
1370
- size_column_1 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等,控制點的大小")
1371
 
1372
- # 圖表一:右側樣式
1373
- with gr.Column(scale=1):
1374
  with gr.Group(elem_classes=["card"]):
1375
  gr.Markdown("**顯示選項**")
1376
- with gr.Row():
1377
- chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1378
- chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1379
  with gr.Row():
1380
  show_grid_1 = gr.Checkbox(label="顯示網格", value=True)
1381
  show_legend_1 = gr.Checkbox(label="顯示圖例", value=True)
1382
-
1383
  color_scheme_1 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)")
1384
  gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
1385
- gr.HTML(generate_color_cards(), elem_id="color_display_1") # 顏色參考
1386
 
1387
  with gr.Group(elem_classes=["card"]):
1388
  gr.Markdown("**圖案與自定義顏色**")
@@ -1390,64 +1859,51 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1390
  pattern1_1 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
1391
  pattern2_1 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
1392
  pattern3_1 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1393
- color_customization_1 = gr.Textbox(
1394
- label="自定義顏色",
1395
- placeholder="類別A:#FF5733, 類別B:#33CFFF",
1396
- info="格式: 類別名:十六進制顏色代碼, ...",
1397
- elem_classes=["color-customization-input"]
1398
- )
1399
-
1400
- # 圖表一:預覽與操作按鈕
1401
- with gr.Row():
1402
- with gr.Column(scale=3):
1403
- gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">圖表一預覽</div>')
1404
- with gr.Group(elem_classes=["chart-previewer"]):
1405
- chart_output_1 = gr.Plot(label="", elem_id="chart_preview_1")
1406
- with gr.Column(scale=1):
1407
- gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">操作</div>')
1408
- update_button_1 = gr.Button("🔄 更新圖表一", variant="primary", elem_classes=["primary-button"])
1409
- export_img_format_1 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG")
1410
- download_button_1 = gr.Button("💾 導出圖表一", elem_classes=["secondary-button"])
1411
- export_chart_1 = gr.File(label="圖表一文件下載", interactive=False)
1412
- export_chart_status_1 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1413
-
1414
 
1415
  # --- 分隔線 ---
1416
  gr.Markdown("---")
1417
 
1418
  # --- 圖表二 ---
1419
- with gr.Group(): # 使用 Group 包裹每個圖表區塊
1420
  gr.Markdown("### 📊 圖表二設置")
1421
  with gr.Row():
1422
- # 圖表二:左側設置
1423
- with gr.Column(scale=2):
1424
  with gr.Group(elem_classes=["card"]):
1425
  gr.Markdown("**基本設置**")
1426
- chart_type_2 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="折線圖", interactive=True) # 預設不同類型
1427
- # 智能推薦按鈕可以只影響圖表一,用戶可以手動調整圖表二
1428
- # recommend_button_2 = gr.Button("🧠 智能推薦 (圖表二)", elem_classes=["secondary-button"])
1429
  chart_title_2 = gr.Textbox(label="圖表標題", placeholder="圖表二:另一種視角")
1430
- agg_function_2 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="平均值", info="選擇如何彙總 Y 軸數據") # 預設不同聚合
1431
 
1432
  gr.Markdown("**數據映射**")
1433
- x_column_2 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇作為圖表主要分類或 X 軸的列")
1434
- y_column_2 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇作為圖表數值或 Y 軸的列 (計數聚合時可忽略)")
1435
- group_column_2 = gr.Dropdown(["無"], label="分組列", info="選擇用於生成多系列或堆疊的列")
1436
- size_column_2 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等,控制點的大小")
1437
 
1438
- # 圖表二:右側樣式
1439
- with gr.Column(scale=1):
1440
  with gr.Group(elem_classes=["card"]):
1441
  gr.Markdown("**顯示選項**")
1442
- with gr.Row():
1443
- chart_width_2 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
1444
- chart_height_2 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
1445
  with gr.Row():
1446
  show_grid_2 = gr.Checkbox(label="顯示網格", value=True)
1447
  show_legend_2 = gr.Checkbox(label="顯示圖例", value=True)
1448
-
1449
- color_scheme_2 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="分類 - Set2") # 預設不同顏色
1450
- # 顏色參考可以共用一個
1451
 
1452
  with gr.Group(elem_classes=["card"]):
1453
  gr.Markdown("**圖案與自定義顏色**")
@@ -1455,74 +1911,32 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1455
  pattern1_2 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
1456
  pattern2_2 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
1457
  pattern3_2 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1458
- color_customization_2 = gr.Textbox(
1459
- label="自定義顏色",
1460
- placeholder="類別C:#FFC300, 類別D:#C70039",
1461
- info="格式: 類別名:十六進制顏色代碼, ...",
1462
- elem_classes=["color-customization-input"]
1463
- )
1464
-
1465
- # 圖表二:預覽與操作按鈕
1466
- with gr.Row():
1467
- with gr.Column(scale=3):
1468
- gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">圖表二預覽</div>')
1469
- with gr.Group(elem_classes=["chart-previewer"]):
1470
- chart_output_2 = gr.Plot(label="", elem_id="chart_preview_2")
1471
- with gr.Column(scale=1):
1472
- gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">操作</div>')
1473
- update_button_2 = gr.Button("🔄 更新圖表二", variant="primary", elem_classes=["primary-button"])
1474
- export_img_format_2 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG")
1475
- download_button_2 = gr.Button("💾 導出圖表二", elem_classes=["secondary-button"])
1476
- export_chart_2 = gr.File(label="圖表二文件下載", interactive=False)
1477
- export_chart_status_2 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1478
-
1479
 
1480
  # --- 使用說明頁籤 ---
1481
  with gr.TabItem("❓ 使用說明", id=2):
1482
- with gr.Group(elem_classes=["card"]):
1483
  gr.HTML("""
1484
  <div class="section-title">使用說明</div>
1485
-
1486
  <h3>數據輸入</h3>
1487
- <ul>
1488
- <li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li>
1489
- <li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li>
1490
- <li>第一行通常被視為欄位名稱(表頭)。</li>
1491
- <li>數據載入或解析成功後,會在右側顯示預覽。</li>
1492
- <li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li>
1493
- </ul>
1494
-
1495
  <h3>圖表創建與比較</h3>
1496
- <ul>
1497
- <li>此頁面提供兩個獨立的圖表設置和預覽區域(圖表一、圖表二)。</li>
1498
- <li><strong>智能推薦:</strong>點擊 "智能推薦 (圖表一)" 按鈕,系統會根據數據結構嘗試為圖表一推薦合適的設置。</li>
1499
- <li><strong>圖表類型:</strong>選擇您想創建的圖表樣式。</li>
1500
- <li><strong>聚合函數:</strong>決定如何匯總 Y 軸數據。選擇 "計數" 時,系統會計算 X 軸(和分組列)組合的出現次數,此時無需選擇 Y 軸列。</li>
1501
- <li><strong>數據映射:</strong>
1502
- <ul>
1503
- <li><strong>X軸/類別:</strong>圖表的主要分類軸。</li>
1504
- <li><strong>Y軸/數值:</strong>圖表的數值軸。若聚合函數為 "計數",此項可忽略。</li>
1505
- <li><strong>分組列:</strong>用於創建堆疊、分組或多系列圖表。</li>
1506
- <li><strong>大小列:</strong>主要用於氣泡圖,控制點的大小。</li>
1507
- </ul>
1508
- </li>
1509
- <li><strong>顯示選項:</strong>調整圖表的外觀,如寬度、高度、顏色方案、是否顯示網格和圖例。</li>
1510
- <li><strong>圖案與自定義顏色:</strong>
1511
- <ul>
1512
- <li>為圖表系列添加不同的填充圖案(適用於部分圖表類型,如條形圖)。</li>
1513
- <li>通過 "類別名:顏色代碼" 的格式為特定類別指定顏色 (例如 <code>正面:#2ca02c, 負面:#d62728</code>)。</li>
1514
- </ul>
1515
- </li>
1516
- <li>點擊 "更新圖表" 按鈕生成或刷新對應的圖表預覽。</li>
1517
- <li>使用 "導出圖表" 功能將生成的圖表保存為圖片文件。</li>
1518
- </ul>
1519
-
1520
  <h3>提示</h3>
1521
- <ul>
1522
- <li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li>
1523
- <li>確保數值列確實包含數字,日期列包含有效的日期格式。</li>
1524
- <li>部分圖表類型對數據結構有特定要求(例如,熱力圖、甘特圖)。</li>
1525
- </ul>
1526
  """)
1527
 
1528
  # =========================================
@@ -1531,289 +1945,91 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
1531
 
1532
  # --- 數據載入與更新 ---
1533
  def load_data_and_update_ui(df, status_msg):
1534
- """輔助函數:更新數據狀態、預覽和所有下拉列表"""
1535
- # 更新數據預覽
1536
  preview_df = df if df is not None else pd.DataFrame()
1537
- # 更新所有列選擇下拉列表
1538
- col_updates = update_columns(df) # 返回 4 個 Dropdown 更新對象
1539
- # 檢查 col_updates 是否為 None 或空,避免解包錯誤
1540
  if col_updates is None or len(col_updates) != 4:
1541
- # 如果更新失敗,返回原始狀態或空更新
1542
  print("警告: update_columns 未返回預期的 4 個組件更新。")
1543
- # 返回空更新避免錯誤,但下拉列表可能不會更新
1544
  return [df, status_msg, preview_df] + [gr.update()] * 8
 
1545
 
1546
- return [df, status_msg, preview_df] + list(col_updates) * 2 # 更新兩組下拉列表
1547
-
1548
- upload_button.click(
1549
- process_upload,
1550
- inputs=[file_upload],
1551
- outputs=[data_state, upload_status] # 先更新狀態和消息
1552
- ).then(
1553
- load_data_and_update_ui,
1554
- inputs=[data_state, upload_status],
1555
- outputs=[
1556
- data_state, upload_status, data_preview,
1557
- x_column_1, y_column_1, group_column_1, size_column_1, # 圖表一的下拉列表
1558
- x_column_2, y_column_2, group_column_2, size_column_2 # 圖表二的下拉列表
1559
- ]
1560
  )
1561
-
1562
- parse_button.click(
1563
- parse_data,
1564
- inputs=[csv_input],
1565
- outputs=[data_state, parse_status] # 先更新狀態和消息
1566
- ).then(
1567
- load_data_and_update_ui,
1568
- inputs=[data_state, parse_status],
1569
- outputs=[
1570
- data_state, parse_status, data_preview,
1571
- x_column_1, y_column_1, group_column_1, size_column_1, # 圖表一的下拉列表
1572
- x_column_2, y_column_2, group_column_2, size_column_2 # 圖表二的下拉列表
1573
- ]
1574
  )
1575
 
1576
  # --- 數據導出 ---
1577
- export_button.click(
1578
- export_data,
1579
- inputs=[data_state, export_format],
1580
- outputs=[export_result, export_status]
1581
- )
1582
 
1583
- # --- 圖表一:處理顏色和圖案狀態 ---
1584
- color_customization_1.change(
1585
- parse_custom_colors,
1586
- inputs=[color_customization_1],
1587
- outputs=[custom_colors_state_1]
1588
- )
1589
- # 將圖案下拉列表的變化連接到 update_patterns 函數
1590
  patterns_inputs_1 = [pattern1_1, pattern2_1, pattern3_1]
1591
- for pattern_dd in patterns_inputs_1:
1592
- pattern_dd.change(
1593
- update_patterns,
1594
- inputs=patterns_inputs_1,
1595
- outputs=[patterns_state_1]
1596
- )
1597
 
1598
  # --- 圖表一:更新圖表 ---
1599
- chart_inputs_1 = [
1600
- data_state, chart_type_1, x_column_1, y_column_1,
1601
- group_column_1, size_column_1, color_scheme_1, patterns_state_1,
1602
- chart_title_1, chart_width_1, chart_height_1, show_grid_1, show_legend_1,
1603
- agg_function_1, custom_colors_state_1
1604
- ]
1605
- update_button_1.click(
1606
- create_plot,
1607
- inputs=chart_inputs_1,
1608
- outputs=[chart_output_1]
1609
- )
1610
- # 當任何相關設置改變時,自動更新圖表一
1611
- # 將 .change 事件綁定移到循環外,以避免在循環內部重複定義函數
1612
- def auto_update_chart_1(*inputs):
1613
- # inputs 是一個包含所有 chart_inputs_1 值的元組
1614
- # 需要將這些值解包傳遞給 create_plot
1615
- # 注意:Gradio 的 .change 會將所有 input 組件的當前值按順序傳遞
1616
- # 我們需要確保這個順序與 create_plot 的參數順序匹配
1617
- # 或者,更安全的方式是從 inputs 列表創建一個字典
1618
- # 但這裡直接按順序傳遞應該可行,因為 inputs 列表順序固定
1619
- return create_plot(*inputs)
1620
-
1621
  for input_component in chart_inputs_1:
1622
- # 避免狀態變量觸發無限循環,只監聽 UI 組件的變化
1623
- if not isinstance(input_component, gr.State): # 排除 State 組件
1624
- input_component.change(
1625
- auto_update_chart_1, # 使用輔助函數
1626
- inputs=chart_inputs_1, # 傳遞所有需要的輸入
1627
- outputs=[chart_output_1]
1628
- )
1629
-
1630
 
1631
  # --- 圖表一:導出圖表 ---
1632
- download_button_1.click(
1633
- download_figure,
1634
- inputs=[chart_output_1, export_img_format_1],
1635
- outputs=[export_chart_1, export_chart_status_1]
1636
- )
1637
 
1638
  # --- 圖表一:智能推薦 ---
1639
  def apply_recommendation(rec_dict):
1640
- """將推薦字典應用到圖表一的 UI 組件"""
1641
- if not isinstance(rec_dict, dict):
1642
- # 如果輸入不是字典,返回空更新列表
1643
- print("警告:apply_recommendation 收到非字典輸入。")
1644
- return [gr.update()] * 5
1645
-
1646
- # 安全地獲取推薦值,提供默認值
1647
- chart_type_val = rec_dict.get("chart_type")
1648
- x_col_val = rec_dict.get("x_column")
1649
- agg_func_val = rec_dict.get("agg_function")
1650
- y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column")
1651
- group_col_val = rec_dict.get("group_column", "無") # 默認為 "無"
1652
-
1653
- # 返回更新列表
1654
- return [
1655
- gr.Dropdown(value=chart_type_val),
1656
- gr.Dropdown(value=x_col_val),
1657
- gr.Dropdown(value=y_col_val),
1658
- gr.Dropdown(value=group_col_val),
1659
- gr.Dropdown(value=agg_func_val)
1660
- ]
1661
-
1662
-
1663
- # 創建一個狀態來存儲推薦結果,以便在 then() 中使用
1664
- recommendation_state = gr.State({})
1665
-
1666
- recommend_button_1.click(
1667
- recommend_chart_settings,
1668
- inputs=[data_state],
1669
- outputs=[recommendation_state] # 將結果存儲在狀態中
1670
- ).then(
1671
- apply_recommendation,
1672
- inputs=[recommendation_state], # 從狀態讀取推薦結果
1673
- outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
1674
- ).then( # 應用推薦後立即觸發一次圖表更新
1675
- create_plot, # 直接調用 create_plot
1676
- inputs=chart_inputs_1, # 再次收集所有當前輸入
1677
- outputs=[chart_output_1]
1678
- )
1679
 
 
 
 
1680
 
1681
- # --- 圖表二:處理顏色和圖案狀態 ---
1682
- color_customization_2.change(
1683
- parse_custom_colors,
1684
- inputs=[color_customization_2],
1685
- outputs=[custom_colors_state_2]
1686
- )
1687
  patterns_inputs_2 = [pattern1_2, pattern2_2, pattern3_2]
1688
- for pattern_dd in patterns_inputs_2:
1689
- pattern_dd.change(
1690
- update_patterns,
1691
- inputs=patterns_inputs_2,
1692
- outputs=[patterns_state_2]
1693
- )
1694
 
1695
  # --- 圖表二:更新圖表 ---
1696
- chart_inputs_2 = [
1697
- data_state, chart_type_2, x_column_2, y_column_2,
1698
- group_column_2, size_column_2, color_scheme_2, patterns_state_2,
1699
- chart_title_2, chart_width_2, chart_height_2, show_grid_2, show_legend_2,
1700
- agg_function_2, custom_colors_state_2
1701
- ]
1702
- update_button_2.click(
1703
- create_plot,
1704
- inputs=chart_inputs_2,
1705
- outputs=[chart_output_2]
1706
- )
1707
- # 當任何相關設置改變時,自動更新圖表二
1708
- def auto_update_chart_2(*inputs):
1709
- return create_plot(*inputs)
1710
-
1711
  for input_component in chart_inputs_2:
1712
- if not isinstance(input_component, gr.State):
1713
- input_component.change(
1714
- auto_update_chart_2,
1715
- inputs=chart_inputs_2,
1716
- outputs=[chart_output_2]
1717
- )
1718
 
1719
  # --- 圖表二:導出圖表 ---
1720
- download_button_2.click(
1721
- download_figure,
1722
- inputs=[chart_output_2, export_img_format_2],
1723
- outputs=[export_chart_2, export_chart_status_2]
1724
- )
1725
 
1726
- # --- 圖表類型改變時更新 UI 元素可見性 (針對兩個圖表) ---
1727
  def update_element_visibility(chart_type):
1728
- """根據圖表類型更新 Y 軸、分組列、大小列的標籤和可見性"""
1729
- try: # 添加 try-except 塊以捕獲潛在錯誤
1730
- # 圓餅圖、環形圖、漏斗圖、樹狀圖:主要關心類別 (X) 和數值 (Y)
1731
- is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]
1732
- # 直方圖:主要關心 X 軸分佈,Y 軸是計數
1733
- is_histogram = chart_type == "直方圖"
1734
- # 箱型圖、小提琴圖:Y 軸是數值,X 軸是可選的分組
1735
- is_box_violin = chart_type in ["箱型圖", "小提琴圖"]
1736
- # 甘特圖:Y 軸是開始時間,分組列是結束時間
1737
- is_gantt = chart_type == "甘特圖"
1738
- # 熱力圖:需要 X, Y 和分組列
1739
- is_heatmap = chart_type == "熱力圖"
1740
- # 雷達圖:Theta (X), R (Y), Color (Group)
1741
- is_radar = chart_type == "雷達圖"
1742
-
1743
- # Y 軸的標籤和需求
1744
- y_label = "Y軸 / 數值"
1745
- y_needed = True
1746
- if is_histogram:
1747
- y_label = "Y軸 (自動計數)"
1748
- y_needed = False # 不需要用戶選擇 Y
1749
- elif is_pie_like:
1750
- y_label = "數值列 (用於大小/值)"
1751
- elif is_box_violin:
1752
- y_label = "數值列"
1753
- elif is_gantt:
1754
- y_label = "開始時間列"
1755
- elif is_radar:
1756
- y_label = "徑向值 (R)"
1757
-
1758
-
1759
- # 分組列的標籤和需求
1760
- group_label = "分組列"
1761
- group_needed = chart_type in [
1762
- "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", # 可選
1763
- "折線圖", "多重折線圖", "階梯折線圖", # 可選
1764
- "區域圖", "堆疊區域圖", "百分比堆疊區域圖", # 可選
1765
- "散點圖", "氣泡圖", # 可選
1766
- "箱型圖", "小提琴圖", # 可選,用於 X 軸
1767
- "熱力圖", # 必需,用於 Index 或 Columns
1768
- "雷達圖", # 必需,用於區分線條
1769
- "極座標圖" # 可選
1770
- ]
1771
- if is_gantt:
1772
- group_label = "結束時間列"
1773
- group_needed = True
1774
- elif is_heatmap:
1775
- group_label = "行/列 分組"
1776
- group_needed = True
1777
-
1778
-
1779
- # 大小列的需求
1780
- size_label = "大小列"
1781
- size_needed = chart_type in ["氣泡圖", "散點圖"] # 甘特圖顏色也可以用 Size 列
1782
- if is_gantt:
1783
- size_label = "顏色列 (可選)"
1784
- size_needed = True # 允許選擇顏色列
1785
-
1786
-
1787
- # 返回更新對象
1788
- return (
1789
- gr.update(label=y_label, visible=y_needed),
1790
- gr.update(label=group_label, visible=group_needed),
1791
- gr.update(label=size_label, visible=size_needed)
1792
- )
1793
- except Exception as e:
1794
- print(f"Error in update_element_visibility: {e}")
1795
- # 返回默認更新以避免應用程序崩潰
1796
- return (gr.update(), gr.update(), gr.update())
1797
-
1798
-
1799
- # 將 chart_type 的變化連接到更新函數,並應用到兩個圖表的對應組件
1800
- chart_type_1.change(
1801
- update_element_visibility,
1802
- inputs=[chart_type_1],
1803
- outputs=[y_column_1, group_column_1, size_column_1]
1804
- )
1805
- chart_type_2.change(
1806
- update_element_visibility,
1807
- inputs=[chart_type_2],
1808
- outputs=[y_column_2, group_column_2, size_column_2]
1809
- )
1810
 
1811
  # =========================================
1812
  # == 應用程式啟動 (Launch Application) ==
1813
  # =========================================
1814
  if __name__ == "__main__":
1815
- # 在 Hugging Face Spaces 上,通常不需要 share=True
1816
- # debug=True 可以在開發時提供更詳細的錯誤信息
1817
- # 增加 server_port 以便在本地運行時指定端口,避免衝突
1818
- # demo.launch(debug=True, server_port=7860)
1819
- demo.launch(debug=True) # 保持原樣,Gradio 會自動選擇端口
 
1273
  """
1274
  Gradio 應用程式:進階數據可視化工具
1275
  作者:Gemini
1276
+ 版本:2.0 (完整修復版 - 簡化佈局,移除下拉選單 CSS)
1277
+ 描述:包含所有功能的完整程式碼,旨在解決下拉選單問題。
1278
+ """
1279
+
1280
+ # =========================================
1281
+ # == 套件導入 (Import Libraries) ==
1282
+ # =========================================
1283
+ import gradio as gr
1284
+ import pandas as pd
1285
+ import numpy as np
1286
+ import plotly.express as px
1287
+ import plotly.graph_objects as go
1288
+ import io
1289
+ import base64
1290
+ from PIL import Image
1291
+ # import matplotlib.pyplot as plt # Matplotlib/Seaborn 在此版本中未使用,暫時註解
1292
+ # import seaborn as sns # Matplotlib/Seaborn 在此版本中未使用,暫時註解
1293
+ from plotly.subplots import make_subplots
1294
+ import re
1295
+ import json
1296
+ import colorsys
1297
+ import traceback # 用於更詳細的錯誤追蹤
1298
+
1299
+ # =========================================
1300
+ # == 常數定義 (Constants) ==
1301
+ # =========================================
1302
+
1303
+ # 圖表類型選項 (Chart Type Options)
1304
+ CHART_TYPES = [
1305
+ "長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖",
1306
+ "折線圖", "多重折線圖", "階梯折線圖",
1307
+ "區域圖", "堆疊區域圖", "百分比堆疊區域圖",
1308
+ "圓餅圖", "環形圖",
1309
+ "散點圖", "氣泡圖",
1310
+ "直方圖", "箱型圖", "小提琴圖",
1311
+ "熱力圖", "樹狀圖",
1312
+ "雷達圖", "漏斗圖", "極座標圖", "甘特圖"
1313
+ ]
1314
+
1315
+ # 顏色方案選項 (Color Scheme Options)
1316
+ COLOR_SCHEMES = {
1317
+ "預設 (Plotly)": px.colors.qualitative.Plotly,
1318
+ "分類 - D3": px.colors.qualitative.D3, "分類 - G10": px.colors.qualitative.G10,
1319
+ "分類 - T10": px.colors.qualitative.T10, "分類 - Alphabet": px.colors.qualitative.Alphabet,
1320
+ "分類 - Dark24": px.colors.qualitative.Dark24, "分類 - Light24": px.colors.qualitative.Light24,
1321
+ "分類 - Set1": px.colors.qualitative.Set1, "分類 - Set2": px.colors.qualitative.Set2,
1322
+ "分類 - Set3": px.colors.qualitative.Set3, "分類 - Pastel": px.colors.qualitative.Pastel,
1323
+ "分類 - Pastel1": px.colors.qualitative.Pastel1, "分類 - Pastel2": px.colors.qualitative.Pastel2,
1324
+ "分類 - Antique": px.colors.qualitative.Antique, "分類 - Bold": px.colors.qualitative.Bold,
1325
+ "分類 - Prism": px.colors.qualitative.Prism, "分類 - Safe": px.colors.qualitative.Safe,
1326
+ "分類 - Vivid": px.colors.qualitative.Vivid,
1327
+ "連續 - Viridis": px.colors.sequential.Viridis, "連續 - Plasma": px.colors.sequential.Plasma,
1328
+ "連續 - Inferno": px.colors.sequential.Inferno, "連續 - Magma": px.colors.sequential.Magma,
1329
+ "連續 - Cividis": px.colors.sequential.Cividis, "連續 - Blues": px.colors.sequential.Blues,
1330
+ "連續 - Reds": px.colors.sequential.Reds, "連續 - Greens": px.colors.sequential.Greens,
1331
+ "連續 - Purples": px.colors.sequential.Purples, "連續 - Oranges": px.colors.sequential.Oranges,
1332
+ "連續 - Greys": px.colors.sequential.Greys, "連續 - Rainbow": px.colors.sequential.Rainbow,
1333
+ "連續 - Turbo": px.colors.sequential.Turbo, "連續 - Electric": px.colors.sequential.Electric,
1334
+ "連續 - Hot": px.colors.sequential.Hot, "連續 - Teal": px.colors.sequential.Teal,
1335
+ "發散 - Spectral": px.colors.diverging.Spectral, "發散 - RdBu": px.colors.diverging.RdBu,
1336
+ "發散 - PRGn": px.colors.diverging.PRGn, "發散 - PiYG": px.colors.diverging.PiYG,
1337
+ "發散 - BrBG": px.colors.diverging.BrBG, "發散 - Geyser": px.colors.diverging.Geyser,
1338
+ "循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
1339
+ }
1340
+
1341
+ # 圖案填充選項 (Pattern Fill Options)
1342
+ PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
1343
+
1344
+ # 聚合函數選項 (Aggregation Function Options)
1345
+ AGGREGATION_FUNCTIONS = [
1346
+ "計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"
1347
+ ]
1348
+
1349
+ # =========================================
1350
+ # == 輔助函數 (Helper Functions) ==
1351
+ # =========================================
1352
+
1353
+ # --- 顏色處理相關 ---
1354
+ COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
1355
+ COLOR_CARD_TEMPLATE = """<div title="{color_name} ({color_hex})" style="width: 20px; height: 20px; background-color: {color_hex}; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; transition: transform 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.1);" onclick="copyToClipboard('{color_hex}')" onmouseover="this.style.transform='scale(1.15)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.2)';" onmouseout="this.style.transform='scale(1)'; this.style.boxShadow='0 1px 2px rgba(0,0,0,0.1)';"></div>"""
1356
+ COPY_SCRIPT = """
1357
+ <script>
1358
+ function copyToClipboard(text) {
1359
+ navigator.clipboard.writeText(text).then(() => {
1360
+ let notificationContainer = document.getElementById('clipboard-notification-container');
1361
+ if (!notificationContainer) {
1362
+ notificationContainer = document.createElement('div');
1363
+ notificationContainer.id = 'clipboard-notification-container';
1364
+ notificationContainer.style.position = 'fixed'; notificationContainer.style.bottom = '20px'; notificationContainer.style.right = '20px'; notificationContainer.style.zIndex = '10000'; notificationContainer.style.display = 'flex'; notificationContainer.style.flexDirection = 'column'; notificationContainer.style.alignItems = 'flex-end';
1365
+ document.body.appendChild(notificationContainer);
1366
+ }
1367
+ const notification = document.createElement('div');
1368
+ notification.textContent = '已複製: ' + text;
1369
+ notification.style.background = 'rgba(0, 0, 0, 0.7)'; notification.style.color = 'white'; notification.style.padding = '8px 15px'; notification.style.borderRadius = '4px'; notification.style.marginTop = '5px'; notification.style.fontSize = '14px'; notification.style.opacity = '1'; notification.style.transition = 'opacity 0.5s ease-out'; notification.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
1370
+ notificationContainer.appendChild(notification);
1371
+ setTimeout(() => {
1372
+ notification.style.opacity = '0';
1373
+ setTimeout(() => { notification.remove(); }, 500);
1374
+ }, 1500);
1375
+ }).catch(err => { console.error('無法複製顏色代碼: ', err); });
1376
+ }
1377
+ </script>
1378
+ """
1379
+ COMMON_COLORS = {
1380
+ "紅色": "#FF0000", "亮紅": "#FF5733", "深紅": "#C70039", "橙色": "#FFA500", "亮橙": "#FFC300", "深橙": "#D35400",
1381
+ "黃色": "#FFFF00", "亮黃": "#F1C40F", "金色": "#FFD700", "綠色": "#008000", "亮綠": "#2ECC71", "深綠": "#1E8449",
1382
+ "橄欖綠": "#808000", "藍色": "#0000FF", "亮藍": "#3498DB", "深藍": "#2874A6", "天藍": "#87CEEB", "紫色": "#800080",
1383
+ "亮紫": "#9B59B6", "深紫": "#6C3483", "薰衣草紫": "#E6E6FA", "粉紅色": "#FFC0CB", "亮粉": "#FF69B4", "深粉": "#C71585",
1384
+ "棕色": "#A52A2A", "亮棕": "#E59866", "深棕": "#6E2C00", "青色": "#00FFFF", "藍綠色": "#008080", "綠松石色": "#40E0D0",
1385
+ "洋紅": "#FF00FF", "紫紅色": "#DC143C", "灰色": "#808080", "淺灰": "#D3D3D3", "深灰": "#696969", "石板灰": "#708090",
1386
+ "黑色": "#000000", "白色": "#FFFFFF", "米色": "#F5F5DC",
1387
+ }
1388
+ def generate_gradient_colors(start_color, end_color, steps=10):
1389
+ def hex_to_rgb(hex_color):
1390
+ hex_color = hex_color.lstrip('#')
1391
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
1392
+ def rgb_to_hex(rgb):
1393
+ r, g, b = [max(0, min(255, int(c))) for c in rgb]
1394
+ return '#{:02x}{:02x}{:02x}'.format(r, g, b)
1395
+ try:
1396
+ start_rgb, end_rgb = hex_to_rgb(start_color), hex_to_rgb(end_color)
1397
+ if steps <= 1: return [start_color] if steps == 1 else []
1398
+ r_step, g_step, b_step = [(end_rgb[i] - start_rgb[i]) / (steps - 1) for i in range(3)]
1399
+ return [rgb_to_hex((start_rgb[0] + r_step * i, start_rgb[1] + g_step * i, start_rgb[2] + b_step * i)) for i in range(steps)]
1400
+ except Exception as e:
1401
+ print(f"生成漸變色時出錯: {e}"); return [start_color, end_color]
1402
+ GRADIENTS = {
1403
+ "紅→黃": generate_gradient_colors("#FF0000", "#FFFF00"), "藍→綠": generate_gradient_colors("#0000FF", "#00FF00"),
1404
+ "紫→粉": generate_gradient_colors("#800080", "#FFC0CB"), "紅→藍": generate_gradient_colors("#FF0000", "#0000FF"),
1405
+ "黑→白": generate_gradient_colors("#000000", "#FFFFFF"), "藍→紅 (發散)": generate_gradient_colors("#0000FF", "#FF0000"),
1406
+ "綠→紫 (發散)": generate_gradient_colors("#00FF00", "#800080"),
1407
+ "彩虹 (簡易)": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"]
1408
+ }
1409
+ def generate_color_cards():
1410
+ common_cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code) for name, hex_code in COMMON_COLORS.items()])
1411
+ gradient_cards_html = ""
1412
+ for name, colors in GRADIENTS.items():
1413
+ cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=f"{name} {i+1}/{len(colors)}", color_hex=color) for i, color in enumerate(colors)])
1414
+ gradient_cards_html += f"""<div style="font-weight: bold; margin-top: 10px; font-size: 14px; color: #555;">{name}</div>{COLOR_CARD_STYLE.format(color_cards=cards)}"""
1415
+ return f"""<div style="font-weight: bold; margin-top: 10px; font-size: 14px; color: #555;">常用單色</div>{COLOR_CARD_STYLE.format(color_cards=common_cards)}{gradient_cards_html}{COPY_SCRIPT}"""
1416
+
1417
+ # --- 數據處理相關 ---
1418
+ def agg_function_map(func_name):
1419
+ mapping = {"計數": "count", "求和": "sum", "平均值": "mean", "中位數": "median", "最大值": "max", "最小值": "min", "標準差": "std", "變異數": "var", "第一筆": "first", "最後一筆": "last"}
1420
+ return mapping.get(func_name, "count")
1421
+ def parse_custom_colors(color_text):
1422
+ custom_colors = {}
1423
+ if color_text and isinstance(color_text, str) and color_text.strip():
1424
+ try:
1425
+ pairs = [p.strip() for p in color_text.split(',') if p.strip()]
1426
+ for pair in pairs:
1427
+ if ':' in pair:
1428
+ key, value = pair.split(':', 1); key, value = key.strip(), value.strip()
1429
+ if re.match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", value): custom_colors[key] = value
1430
+ else: print(f"警告:忽略無效的顏色代碼 '{value}' for key '{key}'")
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) ==
1438
+ # =========================================
1439
+ def process_upload(file):
1440
+ if file is None: return None, "❌ 未上傳任何文件。"
1441
+ try:
1442
+ file_path = file.name; file_type = file_path.split('.')[-1].lower()
1443
+ if file_type == 'csv':
1444
+ try: df = pd.read_csv(file_path, encoding='utf-8')
1445
+ except UnicodeDecodeError:
1446
+ try: df = pd.read_csv(file_path, encoding='big5')
1447
+ except Exception as e: return None, f"❌ 無法使用 UTF-8 或 Big5 解碼 CSV 文件: {e}"
1448
+ except Exception as e: return None, f"❌ 讀取 CSV 文件時出錯: {e}"
1449
+ elif file_type in ['xls', 'xlsx']:
1450
+ try: df = pd.read_excel(file_path)
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
+
1457
+ def parse_data(text_data):
1458
+ if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
1459
+ try:
1460
+ data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
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
+ 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
+
1507
+ group_col, size_col = (None if col == "無" else col for col in [group_column, size_column])
1508
+ df_processed = df.copy()
1509
+
1510
+ # --- 2. 數據類型轉換與準備 ---
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}")
1519
+
1520
+ # --- 3. 數據聚合 (如果需要) ---
1521
+ needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
1522
+ agg_df = None
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
+
1541
+ # --- 4. 獲取顏色方案 ---
1542
+ colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)
1543
+
1544
+ # --- 5. 創建圖表 (核心邏輯) ---
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)
1553
+ elif chart_type == "堆疊長條圖":
1554
+ fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, barmode='stack', **fig_params)
1555
+ elif chart_type == "百分比堆疊長條圖":
1556
+ fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, barmode='relative', text_auto='.1%', **fig_params)
1557
+ fig.update_layout(yaxis_title="百分比 (%)")
1558
+ elif chart_type == "群組長條圖":
1559
+ fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, barmode='group', **fig_params)
1560
+ elif chart_type == "水平長條圖":
1561
+ fig = px.bar(agg_df, y=x_column, x=y_col_agg, color=group_col, orientation='h', **fig_params)
1562
+ elif chart_type == "折線圖":
1563
+ fig = px.line(agg_df, x=x_column, y=y_col_agg, color=group_col, markers=True, **fig_params)
1564
+ elif chart_type == "多重折線圖":
1565
+ fig = px.line(agg_df, x=x_column, y=y_col_agg, color=group_col, markers=True, **fig_params)
1566
+ elif chart_type == "階梯折線圖":
1567
+ fig = px.line(agg_df, x=x_column, y=y_col_agg, color=group_col, line_shape='hv', **fig_params)
1568
+ elif chart_type == "區域圖":
1569
+ fig = px.area(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
1570
+ elif chart_type == "堆疊區域圖":
1571
+ fig = px.area(agg_df, x=x_column, y=y_col_agg, color=group_col, groupnorm=None, **fig_params)
1572
+ elif chart_type == "百分比堆疊區域圖":
1573
+ fig = px.area(agg_df, x=x_column, y=y_col_agg, color=group_col, groupnorm='percent', **fig_params)
1574
+ fig.update_layout(yaxis_title="百分比 (%)")
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()
1612
+ for i, category in enumerate(categories):
1613
+ 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])
1614
+ color = custom_colors_dict.get(str(category), colors[i % len(colors)])
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:
1631
+ print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
1632
+ fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
1633
+ # --- (繪圖邏輯結束) ---
1634
+
1635
+ # --- 6. 應用圖案 (如果支持) ---
1636
+ if patterns:
1637
+ try:
1638
+ num_traces = len(fig.data)
1639
+ if chart_type in ["長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "圓餅圖", "環形圖"]:
1640
+ for i, trace in enumerate(fig.data):
1641
+ pattern_index = i % len(patterns)
1642
+ if patterns[pattern_index] != "無": trace.marker.pattern.shape = patterns[pattern_index]; trace.marker.pattern.solidity = 0.4; trace.marker.pattern.fillmode = "replace"
1643
+ elif chart_type in ["散點圖", "氣泡圖"]:
1644
+ symbol_map = {"/": "diamond", "\\": "square", "x": "x", "-": "line-ew", "|": "line-ns", "+": "cross", ".": "circle-dot"}
1645
+ for i, trace in enumerate(fig.data):
1646
+ pattern_index = i % len(patterns); symbol = symbol_map.get(patterns[pattern_index])
1647
+ if symbol: trace.marker.symbol = symbol
1648
+ elif chart_type in ["折線圖", "多重折線圖", "階梯折線圖"]:
1649
+ dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash", "|": "solid", "+": "solid", ".": "solid"}
1650
+ for i, trace in enumerate(fig.data):
1651
+ pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
1652
+ if dash: trace.line.dash = dash
1653
+ elif chart_type in ["區域圖", "堆疊區域圖", "百分比堆疊區域圖"]:
1654
+ print("提示:區域圖的圖案填充支持有限,將嘗試應用線型。")
1655
+ dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
1656
+ for i, trace in enumerate(fig.data):
1657
+ pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
1658
+ if dash: trace.line.dash = dash; trace.fill = 'tonexty' if 'stackgroup' in trace else 'tozeroy'
1659
+ except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")
1660
+
1661
+ # --- 7. 更新佈局 ---
1662
+ fig.update_layout(
1663
+ showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
1664
+ template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
1665
+ font=dict(family="Inter, sans-serif", size=12),
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()
1675
+ fig = go.Figure(); fig.add_annotation(text=f"⚠️ 創建圖表時出錯:<br>{ve}", align='left', showarrow=False, font=dict(size=14, color="red")); fig.update_layout(xaxis_visible=False, yaxis_visible=False)
1676
+ except Exception as e:
1677
+ error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message)
1678
+ fig = go.Figure(); user_error_msg = f"⚠️ 創建圖表時發生內部錯誤。<br>請檢查數據和設置。<br>詳細錯誤: {str(e)[:100]}..."; fig.add_annotation(text=user_error_msg, align='left', showarrow=False, font=dict(size=14, color="red")); fig.update_layout(xaxis_visible=False, yaxis_visible=False)
1679
+ return fig
1680
+
1681
+ # =========================================
1682
+ # == 導出與下載函數 (Export & Download Functions) ==
1683
+ # =========================================
1684
+ def export_data(df, format_type):
1685
+ if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
1686
+ try:
1687
+ if format_type == "CSV": filename = "exported_data.csv"; df.to_csv(filename, index=False, encoding='utf-8-sig')
1688
+ elif format_type == "Excel": filename = "exported_data.xlsx"; df.to_excel(filename, index=False)
1689
+ elif format_type == "JSON": filename = "exported_data.json"; df.to_json(filename, orient="records", indent=4, force_ascii=False)
1690
+ else: return None, f"❌ 不支持的導出格式: {format_type}"
1691
+ return filename, f"✅ 數據已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
1692
+ except Exception as e: print(f"導出數據時出錯: {e}"); traceback.print_exc(); return None, f"❌ 導出數據時出錯: {e}"
1693
+
1694
+ 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) ==
1707
+ # =========================================
1708
+ def recommend_chart_settings(df):
1709
+ recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
1710
+ if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
1711
+ columns = df.columns.tolist(); num_cols = df.select_dtypes(include=np.number).columns.tolist(); cat_cols = df.select_dtypes(include=['object', 'category', 'string']).columns.tolist()
1712
+ date_cols = [col for col in columns if pd.api.types.is_datetime64_any_dtype(df[col]) or ('日期' in col or '時間' in col)]
1713
+ try:
1714
+ if date_cols and num_cols: recommendation.update({"chart_type": "折線圖", "x_column": date_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到時間列 '{date_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用折線圖顯示趨勢。"})
1715
+ elif len(num_cols) >= 2: recommendation.update({"chart_type": "散點圖", "x_column": num_cols[0], "y_column": num_cols[1], "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}' 和 '{num_cols[1]}',推薦使用散點圖分析相關性。"})
1716
+ elif cat_cols and num_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到類別列 '{cat_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用長條圖比較各類別的數值。"})
1717
+ elif len(cat_cols) >= 2: recommendation.update({"chart_type": "堆疊長條圖", "x_column": cat_cols[0], "y_column": None, "group_column": cat_cols[1], "agg_function": "計數", "message": f"檢測到多個類別列 ('{cat_cols[0]}', '{cat_cols[1]}', ...),推薦使用堆疊長條圖顯示計數分佈。"})
1718
+ elif cat_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": None, "agg_function": "計數", "message": f"檢測到類別列 '{cat_cols[0]}',推薦使用長條圖顯示其頻數分佈。"})
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
+ # =========================================
1730
+ # == CSS 樣式 (CSS Styling) ==
1731
+ # =========================================
1732
+ CUSTOM_CSS = """
1733
+ /* --- 全局和容器 --- */
1734
+ .gradio-container { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
1735
+ /* --- 應用程式標頭 --- */
1736
+ .app-header { text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 25px 20px; border-radius: 12px; color: white; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); }
1737
+ .app-title { font-size: 2.2em; font-weight: 700; margin: 0; letter-spacing: 1px; text-shadow: 1px 1px 3px rgba(0,0,0,0.2); }
1738
+ .app-subtitle { font-size: 1.1em; color: #e0e0e0; margin-top: 8px; font-weight: 300; }
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; }
1758
+ .gradio-dataframe tr:nth-child(even) { background-color: #f8f9fa; }
1759
+ .color-customization-input textarea { font-family: 'Courier New', Courier, monospace; font-size: 0.9em; }
1760
+ .gradio-tabs .tab-nav button { padding: 10px 20px !important; font-weight: 500 !important; border-radius: 8px 8px 0 0 !important; transition: background-color 0.2s ease, color 0.2s ease !important; }
1761
+ .gradio-tabs .tab-nav button.selected { background-color: #667eea !important; color: white !important; border-bottom: none !important; }
1762
+ .gradio-slider label { margin-bottom: 5px !important; }
1763
+ .gradio-slider input[type="range"] { cursor: pointer !important; }
1764
+ .gradio-checkboxgroup label, .gradio-radio label { padding: 8px 0 !important; }
1765
+ .gradio-textbox textarea, .gradio-textbox input { border-radius: 6px !important; border: 1px solid #ced4da !important; padding: 10px !important; }
1766
+ .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; }
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
  # =========================================
 
1782
  </div>
1783
  """)
1784
 
1785
+ # --- 狀態變量 ---
 
1786
  data_state = gr.State(None)
 
1787
  custom_colors_state_1 = gr.State({})
1788
  patterns_state_1 = gr.State([])
1789
  custom_colors_state_2 = gr.State({})
1790
  patterns_state_2 = gr.State([])
1791
+ recommendation_state = gr.State({}) # 用於存儲推薦結果
1792
 
1793
  # --- 主頁籤佈局 ---
1794
  with gr.Tabs() as tabs:
 
1796
  # --- 數據輸入頁籤 ---
1797
  with gr.TabItem("📁 數據輸入與管理", id=0):
1798
  with gr.Row():
1799
+ with gr.Column(scale=2): # 左側:數據輸入
 
1800
  gr.HTML('<div class="section-title">1. 上傳或輸入數據</div>')
1801
  with gr.Group(elem_classes=["card"]):
1802
  gr.Markdown("您可以上傳本地的 CSV 或 Excel 文件,或��接在下方的文本框中貼上數據。")
1803
+ file_upload = gr.File(label="上傳 CSV / Excel 文件", type="filepath")
1804
  upload_button = gr.Button("⬆️ 載入文件數據", elem_classes=["primary-button"])
1805
  upload_status = gr.Textbox(label="載入狀態", lines=1, interactive=False)
 
1806
  with gr.Group(elem_classes=["card"]):
1807
+ csv_input = gr.Textbox(label="或者,在此貼上數據 (逗號、Tab 或空格分隔)", placeholder="例如:\n類別,數值\nA,10\nB,20\nC,15...", lines=8, elem_classes=["data-input-textbox"])
 
 
 
 
 
1808
  parse_button = gr.Button("📝 解析貼上數據", elem_classes=["primary-button"])
1809
  parse_status = gr.Textbox(label="解析狀態", lines=1, interactive=False)
1810
+ with gr.Column(scale=3): # 右側:數據預覽與導出
 
 
1811
  gr.HTML('<div class="section-title">2. 數據預覽與導出</div>')
1812
  with gr.Group(elem_classes=["card"]):
1813
  gr.Markdown("下方將顯示載入或解析後的數據預覽。")
1814
+ data_preview = gr.Dataframe(label="數據表格預覽", interactive=False) # 移除 height
 
 
1815
  with gr.Row():
1816
+ export_format = gr.Dropdown(["CSV", "Excel", "JSON"], label="選擇導出格式", value="CSV")
 
 
 
 
1817
  export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
 
1818
  export_result = gr.File(label="導出文件下載", interactive=False)
1819
  export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
1820
 
 
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():
1850
  show_grid_1 = gr.Checkbox(label="顯示網格", value=True)
1851
  show_legend_1 = gr.Checkbox(label="顯示圖例", value=True)
 
1852
  color_scheme_1 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)")
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("**圖案與自定義顏色**")
 
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():
1903
  show_grid_2 = gr.Checkbox(label="顯示網格", value=True)
1904
  show_legend_2 = gr.Checkbox(label="顯示圖例", value=True)
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("**圖案與自定義顏色**")
 
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):
1931
+ with gr.Group(elem_classes=["card"]): # 使用說明也放在卡片裡
1932
  gr.HTML("""
1933
  <div class="section-title">使用說明</div>
 
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
  # =========================================
 
1945
 
1946
  # --- 數據載入與更新 ---
1947
  def load_data_and_update_ui(df, status_msg):
 
 
1948
  preview_df = df if df is not None else pd.DataFrame()
1949
+ col_updates = update_columns(df)
 
 
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],
1957
+ 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]
 
 
 
 
 
 
 
 
 
 
 
1958
  )
1959
+ parse_button.click(parse_data, inputs=[csv_input], outputs=[data_state, parse_status]).then(
1960
+ load_data_and_update_ui, inputs=[data_state, parse_status],
1961
+ 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]
 
 
 
 
 
 
 
 
 
 
1962
  )
1963
 
1964
  # --- 數據導出 ---
1965
+ export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
 
 
 
 
1966
 
1967
+ # --- 圖表一:顏色和圖案狀態 ---
1968
+ color_customization_1.change(parse_custom_colors, inputs=[color_customization_1], outputs=[custom_colors_state_1])
 
 
 
 
 
1969
  patterns_inputs_1 = [pattern1_1, pattern2_1, pattern3_1]
1970
+ for pattern_dd in patterns_inputs_1: pattern_dd.change(update_patterns, inputs=patterns_inputs_1, outputs=[patterns_state_1])
 
 
 
 
 
1971
 
1972
  # --- 圖表一:更新圖表 ---
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])
 
 
 
 
1981
 
1982
  # --- 圖表一:智能推薦 ---
1983
  def apply_recommendation(rec_dict):
1984
+ if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
1985
+ chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
1986
+ y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", "無")
1987
+ 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)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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])
 
 
 
 
1995
  patterns_inputs_2 = [pattern1_2, pattern2_2, pattern3_2]
1996
+ for pattern_dd in patterns_inputs_2: pattern_dd.change(update_patterns, inputs=patterns_inputs_2, outputs=[patterns_state_2])
 
 
 
 
 
1997
 
1998
  # --- 圖表二:更新圖表 ---
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])
 
 
 
 
2007
 
2008
+ # --- 圖表類型改變時更新 UI 元素可見性 ---
2009
  def update_element_visibility(chart_type):
2010
+ try:
2011
+ is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
2012
+ is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
2013
+ is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
2014
+ y_label, y_needed = "Y / 數值", True
2015
+ if is_histogram: y_label, y_needed = "Y軸 (自動計數)", False
2016
+ elif is_pie_like: y_label = "數值列 (用於大小/值)"
2017
+ elif is_box_violin: y_label = "數值列"
2018
+ elif is_gantt: y_label = "開始時間列"
2019
+ elif is_radar: y_label = "徑向值 (R)"
2020
+ group_label, group_needed = "分組列", chart_type in ["堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "折線圖", "多重折線圖", "階梯折線圖", "區域圖", "堆疊區域圖", "百分比堆疊區域圖", "散點圖", "氣泡圖", "箱型圖", "小提琴圖", "熱力圖", "雷達圖", "極座標圖"]
2021
+ if is_gantt: group_label, group_needed = "結束時間列", True
2022
+ elif is_heatmap: group_label, group_needed = "行/列 分組", True
2023
+ size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
2024
+ if is_gantt: size_label, size_needed = "顏色列 (可選)", True
2025
+ 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))
2026
+ except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
2027
+
2028
+ chart_type_1.change(update_element_visibility, inputs=[chart_type_1], outputs=[y_column_1, group_column_1, size_column_1])
2029
+ chart_type_2.change(update_element_visibility, inputs=[chart_type_2], outputs=[y_column_2, group_column_2, size_column_2])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2030
 
2031
  # =========================================
2032
  # == 應用程式啟動 (Launch Application) ==
2033
  # =========================================
2034
  if __name__ == "__main__":
2035
+ demo.launch(debug=True)