s880453 commited on
Commit
8250165
·
verified ·
1 Parent(s): bd09e02

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +566 -0
app.py ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ LineChart, Line, BarChart, Bar, PieChart, Pie, AreaChart, Area,
4
+ XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
5
+ ScatterChart, Scatter, Cell
6
+ } from 'recharts';
7
+ import Papa from 'papaparse';
8
+
9
+ // 主色調
10
+ const COLORS = ['#8884d8', '#82ca9d', '#ffc658', '#ff8042', '#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#a4de6c'];
11
+
12
+ const App = () => {
13
+ const [data, setData] = useState([]);
14
+ const [headers, setHeaders] = useState([]);
15
+ const [chartType, setChartType] = useState('bar');
16
+ const [xAxis, setXAxis] = useState('');
17
+ const [yAxis, setYAxis] = useState('');
18
+ const [pieColumn, setPieColumn] = useState('');
19
+ const [tableData, setTableData] = useState([['', ''], ['', '']]);
20
+ const [customization, setCustomization] = useState({
21
+ chartWidth: 600,
22
+ chartHeight: 400,
23
+ fontSize: 12,
24
+ legendPosition: 'bottom',
25
+ colorScheme: COLORS,
26
+ showGrid: true,
27
+ showTooltip: true,
28
+ showLegend: true,
29
+ });
30
+
31
+ useEffect(() => {
32
+ // 表格發生變化時更新數據
33
+ processTableData();
34
+ }, [tableData]);
35
+
36
+ const processTableData = () => {
37
+ if (tableData.length <= 1) return;
38
+
39
+ const headers = tableData[0].filter(h => h.trim() !== '');
40
+ if (headers.length < 1) return;
41
+
42
+ setHeaders(headers);
43
+
44
+ const processedData = [];
45
+
46
+ for (let i = 1; i < tableData.length; i++) {
47
+ if (tableData[i].some(cell => cell.trim() !== '')) {
48
+ const rowData = {};
49
+
50
+ tableData[i].forEach((cell, cellIndex) => {
51
+ if (cellIndex < headers.length) {
52
+ // 嘗試將數字字符串轉換為數字
53
+ const value = cell.trim();
54
+ const numValue = parseFloat(value);
55
+ rowData[headers[cellIndex]] = isNaN(numValue) ? value : numValue;
56
+ }
57
+ });
58
+
59
+ processedData.push(rowData);
60
+ }
61
+ }
62
+
63
+ setData(processedData);
64
+
65
+ // 預設第一個列作為X軸,第二個列作為Y軸
66
+ if (headers.length >= 1 && !xAxis) {
67
+ setXAxis(headers[0]);
68
+ }
69
+
70
+ if (headers.length >= 2 && !yAxis) {
71
+ setYAxis(headers[1]);
72
+ }
73
+
74
+ if (headers.length >= 1 && !pieColumn) {
75
+ setPieColumn(headers[0]);
76
+ }
77
+ };
78
+
79
+ const addRow = () => {
80
+ setTableData([...tableData, Array(tableData[0].length).fill('')]);
81
+ };
82
+
83
+ const addColumn = () => {
84
+ setTableData(tableData.map(row => [...row, '']));
85
+ };
86
+
87
+ const handleCellChange = (rowIndex, colIndex, value) => {
88
+ const newData = [...tableData];
89
+ newData[rowIndex][colIndex] = value;
90
+ setTableData(newData);
91
+ };
92
+
93
+ const handleCustomizationChange = (key, value) => {
94
+ setCustomization({
95
+ ...customization,
96
+ [key]: value
97
+ });
98
+ };
99
+
100
+ const handlePasteData = (e) => {
101
+ e.preventDefault();
102
+ const clipboard = e.clipboardData.getData('text');
103
+ const rows = clipboard.split('\n').filter(row => row.trim());
104
+
105
+ if (rows.length > 0) {
106
+ const pastedData = rows.map(row => row.split('\t'));
107
+
108
+ // 確保所有行有相同的列數
109
+ const maxCols = Math.max(...pastedData.map(row => row.length));
110
+ const normalizedData = pastedData.map(row => {
111
+ while (row.length < maxCols) {
112
+ row.push('');
113
+ }
114
+ return row;
115
+ });
116
+
117
+ setTableData(normalizedData);
118
+ }
119
+ };
120
+
121
+ const handleFileUpload = (e) => {
122
+ const file = e.target.files[0];
123
+ if (file) {
124
+ Papa.parse(file, {
125
+ complete: (results) => {
126
+ if (results.data && results.data.length > 0) {
127
+ setTableData(results.data);
128
+ }
129
+ }
130
+ });
131
+ }
132
+ };
133
+
134
+ const renderChart = () => {
135
+ if (data.length === 0) {
136
+ return <div className="p-4 text-center">請輸入或粘貼數據以生成圖表</div>;
137
+ }
138
+
139
+ const { chartWidth, chartHeight, fontSize, showGrid, showTooltip, showLegend, legendPosition, colorScheme } = customization;
140
+
141
+ const style = {
142
+ fontSize: `${fontSize}px`
143
+ };
144
+
145
+ switch (chartType) {
146
+ case 'bar':
147
+ return (
148
+ <ResponsiveContainer width={chartWidth} height={chartHeight}>
149
+ <BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
150
+ {showGrid && <CartesianGrid strokeDasharray="3 3" />}
151
+ <XAxis dataKey={xAxis} style={style} />
152
+ <YAxis style={style} />
153
+ {showTooltip && <Tooltip />}
154
+ {showLegend && <Legend layout="horizontal" verticalAlign={legendPosition} />}
155
+ <Bar dataKey={yAxis} fill={colorScheme[0]} />
156
+ </BarChart>
157
+ </ResponsiveContainer>
158
+ );
159
+
160
+ case 'line':
161
+ return (
162
+ <ResponsiveContainer width={chartWidth} height={chartHeight}>
163
+ <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
164
+ {showGrid && <CartesianGrid strokeDasharray="3 3" />}
165
+ <XAxis dataKey={xAxis} style={style} />
166
+ <YAxis style={style} />
167
+ {showTooltip && <Tooltip />}
168
+ {showLegend && <Legend layout="horizontal" verticalAlign={legendPosition} />}
169
+ <Line type="monotone" dataKey={yAxis} stroke={colorScheme[0]} activeDot={{ r: 8 }} />
170
+ </LineChart>
171
+ </ResponsiveContainer>
172
+ );
173
+
174
+ case 'area':
175
+ return (
176
+ <ResponsiveContainer width={chartWidth} height={chartHeight}>
177
+ <AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
178
+ {showGrid && <CartesianGrid strokeDasharray="3 3" />}
179
+ <XAxis dataKey={xAxis} style={style} />
180
+ <YAxis style={style} />
181
+ {showTooltip && <Tooltip />}
182
+ {showLegend && <Legend layout="horizontal" verticalAlign={legendPosition} />}
183
+ <Area type="monotone" dataKey={yAxis} stroke={colorScheme[0]} fill={colorScheme[0]} />
184
+ </AreaChart>
185
+ </ResponsiveContainer>
186
+ );
187
+
188
+ case 'pie':
189
+ // 對餅圖數據進行特殊處理
190
+ const pieData = data.map(item => ({
191
+ name: String(item[pieColumn]),
192
+ value: typeof item[yAxis] === 'number' ? item[yAxis] : 0
193
+ }));
194
+
195
+ return (
196
+ <ResponsiveContainer width={chartWidth} height={chartHeight}>
197
+ <PieChart margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
198
+ {showTooltip && <Tooltip />}
199
+ {showLegend && <Legend layout="horizontal" verticalAlign={legendPosition} />}
200
+ <Pie
201
+ data={pieData}
202
+ cx="50%"
203
+ cy="50%"
204
+ labelLine={true}
205
+ outerRadius={chartHeight / 3}
206
+ fill="#8884d8"
207
+ dataKey="value"
208
+ nameKey="name"
209
+ label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
210
+ >
211
+ {pieData.map((entry, index) => (
212
+ <Cell key={`cell-${index}`} fill={colorScheme[index % colorScheme.length]} />
213
+ ))}
214
+ </Pie>
215
+ </PieChart>
216
+ </ResponsiveContainer>
217
+ );
218
+
219
+ case 'scatter':
220
+ return (
221
+ <ResponsiveContainer width={chartWidth} height={chartHeight}>
222
+ <ScatterChart margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
223
+ {showGrid && <CartesianGrid strokeDasharray="3 3" />}
224
+ <XAxis dataKey={xAxis} name={xAxis} style={style} />
225
+ <YAxis dataKey={yAxis} name={yAxis} style={style} />
226
+ {showTooltip && <Tooltip cursor={{ strokeDasharray: '3 3' }} />}
227
+ {showLegend && <Legend layout="horizontal" verticalAlign={legendPosition} />}
228
+ <Scatter name={`${xAxis} vs ${yAxis}`} data={data} fill={colorScheme[0]} />
229
+ </ScatterChart>
230
+ </ResponsiveContainer>
231
+ );
232
+
233
+ default:
234
+ return null;
235
+ }
236
+ };
237
+
238
+ const exportSvg = () => {
239
+ // 獲取SVG內容
240
+ const svgElement = document.querySelector('.recharts-wrapper svg');
241
+ if (svgElement) {
242
+ // 創建一個Blob對象
243
+ const svgData = new XMLSerializer().serializeToString(svgElement);
244
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
245
+ const svgUrl = URL.createObjectURL(svgBlob);
246
+
247
+ // 創建下載鏈接
248
+ const downloadLink = document.createElement('a');
249
+ downloadLink.href = svgUrl;
250
+ downloadLink.download = `chart_export_${new Date().toISOString()}.svg`;
251
+ document.body.appendChild(downloadLink);
252
+ downloadLink.click();
253
+ document.body.removeChild(downloadLink);
254
+ }
255
+ };
256
+
257
+ const exportPng = () => {
258
+ // 將SVG轉換為Canvas,然後導出為PNG
259
+ const svgElement = document.querySelector('.recharts-wrapper svg');
260
+ if (svgElement) {
261
+ const svgData = new XMLSerializer().serializeToString(svgElement);
262
+ const canvas = document.createElement('canvas');
263
+ const ctx = canvas.getContext('2d');
264
+
265
+ // 設置Canvas大小
266
+ canvas.width = customization.chartWidth;
267
+ canvas.height = customization.chartHeight;
268
+
269
+ const img = new Image();
270
+ img.onload = function() {
271
+ ctx.drawImage(img, 0, 0);
272
+ const pngUrl = canvas.toDataURL('image/png');
273
+
274
+ const downloadLink = document.createElement('a');
275
+ downloadLink.href = pngUrl;
276
+ downloadLink.download = `chart_export_${new Date().toISOString()}.png`;
277
+ document.body.appendChild(downloadLink);
278
+ downloadLink.click();
279
+ document.body.removeChild(downloadLink);
280
+ };
281
+
282
+ img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
283
+ }
284
+ };
285
+
286
+ const exportCsv = () => {
287
+ // 導出數據為CSV
288
+ const csvContent = Papa.unparse(tableData);
289
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
290
+ const url = URL.createObjectURL(blob);
291
+
292
+ const downloadLink = document.createElement('a');
293
+ downloadLink.href = url;
294
+ downloadLink.download = `data_export_${new Date().toISOString()}.csv`;
295
+ document.body.appendChild(downloadLink);
296
+ downloadLink.click();
297
+ document.body.removeChild(downloadLink);
298
+ };
299
+
300
+ return (
301
+ <div className="p-4 max-w-6xl mx-auto">
302
+ <h1 className="text-2xl font-bold mb-6 text-center">數據可視化工具</h1>
303
+
304
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
305
+ {/* 數據輸入區 */}
306
+ <div className="p-4 border rounded shadow-sm">
307
+ <h2 className="text-lg font-semibold mb-3">數據輸入</h2>
308
+
309
+ <div className="mb-4 flex gap-2">
310
+ <button
311
+ onClick={addRow}
312
+ className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
313
+ >
314
+ 添加行
315
+ </button>
316
+ <button
317
+ onClick={addColumn}
318
+ className="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600"
319
+ >
320
+ 添加列
321
+ </button>
322
+ <label className="px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 cursor-pointer">
323
+ 上傳CSV
324
+ <input
325
+ type="file"
326
+ accept=".csv"
327
+ onChange={handleFileUpload}
328
+ className="hidden"
329
+ />
330
+ </label>
331
+ <button
332
+ onClick={exportCsv}
333
+ className="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600"
334
+ >
335
+ 導出CSV
336
+ </button>
337
+ </div>
338
+
339
+ <div className="mb-4 overflow-x-auto">
340
+ <div
341
+ className="border rounded"
342
+ onPaste={handlePasteData}
343
+ >
344
+ <table className="min-w-full divide-y divide-gray-200">
345
+ <tbody className="bg-white divide-y divide-gray-200">
346
+ {tableData.map((row, rowIndex) => (
347
+ <tr key={rowIndex}>
348
+ {row.map((cell, colIndex) => (
349
+ <td key={colIndex} className="px-2 py-1 border">
350
+ <input
351
+ type="text"
352
+ value={cell}
353
+ onChange={(e) => handleCellChange(rowIndex, colIndex, e.target.value)}
354
+ className="w-full border-0 focus:ring-0"
355
+ />
356
+ </td>
357
+ ))}
358
+ </tr>
359
+ ))}
360
+ </tbody>
361
+ </table>
362
+ </div>
363
+ <p className="text-sm text-gray-500 mt-2">提示:您可以直接從Excel或其他表格軟件複製並粘貼數據</p>
364
+ </div>
365
+ </div>
366
+
367
+ {/* 圖表區 */}
368
+ <div className="p-4 border rounded shadow-sm">
369
+ <h2 className="text-lg font-semibold mb-3">圖表預覽</h2>
370
+
371
+ <div className="mb-4 grid grid-cols-2 gap-4">
372
+ <div>
373
+ <label className="block text-sm font-medium mb-1">圖表類型</label>
374
+ <select
375
+ value={chartType}
376
+ onChange={(e) => setChartType(e.target.value)}
377
+ className="w-full p-2 border rounded"
378
+ >
379
+ <option value="bar">長條圖</option>
380
+ <option value="line">折線圖</option>
381
+ <option value="area">區域圖</option>
382
+ <option value="pie">圓餅圖</option>
383
+ <option value="scatter">散點圖</option>
384
+ </select>
385
+ </div>
386
+
387
+ {chartType !== 'pie' && (
388
+ <div>
389
+ <label className="block text-sm font-medium mb-1">X軸</label>
390
+ <select
391
+ value={xAxis}
392
+ onChange={(e) => setXAxis(e.target.value)}
393
+ className="w-full p-2 border rounded"
394
+ >
395
+ {headers.map((header, index) => (
396
+ <option key={index} value={header}>{header}</option>
397
+ ))}
398
+ </select>
399
+ </div>
400
+ )}
401
+
402
+ <div>
403
+ <label className="block text-sm font-medium mb-1">
404
+ {chartType === 'pie' ? '數值列' : 'Y軸'}
405
+ </label>
406
+ <select
407
+ value={yAxis}
408
+ onChange={(e) => setYAxis(e.target.value)}
409
+ className="w-full p-2 border rounded"
410
+ >
411
+ {headers.map((header, index) => (
412
+ <option key={index} value={header}>{header}</option>
413
+ ))}
414
+ </select>
415
+ </div>
416
+
417
+ {chartType === 'pie' && (
418
+ <div>
419
+ <label className="block text-sm font-medium mb-1">類別列</label>
420
+ <select
421
+ value={pieColumn}
422
+ onChange={(e) => setPieColumn(e.target.value)}
423
+ className="w-full p-2 border rounded"
424
+ >
425
+ {headers.map((header, index) => (
426
+ <option key={index} value={header}>{header}</option>
427
+ ))}
428
+ </select>
429
+ </div>
430
+ )}
431
+ </div>
432
+
433
+ <div className="mb-4 flex justify-center overflow-auto">
434
+ {renderChart()}
435
+ </div>
436
+
437
+ <div className="flex gap-2 justify-center">
438
+ <button
439
+ onClick={exportSvg}
440
+ className="px-3 py-1 bg-indigo-500 text-white rounded hover:bg-indigo-600"
441
+ >
442
+ 導出SVG
443
+ </button>
444
+ <button
445
+ onClick={exportPng}
446
+ className="px-3 py-1 bg-pink-500 text-white rounded hover:bg-pink-600"
447
+ >
448
+ 導出PNG
449
+ </button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ {/* 自定義選項 */}
455
+ <div className="mt-6 p-4 border rounded shadow-sm">
456
+ <h2 className="text-lg font-semibold mb-3">自定義選項</h2>
457
+
458
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
459
+ <div>
460
+ <label className="block text-sm font-medium mb-1">圖表寬度</label>
461
+ <input
462
+ type="range"
463
+ min="300"
464
+ max="1200"
465
+ value={customization.chartWidth}
466
+ onChange={(e) => handleCustomizationChange('chartWidth', Number(e.target.value))}
467
+ className="w-full"
468
+ />
469
+ <div className="text-xs text-center">{customization.chartWidth}px</div>
470
+ </div>
471
+
472
+ <div>
473
+ <label className="block text-sm font-medium mb-1">圖表高度</label>
474
+ <input
475
+ type="range"
476
+ min="200"
477
+ max="800"
478
+ value={customization.chartHeight}
479
+ onChange={(e) => handleCustomizationChange('chartHeight', Number(e.target.value))}
480
+ className="w-full"
481
+ />
482
+ <div className="text-xs text-center">{customization.chartHeight}px</div>
483
+ </div>
484
+
485
+ <div>
486
+ <label className="block text-sm font-medium mb-1">字體大小</label>
487
+ <input
488
+ type="range"
489
+ min="8"
490
+ max="24"
491
+ value={customization.fontSize}
492
+ onChange={(e) => handleCustomizationChange('fontSize', Number(e.target.value))}
493
+ className="w-full"
494
+ />
495
+ <div className="text-xs text-center">{customization.fontSize}px</div>
496
+ </div>
497
+
498
+ <div>
499
+ <label className="block text-sm font-medium mb-1">圖例位置</label>
500
+ <select
501
+ value={customization.legendPosition}
502
+ onChange={(e) => handleCustomizationChange('legendPosition', e.target.value)}
503
+ className="w-full p-2 border rounded"
504
+ >
505
+ <option value="top">頂部</option>
506
+ <option value="bottom">底部</option>
507
+ <option value="left">左側</option>
508
+ <option value="right">右側</option>
509
+ </select>
510
+ </div>
511
+
512
+ <div>
513
+ <label className="block text-sm font-medium mb-1">顯示網格線</label>
514
+ <input
515
+ type="checkbox"
516
+ checked={customization.showGrid}
517
+ onChange={(e) => handleCustomizationChange('showGrid', e.target.checked)}
518
+ className="mr-2"
519
+ />
520
+ </div>
521
+
522
+ <div>
523
+ <label className="block text-sm font-medium mb-1">顯示提示框</label>
524
+ <input
525
+ type="checkbox"
526
+ checked={customization.showTooltip}
527
+ onChange={(e) => handleCustomizationChange('showTooltip', e.target.checked)}
528
+ className="mr-2"
529
+ />
530
+ </div>
531
+
532
+ <div>
533
+ <label className="block text-sm font-medium mb-1">顯示圖例</label>
534
+ <input
535
+ type="checkbox"
536
+ checked={customization.showLegend}
537
+ onChange={(e) => handleCustomizationChange('showLegend', e.target.checked)}
538
+ className="mr-2"
539
+ />
540
+ </div>
541
+
542
+ <div className="col-span-full">
543
+ <label className="block text-sm font-medium mb-1">顏色方案</label>
544
+ <div className="flex flex-wrap gap-2">
545
+ {customization.colorScheme.map((color, index) => (
546
+ <input
547
+ key={index}
548
+ type="color"
549
+ value={color}
550
+ onChange={(e) => {
551
+ const newColors = [...customization.colorScheme];
552
+ newColors[index] = e.target.value;
553
+ handleCustomizationChange('colorScheme', newColors);
554
+ }}
555
+ className="w-8 h-8 p-0 border-0"
556
+ />
557
+ ))}
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ );
564
+ };
565
+
566
+ export default App;