Update app.py
Browse files
app.py
CHANGED
|
@@ -1273,8 +1273,8 @@ def recommend_chart_settings(df):
|
|
| 1273 |
"""
|
| 1274 |
Gradio 應用程式:進階數據可視化工具
|
| 1275 |
作者:Gemini
|
| 1276 |
-
版本:5.
|
| 1277 |
-
|
| 1278 |
"""
|
| 1279 |
|
| 1280 |
# =========================================
|
|
@@ -1423,7 +1423,7 @@ def parse_data(text_data):
|
|
| 1423 |
except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
|
| 1424 |
|
| 1425 |
def update_columns_as_radio(df):
|
| 1426 |
-
"""修改:更新列選擇為 Radio
|
| 1427 |
no_data_choices = [NO_DATA_STR]
|
| 1428 |
no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
|
| 1429 |
|
|
@@ -1431,7 +1431,7 @@ def update_columns_as_radio(df):
|
|
| 1431 |
# 返回空的 Radio 更新
|
| 1432 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1433 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1434 |
-
return no_data_update,
|
| 1435 |
|
| 1436 |
try:
|
| 1437 |
columns = df.columns.tolist()
|
|
@@ -1439,27 +1439,28 @@ def update_columns_as_radio(df):
|
|
| 1439 |
if not valid_columns: # 如果沒有有效列
|
| 1440 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1441 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1442 |
-
return no_data_update,
|
| 1443 |
|
| 1444 |
x_default = valid_columns[0]
|
| 1445 |
-
# Y
|
| 1446 |
-
y_default =
|
| 1447 |
|
| 1448 |
-
# 為 group
|
|
|
|
| 1449 |
group_choices = [NONE_STR] + valid_columns
|
| 1450 |
size_choices = [NONE_STR] + valid_columns
|
| 1451 |
|
| 1452 |
# 返回 Radio 更新對象
|
| 1453 |
-
# 注意:如果列過多,Radio 會很長,但這是為了測試功能性
|
| 1454 |
return (gr.Radio(choices=valid_columns, value=x_default, label="X軸 / 類別"),
|
| 1455 |
-
gr.Radio(choices=
|
| 1456 |
gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
|
| 1457 |
gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
|
| 1458 |
except Exception as e:
|
| 1459 |
print(f"更新列選項 (Radio) 時出錯: {e}")
|
| 1460 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1461 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1462 |
-
|
|
|
|
| 1463 |
|
| 1464 |
|
| 1465 |
# =========================================
|
|
@@ -1475,10 +1476,9 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1475 |
if isinstance(df, pd.DataFrame):
|
| 1476 |
print(f" - df empty: {df.empty}", file=sys.stderr)
|
| 1477 |
print(f" - df shape: {df.shape}", file=sys.stderr)
|
| 1478 |
-
# print(f" - df head:\n{df.head()}", file=sys.stderr) # 打印頭部數據幫助調試
|
| 1479 |
print(f" - chart_type: {chart_type}", file=sys.stderr)
|
| 1480 |
print(f" - x_column: {x_column}", file=sys.stderr)
|
| 1481 |
-
print(f" - y_column: {y_column}", file=sys.stderr)
|
| 1482 |
print(f" - group_column: {group_column}", file=sys.stderr)
|
| 1483 |
print(f" - size_column: {size_column}", file=sys.stderr)
|
| 1484 |
print(f" - agg_func_name: {agg_func_name}", file=sys.stderr)
|
|
@@ -1492,22 +1492,26 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1492 |
show_legend = True if show_legend_str == "是" else False
|
| 1493 |
|
| 1494 |
# --- 1. 輸入驗證 (更嚴格) ---
|
| 1495 |
-
if df is None or not isinstance(df, pd.DataFrame) or df.empty:
|
| 1496 |
raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
|
| 1497 |
if not chart_type: raise ValueError("請選擇圖表類型。")
|
| 1498 |
if not agg_func_name: raise ValueError("請選擇聚合函數。")
|
| 1499 |
if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
|
| 1500 |
|
| 1501 |
-
# 檢查列是否存在
|
| 1502 |
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1503 |
|
| 1504 |
-
#
|
|
|
|
|
|
|
|
|
|
| 1505 |
y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
|
| 1506 |
if y_needed:
|
| 1507 |
-
if not
|
| 1508 |
-
|
|
|
|
|
|
|
| 1509 |
else:
|
| 1510 |
-
|
| 1511 |
|
| 1512 |
# 處理可選列
|
| 1513 |
group_col = None if group_column == NONE_STR or not group_column else group_column
|
|
@@ -1520,44 +1524,44 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1520 |
df_processed = df.copy()
|
| 1521 |
|
| 1522 |
# --- 2. 數據類型轉換與準備 ---
|
| 1523 |
-
# (與 V4 相同)
|
| 1524 |
df_processed[x_column] = df_processed[x_column].astype(str)
|
| 1525 |
if group_col: df_processed[group_col] = df_processed[group_col].astype(str)
|
| 1526 |
-
if
|
| 1527 |
-
try: df_processed[
|
| 1528 |
-
except Exception as e: print(f"警告:轉換 Y 軸列 '{
|
| 1529 |
if size_col:
|
| 1530 |
try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
|
| 1531 |
except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
|
| 1532 |
|
| 1533 |
# --- 3. 數據聚合 (如果需要) ---
|
| 1534 |
-
# (與 V4 相同)
|
| 1535 |
needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
|
| 1536 |
agg_df = None
|
| 1537 |
-
y_col_agg =
|
| 1538 |
if needs_aggregation:
|
| 1539 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
| 1540 |
invalid_grouping_cols = [col for col in grouping_cols if col not in df_processed.columns]
|
| 1541 |
if invalid_grouping_cols: raise ValueError(f"以下分組/X軸列不在數據中: {', '.join(invalid_grouping_cols)}")
|
|
|
|
| 1542 |
if agg_func_name == "計數":
|
| 1543 |
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
|
| 1544 |
-
y_col_agg = '__count__'
|
| 1545 |
else:
|
| 1546 |
agg_func_pd = agg_function_map(agg_func_name)
|
| 1547 |
-
if not
|
| 1548 |
-
if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[
|
| 1549 |
-
try: df_processed[
|
| 1550 |
-
except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{
|
| 1551 |
try:
|
| 1552 |
-
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[
|
| 1553 |
-
y_col_agg
|
| 1554 |
except Exception as agg_e: raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
|
| 1555 |
else:
|
| 1556 |
agg_df = df_processed
|
| 1557 |
-
y_col_agg =
|
| 1558 |
|
| 1559 |
if agg_df is None or agg_df.empty: raise ValueError("數據聚合後沒有產生有效結果。")
|
| 1560 |
required_cols_for_plot = [x_column]
|
|
|
|
| 1561 |
if y_col_agg: required_cols_for_plot.append(y_col_agg)
|
| 1562 |
if group_col: required_cols_for_plot.append(group_col)
|
| 1563 |
if size_col: required_cols_for_plot.append(size_col)
|
|
@@ -1570,89 +1574,98 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1570 |
# --- 5. 創建圖表 (核心邏輯) ---
|
| 1571 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1572 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
|
|
|
|
|
|
| 1573 |
effective_y = y_col_agg if y_needed or agg_func_name == "計數" else None
|
| 1574 |
|
| 1575 |
-
# --- (
|
| 1576 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1577 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1578 |
-
fig = px.bar(
|
| 1579 |
elif chart_type == "堆疊長條圖":
|
| 1580 |
if not effective_y: raise ValueError("堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1581 |
-
fig = px.bar(
|
| 1582 |
elif chart_type == "百分比堆疊長條圖":
|
| 1583 |
if not effective_y: raise ValueError("百分比堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1584 |
-
fig = px.bar(
|
| 1585 |
fig.update_layout(yaxis_title="百分比 (%)")
|
| 1586 |
elif chart_type == "群組長條圖":
|
| 1587 |
if not effective_y: raise ValueError("群組長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1588 |
-
fig = px.bar(
|
| 1589 |
elif chart_type == "水平長條圖":
|
| 1590 |
if not effective_y: raise ValueError("水平長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1591 |
-
fig = px.bar(
|
| 1592 |
elif chart_type == "折線圖":
|
| 1593 |
if not effective_y: raise ValueError("折線圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1594 |
-
fig = px.line(
|
| 1595 |
elif chart_type == "多重折線圖":
|
| 1596 |
if not effective_y: raise ValueError("多重折線圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1597 |
-
fig = px.line(
|
| 1598 |
elif chart_type == "階梯折線圖":
|
| 1599 |
if not effective_y: raise ValueError("階梯折線圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1600 |
-
fig = px.line(
|
| 1601 |
elif chart_type == "區域圖":
|
| 1602 |
if not effective_y: raise ValueError("區域圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1603 |
-
fig = px.area(
|
| 1604 |
elif chart_type == "堆疊區域圖":
|
| 1605 |
if not effective_y: raise ValueError("堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1606 |
-
fig = px.area(
|
| 1607 |
elif chart_type == "百分比堆疊區域圖":
|
| 1608 |
if not effective_y: raise ValueError("百分比堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1609 |
-
fig = px.area(
|
| 1610 |
fig.update_layout(yaxis_title="百分比 (%)")
|
| 1611 |
-
elif chart_type == "圓餅圖":
|
| 1612 |
-
if not effective_y: raise ValueError("圓餅圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1613 |
-
if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
|
| 1614 |
-
fig = px.pie(agg_df, names=x_column, values=effective_y, **fig_params)
|
| 1615 |
-
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])]))
|
| 1616 |
-
elif chart_type == "環形圖":
|
| 1617 |
-
if not effective_y: raise ValueError("環形圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1618 |
-
if group_col: print("警告:環形圖不支持分組列,已忽略。")
|
| 1619 |
-
fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
|
| 1620 |
-
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])]))
|
| 1621 |
elif chart_type == "散點圖":
|
| 1622 |
-
if not
|
| 1623 |
-
fig = px.scatter(
|
| 1624 |
elif chart_type == "氣泡圖":
|
| 1625 |
-
if not
|
| 1626 |
if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
|
| 1627 |
if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
|
| 1628 |
-
fig = px.scatter(
|
| 1629 |
elif chart_type == "直方圖":
|
| 1630 |
if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
|
| 1631 |
-
fig = px.histogram(
|
| 1632 |
elif chart_type == "箱型圖":
|
| 1633 |
-
if not
|
| 1634 |
-
if not pd.api.types.is_numeric_dtype(agg_df[
|
| 1635 |
-
fig = px.box(
|
| 1636 |
-
if not group_col: fig = px.box(
|
| 1637 |
elif chart_type == "小提琴圖":
|
| 1638 |
-
if not
|
| 1639 |
-
if not pd.api.types.is_numeric_dtype(agg_df[
|
| 1640 |
-
fig = px.violin(
|
| 1641 |
-
if not group_col: fig = px.violin(
|
| 1642 |
-
elif chart_type == "熱力圖":
|
| 1643 |
-
if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1644 |
-
if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
|
| 1645 |
-
try:
|
| 1646 |
-
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"熱力圖的值列 '{effective_y}' 必須是數值類型。")
|
| 1647 |
-
pivot_df = pd.pivot_table(agg_df, values=effective_y, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
|
| 1648 |
-
fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params);
|
| 1649 |
-
fig.update_layout(coloraxis_showscale=True)
|
| 1650 |
-
except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
|
| 1651 |
elif chart_type == "樹狀圖":
|
| 1652 |
if not effective_y: raise ValueError("樹狀圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1653 |
path = [group_col, x_column] if group_col else [x_column]
|
| 1654 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"樹狀圖的值列 '{effective_y}' 必須是數值類型。")
|
| 1655 |
-
fig = px.treemap(
|
| 1656 |
elif chart_type == "雷達圖":
|
| 1657 |
if not effective_y: raise ValueError("雷達圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1658 |
fig = go.Figure()
|
|
@@ -1675,9 +1688,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1675 |
elif chart_type == "極座標圖":
|
| 1676 |
if not effective_y: raise ValueError("極座標圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1677 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
|
| 1678 |
-
fig = px.bar_polar(
|
| 1679 |
elif chart_type == "甘特圖":
|
| 1680 |
-
start_col_gantt =
|
|
|
|
|
|
|
| 1681 |
if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1682 |
try:
|
| 1683 |
df_gantt = df.copy()
|
|
@@ -1694,7 +1709,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1694 |
else:
|
| 1695 |
print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
|
| 1696 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1697 |
-
fig = px.bar(
|
| 1698 |
# --- (繪圖邏輯結束) ---
|
| 1699 |
|
| 1700 |
|
|
@@ -1854,13 +1869,13 @@ CUSTOM_CSS = """
|
|
| 1854 |
# =========================================
|
| 1855 |
# == Gradio UI 介面定義 (Gradio UI Definition) ==
|
| 1856 |
# =========================================
|
| 1857 |
-
with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V5", theme=gr.themes.Soft()) as demo:
|
| 1858 |
|
| 1859 |
# --- 應用程式標頭 ---
|
| 1860 |
gr.HTML("""
|
| 1861 |
<div class="app-header">
|
| 1862 |
-
<h1 class="app-title">📊 進階數據可視化工具 V5</h1>
|
| 1863 |
-
<p class="app-subtitle">上傳或貼上數據,創建專業圖表 (極簡化測試版)</p>
|
| 1864 |
</div>
|
| 1865 |
""")
|
| 1866 |
|
|
|
|
| 1273 |
"""
|
| 1274 |
Gradio 應用程式:進階數據可視化工具
|
| 1275 |
作者:Gemini
|
| 1276 |
+
版本:5.1 (修正 TypeError + Y 軸加入無選項)
|
| 1277 |
+
描述:包含所有功能的完整程式碼,修正繪圖函數錯誤並改進 Y 軸選項。
|
| 1278 |
"""
|
| 1279 |
|
| 1280 |
# =========================================
|
|
|
|
| 1423 |
except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
|
| 1424 |
|
| 1425 |
def update_columns_as_radio(df):
|
| 1426 |
+
"""修改:更新列選擇為 Radio 選項,並為 Y 軸添加 '無'"""
|
| 1427 |
no_data_choices = [NO_DATA_STR]
|
| 1428 |
no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
|
| 1429 |
|
|
|
|
| 1431 |
# 返回空的 Radio 更新
|
| 1432 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1433 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1434 |
+
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none # Y 軸包含 "無"
|
| 1435 |
|
| 1436 |
try:
|
| 1437 |
columns = df.columns.tolist()
|
|
|
|
| 1439 |
if not valid_columns: # 如果沒有有效列
|
| 1440 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1441 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1442 |
+
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1443 |
|
| 1444 |
x_default = valid_columns[0]
|
| 1445 |
+
# Y 軸預設值:如果只有一列,預設選 '無';否則選第二列
|
| 1446 |
+
y_default = NONE_STR if len(valid_columns) <= 1 else valid_columns[1]
|
| 1447 |
|
| 1448 |
+
# 為 Y, group, size 添加 "無" 選項
|
| 1449 |
+
y_choices = [NONE_STR] + valid_columns # <--- 修改:Y 軸加入 "無"
|
| 1450 |
group_choices = [NONE_STR] + valid_columns
|
| 1451 |
size_choices = [NONE_STR] + valid_columns
|
| 1452 |
|
| 1453 |
# 返回 Radio 更新對象
|
|
|
|
| 1454 |
return (gr.Radio(choices=valid_columns, value=x_default, label="X軸 / 類別"),
|
| 1455 |
+
gr.Radio(choices=y_choices, value=y_default, label="Y軸 / 數值"), # <--- 修改:使用 y_choices
|
| 1456 |
gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
|
| 1457 |
gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
|
| 1458 |
except Exception as e:
|
| 1459 |
print(f"更新列選項 (Radio) 時出錯: {e}")
|
| 1460 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1461 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1462 |
+
# 返回 Y 軸包含 "無" 的更新
|
| 1463 |
+
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1464 |
|
| 1465 |
|
| 1466 |
# =========================================
|
|
|
|
| 1476 |
if isinstance(df, pd.DataFrame):
|
| 1477 |
print(f" - df empty: {df.empty}", file=sys.stderr)
|
| 1478 |
print(f" - df shape: {df.shape}", file=sys.stderr)
|
|
|
|
| 1479 |
print(f" - chart_type: {chart_type}", file=sys.stderr)
|
| 1480 |
print(f" - x_column: {x_column}", file=sys.stderr)
|
| 1481 |
+
print(f" - y_column: {y_column}", file=sys.stderr) # 檢查傳入的 y_column 值
|
| 1482 |
print(f" - group_column: {group_column}", file=sys.stderr)
|
| 1483 |
print(f" - size_column: {size_column}", file=sys.stderr)
|
| 1484 |
print(f" - agg_func_name: {agg_func_name}", file=sys.stderr)
|
|
|
|
| 1492 |
show_legend = True if show_legend_str == "是" else False
|
| 1493 |
|
| 1494 |
# --- 1. 輸入驗證 (更嚴格) ---
|
| 1495 |
+
if df is None or not isinstance(df, pd.DataFrame) or df.empty:
|
| 1496 |
raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
|
| 1497 |
if not chart_type: raise ValueError("請選擇圖表類型。")
|
| 1498 |
if not agg_func_name: raise ValueError("請選擇聚合函數。")
|
| 1499 |
if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
|
| 1500 |
|
|
|
|
| 1501 |
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1502 |
|
| 1503 |
+
# --- 修改:處理 Y 軸選擇為 "無" ---
|
| 1504 |
+
y_col_selected = None if y_column == NONE_STR or not y_column else y_column
|
| 1505 |
+
|
| 1506 |
+
# 判斷是否需要 Y 軸
|
| 1507 |
y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
|
| 1508 |
if y_needed:
|
| 1509 |
+
if not y_col_selected or y_col_selected == NO_DATA_STR:
|
| 1510 |
+
raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列 (不能是 '無')。")
|
| 1511 |
+
if y_col_selected not in df.columns:
|
| 1512 |
+
raise ValueError(f"Y 軸列 '{y_col_selected}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1513 |
else:
|
| 1514 |
+
y_col_selected = None # 如果不需要 Y 軸,明確設為 None
|
| 1515 |
|
| 1516 |
# 處理可選列
|
| 1517 |
group_col = None if group_column == NONE_STR or not group_column else group_column
|
|
|
|
| 1524 |
df_processed = df.copy()
|
| 1525 |
|
| 1526 |
# --- 2. 數據類型轉換與準備 ---
|
|
|
|
| 1527 |
df_processed[x_column] = df_processed[x_column].astype(str)
|
| 1528 |
if group_col: df_processed[group_col] = df_processed[group_col].astype(str)
|
| 1529 |
+
if y_col_selected: # 只在 Y 軸被選中時轉換
|
| 1530 |
+
try: df_processed[y_col_selected] = pd.to_numeric(df_processed[y_col_selected], errors='coerce')
|
| 1531 |
+
except Exception as e: print(f"警告:轉換 Y 軸列 '{y_col_selected}' 為數值時出錯: {e}")
|
| 1532 |
if size_col:
|
| 1533 |
try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
|
| 1534 |
except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
|
| 1535 |
|
| 1536 |
# --- 3. 數據聚合 (如果需要) ---
|
|
|
|
| 1537 |
needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
|
| 1538 |
agg_df = None
|
| 1539 |
+
y_col_agg = y_col_selected # 聚合時使用的 Y 軸列名
|
| 1540 |
if needs_aggregation:
|
| 1541 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
| 1542 |
invalid_grouping_cols = [col for col in grouping_cols if col not in df_processed.columns]
|
| 1543 |
if invalid_grouping_cols: raise ValueError(f"以下分組/X軸列不在數據中: {', '.join(invalid_grouping_cols)}")
|
| 1544 |
+
|
| 1545 |
if agg_func_name == "計數":
|
| 1546 |
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
|
| 1547 |
+
y_col_agg = '__count__' # 聚合結果列名
|
| 1548 |
else:
|
| 1549 |
agg_func_pd = agg_function_map(agg_func_name)
|
| 1550 |
+
if not y_col_agg: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。") # y_col_agg 此時就是 y_col_selected
|
| 1551 |
+
if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_col_agg]):
|
| 1552 |
+
try: df_processed[y_col_agg] = pd.to_numeric(df_processed[y_col_agg], errors='raise')
|
| 1553 |
+
except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_col_agg}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
|
| 1554 |
try:
|
| 1555 |
+
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_col_agg].agg(agg_func_pd).reset_index()
|
| 1556 |
+
# y_col_agg 保持為選擇的 Y 軸列名
|
| 1557 |
except Exception as agg_e: raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
|
| 1558 |
else:
|
| 1559 |
agg_df = df_processed
|
| 1560 |
+
y_col_agg = y_col_selected # 使用選擇的 Y 軸列名 (可能為 None)
|
| 1561 |
|
| 1562 |
if agg_df is None or agg_df.empty: raise ValueError("數據聚合後沒有產生有效結果。")
|
| 1563 |
required_cols_for_plot = [x_column]
|
| 1564 |
+
# 修正:只有在 y_col_agg 實際有值時才檢查
|
| 1565 |
if y_col_agg: required_cols_for_plot.append(y_col_agg)
|
| 1566 |
if group_col: required_cols_for_plot.append(group_col)
|
| 1567 |
if size_col: required_cols_for_plot.append(size_col)
|
|
|
|
| 1574 |
# --- 5. 創建圖表 (核心邏輯) ---
|
| 1575 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1576 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
| 1577 |
+
|
| 1578 |
+
# 修正:effective_y 應該基於 y_col_agg (聚合結果列 或 選擇的 Y 軸列)
|
| 1579 |
effective_y = y_col_agg if y_needed or agg_func_name == "計數" else None
|
| 1580 |
|
| 1581 |
+
# --- (繪圖邏輯開始) ---
|
| 1582 |
+
# --- 修正 px.pie 和 px.imshow 的調用 ---
|
| 1583 |
+
if chart_type == "圓餅圖":
|
| 1584 |
+
if not effective_y: raise ValueError("圓餅圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1585 |
+
if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
|
| 1586 |
+
# 修正:移除第一個 positional argument agg_df
|
| 1587 |
+
fig = px.pie(names=x_column, values=effective_y, **fig_params)
|
| 1588 |
+
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])]))
|
| 1589 |
+
elif chart_type == "環形圖":
|
| 1590 |
+
if not effective_y: raise ValueError("環形圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1591 |
+
if group_col: print("警告:環形圖不支持分組列,已忽略。")
|
| 1592 |
+
# 修正:移除第一個 positional argument agg_df
|
| 1593 |
+
fig = px.pie(names=x_column, values=effective_y, hole=0.4, **fig_params)
|
| 1594 |
+
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])]))
|
| 1595 |
+
elif chart_type == "熱力圖":
|
| 1596 |
+
if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1597 |
+
if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
|
| 1598 |
+
try:
|
| 1599 |
+
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"熱力圖的值列 '{effective_y}' 必須是數值類型。")
|
| 1600 |
+
pivot_df = pd.pivot_table(agg_df, values=effective_y, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
|
| 1601 |
+
# 修正:為 imshow 創建不含 data_frame 的參數字典
|
| 1602 |
+
fig_params_imshow = fig_params.copy()
|
| 1603 |
+
fig_params_imshow.pop('data_frame', None) # 移除 data_frame
|
| 1604 |
+
fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params_imshow); # 使用修改後的參數
|
| 1605 |
+
fig.update_layout(coloraxis_showscale=True)
|
| 1606 |
+
except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
|
| 1607 |
+
# --- 其他圖表類型 (與 V4/V5 邏輯相同,確保 effective_y 使用正確) ---
|
| 1608 |
+
elif chart_type == "長條圖":
|
| 1609 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1610 |
+
fig = px.bar(x=x_column, y=effective_y, color=group_col, **fig_params)
|
| 1611 |
elif chart_type == "堆疊長條圖":
|
| 1612 |
if not effective_y: raise ValueError("堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1613 |
+
fig = px.bar(x=x_column, y=effective_y, color=group_col, barmode='stack', **fig_params)
|
| 1614 |
elif chart_type == "百分比堆疊長條圖":
|
| 1615 |
if not effective_y: raise ValueError("百分比堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1616 |
+
fig = px.bar(x=x_column, y=effective_y, color=group_col, barmode='relative', text_auto='.1%', **fig_params)
|
| 1617 |
fig.update_layout(yaxis_title="百分比 (%)")
|
| 1618 |
elif chart_type == "群組長條圖":
|
| 1619 |
if not effective_y: raise ValueError("群組長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1620 |
+
fig = px.bar(x=x_column, y=effective_y, color=group_col, barmode='group', **fig_params)
|
| 1621 |
elif chart_type == "水平長條圖":
|
| 1622 |
if not effective_y: raise ValueError("水平長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1623 |
+
fig = px.bar(y=x_column, x=effective_y, color=group_col, orientation='h', **fig_params)
|
| 1624 |
elif chart_type == "折線圖":
|
| 1625 |
if not effective_y: raise ValueError("折線圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1626 |
+
fig = px.line(x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
|
| 1627 |
elif chart_type == "多重折線圖":
|
| 1628 |
if not effective_y: raise ValueError("多重折線圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1629 |
+
fig = px.line(x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
|
| 1630 |
elif chart_type == "階梯折線圖":
|
| 1631 |
if not effective_y: raise ValueError("階梯折線圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1632 |
+
fig = px.line(x=x_column, y=effective_y, color=group_col, line_shape='hv', **fig_params)
|
| 1633 |
elif chart_type == "區域圖":
|
| 1634 |
if not effective_y: raise ValueError("區域圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1635 |
+
fig = px.area(x=x_column, y=effective_y, color=group_col, **fig_params)
|
| 1636 |
elif chart_type == "堆疊區域圖":
|
| 1637 |
if not effective_y: raise ValueError("堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1638 |
+
fig = px.area(x=x_column, y=effective_y, color=group_col, groupnorm=None, **fig_params)
|
| 1639 |
elif chart_type == "百分比堆疊區域圖":
|
| 1640 |
if not effective_y: raise ValueError("百分比堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1641 |
+
fig = px.area(x=x_column, y=effective_y, color=group_col, groupnorm='percent', **fig_params)
|
| 1642 |
fig.update_layout(yaxis_title="百分比 (%)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1643 |
elif chart_type == "散點圖":
|
| 1644 |
+
if not y_col_selected: raise ValueError("散點圖需要選擇 Y 軸列。") # 使用 y_col_selected
|
| 1645 |
+
fig = px.scatter(x=x_column, y=y_col_selected, color=group_col, size=size_col, **fig_params)
|
| 1646 |
elif chart_type == "氣泡圖":
|
| 1647 |
+
if not y_col_selected: raise ValueError("氣泡圖需要選擇 Y 軸列。")
|
| 1648 |
if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
|
| 1649 |
if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
|
| 1650 |
+
fig = px.scatter(x=x_column, y=y_col_selected, color=group_col, size=size_col, size_max=60, **fig_params)
|
| 1651 |
elif chart_type == "直方圖":
|
| 1652 |
if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
|
| 1653 |
+
fig = px.histogram(x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
|
| 1654 |
elif chart_type == "箱型圖":
|
| 1655 |
+
if not y_col_selected: raise ValueError("箱型圖需要選擇 Y 軸列。")
|
| 1656 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_selected]): raise ValueError(f"箱型圖的 Y 軸列 '{y_col_selected}' 必須是數值類型。")
|
| 1657 |
+
fig = px.box(x=group_col, y=y_col_selected, color=group_col, **fig_params)
|
| 1658 |
+
if not group_col: fig = px.box(y=y_col_selected, **fig_params)
|
| 1659 |
elif chart_type == "小提琴圖":
|
| 1660 |
+
if not y_col_selected: raise ValueError("小提琴圖需要選擇 Y 軸列。")
|
| 1661 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_col_selected]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_col_selected}' 必須是數值類型。")
|
| 1662 |
+
fig = px.violin(x=group_col, y=y_col_selected, color=group_col, box=True, points="all", **fig_params)
|
| 1663 |
+
if not group_col: fig = px.violin(y=y_col_selected, box=True, points="all", **fig_params)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1664 |
elif chart_type == "樹狀圖":
|
| 1665 |
if not effective_y: raise ValueError("樹狀圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1666 |
path = [group_col, x_column] if group_col else [x_column]
|
| 1667 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"樹狀圖的值列 '{effective_y}' 必須是數值類型。")
|
| 1668 |
+
fig = px.treemap(path=path, values=effective_y, color=group_col if group_col else x_column, **fig_params)
|
| 1669 |
elif chart_type == "雷達圖":
|
| 1670 |
if not effective_y: raise ValueError("雷達圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1671 |
fig = go.Figure()
|
|
|
|
| 1688 |
elif chart_type == "極座標圖":
|
| 1689 |
if not effective_y: raise ValueError("極座標圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1690 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
|
| 1691 |
+
fig = px.bar_polar(r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1692 |
elif chart_type == "甘特圖":
|
| 1693 |
+
start_col_gantt = y_col_selected # Y 軸被用作開始列
|
| 1694 |
+
end_col_gantt = group_col # 分組列被用作結束列
|
| 1695 |
+
task_col_gantt = x_column # X 軸被用作任務列
|
| 1696 |
if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1697 |
try:
|
| 1698 |
df_gantt = df.copy()
|
|
|
|
| 1709 |
else:
|
| 1710 |
print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
|
| 1711 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1712 |
+
fig = px.bar(x=x_column, y=effective_y, color=group_col, **fig_params)
|
| 1713 |
# --- (繪圖邏輯結束) ---
|
| 1714 |
|
| 1715 |
|
|
|
|
| 1869 |
# =========================================
|
| 1870 |
# == Gradio UI 介面定義 (Gradio UI Definition) ==
|
| 1871 |
# =========================================
|
| 1872 |
+
with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V5.1", theme=gr.themes.Soft()) as demo:
|
| 1873 |
|
| 1874 |
# --- 應用程式標頭 ---
|
| 1875 |
gr.HTML("""
|
| 1876 |
<div class="app-header">
|
| 1877 |
+
<h1 class="app-title">📊 進階數據可視化工具 V5.1</h1>
|
| 1878 |
+
<p class="app-subtitle">上傳或貼上數據,創建專業圖表 (極簡化測試版 - 修正)</p>
|
| 1879 |
</div>
|
| 1880 |
""")
|
| 1881 |
|