Update app.py
Browse files
app.py
CHANGED
|
@@ -1236,8 +1236,8 @@ def recommend_chart_settings(df):
|
|
| 1236 |
"""
|
| 1237 |
Gradio 應用程式:進階數據可視化工具
|
| 1238 |
作者:Gemini
|
| 1239 |
-
版本:5.
|
| 1240 |
-
|
| 1241 |
"""
|
| 1242 |
|
| 1243 |
# =========================================
|
|
@@ -1251,14 +1251,12 @@ import plotly.graph_objects as go
|
|
| 1251 |
import io
|
| 1252 |
import base64
|
| 1253 |
from PIL import Image
|
| 1254 |
-
# import matplotlib.pyplot as plt # Matplotlib/Seaborn 在此版本中未使用,暫時註解
|
| 1255 |
-
# import seaborn as sns # Matplotlib/Seaborn 在此版本中未使用,暫時註解
|
| 1256 |
from plotly.subplots import make_subplots
|
| 1257 |
import re
|
| 1258 |
import json
|
| 1259 |
import colorsys
|
| 1260 |
import traceback # 用於更詳細的錯誤追蹤
|
| 1261 |
-
import sys # 用於打印調試信息
|
| 1262 |
|
| 1263 |
# =========================================
|
| 1264 |
# == 常數定義 (Constants) ==
|
|
@@ -1281,7 +1279,7 @@ NONE_STR = "無" # 代表 '無' 選項的值
|
|
| 1281 |
# =========================================
|
| 1282 |
# == 輔助函數 (Helper Functions) ==
|
| 1283 |
# =========================================
|
| 1284 |
-
# --- 顏色處理相關
|
| 1285 |
COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
|
| 1286 |
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>"""
|
| 1287 |
COPY_SCRIPT = """
|
|
@@ -1351,7 +1349,6 @@ def update_patterns(*patterns_input):
|
|
| 1351 |
# == 數據處理函數 (Data Processing Functions) ==
|
| 1352 |
# =========================================
|
| 1353 |
def process_upload(file):
|
| 1354 |
-
# (與 V4 相同)
|
| 1355 |
if file is None: return None, "❌ 未上傳任何文件。"
|
| 1356 |
try:
|
| 1357 |
file_path = file.name; file_type = file_path.split('.')[-1].lower()
|
|
@@ -1370,7 +1367,6 @@ def process_upload(file):
|
|
| 1370 |
except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
|
| 1371 |
|
| 1372 |
def parse_data(text_data):
|
| 1373 |
-
# (與 V4 相同)
|
| 1374 |
if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
|
| 1375 |
try:
|
| 1376 |
data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
|
|
@@ -1386,43 +1382,38 @@ def parse_data(text_data):
|
|
| 1386 |
except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
|
| 1387 |
|
| 1388 |
def update_columns_as_radio(df):
|
| 1389 |
-
"""
|
| 1390 |
no_data_choices = [NO_DATA_STR]
|
| 1391 |
no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
|
| 1392 |
|
| 1393 |
if df is None or df.empty:
|
| 1394 |
-
# 返回空的 Radio 更新
|
| 1395 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1396 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1397 |
-
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1398 |
|
| 1399 |
try:
|
| 1400 |
columns = df.columns.tolist()
|
| 1401 |
-
valid_columns = [str(col) for col in columns if col is not None and str(col) != ""]
|
| 1402 |
-
if not valid_columns:
|
| 1403 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1404 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1405 |
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1406 |
|
| 1407 |
x_default = valid_columns[0]
|
| 1408 |
-
# Y 軸預設值:如果只有一列,預設選 '無';否則選第二列
|
| 1409 |
y_default = NONE_STR if len(valid_columns) <= 1 else valid_columns[1]
|
| 1410 |
|
| 1411 |
-
|
| 1412 |
-
y_choices = [NONE_STR] + valid_columns # <--- 修改:Y 軸加入 "無"
|
| 1413 |
group_choices = [NONE_STR] + valid_columns
|
| 1414 |
size_choices = [NONE_STR] + valid_columns
|
| 1415 |
|
| 1416 |
-
# 返回 Radio 更新對象
|
| 1417 |
return (gr.Radio(choices=valid_columns, value=x_default, label="X�� / 類別"),
|
| 1418 |
-
gr.Radio(choices=y_choices, value=y_default, label="Y軸 / 數值"),
|
| 1419 |
gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
|
| 1420 |
gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
|
| 1421 |
except Exception as e:
|
| 1422 |
print(f"更新列選項 (Radio) 時出錯: {e}")
|
| 1423 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1424 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1425 |
-
# 返回 Y 軸包含 "無" 的更新
|
| 1426 |
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1427 |
|
| 1428 |
|
|
@@ -1433,27 +1424,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1433 |
color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
|
| 1434 |
show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
|
| 1435 |
"""
|
| 1436 |
-
根據用戶選擇創建 Plotly 圖表 (已加入 Null/空白 過濾)。
|
| 1437 |
-
|
| 1438 |
-
Args:
|
| 1439 |
-
df (pd.DataFrame): 輸入數據。
|
| 1440 |
-
chart_type (str): 圖表類型。
|
| 1441 |
-
x_column (str): X軸或類別列。
|
| 1442 |
-
y_column (str): Y軸或數值列 (可能為 None)。
|
| 1443 |
-
group_column (str, optional): 分組列 (可能為 "無" 或 None)。 Defaults to None.
|
| 1444 |
-
size_column (str, optional): 大小列 (可能為 "無" 或 None)。 Defaults to None.
|
| 1445 |
-
color_scheme_name (str, optional): 顏色方案名稱。 Defaults to "預設 (Plotly)".
|
| 1446 |
-
patterns (list, optional): 圖案列表。 Defaults to [].
|
| 1447 |
-
title (str, optional): 圖表標題。 Defaults to "".
|
| 1448 |
-
width (int, optional): 圖表寬度。 Defaults to 800.
|
| 1449 |
-
height (int, optional): 圖表高度。 Defaults to 500.
|
| 1450 |
-
show_grid_str (str, optional): 是否顯示網格 ("是" 或 "否")。 Defaults to "是".
|
| 1451 |
-
show_legend_str (str, optional): 是否顯示圖例 ("是" 或 "否")。 Defaults to "是".
|
| 1452 |
-
agg_func_name (str, optional): 聚合函數名稱。 Defaults to "計數".
|
| 1453 |
-
custom_colors_dict (dict, optional): 自定義顏色映射。 Defaults to {}.
|
| 1454 |
-
|
| 1455 |
-
Returns:
|
| 1456 |
-
go.Figure: Plotly 圖表對象。
|
| 1457 |
"""
|
| 1458 |
# --- 添加調試信息 ---
|
| 1459 |
print("-" * 20, file=sys.stderr)
|
|
@@ -1484,23 +1455,22 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1484 |
raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
|
| 1485 |
if not chart_type: raise ValueError("請選擇圖表類型。")
|
| 1486 |
if not agg_func_name: raise ValueError("請選擇聚合函數。")
|
| 1487 |
-
# NO_DATA_STR = "-- 無數據 --" # 確保此變數已定義或直接使用字符串
|
| 1488 |
if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
|
| 1489 |
|
| 1490 |
# 檢查列是否存在
|
| 1491 |
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1492 |
|
| 1493 |
-
# 判斷是否需要 Y 軸
|
|
|
|
| 1494 |
y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
|
| 1495 |
|
| 1496 |
if y_needed:
|
| 1497 |
-
if not
|
| 1498 |
-
if
|
| 1499 |
-
else:
|
| 1500 |
-
|
| 1501 |
|
| 1502 |
# 處理可選列 (從 Radio 傳來的值可能是 NONE_STR)
|
| 1503 |
-
# NONE_STR = "無" # 確保此變數已定義或直接使用字符串
|
| 1504 |
group_col = None if group_column == NONE_STR or not group_column else group_column
|
| 1505 |
size_col = None if size_column == NONE_STR or not size_column else size_column
|
| 1506 |
|
|
@@ -1513,12 +1483,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1513 |
|
| 1514 |
# --- NEW: 過濾 Null/空白值 ---
|
| 1515 |
columns_to_filter = [x_column]
|
| 1516 |
-
if y_needed and
|
| 1517 |
-
columns_to_filter.append(
|
| 1518 |
if group_col:
|
| 1519 |
columns_to_filter.append(group_col)
|
| 1520 |
|
| 1521 |
-
# 移除在關鍵列中有 Null (NaN, None) 值的行
|
| 1522 |
valid_columns_to_filter = [col for col in columns_to_filter if col in df_processed.columns]
|
| 1523 |
if valid_columns_to_filter:
|
| 1524 |
original_rows = len(df_processed)
|
|
@@ -1527,11 +1496,9 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1527 |
else:
|
| 1528 |
print("警告: 沒有有效的列用於 Null 值過濾。", file=sys.stderr)
|
| 1529 |
|
| 1530 |
-
# 對於 X 軸和分組列,額外移除空白字符串 (轉換為字符串後判斷)
|
| 1531 |
if x_column in df_processed.columns:
|
| 1532 |
try:
|
| 1533 |
original_rows = len(df_processed)
|
| 1534 |
-
# 僅移除完全是空白或空字符串的行
|
| 1535 |
df_processed = df_processed[df_processed[x_column].astype(str).str.strip() != '']
|
| 1536 |
print(f"移除 X 軸 '{x_column}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
|
| 1537 |
except Exception as e:
|
|
@@ -1545,22 +1512,20 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1545 |
except Exception as e:
|
| 1546 |
print(f"警告: 過濾分組列 '{group_col}' 空白字符串時出錯: {e}", file=sys.stderr)
|
| 1547 |
|
| 1548 |
-
# 檢查過濾後是否還有數據
|
| 1549 |
if df_processed.empty:
|
| 1550 |
raise ValueError("過濾掉 Null 或空白值後,沒有剩餘數據可供繪圖。")
|
| 1551 |
# --- END NEW ---
|
| 1552 |
|
| 1553 |
|
| 1554 |
# --- 2. 數據類型轉換與準備 ---
|
| 1555 |
-
# 將 X 軸和分組列強制轉為字符串,以便正確分組
|
| 1556 |
df_processed[x_column] = df_processed[x_column].astype(str)
|
| 1557 |
if group_col:
|
| 1558 |
df_processed[group_col] = df_processed[group_col].astype(str)
|
| 1559 |
|
| 1560 |
-
#
|
| 1561 |
-
if
|
| 1562 |
-
try: df_processed[
|
| 1563 |
-
except Exception as e: print(f"警告:轉換 Y 軸列 '{
|
| 1564 |
if size_col:
|
| 1565 |
try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
|
| 1566 |
except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
|
|
@@ -1569,49 +1534,48 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1569 |
# --- 3. 數據聚合 (如果需要) ---
|
| 1570 |
needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
|
| 1571 |
agg_df = None
|
| 1572 |
-
y_col_agg =
|
| 1573 |
if needs_aggregation:
|
| 1574 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
| 1575 |
-
# 檢查分組列是否有效 (已在驗證部分完成)
|
| 1576 |
|
| 1577 |
if agg_func_name == "計數":
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
agg_df = agg_df.reset_index(name='__count__')
|
| 1581 |
-
y_col_agg = '__count__' # 使用新生成的計數列
|
| 1582 |
else:
|
| 1583 |
agg_func_pd = agg_function_map(agg_func_name)
|
| 1584 |
-
if not
|
| 1585 |
# 確保 Y 軸是數值類型 (除非 first/last)
|
| 1586 |
-
if agg_func_pd not in ['first', 'last']
|
| 1587 |
-
|
| 1588 |
-
|
| 1589 |
-
|
|
|
|
|
|
|
|
|
|
| 1590 |
|
| 1591 |
try:
|
| 1592 |
# 執行聚合
|
| 1593 |
-
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[
|
| 1594 |
agg_df = agg_df.reset_index()
|
| 1595 |
-
y_col_agg =
|
| 1596 |
except Exception as agg_e:
|
| 1597 |
raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
|
| 1598 |
else:
|
| 1599 |
-
# 不需要聚合,直接使用處理過的數據
|
| 1600 |
agg_df = df_processed
|
| 1601 |
-
y_col_agg =
|
| 1602 |
|
| 1603 |
-
# 再次檢查聚合後的 DataFrame
|
| 1604 |
if agg_df is None or agg_df.empty:
|
| 1605 |
-
raise ValueError("
|
|
|
|
| 1606 |
# 確保繪圖所需的列存在於 agg_df 中
|
| 1607 |
required_cols_for_plot = [x_column]
|
| 1608 |
-
#
|
| 1609 |
-
if y_col_agg: required_cols_for_plot.append(y_col_agg)
|
| 1610 |
if group_col: required_cols_for_plot.append(group_col)
|
| 1611 |
if size_col: required_cols_for_plot.append(size_col)
|
| 1612 |
missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
|
| 1613 |
if missing_cols:
|
| 1614 |
-
raise ValueError(f"
|
| 1615 |
|
| 1616 |
|
| 1617 |
# --- 4. 獲取顏色方案 ---
|
|
@@ -1620,10 +1584,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1620 |
# --- 5. 創建圖表 (核心邏輯) ---
|
| 1621 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1622 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
| 1623 |
-
|
| 1624 |
-
effective_y = y_col_agg # 使用聚合後確定的 Y 軸列名
|
| 1625 |
|
| 1626 |
# --- (繪圖邏輯開始) ---
|
|
|
|
|
|
|
| 1627 |
if chart_type == "長條圖":
|
| 1628 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1629 |
fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
|
|
@@ -1670,29 +1635,26 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1670 |
fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
|
| 1671 |
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])]))
|
| 1672 |
elif chart_type == "散點圖":
|
| 1673 |
-
|
| 1674 |
-
|
| 1675 |
-
fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
|
| 1676 |
elif chart_type == "氣泡圖":
|
| 1677 |
-
if not
|
| 1678 |
if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
|
| 1679 |
if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
|
| 1680 |
-
fig = px.scatter(agg_df, x=x_column, y=
|
| 1681 |
elif chart_type == "直方圖":
|
| 1682 |
-
# 直方圖使用原始數據的 x_column
|
| 1683 |
if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
|
| 1684 |
fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
|
| 1685 |
elif chart_type == "箱型圖":
|
| 1686 |
-
|
| 1687 |
-
if not
|
| 1688 |
-
|
| 1689 |
-
fig = px.box(agg_df,
|
| 1690 |
-
if not group_col: fig = px.box(agg_df, y=y_column, **fig_params)
|
| 1691 |
elif chart_type == "小提琴圖":
|
| 1692 |
-
if not
|
| 1693 |
-
if not pd.api.types.is_numeric_dtype(agg_df[
|
| 1694 |
-
fig = px.violin(agg_df, x=group_col, y=
|
| 1695 |
-
if not group_col: fig = px.violin(agg_df, y=
|
| 1696 |
elif chart_type == "熱力圖":
|
| 1697 |
if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1698 |
if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
|
|
@@ -1731,7 +1693,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1731 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
|
| 1732 |
fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1733 |
elif chart_type == "甘特圖":
|
| 1734 |
-
start_col_gantt =
|
| 1735 |
if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1736 |
try:
|
| 1737 |
df_gantt = df.copy() # 使用原始 df
|
|
@@ -1791,18 +1753,18 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1791 |
if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
|
| 1792 |
elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
|
| 1793 |
elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
|
| 1794 |
-
elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=
|
| 1795 |
else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
|
| 1796 |
|
| 1797 |
-
|
| 1798 |
-
|
| 1799 |
-
|
| 1800 |
-
|
| 1801 |
-
|
| 1802 |
-
|
| 1803 |
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
|
| 1807 |
|
| 1808 |
# =========================================
|
|
@@ -1810,52 +1772,52 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1810 |
# =========================================
|
| 1811 |
# (與 V4 相同)
|
| 1812 |
def export_data(df, format_type):
|
| 1813 |
-
|
| 1814 |
-
|
| 1815 |
-
|
| 1816 |
-
|
| 1817 |
-
|
| 1818 |
-
|
| 1819 |
-
|
| 1820 |
-
|
| 1821 |
|
| 1822 |
def download_figure(fig, format_type="PNG"):
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
-
|
| 1826 |
-
|
| 1827 |
-
|
| 1828 |
-
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
| 1832 |
-
|
| 1833 |
-
|
| 1834 |
|
| 1835 |
# =========================================
|
| 1836 |
# == 智能推薦函數 (Recommendation Function) ==
|
| 1837 |
# =========================================
|
| 1838 |
# (與 V4 相同)
|
| 1839 |
def recommend_chart_settings(df):
|
| 1840 |
-
|
| 1841 |
-
|
| 1842 |
-
|
| 1843 |
-
|
| 1844 |
-
|
| 1845 |
-
|
| 1846 |
-
|
| 1847 |
-
|
| 1848 |
-
|
| 1849 |
-
|
| 1850 |
-
|
| 1851 |
-
|
| 1852 |
-
|
| 1853 |
-
|
| 1854 |
-
|
| 1855 |
-
|
| 1856 |
-
|
| 1857 |
-
|
| 1858 |
-
|
| 1859 |
|
| 1860 |
# =========================================
|
| 1861 |
# == CSS 樣式 (CSS Styling) ==
|
|
|
|
| 1236 |
"""
|
| 1237 |
Gradio 應用程式:進階數據可視化工具
|
| 1238 |
作者:Gemini
|
| 1239 |
+
版本:5.2 (完整修正版 - 清理格式, 確保一致性)
|
| 1240 |
+
描述:包含所有功能的完整程式碼,修正導入、CSS、格式問題。
|
| 1241 |
"""
|
| 1242 |
|
| 1243 |
# =========================================
|
|
|
|
| 1251 |
import io
|
| 1252 |
import base64
|
| 1253 |
from PIL import Image
|
|
|
|
|
|
|
| 1254 |
from plotly.subplots import make_subplots
|
| 1255 |
import re
|
| 1256 |
import json
|
| 1257 |
import colorsys
|
| 1258 |
import traceback # 用於更詳細的錯誤追蹤
|
| 1259 |
+
import sys # 用於打印調試信息 - 已加入
|
| 1260 |
|
| 1261 |
# =========================================
|
| 1262 |
# == 常數定義 (Constants) ==
|
|
|
|
| 1279 |
# =========================================
|
| 1280 |
# == 輔助函數 (Helper Functions) ==
|
| 1281 |
# =========================================
|
| 1282 |
+
# --- 顏色處理相關 ---
|
| 1283 |
COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
|
| 1284 |
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>"""
|
| 1285 |
COPY_SCRIPT = """
|
|
|
|
| 1349 |
# == 數據處理函數 (Data Processing Functions) ==
|
| 1350 |
# =========================================
|
| 1351 |
def process_upload(file):
|
|
|
|
| 1352 |
if file is None: return None, "❌ 未上傳任何文件。"
|
| 1353 |
try:
|
| 1354 |
file_path = file.name; file_type = file_path.split('.')[-1].lower()
|
|
|
|
| 1367 |
except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
|
| 1368 |
|
| 1369 |
def parse_data(text_data):
|
|
|
|
| 1370 |
if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
|
| 1371 |
try:
|
| 1372 |
data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
|
|
|
|
| 1382 |
except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
|
| 1383 |
|
| 1384 |
def update_columns_as_radio(df):
|
| 1385 |
+
"""更新列選擇為 Radio 選項,並為 Y/Group/Size 軸添加 '無'"""
|
| 1386 |
no_data_choices = [NO_DATA_STR]
|
| 1387 |
no_data_choices_with_none = [NONE_STR, NO_DATA_STR]
|
| 1388 |
|
| 1389 |
if df is None or df.empty:
|
|
|
|
| 1390 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1391 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1392 |
+
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1393 |
|
| 1394 |
try:
|
| 1395 |
columns = df.columns.tolist()
|
| 1396 |
+
valid_columns = [str(col) for col in columns if col is not None and str(col) != ""]
|
| 1397 |
+
if not valid_columns:
|
| 1398 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1399 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
| 1400 |
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1401 |
|
| 1402 |
x_default = valid_columns[0]
|
|
|
|
| 1403 |
y_default = NONE_STR if len(valid_columns) <= 1 else valid_columns[1]
|
| 1404 |
|
| 1405 |
+
y_choices = [NONE_STR] + valid_columns
|
|
|
|
| 1406 |
group_choices = [NONE_STR] + valid_columns
|
| 1407 |
size_choices = [NONE_STR] + valid_columns
|
| 1408 |
|
|
|
|
| 1409 |
return (gr.Radio(choices=valid_columns, value=x_default, label="X�� / 類別"),
|
| 1410 |
+
gr.Radio(choices=y_choices, value=y_default, label="Y軸 / 數值"),
|
| 1411 |
gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
|
| 1412 |
gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
|
| 1413 |
except Exception as e:
|
| 1414 |
print(f"更新列選項 (Radio) 時出錯: {e}")
|
| 1415 |
no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
|
| 1416 |
no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
|
|
|
|
| 1417 |
return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none
|
| 1418 |
|
| 1419 |
|
|
|
|
| 1424 |
color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
|
| 1425 |
show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
|
| 1426 |
"""
|
| 1427 |
+
根據用戶選擇創建 Plotly 圖表 (已加入 Null/空白 過濾)。 V5.2 版
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1428 |
"""
|
| 1429 |
# --- 添加調試信息 ---
|
| 1430 |
print("-" * 20, file=sys.stderr)
|
|
|
|
| 1455 |
raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
|
| 1456 |
if not chart_type: raise ValueError("請選擇圖表類型。")
|
| 1457 |
if not agg_func_name: raise ValueError("請選擇聚合函數。")
|
|
|
|
| 1458 |
if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")
|
| 1459 |
|
| 1460 |
# 檢查列是否存在
|
| 1461 |
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1462 |
|
| 1463 |
+
# 判斷是否需要 Y 軸 (修正 V5.1 錯誤: y_column 可能來自 Radio 且值為 NONE_STR)
|
| 1464 |
+
y_column_selected = None if y_column == NONE_STR or y_column == NO_DATA_STR or not y_column else y_column
|
| 1465 |
y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
|
| 1466 |
|
| 1467 |
if y_needed:
|
| 1468 |
+
if not y_column_selected: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列 (不能選 '無')。")
|
| 1469 |
+
if y_column_selected not in df.columns: raise ValueError(f"Y 軸列 '{y_column_selected}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1470 |
+
# else:
|
| 1471 |
+
# y_column_selected 保持為 None
|
| 1472 |
|
| 1473 |
# 處理可選列 (從 Radio 傳來的值可能是 NONE_STR)
|
|
|
|
| 1474 |
group_col = None if group_column == NONE_STR or not group_column else group_column
|
| 1475 |
size_col = None if size_column == NONE_STR or not size_column else size_column
|
| 1476 |
|
|
|
|
| 1483 |
|
| 1484 |
# --- NEW: 過濾 Null/空白值 ---
|
| 1485 |
columns_to_filter = [x_column]
|
| 1486 |
+
if y_needed and y_column_selected: # Filter Y only if it's needed and selected
|
| 1487 |
+
columns_to_filter.append(y_column_selected)
|
| 1488 |
if group_col:
|
| 1489 |
columns_to_filter.append(group_col)
|
| 1490 |
|
|
|
|
| 1491 |
valid_columns_to_filter = [col for col in columns_to_filter if col in df_processed.columns]
|
| 1492 |
if valid_columns_to_filter:
|
| 1493 |
original_rows = len(df_processed)
|
|
|
|
| 1496 |
else:
|
| 1497 |
print("警告: 沒有有效的列用於 Null 值過濾。", file=sys.stderr)
|
| 1498 |
|
|
|
|
| 1499 |
if x_column in df_processed.columns:
|
| 1500 |
try:
|
| 1501 |
original_rows = len(df_processed)
|
|
|
|
| 1502 |
df_processed = df_processed[df_processed[x_column].astype(str).str.strip() != '']
|
| 1503 |
print(f"移除 X 軸 '{x_column}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
|
| 1504 |
except Exception as e:
|
|
|
|
| 1512 |
except Exception as e:
|
| 1513 |
print(f"警告: 過濾分組列 '{group_col}' 空白字符串時出錯: {e}", file=sys.stderr)
|
| 1514 |
|
|
|
|
| 1515 |
if df_processed.empty:
|
| 1516 |
raise ValueError("過濾掉 Null 或空白值後,沒有剩餘數據可供繪圖。")
|
| 1517 |
# --- END NEW ---
|
| 1518 |
|
| 1519 |
|
| 1520 |
# --- 2. 數據類型轉換與準備 ---
|
|
|
|
| 1521 |
df_processed[x_column] = df_processed[x_column].astype(str)
|
| 1522 |
if group_col:
|
| 1523 |
df_processed[group_col] = df_processed[group_col].astype(str)
|
| 1524 |
|
| 1525 |
+
# 僅在需要時轉換 Y 軸和大小列
|
| 1526 |
+
if y_column_selected:
|
| 1527 |
+
try: df_processed[y_column_selected] = pd.to_numeric(df_processed[y_column_selected], errors='coerce')
|
| 1528 |
+
except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column_selected}' 為數值時出錯: {e}")
|
| 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}")
|
|
|
|
| 1534 |
# --- 3. 數據聚合 (如果需要) ---
|
| 1535 |
needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
|
| 1536 |
agg_df = None
|
| 1537 |
+
y_col_agg = y_column_selected # 使用處理過的 Y 軸列名 (可能為 None)
|
| 1538 |
if needs_aggregation:
|
| 1539 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
|
|
|
| 1540 |
|
| 1541 |
if agg_func_name == "計數":
|
| 1542 |
+
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
|
| 1543 |
+
y_col_agg = '__count__'
|
|
|
|
|
|
|
| 1544 |
else:
|
| 1545 |
agg_func_pd = agg_function_map(agg_func_name)
|
| 1546 |
+
if not y_column_selected: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列 (不能選 '無')。")
|
| 1547 |
# 確保 Y 軸是數值類型 (除非 first/last)
|
| 1548 |
+
if agg_func_pd not in ['first', 'last']:
|
| 1549 |
+
if not pd.api.types.is_numeric_dtype(df_processed[y_column_selected]):
|
| 1550 |
+
try: df_processed[y_column_selected] = pd.to_numeric(df_processed[y_column_selected], errors='raise')
|
| 1551 |
+
except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column_selected}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
|
| 1552 |
+
# 檢查轉換後是否有非 NaN 值
|
| 1553 |
+
if df_processed[y_column_selected].isnull().all():
|
| 1554 |
+
raise ValueError(f"Y 軸列 '{y_column_selected}' 在轉換為數值後全為無效值 (NaN),無法執行聚合 '{agg_func_name}'。")
|
| 1555 |
|
| 1556 |
try:
|
| 1557 |
# 執行聚合
|
| 1558 |
+
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column_selected].agg(agg_func_pd)
|
| 1559 |
agg_df = agg_df.reset_index()
|
| 1560 |
+
y_col_agg = y_column_selected # 保持原始列名
|
| 1561 |
except Exception as agg_e:
|
| 1562 |
raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
|
| 1563 |
else:
|
|
|
|
| 1564 |
agg_df = df_processed
|
| 1565 |
+
y_col_agg = y_column_selected # 保持處理過的 Y 軸列名 (可能為 None)
|
| 1566 |
|
|
|
|
| 1567 |
if agg_df is None or agg_df.empty:
|
| 1568 |
+
raise ValueError("數據聚合或處理後沒有產生有效結果。")
|
| 1569 |
+
|
| 1570 |
# 確保繪圖所需的列存在於 agg_df 中
|
| 1571 |
required_cols_for_plot = [x_column]
|
| 1572 |
+
# 修正:只有在 y_col_agg 實際有值(不是 None)時才加入檢查
|
| 1573 |
+
if y_col_agg is not None: required_cols_for_plot.append(y_col_agg)
|
| 1574 |
if group_col: required_cols_for_plot.append(group_col)
|
| 1575 |
if size_col: required_cols_for_plot.append(size_col)
|
| 1576 |
missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
|
| 1577 |
if missing_cols:
|
| 1578 |
+
raise ValueError(f"處理後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
|
| 1579 |
|
| 1580 |
|
| 1581 |
# --- 4. 獲取顏色方案 ---
|
|
|
|
| 1584 |
# --- 5. 創建圖表 (核心邏輯) ---
|
| 1585 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1586 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
| 1587 |
+
effective_y = y_col_agg # 使用聚合後或處理過的 Y 軸列名
|
|
|
|
| 1588 |
|
| 1589 |
# --- (繪圖邏輯開始) ---
|
| 1590 |
+
# 修正:繪圖時使用 y_column_selected (處理過的 Y 軸) 而不是原始 y_column
|
| 1591 |
+
# 修正:甘特圖需要原始 df,而不是 df_processed 或 agg_df
|
| 1592 |
if chart_type == "長條圖":
|
| 1593 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1594 |
fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
|
|
|
|
| 1635 |
fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
|
| 1636 |
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])]))
|
| 1637 |
elif chart_type == "散點圖":
|
| 1638 |
+
if not y_column_selected: raise ValueError("散點圖需要選擇 Y 軸列。")
|
| 1639 |
+
fig = px.scatter(agg_df, x=x_column, y=y_column_selected, color=group_col, size=size_col, **fig_params)
|
|
|
|
| 1640 |
elif chart_type == "氣泡圖":
|
| 1641 |
+
if not y_column_selected: raise ValueError("氣泡圖需要選擇 Y 軸列。")
|
| 1642 |
if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
|
| 1643 |
if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
|
| 1644 |
+
fig = px.scatter(agg_df, x=x_column, y=y_column_selected, color=group_col, size=size_col, size_max=60, **fig_params)
|
| 1645 |
elif chart_type == "直方圖":
|
|
|
|
| 1646 |
if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
|
| 1647 |
fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
|
| 1648 |
elif chart_type == "箱型圖":
|
| 1649 |
+
if not y_column_selected: raise ValueError("箱型圖需要選擇 Y 軸列。")
|
| 1650 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_column_selected]): raise ValueError(f"箱型圖的 Y 軸列 '{y_column_selected}' 必須是數值類型。")
|
| 1651 |
+
fig = px.box(agg_df, x=group_col, y=y_column_selected, color=group_col, **fig_params)
|
| 1652 |
+
if not group_col: fig = px.box(agg_df, y=y_column_selected, **fig_params)
|
|
|
|
| 1653 |
elif chart_type == "小提琴圖":
|
| 1654 |
+
if not y_column_selected: raise ValueError("小提琴圖需要選擇 Y 軸列。")
|
| 1655 |
+
if not pd.api.types.is_numeric_dtype(agg_df[y_column_selected]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_column_selected}' 必須是數值類型。")
|
| 1656 |
+
fig = px.violin(agg_df, x=group_col, y=y_column_selected, color=group_col, box=True, points="all", **fig_params)
|
| 1657 |
+
if not group_col: fig = px.violin(agg_df, y=y_column_selected, box=True, points="all", **fig_params)
|
| 1658 |
elif chart_type == "熱力圖":
|
| 1659 |
if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1660 |
if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
|
|
|
|
| 1693 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
|
| 1694 |
fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1695 |
elif chart_type == "甘特圖":
|
| 1696 |
+
start_col_gantt = y_column_selected; end_col_gantt = group_col; task_col_gantt = x_column
|
| 1697 |
if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1698 |
try:
|
| 1699 |
df_gantt = df.copy() # 使用原始 df
|
|
|
|
| 1753 |
if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
|
| 1754 |
elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
|
| 1755 |
elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
|
| 1756 |
+
elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=task_col_gantt) # 使用任務列作為 Y 軸標籤
|
| 1757 |
else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
|
| 1758 |
|
| 1759 |
+
except ValueError as ve:
|
| 1760 |
+
print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
|
| 1761 |
+
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)
|
| 1762 |
+
except Exception as e:
|
| 1763 |
+
error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
|
| 1764 |
+
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)
|
| 1765 |
|
| 1766 |
+
print("create_plot 函數執行完畢。", file=sys.stderr) # 調試信息
|
| 1767 |
+
return fig
|
| 1768 |
|
| 1769 |
|
| 1770 |
# =========================================
|
|
|
|
| 1772 |
# =========================================
|
| 1773 |
# (與 V4 相同)
|
| 1774 |
def export_data(df, format_type):
|
| 1775 |
+
if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
|
| 1776 |
+
try:
|
| 1777 |
+
if format_type == "CSV": filename = "exported_data.csv"; df.to_csv(filename, index=False, encoding='utf-8-sig')
|
| 1778 |
+
elif format_type == "Excel": filename = "exported_data.xlsx"; df.to_excel(filename, index=False)
|
| 1779 |
+
elif format_type == "JSON": filename = "exported_data.json"; df.to_json(filename, orient="records", indent=4, force_ascii=False)
|
| 1780 |
+
else: return None, f"❌ 不支持的導出格式: {format_type}"
|
| 1781 |
+
return filename, f"✅ 數據已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
|
| 1782 |
+
except Exception as e: print(f"導出數據時出錯: {e}"); traceback.print_exc(); return None, f"❌ 導出數據時出錯: {e}"
|
| 1783 |
|
| 1784 |
def download_figure(fig, format_type="PNG"):
|
| 1785 |
+
if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
|
| 1786 |
+
try:
|
| 1787 |
+
format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
|
| 1788 |
+
import kaleido # 確保導入
|
| 1789 |
+
fig.write_image(filename, format=format_lower)
|
| 1790 |
+
return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
|
| 1791 |
+
except ImportError: error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
|
| 1792 |
+
except ValueError as ve:
|
| 1793 |
+
if "kaleido" in str(ve).lower(): error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"; print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
|
| 1794 |
+
else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
|
| 1795 |
+
except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
|
| 1796 |
|
| 1797 |
# =========================================
|
| 1798 |
# == 智能推薦函數 (Recommendation Function) ==
|
| 1799 |
# =========================================
|
| 1800 |
# (與 V4 相同)
|
| 1801 |
def recommend_chart_settings(df):
|
| 1802 |
+
recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
|
| 1803 |
+
if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
|
| 1804 |
+
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()
|
| 1805 |
+
date_cols = [col for col in columns if pd.api.types.is_datetime64_any_dtype(df[col]) or ('日期' in col or '時間' in col)]
|
| 1806 |
+
try:
|
| 1807 |
+
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]}',推薦使用折線圖顯示趨勢。"})
|
| 1808 |
+
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]}',推薦使用散點圖分析相關性。"})
|
| 1809 |
+
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]}',推薦使用長條圖比較各類別的��值。"})
|
| 1810 |
+
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]}', ...),推薦使用堆疊長條圖顯示計數分佈。"})
|
| 1811 |
+
elif cat_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": None, "agg_function": "計數", "message": f"檢測到類別列 '{cat_cols[0]}',推薦使用長條圖顯示其頻數分佈。"})
|
| 1812 |
+
elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
|
| 1813 |
+
else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
|
| 1814 |
+
except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
|
| 1815 |
+
if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
|
| 1816 |
+
if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
|
| 1817 |
+
if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
|
| 1818 |
+
if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
|
| 1819 |
+
if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
|
| 1820 |
+
return recommendation
|
| 1821 |
|
| 1822 |
# =========================================
|
| 1823 |
# == CSS 樣式 (CSS Styling) ==
|