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 |
# =========================================
|
|
@@ -1295,67 +1295,30 @@ import re
|
|
| 1295 |
import json
|
| 1296 |
import colorsys
|
| 1297 |
import traceback # 用於更詳細的錯誤追蹤
|
|
|
|
| 1298 |
|
| 1299 |
# =========================================
|
| 1300 |
# == 常數定義 (Constants) ==
|
| 1301 |
# =========================================
|
| 1302 |
-
|
| 1303 |
-
# 圖表類型選項 (Chart Type Options) - 保持列表
|
| 1304 |
-
CHART_TYPES = [
|
| 1305 |
-
"長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖",
|
| 1306 |
-
"折線圖", "多重折線圖", "階梯折線圖",
|
| 1307 |
-
"區域圖", "堆疊區域圖", "百分比堆疊區域圖",
|
| 1308 |
-
"圓餅圖", "環形圖",
|
| 1309 |
-
"散點圖", "氣泡圖",
|
| 1310 |
-
"直方圖", "箱型圖", "小提琴圖",
|
| 1311 |
-
"熱力圖", "樹狀圖",
|
| 1312 |
-
"雷達圖", "漏斗圖", "極座標圖", "甘特圖"
|
| 1313 |
-
]
|
| 1314 |
-
|
| 1315 |
-
# 顏色方案選項 (Color Scheme Options) - 保持字典,用於 Dropdown
|
| 1316 |
COLOR_SCHEMES = {
|
| 1317 |
-
"預設 (Plotly)": px.colors.qualitative.Plotly,
|
| 1318 |
-
"
|
| 1319 |
-
"
|
| 1320 |
-
"分類 - Dark24": px.colors.qualitative.Dark24, "分類 - Light24": px.colors.qualitative.Light24,
|
| 1321 |
-
"分類 - Set1": px.colors.qualitative.Set1, "分類 - Set2": px.colors.qualitative.Set2,
|
| 1322 |
-
"分類 - Set3": px.colors.qualitative.Set3, "分類 - Pastel": px.colors.qualitative.Pastel,
|
| 1323 |
-
"分類 - Pastel1": px.colors.qualitative.Pastel1, "分類 - Pastel2": px.colors.qualitative.Pastel2,
|
| 1324 |
-
"分類 - Antique": px.colors.qualitative.Antique, "分類 - Bold": px.colors.qualitative.Bold,
|
| 1325 |
-
"分類 - Prism": px.colors.qualitative.Prism, "分類 - Safe": px.colors.qualitative.Safe,
|
| 1326 |
-
"分類 - Vivid": px.colors.qualitative.Vivid,
|
| 1327 |
-
"連續 - Viridis": px.colors.sequential.Viridis, "連續 - Plasma": px.colors.sequential.Plasma,
|
| 1328 |
-
"連續 - Inferno": px.colors.sequential.Inferno, "連續 - Magma": px.colors.sequential.Magma,
|
| 1329 |
-
"連續 - Cividis": px.colors.sequential.Cividis, "連續 - Blues": px.colors.sequential.Blues,
|
| 1330 |
-
"連續 - Reds": px.colors.sequential.Reds, "連續 - Greens": px.colors.sequential.Greens,
|
| 1331 |
-
"連續 - Purples": px.colors.sequential.Purples, "連續 - Oranges": px.colors.sequential.Oranges,
|
| 1332 |
-
"連續 - Greys": px.colors.sequential.Greys, "連續 - Rainbow": px.colors.sequential.Rainbow,
|
| 1333 |
-
"連續 - Turbo": px.colors.sequential.Turbo, "連續 - Electric": px.colors.sequential.Electric,
|
| 1334 |
-
"連續 - Hot": px.colors.sequential.Hot, "連續 - Teal": px.colors.sequential.Teal,
|
| 1335 |
-
"發散 - Spectral": px.colors.diverging.Spectral, "發散 - RdBu": px.colors.diverging.RdBu,
|
| 1336 |
-
"發散 - PRGn": px.colors.diverging.PRGn, "發散 - PiYG": px.colors.diverging.PiYG,
|
| 1337 |
-
"發散 - BrBG": px.colors.diverging.BrBG, "發散 - Geyser": px.colors.diverging.Geyser,
|
| 1338 |
"循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
|
| 1339 |
}
|
| 1340 |
-
|
| 1341 |
-
# 圖案填充選項 (Pattern Fill Options) - 保持列表,用於 Radio
|
| 1342 |
PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
|
| 1343 |
-
|
| 1344 |
-
# 聚合函數選項 (Aggregation Function Options) - 保持列表,用於 Radio
|
| 1345 |
-
AGGREGATION_FUNCTIONS = [
|
| 1346 |
-
"計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"
|
| 1347 |
-
]
|
| 1348 |
-
|
| 1349 |
-
# 導出格式選項
|
| 1350 |
EXPORT_FORMATS_DATA = ["CSV", "Excel", "JSON"]
|
| 1351 |
EXPORT_FORMATS_IMG = ["PNG", "SVG", "PDF", "JPEG"]
|
| 1352 |
YES_NO_CHOICES = ["是", "否"]
|
|
|
|
|
|
|
| 1353 |
|
| 1354 |
# =========================================
|
| 1355 |
# == 輔助函數 (Helper Functions) ==
|
| 1356 |
# =========================================
|
| 1357 |
-
|
| 1358 |
-
# --- 顏色處理相關 ---
|
| 1359 |
COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
|
| 1360 |
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>"""
|
| 1361 |
COPY_SCRIPT = """
|
|
@@ -1382,35 +1345,18 @@ function copyToClipboard(text) {
|
|
| 1382 |
</script>
|
| 1383 |
"""
|
| 1384 |
COMMON_COLORS = {
|
| 1385 |
-
"紅色": "#FF0000", "亮紅": "#FF5733", "深紅": "#C70039", "橙色": "#FFA500", "亮橙": "#FFC300", "深橙": "#D35400",
|
| 1386 |
-
"黃色": "#FFFF00", "亮黃": "#F1C40F", "金色": "#FFD700", "綠色": "#008000", "亮綠": "#2ECC71", "深綠": "#1E8449",
|
| 1387 |
-
"橄欖綠": "#808000", "藍色": "#0000FF", "亮藍": "#3498DB", "深藍": "#2874A6", "天藍": "#87CEEB", "紫色": "#800080",
|
| 1388 |
-
"亮紫": "#9B59B6", "深紫": "#6C3483", "薰衣草紫": "#E6E6FA", "粉紅色": "#FFC0CB", "亮粉": "#FF69B4", "深粉": "#C71585",
|
| 1389 |
-
"棕色": "#A52A2A", "亮棕": "#E59866", "深棕": "#6E2C00", "青色": "#00FFFF", "藍綠色": "#008080", "綠松石色": "#40E0D0",
|
| 1390 |
-
"洋紅": "#FF00FF", "紫紅色": "#DC143C", "灰色": "#808080", "淺灰": "#D3D3D3", "深灰": "#696969", "石板灰": "#708090",
|
| 1391 |
-
"黑色": "#000000", "白色": "#FFFFFF", "米色": "#F5F5DC",
|
| 1392 |
}
|
| 1393 |
def generate_gradient_colors(start_color, end_color, steps=10):
|
| 1394 |
-
def hex_to_rgb(hex_color):
|
| 1395 |
-
|
| 1396 |
-
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
| 1397 |
-
def rgb_to_hex(rgb):
|
| 1398 |
-
r, g, b = [max(0, min(255, int(c))) for c in rgb]
|
| 1399 |
-
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
|
| 1400 |
try:
|
| 1401 |
-
start_rgb, end_rgb = hex_to_rgb(start_color), hex_to_rgb(end_color)
|
| 1402 |
if steps <= 1: return [start_color] if steps == 1 else []
|
| 1403 |
r_step, g_step, b_step = [(end_rgb[i] - start_rgb[i]) / (steps - 1) for i in range(3)]
|
| 1404 |
return [rgb_to_hex((start_rgb[0] + r_step * i, start_rgb[1] + g_step * i, start_rgb[2] + b_step * i)) for i in range(steps)]
|
| 1405 |
-
except Exception as e:
|
| 1406 |
-
|
| 1407 |
-
GRADIENTS = {
|
| 1408 |
-
"紅→黃": generate_gradient_colors("#FF0000", "#FFFF00"), "藍→綠": generate_gradient_colors("#0000FF", "#00FF00"),
|
| 1409 |
-
"紫→粉": generate_gradient_colors("#800080", "#FFC0CB"), "紅→藍": generate_gradient_colors("#FF0000", "#0000FF"),
|
| 1410 |
-
"黑→白": generate_gradient_colors("#000000", "#FFFFFF"), "藍→紅 (發散)": generate_gradient_colors("#0000FF", "#FF0000"),
|
| 1411 |
-
"綠→紫 (發散)": generate_gradient_colors("#00FF00", "#800080"),
|
| 1412 |
-
"彩虹 (簡易)": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"]
|
| 1413 |
-
}
|
| 1414 |
def generate_color_cards():
|
| 1415 |
common_cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code) for name, hex_code in COMMON_COLORS.items()])
|
| 1416 |
gradient_cards_html = ""
|
|
@@ -1436,14 +1382,13 @@ def parse_custom_colors(color_text):
|
|
| 1436 |
except Exception as e: print(f"解析自定義顏色時出錯: {e}"); return {}
|
| 1437 |
return custom_colors
|
| 1438 |
def update_patterns(*patterns_input):
|
| 1439 |
-
# 確保只返回有效圖案
|
| 1440 |
return [p for p in patterns_input if p in PATTERN_TYPES and p != "無"]
|
| 1441 |
|
| 1442 |
-
|
| 1443 |
# =========================================
|
| 1444 |
# == 數據處理函數 (Data Processing Functions) ==
|
| 1445 |
# =========================================
|
| 1446 |
def process_upload(file):
|
|
|
|
| 1447 |
if file is None: return None, "❌ 未上傳任何文件。"
|
| 1448 |
try:
|
| 1449 |
file_path = file.name; file_type = file_path.split('.')[-1].lower()
|
|
@@ -1458,53 +1403,62 @@ def process_upload(file):
|
|
| 1458 |
except Exception as e: return None, f"❌ 讀取 Excel 文件時出錯: {e}"
|
| 1459 |
else: return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"
|
| 1460 |
df.columns = df.columns.str.strip()
|
| 1461 |
-
# 清理數據中的潛在空格(可選,但有助於分組)
|
| 1462 |
-
# for col in df.select_dtypes(include=['object', 'string']).columns:
|
| 1463 |
-
# df[col] = df[col].str.strip()
|
| 1464 |
return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"
|
| 1465 |
except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
|
| 1466 |
|
| 1467 |
def parse_data(text_data):
|
|
|
|
| 1468 |
if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
|
| 1469 |
try:
|
| 1470 |
data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
|
| 1471 |
if ',' in first_line: separator = ','
|
| 1472 |
elif '\t' in first_line: separator = '\t'
|
| 1473 |
elif ' ' in first_line: separator = r'\s+'
|
| 1474 |
-
else: separator = ','
|
| 1475 |
-
try:
|
| 1476 |
-
df = pd.read_csv(data_io, sep=separator, skipinitialspace=True) # skipinitialspace 處理分隔符後的空格
|
| 1477 |
except pd.errors.ParserError as pe: return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
|
| 1478 |
except Exception as e: return None, f"❌ 解析數據時出錯: {e}"
|
| 1479 |
df.columns = df.columns.str.strip()
|
| 1480 |
-
# 清理數據中的潛在空格
|
| 1481 |
-
# for col in df.select_dtypes(include=['object', 'string']).columns:
|
| 1482 |
-
# df[col] = df[col].str.strip()
|
| 1483 |
return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"
|
| 1484 |
except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"
|
| 1485 |
|
| 1486 |
-
def
|
| 1487 |
-
|
|
|
|
|
|
|
|
|
|
| 1488 |
if df is None or df.empty:
|
| 1489 |
-
# 返回空的
|
| 1490 |
-
no_data_update = gr.
|
| 1491 |
-
no_data_update_with_none = gr.
|
| 1492 |
return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
|
| 1493 |
|
| 1494 |
try:
|
| 1495 |
columns = df.columns.tolist()
|
| 1496 |
-
x_default = columns[0] if columns else None
|
| 1497 |
-
y_default = columns[1] if len(columns) > 1 else (columns[0] if columns else None)
|
| 1498 |
valid_columns = [str(col) for col in columns if col is not None and str(col) != ""] # 確保列名是字符串
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1504 |
except Exception as e:
|
| 1505 |
-
print(f"
|
| 1506 |
-
no_data_update = gr.
|
| 1507 |
-
no_data_update_with_none = gr.
|
| 1508 |
return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
|
| 1509 |
|
| 1510 |
|
|
@@ -1514,6 +1468,23 @@ def update_columns(df):
|
|
| 1514 |
def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
|
| 1515 |
color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
|
| 1516 |
show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1517 |
fig = go.Figure()
|
| 1518 |
try:
|
| 1519 |
# --- 0. 將 "是"/"否" 轉換為布林值 ---
|
|
@@ -1521,40 +1492,37 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1521 |
show_legend = True if show_legend_str == "是" else False
|
| 1522 |
|
| 1523 |
# --- 1. 輸入驗證 (更嚴格) ---
|
| 1524 |
-
if df is None or df.empty:
|
|
|
|
| 1525 |
if not chart_type: raise ValueError("請選擇圖表類型。")
|
| 1526 |
if not agg_func_name: raise ValueError("請選擇聚合函數。")
|
| 1527 |
-
if not x_column or x_column ==
|
| 1528 |
|
| 1529 |
# 檢查列是否存在
|
| 1530 |
-
if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}'
|
| 1531 |
|
| 1532 |
# 只有在非計數且非直方圖時才嚴格要求 Y 軸
|
| 1533 |
y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
|
| 1534 |
if y_needed:
|
| 1535 |
-
if not y_column or y_column ==
|
| 1536 |
-
if y_column not in df.columns: raise ValueError(f"Y 軸列 '{y_column}'
|
| 1537 |
else:
|
| 1538 |
y_column = None # 如果不需要 Y 軸,明確設為 None
|
| 1539 |
|
| 1540 |
# 處理可選列
|
| 1541 |
-
group_col = None if group_column ==
|
| 1542 |
-
size_col = None if size_column ==
|
| 1543 |
|
| 1544 |
-
if group_col and group_col not in df.columns: raise ValueError(f"分組列 '{group_col}'
|
| 1545 |
-
if size_col and size_col not in df.columns: raise ValueError(f"大小列 '{size_col}'
|
| 1546 |
if group_col == x_column: raise ValueError("分組列不能與 X 軸列相同。")
|
| 1547 |
-
# if y_needed and y_column == x_column: raise ValueError("Y 軸列不能與 X 軸列相同(除非特殊情況)。") # 允許某些情況相同
|
| 1548 |
|
| 1549 |
df_processed = df.copy()
|
| 1550 |
|
| 1551 |
# --- 2. 數據類型轉換與準備 ---
|
| 1552 |
-
#
|
| 1553 |
df_processed[x_column] = df_processed[x_column].astype(str)
|
| 1554 |
-
if group_col:
|
| 1555 |
-
df_processed[group_col] = df_processed[group_col].astype(str)
|
| 1556 |
-
|
| 1557 |
-
# 嘗試將 Y 軸和大小列轉為數值
|
| 1558 |
if y_column:
|
| 1559 |
try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='coerce')
|
| 1560 |
except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column}' 為數值時出錯: {e}")
|
|
@@ -1562,48 +1530,39 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1562 |
try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
|
| 1563 |
except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")
|
| 1564 |
|
| 1565 |
-
|
| 1566 |
# --- 3. 數據聚合 (如果需要) ---
|
|
|
|
| 1567 |
needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
|
| 1568 |
agg_df = None
|
| 1569 |
-
y_col_agg = y_column
|
| 1570 |
if needs_aggregation:
|
| 1571 |
grouping_cols = [x_column] + ([group_col] if group_col else [])
|
| 1572 |
-
|
|
|
|
| 1573 |
if agg_func_name == "計數":
|
| 1574 |
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
|
| 1575 |
y_col_agg = '__count__'
|
| 1576 |
else:
|
| 1577 |
agg_func_pd = agg_function_map(agg_func_name)
|
| 1578 |
if not y_column: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
|
| 1579 |
-
# 確保 Y 軸是數值類型 (除非 first/last)
|
| 1580 |
if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_column]):
|
| 1581 |
-
# 嘗試再次轉換,如果失敗則報錯
|
| 1582 |
try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='raise')
|
| 1583 |
except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
|
| 1584 |
-
|
| 1585 |
try:
|
| 1586 |
-
# 執行聚合
|
| 1587 |
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd).reset_index()
|
| 1588 |
-
y_col_agg = y_column
|
| 1589 |
-
except Exception as agg_e:
|
| 1590 |
-
raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
|
| 1591 |
else:
|
| 1592 |
agg_df = df_processed
|
| 1593 |
-
y_col_agg = y_column
|
| 1594 |
|
| 1595 |
-
|
| 1596 |
-
if agg_df is None or agg_df.empty:
|
| 1597 |
-
raise ValueError("數據聚合後沒有產生有效結果。")
|
| 1598 |
-
# 確保繪圖所需的列存在於 agg_df 中
|
| 1599 |
required_cols_for_plot = [x_column]
|
| 1600 |
if y_col_agg: required_cols_for_plot.append(y_col_agg)
|
| 1601 |
if group_col: required_cols_for_plot.append(group_col)
|
| 1602 |
if size_col: required_cols_for_plot.append(size_col)
|
| 1603 |
missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
|
| 1604 |
-
if missing_cols:
|
| 1605 |
-
raise ValueError(f"聚合後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
|
| 1606 |
-
|
| 1607 |
|
| 1608 |
# --- 4. 獲取顏色方案 ---
|
| 1609 |
colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)
|
|
@@ -1611,12 +1570,9 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1611 |
# --- 5. 創建圖表 (核心邏輯) ---
|
| 1612 |
fig_params = {"data_frame": agg_df, "title": title, "color_discrete_sequence": colors, "width": width, "height": height}
|
| 1613 |
if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
|
| 1614 |
-
|
| 1615 |
-
# --- (繪圖邏輯開始) ---
|
| 1616 |
-
# (與 V3 版本相同,此處省略以保持簡潔,實際代碼應包含完整的 if/elif 結構)
|
| 1617 |
-
# 確保 y_col_agg 在不需要時為 None
|
| 1618 |
effective_y = y_col_agg if y_needed or agg_func_name == "計數" else None
|
| 1619 |
|
|
|
|
| 1620 |
if chart_type == "長條圖":
|
| 1621 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1622 |
fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
|
|
@@ -1663,7 +1619,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1663 |
fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
|
| 1664 |
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])]))
|
| 1665 |
elif chart_type == "散點圖":
|
| 1666 |
-
if not y_column: raise ValueError("散點圖需要選擇 Y 軸列。")
|
| 1667 |
fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
|
| 1668 |
elif chart_type == "氣泡圖":
|
| 1669 |
if not y_column: raise ValueError("氣泡圖需要選擇 Y 軸列。")
|
|
@@ -1721,13 +1677,10 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1721 |
if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
|
| 1722 |
fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1723 |
elif chart_type == "甘特圖":
|
| 1724 |
-
|
| 1725 |
-
start_col_gantt = y_column # Y 軸被用作開始列
|
| 1726 |
-
end_col_gantt = group_col # 分組列被用作結束列
|
| 1727 |
-
task_col_gantt = x_column # X 軸被用作任務列
|
| 1728 |
if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1729 |
try:
|
| 1730 |
-
df_gantt = df.copy()
|
| 1731 |
if start_col_gantt not in df_gantt.columns: raise ValueError(f"開始列 '{start_col_gantt}' 不在數據中。")
|
| 1732 |
if end_col_gantt not in df_gantt.columns: raise ValueError(f"結束列 '{end_col_gantt}' 不在數據中。")
|
| 1733 |
if task_col_gantt not in df_gantt.columns: raise ValueError(f"任務列 '{task_col_gantt}' 不在數據中。")
|
|
@@ -1746,6 +1699,7 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1746 |
|
| 1747 |
|
| 1748 |
# --- 6. 應用圖案 (如果支持) ---
|
|
|
|
| 1749 |
if patterns:
|
| 1750 |
try:
|
| 1751 |
num_traces = len(fig.data)
|
|
@@ -1768,10 +1722,11 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1768 |
dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
|
| 1769 |
for i, trace in enumerate(fig.data):
|
| 1770 |
pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
|
| 1771 |
-
if dash: trace.line.dash = dash; trace.fill = 'tonexty' if hasattr(trace, 'stackgroup') and trace.stackgroup else 'tozeroy'
|
| 1772 |
except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")
|
| 1773 |
|
| 1774 |
# --- 7. 更新佈局 ---
|
|
|
|
| 1775 |
fig.update_layout(
|
| 1776 |
showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
|
| 1777 |
template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
|
|
@@ -1779,25 +1734,25 @@ def create_plot(df, chart_type, x_column, y_column, group_column=None, size_colu
|
|
| 1779 |
hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
|
| 1780 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
|
| 1781 |
)
|
| 1782 |
-
# 根據圖表類型更新軸標籤
|
| 1783 |
final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
|
| 1784 |
if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
|
| 1785 |
elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
|
| 1786 |
elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
|
| 1787 |
-
elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column)
|
| 1788 |
else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
|
| 1789 |
|
| 1790 |
except ValueError as ve:
|
| 1791 |
-
print(f"圖表創建錯誤 (ValueError): {ve}"); traceback.print_exc()
|
| 1792 |
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)
|
| 1793 |
except Exception as e:
|
| 1794 |
-
error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message)
|
| 1795 |
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)
|
| 1796 |
return fig
|
| 1797 |
|
| 1798 |
# =========================================
|
| 1799 |
# == 導出與下載函數 (Export & Download Functions) ==
|
| 1800 |
# =========================================
|
|
|
|
| 1801 |
def export_data(df, format_type):
|
| 1802 |
if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
|
| 1803 |
try:
|
|
@@ -1812,26 +1767,19 @@ def download_figure(fig, format_type="PNG"):
|
|
| 1812 |
if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
|
| 1813 |
try:
|
| 1814 |
format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
|
| 1815 |
-
#
|
| 1816 |
-
import kaleido
|
| 1817 |
fig.write_image(filename, format=format_lower)
|
| 1818 |
return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
|
| 1819 |
-
except ImportError:
|
| 1820 |
-
error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"
|
| 1821 |
-
print(error_msg); return None, error_msg
|
| 1822 |
except ValueError as ve:
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
-
|
| 1826 |
-
print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
|
| 1827 |
-
else:
|
| 1828 |
-
print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
|
| 1829 |
-
except Exception as e:
|
| 1830 |
-
print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
|
| 1831 |
|
| 1832 |
# =========================================
|
| 1833 |
# == 智能推薦函數 (Recommendation Function) ==
|
| 1834 |
# =========================================
|
|
|
|
| 1835 |
def recommend_chart_settings(df):
|
| 1836 |
recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
|
| 1837 |
if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
|
|
@@ -1846,18 +1794,17 @@ def recommend_chart_settings(df):
|
|
| 1846 |
elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
|
| 1847 |
else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
|
| 1848 |
except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
|
| 1849 |
-
# 驗證推薦的列名
|
| 1850 |
if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
|
| 1851 |
if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
|
| 1852 |
if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
|
| 1853 |
-
# 清理無效聚合
|
| 1854 |
if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
|
| 1855 |
-
if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
|
| 1856 |
return recommendation
|
| 1857 |
|
| 1858 |
# =========================================
|
| 1859 |
# == CSS 樣式 (CSS Styling) ==
|
| 1860 |
# =========================================
|
|
|
|
| 1861 |
CUSTOM_CSS = """
|
| 1862 |
/* --- 全局和容器 --- */
|
| 1863 |
.gradio-container { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
|
|
@@ -1907,23 +1854,21 @@ CUSTOM_CSS = """
|
|
| 1907 |
# =========================================
|
| 1908 |
# == Gradio UI 介面定義 (Gradio UI Definition) ==
|
| 1909 |
# =========================================
|
| 1910 |
-
with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具
|
| 1911 |
|
| 1912 |
# --- 應用程式標頭 ---
|
| 1913 |
gr.HTML("""
|
| 1914 |
<div class="app-header">
|
| 1915 |
-
<h1 class="app-title">📊 進階數據可視化工具
|
| 1916 |
-
<p class="app-subtitle"
|
| 1917 |
</div>
|
| 1918 |
""")
|
| 1919 |
|
| 1920 |
# --- 狀態變量 ---
|
| 1921 |
data_state = gr.State(None)
|
| 1922 |
-
|
| 1923 |
-
|
| 1924 |
-
|
| 1925 |
-
patterns_state_2 = gr.State([])
|
| 1926 |
-
recommendation_state = gr.State({}) # 用於存儲推薦結果
|
| 1927 |
|
| 1928 |
# --- 主頁籤佈局 ---
|
| 1929 |
with gr.Tabs() as tabs:
|
|
@@ -1948,116 +1893,79 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
|
|
| 1948 |
gr.Markdown("下方將顯示載入或解析後的數據預覽。")
|
| 1949 |
data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
|
| 1950 |
with gr.Row():
|
| 1951 |
-
export_format = gr.Radio(EXPORT_FORMATS_DATA, label="選擇導出格式", value="CSV") #
|
| 1952 |
export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
|
| 1953 |
export_result = gr.File(label="導出文件下載", interactive=False)
|
| 1954 |
export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 1955 |
|
| 1956 |
-
# --- 圖表創建頁籤 (
|
| 1957 |
-
with gr.TabItem("📈
|
| 1958 |
-
gr.HTML('<div class="section-title"
|
| 1959 |
-
gr.Markdown("
|
| 1960 |
-
|
| 1961 |
-
with gr.Row(): # 主 Row,包含左右兩個圖表區塊
|
| 1962 |
-
# --- 圖表一 (左側 Column) ---
|
| 1963 |
-
with gr.Column(scale=1):
|
| 1964 |
-
gr.Markdown("### 📊 圖表一")
|
| 1965 |
-
with gr.Group(elem_classes=["card"]): # 將所有設定放在一個卡片中
|
| 1966 |
-
gr.Markdown("**基本設置**")
|
| 1967 |
-
chart_type_1 = gr.Radio(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True) # 改用 Radio
|
| 1968 |
-
recommend_button_1 = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm")
|
| 1969 |
-
chart_title_1 = gr.Textbox(label="圖表標題", placeholder="圖表一標題")
|
| 1970 |
-
agg_function_1 = gr.Radio(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數") # 改用 Radio
|
| 1971 |
-
|
| 1972 |
-
gr.Markdown("**數據映射**")
|
| 1973 |
-
x_column_1 = gr.Dropdown(["-- 無數據 --"], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸") # 保留 Dropdown
|
| 1974 |
-
y_column_1 = gr.Dropdown(["-- 無數據 --"], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)") # 保留 Dropdown
|
| 1975 |
-
group_column_1 = gr.Dropdown(["無"], label="分組列", info="用於生成多系列或堆疊") # 保留 Dropdown
|
| 1976 |
-
size_column_1 = gr.Dropdown(["無"], label="大小列", info="用於氣泡圖等控制點的大小") # 保留 Dropdown
|
| 1977 |
-
|
| 1978 |
-
gr.Markdown("**顯示選項**")
|
| 1979 |
-
chart_width_1 = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
|
| 1980 |
-
chart_height_1 = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
|
| 1981 |
-
show_grid_1 = gr.Radio(YES_NO_CHOICES, label="顯示網格", value="是") # 改用 Radio
|
| 1982 |
-
show_legend_1 = gr.Radio(YES_NO_CHOICES, label="顯示圖例", value="是") # 改用 Radio
|
| 1983 |
-
color_scheme_1 = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)") # 保留 Dropdown
|
| 1984 |
-
gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
|
| 1985 |
-
gr.HTML(generate_color_cards(), elem_id="color_display_1")
|
| 1986 |
-
|
| 1987 |
-
gr.Markdown("**圖案與自定義顏色**")
|
| 1988 |
-
pattern1_1 = gr.Radio(PATTERN_TYPES, label="圖案1", value="無") # 改用 Radio
|
| 1989 |
-
pattern2_1 = gr.Radio(PATTERN_TYPES, label="圖案2", value="無") # 改用 Radio
|
| 1990 |
-
pattern3_1 = gr.Radio(PATTERN_TYPES, label="圖案3", value="無") # 改用 Radio
|
| 1991 |
-
color_customization_1 = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
|
| 1992 |
-
|
| 1993 |
-
# 圖表一:操作按鈕
|
| 1994 |
-
with gr.Row():
|
| 1995 |
-
update_button_1 = gr.Button("🔄 更新圖表一", variant="primary", elem_classes=["primary-button"])
|
| 1996 |
-
export_img_format_1 = gr.Radio(EXPORT_FORMATS_IMG, label="導出格式", value="PNG", scale=1) # 改用 Radio
|
| 1997 |
-
download_button_1 = gr.Button("💾 導出圖表一", elem_classes=["secondary-button"], scale=1)
|
| 1998 |
-
export_chart_1 = gr.File(label="圖表一文件下載", interactive=False)
|
| 1999 |
-
export_chart_status_1 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 2000 |
-
|
| 2001 |
-
# 圖表一:預覽區域
|
| 2002 |
-
with gr.Group(elem_classes=["chart-previewer"]):
|
| 2003 |
-
chart_output_1 = gr.Plot(label="", elem_id="chart_preview_1")
|
| 2004 |
-
|
| 2005 |
|
| 2006 |
-
|
|
|
|
| 2007 |
with gr.Column(scale=1):
|
| 2008 |
-
gr.Markdown("### 📊
|
| 2009 |
with gr.Group(elem_classes=["card"]):
|
| 2010 |
gr.Markdown("**基本設置**")
|
| 2011 |
-
|
| 2012 |
-
|
| 2013 |
-
|
| 2014 |
-
|
| 2015 |
-
|
| 2016 |
-
|
| 2017 |
-
|
| 2018 |
-
|
| 2019 |
-
|
|
|
|
|
|
|
| 2020 |
|
| 2021 |
gr.Markdown("**顯示選項**")
|
| 2022 |
-
|
| 2023 |
-
|
| 2024 |
-
|
| 2025 |
-
|
| 2026 |
-
|
| 2027 |
-
|
|
|
|
| 2028 |
|
| 2029 |
gr.Markdown("**圖案與自定義顏色**")
|
| 2030 |
-
|
| 2031 |
-
|
| 2032 |
-
|
| 2033 |
-
|
| 2034 |
-
|
| 2035 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2036 |
with gr.Row():
|
| 2037 |
-
|
| 2038 |
-
|
| 2039 |
-
|
| 2040 |
-
|
| 2041 |
-
export_chart_status_2 = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 2042 |
|
| 2043 |
-
#
|
|
|
|
| 2044 |
with gr.Group(elem_classes=["chart-previewer"]):
|
| 2045 |
-
|
| 2046 |
|
| 2047 |
|
| 2048 |
# --- 使用說明頁籤 ---
|
| 2049 |
with gr.TabItem("❓ 使用說明", id=2):
|
| 2050 |
with gr.Group(elem_classes=["card"]):
|
| 2051 |
gr.HTML("""
|
| 2052 |
-
<div class="section-title"
|
| 2053 |
<h3>數據輸入</h3>
|
| 2054 |
<ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
|
| 2055 |
-
<h3
|
| 2056 |
-
<ul><li
|
| 2057 |
-
<ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong
|
| 2058 |
-
</li><li><strong>數據映射 (
|
| 2059 |
<h3>提示</h3>
|
| 2060 |
-
<ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li
|
| 2061 |
""")
|
| 2062 |
|
| 2063 |
# =========================================
|
|
@@ -2065,80 +1973,37 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
|
|
| 2065 |
# =========================================
|
| 2066 |
|
| 2067 |
# --- 數據載入與更新 ---
|
| 2068 |
-
def
|
| 2069 |
-
"""
|
|
|
|
| 2070 |
preview_df = df if df is not None else pd.DataFrame()
|
| 2071 |
-
#
|
| 2072 |
-
col_updates =
|
| 2073 |
if col_updates is None or len(col_updates) != 4:
|
| 2074 |
-
print("警告:
|
| 2075 |
-
# 返回空更新,避免錯誤
|
| 2076 |
-
|
| 2077 |
-
# 額外返回兩個空的 Plot 更新以匹配輸出數量
|
| 2078 |
-
updates += [gr.Plot(value=None), gr.Plot(value=None)]
|
| 2079 |
-
return updates
|
| 2080 |
-
|
| 2081 |
-
# 準備所有更新 (狀態、狀態消息、預覽表格、8 個下拉選單)
|
| 2082 |
-
updates = [df, status_msg, preview_df] + list(col_updates) * 2
|
| 2083 |
-
# 添加兩個空的 Plot 更新以匹配輸出數量
|
| 2084 |
-
updates += [gr.Plot(value=None), gr.Plot(value=None)] # 讓圖表區域保持空白
|
| 2085 |
-
|
| 2086 |
-
# 檢查返回的列表長度是否為 13
|
| 2087 |
-
if len(updates) != 13:
|
| 2088 |
-
print(f"錯誤:load_data_and_update_ui 返回了 {len(updates)} 個元素,預期 13 個。")
|
| 2089 |
-
# 嘗試返回一個包含 13 個 None 的列表以避免崩潰
|
| 2090 |
-
return [None] * 13
|
| 2091 |
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
#
|
| 2095 |
-
updates
|
| 2096 |
-
|
| 2097 |
-
# 準備觸發圖表一初始繪圖的輸入
|
| 2098 |
-
# 使用更新後的下拉列表的值(如果有效)或預設值
|
| 2099 |
-
x_col_val = col_updates[0].value if col_updates[0].value else None
|
| 2100 |
-
y_col_val = col_updates[1].value if col_updates[1].value else None
|
| 2101 |
-
# 獲取圖表一的其他預設值
|
| 2102 |
-
chart_type_val = "長條圖" # 來自 chart_type_1 的預設值
|
| 2103 |
-
agg_func_val = "計數" # 來自 agg_function_1 的預設值
|
| 2104 |
-
|
| 2105 |
-
# 只有在數據有效且 X 軸有效時才嘗試繪圖
|
| 2106 |
-
initial_plot = go.Figure() # 默認空圖
|
| 2107 |
-
if df is not None and not df.empty and x_col_val and x_col_val != "-- 無數據 --":
|
| 2108 |
-
try:
|
| 2109 |
-
# 使用預設值嘗試繪製圖表一
|
| 2110 |
-
initial_plot = create_plot(df, chart_type_val, x_col_val, y_col_val,
|
| 2111 |
-
None, None, # group, size
|
| 2112 |
-
"預設 (Plotly)", [], "", 700, 450, # color, patterns, title, w, h
|
| 2113 |
-
"是", "是", agg_func_val, {}) # grid, legend, agg, custom_colors
|
| 2114 |
-
except Exception as initial_plot_e:
|
| 2115 |
-
print(f"載入數據後嘗試初始繪圖時出錯: {initial_plot_e}")
|
| 2116 |
-
initial_plot = go.Figure()
|
| 2117 |
-
initial_plot.add_annotation(text=f"⚠️ 無法生成初始圖表:<br>{initial_plot_e}", align='left', showarrow=False, font=dict(size=14, color="orange"))
|
| 2118 |
-
initial_plot.update_layout(xaxis_visible=False, yaxis_visible=False)
|
| 2119 |
-
|
| 2120 |
-
# 將初始圖表添加到更新列表中
|
| 2121 |
-
updates.append(initial_plot)
|
| 2122 |
-
# 圖表二保持空白
|
| 2123 |
-
updates.append(gr.Plot(value=None)) # 更新圖表二為空
|
| 2124 |
|
|
|
|
| 2125 |
return updates
|
| 2126 |
|
| 2127 |
-
|
| 2128 |
-
# 綁定數據載入事件
|
| 2129 |
upload_button.click(
|
| 2130 |
process_upload,
|
| 2131 |
inputs=[file_upload],
|
| 2132 |
outputs=[data_state, upload_status]
|
| 2133 |
).then(
|
| 2134 |
-
|
| 2135 |
inputs=[data_state, upload_status],
|
| 2136 |
outputs=[
|
| 2137 |
data_state, upload_status, data_preview,
|
| 2138 |
-
|
| 2139 |
-
|
| 2140 |
-
chart_output_1, # 圖表一初始繪圖
|
| 2141 |
-
chart_output_2 # 圖表二初始空白
|
| 2142 |
]
|
| 2143 |
)
|
| 2144 |
parse_button.click(
|
|
@@ -2146,13 +2011,12 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
|
|
| 2146 |
inputs=[csv_input],
|
| 2147 |
outputs=[data_state, parse_status]
|
| 2148 |
).then(
|
| 2149 |
-
|
| 2150 |
inputs=[data_state, parse_status],
|
| 2151 |
outputs=[
|
| 2152 |
data_state, parse_status, data_preview,
|
| 2153 |
-
|
| 2154 |
-
|
| 2155 |
-
chart_output_1, chart_output_2
|
| 2156 |
]
|
| 2157 |
)
|
| 2158 |
|
|
@@ -2160,63 +2024,56 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
|
|
| 2160 |
# --- 數據導出 ---
|
| 2161 |
export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
|
| 2162 |
|
| 2163 |
-
# ---
|
| 2164 |
-
|
| 2165 |
-
|
| 2166 |
-
for
|
| 2167 |
-
|
| 2168 |
-
|
| 2169 |
-
|
| 2170 |
-
|
| 2171 |
-
|
| 2172 |
-
|
| 2173 |
-
|
| 2174 |
-
|
| 2175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2176 |
|
|
|
|
| 2177 |
|
| 2178 |
-
# ---
|
| 2179 |
-
|
| 2180 |
|
| 2181 |
-
# ---
|
| 2182 |
-
def
|
| 2183 |
if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
|
| 2184 |
chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
|
| 2185 |
-
y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", "無"
|
| 2186 |
# 返回 Radio 和 Dropdown 的更新
|
| 2187 |
return [
|
| 2188 |
gr.Radio(value=chart_type_val), # 更新 Chart Type Radio
|
| 2189 |
-
gr.
|
| 2190 |
-
gr.
|
| 2191 |
-
gr.
|
| 2192 |
-
gr.Radio(value=agg_func_val)
|
| 2193 |
]
|
| 2194 |
|
| 2195 |
-
|
| 2196 |
-
|
| 2197 |
-
)
|
| 2198 |
-
|
| 2199 |
-
# ---
|
| 2200 |
-
|
| 2201 |
-
patterns_inputs_2 = [pattern1_2, pattern2_2, pattern3_2]
|
| 2202 |
-
for pattern_dd in patterns_inputs_2: pattern_dd.change(update_patterns, inputs=patterns_inputs_2, outputs=[patterns_state_2])
|
| 2203 |
-
|
| 2204 |
-
# --- 圖表二:更新圖表 ---
|
| 2205 |
-
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]
|
| 2206 |
-
update_button_2.click(create_plot, inputs=chart_inputs_2, outputs=[chart_output_2])
|
| 2207 |
-
def auto_update_chart_2(*inputs): return create_plot(*inputs)
|
| 2208 |
-
# 綁定 change 事件到會影響圖表的 UI 組件
|
| 2209 |
-
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]:
|
| 2210 |
-
if input_component is not None and hasattr(input_component, 'change'):
|
| 2211 |
-
input_component.change(auto_update_chart_2, inputs=chart_inputs_2, outputs=[chart_output_2])
|
| 2212 |
-
|
| 2213 |
-
# --- 圖表二:導出圖表 ---
|
| 2214 |
-
download_button_2.click(download_figure, inputs=[chart_output_2, export_img_format_2], outputs=[export_chart_2, export_chart_status_2])
|
| 2215 |
-
|
| 2216 |
-
# --- 圖表類型改變時更新 UI 元素可見性 (保持不變) ---
|
| 2217 |
-
def update_element_visibility(chart_type):
|
| 2218 |
try:
|
| 2219 |
-
# (與
|
| 2220 |
is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
|
| 2221 |
is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
|
| 2222 |
is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
|
|
@@ -2231,13 +2088,12 @@ with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具 V4", theme=gr.
|
|
| 2231 |
elif is_heatmap: group_label, group_needed = "行/列 分組", True
|
| 2232 |
size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
|
| 2233 |
if is_gantt: size_label, size_needed = "顏色列 (可選)", True
|
| 2234 |
-
# 返回
|
| 2235 |
return (gr.update(label=y_label, visible=y_needed), gr.update(label=group_label, visible=group_needed), gr.update(label=size_label, visible=size_needed))
|
| 2236 |
except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
|
| 2237 |
|
| 2238 |
# 綁定到 Radio chart_type 的 change 事件
|
| 2239 |
-
|
| 2240 |
-
chart_type_2.change(update_element_visibility, inputs=[chart_type_2], outputs=[y_column_2, group_column_2, size_column_2])
|
| 2241 |
|
| 2242 |
# =========================================
|
| 2243 |
# == 應用程式啟動 (Launch Application) ==
|
|
|
|
| 1273 |
"""
|
| 1274 |
Gradio 應用程式:進階數據可視化工具
|
| 1275 |
作者:Gemini
|
| 1276 |
+
版本:5.0 (極簡化 - 單圖表 + 全 Radio)
|
| 1277 |
+
描述:包含所有功能的完整程式碼,極度簡化 UI,使用 Radio 替代 Dropdown,移除自動更新。
|
| 1278 |
"""
|
| 1279 |
|
| 1280 |
# =========================================
|
|
|
|
| 1295 |
import json
|
| 1296 |
import colorsys
|
| 1297 |
import traceback # 用於更詳細的錯誤追蹤
|
| 1298 |
+
import sys # 用於打印調試信息
|
| 1299 |
|
| 1300 |
# =========================================
|
| 1301 |
# == 常數定義 (Constants) ==
|
| 1302 |
# =========================================
|
| 1303 |
+
CHART_TYPES = ["長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "折線圖", "多重折線圖", "階梯折線圖", "區域圖", "堆疊區域圖", "百分比堆疊區域圖", "圓餅圖", "環形圖", "散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "熱力圖", "樹狀圖", "雷達圖", "漏斗圖", "極座標圖", "甘特圖"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1304 |
COLOR_SCHEMES = {
|
| 1305 |
+
"預設 (Plotly)": px.colors.qualitative.Plotly, "分類 - D3": px.colors.qualitative.D3, "分類 - G10": px.colors.qualitative.G10, "分類 - T10": px.colors.qualitative.T10, "分類 - Alphabet": px.colors.qualitative.Alphabet, "分類 - Dark24": px.colors.qualitative.Dark24, "分類 - Light24": px.colors.qualitative.Light24, "分類 - Set1": px.colors.qualitative.Set1, "分類 - Set2": px.colors.qualitative.Set2, "分類 - Set3": px.colors.qualitative.Set3, "分類 - Pastel": px.colors.qualitative.Pastel, "分類 - Pastel1": px.colors.qualitative.Pastel1, "分類 - Pastel2": px.colors.qualitative.Pastel2, "分類 - Antique": px.colors.qualitative.Antique, "分類 - Bold": px.colors.qualitative.Bold, "分類 - Prism": px.colors.qualitative.Prism, "分類 - Safe": px.colors.qualitative.Safe, "分類 - Vivid": px.colors.qualitative.Vivid,
|
| 1306 |
+
"連續 - Viridis": px.colors.sequential.Viridis, "連續 - Plasma": px.colors.sequential.Plasma, "連續 - Inferno": px.colors.sequential.Inferno, "連續 - Magma": px.colors.sequential.Magma, "連續 - Cividis": px.colors.sequential.Cividis, "連續 - Blues": px.colors.sequential.Blues, "連續 - Reds": px.colors.sequential.Reds, "連續 - Greens": px.colors.sequential.Greens, "連續 - Purples": px.colors.sequential.Purples, "連續 - Oranges": px.colors.sequential.Oranges, "連續 - Greys": px.colors.sequential.Greys, "連續 - Rainbow": px.colors.sequential.Rainbow, "連續 - Turbo": px.colors.sequential.Turbo, "連續 - Electric": px.colors.sequential.Electric, "連續 - Hot": px.colors.sequential.Hot, "連續 - Teal": px.colors.sequential.Teal,
|
| 1307 |
+
"發散 - Spectral": px.colors.diverging.Spectral, "發散 - RdBu": px.colors.diverging.RdBu, "發散 - PRGn": px.colors.diverging.PRGn, "發散 - PiYG": px.colors.diverging.PiYG, "發散 - BrBG": px.colors.diverging.BrBG, "發散 - Geyser": px.colors.diverging.Geyser,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1308 |
"循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
|
| 1309 |
}
|
|
|
|
|
|
|
| 1310 |
PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
|
| 1311 |
+
AGGREGATION_FUNCTIONS = ["計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1312 |
EXPORT_FORMATS_DATA = ["CSV", "Excel", "JSON"]
|
| 1313 |
EXPORT_FORMATS_IMG = ["PNG", "SVG", "PDF", "JPEG"]
|
| 1314 |
YES_NO_CHOICES = ["是", "否"]
|
| 1315 |
+
NO_DATA_STR = "-- 無數據 --"
|
| 1316 |
+
NONE_STR = "無" # 代表 '無' 選項的值
|
| 1317 |
|
| 1318 |
# =========================================
|
| 1319 |
# == 輔助函數 (Helper Functions) ==
|
| 1320 |
# =========================================
|
| 1321 |
+
# --- 顏色處理相關 (與 V4 相同) ---
|
|
|
|
| 1322 |
COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
|
| 1323 |
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>"""
|
| 1324 |
COPY_SCRIPT = """
|
|
|
|
| 1345 |
</script>
|
| 1346 |
"""
|
| 1347 |
COMMON_COLORS = {
|
| 1348 |
+
"紅色": "#FF0000", "亮紅": "#FF5733", "深紅": "#C70039", "橙色": "#FFA500", "亮橙": "#FFC300", "深橙": "#D35400", "黃色": "#FFFF00", "亮黃": "#F1C40F", "金色": "#FFD700", "綠色": "#008000", "亮綠": "#2ECC71", "深綠": "#1E8449", "橄欖綠": "#808000", "藍色": "#0000FF", "亮藍": "#3498DB", "深藍": "#2874A6", "天藍": "#87CEEB", "紫色": "#800080", "亮紫": "#9B59B6", "深紫": "#6C3483", "薰衣草紫": "#E6E6FA", "粉紅色": "#FFC0CB", "亮粉": "#FF69B4", "深粉": "#C71585", "棕色": "#A52A2A", "亮棕": "#E59866", "深棕": "#6E2C00", "青色": "#00FFFF", "藍綠色": "#008080", "綠松石色": "#40E0D0", "洋紅": "#FF00FF", "紫紅色": "#DC143C", "灰色": "#808080", "淺灰": "#D3D3D3", "深灰": "#696969", "石板灰": "#708090", "黑色": "#000000", "白色": "#FFFFFF", "米色": "#F5F5DC",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
}
|
| 1350 |
def generate_gradient_colors(start_color, end_color, steps=10):
|
| 1351 |
+
def hex_to_rgb(hex_color): hex_color = hex_color.lstrip('#'); return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
| 1352 |
+
def rgb_to_hex(rgb): r, g, b = [max(0, min(255, int(c))) for c in rgb]; return '#{:02x}{:02x}{:02x}'.format(r, g, b)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1353 |
try:
|
| 1354 |
+
start_rgb, end_rgb = hex_to_rgb(start_color), hex_to_rgb(end_color);
|
| 1355 |
if steps <= 1: return [start_color] if steps == 1 else []
|
| 1356 |
r_step, g_step, b_step = [(end_rgb[i] - start_rgb[i]) / (steps - 1) for i in range(3)]
|
| 1357 |
return [rgb_to_hex((start_rgb[0] + r_step * i, start_rgb[1] + g_step * i, start_rgb[2] + b_step * i)) for i in range(steps)]
|
| 1358 |
+
except Exception as e: print(f"生成漸變色時出錯: {e}"); return [start_color, end_color]
|
| 1359 |
+
GRADIENTS = {"紅→黃": generate_gradient_colors("#FF0000", "#FFFF00"), "藍→綠": generate_gradient_colors("#0000FF", "#00FF00"), "紫→粉": generate_gradient_colors("#800080", "#FFC0CB"), "紅→藍": generate_gradient_colors("#FF0000", "#0000FF"), "黑→白": generate_gradient_colors("#000000", "#FFFFFF"), "藍→紅 (發散)": generate_gradient_colors("#0000FF", "#FF0000"), "綠→紫 (發散)": generate_gradient_colors("#00FF00", "#800080"), "彩虹 (簡易)": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1360 |
def generate_color_cards():
|
| 1361 |
common_cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code) for name, hex_code in COMMON_COLORS.items()])
|
| 1362 |
gradient_cards_html = ""
|
|
|
|
| 1382 |
except Exception as e: print(f"解析自定義顏色時出錯: {e}"); return {}
|
| 1383 |
return custom_colors
|
| 1384 |
def update_patterns(*patterns_input):
|
|
|
|
| 1385 |
return [p for p in patterns_input if p in PATTERN_TYPES and p != "無"]
|
| 1386 |
|
|
|
|
| 1387 |
# =========================================
|
| 1388 |
# == 數據處理函數 (Data Processing Functions) ==
|
| 1389 |
# =========================================
|
| 1390 |
def process_upload(file):
|
| 1391 |
+
# (與 V4 相同)
|
| 1392 |
if file is None: return None, "❌ 未上傳任何文件。"
|
| 1393 |
try:
|
| 1394 |
file_path = file.name; file_type = file_path.split('.')[-1].lower()
|
|
|
|
| 1403 |
except Exception as e: return None, f"❌ 讀取 Excel 文件時出錯: {e}"
|
| 1404 |
else: return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"
|
| 1405 |
df.columns = df.columns.str.strip()
|
|
|
|
|
|
|
|
|
|
| 1406 |
return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"
|
| 1407 |
except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"
|
| 1408 |
|
| 1409 |
def parse_data(text_data):
|
| 1410 |
+
# (與 V4 相同)
|
| 1411 |
if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
|
| 1412 |
try:
|
| 1413 |
data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
|
| 1414 |
if ',' in first_line: separator = ','
|
| 1415 |
elif '\t' in first_line: separator = '\t'
|
| 1416 |
elif ' ' in first_line: separator = r'\s+'
|
| 1417 |
+
else: separator = ','
|
| 1418 |
+
try: df = pd.read_csv(data_io, sep=separator, skipinitialspace=True)
|
|
|
|
| 1419 |
except pd.errors.ParserError as pe: return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
|
| 1420 |
except Exception as e: return None, f"❌ 解析數據時出錯: {e}"
|
| 1421 |
df.columns = df.columns.str.strip()
|
|
|
|
|
|
|
|
|
|
| 1422 |
return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"
|
| 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 |
+
|
| 1430 |
if df is None or df.empty:
|
| 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, no_data_update_with_none, no_data_update_with_none
|
| 1435 |
|
| 1436 |
try:
|
| 1437 |
columns = df.columns.tolist()
|
|
|
|
|
|
|
| 1438 |
valid_columns = [str(col) for col in columns if col is not None and str(col) != ""] # 確保列名是字符串
|
| 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, no_data_update_with_none, no_data_update_with_none
|
| 1443 |
+
|
| 1444 |
+
x_default = valid_columns[0]
|
| 1445 |
+
# Y 軸預設選第二個(如果存在),否則選第一個
|
| 1446 |
+
y_default = valid_columns[1] if len(valid_columns) > 1 else valid_columns[0]
|
| 1447 |
+
|
| 1448 |
+
# 為 group 和 size 添加 "無" 選項
|
| 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=valid_columns, value=y_default, label="Y軸 / 數值"),
|
| 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 |
return no_data_update, no_data_update, no_data_update_with_none, no_data_update_with_none
|
| 1463 |
|
| 1464 |
|
|
|
|
| 1468 |
def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
|
| 1469 |
color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
|
| 1470 |
show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
|
| 1471 |
+
# --- 添加調試信息 ---
|
| 1472 |
+
print("-" * 20, file=sys.stderr)
|
| 1473 |
+
print(f"調用 create_plot:", file=sys.stderr)
|
| 1474 |
+
print(f" - df type: {type(df)}", file=sys.stderr)
|
| 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)
|
| 1485 |
+
print("-" * 20, file=sys.stderr)
|
| 1486 |
+
# --- 結束調試信息 ---
|
| 1487 |
+
|
| 1488 |
fig = go.Figure()
|
| 1489 |
try:
|
| 1490 |
# --- 0. 將 "是"/"否" 轉換為布林值 ---
|
|
|
|
| 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: # 嚴格檢查 df
|
| 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 |
# 只有在非計數且非直方圖時才嚴格要求 Y 軸
|
| 1505 |
y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]
|
| 1506 |
if y_needed:
|
| 1507 |
+
if not y_column or y_column == NO_DATA_STR: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列。")
|
| 1508 |
+
if y_column not in df.columns: raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1509 |
else:
|
| 1510 |
y_column = None # 如果不需要 Y 軸,明確設為 None
|
| 1511 |
|
| 1512 |
# 處理可選列
|
| 1513 |
+
group_col = None if group_column == NONE_STR or not group_column else group_column
|
| 1514 |
+
size_col = None if size_column == NONE_STR or not size_column else size_column
|
| 1515 |
|
| 1516 |
+
if group_col and group_col not in df.columns: raise ValueError(f"分組列 '{group_col}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1517 |
+
if size_col and size_col not in df.columns: raise ValueError(f"大小列 '{size_col}' 不在數據中。可用列: {', '.join(df.columns)}")
|
| 1518 |
if group_col == x_column: raise ValueError("分組列不能與 X 軸列相同。")
|
|
|
|
| 1519 |
|
| 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 y_column:
|
| 1527 |
try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='coerce')
|
| 1528 |
except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column}' 為數值時出錯: {e}")
|
|
|
|
| 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 = y_column
|
| 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 y_column: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
|
|
|
|
| 1548 |
if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_column]):
|
|
|
|
| 1549 |
try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='raise')
|
| 1550 |
except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
|
|
|
|
| 1551 |
try:
|
|
|
|
| 1552 |
agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd).reset_index()
|
| 1553 |
+
y_col_agg = y_column
|
| 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 = y_column
|
| 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)
|
| 1564 |
missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
|
| 1565 |
+
if missing_cols: raise ValueError(f"聚合後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")
|
|
|
|
|
|
|
| 1566 |
|
| 1567 |
# --- 4. 獲取顏色方案 ---
|
| 1568 |
colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)
|
|
|
|
| 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 |
+
# --- (繪圖邏輯 - 與 V4 相同,省略) ---
|
| 1576 |
if chart_type == "長條圖":
|
| 1577 |
if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
|
| 1578 |
fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
|
|
|
|
| 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 y_column: raise ValueError("散點圖需要選擇 Y 軸列。")
|
| 1623 |
fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
|
| 1624 |
elif chart_type == "氣泡圖":
|
| 1625 |
if not y_column: 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(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
|
| 1679 |
elif chart_type == "甘特圖":
|
| 1680 |
+
start_col_gantt = y_column; end_col_gantt = group_col; task_col_gantt = x_column
|
|
|
|
|
|
|
|
|
|
| 1681 |
if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
|
| 1682 |
try:
|
| 1683 |
+
df_gantt = df.copy()
|
| 1684 |
if start_col_gantt not in df_gantt.columns: raise ValueError(f"開始列 '{start_col_gantt}' 不在數據中。")
|
| 1685 |
if end_col_gantt not in df_gantt.columns: raise ValueError(f"結束列 '{end_col_gantt}' 不在數據中。")
|
| 1686 |
if task_col_gantt not in df_gantt.columns: raise ValueError(f"任務列 '{task_col_gantt}' 不在數據中。")
|
|
|
|
| 1699 |
|
| 1700 |
|
| 1701 |
# --- 6. 應用圖案 (如果支持) ---
|
| 1702 |
+
# (與 V4 相同,省略)
|
| 1703 |
if patterns:
|
| 1704 |
try:
|
| 1705 |
num_traces = len(fig.data)
|
|
|
|
| 1722 |
dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
|
| 1723 |
for i, trace in enumerate(fig.data):
|
| 1724 |
pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
|
| 1725 |
+
if dash: trace.line.dash = dash; trace.fill = 'tonexty' if hasattr(trace, 'stackgroup') and trace.stackgroup else 'tozeroy'
|
| 1726 |
except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")
|
| 1727 |
|
| 1728 |
# --- 7. 更新佈局 ---
|
| 1729 |
+
# (與 V4 相同)
|
| 1730 |
fig.update_layout(
|
| 1731 |
showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
|
| 1732 |
template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
|
|
|
|
| 1734 |
hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
|
| 1735 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
|
| 1736 |
)
|
|
|
|
| 1737 |
final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
|
| 1738 |
if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
|
| 1739 |
elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
|
| 1740 |
elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
|
| 1741 |
+
elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column)
|
| 1742 |
else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)
|
| 1743 |
|
| 1744 |
except ValueError as ve:
|
| 1745 |
+
print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
|
| 1746 |
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)
|
| 1747 |
except Exception as e:
|
| 1748 |
+
error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
|
| 1749 |
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)
|
| 1750 |
return fig
|
| 1751 |
|
| 1752 |
# =========================================
|
| 1753 |
# == 導出與下載函數 (Export & Download Functions) ==
|
| 1754 |
# =========================================
|
| 1755 |
+
# (與 V4 相同)
|
| 1756 |
def export_data(df, format_type):
|
| 1757 |
if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
|
| 1758 |
try:
|
|
|
|
| 1767 |
if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
|
| 1768 |
try:
|
| 1769 |
format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
|
| 1770 |
+
import kaleido # 確保導入
|
|
|
|
| 1771 |
fig.write_image(filename, format=format_lower)
|
| 1772 |
return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
|
| 1773 |
+
except ImportError: error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
|
|
|
|
|
|
|
| 1774 |
except ValueError as ve:
|
| 1775 |
+
if "kaleido" in str(ve).lower(): error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"; print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
|
| 1776 |
+
else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
|
| 1777 |
+
except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1778 |
|
| 1779 |
# =========================================
|
| 1780 |
# == 智能推薦函數 (Recommendation Function) ==
|
| 1781 |
# =========================================
|
| 1782 |
+
# (與 V4 相同)
|
| 1783 |
def recommend_chart_settings(df):
|
| 1784 |
recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
|
| 1785 |
if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
|
|
|
|
| 1794 |
elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
|
| 1795 |
else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
|
| 1796 |
except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
|
|
|
|
| 1797 |
if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
|
| 1798 |
if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
|
| 1799 |
if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
|
|
|
|
| 1800 |
if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
|
| 1801 |
+
if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
|
| 1802 |
return recommendation
|
| 1803 |
|
| 1804 |
# =========================================
|
| 1805 |
# == CSS 樣式 (CSS Styling) ==
|
| 1806 |
# =========================================
|
| 1807 |
+
# (與 V4 相同)
|
| 1808 |
CUSTOM_CSS = """
|
| 1809 |
/* --- 全局和容器 --- */
|
| 1810 |
.gradio-container { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
|
|
|
|
| 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 |
|
| 1867 |
# --- 狀態變量 ---
|
| 1868 |
data_state = gr.State(None)
|
| 1869 |
+
custom_colors_state = gr.State({}) # 只保留一組狀態
|
| 1870 |
+
patterns_state = gr.State([])
|
| 1871 |
+
recommendation_state = gr.State({})
|
|
|
|
|
|
|
| 1872 |
|
| 1873 |
# --- 主頁籤佈局 ---
|
| 1874 |
with gr.Tabs() as tabs:
|
|
|
|
| 1893 |
gr.Markdown("下方將顯示載入或解析後的數據預覽。")
|
| 1894 |
data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
|
| 1895 |
with gr.Row():
|
| 1896 |
+
export_format = gr.Radio(EXPORT_FORMATS_DATA, label="選擇導出格式", value="CSV") # Radio
|
| 1897 |
export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
|
| 1898 |
export_result = gr.File(label="導出文件下載", interactive=False)
|
| 1899 |
export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
| 1900 |
|
| 1901 |
+
# --- 圖表創建頁籤 (單圖表, 移除 Accordion, 使用 Radio) ---
|
| 1902 |
+
with gr.TabItem("📈 圖表創建", id=1):
|
| 1903 |
+
gr.HTML('<div class="section-title">創建圖表</div>')
|
| 1904 |
+
gr.Markdown("在此設置並生成圖表。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1905 |
|
| 1906 |
+
with gr.Row(): # 主 Row
|
| 1907 |
+
# --- 設定欄 (左側 Column) ---
|
| 1908 |
with gr.Column(scale=1):
|
| 1909 |
+
gr.Markdown("### 📊 圖表設置")
|
| 1910 |
with gr.Group(elem_classes=["card"]):
|
| 1911 |
gr.Markdown("**基本設置**")
|
| 1912 |
+
chart_type = gr.Radio(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True) # Radio
|
| 1913 |
+
recommend_button = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm")
|
| 1914 |
+
chart_title = gr.Textbox(label="圖表標題", placeholder="我的圖表")
|
| 1915 |
+
agg_function = gr.Radio(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數") # Radio
|
| 1916 |
+
|
| 1917 |
+
gr.Markdown("**數據映射 (請選擇)**")
|
| 1918 |
+
# 使用 Radio 進行欄位選擇 - 如果欄位過多會很長!
|
| 1919 |
+
x_column = gr.Radio([NO_DATA_STR], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
|
| 1920 |
+
y_column = gr.Radio([NO_DATA_STR], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
|
| 1921 |
+
group_column = gr.Radio([NONE_STR, NO_DATA_STR], label="分組列", info="用於生成多系列或堆疊", value=NONE_STR)
|
| 1922 |
+
size_column = gr.Radio([NONE_STR, NO_DATA_STR], label="大小列", info="用於氣泡圖等控制點的大小", value=NONE_STR)
|
| 1923 |
|
| 1924 |
gr.Markdown("**顯示選項**")
|
| 1925 |
+
chart_width = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
|
| 1926 |
+
chart_height = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
|
| 1927 |
+
show_grid = gr.Radio(YES_NO_CHOICES, label="顯示網格", value="是") # Radio
|
| 1928 |
+
show_legend = gr.Radio(YES_NO_CHOICES, label="顯示圖例", value="是") # Radio
|
| 1929 |
+
color_scheme = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)") # 保留 Dropdown
|
| 1930 |
+
gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
|
| 1931 |
+
gr.HTML(generate_color_cards(), elem_id="color_display")
|
| 1932 |
|
| 1933 |
gr.Markdown("**圖案與自定義顏色**")
|
| 1934 |
+
pattern1 = gr.Radio(PATTERN_TYPES, label="圖案1", value="無") # Radio
|
| 1935 |
+
pattern2 = gr.Radio(PATTERN_TYPES, label="圖案2", value="無") # Radio
|
| 1936 |
+
pattern3 = gr.Radio(PATTERN_TYPES, label="圖案3", value="無") # Radio
|
| 1937 |
+
color_customization = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])
|
| 1938 |
+
|
| 1939 |
+
# --- 預覽與操作欄 (右側 Column) ---
|
| 1940 |
+
with gr.Column(scale=2):
|
| 1941 |
+
# 操作按鈕 (預覽上方)
|
| 1942 |
+
gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">操作</div>')
|
| 1943 |
+
update_button = gr.Button("🔄 更新圖表", variant="primary", elem_classes=["primary-button"])
|
| 1944 |
with gr.Row():
|
| 1945 |
+
export_img_format = gr.Radio(EXPORT_FORMATS_IMG, label="導出格式", value="PNG", scale=1) # Radio
|
| 1946 |
+
download_button = gr.Button("💾 導出圖表", elem_classes=["secondary-button"], scale=1)
|
| 1947 |
+
export_chart = gr.File(label="圖表文件下載", interactive=False)
|
| 1948 |
+
export_chart_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)
|
|
|
|
| 1949 |
|
| 1950 |
+
# 預覽區域
|
| 1951 |
+
gr.HTML('<div class="section-title" style="margin-top:20px; margin-bottom:10px;">圖表預覽</div>')
|
| 1952 |
with gr.Group(elem_classes=["chart-previewer"]):
|
| 1953 |
+
chart_output = gr.Plot(label="", elem_id="chart_preview")
|
| 1954 |
|
| 1955 |
|
| 1956 |
# --- 使用說明頁籤 ---
|
| 1957 |
with gr.TabItem("❓ 使用說明", id=2):
|
| 1958 |
with gr.Group(elem_classes=["card"]):
|
| 1959 |
gr.HTML("""
|
| 1960 |
+
<div class="section-title">使用說明 (V5 - 極簡測試版)</div>
|
| 1961 |
<h3>數據輸入</h3>
|
| 1962 |
<ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
|
| 1963 |
+
<h3>圖表創建</h3>
|
| 1964 |
+
<ul><li>此頁面僅提供一個圖表進行測試。</li><li><strong>智能推薦:</strong>點擊 "智能推薦" 按鈕,系統會根據數據結構嘗試推薦合適的設置。</li><li><strong>圖表類型/聚合函數/圖案等:</strong>使用點選按鈕 (Radio) 進行選擇。</li>
|
| 1965 |
+
<ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong>若要統計某一欄位中各個項目出現的次數,請在 <strong>X軸/類別</strong> 選擇該欄位,並將 <strong>聚合函數</strong> 設為 <strong>計數</strong>,此時 <strong>無需選擇 Y軸/數值</strong>。然後選擇「長條圖」或「圓餅圖」。</li></ul>
|
| 1966 |
+
</li><li><strong>數據映射 (欄位選擇):</strong>使用點選按鈕 (Radio) 選擇 X軸、Y軸、分組列、大小列。<strong>注意:如果數據欄位過多,這裡會顯示得很長。</strong></li><li><strong>顯示選項/顏色/自定義:</strong>調整圖表外觀。顏色方案仍為下拉選單。</li><li>點擊 <strong>"更新圖表"</strong> 按鈕生成或刷新圖表預覽 (已移除自動更新)。</li><li>使用 "導出圖表" 功能將生成的圖表保存為圖片文件。</li></ul>
|
| 1967 |
<h3>提示</h3>
|
| 1968 |
+
<ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>如果欄位選擇的 Radio 按鈕區域過長或無法使用,表示此方法不適用於您的數據,且 Gradio 可能存在根本的元件衝突問題。</li></ul>
|
| 1969 |
""")
|
| 1970 |
|
| 1971 |
# =========================================
|
|
|
|
| 1973 |
# =========================================
|
| 1974 |
|
| 1975 |
# --- 數據載入與更新 ---
|
| 1976 |
+
def load_data_and_update_ui_v5(df, status_msg):
|
| 1977 |
+
"""輔助函數:更新數據狀態、預覽和所有列選擇 Radio"""
|
| 1978 |
+
print("調用 load_data_and_update_ui_v5...", file=sys.stderr)
|
| 1979 |
preview_df = df if df is not None else pd.DataFrame()
|
| 1980 |
+
# 更新列選擇 Radio
|
| 1981 |
+
col_updates = update_columns_as_radio(df)
|
| 1982 |
if col_updates is None or len(col_updates) != 4:
|
| 1983 |
+
print("警告: update_columns_as_radio 未返回預期的 4 個組件更新。", file=sys.stderr)
|
| 1984 |
+
# 返回空更新,避免錯誤 (狀態, 消息, 預覽, 4個Radio, 1個Plot)
|
| 1985 |
+
return [df, status_msg, preview_df] + [gr.update()] * 4 + [gr.Plot(value=None)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1986 |
|
| 1987 |
+
# 準備所有更新 (狀態, 消息, 預覽表格, 4 個 Radio)
|
| 1988 |
+
updates = [df, status_msg, preview_df] + list(col_updates)
|
| 1989 |
+
# 添加一個空的 Plot 更新
|
| 1990 |
+
updates.append(gr.Plot(value=None)) # 初始不繪圖
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1991 |
|
| 1992 |
+
print(f"load_data_and_update_ui_v5 返回 {len(updates)} 個更新。", file=sys.stderr)
|
| 1993 |
return updates
|
| 1994 |
|
| 1995 |
+
# 綁定數據載入事件 - 移除初始繪圖
|
|
|
|
| 1996 |
upload_button.click(
|
| 1997 |
process_upload,
|
| 1998 |
inputs=[file_upload],
|
| 1999 |
outputs=[data_state, upload_status]
|
| 2000 |
).then(
|
| 2001 |
+
load_data_and_update_ui_v5,
|
| 2002 |
inputs=[data_state, upload_status],
|
| 2003 |
outputs=[
|
| 2004 |
data_state, upload_status, data_preview,
|
| 2005 |
+
x_column, y_column, group_column, size_column, # 更新 Radio
|
| 2006 |
+
chart_output # 更新 Plot 為空
|
|
|
|
|
|
|
| 2007 |
]
|
| 2008 |
)
|
| 2009 |
parse_button.click(
|
|
|
|
| 2011 |
inputs=[csv_input],
|
| 2012 |
outputs=[data_state, parse_status]
|
| 2013 |
).then(
|
| 2014 |
+
load_data_and_update_ui_v5,
|
| 2015 |
inputs=[data_state, parse_status],
|
| 2016 |
outputs=[
|
| 2017 |
data_state, parse_status, data_preview,
|
| 2018 |
+
x_column, y_column, group_column, size_column,
|
| 2019 |
+
chart_output
|
|
|
|
| 2020 |
]
|
| 2021 |
)
|
| 2022 |
|
|
|
|
| 2024 |
# --- 數據導出 ---
|
| 2025 |
export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])
|
| 2026 |
|
| 2027 |
+
# --- 顏色和圖案狀態 ---
|
| 2028 |
+
color_customization.change(parse_custom_colors, inputs=[color_customization], outputs=[custom_colors_state])
|
| 2029 |
+
patterns_inputs = [pattern1, pattern2, pattern3]
|
| 2030 |
+
for pattern_radio in patterns_inputs: # 改為 Radio
|
| 2031 |
+
pattern_radio.change(update_patterns, inputs=patterns_inputs, outputs=[patterns_state])
|
| 2032 |
+
|
| 2033 |
+
# --- 更新圖表 (僅通過按鈕) ---
|
| 2034 |
+
chart_inputs = [data_state, chart_type, x_column, y_column, group_column, size_column, color_scheme, patterns_state, chart_title, chart_width, chart_height, show_grid, show_legend, agg_function, custom_colors_state]
|
| 2035 |
+
|
| 2036 |
+
def update_chart_action(*inputs):
|
| 2037 |
+
"""按鈕點擊時的處理函數,包含調試信息"""
|
| 2038 |
+
print("="*30, file=sys.stderr)
|
| 2039 |
+
print("更新圖表按鈕點擊!", file=sys.stderr)
|
| 2040 |
+
# 打印傳入 create_plot 的數據狀態
|
| 2041 |
+
df_input = inputs[0] # data_state 在列表第一個
|
| 2042 |
+
print(f" - data_state type in handler: {type(df_input)}", file=sys.stderr)
|
| 2043 |
+
if isinstance(df_input, pd.DataFrame):
|
| 2044 |
+
print(f" - data_state empty in handler: {df_input.empty}", file=sys.stderr)
|
| 2045 |
+
else:
|
| 2046 |
+
print(f" - data_state is not a DataFrame!", file=sys.stderr)
|
| 2047 |
+
print("="*30, file=sys.stderr)
|
| 2048 |
+
return create_plot(*inputs)
|
| 2049 |
|
| 2050 |
+
update_button.click(update_chart_action, inputs=chart_inputs, outputs=[chart_output])
|
| 2051 |
|
| 2052 |
+
# --- 導出圖表 ---
|
| 2053 |
+
download_button.click(download_figure, inputs=[chart_output, export_img_format], outputs=[export_chart, export_chart_status])
|
| 2054 |
|
| 2055 |
+
# --- 智能推薦 ---
|
| 2056 |
+
def apply_recommendation_v5(rec_dict):
|
| 2057 |
if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
|
| 2058 |
chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
|
| 2059 |
+
y_col_val = None if agg_func_val == "計數" else rec_dict.get("y_column"); group_col_val = rec_dict.get("group_column", NONE_STR) # 默認為 "無"
|
| 2060 |
# 返回 Radio 和 Dropdown 的更新
|
| 2061 |
return [
|
| 2062 |
gr.Radio(value=chart_type_val), # 更新 Chart Type Radio
|
| 2063 |
+
gr.Radio(value=x_col_val), # 更新 X Column Radio
|
| 2064 |
+
gr.Radio(value=y_col_val), # 更新 Y Column Radio
|
| 2065 |
+
gr.Radio(value=group_col_val), # 更新 Group Column Radio
|
| 2066 |
+
gr.Radio(value=agg_func_val) # 更新 Agg Function Radio
|
| 2067 |
]
|
| 2068 |
|
| 2069 |
+
recommend_button.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
|
| 2070 |
+
apply_recommendation_v5, inputs=[recommendation_state], outputs=[chart_type, x_column, y_column, group_column, agg_function]
|
| 2071 |
+
) # 應用推薦後不自動更新圖表,讓用戶點擊按鈕
|
| 2072 |
+
|
| 2073 |
+
# --- 圖表類型改變時更新 UI 元素可見性 ---
|
| 2074 |
+
def update_element_visibility_v5(chart_type):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2075 |
try:
|
| 2076 |
+
# (與 V4 相同的邏輯)
|
| 2077 |
is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
|
| 2078 |
is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
|
| 2079 |
is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
|
|
|
|
| 2088 |
elif is_heatmap: group_label, group_needed = "行/列 分組", True
|
| 2089 |
size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
|
| 2090 |
if is_gantt: size_label, size_needed = "顏色列 (可選)", True
|
| 2091 |
+
# 返回 Radio 的更新對象
|
| 2092 |
return (gr.update(label=y_label, visible=y_needed), gr.update(label=group_label, visible=group_needed), gr.update(label=size_label, visible=size_needed))
|
| 2093 |
except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())
|
| 2094 |
|
| 2095 |
# 綁定到 Radio chart_type 的 change 事件
|
| 2096 |
+
chart_type.change(update_element_visibility_v5, inputs=[chart_type], outputs=[y_column, group_column, size_column])
|
|
|
|
| 2097 |
|
| 2098 |
# =========================================
|
| 2099 |
# == 應用程式啟動 (Launch Application) ==
|