Update app.py
Browse files
app.py
CHANGED
|
@@ -1273,8 +1273,500 @@ def recommend_chart_settings(df):
|
|
| 1273 |
"""
|
| 1274 |
Gradio 應用程式:進階數據可視化工具
|
| 1275 |
作者:Gemini
|
| 1276 |
-
版本:
|
| 1277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1278 |
"""
|
| 1279 |
|
| 1280 |
# =========================================
|
|
@@ -1290,14 +1782,13 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1290 |
</div>
|
| 1291 |
""")
|
| 1292 |
|
| 1293 |
-
# --- 狀態變量
|
| 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")
|
| 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 |
-
#
|
| 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():
|
| 1353 |
gr.Markdown("### 📊 圖表一設置")
|
| 1354 |
with gr.Row():
|
| 1355 |
-
#
|
| 1356 |
-
with gr.Column(scale=
|
| 1357 |
with gr.Group(elem_classes=["card"]):
|
| 1358 |
gr.Markdown("**基本設置**")
|
| 1359 |
chart_type_1 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True)
|
| 1360 |
-
|
| 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="
|
| 1368 |
-
y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="
|
| 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 |
-
|
| 1377 |
-
|
| 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 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 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():
|
| 1420 |
gr.Markdown("### 📊 圖表二設置")
|
| 1421 |
with gr.Row():
|
| 1422 |
-
#
|
| 1423 |
-
with gr.Column(scale=
|
| 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="
|
| 1434 |
-
y_column_2 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="
|
| 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 |
-
|
| 1443 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
|
| 1471 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1547 |
-
|
| 1548 |
-
|
| 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 |
-
|
| 1563 |
-
|
| 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 |
-
|
| 1601 |
-
|
| 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 |
-
|
| 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 |
-
""
|
| 1641 |
-
|
| 1642 |
-
|
| 1643 |
-
|
| 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 |
-
|
| 1698 |
-
|
| 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 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
| 1732 |
-
|
| 1733 |
-
is_histogram
|
| 1734 |
-
|
| 1735 |
-
is_box_violin =
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
|
| 1746 |
-
|
| 1747 |
-
|
| 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 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|