s880453 commited on
Commit
a866508
·
verified ·
1 Parent(s): dd03fc6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +78 -1563
app.py CHANGED
@@ -1,1587 +1,102 @@
1
  import gradio as gr
2
  import pandas as pd
3
- import numpy as np
4
  import plotly.express as px
5
  import plotly.graph_objects as go
6
- import io
7
- import base64
8
- from PIL import Image
9
- import matplotlib.pyplot as plt
10
- import seaborn as sns
11
- from plotly.subplots import make_subplots
12
- import re
13
- import json
14
- import colorsys
15
 
16
- # 擴展的圖表類型
17
- CHART_TYPES = [
18
- "長條圖", "堆疊長條圖", "群組長條圖", "水平長條圖",
19
- "折線圖", "多重折線圖", "階梯折線圖",
20
- "圓餅圖", "環形圖", "散點圖", "氣泡圖",
21
- "區域圖", "堆疊區域圖", "雷達圖", "熱力圖",
22
- "箱型圖", "小提琴圖", "漏斗圖", "樹狀圖",
23
- "直方圖", "極座標圖", "甘特圖"
24
- ]
25
-
26
- # 可用的顏色方案 (美觀且有實用價值的顏色選擇)
27
- COLOR_SCHEMES = {
28
- "默認": px.colors.qualitative.Plotly,
29
- "Pastel": px.colors.qualitative.Pastel,
30
- "Safe": px.colors.qualitative.Safe,
31
- "Vivid": px.colors.qualitative.Vivid,
32
- "Prism": px.colors.qualitative.Prism,
33
- "Antique": px.colors.qualitative.Antique,
34
- "Bold": px.colors.qualitative.Bold,
35
- "Pastel1": px.colors.qualitative.Pastel1,
36
- "Pastel2": px.colors.qualitative.Pastel2,
37
- "Set1": px.colors.qualitative.Set1,
38
- "Set2": px.colors.qualitative.Set2,
39
- "Set3": px.colors.qualitative.Set3,
40
- "藍綠色系": px.colors.sequential.Blues,
41
- "紅色系": px.colors.sequential.Reds,
42
- "綠色系": px.colors.sequential.Greens,
43
- "紫色系": px.colors.sequential.Purples,
44
- "灰度": px.colors.sequential.Greys,
45
- "彩虹": px.colors.sequential.Rainbow,
46
- "漸變藍綠": px.colors.sequential.Turbo,
47
- "漸變紫橙": px.colors.diverging.Spectral,
48
- "漸變紅藍": px.colors.diverging.RdBu,
49
- "漸變棕綠": px.colors.diverging.BrBG
50
- }
51
-
52
- # 圖案填充類型 (黑白印刷用)
53
- PATTERN_TYPES = [
54
- "無", "/", "\\", "x", "-", "|", "+", ".", "*"
55
- ]
56
-
57
- # 統計函數選項
58
- AGGREGATION_FUNCTIONS = [
59
- "計數", "求和", "平均值", "最大值", "最小值", "中位數", "標準差", "變異數"
60
- ]
61
-
62
- # HTML顏色展示卡片樣式
63
- COLOR_CARD_STYLE = """
64
- <div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">
65
- {color_cards}
66
- </div>
67
- """
68
-
69
- COLOR_CARD_TEMPLATE = """
70
- <div title="{color_name}" style="
71
- width: 25px;
72
- height: 25px;
73
- background-color: {color_hex};
74
- border-radius: 3px;
75
- cursor: pointer;
76
- border: 1px solid #ddd;
77
- transition: transform 0.2s;
78
- " onclick="copyToClipboard('{color_hex}')" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'"></div>
79
- """
80
-
81
- COPY_SCRIPT = """
82
- <script>
83
- function copyToClipboard(text) {
84
- navigator.clipboard.writeText(text);
85
- const notification = document.createElement('div');
86
- notification.textContent = '已複製: ' + text;
87
- notification.style.position = 'fixed';
88
- notification.style.bottom = '20px';
89
- notification.style.right = '20px';
90
- notification.style.padding = '10px';
91
- notification.style.background = '#333';
92
- notification.style.color = 'white';
93
- notification.style.borderRadius = '4px';
94
- notification.style.zIndex = '1000';
95
- document.body.appendChild(notification);
96
- setTimeout(() => {
97
- notification.style.opacity = '0';
98
- notification.style.transition = 'opacity 0.5s';
99
- setTimeout(() => document.body.removeChild(notification), 500);
100
- }, 1500);
101
- }
102
- </script>
103
- """
104
-
105
- # 常見的顏色名稱和十六進制代碼
106
- COMMON_COLORS = {
107
- "紅色": "#FF0000", "橙色": "#FFA500", "黃色": "#FFFF00", "綠色": "#008000",
108
- "藍色": "#0000FF", "紫色": "#800080", "粉紅色": "#FFC0CB", "棕色": "#A52A2A",
109
- "灰色": "#808080", "黑色": "#000000", "白色": "#FFFFFF", "青色": "#00FFFF",
110
- "洋紅": "#FF00FF", "淺藍": "#ADD8E6", "淺綠": "#90EE90", "淺黃": "#FFFFE0"
111
- }
112
-
113
- # 生成漸變色系
114
- def generate_gradient_colors(start_color, end_color, steps=10):
115
- def hex_to_rgb(hex_color):
116
- hex_color = hex_color.lstrip('#')
117
- return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
118
-
119
- def rgb_to_hex(rgb):
120
- return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2]))
121
-
122
- start_rgb = hex_to_rgb(start_color)
123
- end_rgb = hex_to_rgb(end_color)
124
-
125
- r_step = (end_rgb[0] - start_rgb[0]) / (steps - 1)
126
- g_step = (end_rgb[1] - start_rgb[1]) / (steps - 1)
127
- b_step = (end_rgb[2] - start_rgb[2]) / (steps - 1)
128
-
129
- gradient_colors = []
130
- for i in range(steps):
131
- r = start_rgb[0] + r_step * i
132
- g = start_rgb[1] + g_step * i
133
- b = start_rgb[2] + b_step * i
134
- gradient_colors.append(rgb_to_hex((r, g, b)))
135
-
136
- return gradient_colors
137
-
138
- # 為顏色選擇添加的各種漸變色系
139
- GRADIENTS = {
140
- "紅→黃": generate_gradient_colors("#FF0000", "#FFFF00"),
141
- "藍→綠": generate_gradient_colors("#0000FF", "#00FF00"),
142
- "紫→粉": generate_gradient_colors("#800080", "#FFC0CB"),
143
- "紅→藍": generate_gradient_colors("#FF0000", "#0000FF"),
144
- "黑→白": generate_gradient_colors("#000000", "#FFFFFF"),
145
- "彩虹": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"]
146
- }
147
-
148
- # 生成顏色卡片展示
149
- def generate_color_cards():
150
- # 常見顏色卡片
151
- common_cards = ""
152
- for name, hex_code in COMMON_COLORS.items():
153
- common_cards += COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code)
154
-
155
- # 漸變色系卡片
156
- gradient_cards = {}
157
- for name, colors in GRADIENTS.items():
158
- gradient_cards[name] = ""
159
- for i, color in enumerate(colors):
160
- gradient_cards[name] += COLOR_CARD_TEMPLATE.format(
161
- color_name=f"{name} {i+1}/{len(colors)}",
162
- color_hex=color
163
- )
164
-
165
- # 合成卡片展示HTML
166
- color_display = f"""
167
- <div style="font-weight: bold; margin-top: 10px;">常見顏色</div>
168
- {COLOR_CARD_STYLE.format(color_cards=common_cards)}
169
- """
170
-
171
- for name, cards in gradient_cards.items():
172
- color_display += f"""
173
- <div style="font-weight: bold; margin-top: 10px;">{name}</div>
174
- {COLOR_CARD_STYLE.format(color_cards=cards)}
175
- """
176
-
177
- color_display += COPY_SCRIPT
178
-
179
- return color_display
180
 
181
- def agg_function_map(func_name):
182
- """映射中文統計函數名稱到Pandas函數"""
183
- mapping = {
184
- "求和": "sum",
185
- "平均值": "mean",
186
- "最大值": "max",
187
- "最小值": "min",
188
- "計數": "count",
189
- "中位數": "median",
190
- "標準差": "std",
191
- "變異數": "var"
192
- }
193
- return mapping.get(func_name, "count")
194
 
195
- def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
196
- color_scheme="默認", patterns=[], title="", width=700, height=500,
197
- show_grid=True, show_legend=True, agg_function="計數", custom_colors={}):
198
- """創建圖表函數"""
199
-
200
- # 數據預處理
201
- if df is None or df.empty:
202
- return go.Figure()
203
-
204
- # 確保列存在
205
- if x_column not in df.columns or y_column not in df.columns:
206
- return go.Figure()
207
-
208
- # 獲取選擇的顏色方案
209
- colors = COLOR_SCHEMES[color_scheme]
210
-
211
- # 將非數值列轉換為類別型
212
- for col in df.columns:
213
- if df[col].dtype == 'object' or df[col].dtype == 'string':
214
- df[col] = df[col].astype('category')
215
-
216
- # 使用pandas的groupby進行數據聚合
217
- agg_func = agg_function_map(agg_function)
218
-
219
- # 設置圖表參數
220
- fig_params = {
221
- "title": title
222
- }
223
-
224
- # 創建基本圖形
225
- fig = go.Figure()
226
-
227
  try:
228
- # 基於選擇的圖表類型創建圖表
229
  if chart_type == "長條圖":
230
- # 進行數據分組和聚合
231
- if group_column and group_column in df.columns and group_column != x_column:
232
- grouped_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
233
- fig = px.bar(grouped_df, x=x_column, y=y_column, color=group_column,
234
- color_discrete_sequence=colors, **fig_params)
235
  else:
236
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
237
- fig = px.bar(grouped_df, x=x_column, y=y_column, color_discrete_sequence=colors, **fig_params)
238
-
239
- # 應用自定義顏色和圖案
240
- if patterns and len(patterns) > 0:
241
- for i, bar in enumerate(fig.data):
242
- pattern_index = i % len(patterns)
243
- if patterns[pattern_index] != "無":
244
- bar.marker.pattern = {
245
- 'shape': patterns[pattern_index],
246
- 'solidity': 0.5
247
- }
248
-
249
- if chart_type == "堆疊長條圖":
250
- if group_column and group_column in df.columns:
251
- # 明確將字符串列轉換為類別型
252
- df[x_column] = df[x_column].astype('category')
253
- df[group_column] = df[group_column].astype('category')
254
-
255
- # 根據使用者選的聚合函數進行分組計算
256
- if agg_func == "count" or y_column == "計數":
257
- grouped_df = df.groupby([x_column, group_column]).size().reset_index(name='__y__')
258
- else:
259
- grouped_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index(name='__y__')
260
-
261
- # 建立樞紐表
262
- pivot_df = grouped_df.pivot_table(index=x_column, columns=group_column,
263
- values='__y__', aggfunc='sum').reset_index().fillna(0)
264
-
265
- # 類別清單
266
- categories = pivot_df.columns.tolist()
267
- categories.remove(x_column)
268
-
269
- # 加入每一組 Bar
270
- for i, category in enumerate(categories):
271
- color = colors[i % len(colors)]
272
- if str(category) in custom_colors:
273
- color = custom_colors[str(category)]
274
-
275
- pattern_shape = None
276
- if patterns and i < len(patterns) and patterns[i] != "無":
277
- pattern_shape = patterns[i]
278
-
279
- fig.add_trace(go.Bar(
280
- x=pivot_df[x_column],
281
- y=pivot_df[category],
282
- name=str(category),
283
- marker_color=color,
284
- marker_pattern_shape=pattern_shape
285
- ))
286
 
 
 
 
 
287
  fig.update_layout(barmode='stack')
288
  else:
289
- # 如果沒有分組欄,退回一般長條圖的做法
290
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
291
- fig = px.bar(grouped_df, x=x_column, y=y_column, color_discrete_sequence=colors, **fig_params)
292
-
293
- elif chart_type == "群組長條圖":
294
- if group_column and group_column in df.columns:
295
- # 明確將字符串列轉換為類別型
296
- df[x_column] = df[x_column].astype('category')
297
- df[group_column] = df[group_column].astype('category')
298
-
299
- # 先進行分組統計
300
- group_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
301
-
302
- # 創建樞紐表
303
- pivot_df = group_df.pivot_table(index=x_column, columns=group_column,
304
- values=y_column).reset_index().fillna(0)
305
-
306
- # 獲取所有類別
307
- categories = pivot_df.columns.tolist()
308
- categories.remove(x_column)
309
-
310
- for i, category in enumerate(categories):
311
- color = colors[i % len(colors)]
312
- if str(category) in custom_colors:
313
- color = custom_colors[str(category)]
314
-
315
- pattern_shape = None
316
- if patterns and i < len(patterns) and patterns[i] != "無":
317
- pattern_shape = patterns[i]
318
-
319
- fig.add_trace(go.Bar(
320
- x=pivot_df[x_column],
321
- y=pivot_df[category],
322
- name=str(category),
323
- marker_color=color,
324
- marker_pattern_shape=pattern_shape
325
- ))
326
-
327
- fig.update_layout(barmode='group')
328
- else:
329
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
330
- fig = px.bar(grouped_df, x=x_column, y=y_column, **fig_params)
331
-
332
- elif chart_type == "水平長條圖":
333
- if group_column and group_column in df.columns:
334
- grouped_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
335
- fig = px.bar(grouped_df, y=x_column, x=y_column, color=group_column,
336
- color_discrete_sequence=colors, orientation='h', **fig_params)
337
- else:
338
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
339
- fig = px.bar(grouped_df, y=x_column, x=y_column, orientation='h',
340
- color_discrete_sequence=colors, **fig_params)
341
-
342
  elif chart_type == "折線圖":
343
- if group_column and group_column in df.columns and group_column != x_column:
344
- grouped_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
345
- fig = px.line(grouped_df, x=x_column, y=y_column, color=group_column,
346
- color_discrete_sequence=colors, markers=True, **fig_params)
347
-
348
- # 根據自定義顏色和線型
349
- for i, trace in enumerate(fig.data):
350
- if i < len(patterns) and patterns[i] != "無":
351
- if patterns[i] == '/':
352
- trace.line.dash = 'dash'
353
- elif patterns[i] == '\\':
354
- trace.line.dash = 'dot'
355
- elif patterns[i] == 'x':
356
- trace.line.dash = 'dashdot'
357
- elif patterns[i] == '-':
358
- trace.line.dash = 'longdash'
359
- else:
360
- trace.line.dash = 'solid'
361
- else:
362
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
363
- fig = px.line(grouped_df, x=x_column, y=y_column, markers=True,
364
- color_discrete_sequence=colors, **fig_params)
365
-
366
- elif chart_type == "多重折線圖":
367
- if group_column and group_column in df.columns:
368
- # 明確將字符串列轉換為類別型
369
- df[x_column] = df[x_column].astype('category')
370
- df[group_column] = df[group_column].astype('category')
371
-
372
- # 先進行分組統計
373
- group_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
374
-
375
- # 創建樞紐表
376
- pivot_df = group_df.pivot_table(index=x_column, columns=group_column,
377
- values=y_column).reset_index().fillna(0)
378
-
379
- # 獲取所有類別
380
- categories = pivot_df.columns.tolist()
381
- categories.remove(x_column)
382
-
383
- for i, category in enumerate(categories):
384
- color = colors[i % len(colors)]
385
- if str(category) in custom_colors:
386
- color = custom_colors[str(category)]
387
-
388
- line_dash = 'solid'
389
- if patterns and i < len(patterns) and patterns[i] != "無":
390
- if patterns[i] == '/':
391
- line_dash = 'dash'
392
- elif patterns[i] == '\\':
393
- line_dash = 'dot'
394
- elif patterns[i] == 'x':
395
- line_dash = 'dashdot'
396
- elif patterns[i] == '-':
397
- line_dash = 'longdash'
398
-
399
- fig.add_trace(go.Scatter(
400
- x=pivot_df[x_column],
401
- y=pivot_df[category],
402
- mode='lines+markers',
403
- name=str(category),
404
- line=dict(color=color, dash=line_dash),
405
- marker=dict(color=color)
406
- ))
407
  else:
408
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
409
- fig = px.line(grouped_df, x=x_column, y=y_column, markers=True, **fig_params)
410
-
411
- elif chart_type == "階梯折線圖":
412
- if group_column and group_column in df.columns:
413
- # 明確將字符串列轉換為類別型
414
- df[x_column] = df[x_column].astype('category')
415
- df[group_column] = df[group_column].astype('category')
416
-
417
- # 先進行分組統計
418
- group_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
419
-
420
- # 創建樞紐表
421
- pivot_df = group_df.pivot_table(index=x_column, columns=group_column,
422
- values=y_column).reset_index().fillna(0)
423
-
424
- # 獲取所有類別
425
- categories = pivot_df.columns.tolist()
426
- categories.remove(x_column)
427
-
428
- for i, category in enumerate(categories):
429
- color = colors[i % len(colors)]
430
- if str(category) in custom_colors:
431
- color = custom_colors[str(category)]
432
-
433
- line_dash = 'solid'
434
- if patterns and i < len(patterns) and patterns[i] != "無":
435
- if patterns[i] == '/':
436
- line_dash = 'dash'
437
- elif patterns[i] == '\\':
438
- line_dash = 'dot'
439
- elif patterns[i] == 'x':
440
- line_dash = 'dashdot'
441
- elif patterns[i] == '-':
442
- line_dash = 'longdash'
443
-
444
- fig.add_trace(go.Scatter(
445
- x=pivot_df[x_column],
446
- y=pivot_df[category],
447
- mode='lines',
448
- name=str(category),
449
- line=dict(shape='hv', color=color, dash=line_dash)
450
- ))
451
- else:
452
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
453
- fig.add_trace(go.Scatter(
454
- x=grouped_df[x_column],
455
- y=grouped_df[y_column],
456
- mode='lines',
457
- line=dict(shape='hv', color=colors[0])
458
- ))
459
-
460
- elif chart_type == "圓餅圖":
461
- # 圓餅圖只需要分類和對應的數值
462
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
463
-
464
- # 設置自定義顏色
465
- pie_colors = colors
466
- if custom_colors and len(custom_colors) > 0:
467
- pie_colors = [custom_colors.get(str(cat), colors[i % len(colors)])
468
- for i, cat in enumerate(grouped_df[x_column])]
469
-
470
- # 設置自定義圖案
471
- pattern_shapes = None
472
- if patterns and len(patterns) > 0:
473
- pattern_shapes = [p if p != "無" else None for p in patterns]
474
- if len(pattern_shapes) < len(grouped_df):
475
- # 重複圖案以匹配數據長度
476
- pattern_shapes = pattern_shapes * (len(grouped_df) // len(pattern_shapes) + 1)
477
- pattern_shapes = pattern_shapes[:len(grouped_df)]
478
-
479
- fig = px.pie(grouped_df, names=x_column, values=y_column,
480
- color_discrete_sequence=pie_colors, **fig_params)
481
-
482
- # 應用圖案填充 (Plotly可能不支持直接在餅圖上應用花紋,此處為嘗試)
483
- if pattern_shapes:
484
- for i, trace in enumerate(fig.data):
485
- trace.marker.pattern = dict(
486
- shape=pattern_shapes[i % len(pattern_shapes)] if i < len(pattern_shapes) else None,
487
- solidity=0.5
488
- )
489
-
490
- elif chart_type == "環形圖":
491
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
492
-
493
- # 設置自定義顏色
494
- pie_colors = colors
495
- if custom_colors and len(custom_colors) > 0:
496
- pie_colors = [custom_colors.get(str(cat), colors[i % len(colors)])
497
- for i, cat in enumerate(grouped_df[x_column])]
498
-
499
- fig = px.pie(grouped_df, names=x_column, values=y_column, hole=0.4,
500
- color_discrete_sequence=pie_colors, **fig_params)
501
-
502
- # 應用圖案填充
503
- if patterns and len(patterns) > 0:
504
- for i, trace in enumerate(fig.data):
505
- trace.marker.pattern = dict(
506
- shape=patterns[i % len(patterns)] if patterns[i % len(patterns)] != "無" else None,
507
- solidity=0.5
508
- )
509
-
510
  elif chart_type == "散點圖":
511
- if group_column and group_column in df.columns:
512
- fig = px.scatter(df, x=x_column, y=y_column, color=group_column,
513
- color_discrete_sequence=colors, **fig_params)
514
-
515
- if size_column and size_column in df.columns:
516
- fig = px.scatter(df, x=x_column, y=y_column, color=group_column,
517
- size=size_column, color_discrete_sequence=colors, **fig_params)
518
- else:
519
- fig = px.scatter(df, x=x_column, y=y_column, color_discrete_sequence=colors, **fig_params)
520
-
521
- # 應用散點圖符號
522
- if patterns and len(patterns) > 0:
523
- for i, trace in enumerate(fig.data):
524
- pattern_idx = i % len(patterns)
525
- if patterns[pattern_idx] != "無":
526
- if patterns[pattern_idx] == '/':
527
- trace.marker.symbol = 'diamond'
528
- elif patterns[pattern_idx] == '\\':
529
- trace.marker.symbol = 'square'
530
- elif patterns[pattern_idx] == 'x':
531
- trace.marker.symbol = 'x'
532
- elif patterns[pattern_idx] == '-':
533
- trace.marker.symbol = 'line-ew'
534
- elif patterns[pattern_idx] == '|':
535
- trace.marker.symbol = 'line-ns'
536
- elif patterns[pattern_idx] == '+':
537
- trace.marker.symbol = 'cross'
538
- elif patterns[pattern_idx] == '.':
539
- trace.marker.symbol = 'circle'
540
- elif patterns[pattern_idx] == '*':
541
- trace.marker.symbol = 'star'
542
- else:
543
- trace.marker.symbol = 'circle'
544
-
545
- elif chart_type == "氣泡圖":
546
- if size_column and size_column in df.columns:
547
- if group_column and group_column in df.columns:
548
- fig = px.scatter(df, x=x_column, y=y_column, color=group_column,
549
- size=size_column, size_max=30,
550
- color_discrete_sequence=colors, **fig_params)
551
- else:
552
- fig = px.scatter(df, x=x_column, y=y_column, size=size_column,
553
- size_max=30, color_discrete_sequence=colors, **fig_params)
554
- else:
555
- # 如果沒有指定大小列,則退回到一般散點圖
556
- fig = px.scatter(df, x=x_column, y=y_column, color_discrete_sequence=colors, **fig_params)
557
-
558
- elif chart_type == "區域圖":
559
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
560
- fig = px.area(grouped_df, x=x_column, y=y_column,
561
- color_discrete_sequence=colors, **fig_params)
562
-
563
- # 應用填充圖案
564
- if patterns and len(patterns) > 0 and patterns[0] != "無":
565
- for trace in fig.data:
566
- trace.fill = 'tozeroy'
567
- # Plotly的區域圖不直接支持填充圖案,但可以使用線條樣式來模擬
568
- if patterns[0] == '/':
569
- trace.line.dash = 'dash'
570
- elif patterns[0] == '\\':
571
- trace.line.dash = 'dot'
572
- elif patterns[0] == 'x':
573
- trace.line.dash = 'dashdot'
574
-
575
- elif chart_type == "堆疊區域圖":
576
- if group_column and group_column in df.columns:
577
- # 明確將字符串列轉換為類別型
578
- df[x_column] = df[x_column].astype('category')
579
- df[group_column] = df[group_column].astype('category')
580
-
581
- # 先進行分組統計
582
- group_df = df.groupby([x_column, group_column])[y_column].agg(agg_func).reset_index()
583
-
584
- # 創建樞紐表
585
- pivot_df = group_df.pivot_table(index=x_column, columns=group_column,
586
- values=y_column).reset_index().fillna(0)
587
-
588
- # 獲取所有類別
589
- categories = pivot_df.columns.tolist()
590
- categories.remove(x_column)
591
-
592
- # 建立堆疊區域圖
593
- for i, category in enumerate(categories):
594
- color = colors[i % len(colors)]
595
- if str(category) in custom_colors:
596
- color = custom_colors[str(category)]
597
-
598
- # 添加區域軌跡
599
- fig.add_trace(go.Scatter(
600
- x=pivot_df[x_column],
601
- y=pivot_df[category],
602
- mode='lines',
603
- line=dict(width=0.5, color=color),
604
- stackgroup='one',
605
- name=str(category)
606
- ))
607
- else:
608
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
609
- fig = px.area(grouped_df, x=x_column, y=y_column, **fig_params)
610
-
611
- elif chart_type == "雷達圖":
612
- if group_column and group_column in df.columns:
613
- # 對於每個組創建一個雷達圖的軌跡
614
- groups = df[group_column].unique()
615
-
616
- for i, group in enumerate(groups):
617
- group_data = df[df[group_column] == group]
618
- grouped_df = group_data.groupby(x_column)[y_column].agg(agg_func).reset_index()
619
-
620
- theta = grouped_df[x_column].tolist()
621
- r = grouped_df[y_column].tolist()
622
-
623
- # 封閉雷達圖
624
- theta.append(theta[0])
625
- r.append(r[0])
626
-
627
- color = colors[i % len(colors)]
628
- if str(group) in custom_colors:
629
- color = custom_colors[str(group)]
630
-
631
- fig.add_trace(go.Scatterpolar(
632
- r=r,
633
- theta=theta,
634
- fill='toself',
635
- name=str(group),
636
- line_color=color
637
- ))
638
  else:
639
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
640
-
641
- theta = grouped_df[x_column].tolist()
642
- r = grouped_df[y_column].tolist()
643
-
644
- # 封閉雷達圖
645
- theta.append(theta[0])
646
- r.append(r[0])
647
-
648
- fig.add_trace(go.Scatterpolar(
649
- r=r,
650
- theta=theta,
651
- fill='toself',
652
- line_color=colors[0]
653
- ))
654
-
655
- fig.update_layout(
656
- polar=dict(
657
- radialaxis=dict(
658
- visible=True
659
- )
660
- )
661
- )
662
-
663
- elif chart_type == "熱力圖":
664
- # 熱力圖需要兩個分類變量和一個連續變量
665
- if group_column and group_column in df.columns:
666
- # 創建樞紐表
667
- pivot_df = df.pivot_table(index=x_column, columns=group_column,
668
- values=y_column, aggfunc=agg_func)
669
-
670
- # 創建熱力圖
671
- fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, **fig_params)
672
- fig.update_layout(coloraxis_showscale=True)
673
- else:
674
- # 如果沒有組列,則沒有足夠的維度來創建熱力圖
675
- fig = go.Figure()
676
- fig.add_annotation(
677
- text="熱力圖需要選擇一個分組列",
678
- showarrow=False,
679
- font=dict(size=16)
680
- )
681
-
682
- elif chart_type == "箱型圖":
683
- if group_column and group_column in df.columns:
684
- fig = px.box(df, x=group_column, y=y_column, color=group_column,
685
- color_discrete_sequence=colors, **fig_params)
686
- else:
687
- fig = px.box(df, y=y_column, color_discrete_sequence=colors, **fig_params)
688
-
689
- elif chart_type == "小提琴圖":
690
- if group_column and group_column in df.columns:
691
- fig = px.violin(df, x=group_column, y=y_column, color=group_column,
692
- box=True, points="all", color_discrete_sequence=colors, **fig_params)
693
- else:
694
- fig = px.violin(df, y=y_column, box=True, points="all",
695
- color_discrete_sequence=colors, **fig_params)
696
-
697
- elif chart_type == "漏斗圖":
698
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
699
-
700
- # 按值排序
701
- grouped_df = grouped_df.sort_values(by=y_column, ascending=False)
702
-
703
- # 設置自定義顏色
704
- funnel_colors = colors[:len(grouped_df)]
705
- if custom_colors and len(custom_colors) > 0:
706
- funnel_colors = [custom_colors.get(str(cat), colors[i % len(colors)])
707
- for i, cat in enumerate(grouped_df[x_column])]
708
-
709
- # 創建漏斗圖
710
- fig = go.Figure(go.Funnel(
711
- y=grouped_df[x_column],
712
- x=grouped_df[y_column],
713
- textposition="inside",
714
- textinfo="value+percent initial",
715
- marker={"color": funnel_colors}
716
- ))
717
-
718
- fig.update_layout(title=title)
719
-
720
- elif chart_type == "樹狀圖":
721
- if group_column and group_column in df.columns:
722
- # 創建層次結構
723
- fig = px.treemap(df, path=[group_column, x_column], values=y_column,
724
- color_discrete_sequence=colors, **fig_params)
725
- else:
726
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
727
- fig = px.treemap(grouped_df, path=[x_column], values=y_column,
728
- color_discrete_sequence=colors, **fig_params)
729
-
730
- elif chart_type == "直方圖":
731
- # 直方圖顯示單一變量的分佈
732
- fig = px.histogram(df, x=x_column, color=group_column if group_column else None,
733
- color_discrete_sequence=colors, **fig_params)
734
-
735
- elif chart_type == "極座標圖":
736
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
737
-
738
- # 設置自定義顏色
739
- polar_colors = colors[:len(grouped_df)]
740
- if custom_colors and len(custom_colors) > 0:
741
- polar_colors = [custom_colors.get(str(cat), colors[i % len(colors)])
742
- for i, cat in enumerate(grouped_df[x_column])]
743
-
744
- # 創建極座標條形圖
745
- fig = px.bar_polar(grouped_df, r=y_column, theta=x_column,
746
- color=x_column, color_discrete_sequence=polar_colors, **fig_params)
747
-
748
- elif chart_type == "甘特圖":
749
- # 甘特圖需要開始和結束時間
750
- # 假設x_column是任務名稱,y_column是開始時間,group_column是結束時間
751
- if group_column and group_column in df.columns:
752
- # 確保日期時間格式
753
- try:
754
- df[y_column] = pd.to_datetime(df[y_column])
755
- df[group_column] = pd.to_datetime(df[group_column])
756
-
757
- # 創建甘特圖
758
- fig = px.timeline(df, x_start=y_column, x_end=group_column, y=x_column,
759
- color=size_column if size_column else None,
760
- color_discrete_sequence=colors, **fig_params)
761
-
762
- fig.update_layout(xaxis_type="date")
763
- except:
764
- fig = go.Figure()
765
- fig.add_annotation(
766
- text="無法將列轉換為日期格式,甘特圖需要日期時間格式的開始和結束列",
767
- showarrow=False,
768
- font=dict(size=14)
769
- )
770
- else:
771
- fig = go.Figure()
772
- fig.add_annotation(
773
- text="甘特圖需要開始日期和結束日期列",
774
- showarrow=False,
775
- font=dict(size=16)
776
- )
777
-
778
- else:
779
- # 默認圖表類型
780
- grouped_df = df.groupby(x_column)[y_column].agg(agg_func).reset_index()
781
- fig = px.bar(grouped_df, x=x_column, y=y_column, **fig_params)
782
-
783
- except Exception as e:
784
- # 處理創建圖表時的錯誤
785
- fig = go.Figure()
786
- fig.add_annotation(
787
- text=f"創建圖表時出錯: {str(e)}",
788
- showarrow=False,
789
- font=dict(size=14)
790
- )
791
-
792
- # 設置網格和圖例
793
- fig.update_layout(
794
- width=width,
795
- height=height,
796
- showlegend=show_legend,
797
- xaxis=dict(showgrid=show_grid),
798
- yaxis=dict(showgrid=show_grid),
799
- # 現代化樣式設置
800
- template="plotly_white",
801
- margin=dict(l=50, r=50, t=50, b=50),
802
- font=dict(family="Arial, sans-serif"),
803
- hoverlabel=dict(
804
- bgcolor="white",
805
- font_size=12,
806
- font_family="Arial, sans-serif"
807
- )
808
- )
809
-
810
- return fig
811
 
812
- def process_upload(file):
813
- """處理上傳的文件"""
814
- try:
815
- if file is None:
816
- return None, "未上傳文件"
817
-
818
- # 檢查文件類型
819
- file_type = file.name.split('.')[-1].lower()
820
-
821
- if file_type == 'csv':
822
- df = pd.read_csv(file.name)
823
- elif file_type in ['xls', 'xlsx']:
824
- df = pd.read_excel(file.name)
825
  else:
826
- return None, f"不支持的文件類型: {file_type}。請上傳CSV或Excel文件。"
827
-
828
- # 添加計數列
829
- df['計數'] = 1
830
-
831
- return df, f"成功載入數據,共{len(df)}行,{len(df.columns)}列"
832
-
833
- except Exception as e:
834
- return None, f"載入文件時出錯: {str(e)}"
835
-
836
- def parse_data(csv_data):
837
- """解析CSV文本數據,支持逗號或空格分隔"""
838
- try:
839
- if not csv_data or csv_data.strip() == "":
840
- return None, "未提供數據"
841
-
842
- # 嘗試檢測分隔符
843
- first_line = csv_data.strip().split('\n')[0]
844
- if ',' in first_line:
845
- # 優先使用逗號作為分隔符
846
- df = pd.read_csv(io.StringIO(csv_data), sep=',')
847
- elif ' ' in first_line or '\t' in first_line:
848
- # 如果沒有逗號但有空格或制表符,使用空格作為分隔符
849
- df = pd.read_csv(io.StringIO(csv_data), sep='\\s+')
850
- else:
851
- # 默認使用逗號
852
- df = pd.read_csv(io.StringIO(csv_data))
853
-
854
- # 添加計數列
855
- df['計數'] = 1
856
-
857
- return df, f"成功解析數據,共{len(df)}行,{len(df.columns)}列"
858
-
859
- except Exception as e:
860
- return None, f"解析數據時出錯: {str(e)}"
861
-
862
- def export_data(df, format_type):
863
- """導出數據為各種格式"""
864
- if df is None or df.empty:
865
- return None, "沒有數據可以導出"
866
-
867
- try:
868
- if format_type == "CSV":
869
- buffer = io.StringIO()
870
- df.to_csv(buffer, index=False)
871
- data = buffer.getvalue()
872
- filename = "exported_data.csv"
873
- mime_type = "text/csv"
874
-
875
- elif format_type == "Excel":
876
- buffer = io.BytesIO()
877
- df.to_excel(buffer, index=False)
878
- data = buffer.getvalue()
879
- filename = "exported_data.xlsx"
880
- mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
881
-
882
- elif format_type == "JSON":
883
- buffer = io.StringIO()
884
- data = df.to_json(orient="records")
885
- filename = "exported_data.json"
886
- mime_type = "application/json"
887
-
888
- else:
889
- return None, f"不支持的導出格式: {format_type}"
890
-
891
- return (data, filename, mime_type), f"數據已成功導出為{format_type}格式"
892
-
893
- except Exception as e:
894
- return None, f"導出數據時出錯: {str(e)}"
895
 
896
- def update_columns(df):
897
- """更新列選擇下拉菜單"""
898
- if df is None or df.empty:
899
- # 默認列
900
- return gr.Dropdown(choices=["類別", "數值", "計數"], value="類別"), gr.Dropdown(choices=["類別", "數值", "計數"], value="計數"), gr.Dropdown(choices=["無", "類別", "數值", "計數"], value="無"), gr.Dropdown(choices=["無", "類別", "數值", "計數"], value="無")
901
-
902
- columns = df.columns.tolist()
903
- x_dropdown = gr.Dropdown(choices=columns, value=columns[0] if columns else None)
904
- y_dropdown = gr.Dropdown(choices=columns, value="計數" if "計數" in columns else (columns[1] if len(columns) > 1 else columns[0]))
905
- group_dropdown = gr.Dropdown(choices=["無"] + columns, value="無")
906
- size_dropdown = gr.Dropdown(choices=["無"] + columns, value="無")
907
-
908
- return x_dropdown, y_dropdown, group_dropdown, size_dropdown
909
 
910
- def download_figure(fig, format_type="PNG"):
911
- """導出圖表為圖像"""
912
- if fig is None:
913
- return None, "沒有圖表可以導出"
914
-
915
- try:
916
- # 選擇導出格式
917
- if format_type == "PNG":
918
- img_bytes = fig.to_image(format="png")
919
- mime_type = "image/png"
920
- ext = "png"
921
- elif format_type == "SVG":
922
- img_bytes = fig.to_image(format="svg")
923
- mime_type = "image/svg+xml"
924
- ext = "svg"
925
- elif format_type == "PDF":
926
- img_bytes = fig.to_image(format="pdf")
927
- mime_type = "application/pdf"
928
- ext = "pdf"
929
- elif format_type == "JPEG":
930
- img_bytes = fig.to_image(format="jpeg")
931
- mime_type = "image/jpeg"
932
- ext = "jpg"
933
- else:
934
- img_bytes = fig.to_image(format="png")
935
- mime_type = "image/png"
936
- ext = "png"
937
-
938
- # 創建文件對象
939
- filename = f"chart_export.{ext}"
940
-
941
- return (img_bytes, filename, mime_type), f"圖表已成功導出為{format_type}格式"
942
-
943
  except Exception as e:
944
- return None, f"導出圖表時出錯: {str(e)}"
 
 
945
 
946
- def recommend_chart_settings(df):
947
- """智能分析數據並推薦最佳圖表設置"""
948
  if df is None or df.empty:
949
- return {
950
- "message": "請先上傳或輸入數據"
951
- }
952
-
953
- # 獲取列名和數據類型
954
- columns = df.columns.tolist()
955
- num_columns = df.select_dtypes(include=['number']).columns.tolist()
956
- cat_columns = df.select_dtypes(include=['object', 'category']).columns.tolist()
957
-
958
- # 排除"計數"
959
- if "計數" in num_columns:
960
- num_columns.remove("計數")
961
-
962
- # 統計數據特徵
963
- num_rows = len(df)
964
- num_unique_values = {col: df[col].nunique() for col in columns}
965
-
966
- # 初始化推薦結果
967
- recommendation = {
968
- "chart_type": None,
969
- "x_column": None,
970
- "y_column": None,
971
- "group_column": None,
972
- "agg_function": None,
973
- "message": ""
974
- }
975
-
976
- # 根據數據特徵推薦圖表
977
- # 案例1: 有2個類別列,需要分析它們之間的關係
978
- if len(cat_columns) >= 2 and '計數' in columns:
979
- # 例如機構類型和情緒分析數據
980
- recommendation["chart_type"] = "堆疊長條圖"
981
- recommendation["x_column"] = cat_columns[0] # 第一個類別列作為X軸
982
- recommendation["y_column"] = "計數" # 使用計數列作為Y軸
983
- recommendation["group_column"] = cat_columns[1] # 第二個類別列作為分組
984
- recommendation["agg_function"] = "求和"
985
- recommendation["message"] = f"檢測到有類別列 '{cat_columns[0]}' 和 '{cat_columns[1]}',推薦使用堆疊長條圖來顯示兩者關係"
986
-
987
- # 案例2: 只有一個類別列
988
- elif len(cat_columns) == 1 and '計數' in columns:
989
- recommendation["chart_type"] = "長條圖"
990
- recommendation["x_column"] = cat_columns[0]
991
- recommendation["y_column"] = "計數"
992
- recommendation["agg_function"] = "求和"
993
- recommendation["message"] = f"檢測到類別列 '{cat_columns[0]}',推薦使用長條圖來顯示分佈"
994
-
995
- # 案例3: 有時間序列數據
996
- elif any("日期" in col or "時間" in col for col in columns) and len(num_columns) > 0:
997
- date_col = next((col for col in columns if "日期" in col or "時間" in col), None)
998
- recommendation["chart_type"] = "折線圖"
999
- recommendation["x_column"] = date_col
1000
- recommendation["y_column"] = num_columns[0] if num_columns else "計數"
1001
- recommendation["agg_function"] = "平均值"
1002
- recommendation["message"] = f"檢測到時間列 '{date_col}',推薦使用折線圖來顯示趨勢"
1003
-
1004
- # 案例4: 有多個數值列
1005
- elif len(num_columns) >= 2:
1006
- recommendation["chart_type"] = "散點圖"
1007
- recommendation["x_column"] = num_columns[0]
1008
- recommendation["y_column"] = num_columns[1]
1009
- recommendation["message"] = f"檢測到數值列 '{num_columns[0]}' 和 '{num_columns[1]}',推薦使用散點圖來分析相關性"
1010
-
1011
- # 預設情況
1012
- else:
1013
- if len(cat_columns) > 0 and "計數" in columns:
1014
- recommendation["chart_type"] = "長條圖"
1015
- recommendation["x_column"] = cat_columns[0]
1016
- recommendation["y_column"] = "計數"
1017
- recommendation["agg_function"] = "求和"
1018
- recommendation["message"] = "根據數據結構,推薦使用長條圖"
1019
- elif len(cat_columns) > 0 and len(num_columns) > 0:
1020
- recommendation["chart_type"] = "長條圖"
1021
- recommendation["x_column"] = cat_columns[0]
1022
- recommendation["y_column"] = num_columns[0]
1023
- recommendation["agg_function"] = "平均值"
1024
- recommendation["message"] = "根據數據結構,推薦使用長條圖"
1025
- else:
1026
- recommendation["message"] = "無法確定最佳圖表類型,請手動選擇"
1027
-
1028
- return recommendation
1029
-
1030
- # CSS樣式
1031
-
1032
- CUSTOM_CSS = """
1033
- .gradio-container {
1034
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1035
- background: linear-gradient(to bottom right, #f7f9fc, #e9f0f8);
1036
- overflow: visible !important;
1037
- }
1038
-
1039
- .app-header {
1040
- text-align: center;
1041
- margin-bottom: 20px;
1042
- background: linear-gradient(90deg, #4568dc, #3a6073);
1043
- padding: 20px;
1044
- border-radius: 10px;
1045
- color: white;
1046
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1047
- }
1048
-
1049
- .app-title {
1050
- font-size: 32px;
1051
- font-weight: bold;
1052
- margin: 0;
1053
- display: inline-block;
1054
- background: linear-gradient(to right, #ffffff, #e0e0e0);
1055
- -webkit-background-clip: text;
1056
- -webkit-text-fill-color: transparent;
1057
- }
1058
-
1059
- .app-subtitle {
1060
- font-size: 16px;
1061
- color: #f0f0f0;
1062
- margin-top: 5px;
1063
- }
1064
-
1065
- .section-title {
1066
- font-size: 20px;
1067
- font-weight: bold;
1068
- color: #333;
1069
- border-bottom: 2px solid #4568dc;
1070
- padding-bottom: 5px;
1071
- margin-top: 20px;
1072
- margin-bottom: 15px;
1073
- }
1074
-
1075
- .card {
1076
- background-color: white;
1077
- border-radius: 10px;
1078
- padding: 20px;
1079
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1080
- margin-bottom: 20px;
1081
- transition: transform 0.3s, box-shadow 0.3s;
1082
- }
1083
-
1084
- .card:hover {
1085
- transform: translateY(-5px);
1086
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
1087
- }
1088
-
1089
- .primary-button {
1090
- background: linear-gradient(90deg, #4568dc, #3a6073) !important;
1091
- border: none !important;
1092
- color: white !important;
1093
- font-weight: bold !important;
1094
- padding: 10px 20px !important;
1095
- border-radius: 5px !important;
1096
- cursor: pointer !important;
1097
- transition: all 0.3s ease !important;
1098
- }
1099
-
1100
- .primary-button:hover {
1101
- background: linear-gradient(90deg, #3a6073, #4568dc) !important;
1102
- transform: translateY(-2px) !important;
1103
- box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1) !important;
1104
- }
1105
-
1106
- .secondary-button {
1107
- background: linear-gradient(90deg, #6a85b6, #bac8e0) !important;
1108
- border: none !important;
1109
- color: white !important;
1110
- font-weight: bold !important;
1111
- padding: 10px 20px !important;
1112
- border-radius: 5px !important;
1113
- cursor: pointer !important;
1114
- transition: all 0.3s ease !important;
1115
- }
1116
-
1117
- .secondary-button:hover {
1118
- background: linear-gradient(90deg, #bac8e0, #6a85b6) !important;
1119
- transform: translateY(-2px) !important;
1120
- box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1) !important;
1121
- }
1122
-
1123
- .color-panel {
1124
- background-color: white;
1125
- border-radius: 5px;
1126
- padding: 10px;
1127
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
1128
- }
1129
-
1130
- .tips-box {
1131
- background-color: #f1f7fe;
1132
- border-left: 4px solid #4568dc;
1133
- padding: 15px;
1134
- border-radius: 5px;
1135
- margin: 20px 0;
1136
- }
1137
-
1138
- .chart-previewer {
1139
- border: 2px dashed #ccc;
1140
- border-radius: 10px;
1141
- padding: 20px;
1142
- min-height: 400px;
1143
- display: flex;
1144
- justify-content: center;
1145
- align-items: center;
1146
- background-color: rgba(255, 255, 255, 0.7);
1147
- }
1148
-
1149
- /* Loading animation */
1150
- @keyframes pulse {
1151
- 0% { opacity: 0.6; }
1152
- 50% { opacity: 1; }
1153
- 100% { opacity: 0.6; }
1154
- }
1155
-
1156
- .loading {
1157
- animation: pulse 1.5s infinite;
1158
- }
1159
-
1160
- /* 下拉選單位置修正 */
1161
- .gradio-dropdown {
1162
- position: relative !important;
1163
- overflow: visible !important; /* ✅ 關鍵補充 */
1164
- z-index: 10 !important; /* 確保浮在上層 */
1165
- }
1166
-
1167
- .gradio-dropdown .choices__list--dropdown {
1168
- position: absolute !important;
1169
- top: 100% !important;
1170
- left: 0 !important;
1171
- z-index: 1000 !important;
1172
- max-height: 300px !important;
1173
- overflow-y: auto !important;
1174
- background: white !important;
1175
- border: 1px solid #ddd !important;
1176
- border-radius: 4px !important;
1177
- width: 100% !important;
1178
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
1179
- }
1180
-
1181
- /* 確保下拉項目可見 */
1182
- .gradio-dropdown .choices__list--dropdown .choices__item {
1183
- padding: 8px 10px !important;
1184
- cursor: pointer !important;
1185
- }
1186
-
1187
- """
1188
-
1189
- # 現代化UI界面
1190
- with gr.Blocks(css=CUSTOM_CSS, title="進階數據可視化工具") as demo:
1191
- gr.HTML("""
1192
- <div class="app-header">
1193
- <h1 class="app-title">🎨 進階數據可視化工具</h1>
1194
- <p class="app-subtitle">上傳數據,創建各種專業圖表,輕鬆實現數據可視化</p>
1195
- </div>
1196
- """)
1197
-
1198
- # 狀態變量
1199
- data_state = gr.State(None)
1200
- custom_colors_state = gr.State({})
1201
- patterns_state = gr.State([])
1202
-
1203
- with gr.Tabs():
1204
- # 數據輸入頁籤
1205
- with gr.TabItem("📊 數據輸入") as tab_data:
1206
- with gr.Row():
1207
- with gr.Column(scale=1):
1208
- gr.HTML('<div class="section-title">上傳或輸入數據</div>')
1209
-
1210
- with gr.Group(elem_classes=["card"]):
1211
- file_upload = gr.File(label="上傳CSV或Excel文件")
1212
- upload_button = gr.Button("載入文件", elem_classes=["primary-button"])
1213
- upload_status = gr.Textbox(label="上傳狀態", lines=2)
1214
-
1215
- with gr.Group(elem_classes=["card"]):
1216
- csv_input = gr.Textbox(
1217
- label="直接輸入數據(逗號或空格分隔)",
1218
- placeholder="類別,數值\nA,10\nB,20\nC,15\nD,25\nE,30\n\n或\n\n類別 數值\nA 10\nB 20\nC 15\nD 25\nE 30",
1219
- lines=10
1220
- )
1221
- parse_button = gr.Button("解析數據", elem_classes=["primary-button"])
1222
- parse_status = gr.Textbox(label="解析狀態", lines=2)
1223
-
1224
- with gr.Column(scale=1):
1225
- gr.HTML('<div class="section-title">數據預覽</div>')
1226
-
1227
- with gr.Group(elem_classes=["card"]):
1228
- data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
1229
-
1230
- with gr.Row():
1231
- export_format = gr.Dropdown(
1232
- ["CSV", "Excel", "JSON"],
1233
- label="導出格式",
1234
- value="CSV"
1235
- )
1236
- export_button = gr.Button("導出數據", elem_classes=["secondary-button"])
1237
-
1238
- export_result = gr.File(label="導出結果")
1239
- export_status = gr.Textbox(label="導出狀態", lines=2)
1240
-
1241
- # 圖表創建頁籤
1242
- with gr.TabItem("📈 圖表創建") as tab_chart:
1243
- with gr.Row():
1244
- with gr.Column(scale=1):
1245
- gr.HTML('<div class="section-title">圖表設置</div>')
1246
-
1247
- with gr.Group(elem_classes=["card"]):
1248
- chart_type = gr.Dropdown(
1249
- CHART_TYPES,
1250
- label="📊 圖表類型",
1251
- value="長條圖",
1252
- interactive=True
1253
- )
1254
-
1255
- with gr.Row():
1256
- recommend_button = gr.Button("🧠 智能推薦圖表", variant="secondary", elem_classes=["secondary-button"])
1257
- recommendation_result = gr.Textbox(label="推薦結果", lines=2)
1258
-
1259
- chart_title = gr.Textbox(label="📝 圖表標題", placeholder="我的數據圖表")
1260
-
1261
- agg_function = gr.Dropdown(
1262
- AGGREGATION_FUNCTIONS,
1263
- label="🔄 聚合函數",
1264
- value="計數",
1265
- info="選擇如何彙總數據"
1266
- )
1267
-
1268
- gr.HTML("<div class='section-title'>數據映射</div>")
1269
-
1270
- # 軸和分組選擇
1271
- x_column = gr.Dropdown(["類別"], label="X軸(或類別)")
1272
- y_column = gr.Dropdown(["計數"], label="Y軸(或數值)")
1273
- group_column = gr.Dropdown(["無"], label="分組列(用於多系列圖表)")
1274
- size_column = gr.Dropdown(["無"], label="大小列(用於氣泡圖等)")
1275
-
1276
- gr.HTML("<div class='tips-box'>💡 提示: 選擇不同圖表類型時,界面會自動調整顯示相關設置選項</div>")
1277
-
1278
- with gr.Column(scale=1):
1279
- gr.HTML('<div class="section-title">顯示選項</div>')
1280
-
1281
- with gr.Group(elem_classes=["card"]):
1282
- # 尺寸控制
1283
- with gr.Row():
1284
- chart_width = gr.Slider(300, 1200, 800, label="圖表寬度")
1285
- chart_height = gr.Slider(300, 800, 500, label="圖表高度")
1286
-
1287
- with gr.Row():
1288
- show_grid = gr.Checkbox(label="顯示網格", value=True)
1289
- show_legend = gr.Checkbox(label="顯示圖例", value=True)
1290
-
1291
- color_scheme = gr.Dropdown(
1292
- list(COLOR_SCHEMES.keys()),
1293
- label="🎨 顏色方案",
1294
- value="默認"
1295
- )
1296
-
1297
- gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊顏色可複製顏色代碼)</div>')
1298
- gr.HTML(generate_color_cards(), elem_id="color_display")
1299
-
1300
- # 圖案和顏色自定義區
1301
- with gr.Group(elem_classes=["card"]):
1302
- gr.HTML('<div class="section-title">自定義圖案和顏色</div>')
1303
-
1304
- # 動態添加圖案,先默認提供三個
1305
- with gr.Row():
1306
- pattern1 = gr.Dropdown(PATTERN_TYPES, label="圖案1", value="無")
1307
- pattern2 = gr.Dropdown(PATTERN_TYPES, label="圖案2", value="無")
1308
- pattern3 = gr.Dropdown(PATTERN_TYPES, label="圖案3", value="無")
1309
-
1310
- # 自定義顏色區域
1311
- color_customization = gr.Textbox(
1312
- label="自定義顏色 (格式: 類別1:#FF0000,類別2:#00FF00)",
1313
- placeholder="正面:#2ca02c,負面:#ff7f0e,中性:#1f77b4",
1314
- info="輸入類別名稱和十六進制顏色代碼,用逗號分隔多個項目"
1315
- )
1316
-
1317
- with gr.Row():
1318
- update_button = gr.Button("更新圖表", variant="primary", elem_classes=["primary-button"])
1319
-
1320
- with gr.Row():
1321
- export_img_format = gr.Dropdown(
1322
- ["PNG", "SVG", "PDF", "JPEG"],
1323
- label="導出格式",
1324
- value="PNG"
1325
- )
1326
- download_button = gr.Button("導出圖表", elem_classes=["secondary-button"])
1327
-
1328
- export_chart = gr.File(label="導出的圖表")
1329
- export_chart_status = gr.Textbox(label="導出狀態", lines=2)
1330
-
1331
- # 圖表預覽區
1332
- gr.HTML('<div class="section-title">圖表預覽</div>')
1333
- with gr.Group(elem_classes=["chart-previewer"]):
1334
- chart_output = gr.Plot(label="", elem_id="chart_preview")
1335
-
1336
- # 使用說明頁籤
1337
- with gr.TabItem("📖 使用說明") as tab_help:
1338
- gr.HTML("""
1339
- <div class="card">
1340
- <div class="section-title">使用說明</div>
1341
-
1342
- <h3>數據輸入</h3>
1343
- <ul>
1344
- <li>上傳CSV或Excel文件,或在文本框中直接輸入數據</li>
1345
- <li>第一行被視為欄位名稱(表頭),不會納入統計</li>
1346
- <li>支持逗號分隔(CSV)或空格分隔的數據格式</li>
1347
- <li>系統會自動添加「計數」列,方便進行計數統計</li>
1348
- </ul>
1349
-
1350
- <h3>圖表創建</h3>
1351
- <ul>
1352
- <li><strong>智能推薦:</strong>系統可根據您的數據結構智能推薦最適合的圖表類型和設置</li>
1353
- <li><strong>圖表類型:</strong>支持20多種專業圖表,包括長條圖、堆疊長條圖、折線圖、圓餅圖等</li>
1354
- <li><strong>聚合函數:</strong>選擇如何彙總數據(計數、求和、平均值、最大值等)</li>
1355
- <li><strong>分組列:</strong>用於創建多系列圖表,例如按類別分組的長條圖</li>
1356
- <li><strong>大小列:</strong>用於氣泡圖等需要額外數值控制大小的圖表</li>
1357
- </ul>
1358
-
1359
- <h3>自定義選項</h3>
1360
- <ul>
1361
- <li><strong>顏色方案:</strong>選擇預設的顏色系列,包括明亮、柔和、漸變等多種風格</li>
1362
- <li><strong>自定義顏色:</strong>為特定類別設置顏色,格式為"類別1:#FF0000,類別2:#00FF00"</li>
1363
- <li><strong>圖案填充:</strong>為圖表元素設置填充圖案,特別適用於黑白印刷</li>
1364
- <li><strong>導出格式:</strong>支持PNG、SVG、PDF和JPEG格式導出</li>
1365
- </ul>
1366
-
1367
- <h3>常見使用場景</h3>
1368
- <ul>
1369
- <li><strong>分類數據分析:</strong>使用長條圖或圓餅圖展示不同類別的分布</li>
1370
- <li><strong>多分類比較:</strong>使用堆疊���條圖或群組長條圖展示多個分類維度的關係</li>
1371
- <li><strong>趨勢分析:</strong>使用折線圖或區域圖展示數據隨時間的變化</li>
1372
- <li><strong>相關性分析:</strong>使用散點圖或熱力圖分析變量之間的關係</li>
1373
- </ul>
1374
- </div>
1375
- """)
1376
-
1377
- # 輔助函數
1378
- def parse_custom_colors(color_text):
1379
- """解析自定義顏色文本"""
1380
- custom_colors = {}
1381
- if color_text and color_text.strip():
1382
- try:
1383
- pairs = color_text.split(',')
1384
- for pair in pairs:
1385
- if ':' in pair:
1386
- key, value = pair.split(':', 1)
1387
- custom_colors[key.strip()] = value.strip()
1388
- except:
1389
- pass
1390
- return custom_colors
1391
-
1392
- def update_patterns(p1, p2, p3):
1393
- """更新圖案列表"""
1394
- patterns = []
1395
- if p1:
1396
- patterns.append(p1)
1397
- if p2:
1398
- patterns.append(p2)
1399
- if p3:
1400
- patterns.append(p3)
1401
- return patterns
1402
-
1403
- # 事件處理
1404
- upload_button.click(
1405
- process_upload,
1406
- inputs=[file_upload],
1407
- outputs=[data_state, upload_status]
1408
- ).then(
1409
- lambda df: df if df is not None else pd.DataFrame(),
1410
- inputs=[data_state],
1411
- outputs=[data_preview]
1412
- ).then(
1413
- update_columns,
1414
- inputs=[data_state],
1415
- outputs=[x_column, y_column, group_column, size_column]
1416
- )
1417
-
1418
- parse_button.click(
1419
- parse_data,
1420
- inputs=[csv_input],
1421
- outputs=[data_state, parse_status]
1422
- ).then(
1423
- lambda df: df if df is not None else pd.DataFrame(),
1424
- inputs=[data_state],
1425
- outputs=[data_preview]
1426
- ).then(
1427
- update_columns,
1428
- inputs=[data_state],
1429
- outputs=[x_column, y_column, group_column, size_column]
1430
- )
1431
-
1432
- export_button.click(
1433
- export_data,
1434
- inputs=[data_state, export_format],
1435
- outputs=[export_result, export_status]
1436
- )
1437
-
1438
- # 處理圖案和顏色設置
1439
- color_customization.change(
1440
- parse_custom_colors,
1441
- inputs=[color_customization],
1442
- outputs=[custom_colors_state]
1443
- )
1444
-
1445
- pattern1.change(
1446
- update_patterns,
1447
- inputs=[pattern1, pattern2, pattern3],
1448
- outputs=[patterns_state]
1449
- )
1450
-
1451
- pattern2.change(
1452
- update_patterns,
1453
- inputs=[pattern1, pattern2, pattern3],
1454
- outputs=[patterns_state]
1455
- )
1456
-
1457
- pattern3.change(
1458
- update_patterns,
1459
- inputs=[pattern1, pattern2, pattern3],
1460
- outputs=[patterns_state]
1461
- )
1462
-
1463
- # 更新圖表
1464
- update_button.click(
1465
- lambda df, chart_type, x_col, y_col, group_col, size_col, color_scheme, patterns, title, width, height, show_grid, show_legend, agg_func, custom_colors:
1466
- create_plot(
1467
- df, chart_type, x_col, y_col,
1468
- None if group_col == "無" else group_col,
1469
- None if size_col == "無" else size_col,
1470
- color_scheme, patterns, title, width, height,
1471
- show_grid, show_legend, agg_func, custom_colors
1472
- ),
1473
- inputs=[
1474
- data_state, chart_type, x_column, y_column,
1475
- group_column, size_column,
1476
- color_scheme, patterns_state, chart_title, chart_width, chart_height,
1477
- show_grid, show_legend, agg_function, custom_colors_state
1478
- ],
1479
- outputs=[chart_output]
1480
- )
1481
-
1482
- # 導出圖表
1483
- download_button.click(
1484
- download_figure,
1485
- inputs=[chart_output, export_img_format],
1486
- outputs=[export_chart, export_chart_status]
1487
- )
1488
-
1489
- # 圖表類型改變時更新界面元素可見性
1490
- def update_element_visibility(chart_type):
1491
- """根據圖表類型更新UI元素的可見性"""
1492
- # 圓餅圖和環形圖不需要X軸,只需要類別和數值
1493
- is_pie_chart = chart_type in ["圓餅圖", "環形圖"]
1494
-
1495
- # 氣泡圖需要額外的大小控制列
1496
- needs_size_column = chart_type in ["氣泡圖", "甘特圖", "樹狀圖"]
1497
-
1498
- # 需要分組列的圖表類型
1499
- needs_group_column = chart_type in [
1500
- "群組長條圖", "堆疊長條圖", "多重折線圖", "堆疊區域圖",
1501
- "熱力圖", "雷達圖", "散點圖", "氣泡圖"
1502
- ]
1503
-
1504
- return (
1505
- gr.update(visible=not is_pie_chart, label="類別列" if is_pie_chart else "X軸"),
1506
- gr.update(visible=True, label="數值列" if is_pie_chart else "Y軸"),
1507
- gr.update(visible=needs_group_column, label="分組列"),
1508
- gr.update(visible=needs_size_column, label="大小列")
1509
- )
1510
-
1511
- chart_type.change(
1512
- update_element_visibility,
1513
- inputs=[chart_type],
1514
- outputs=[x_column, y_column, group_column, size_column]
1515
- ).then(
1516
- lambda df, chart_type, x_col, y_col, group_col, size_col, color_scheme, patterns, title, width, height, show_grid, show_legend, agg_func, custom_colors:
1517
- create_plot(
1518
- df, chart_type, x_col, y_col,
1519
- None if group_col == "無" else group_col,
1520
- None if size_col == "無" else size_col,
1521
- color_scheme, patterns, title, width, height,
1522
- show_grid, show_legend, agg_func, custom_colors
1523
- ),
1524
- inputs=[
1525
- data_state, chart_type, x_column, y_column,
1526
- group_column, size_column,
1527
- color_scheme, patterns_state, chart_title, chart_width, chart_height,
1528
- show_grid, show_legend, agg_function, custom_colors_state
1529
- ],
1530
- outputs=[chart_output]
1531
- )
1532
-
1533
- # 智能推薦功能
1534
- recommend_button.click(
1535
- recommend_chart_settings,
1536
- inputs=[data_state],
1537
- outputs=[recommendation_result]
1538
- ).then(
1539
- lambda rec: (
1540
- rec.get("chart_type") if isinstance(rec, dict) and rec.get("chart_type") else None,
1541
- rec.get("x_column") if isinstance(rec, dict) and rec.get("x_column") else None,
1542
- rec.get("y_column") if isinstance(rec, dict) and rec.get("y_column") else None,
1543
- rec.get("group_column") if isinstance(rec, dict) and rec.get("group_column") else "無",
1544
- rec.get("agg_function") if isinstance(rec, dict) and rec.get("agg_function") else None
1545
- ),
1546
- inputs=[recommendation_result],
1547
- outputs=[chart_type, x_column, y_column, group_column, agg_function]
1548
- ).then(
1549
- lambda df, chart_type, x_col, y_col, group_col, size_col, color_scheme, patterns, title, width, height, show_grid, show_legend, agg_func, custom_colors:
1550
- create_plot(
1551
- df, chart_type, x_col, y_col,
1552
- None if group_col == "無" else group_col,
1553
- None if size_col == "無" else size_col,
1554
- color_scheme, patterns, title, width, height,
1555
- show_grid, show_legend, agg_func, custom_colors
1556
- ),
1557
- inputs=[
1558
- data_state, chart_type, x_column, y_column,
1559
- group_column, size_column,
1560
- color_scheme, patterns_state, chart_title, chart_width, chart_height,
1561
- show_grid, show_legend, agg_function, custom_colors_state
1562
- ],
1563
- outputs=[chart_output]
1564
- )
1565
-
1566
- # 其他輸入變化自動更新圖表
1567
- for input_component in [x_column, y_column, group_column, size_column, agg_function, color_scheme]:
1568
- input_component.change(
1569
- lambda df, chart_type, x_col, y_col, group_col, size_col, color_scheme, patterns, title, width, height, show_grid, show_legend, agg_func, custom_colors:
1570
- create_plot(
1571
- df, chart_type, x_col, y_col,
1572
- None if group_col == "無" else group_col,
1573
- None if size_col == "無" else size_col,
1574
- color_scheme, patterns, title, width, height,
1575
- show_grid, show_legend, agg_func, custom_colors
1576
- ),
1577
- inputs=[
1578
- data_state, chart_type, x_column, y_column,
1579
- group_column, size_column,
1580
- color_scheme, patterns_state, chart_title, chart_width, chart_height,
1581
- show_grid, show_legend, agg_function, custom_colors_state
1582
- ],
1583
- outputs=[chart_output]
1584
- )
1585
-
1586
- # 啟動應用
1587
- demo.launch()
 
1
  import gradio as gr
2
  import pandas as pd
 
3
  import plotly.express as px
4
  import plotly.graph_objects as go
 
 
 
 
 
 
 
 
 
5
 
6
+ def create_plot(df, chart_type, x_col, y_col, group_col):
7
+ if df is None or x_col not in df.columns or y_col not in df.columns:
8
+ return go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ group_col = None if group_col == "無" else group_col
 
 
 
 
 
 
 
 
 
 
 
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  try:
 
13
  if chart_type == "長條圖":
14
+ if group_col:
15
+ fig = px.bar(df, x=x_col, y=y_col, color=group_col)
 
 
 
16
  else:
17
+ fig = px.bar(df, x=x_col, y=y_col)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ elif chart_type == "堆疊長條圖":
20
+ if group_col:
21
+ df_agg = df.groupby([x_col, group_col])[y_col].sum().reset_index()
22
+ fig = px.bar(df_agg, x=x_col, y=y_col, color=group_col)
23
  fig.update_layout(barmode='stack')
24
  else:
25
+ df_agg = df.groupby(x_col)[y_col].sum().reset_index()
26
+ fig = px.bar(df_agg, x=x_col, y=y_col)
27
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  elif chart_type == "折線圖":
29
+ if group_col:
30
+ fig = px.line(df, x=x_col, y=y_col, color=group_col, markers=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  else:
32
+ fig = px.line(df, x=x_col, y=y_col, markers=True)
33
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  elif chart_type == "散點圖":
35
+ if group_col:
36
+ fig = px.scatter(df, x=x_col, y=y_col, color=group_col)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  else:
38
+ fig = px.scatter(df, x=x_col, y=y_col)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  else:
41
+ fig = go.Figure()
42
+ fig.add_annotation(text="目前支援:長條圖、堆疊圖、折線圖、散點圖", showarrow=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ fig.update_layout(template="plotly_white", height=400)
45
+ return fig
 
 
 
 
 
 
 
 
 
 
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  except Exception as e:
48
+ fig = go.Figure()
49
+ fig.add_annotation(text=f"圖表錯誤:{str(e)}", showarrow=False)
50
+ return fig
51
 
52
+ def update_column_options(df):
 
53
  if df is None or df.empty:
54
+ return gr.update(choices=[]), gr.update(choices=[]), gr.update(choices=["無"])
55
+ cols = df.columns.tolist()
56
+ return gr.update(choices=cols, value=cols[0]), gr.update(choices=cols, value=cols[-1]), gr.update(choices=["無"] + cols, value="無")
57
+
58
+ with gr.Blocks(title="雙圖比較") as demo:
59
+ gr.Markdown("## 📊 雙圖比較視覺化工具")
60
+
61
+ with gr.Row():
62
+ file = gr.File(label="上傳 CSV")
63
+ status = gr.Textbox(label="狀態", interactive=False)
64
+
65
+ df_state = gr.State()
66
+
67
+ def load_csv(file_obj):
68
+ try:
69
+ df = pd.read_csv(file_obj.name)
70
+ return df, f"成功載入 {len(df)} 筆資料"
71
+ except Exception as e:
72
+ return None, f"讀取錯誤: {e}"
73
+
74
+ file.change(fn=load_csv, inputs=[file], outputs=[df_state, status])
75
+
76
+ gr.Markdown("### 📌 圖表設定")
77
+
78
+ with gr.Row():
79
+ with gr.Column():
80
+ gr.Markdown("#### 圖一")
81
+ chart_type1 = gr.Dropdown(["長條圖", "堆疊長條圖", "折線圖", "散點圖"], label="圖表類型", value="長條圖")
82
+ x1 = gr.Dropdown(label="X 軸")
83
+ y1 = gr.Dropdown(label="Y 軸")
84
+ group1 = gr.Dropdown(label="分組欄位", choices=["無"], value="無")
85
+ plot1 = gr.Plot()
86
+
87
+ with gr.Column():
88
+ gr.Markdown("#### 圖二")
89
+ chart_type2 = gr.Dropdown(["長條圖", "堆疊長條圖", "折線圖", "散點圖"], label="圖表類型", value="堆疊長條圖")
90
+ x2 = gr.Dropdown(label="X ")
91
+ y2 = gr.Dropdown(label="Y 軸")
92
+ group2 = gr.Dropdown(label="分組欄位", choices=["無"], value="無")
93
+ plot2 = gr.Plot()
94
+
95
+ update_btn = gr.Button("產生圖表")
96
+
97
+ update_btn.click(fn=create_plot, inputs=[df_state, chart_type1, x1, y1, group1], outputs=[plot1])
98
+ update_btn.click(fn=create_plot, inputs=[df_state, chart_type2, x2, y2, group2], outputs=[plot2])
99
+ file.change(fn=update_column_options, inputs=[df_state], outputs=[x1, y1, group1])
100
+ file.change(fn=update_column_options, inputs=[df_state], outputs=[x2, y2, group2])
101
+
102
+ demo.launch()