Update app.py
Browse files
app.py
CHANGED
|
@@ -1273,8 +1273,8 @@ def recommend_chart_settings(df):
|
|
| 1273 |
"""
|
| 1274 |
Gradio 應用程式:進階數據可視化工具
|
| 1275 |
作者:Gemini
|
| 1276 |
-
版本:
|
| 1277 |
-
描述:包含所有功能的完整程式碼,旨在解決下拉選單問題。
|
| 1278 |
"""
|
| 1279 |
|
| 1280 |
# =========================================
|
|
@@ -1497,10 +1497,13 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1497 |
# --- 1. 輸入驗證 ---
|
| 1498 |
if df is None or df.empty: raise ValueError("沒有有效的數據可供繪圖。")
|
| 1499 |
if not x_column or x_column == "-- 無數據 --": raise ValueError("請選擇有效的 X 軸或類別列。")
|
| 1500 |
-
|
| 1501 |
-
|
|
|
|
| 1502 |
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。")
|
| 1503 |
-
|
|
|
|
|
|
|
| 1504 |
if group_column and group_column != "無" and group_column not in df.columns: raise ValueError(f"分組列 '{group_column}' 不在數據中。")
|
| 1505 |
if size_column and size_column != "無" and size_column not in df.columns: raise ValueError(f"大小列 '{size_column}' 不在數據中。")
|
| 1506 |
|
|
@@ -1511,8 +1514,10 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1511 |
for col in [x_column, y_column, group_col, size_col]:
|
| 1512 |
if col:
|
| 1513 |
try:
|
| 1514 |
-
|
|
|
|
| 1515 |
df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
|
|
|
|
| 1516 |
elif col in [x_column, group_col]:
|
| 1517 |
df_processed[col] = df_processed[col].astype(str)
|
| 1518 |
except Exception as e: print(f"警告:轉換列 '{col}' 類型時出錯: {e}")
|
|
@@ -1523,18 +1528,34 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1523 |
y_col_agg = y_column # 預設 Y 軸列名
|
| 1524 |
if needs_aggregation:
|
| 1525 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1526 |
if agg_func_name == "計數":
|
| 1527 |
-
|
| 1528 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1533 |
try:
|
| 1534 |
-
|
|
|
|
|
|
|
| 1535 |
# y_col_agg 保持為 y_column
|
| 1536 |
-
except Exception as agg_e:
|
|
|
|
| 1537 |
else:
|
|
|
|
| 1538 |
agg_df = df_processed
|
| 1539 |
# y_col_agg 保持為 y_column
|
| 1540 |
|
|
@@ -1545,8 +1566,6 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1545 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1546 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
| 1547 |
|
| 1548 |
-
# 繪圖邏輯... (與 Part 2 相同,此處省略以縮短篇幅,實際代碼中應包含)
|
| 1549 |
-
# ... (省略 chart_type 判斷和 px/go 繪圖代碼) ...
|
| 1550 |
# --- (繪圖邏輯開始) ---
|
| 1551 |
if chart_type == "長條圖":
|
| 1552 |
fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
|
|
@@ -1575,37 +1594,60 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1575 |
elif chart_type == "圓餅圖":
|
| 1576 |
if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
|
| 1577 |
fig = px.pie(agg_df, names=x_column, values=y_col_agg, **fig_params)
|
| 1578 |
-
if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(cat, colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
|
| 1579 |
elif chart_type == "環形圖":
|
| 1580 |
if group_col: print("警告:環形圖不支持分組列,已忽略。")
|
| 1581 |
fig = px.pie(agg_df, names=x_column, values=y_col_agg, hole=0.4, **fig_params)
|
| 1582 |
-
if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(cat, colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
|
| 1583 |
elif chart_type == "散點圖":
|
| 1584 |
fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, **fig_params)
|
| 1585 |
elif chart_type == "氣泡圖":
|
| 1586 |
if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
|
|
|
|
|
|
|
|
|
|
| 1587 |
fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, size_max=60, **fig_params)
|
| 1588 |
elif chart_type == "直方圖":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1589 |
fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
|
| 1590 |
elif chart_type == "箱型圖":
|
|
|
|
|
|
|
|
|
|
| 1591 |
fig = px.box(agg_df, x=group_col, y=y_col_agg, color=group_col, **fig_params)
|
| 1592 |
-
if not group_col: fig = px.box(agg_df, y=y_col_agg, **fig_params)
|
| 1593 |
elif chart_type == "小提琴圖":
|
|
|
|
|
|
|
| 1594 |
fig = px.violin(agg_df, x=group_col, y=y_col_agg, color=group_col, box=True, points="all", **fig_params)
|
| 1595 |
if not group_col: fig = px.violin(agg_df, y=y_col_agg, box=True, points="all", **fig_params)
|
| 1596 |
elif chart_type == "熱力圖":
|
| 1597 |
if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列 (用於顏色或數值)。")
|
| 1598 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1599 |
pivot_df = pd.pivot_table(agg_df, values=y_col_agg, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
|
| 1600 |
-
fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", **fig_params);
|
|
|
|
| 1601 |
except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
|
| 1602 |
elif chart_type == "樹狀圖":
|
| 1603 |
path = [group_col, x_column] if group_col else [x_column]
|
|
|
|
|
|
|
|
|
|
| 1604 |
fig = px.treemap(agg_df, path=path, values=y_col_agg, color=group_col if group_col else x_column, **fig_params)
|
| 1605 |
elif chart_type == "雷達圖":
|
| 1606 |
fig = go.Figure() # 使用 go.Figure 創建
|
|
|
|
|
|
|
|
|
|
| 1607 |
if not group_col: # 單系列
|
| 1608 |
-
theta = agg_df[x_column].tolist(); r = agg_df[y_col_agg].tolist(); theta.append(theta[0]); r.append(r[0])
|
| 1609 |
fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=y_col_agg, line_color=colors[0]))
|
| 1610 |
else: # 多系列
|
| 1611 |
categories = agg_df[group_col].unique()
|
|
@@ -1615,16 +1657,39 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1615 |
fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
|
| 1616 |
fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
|
| 1617 |
elif chart_type == "漏斗圖":
|
|
|
|
|
|
|
|
|
|
| 1618 |
sorted_df = agg_df.sort_values(by=y_col_agg, ascending=False)
|
| 1619 |
fig = px.funnel(sorted_df, x=y_col_agg, y=x_column, color=group_col, **fig_params)
|
| 1620 |
elif chart_type == "極座標圖":
|
|
|
|
|
|
|
|
|
|
| 1621 |
fig = px.bar_polar(agg_df, r=y_col_agg, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1622 |
elif chart_type == "甘特圖":
|
|
|
|
| 1623 |
if not y_column or not group_col: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1624 |
try:
|
| 1625 |
-
df_gantt = df.copy()
|
| 1626 |
-
|
| 1627 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1628 |
fig.update_layout(xaxis_type="date")
|
| 1629 |
except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
|
| 1630 |
else:
|
|
@@ -1666,9 +1731,12 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1666 |
hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
|
| 1667 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
|
| 1668 |
)
|
|
|
|
|
|
|
| 1669 |
if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
|
| 1670 |
-
elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=
|
| 1671 |
-
|
|
|
|
| 1672 |
|
| 1673 |
except ValueError as ve:
|
| 1674 |
print(f"圖表創建錯誤 (ValueError): {ve}"); traceback.print_exc()
|
|
@@ -1695,12 +1763,22 @@ def download_figure(fig, format_type="PNG"):
|
|
| 1695 |
if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
|
| 1696 |
try:
|
| 1697 |
format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
|
|
|
|
|
|
|
| 1698 |
fig.write_image(filename, format=format_lower)
|
| 1699 |
return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
|
|
|
|
|
|
|
|
|
|
| 1700 |
except ValueError as ve:
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1704 |
|
| 1705 |
# =========================================
|
| 1706 |
# == 智能推薦函數 (Recommendation Function) ==
|
|
@@ -1719,11 +1797,13 @@ def recommend_chart_settings(df):
|
|
| 1719 |
elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
|
| 1720 |
else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
|
| 1721 |
except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
|
|
|
|
| 1722 |
if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
|
| 1723 |
if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
|
| 1724 |
if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
|
|
|
|
| 1725 |
if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
|
| 1726 |
-
if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
|
| 1727 |
return recommendation
|
| 1728 |
|
| 1729 |
# =========================================
|
|
@@ -1739,19 +1819,19 @@ CUSTOM_CSS = """
|
|
| 1739 |
/* --- 區塊標題 --- */
|
| 1740 |
.section-title { font-size: 1.4em; font-weight: 600; color: #343a40; border-bottom: 3px solid #7367f0; padding-bottom: 8px; margin-top: 25px; margin-bottom: 20px; }
|
| 1741 |
/* --- 卡片樣式 --- */
|
| 1742 |
-
.card { background-color: white; border-radius: 10px; padding:
|
| 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:
|
| 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:
|
| 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:
|
| 1755 |
.gradio-dataframe table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
|
| 1756 |
.gradio-dataframe th, .gradio-dataframe td { border: 1px solid #dee2e6; padding: 8px 10px; text-align: left; }
|
| 1757 |
.gradio-dataframe th { background-color: #f8f9fa; font-weight: 600; }
|
|
@@ -1767,18 +1847,20 @@ CUSTOM_CSS = """
|
|
| 1767 |
.gradio-file .hidden-upload, .gradio-file .download-button { border-radius: 6px !important; }
|
| 1768 |
.gradio-file .upload-button { border-radius: 6px !important; background: #6c757d !important; color: white !important; padding: 8px 15px !important; }
|
| 1769 |
.gradio-file .upload-button:hover { background: #5a6268 !important; }
|
|
|
|
|
|
|
| 1770 |
"""
|
| 1771 |
|
| 1772 |
# =========================================
|
| 1773 |
# == Gradio UI 介面定義 (Gradio UI Definition) ==
|
| 1774 |
# =========================================
|
| 1775 |
-
with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具
|
| 1776 |
|
| 1777 |
# --- 應用程式標頭 ---
|
| 1778 |
gr.HTML("""
|
| 1779 |
<div class="app-header">
|
| 1780 |
-
<h1 class="app-title">📊 進階數據可視化工具
|
| 1781 |
-
<p class="app-subtitle">上傳或貼上數據,輕鬆創建和比較多種專業圖表</p>
|
| 1782 |
</div>
|
| 1783 |
""")
|
| 1784 |
|
|
@@ -1818,32 +1900,29 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1818 |
export_result = gr.File(label="導出文件下載", interactive=False)
|
| 1819 |
export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 1820 |
|
| 1821 |
-
# --- 圖表創建頁籤 ---
|
| 1822 |
with gr.TabItem("📈 圖表創建與比較", id=1):
|
| 1823 |
-
gr.HTML('<div class="section-title">創建與比較圖表</div>')
|
| 1824 |
gr.Markdown("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
|
| 1825 |
|
| 1826 |
-
#
|
| 1827 |
-
|
| 1828 |
-
gr.
|
| 1829 |
-
|
| 1830 |
-
#
|
| 1831 |
-
|
| 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("🧠 智能推薦
|
| 1836 |
-
chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一
|
| 1837 |
agg_function_1 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數", info="選擇如何彙總 Y 軸數據")
|
| 1838 |
|
| 1839 |
-
|
| 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.
|
| 1846 |
-
gr.Markdown("**顯示選項**")
|
| 1847 |
chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
|
| 1848 |
chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
|
| 1849 |
with gr.Row():
|
|
@@ -1853,50 +1932,43 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1853 |
gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
|
| 1854 |
gr.HTML(generate_color_cards(), elem_id="color_display_1")
|
| 1855 |
|
| 1856 |
-
with gr.
|
| 1857 |
-
gr.Markdown("**圖案與自定義顏色**")
|
| 1858 |
with gr.Row():
|
| 1859 |
pattern1_1 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
|
| 1860 |
pattern2_1 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
|
| 1861 |
pattern3_1 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
|
| 1862 |
color_customization_1 = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
|
| 1863 |
|
| 1864 |
-
# 圖表一:
|
| 1865 |
-
with gr.
|
| 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 |
-
|
| 1872 |
-
|
| 1873 |
-
|
| 1874 |
-
|
| 1875 |
-
|
| 1876 |
-
|
| 1877 |
-
|
| 1878 |
-
|
| 1879 |
-
|
| 1880 |
-
|
| 1881 |
-
|
| 1882 |
-
gr.
|
| 1883 |
-
|
| 1884 |
-
#
|
| 1885 |
-
|
| 1886 |
-
with gr.Group(elem_classes=["card"]):
|
| 1887 |
-
gr.Markdown("**基本設置**")
|
| 1888 |
chart_type_2 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="折線圖", interactive=True)
|
| 1889 |
-
|
|
|
|
| 1890 |
agg_function_2 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="平均值", info="選擇如何彙總 Y 軸數據")
|
| 1891 |
|
| 1892 |
-
|
| 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.
|
| 1899 |
-
gr.Markdown("**顯示選項**")
|
| 1900 |
chart_width_2 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
|
| 1901 |
chart_height_2 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
|
| 1902 |
with gr.Row():
|
|
@@ -1905,26 +1977,25 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1905 |
color_scheme_2 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="分類 - Set2")
|
| 1906 |
# 顏色參考共用
|
| 1907 |
|
| 1908 |
-
with gr.
|
| 1909 |
-
gr.Markdown("**圖案與自定義顏色**")
|
| 1910 |
with gr.Row():
|
| 1911 |
pattern1_2 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
|
| 1912 |
pattern2_2 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
|
| 1913 |
pattern3_2 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
|
| 1914 |
color_customization_2 = gr.Textbox(label="自定義顏色", placeholder="���別C:#FFC300, 類別D:#C70039", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
|
| 1915 |
|
| 1916 |
-
# 圖表二:
|
| 1917 |
-
with gr.
|
| 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 |
-
|
| 1924 |
-
|
| 1925 |
-
|
| 1926 |
-
|
| 1927 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1928 |
|
| 1929 |
# --- 使用說明頁籤 ---
|
| 1930 |
with gr.TabItem("❓ 使用說明", id=2):
|
|
@@ -1934,9 +2005,11 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1934 |
<h3>數據輸入</h3>
|
| 1935 |
<ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
|
| 1936 |
<h3>圖表創建與比較</h3>
|
| 1937 |
-
<ul><li>此頁面提供兩個獨立的圖表設置和預覽區域(圖表一、圖表二)。</li><li><strong>智能推薦:</strong>點擊 "智能推薦
|
|
|
|
|
|
|
| 1938 |
<h3>提示</h3>
|
| 1939 |
-
<ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>確保數值列確實包含數字,日期列包含有效的日期格式。</li><li>部分圖表類型對數據結構有特定要求(例如,熱力圖、甘特圖)。</li></ul>
|
| 1940 |
""")
|
| 1941 |
|
| 1942 |
# =========================================
|
|
@@ -1950,7 +2023,7 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1950 |
if col_updates is None or len(col_updates) != 4:
|
| 1951 |
print("警告: update_columns 未返回預期的 4 個組件更新。")
|
| 1952 |
return [df, status_msg, preview_df] + [gr.update()] * 8
|
| 1953 |
-
return [df, status_msg, preview_df] + list(col_updates) * 2
|
| 1954 |
|
| 1955 |
upload_button.click(process_upload, inputs=[file_upload], outputs=[data_state, upload_status]).then(
|
| 1956 |
load_data_and_update_ui, inputs=[data_state, upload_status],
|
|
@@ -1973,8 +2046,12 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1973 |
chart_inputs_1 = [data_state, chart_type_1, x_column_1, y_column_1, group_column_1, size_column_1, color_scheme_1, patterns_state_1, chart_title_1, chart_width_1, chart_height_1, show_grid_1, show_legend_1, agg_function_1, custom_colors_state_1]
|
| 1974 |
update_button_1.click(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
|
| 1975 |
def auto_update_chart_1(*inputs): return create_plot(*inputs)
|
| 1976 |
-
|
| 1977 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1978 |
|
| 1979 |
# --- 圖表一:導出圖表 ---
|
| 1980 |
download_button_1.click(download_figure, inputs=[chart_output_1, export_img_format_1], outputs=[export_chart_1, export_chart_status_1])
|
|
@@ -1988,7 +2065,7 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1988 |
|
| 1989 |
recommend_button_1.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
|
| 1990 |
apply_recommendation, inputs=[recommendation_state], outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
|
| 1991 |
-
).then(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
|
| 1992 |
|
| 1993 |
# --- 圖表二:顏色和圖案狀態 ---
|
| 1994 |
color_customization_2.change(parse_custom_colors, inputs=[color_customization_2], outputs=[custom_colors_state_2])
|
|
@@ -1999,8 +2076,10 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V2", theme=gr.
|
|
| 1999 |
chart_inputs_2 = [data_state, chart_type_2, x_column_2, y_column_2, group_column_2, size_column_2, color_scheme_2, patterns_state_2, chart_title_2, chart_width_2, chart_height_2, show_grid_2, show_legend_2, agg_function_2, custom_colors_state_2]
|
| 2000 |
update_button_2.click(create_plot, inputs=chart_inputs_2, outputs=[chart_output_2])
|
| 2001 |
def auto_update_chart_2(*inputs): return create_plot(*inputs)
|
| 2002 |
-
|
| 2003 |
-
|
|
|
|
|
|
|
| 2004 |
|
| 2005 |
# --- 圖表二:導出圖表 ---
|
| 2006 |
download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
|
|
|
|
| 1273 |
"""
|
| 1274 |
Gradio 應用程式:進階數據可視化工具
|
| 1275 |
作者:Gemini
|
| 1276 |
+
版本:3.0 (完整修復版 - 左右佈局, Accordion, 移除下拉選單 CSS)
|
| 1277 |
+
描述:包含所有功能的完整程式碼,旨在解決下拉選單問題並改進佈局。
|
| 1278 |
"""
|
| 1279 |
|
| 1280 |
# =========================================
|
|
|
|
| 1497 |
# --- 1. 輸入驗證 ---
|
| 1498 |
if df is None or df.empty: raise ValueError("沒有有效的數據可供繪圖。")
|
| 1499 |
if not x_column or x_column == "-- 無數據 --": raise ValueError("請選擇有效的 X 軸或類別列。")
|
| 1500 |
+
# 只有在非計數且非直方圖時才嚴格要求 Y 軸
|
| 1501 |
+
if agg_func_name != "計數" and chart_type not in ["直方圖"] and (not y_column or y_column == "-- 無數據 --"):
|
| 1502 |
+
raise ValueError("請選擇有效的 Y 軸或數值列。")
|
| 1503 |
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。")
|
| 1504 |
+
# 只有在需要 Y 軸時才檢查 Y 軸是否存在
|
| 1505 |
+
if agg_func_name != "計數" and chart_type not in ["直方圖"] and y_column not in df.columns:
|
| 1506 |
+
raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。")
|
| 1507 |
if group_column and group_column != "無" and group_column not in df.columns: raise ValueError(f"分組列 '{group_column}' 不在數據中。")
|
| 1508 |
if size_column and size_column != "無" and size_column not in df.columns: raise ValueError(f"大小列 '{size_column}' 不在數據中。")
|
| 1509 |
|
|
|
|
| 1514 |
for col in [x_column, y_column, group_col, size_col]:
|
| 1515 |
if col:
|
| 1516 |
try:
|
| 1517 |
+
# 嘗試將 Y 軸和大小列轉為數值 (如果需要聚合或用於大小)
|
| 1518 |
+
if (agg_func_name != "計數" and col == y_column) or (col == size_col):
|
| 1519 |
df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
|
| 1520 |
+
# X 軸和分組列通常視為分類 (字符串)
|
| 1521 |
elif col in [x_column, group_col]:
|
| 1522 |
df_processed[col] = df_processed[col].astype(str)
|
| 1523 |
except Exception as e: print(f"警告:轉換列 '{col}' 類型時出錯: {e}")
|
|
|
|
| 1528 |
y_col_agg = y_column # 預設 Y 軸列名
|
| 1529 |
if needs_aggregation:
|
| 1530 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
| 1531 |
+
# 檢查分組列是否有效
|
| 1532 |
+
invalid_grouping_cols = [col for col in grouping_cols if col not in df_processed.columns]
|
| 1533 |
+
if invalid_grouping_cols:
|
| 1534 |
+
raise ValueError(f"以下分組/X軸列不在數據中: {', '.join(invalid_grouping_cols)}")
|
| 1535 |
+
|
| 1536 |
if agg_func_name == "計數":
|
| 1537 |
+
# 使用 size() 計算每個組的行數
|
| 1538 |
+
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size() # dropna=False 包含 NaN 類別
|
| 1539 |
+
agg_df = agg_df.reset_index(name='__count__')
|
| 1540 |
+
y_col_agg = '__count__' # 使用新生成的計數列
|
| 1541 |
else:
|
| 1542 |
agg_func_pd = agg_function_map(agg_func_name)
|
| 1543 |
if not y_column or y_column not in df_processed.columns: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
|
| 1544 |
+
# 再次檢查 Y 軸是否為數值
|
| 1545 |
+
if not pd.api.types.is_numeric_dtype(df_processed[y_column]):
|
| 1546 |
+
# 如果聚合函數是 first 或 last,允許非數值
|
| 1547 |
+
if agg_func_pd not in ['first', 'last']:
|
| 1548 |
+
raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
|
| 1549 |
+
|
| 1550 |
try:
|
| 1551 |
+
# 執行聚合
|
| 1552 |
+
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd)
|
| 1553 |
+
agg_df = agg_df.reset_index()
|
| 1554 |
# y_col_agg 保持為 y_column
|
| 1555 |
+
except Exception as agg_e:
|
| 1556 |
+
raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
|
| 1557 |
else:
|
| 1558 |
+
# 不需要聚合,直接使用處理過的數據
|
| 1559 |
agg_df = df_processed
|
| 1560 |
# y_col_agg 保持為 y_column
|
| 1561 |
|
|
|
|
| 1566 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1567 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
| 1568 |
|
|
|
|
|
|
|
| 1569 |
# --- (繪圖邏輯開始) ---
|
| 1570 |
if chart_type == "長條圖":
|
| 1571 |
fig = px.bar(agg_df, x=x_column, y=y_col_agg, color=group_col, **fig_params)
|
|
|
|
| 1594 |
elif chart_type == "圓餅圖":
|
| 1595 |
if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
|
| 1596 |
fig = px.pie(agg_df, names=x_column, values=y_col_agg, **fig_params)
|
| 1597 |
+
if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(str(cat), colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
|
| 1598 |
elif chart_type == "環形圖":
|
| 1599 |
if group_col: print("警告:環形圖不支持分組列,已忽略。")
|
| 1600 |
fig = px.pie(agg_df, names=x_column, values=y_col_agg, hole=0.4, **fig_params)
|
| 1601 |
+
if not group_col and custom_colors_dict: fig.update_traces(marker=dict(colors=[custom_colors_dict.get(str(cat), colors[i % len(colors)]) for i, cat in enumerate(agg_df[x_column])]))
|
| 1602 |
elif chart_type == "散點圖":
|
| 1603 |
fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, **fig_params)
|
| 1604 |
elif chart_type == "氣泡圖":
|
| 1605 |
if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
|
| 1606 |
+
# 確保大小列是數值
|
| 1607 |
+
if not pd.api.types.is_numeric_dtype(agg_df[size_col]):
|
| 1608 |
+
raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
|
| 1609 |
fig = px.scatter(agg_df, x=x_column, y=y_col_agg, color=group_col, size=size_col, size_max=60, **fig_params)
|
| 1610 |
elif chart_type == "直方圖":
|
| 1611 |
+
# 直方圖分析單一變量分佈,Y 軸通常是計數
|
| 1612 |
+
# 確保 x_column 是數值類型
|
| 1613 |
+
if not pd.api.types.is_numeric_dtype(agg_df[x_column]):
|
| 1614 |
+
raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
|
| 1615 |
fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
|
| 1616 |
elif chart_type == "箱型圖":
|
| 1617 |
+
# Y 軸必須是數值
|
| 1618 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1619 |
+
raise ValueError(f"箱型圖的 Y 軸列 '{y_col_agg}' 必須是數值類型。")
|
| 1620 |
fig = px.box(agg_df, x=group_col, y=y_col_agg, color=group_col, **fig_params)
|
| 1621 |
+
if not group_col: fig = px.box(agg_df, y=y_col_agg, **fig_params) # 無分組
|
| 1622 |
elif chart_type == "小提琴圖":
|
| 1623 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1624 |
+
raise ValueError(f"小提琴圖的 Y 軸列 '{y_col_agg}' 必須是數值類型。")
|
| 1625 |
fig = px.violin(agg_df, x=group_col, y=y_col_agg, color=group_col, box=True, points="all", **fig_params)
|
| 1626 |
if not group_col: fig = px.violin(agg_df, y=y_col_agg, box=True, points="all", **fig_params)
|
| 1627 |
elif chart_type == "熱力圖":
|
| 1628 |
if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列 (用於顏色或數值)。")
|
| 1629 |
try:
|
| 1630 |
+
# 確保值列是數值
|
| 1631 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1632 |
+
raise ValueError(f"熱力圖的值列 '{y_col_agg}' 必須是數值類型。")
|
| 1633 |
+
# 創建數據透視表
|
| 1634 |
pivot_df = pd.pivot_table(agg_df, values=y_col_agg, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
|
| 1635 |
+
fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params); # text_auto 顯示數值
|
| 1636 |
+
fig.update_layout(coloraxis_showscale=True)
|
| 1637 |
except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
|
| 1638 |
elif chart_type == "樹狀圖":
|
| 1639 |
path = [group_col, x_column] if group_col else [x_column]
|
| 1640 |
+
# 確保值列是數值
|
| 1641 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1642 |
+
raise ValueError(f"樹狀圖的值列 '{y_col_agg}' 必須是數值類型。")
|
| 1643 |
fig = px.treemap(agg_df, path=path, values=y_col_agg, color=group_col if group_col else x_column, **fig_params)
|
| 1644 |
elif chart_type == "雷達圖":
|
| 1645 |
fig = go.Figure() # 使用 go.Figure 創建
|
| 1646 |
+
# 確保 R 值是數值
|
| 1647 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1648 |
+
raise ValueError(f"雷達圖的徑向值列 '{y_col_agg}' 必須是數值類型。")
|
| 1649 |
if not group_col: # 單系列
|
| 1650 |
+
theta = agg_df[x_column].tolist(); r = agg_df[y_col_agg].tolist(); theta.append(theta[0]); r.append(r[0]) # 封閉圖形
|
| 1651 |
fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=y_col_agg, line_color=colors[0]))
|
| 1652 |
else: # 多系列
|
| 1653 |
categories = agg_df[group_col].unique()
|
|
|
|
| 1657 |
fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
|
| 1658 |
fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
|
| 1659 |
elif chart_type == "漏斗圖":
|
| 1660 |
+
# 確保值列是數值
|
| 1661 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1662 |
+
raise ValueError(f"漏斗圖的值列 '{y_col_agg}' 必須是數值類型。")
|
| 1663 |
sorted_df = agg_df.sort_values(by=y_col_agg, ascending=False)
|
| 1664 |
fig = px.funnel(sorted_df, x=y_col_agg, y=x_column, color=group_col, **fig_params)
|
| 1665 |
elif chart_type == "極座標圖":
|
| 1666 |
+
# 確保 R 值是數值
|
| 1667 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_agg]):
|
| 1668 |
+
raise ValueError(f"極座標圖的徑向值列 '{y_col_agg}' 必須是數值類型。")
|
| 1669 |
fig = px.bar_polar(agg_df, r=y_col_agg, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1670 |
elif chart_type == "甘特圖":
|
| 1671 |
+
# 甘特圖使用原始數據,需要開始和結束列
|
| 1672 |
if not y_column or not group_col: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1673 |
try:
|
| 1674 |
+
df_gantt = df.copy() # 使用原始 df
|
| 1675 |
+
start_col, end_col = y_column, group_col # 根據用戶選擇
|
| 1676 |
+
task_col = x_column # 任務列
|
| 1677 |
+
# 檢查列是否存在
|
| 1678 |
+
if start_col not in df_gantt.columns: raise ValueError(f"開始列 '{start_col}' 不在數據中。")
|
| 1679 |
+
if end_col not in df_gantt.columns: raise ValueError(f"結束列 '{end_col}' 不在數據中。")
|
| 1680 |
+
if task_col not in df_gantt.columns: raise ValueError(f"任務列 '{task_col}' 不在數據中。")
|
| 1681 |
+
|
| 1682 |
+
# 嘗試轉換為日期時間
|
| 1683 |
+
df_gantt['_start_'] = pd.to_datetime(df_gantt[start_col], errors='coerce')
|
| 1684 |
+
df_gantt['_end_'] = pd.to_datetime(df_gantt[end_col], errors='coerce')
|
| 1685 |
+
# 檢查是否有無效日期
|
| 1686 |
+
if df_gantt['_start_'].isnull().any(): raise ValueError(f"開始列 '{start_col}' 包含無效或無法解析的日期時間格式。")
|
| 1687 |
+
if df_gantt['_end_'].isnull().any(): raise ValueError(f"結束列 '{end_col}' 包含無效或無法解析的日期時間格式。")
|
| 1688 |
+
|
| 1689 |
+
# 繪製甘特圖
|
| 1690 |
+
fig = px.timeline(df_gantt, x_start='_start_', x_end='_end_', y=task_col,
|
| 1691 |
+
color=size_col if size_col else None, # 可以用大小列來區分顏色
|
| 1692 |
+
title=title, color_discrete_sequence=colors, width=width, height=height)
|
| 1693 |
fig.update_layout(xaxis_type="date")
|
| 1694 |
except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
|
| 1695 |
else:
|
|
|
|
| 1731 |
hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
|
| 1732 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
|
| 1733 |
)
|
| 1734 |
+
# 根據圖表類型更新軸標籤
|
| 1735 |
+
final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
|
| 1736 |
if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
|
| 1737 |
+
elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
|
| 1738 |
+
elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
|
| 1739 |
+
else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
|
| 1740 |
|
| 1741 |
except ValueError as ve:
|
| 1742 |
print(f"圖表創建錯誤 (ValueError): {ve}"); traceback.print_exc()
|
|
|
|
| 1763 |
if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
|
| 1764 |
try:
|
| 1765 |
format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
|
| 1766 |
+
# 確保有 kaleido
|
| 1767 |
+
import kaleido
|
| 1768 |
fig.write_image(filename, format=format_lower)
|
| 1769 |
return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
|
| 1770 |
+
except ImportError:
|
| 1771 |
+
error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"
|
| 1772 |
+
print(error_msg); return None, error_msg
|
| 1773 |
except ValueError as ve:
|
| 1774 |
+
# kaleido 可能安裝但無法運行
|
| 1775 |
+
if "kaleido" in str(ve).lower():
|
| 1776 |
+
error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"
|
| 1777 |
+
print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
|
| 1778 |
+
else:
|
| 1779 |
+
print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
|
| 1780 |
+
except Exception as e:
|
| 1781 |
+
print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
|
| 1782 |
|
| 1783 |
# =========================================
|
| 1784 |
# == 智能推薦函數 (Recommendation Function) ==
|
|
|
|
| 1797 |
elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
|
| 1798 |
else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
|
| 1799 |
except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
|
| 1800 |
+
# 驗證推薦的列名
|
| 1801 |
if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
|
| 1802 |
if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
|
| 1803 |
if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
|
| 1804 |
+
# 清理無效聚合
|
| 1805 |
if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
|
| 1806 |
+
if recommendation["agg_function"] == "計數": recommendation["y_column"] = None # 計數時 Y 軸應為空
|
| 1807 |
return recommendation
|
| 1808 |
|
| 1809 |
# =========================================
|
|
|
|
| 1819 |
/* --- 區塊標題 --- */
|
| 1820 |
.section-title { font-size: 1.4em; font-weight: 600; color: #343a40; border-bottom: 3px solid #7367f0; padding-bottom: 8px; margin-top: 25px; margin-bottom: 20px; }
|
| 1821 |
/* --- 卡片樣式 --- */
|
| 1822 |
+
.card { background-color: white; border-radius: 10px; padding: 20px; /* 稍微減少 padding */ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); margin-bottom: 20px; transition: transform 0.25s ease-out, box-shadow 0.25s ease-out; border: 1px solid #e0e0e0; }
|
| 1823 |
.card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); }
|
| 1824 |
/* --- 按鈕樣式 --- */
|
| 1825 |
+
.primary-button { background: linear-gradient(to right, #667eea, #764ba2) !important; border: none !important; color: white !important; font-weight: 600 !important; padding: 10px 20px !important; /* 調整 padding */ border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
|
| 1826 |
.primary-button:hover { background: linear-gradient(to right, #764ba2, #667eea) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
|
| 1827 |
+
.secondary-button { background: linear-gradient(to right, #89f7fe, #66a6ff) !important; border: none !important; color: #333 !important; font-weight: 600 !important; padding: 8px 16px !important; /* 調整 padding */ border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
|
| 1828 |
.secondary-button:hover { background: linear-gradient(to right, #66a6ff, #89f7fe) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
|
| 1829 |
/* --- 下拉選單修正 (Dropdown Fix) --- */
|
| 1830 |
/* 移除自定義下拉選單樣式,使用 Gradio 預設 */
|
| 1831 |
/* --- 其他 UI 元素 --- */
|
| 1832 |
.tips-box { background-color: #e7f3ff; border-left: 5px solid #66a6ff; padding: 15px 20px; border-radius: 8px; margin: 20px 0; font-size: 0.95em; color: #333; }
|
| 1833 |
.tips-box code { background-color: #d1e7fd; padding: 2px 5px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; }
|
| 1834 |
+
.chart-previewer { border: 2px dashed #ced4da; border-radius: 10px; padding: 15px; /* 減少 padding */ min-height: 400px; /* 調整高度 */ display: flex; justify-content: center; align-items: center; background-color: #ffffff; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); margin-top: 10px; /* 減少 margin */ }
|
| 1835 |
.gradio-dataframe table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
|
| 1836 |
.gradio-dataframe th, .gradio-dataframe td { border: 1px solid #dee2e6; padding: 8px 10px; text-align: left; }
|
| 1837 |
.gradio-dataframe th { background-color: #f8f9fa; font-weight: 600; }
|
|
|
|
| 1847 |
.gradio-file .hidden-upload, .gradio-file .download-button { border-radius: 6px !important; }
|
| 1848 |
.gradio-file .upload-button { border-radius: 6px !important; background: #6c757d !important; color: white !important; padding: 8px 15px !important; }
|
| 1849 |
.gradio-file .upload-button:hover { background: #5a6268 !important; }
|
| 1850 |
+
/* Accordion 樣式微調 */
|
| 1851 |
+
.gradio-accordion > .label { font-weight: 600 !important; font-size: 1.1em !important; padding: 10px 0 !important; }
|
| 1852 |
"""
|
| 1853 |
|
| 1854 |
# =========================================
|
| 1855 |
# == Gradio UI 介面定義 (Gradio UI Definition) ==
|
| 1856 |
# =========================================
|
| 1857 |
+
with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V3", theme=gr.themes.Soft()) as demo:
|
| 1858 |
|
| 1859 |
# --- 應用程式標頭 ---
|
| 1860 |
gr.HTML("""
|
| 1861 |
<div class="app-header">
|
| 1862 |
+
<h1 class="app-title">📊 進階數據可視化工具 V3</h1>
|
| 1863 |
+
<p class="app-subtitle">上傳或貼上數據,輕鬆創建和比較多種專業圖表 (左右佈局)</p>
|
| 1864 |
</div>
|
| 1865 |
""")
|
| 1866 |
|
|
|
|
| 1900 |
export_result = gr.File(label="導出文件下載", interactive=False)
|
| 1901 |
export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 1902 |
|
| 1903 |
+
# --- 圖表創建頁籤 (左右佈局) ---
|
| 1904 |
with gr.TabItem("📈 圖表創建與比較", id=1):
|
| 1905 |
+
gr.HTML('<div class="section-title">創建與比較圖表 (左右並列)</div>')
|
| 1906 |
gr.Markdown("在這裡,您可以分別設置並生成兩張圖表,方便進行對比分析。")
|
| 1907 |
|
| 1908 |
+
with gr.Row(): # 主 Row,包含左右兩個圖表區塊
|
| 1909 |
+
# --- 圖表一 (左側 Column) ---
|
| 1910 |
+
with gr.Column(scale=1):
|
| 1911 |
+
gr.Markdown("### 📊 圖表一")
|
| 1912 |
+
with gr.Group(elem_classes=["card"]): # 將所有設定放在一個卡片中
|
| 1913 |
+
with gr.Accordion("基本設置", open=True): # 使用 Accordion
|
|
|
|
|
|
|
| 1914 |
chart_type_1 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True)
|
| 1915 |
+
recommend_button_1 = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm") # 縮小按鈕
|
| 1916 |
+
chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一標題")
|
| 1917 |
agg_function_1 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數", info="選擇如何彙總 Y 軸數據")
|
| 1918 |
|
| 1919 |
+
with gr.Accordion("數據映射", open=True): # 使用 Accordion
|
| 1920 |
x_column_1 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
|
| 1921 |
y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
|
| 1922 |
group_column_1 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊")
|
| 1923 |
size_column_1 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小")
|
| 1924 |
|
| 1925 |
+
with gr.Accordion("顯示選項", open=False): # 預設關閉
|
|
|
|
| 1926 |
chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
|
| 1927 |
chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
|
| 1928 |
with gr.Row():
|
|
|
|
| 1932 |
gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
|
| 1933 |
gr.HTML(generate_color_cards(), elem_id="color_display_1")
|
| 1934 |
|
| 1935 |
+
with gr.Accordion("圖案與自定義顏色", open=False): # 預設關閉
|
|
|
|
| 1936 |
with gr.Row():
|
| 1937 |
pattern1_1 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
|
| 1938 |
pattern2_1 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
|
| 1939 |
pattern3_1 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
|
| 1940 |
color_customization_1 = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
|
| 1941 |
|
| 1942 |
+
# 圖表一:操作按鈕 (放在設定下方,預覽上方)
|
| 1943 |
+
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1944 |
update_button_1 = gr.Button("🔄 更新圖表一", variant="primary", elem_classes=["primary-button"])
|
| 1945 |
+
export_img_format_1 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG", scale=1)
|
| 1946 |
+
download_button_1 = gr.Button("💾 導出圖表一", elem_classes=["secondary-button"], scale=1)
|
| 1947 |
+
export_chart_1 = gr.File(label="圖表一文件下載", interactive=False)
|
| 1948 |
+
export_chart_status_1 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 1949 |
+
|
| 1950 |
+
# 圖表一:預覽區域
|
| 1951 |
+
with gr.Group(elem_classes=["chart-previewer"]):
|
| 1952 |
+
chart_output_1 = gr.Plot(label="", elem_id="chart_preview_1")
|
| 1953 |
+
|
| 1954 |
+
|
| 1955 |
+
# --- 圖表二 (右側 Column) ---
|
| 1956 |
+
with gr.Column(scale=1):
|
| 1957 |
+
gr.Markdown("### 📊 圖表二")
|
| 1958 |
+
with gr.Group(elem_classes=["card"]): # 將所有設定放在一個卡片中
|
| 1959 |
+
with gr.Accordion("基本設置", open=True): # 使用 Accordion
|
|
|
|
|
|
|
| 1960 |
chart_type_2 = gr.Dropdown(CHART_TYPES, label="圖表類型", value="折線圖", interactive=True)
|
| 1961 |
+
# 智能推薦只放在圖表一
|
| 1962 |
+
chart_title_2 = gr.Textbox(label="圖表標題", placeholder="圖表二標題")
|
| 1963 |
agg_function_2 = gr.Dropdown(AGGREGATION_FUNCTIONS, label="聚合函數", value="平均值", info="選擇如何彙總 Y 軸數據")
|
| 1964 |
|
| 1965 |
+
with gr.Accordion("數據映射", open=True): # 使用 Accordion
|
| 1966 |
x_column_2 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
|
| 1967 |
y_column_2 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
|
| 1968 |
group_column_2 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊")
|
| 1969 |
size_column_2 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小")
|
| 1970 |
|
| 1971 |
+
with gr.Accordion("顯示選項", open=False): # 預設關閉
|
|
|
|
| 1972 |
chart_width_2 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
|
| 1973 |
chart_height_2 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
|
| 1974 |
with gr.Row():
|
|
|
|
| 1977 |
color_scheme_2 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="分類 - Set2")
|
| 1978 |
# 顏色參考共用
|
| 1979 |
|
| 1980 |
+
with gr.Accordion("圖案與自定義顏色", open=False): # 預設關閉
|
|
|
|
| 1981 |
with gr.Row():
|
| 1982 |
pattern1_2 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
|
| 1983 |
pattern2_2 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
|
| 1984 |
pattern3_2 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
|
| 1985 |
color_customization_2 = gr.Textbox(label="自定義顏色", placeholder="���別C:#FFC300, 類別D:#C70039", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
|
| 1986 |
|
| 1987 |
+
# 圖表二:操作按鈕
|
| 1988 |
+
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1989 |
update_button_2 = gr.Button("🔄 更新圖表二", variant="primary", elem_classes=["primary-button"])
|
| 1990 |
+
export_img_format_2 = gr.Dropdown(["PNG", "SVG", "PDF", "JPEG"], label="導出格式", value="PNG", scale=1)
|
| 1991 |
+
download_button_2 = gr.Button("💾 導出圖表二", elem_classes=["secondary-button"], scale=1)
|
| 1992 |
+
export_chart_2 = gr.File(label="圖表二文件下載", interactive=False)
|
| 1993 |
+
export_chart_status_2 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 1994 |
+
|
| 1995 |
+
# 圖表二:預覽區域
|
| 1996 |
+
with gr.Group(elem_classes=["chart-previewer"]):
|
| 1997 |
+
chart_output_2 = gr.Plot(label="", elem_id="chart_preview_2")
|
| 1998 |
+
|
| 1999 |
|
| 2000 |
# --- 使用說明頁籤 ---
|
| 2001 |
with gr.TabItem("❓ 使用說明", id=2):
|
|
|
|
| 2005 |
<h3>數據輸入</h3>
|
| 2006 |
<ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
|
| 2007 |
<h3>圖表創建與比較</h3>
|
| 2008 |
+
<ul><li>此頁面提供左右兩個獨立的圖表設置和預覽區域(圖表一、圖表二)。</li><li><strong>智能推薦:</strong>點擊圖表一的 "智能推薦" 按鈕,系統會根據數據結構嘗試為圖表一推薦合適的設置。</li><li><strong>圖表類型:</strong>選擇您想創建的圖表樣式。</li><li><strong>聚合函數:</strong>決定如何匯總 Y 軸數據。
|
| 2009 |
+
<ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong>若要統計某一欄位中各個項目出現的次數(例如,統計不同產品的銷售筆數),請在 <strong>X軸/類別</strong> 選擇該欄位,並將 <strong>聚合函數</strong> 設為 <strong>計數</strong>,此時 <strong>無需��擇 Y軸/數值</strong>。然後選擇「長條圖」或「圓餅圖」。</li></ul>
|
| 2010 |
+
</li><li><strong>數據映射:</strong><ul><li><strong>X軸/類別:</strong>圖表的主要分類軸。</li><li><strong>Y軸/數值:</strong>圖表的數值軸。若聚合函數為 "計數",此項可忽略。</li><li><strong>分組列:</strong>用於創建堆疊、分組或多系列圖表。</li><li><strong>大小列:</strong>主要用於氣泡圖,控制點的大小。</li></ul></li><li><strong>顯示選項:</strong>調整圖表的外觀,如寬度、高度、顏色方案、是否顯示網格和圖例。</li><li><strong>圖案與自定義顏色:</strong><ul><li>為圖表系列添加不同的填充圖案(適用於部分圖表類型,如條形圖)。</li><li>通過 "類別名:顏色代碼" 的格式為特定類別指定顏色 (例如 <code>正面:#2ca02c, 負面:#d62728</code>)。</li></ul></li><li>點擊 "更新圖表" 按鈕生成或刷新對應的圖表預覽。</li><li>使用 "導出圖表" 功能將生成的圖表保存為圖片文件。</li></ul>
|
| 2011 |
<h3>提示</h3>
|
| 2012 |
+
<ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>確保數值列確實包含數字,日期列包含有效的日期格式。</li><li>部分圖表類型對數據結構有特定要求(例如,熱力圖、甘特圖)。</li><li>如果下拉選單仍然有問題,嘗試刷新頁面或在不同的瀏覽器中打開。</li></ul>
|
| 2013 |
""")
|
| 2014 |
|
| 2015 |
# =========================================
|
|
|
|
| 2023 |
if col_updates is None or len(col_updates) != 4:
|
| 2024 |
print("警告: update_columns 未返回預期的 4 個組件更新。")
|
| 2025 |
return [df, status_msg, preview_df] + [gr.update()] * 8
|
| 2026 |
+
return [df, status_msg, preview_df] + list(col_updates) * 2 # 更新兩組下拉列表
|
| 2027 |
|
| 2028 |
upload_button.click(process_upload, inputs=[file_upload], outputs=[data_state, upload_status]).then(
|
| 2029 |
load_data_and_update_ui, inputs=[data_state, upload_status],
|
|
|
|
| 2046 |
chart_inputs_1 = [data_state, chart_type_1, x_column_1, y_column_1, group_column_1, size_column_1, color_scheme_1, patterns_state_1, chart_title_1, chart_width_1, chart_height_1, show_grid_1, show_legend_1, agg_function_1, custom_colors_state_1]
|
| 2047 |
update_button_1.click(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1])
|
| 2048 |
def auto_update_chart_1(*inputs): return create_plot(*inputs)
|
| 2049 |
+
# 綁定 change 事件到會影響圖表的 UI 組件
|
| 2050 |
+
for input_component in [chart_type_1, x_column_1, y_column_1, group_column_1, size_column_1, color_scheme_1, chart_title_1, chart_width_1, chart_height_1, show_grid_1, show_legend_1, agg_function_1, color_customization_1, pattern1_1, pattern2_1, pattern3_1]:
|
| 2051 |
+
# 確保組件不是 None 且有 change 方法
|
| 2052 |
+
if input_component is not None and hasattr(input_component, 'change'):
|
| 2053 |
+
input_component.change(auto_update_chart_1, inputs=chart_inputs_1, outputs=[chart_output_1])
|
| 2054 |
+
|
| 2055 |
|
| 2056 |
# --- 圖表一:導出圖表 ---
|
| 2057 |
download_button_1.click(download_figure, inputs=[chart_output_1, export_img_format_1], outputs=[export_chart_1, export_chart_status_1])
|
|
|
|
| 2065 |
|
| 2066 |
recommend_button_1.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
|
| 2067 |
apply_recommendation, inputs=[recommendation_state], outputs=[chart_type_1, x_column_1, y_column_1, group_column_1, agg_function_1]
|
| 2068 |
+
).then(create_plot, inputs=chart_inputs_1, outputs=[chart_output_1]) # 應用推薦後觸發更新
|
| 2069 |
|
| 2070 |
# --- 圖表二:顏色和圖案狀態 ---
|
| 2071 |
color_customization_2.change(parse_custom_colors, inputs=[color_customization_2], outputs=[custom_colors_state_2])
|
|
|
|
| 2076 |
chart_inputs_2 = [data_state, chart_type_2, x_column_2, y_column_2, group_column_2, size_column_2, color_scheme_2, patterns_state_2, chart_title_2, chart_width_2, chart_height_2, show_grid_2, show_legend_2, agg_function_2, custom_colors_state_2]
|
| 2077 |
update_button_2.click(create_plot, inputs=chart_inputs_2, outputs=[chart_output_2])
|
| 2078 |
def auto_update_chart_2(*inputs): return create_plot(*inputs)
|
| 2079 |
+
# 綁定 change 事件到會影響圖表的 UI 組件
|
| 2080 |
+
for input_component in [chart_type_2, x_column_2, y_column_2, group_column_2, size_column_2, color_scheme_2, chart_title_2, chart_width_2, chart_height_2, show_grid_2, show_legend_2, agg_function_2, color_customization_2, pattern1_2, pattern2_2, pattern3_2]:
|
| 2081 |
+
if input_component is not None and hasattr(input_component, 'change'):
|
| 2082 |
+
input_component.change(auto_update_chart_2, inputs=chart_inputs_2, outputs=[chart_output_2])
|
| 2083 |
|
| 2084 |
# --- 圖表二:導出圖表 ---
|
| 2085 |
download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
|