File size: 130,279 Bytes
9a186a0
 
 
 
 
 
 
 
 
 
 
705707d
 
9a186a0
705707d
 
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ac2b99
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
 
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
 
9a186a0
 
 
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
d823ce6
9a186a0
 
 
 
 
72f82d7
9a186a0
72f82d7
9a186a0
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
a866508
72f82d7
9a186a0
 
 
a866508
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705707d
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
 
 
0f3721f
9a186a0
0f3721f
9a186a0
 
 
 
 
0f3721f
 
 
9a186a0
 
 
 
 
0f3721f
 
9a186a0
 
 
 
 
 
0f3721f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
0f3721f
72f82d7
0f3721f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a186a0
0f3721f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a186a0
 
0f3721f
 
 
 
 
 
 
 
 
 
 
 
 
9a186a0
 
0f3721f
9a186a0
0f3721f
9a186a0
0f3721f
 
9a186a0
 
0f3721f
 
 
 
72f82d7
9a186a0
0f3721f
 
 
 
 
 
9a186a0
 
0f3721f
 
9a186a0
0f3721f
9a186a0
 
72f82d7
0f3721f
9a186a0
0f3721f
 
 
 
 
 
 
 
 
 
 
 
 
 
9a186a0
 
 
 
 
 
e6d0b8d
0f3721f
 
 
 
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
9a186a0
 
0f3721f
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
 
 
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
9a186a0
 
0f3721f
 
 
 
9a186a0
0f3721f
 
 
 
9a186a0
0f3721f
 
 
9a186a0
0f3721f
 
 
 
9a186a0
0f3721f
 
 
9a186a0
0f3721f
 
 
 
 
9a186a0
0f3721f
 
 
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
 
9a186a0
0f3721f
9a186a0
0f3721f
 
 
 
9a186a0
0f3721f
 
 
 
 
 
 
9a186a0
 
0f3721f
9a186a0
 
0f3721f
9a186a0
0f3721f
 
 
 
9a186a0
0f3721f
 
 
9a186a0
0f3721f
 
9a186a0
0f3721f
 
 
 
 
 
 
 
 
9a186a0
0f3721f
9a186a0
 
0f3721f
 
 
9a186a0
 
 
 
 
0f3721f
9a186a0
 
 
0f3721f
9a186a0
 
 
0f3721f
 
9a186a0
0f3721f
9a186a0
0f3721f
 
9a186a0
 
 
 
0f3721f
 
 
9a186a0
 
 
0f3721f
 
 
 
 
9a186a0
0f3721f
 
 
 
 
 
 
9a186a0
 
0f3721f
 
72f82d7
0f3721f
 
72f82d7
0f3721f
9a186a0
72f82d7
9a186a0
 
 
8250165
9a186a0
 
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
 
8250165
72f82d7
9a186a0
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705707d
9a186a0
 
 
72f82d7
705707d
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
fac038f
9a186a0
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f82d7
9a186a0
 
 
 
72f82d7
9a186a0
 
 
 
72f82d7
9a186a0
 
 
 
a866508
9a186a0
 
 
72f82d7
9a186a0
 
 
 
 
 
 
 
 
 
d021b9c
 
8ebb2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d021b9c
8ebb2b8
 
 
 
ddfbd0d
8ebb2b8
ddfbd0d
 
 
8ebb2b8
 
 
ddfbd0d
2f05ed2
 
 
ddfbd0d
 
2f05ed2
8ebb2b8
 
 
d021b9c
8ebb2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddfbd0d
8ebb2b8
 
ddfbd0d
 
8ebb2b8
ddfbd0d
8ebb2b8
 
 
ddfbd0d
 
8ebb2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f05ed2
 
8ebb2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddfbd0d
 
8ebb2b8
 
 
 
 
 
ddfbd0d
d021b9c
ddfbd0d
 
 
8ebb2b8
ddfbd0d
 
d021b9c
2f05ed2
 
 
d021b9c
 
ddfbd0d
 
c1d8626
ddfbd0d
 
c1d8626
ddfbd0d
d021b9c
ddfbd0d
 
 
 
d021b9c
ddfbd0d
 
2f05ed2
ddfbd0d
 
 
c1d8626
2f05ed2
8ebb2b8
 
 
 
 
 
2f05ed2
90222e0
d021b9c
90222e0
ddfbd0d
 
 
 
 
 
 
 
 
90222e0
ddfbd0d
 
 
90222e0
 
ddfbd0d
 
 
8ebb2b8
 
2f05ed2
 
 
 
 
c1d8626
ddfbd0d
2f05ed2
 
ddfbd0d
2f05ed2
90222e0
ddfbd0d
8ebb2b8
d021b9c
 
2f05ed2
90222e0
2f05ed2
d021b9c
 
 
 
2f05ed2
90222e0
ddfbd0d
 
2f05ed2
ddfbd0d
 
2f05ed2
 
8ebb2b8
90222e0
 
 
 
d021b9c
 
90222e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ebb2b8
 
2f05ed2
90222e0
 
 
d021b9c
 
 
 
2f05ed2
 
 
 
90222e0
8ebb2b8
 
 
d021b9c
8ebb2b8
 
c1d8626
8ebb2b8
d021b9c
 
8ebb2b8
 
d021b9c
90222e0
d021b9c
 
 
 
 
 
 
90222e0
8ebb2b8
90222e0
d021b9c
90222e0
d021b9c
90222e0
 
8ebb2b8
 
d021b9c
2f05ed2
90222e0
d021b9c
 
90222e0
2f05ed2
d021b9c
 
2f05ed2
 
 
90222e0
d021b9c
90222e0
8ebb2b8
 
 
 
 
26f2127
8ebb2b8
d021b9c
2f05ed2
c1d8626
d021b9c
 
90222e0
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
 
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
90222e0
8ebb2b8
90222e0
 
 
 
 
 
 
 
 
 
8ebb2b8
d021b9c
 
8ebb2b8
d021b9c
8ebb2b8
2f05ed2
d021b9c
8ebb2b8
2f05ed2
90222e0
8ebb2b8
d021b9c
 
 
 
8ebb2b8
d021b9c
 
 
 
90222e0
 
 
 
 
 
 
 
 
8ebb2b8
2f05ed2
8ebb2b8
2f05ed2
90222e0
8ebb2b8
2f05ed2
 
 
 
 
 
 
8ebb2b8
 
2f05ed2
8ebb2b8
 
 
 
2f05ed2
 
 
 
8ebb2b8
2f05ed2
 
90222e0
8ebb2b8
d021b9c
2f05ed2
8ebb2b8
90222e0
2f05ed2
 
 
 
 
 
 
 
8ebb2b8
 
 
 
2f05ed2
90222e0
8ebb2b8
 
2f05ed2
8ebb2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddfbd0d
8ebb2b8
 
 
 
 
 
 
 
 
 
90222e0
f4427e4
8ebb2b8
f4427e4
 
920dd49
f4427e4
8ebb2b8
920dd49
fed829f
 
920dd49
fed829f
 
90222e0
fed829f
 
8ebb2b8
90222e0
8ebb2b8
 
 
ddfbd0d
8ebb2b8
5c697d2
 
 
 
 
 
 
 
8ebb2b8
 
5c697d2
 
 
 
 
 
 
 
 
 
 
8ebb2b8
 
 
 
ddfbd0d
8ebb2b8
5c697d2
421840a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ebb2b8
 
 
 
421840a
8ebb2b8
 
 
 
 
 
 
 
 
 
2f05ed2
8ebb2b8
 
2f05ed2
8ebb2b8
2f05ed2
8ebb2b8
 
 
 
 
 
2f05ed2
8ebb2b8
 
 
 
 
 
 
 
 
2f05ed2
 
 
 
 
8ebb2b8
 
 
 
 
2f05ed2
 
9a186a0
 
 
 
 
5170007
9a186a0
 
 
 
5170007
c1d8626
9a186a0
 
 
8ebb2b8
9a186a0
ddfbd0d
 
 
9a186a0
 
 
 
 
 
72f82d7
8ebb2b8
9a186a0
 
 
8ebb2b8
9a186a0
 
 
8ebb2b8
9a186a0
 
8ebb2b8
9a186a0
 
 
2f05ed2
9a186a0
ddfbd0d
9a186a0
 
 
 
ddfbd0d
 
 
 
f4427e4
ddfbd0d
 
f4427e4
ddfbd0d
2f05ed2
 
ddfbd0d
 
 
 
 
 
 
 
 
 
 
2f05ed2
 
ddfbd0d
 
 
 
 
 
 
2f05ed2
 
ddfbd0d
 
 
 
 
 
 
 
 
 
f4427e4
ddfbd0d
 
 
 
f4427e4
ddfbd0d
 
f4427e4
ddfbd0d
f4427e4
9a186a0
 
 
2f05ed2
9a186a0
ddfbd0d
9a186a0
8ebb2b8
ddfbd0d
 
 
 
9a186a0
ddfbd0d
9a186a0
 
 
 
 
 
 
ddfbd0d
 
 
9a186a0
ddfbd0d
 
c297ae2
ddfbd0d
 
 
0a5a399
ddfbd0d
 
 
 
2f05ed2
ddfbd0d
2f05ed2
 
ddfbd0d
2f05ed2
 
 
 
 
ddfbd0d
2f05ed2
 
 
ddfbd0d
 
2f05ed2
9a186a0
2f05ed2
 
 
 
 
ddfbd0d
2f05ed2
 
 
ddfbd0d
 
2f05ed2
9a186a0
 
2f05ed2
9a186a0
8ebb2b8
9a186a0
ddfbd0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421840a
 
 
 
 
 
 
 
 
 
 
 
f4427e4
ddfbd0d
c297ae2
ddfbd0d
 
9a186a0
ddfbd0d
 
8ebb2b8
 
421840a
 
 
 
2f05ed2
 
ddfbd0d
 
 
 
2f05ed2
72f82d7
ddfbd0d
 
 
 
 
 
8ebb2b8
ddfbd0d
8ebb2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
ddfbd0d
8ebb2b8
 
 
2f05ed2
ddfbd0d
9a186a0
 
 
 
72f82d7
421840a
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
# -*- coding: utf-8 -*-
"""
Gradio 應用程式:進階數據可視化工具
作者:Gemini
版本:1.0 (分段提供 - Part 1)
描述:此部分包含套件導入、常數定義、輔助函數和 CSS 樣式。
"""

# =========================================
# == 套件導入 (Import Libraries) ==
# =========================================
import gradio as gr
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import io
import base64
from PIL import Image
# import matplotlib.pyplot as plt # Matplotlib/Seaborn 在此版本中未使用,暫時註解
# import seaborn as sns           # Matplotlib/Seaborn 在此版本中未使用,暫時註解
from plotly.subplots import make_subplots
import re
import json
import colorsys
import traceback # 用於更詳細的錯誤追蹤

# =========================================
# == 常數定義 (Constants) ==
# =========================================

# 圖表類型選項 (Chart Type Options)
# 擴展並稍微調整順序以符合常見用法
CHART_TYPES = [
    # --- 長條圖系列 ---
    "長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖",
    # --- 折線圖系列 ---
    "折線圖", "多重折線圖", "階梯折線圖",
    # --- 區域圖系列 ---
    "區域圖", "堆疊區域圖", "百分比堆疊區域圖",
    # --- 圓形圖系列 ---
    "圓餅圖", "環形圖",
    # --- 散佈圖系列 ---
    "散點圖", "氣泡圖",
    # --- 分佈圖系列 ---
    "直方圖", "箱型圖", "小提琴圖",
    # --- 關係圖系列 ---
    "熱力圖", "樹狀圖",
    # --- 其他圖表 ---
    "雷達圖", "漏斗圖", "極座標圖", "甘特圖"
]

# 顏色方案選項 (Color Scheme Options)
# 增加更多 Plotly 內建方案並分類
COLOR_SCHEMES = {
    "預設 (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,
    "連續 - 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,
    "發散 - 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,
    "循環 - Twilight": px.colors.cyclical.Twilight,
    "循環 - IceFire": px.colors.cyclical.IceFire,
}

# 圖案填充選項 (Pattern Fill Options)
PATTERN_TYPES = [
    "無", "/", "\\", "x", "-", "|", "+", "."
]

# 聚合函數選項 (Aggregation Function Options)
AGGREGATION_FUNCTIONS = [
    "計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"
]

# =========================================
# == 輔助函數 (Helper Functions) ==
# =========================================

# --- 顏色處理相關 ---

# HTML 顏色展示卡片樣式
COLOR_CARD_STYLE = """
<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">
    {color_cards}
</div>
"""

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>
"""

COPY_SCRIPT = """
<script>
function copyToClipboard(text) {
    navigator.clipboard.writeText(text).then(() => {
        // 查找或創建通知容器
        let notificationContainer = document.getElementById('clipboard-notification-container');
        if (!notificationContainer) {
            notificationContainer = document.createElement('div');
            notificationContainer.id = 'clipboard-notification-container';
            notificationContainer.style.position = 'fixed';
            notificationContainer.style.bottom = '20px';
            notificationContainer.style.right = '20px';
            notificationContainer.style.zIndex = '10000'; // 確保在最上層
            notificationContainer.style.display = 'flex';
            notificationContainer.style.flexDirection = 'column';
            notificationContainer.style.alignItems = 'flex-end';
            document.body.appendChild(notificationContainer);
        }

        // 創建通知元素
        const notification = document.createElement('div');
        notification.textContent = '已複製: ' + text;
        notification.style.background = 'rgba(0, 0, 0, 0.7)';
        notification.style.color = 'white';
        notification.style.padding = '8px 15px';
        notification.style.borderRadius = '4px';
        notification.style.marginTop = '5px';
        notification.style.fontSize = '14px';
        notification.style.opacity = '1';
        notification.style.transition = 'opacity 0.5s ease-out';
        notification.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';

        // 添加到容器並設置消失
        notificationContainer.appendChild(notification);

        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                notification.remove();
                // 如果容器空了,可以考慮移除容器
                if (notificationContainer.children.length === 0) {
                   // notificationContainer.remove(); // 或者保留以便後續使用
                }
            }, 500); // 等待淡出動畫完成
        }, 1500); // 顯示 1.5 秒
    }).catch(err => {
        console.error('無法複製顏色代碼: ', err);
        // 可以添加一個錯誤提示
    });
}
</script>
"""

# 常見顏色名稱和十六進制代碼
COMMON_COLORS = {
    "紅色": "#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",
}

# 生成漸變色系
def generate_gradient_colors(start_color, end_color, steps=10):
    """生成從開始顏色到結束顏色的漸變色列表"""
    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))

    def rgb_to_hex(rgb):
        # 確保 RGB 值在 0-255 範圍內
        r, g, b = [max(0, min(255, int(c))) for c in rgb]
        return '#{:02x}{:02x}{:02x}'.format(r, g, b)

    try:
        start_rgb = hex_to_rgb(start_color)
        end_rgb = hex_to_rgb(end_color)

        if steps <= 1:
            return [start_color] if steps == 1 else []

        r_step = (end_rgb[0] - start_rgb[0]) / (steps - 1)
        g_step = (end_rgb[1] - start_rgb[1]) / (steps - 1)
        b_step = (end_rgb[2] - start_rgb[2]) / (steps - 1)

        gradient_colors = []
        for i in range(steps):
            r = start_rgb[0] + r_step * i
            g = start_rgb[1] + g_step * i
            b = start_rgb[2] + b_step * i
            gradient_colors.append(rgb_to_hex((r, g, b)))

        return gradient_colors
    except Exception as e:
        print(f"生成漸變色時出錯: {e}")
        return [start_color, end_color] # 出錯時返回基礎顏色

# 定義一些常用的漸變色系
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"]
}

# 生成顏色卡片展示 HTML
def generate_color_cards():
    """生成包含常見顏色和漸變色的 HTML 卡片展示"""
    # 常見顏色卡片
    common_cards = ""
    for name, hex_code in COMMON_COLORS.items():
        common_cards += COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code)

    # 漸變色系卡片
    gradient_cards_html = ""
    for name, colors in GRADIENTS.items():
        cards = ""
        for i, color in enumerate(colors):
            cards += COLOR_CARD_TEMPLATE.format(
                color_name=f"{name} {i+1}/{len(colors)}",
                color_hex=color
            )
        gradient_cards_html += f"""
        <div style="font-weight: bold; margin-top: 10px; font-size: 14px; color: #555;">{name}</div>
        {COLOR_CARD_STYLE.format(color_cards=cards)}
        """

    # 合成卡片展示HTML
    color_display = f"""
    <div style="font-weight: bold; margin-top: 10px; font-size: 14px; color: #555;">常用單色</div>
    {COLOR_CARD_STYLE.format(color_cards=common_cards)}
    {gradient_cards_html}
    {COPY_SCRIPT}
    """
    return color_display

# --- 數據處理相關 ---

def agg_function_map(func_name):
    """映射中文聚合函數名稱到 Pandas/Numpy 函數或標識符"""
    mapping = {
        "計數": "count",  # Pandas count non-NA values
        "求和": "sum",
        "平均值": "mean",
        "中位數": "median",
        "最大值": "max",
        "最小值": "min",
        "標準差": "std",
        "變異數": "var",
        "第一筆": "first",
        "最後一筆": "last",
        # 'size' is handled specially in create_plot for counting rows including NA
    }
    return mapping.get(func_name, "count") # 默認返回 count

def parse_custom_colors(color_text):
    """解析自定義顏色文本 (例如 "類別A:#FF0000, 類別B:#00FF00")"""
    custom_colors = {}
    if color_text and isinstance(color_text, str) and color_text.strip():
        try:
            # 移除多餘空格並按逗號分割
            pairs = [p.strip() for p in color_text.split(',') if p.strip()]
            for pair in pairs:
                if ':' in pair:
                    # 按第一個冒號分割
                    key, value = pair.split(':', 1)
                    key = key.strip()
                    value = value.strip()
                    # 簡單驗證顏色代碼格式 (以 # 開頭,後跟 3 或 6 個十六進制字符)
                    if re.match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", value):
                        custom_colors[key] = value
                    else:
                        print(f"警告:忽略無效的顏色代碼格式 '{value}' for key '{key}'")
        except Exception as e:
            print(f"解析自定義顏色時出錯: {e}")
            # 出錯時返回空字典
            return {}
    return custom_colors

def update_patterns(*patterns_input):
    """從 Gradio 輸入更新圖案列表,過濾掉 '無'"""
    # patterns_input 會是一個包含 pattern1, pattern2, pattern3... 的元組
    patterns = [p for p in patterns_input if p and p != "無"]
    return patterns

# =========================================
# == CSS 樣式 (CSS Styling) ==
# =========================================
CUSTOM_CSS = """
/* --- 全局和容器 --- */
.gradio-container {
    font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 使用更現代的字體 */
    background: #f8f9fa; /* 更柔和的背景色 */
    /* overflow: visible !important; */ /* 移除全局 overflow,可能導致問題 */
}

/* --- 應用程式標頭 --- */
.app-header {
    text-align: center;
    margin-bottom: 25px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* 漸變色更新 */
    padding: 25px 20px;
    border-radius: 12px; /* 圓角加大 */
    color: white;
    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); /* 陰影加深 */
}
.app-title {
    font-size: 2.2em; /* 標題加大 */
    font-weight: 700; /* 字重加粗 */
    margin: 0;
    letter-spacing: 1px; /* 增加字間距 */
    text-shadow: 1px 1px 3px rgba(0,0,0,0.2); /* 文字陰影 */
}
.app-subtitle {
    font-size: 1.1em; /* 副標題加大 */
    color: #e0e0e0; /* 副標題顏色調整 */
    margin-top: 8px;
    font-weight: 300;
}

/* --- 區塊標題 --- */
.section-title {
    font-size: 1.4em; /* 區塊標題加大 */
    font-weight: 600; /* 字重調整 */
    color: #343a40; /* 標題顏色加深 */
    border-bottom: 3px solid #7367f0; /* 邊框顏色和寬度調整 */
    padding-bottom: 8px;
    margin-top: 25px;
    margin-bottom: 20px;
}

/* --- 卡片樣式 --- */
.card {
    background-color: white;
    border-radius: 10px;
    padding: 25px; /* 內邊距加大 */
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); /* 陰影調整 */
    margin-bottom: 20px;
    transition: transform 0.25s ease-out, box-shadow 0.25s ease-out;
    border: 1px solid #e0e0e0; /* 添加細邊框 */
}
.card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); /* 懸停陰影加強 */
}

/* --- 按鈕樣式 --- */
.primary-button {
    background: linear-gradient(to right, #667eea, #764ba2) !important;
    border: none !important;
    color: white !important;
    font-weight: 600 !important; /* 字重調整 */
    padding: 12px 24px !important; /* 按鈕加大 */
    border-radius: 8px !important; /* 圓角加大 */
    cursor: pointer !important;
    transition: all 0.3s ease !important;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.primary-button:hover {
    background: linear-gradient(to right, #764ba2, #667eea) !important; /* 懸停漸變反轉 */
    transform: translateY(-2px) !important;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; /* 懸停陰影加強 */
}

.secondary-button {
    background: linear-gradient(to right, #89f7fe, #66a6ff) !important; /* 不同漸變色 */
    border: none !important;
    color: #333 !important; /* 文字顏色調整 */
    font-weight: 600 !important;
    padding: 10px 20px !important;
    border-radius: 8px !important;
    cursor: pointer !important;
    transition: all 0.3s ease !important;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.secondary-button:hover {
    background: linear-gradient(to right, #66a6ff, #89f7fe) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
}

/* --- 其他 UI 元素 --- */
.tips-box {
    background-color: #e7f3ff; /* 提示框背景色 */
    border-left: 5px solid #66a6ff; /* 提示框邊框 */
    padding: 15px 20px;
    border-radius: 8px;
    margin: 20px 0;
    font-size: 0.95em;
    color: #333;
}
.tips-box code { /* 提示框中的代碼樣式 */
    background-color: #d1e7fd;
    padding: 2px 5px;
    border-radius: 4px;
    font-family: 'Courier New', Courier, monospace;
}

.chart-previewer {
    border: 2px dashed #ced4da; /* 預覽區邊框 */
    border-radius: 10px;
    padding: 20px;
    min-height: 450px; /* 預覽區最小高度 */
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #ffffff; /* 白色背景 */
    box-shadow: inset 0 0 10px rgba(0,0,0,0.05); /* 內陰影 */
    margin-top: 15px;
}

/* 數據表格預覽 */
.gradio-dataframe table {
    border-collapse: collapse;
    width: 100%;
    font-size: 0.9em;
}
.gradio-dataframe th, .gradio-dataframe td {
    border: 1px solid #dee2e6;
    padding: 8px 10px;
    text-align: left;
}
.gradio-dataframe th {
    background-color: #f8f9fa;
    font-weight: 600;
}
.gradio-dataframe tr:nth-child(even) {
    background-color: #f8f9fa;
}

/* 顏色自定義輸入框 */
.color-customization-input textarea {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.9em;
}

/* 確保 Gradio Tabs 樣式一致 */
.gradio-tabs .tab-nav button {
    padding: 10px 20px !important;
    font-weight: 500 !important;
    border-radius: 8px 8px 0 0 !important;
    transition: background-color 0.2s ease, color 0.2s ease !important;
}
.gradio-tabs .tab-nav button.selected {
    background-color: #667eea !important;
    color: white !important;
    border-bottom: none !important;
}

/* 調整 Slider 樣式 */
.gradio-slider label {
    margin-bottom: 5px !important;
}
.gradio-slider input[type="range"] {
    cursor: pointer !important;
}

/* 調整 Checkbox/Radio 樣式 */
.gradio-checkboxgroup label, .gradio-radio label {
    padding: 8px 0 !important;
}

/* 調整 Textbox/Textarea 樣式 */
.gradio-textbox textarea, .gradio-textbox input {
    border-radius: 6px !important;
    border: 1px solid #ced4da !important;
    padding: 10px !important;
}
.gradio-textbox textarea:focus, .gradio-textbox input:focus {
    border-color: #80bdff !important;
    box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25) !important;
}

/* 檔案上傳/下載區域 */
.gradio-file .hidden-upload, .gradio-file .download-button {
    border-radius: 6px !important;
}
.gradio-file .upload-button {
     border-radius: 6px !important;
     background: #6c757d !important; /* 上傳按鈕顏色 */
     color: white !important;
     padding: 8px 15px !important;
}
.gradio-file .upload-button:hover {
    background: #5a6268 !important;
}

"""

# =========================================
# == (第一部分結束) ==
# =========================================
# -*- coding: utf-8 -*-
"""
Gradio 應用程式:進階數據可視化工具
作者:Gemini
版本:1.0 (分段提供 - Part 2)
描述:此部分包含核心邏輯函數,如數據處理、圖表創建、導出等。
"""

# =========================================
# == 數據處理函數 (Data Processing Functions) ==
# =========================================

def process_upload(file):
    """
    處理上傳的文件 (CSV 或 Excel)。
    Args:
        file: Gradio 文件對象。
    Returns:
        tuple: (DataFrame 或 None, 狀態消息)。
    """
    if file is None:
        return None, "❌ 未上傳任何文件。"
    try:
        file_path = file.name
        file_type = file_path.split('.')[-1].lower()

        if file_type == 'csv':
            # 嘗試自動檢測編碼
            try:
                df = pd.read_csv(file_path, encoding='utf-8')
            except UnicodeDecodeError:
                try:
                    df = pd.read_csv(file_path, encoding='big5') # 嘗試台灣常用編碼
                except Exception as e:
                    return None, f"❌ 無法使用 UTF-8 或 Big5 解碼 CSV 文件: {e}"
            except Exception as e:
                 return None, f"❌ 讀取 CSV 文件時出錯: {e}"

        elif file_type in ['xls', 'xlsx']:
            try:
                df = pd.read_excel(file_path)
            except Exception as e:
                 return None, f"❌ 讀取 Excel 文件時出錯: {e}"
        else:
            return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"

        # 清理列名中的潛在空格
        df.columns = df.columns.str.strip()

        # **移除自動添加計數列**
        # df['計數'] = 1 # 不再自動添加

        return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"

    except Exception as e:
        print(f"處理上傳文件時發生未預期錯誤: {e}")
        traceback.print_exc()
        return None, f"❌ 處理文件時發生未預期錯誤: {e}"

def parse_data(text_data):
    """
    解析文本框中輸入的數據 (CSV 或空格分隔)。
    Args:
        text_data (str): 包含數據的字符串。
    Returns:
        tuple: (DataFrame 或 None, 狀態消息)。
    """
    if not text_data or not text_data.strip():
        return None, "❌ 未輸入任何數據。"
    try:
        # 使用 StringIO 模擬文件讀取
        data_io = io.StringIO(text_data.strip())
        first_line = data_io.readline().strip() # 讀取第一行以判斷分隔符
        data_io.seek(0) # 重置讀取位置

        # 判斷分隔符
        if ',' in first_line:
            separator = ','
        elif '\t' in first_line:
            separator = '\t'
        elif ' ' in first_line:
            # 如果包含空格,使用正則表達式匹配多個空格作為分隔符
            separator = r'\s+'
        else:
            # 如果只有一列或無法判斷,默認為逗號,讓 pandas 嘗試
            separator = ','

        try:
            df = pd.read_csv(data_io, sep=separator)
        except pd.errors.ParserError as pe:
             return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
        except Exception as e:
             return None, f"❌ 解析數據時出錯: {e}"

        # 清理列名中的潛在空格
        df.columns = df.columns.str.strip()

        # **移除自動添加計數列**
        # df['計數'] = 1 # 不再自動添加

        return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"

    except Exception as e:
        print(f"解析文本數據時發生未預期錯誤: {e}")
        traceback.print_exc()
        return None, f"❌ 解析數據時發生未預期錯誤: {e}"

def update_columns(df):
    """
    根據 DataFrame 更新 Gradio 下拉菜單的選項。
    Args:
        df (pd.DataFrame): 輸入的 DataFrame。
    Returns:
        tuple: 更新後的 Gradio Dropdown 組件 (x, y, group, size)。
    """
    default_choices = ["-- 無數據 --"]
    if df is None or df.empty:
        # 提供預設或空選項
        return (
            gr.Dropdown(choices=default_choices, value=default_choices[0], label="X軸 / 類別"),
            gr.Dropdown(choices=default_choices, value=default_choices[0], label="Y軸 / 數值"),
            gr.Dropdown(choices=["無"] + default_choices, value="無", label="分組列"),
            gr.Dropdown(choices=["無"] + default_choices, value="無", label="大小列")
        )

    columns = df.columns.tolist()
    # 嘗試猜測合適的預設值
    x_default = columns[0] if columns else None
    # 預設 Y 軸為第二列(如果存在),否則為第一列
    y_default = columns[1] if len(columns) > 1 else (columns[0] if columns else None)

    # 移除 '無' 選項中的 None 或空字符串
    valid_columns = [col for col in columns if col is not None and col != ""]
    group_choices = ["無"] + valid_columns
    size_choices = ["無"] + valid_columns

    # 更新下拉選單
    x_dropdown = gr.Dropdown(choices=valid_columns, value=x_default, label="X軸 / 類別")
    y_dropdown = gr.Dropdown(choices=valid_columns, value=y_default, label="Y軸 / 數值")
    group_dropdown = gr.Dropdown(choices=group_choices, value="無", label="分組列")
    size_dropdown = gr.Dropdown(choices=size_choices, value="無", label="大小列")

    return x_dropdown, y_dropdown, group_dropdown, size_dropdown

# =========================================
# == 圖表創建核心函數 (Core Plotting Function) ==
# =========================================
def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
                color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
                show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
    """
    根據用戶選擇創建 Plotly 圖表 (已加入 Null/空白 過濾)。

    Args:
        df (pd.DataFrame): 輸入數據。
        chart_type (str): 圖表類型。
        x_column (str): X軸或類別列。
        y_column (str): Y軸或數值列 (可能為 None)。
        group_column (str, optional): 分組列 (可能為 "無" 或 None)。 Defaults to None.
        size_column (str, optional): 大小列 (可能為 "無" 或 None)。 Defaults to None.
        color_scheme_name (str, optional): 顏色方案名稱。 Defaults to "預設 (Plotly)".
        patterns (list, optional): 圖案列表。 Defaults to [].
        title (str, optional): 圖表標題。 Defaults to "".
        width (int, optional): 圖表寬度。 Defaults to 800.
        height (int, optional): 圖表高度。 Defaults to 500.
        show_grid_str (str, optional): 是否顯示網格 ("是" 或 "否")。 Defaults to "是".
        show_legend_str (str, optional): 是否顯示圖例 ("是" 或 "否")。 Defaults to "是".
        agg_func_name (str, optional): 聚合函數名稱。 Defaults to "計數".
        custom_colors_dict (dict, optional): 自定義顏色映射。 Defaults to {}.

    Returns:
        go.Figure: Plotly 圖表對象。
    """
    # --- 添加調試信息 ---
    print("-" * 20, file=sys.stderr)
    print(f"調用 create_plot:", file=sys.stderr)
    print(f"  - df type: {type(df)}", file=sys.stderr)
    if isinstance(df, pd.DataFrame):
        print(f"  - df empty: {df.empty}", file=sys.stderr)
        print(f"  - df shape: {df.shape}", file=sys.stderr)
    print(f"  - chart_type: {chart_type}", file=sys.stderr)
    print(f"  - x_column: {x_column}", file=sys.stderr)
    print(f"  - y_column: {y_column}", file=sys.stderr)
    print(f"  - group_column: {group_column}", file=sys.stderr)
    print(f"  - size_column: {size_column}", file=sys.stderr)
    print(f"  - agg_func_name: {agg_func_name}", file=sys.stderr)
    print(f"  - show_grid_str: {show_grid_str}", file=sys.stderr)
    print(f"  - show_legend_str: {show_legend_str}", file=sys.stderr)
    print("-" * 20, file=sys.stderr)
    # --- 結束調試信息 ---

    fig = go.Figure()
    try:
        # --- 0. 將 "是"/"否" 轉換為布林值 ---
        show_grid = True if show_grid_str == "是" else False
        show_legend = True if show_legend_str == "是" else False

        # --- 1. 輸入驗證 (更嚴格) ---
        if df is None or not isinstance(df, pd.DataFrame) or df.empty:
            raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
        if not chart_type: raise ValueError("請選擇圖表類型。")
        if not agg_func_name: raise ValueError("請選擇聚合函數。")
        # NO_DATA_STR = "-- 無數據 --" # 確保此變數已定義或直接使用字符串
        if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")

        # 檢查列是否存在
        if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")

        # 判斷是否需要 Y 軸
        y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]

        if y_needed:
            if not y_column or y_column == NO_DATA_STR: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列。")
            if y_column not in df.columns: raise ValueError(f"Y 軸列 '{y_column}' 不在數據中。可用列: {', '.join(df.columns)}")
        else:
            y_column = None # 如果不需要 Y 軸,明確設為 None

        # 處理可選列 (從 Radio 傳來的值可能是 NONE_STR)
        # NONE_STR = "無" # 確保此變數已定義或直接使用字符串
        group_col = None if group_column == NONE_STR or not group_column else group_column
        size_col = None if size_column == NONE_STR or not size_column else size_column

        if group_col and group_col not in df.columns: raise ValueError(f"分組列 '{group_col}' 不在數據中。可用列: {', '.join(df.columns)}")
        if size_col and size_col not in df.columns: raise ValueError(f"大小列 '{size_col}' 不在數據中。可用列: {', '.join(df.columns)}")
        if group_col == x_column: raise ValueError("分組列不能與 X 軸列相同。")

        df_processed = df.copy()
        print(f"原始數據行數: {len(df_processed)}", file=sys.stderr)

        # --- NEW: 過濾 Null/空白值 ---
        columns_to_filter = [x_column]
        if y_needed and y_column: # Filter Y only if it's needed and selected
            columns_to_filter.append(y_column)
        if group_col:
            columns_to_filter.append(group_col)

        # 移除在關鍵列中有 Null (NaN, None) 值的行
        valid_columns_to_filter = [col for col in columns_to_filter if col in df_processed.columns]
        if valid_columns_to_filter:
             original_rows = len(df_processed)
             df_processed.dropna(subset=valid_columns_to_filter, inplace=True)
             print(f"移除 Null ({', '.join(valid_columns_to_filter)}) 後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
        else:
             print("警告: 沒有有效的列用於 Null 值過濾。", file=sys.stderr)

        # 對於 X 軸和分組列,額外移除空白字符串 (轉換為字符串後判斷)
        if x_column in df_processed.columns:
             try:
                original_rows = len(df_processed)
                # 僅移除完全是空白或空字符串的行
                df_processed = df_processed[df_processed[x_column].astype(str).str.strip() != '']
                print(f"移除 X 軸 '{x_column}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
             except Exception as e:
                 print(f"警告: 過濾 X 軸 '{x_column}' 空白字符串時出錯: {e}", file=sys.stderr)

        if group_col and group_col in df_processed.columns:
             try:
                original_rows = len(df_processed)
                df_processed = df_processed[df_processed[group_col].astype(str).str.strip() != '']
                print(f"移除分組列 '{group_col}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
             except Exception as e:
                 print(f"警告: 過濾分組列 '{group_col}' 空白字符串時出錯: {e}", file=sys.stderr)

        # 檢查過濾後是否還有數據
        if df_processed.empty:
            raise ValueError("過濾掉 Null 或空白值後,沒有剩餘數據可供繪圖。")
        # --- END NEW ---


        # --- 2. 數據類型轉換與準備 ---
        # 將 X 軸和分組列強制轉為字符串,以便正確分組
        df_processed[x_column] = df_processed[x_column].astype(str)
        if group_col:
            df_processed[group_col] = df_processed[group_col].astype(str)

        # 嘗試將 Y 軸和大小列轉為數值
        if y_column: # 這裡 y_column 可能為 None
            try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='coerce')
            except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column}' 為數值時出錯: {e}")
        if size_col:
            try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
            except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")


        # --- 3. 數據聚合 (如果需要) ---
        needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
        agg_df = None
        y_col_agg = y_column # 預設 Y 軸列名 (可能為 None)
        if needs_aggregation:
            grouping_cols = [x_column] + ([group_col] if group_col else [])
            # 檢查分組列是否有效 (已在驗證部分完成)

            if agg_func_name == "計數":
                # 使用 size() 計算每個組的行數
                agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size() # dropna=False 包含 NaN 類別 (已被過濾?)
                agg_df = agg_df.reset_index(name='__count__')
                y_col_agg = '__count__' # 使用新生成的計數列
            else:
                agg_func_pd = agg_function_map(agg_func_name)
                if not y_column: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列。")
                # 確保 Y 軸是數值類型 (除非 first/last)
                if agg_func_pd not in ['first', 'last'] and not pd.api.types.is_numeric_dtype(df_processed[y_column]):
                     # 嘗試再次轉換,如果失敗則報錯
                     try: df_processed[y_column] = pd.to_numeric(df_processed[y_column], errors='raise')
                     except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")

                try:
                    # 執行聚合
                    agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column].agg(agg_func_pd)
                    agg_df = agg_df.reset_index()
                    y_col_agg = y_column # 保持原始列名
                except Exception as agg_e:
                    raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
        else:
            # 不需要聚合,直接使用處理過的數據
            agg_df = df_processed
            y_col_agg = y_column # 保持原始列名 (可能為 None)

        # 再次檢查聚合後的 DataFrame
        if agg_df is None or agg_df.empty:
             raise ValueError("數據聚合後沒有產生有效結果。")
        # 確保繪圖所需的列存在於 agg_df 中
        required_cols_for_plot = [x_column]
        # 只有在 y_col_agg 有效時才加入檢查
        if y_col_agg: required_cols_for_plot.append(y_col_agg)
        if group_col: required_cols_for_plot.append(group_col)
        if size_col: required_cols_for_plot.append(size_col)
        missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
        if missing_cols:
             raise ValueError(f"聚合後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")


        # --- 4. 獲取顏色方案 ---
        colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)

        # --- 5. 創建圖表 (核心邏輯) ---
        fig_params = {"title": title, "color_discrete_sequence": colors, "width": width, "height": height} # 移除 data_frame
        if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
        # 確定實際用於繪圖的 Y 軸列名 (可能是 '__count__' 或原始 Y 列名)
        effective_y = y_col_agg # 使用聚合後確定的 Y 軸列名

        # --- (繪圖邏輯開始) ---
        if chart_type == "長條圖":
            if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
        elif chart_type == "堆疊長條圖":
            if not effective_y: raise ValueError("堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='stack', **fig_params)
        elif chart_type == "百分比堆疊長條圖":
             if not effective_y: raise ValueError("百分比堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
             fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='relative', text_auto='.1%', **fig_params)
             fig.update_layout(yaxis_title="百分比 (%)")
        elif chart_type == "群組長條圖":
            if not effective_y: raise ValueError("群組長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='group', **fig_params)
        elif chart_type == "水平長條圖":
            if not effective_y: raise ValueError("水平長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, y=x_column, x=effective_y, color=group_col, orientation='h', **fig_params)
        elif chart_type == "折線圖":
            if not effective_y: raise ValueError("折線圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
        elif chart_type == "多重折線圖":
            if not effective_y: raise ValueError("多重折線圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
        elif chart_type == "階梯折線圖":
            if not effective_y: raise ValueError("階梯折線圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, line_shape='hv', **fig_params)
        elif chart_type == "區域圖":
            if not effective_y: raise ValueError("區域圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
        elif chart_type == "堆疊區域圖":
            if not effective_y: raise ValueError("堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, groupnorm=None, **fig_params)
        elif chart_type == "百分比堆疊區域圖":
             if not effective_y: raise ValueError("百分比堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
             fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, groupnorm='percent', **fig_params)
             fig.update_layout(yaxis_title="百分比 (%)")
        elif chart_type == "圓餅圖":
            if not effective_y: raise ValueError("圓餅圖需要 Y 軸數值或 '計數' 聚合。")
            if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
            fig = px.pie(agg_df, names=x_column, values=effective_y, **fig_params)
            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])]))
        elif chart_type == "環形圖":
            if not effective_y: raise ValueError("環形圖需要 Y 軸數值或 '計數' 聚合。")
            if group_col: print("警告:環形圖不支持分組列,已忽略。")
            fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
            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])]))
        elif chart_type == "散點圖":
            # 散點圖不需要聚合,使用原始 y_column
            if not y_column: raise ValueError("散點圖需要選擇 Y 軸列。")
            fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, **fig_params)
        elif chart_type == "氣泡圖":
            if not y_column: raise ValueError("氣泡圖需要選擇 Y 軸列。")
            if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
            if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
            fig = px.scatter(agg_df, x=x_column, y=y_column, color=group_col, size=size_col, size_max=60, **fig_params)
        elif chart_type == "直方圖":
            # 直方圖使用原始數據的 x_column
            if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
            fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
        elif chart_type == "箱型圖":
            # 箱型圖使用原始 y_column
            if not y_column: raise ValueError("箱型圖需要選擇 Y 軸列。")
            if not pd.api.types.is_numeric_dtype(agg_df[y_column]): raise ValueError(f"箱型圖的 Y 軸列 '{y_column}' 必須是數值類型。")
            fig = px.box(agg_df, x=group_col, y=y_column, color=group_col, **fig_params)
            if not group_col: fig = px.box(agg_df, y=y_column, **fig_params)
        elif chart_type == "小提琴圖":
            if not y_column: raise ValueError("小提琴圖需要選擇 Y 軸列。")
            if not pd.api.types.is_numeric_dtype(agg_df[y_column]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_column}' 必須是數值類型。")
            fig = px.violin(agg_df, x=group_col, y=y_column, color=group_col, box=True, points="all", **fig_params)
            if not group_col: fig = px.violin(agg_df, y=y_column, box=True, points="all", **fig_params)
        elif chart_type == "熱力圖":
            if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
            if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
            try:
                if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"熱力圖的值列 '{effective_y}' 必須是數值類型。")
                pivot_df = pd.pivot_table(agg_df, values=effective_y, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
                fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params);
                fig.update_layout(coloraxis_showscale=True)
            except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
        elif chart_type == "樹狀圖":
             if not effective_y: raise ValueError("樹狀圖需要 Y 軸數值或 '計數' 聚合。")
             path = [group_col, x_column] if group_col else [x_column]
             if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"樹狀圖的值列 '{effective_y}' 必須是數值類型。")
             fig = px.treemap(agg_df, path=path, values=effective_y, color=group_col if group_col else x_column, **fig_params)
        elif chart_type == "雷達圖":
            if not effective_y: raise ValueError("雷達圖需要 Y 軸數值或 '計數' 聚合。")
            fig = go.Figure()
            if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"雷達圖的徑向值列 '{effective_y}' 必須是數值類型。")
            if not group_col:
                 theta = agg_df[x_column].tolist(); r = agg_df[effective_y].tolist(); theta.append(theta[0]); r.append(r[0])
                 fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=effective_y if effective_y != '__count__' else '計數', line_color=colors[0]))
            else:
                 categories = agg_df[group_col].unique()
                 for i, category in enumerate(categories):
                     subset = agg_df[agg_df[group_col] == category]; theta = subset[x_column].tolist(); r = subset[effective_y].tolist(); theta.append(theta[0]); r.append(r[0])
                     color = custom_colors_dict.get(str(category), colors[i % len(colors)])
                     fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
            fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
        elif chart_type == "漏斗圖":
            if not effective_y: raise ValueError("漏斗圖需要 Y 軸數值或 '計數' 聚合。")
            if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"漏斗圖的值列 '{effective_y}' 必須是數值類型。")
            sorted_df = agg_df.sort_values(by=effective_y, ascending=False)
            fig = px.funnel(sorted_df, x=effective_y, y=x_column, color=group_col, **fig_params)
        elif chart_type == "極座標圖":
            if not effective_y: raise ValueError("極座標圖需要 Y 軸數值或 '計數' 聚合。")
            if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
            fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
        elif chart_type == "甘特圖":
             start_col_gantt = y_column; end_col_gantt = group_col; task_col_gantt = x_column
             if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
             try:
                 df_gantt = df.copy() # 使用原始 df
                 if start_col_gantt not in df_gantt.columns: raise ValueError(f"開始列 '{start_col_gantt}' 不在數據中。")
                 if end_col_gantt not in df_gantt.columns: raise ValueError(f"結束列 '{end_col_gantt}' 不在數據中。")
                 if task_col_gantt not in df_gantt.columns: raise ValueError(f"任務列 '{task_col_gantt}' 不在數據中。")
                 df_gantt['_start_'] = pd.to_datetime(df_gantt[start_col_gantt], errors='coerce')
                 df_gantt['_end_'] = pd.to_datetime(df_gantt[end_col_gantt], errors='coerce')
                 if df_gantt['_start_'].isnull().any(): raise ValueError(f"開始列 '{start_col_gantt}' 包含無效或無法解析的日期時間格式。")
                 if df_gantt['_end_'].isnull().any(): raise ValueError(f"結束列 '{end_col_gantt}' 包含無效或無法解析的日期時間格式。")
                 fig = px.timeline(df_gantt, x_start='_start_', x_end='_end_', y=task_col_gantt, color=size_col if size_col else None, title=title, color_discrete_sequence=colors, width=width, height=height)
                 fig.update_layout(xaxis_type="date")
             except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
        else:
            print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
            if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
        # --- (繪圖邏輯結束) ---


        # --- 6. 應用圖案 (如果支持) ---
        if patterns:
            try:
                num_traces = len(fig.data)
                if chart_type in ["長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "圓餅圖", "環形圖"]:
                    for i, trace in enumerate(fig.data):
                        pattern_index = i % len(patterns)
                        if patterns[pattern_index] != "無": trace.marker.pattern.shape = patterns[pattern_index]; trace.marker.pattern.solidity = 0.4; trace.marker.pattern.fillmode = "replace"
                elif chart_type in ["散點圖", "氣泡圖"]:
                     symbol_map = {"/": "diamond", "\\": "square", "x": "x", "-": "line-ew", "|": "line-ns", "+": "cross", ".": "circle-dot"}
                     for i, trace in enumerate(fig.data):
                         pattern_index = i % len(patterns); symbol = symbol_map.get(patterns[pattern_index])
                         if symbol: trace.marker.symbol = symbol
                elif chart_type in ["折線圖", "多重折線圖", "階梯折線圖"]:
                     dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash", "|": "solid", "+": "solid", ".": "solid"}
                     for i, trace in enumerate(fig.data):
                         pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
                         if dash: trace.line.dash = dash
                elif chart_type in ["區域圖", "堆疊區域圖", "百分比堆疊區域圖"]:
                     print("提示:區域圖的圖案填充支持有限,將嘗試應用線型。")
                     dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
                     for i, trace in enumerate(fig.data):
                         pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
                         if dash: trace.line.dash = dash; trace.fill = 'tonexty' if hasattr(trace, 'stackgroup') and trace.stackgroup else 'tozeroy'
            except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")

        # --- 7. 更新佈局 ---
        fig.update_layout(
            showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
            template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
            font=dict(family="Inter, sans-serif", size=12),
            hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
            legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
        )
        # 根據圖表類型更新軸標籤
        final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
        if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
        elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
        elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
        elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column) # 甘特圖軸標籤
        else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)

    except ValueError as ve:
        print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
        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)
    except Exception as e:
        error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
        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)

    print("create_plot 函數執行完畢。", file=sys.stderr) # 調試信息
    return fig

# =========================================
# == 導出與下載函數 (Export & Download Functions) ==
# =========================================

def export_data(df, format_type):
    """
    將 DataFrame 導出為指定格式的文件。
    Args:
        df (pd.DataFrame): 要導出的 DataFrame。
        format_type (str): 導出格式 ("CSV", "Excel", "JSON")。
    Returns:
        tuple: (文件路徑或 None, 狀態消息)。 返回文件路徑供 Gradio 下載。
    """
    if df is None or df.empty:
        # 不能直接返回 None,需要返回一個空的 File output
        return None, "❌ 沒有數據可以導出。"

    try:
        if format_type == "CSV":
            filename = "exported_data.csv"
            df.to_csv(filename, index=False, encoding='utf-8-sig') # utf-8-sig 確保 Excel 正確讀取 BOM
        elif format_type == "Excel":
            filename = "exported_data.xlsx"
            df.to_excel(filename, index=False)
        elif format_type == "JSON":
            filename = "exported_data.json"
            df.to_json(filename, orient="records", indent=4, force_ascii=False) # indent 美化輸出, force_ascii=False 處理中文
        else:
            return None, f"❌ 不支持的導出格式: {format_type}"

        return filename, f"✅ 數據已成功準備為 {format_type} 格式,點擊下方鏈接下載。"

    except Exception as e:
        print(f"導出數據時出錯: {e}")
        traceback.print_exc()
        return None, f"❌ 導出數據時出錯: {e}"

def download_figure(fig, format_type="PNG"):
    """
    將 Plotly 圖表導出為圖像文件。
    Args:
        fig (go.Figure): Plotly 圖表對象。
        format_type (str): 導出格式 ("PNG", "SVG", "PDF", "JPEG")。 Defaults to "PNG".
    Returns:
        tuple: (文件路徑或 None, 狀態消息)。 返回文件路徑供 Gradio 下載。
    """
    if fig is None or not fig.data:
        return None, "❌ 沒有圖表可以導出。"

    try:
        format_lower = format_type.lower()
        filename = f"chart_export.{format_lower}"

        # 使用 fig.write_image 寫入文件
        fig.write_image(filename, format=format_lower)

        return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"

    except ValueError as ve:
         # kaleido 可能未安裝或配置錯誤
         if "kaleido" in str(ve):
             error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"
             print(error_msg)
             return None, error_msg
         else:
            print(f"導出圖表時出錯 (ValueError): {ve}")
            traceback.print_exc()
            return None, f"❌ 導出圖表時出錯: {ve}"
    except Exception as e:
        print(f"導出圖表時發生未預期錯誤: {e}")
        traceback.print_exc()
        return None, f"❌ 導出圖表時發生未預期錯誤: {e}"


# =========================================
# == 智能推薦函數 (Recommendation Function) ==
# =========================================
def recommend_chart_settings(df):
    """
    根據輸入的 DataFrame 智能推薦圖表設置。
    Args:
        df (pd.DataFrame): 輸入數據。
    Returns:
        dict: 包含推薦設置和消息的字典。
               Keys: chart_type, x_column, y_column, group_column, agg_function, message
    """
    recommendation = {
        "chart_type": None, "x_column": None, "y_column": None,
        "group_column": "無", "agg_function": None, "message": "無法提供推薦。"
    }

    if df is None or df.empty:
        recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"
        return recommendation

    columns = df.columns.tolist()
    num_cols = df.select_dtypes(include=np.number).columns.tolist()
    cat_cols = df.select_dtypes(include=['object', 'category', 'string']).columns.tolist()
    # 嘗試檢測日期時間列
    date_cols = [col for col in columns if pd.api.types.is_datetime64_any_dtype(df[col]) or ('日期' in col or '時間' in col)]

    # --- 推薦邏輯 ---
    try:
        # 優先處理時間序列
        if date_cols and num_cols:
            recommendation["chart_type"] = "折線圖"
            recommendation["x_column"] = date_cols[0]
            recommendation["y_column"] = num_cols[0]
            recommendation["agg_function"] = "平均值" # 或求和,取決於數據性質
            recommendation["message"] = f"檢測到時間列 '{date_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用折線圖顯示趨勢。"
        # 兩個數值列 -> 散點圖
        elif len(num_cols) >= 2:
            recommendation["chart_type"] = "散點圖"
            recommendation["x_column"] = num_cols[0]
            recommendation["y_column"] = num_cols[1]
            recommendation["agg_function"] = None # 散點圖通常不需要聚合
            recommendation["message"] = f"檢測到數值列 '{num_cols[0]}' 和 '{num_cols[1]}',推薦使用散點圖分析相關性。"
        # 一個類別列和一個數值列 -> 長條圖
        elif cat_cols and num_cols:
            recommendation["chart_type"] = "長條圖"
            recommendation["x_column"] = cat_cols[0]
            recommendation["y_column"] = num_cols[0]
            recommendation["agg_function"] = "平均值" # 或求和
            recommendation["message"] = f"檢測到類別列 '{cat_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用長條圖比較各類別的數值。"
        # 兩個或更多類別列 -> 堆疊/群組長條圖 (計數)
        elif len(cat_cols) >= 2:
            recommendation["chart_type"] = "堆疊長條圖" # 或群組長條圖
            recommendation["x_column"] = cat_cols[0]
            recommendation["y_column"] = None # Y軸由計數聚合產生
            recommendation["group_column"] = cat_cols[1]
            recommendation["agg_function"] = "計數"
            recommendation["message"] = f"檢測到多個類別列 ('{cat_cols[0]}', '{cat_cols[1]}', ...),推薦使用堆疊長條圖顯示計數分佈。"
        # 只有一個類別列 -> 長條圖 (計數)
        elif cat_cols:
            recommendation["chart_type"] = "長條圖"
            recommendation["x_column"] = cat_cols[0]
            recommendation["y_column"] = None # Y軸由計數聚合產生
            recommendation["agg_function"] = "計數"
            recommendation["message"] = f"檢測到類別列 '{cat_cols[0]}',推薦使用長條圖顯示其頻數分佈。"
        # 只有數值列 -> 直方圖
        elif num_cols:
            recommendation["chart_type"] = "直方圖"
            recommendation["x_column"] = num_cols[0]
            recommendation["y_column"] = None
            recommendation["agg_function"] = None # 直方圖自動計數
            recommendation["message"] = f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"
        else:
            recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"

    except Exception as e:
        recommendation["message"] = f"❌ 推薦時出錯: {e}"
        print(f"智能推薦時出錯: {e}")
        traceback.print_exc()

    # 確保推薦的列名是有效的
    if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
    if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
    if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"

    # 如果推薦了聚合但 Y 軸無效,則清空 Y 軸
    if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]:
        recommendation["agg_function"] = None # 無法聚合
        recommendation["message"] += " (無法確定聚合的數值列)"

    # 如果聚合是計數,Y 軸應為空
    if recommendation["agg_function"] == "計數":
        recommendation["y_column"] = None # 強制 Y 軸為空,因為值由計數產生

    return recommendation


# =========================================
# == (第二部分結束) ==
# =========================================
# -*- coding: utf-8 -*-
"""
Gradio 應用程式:進階數據可視化工具
作者:Gemini
版本:5.2 (完整修正版 - 清理格式, 確保一致性)
描述:包含所有功能的完整程式碼,修正導入、CSS、格式問題。
"""

# =========================================
# == 套件導入 (Import Libraries) ==
# =========================================
import gradio as gr
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import io
import base64
from PIL import Image
from plotly.subplots import make_subplots
import re
import json
import colorsys
import traceback # 用於更詳細的錯誤追蹤
import sys # 用於打印調試信息 - 已加入

# =========================================
# == 常數定義 (Constants) ==
# =========================================
CHART_TYPES = ["長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "折線圖", "多重折線圖", "階梯折線圖", "區域圖", "堆疊區域圖", "百分比堆疊區域圖", "圓餅圖", "環形圖", "散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "熱力圖", "樹狀圖", "雷達圖", "漏斗圖", "極座標圖", "甘特圖"]
COLOR_SCHEMES = {
    "預設 (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,
    "連續 - 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,
    "發散 - 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,
    "循環 - Twilight": px.colors.cyclical.Twilight, "循環 - IceFire": px.colors.cyclical.IceFire,
}
PATTERN_TYPES = ["無", "/", "\\", "x", "-", "|", "+", "."]
AGGREGATION_FUNCTIONS = ["計數", "求和", "平均值", "中位數", "最大值", "最小值", "標準差", "變異數", "第一筆", "最後一筆"]
EXPORT_FORMATS_DATA = ["CSV", "Excel", "JSON"]
EXPORT_FORMATS_IMG = ["PNG", "SVG", "PDF", "JPEG"]
YES_NO_CHOICES = ["是", "否"]
NO_DATA_STR = "-- 無數據 --"
NONE_STR = "無" # 代表 '無' 選項的值

# =========================================
# == 輔助函數 (Helper Functions) ==
# =========================================
# --- 顏色處理相關 ---
COLOR_CARD_STYLE = """<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">{color_cards}</div>"""
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>"""
COPY_SCRIPT = """
<script>
function copyToClipboard(text) {
    navigator.clipboard.writeText(text).then(() => {
        let notificationContainer = document.getElementById('clipboard-notification-container');
        if (!notificationContainer) {
            notificationContainer = document.createElement('div');
            notificationContainer.id = 'clipboard-notification-container';
            notificationContainer.style.position = 'fixed'; notificationContainer.style.bottom = '20px'; notificationContainer.style.right = '20px'; notificationContainer.style.zIndex = '10000'; notificationContainer.style.display = 'flex'; notificationContainer.style.flexDirection = 'column'; notificationContainer.style.alignItems = 'flex-end';
            document.body.appendChild(notificationContainer);
        }
        const notification = document.createElement('div');
        notification.textContent = '已複製: ' + text;
        notification.style.background = 'rgba(0, 0, 0, 0.7)'; notification.style.color = 'white'; notification.style.padding = '8px 15px'; notification.style.borderRadius = '4px'; notification.style.marginTop = '5px'; notification.style.fontSize = '14px'; notification.style.opacity = '1'; notification.style.transition = 'opacity 0.5s ease-out'; notification.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        notificationContainer.appendChild(notification);
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => { notification.remove(); }, 500);
        }, 1500);
    }).catch(err => { console.error('無法複製顏色代碼: ', err); });
}
</script>
"""
COMMON_COLORS = {
    "紅色": "#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",
}
def generate_gradient_colors(start_color, end_color, steps=10):
    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))
    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)
    try:
        start_rgb, end_rgb = hex_to_rgb(start_color), hex_to_rgb(end_color);
        if steps <= 1: return [start_color] if steps == 1 else []
        r_step, g_step, b_step = [(end_rgb[i] - start_rgb[i]) / (steps - 1) for i in range(3)]
        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)]
    except Exception as e: print(f"生成漸變色時出錯: {e}"); return [start_color, end_color]
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"]}
def generate_color_cards():
    common_cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=name, color_hex=hex_code) for name, hex_code in COMMON_COLORS.items()])
    gradient_cards_html = ""
    for name, colors in GRADIENTS.items():
        cards = "".join([COLOR_CARD_TEMPLATE.format(color_name=f"{name} {i+1}/{len(colors)}", color_hex=color) for i, color in enumerate(colors)])
        gradient_cards_html += f"""<div style="font-weight: bold; margin-top: 10px; font-size: 14px; color: #555;">{name}</div>{COLOR_CARD_STYLE.format(color_cards=cards)}"""
    return f"""<div style="font-weight: bold; margin-top: 10px; font-size: 14px; color: #555;">常用單色</div>{COLOR_CARD_STYLE.format(color_cards=common_cards)}{gradient_cards_html}{COPY_SCRIPT}"""

# --- 數據處理相關 ---
def agg_function_map(func_name):
    mapping = {"計數": "count", "求和": "sum", "平均值": "mean", "中位數": "median", "最大值": "max", "最小值": "min", "標準差": "std", "變異數": "var", "第一筆": "first", "最後一筆": "last"}
    return mapping.get(func_name, "count")
def parse_custom_colors(color_text):
    custom_colors = {}
    if color_text and isinstance(color_text, str) and color_text.strip():
        try:
            pairs = [p.strip() for p in color_text.split(',') if p.strip()]
            for pair in pairs:
                if ':' in pair:
                    key, value = pair.split(':', 1); key, value = key.strip(), value.strip()
                    if re.match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", value): custom_colors[key] = value
                    else: print(f"警告:忽略無效的顏色代碼 '{value}' for key '{key}'")
        except Exception as e: print(f"解析自定義顏色時出錯: {e}"); return {}
    return custom_colors
def update_patterns(*patterns_input):
    return [p for p in patterns_input if p in PATTERN_TYPES and p != "無"]

# =========================================
# == 數據處理函數 (Data Processing Functions) ==
# =========================================
def process_upload(file):
    if file is None: return None, "❌ 未上傳任何文件。"
    try:
        file_path = file.name; file_type = file_path.split('.')[-1].lower()
        if file_type == 'csv':
            try: df = pd.read_csv(file_path, encoding='utf-8')
            except UnicodeDecodeError:
                try: df = pd.read_csv(file_path, encoding='big5')
                except Exception as e: return None, f"❌ 無法使用 UTF-8 或 Big5 解碼 CSV 文件: {e}"
            except Exception as e: return None, f"❌ 讀取 CSV 文件時出錯: {e}"
        elif file_type in ['xls', 'xlsx']:
            try: df = pd.read_excel(file_path)
            except Exception as e: return None, f"❌ 讀取 Excel 文件時出錯: {e}"
        else: return None, f"❌ 不支持的文件類型: '{file_type}'。請上傳 CSV 或 Excel 文件。"
        df.columns = df.columns.str.strip()
        return df, f"✅ 成功載入 '{file_path.split('/')[-1]}',共 {len(df)} 行,{len(df.columns)} 列。"
    except Exception as e: print(f"處理上傳文件時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 處理文件時發生未預期錯誤: {e}"

def parse_data(text_data):
    if not text_data or not text_data.strip(): return None, "❌ 未輸入任何數據。"
    try:
        data_io = io.StringIO(text_data.strip()); first_line = data_io.readline().strip(); data_io.seek(0)
        if ',' in first_line: separator = ','
        elif '\t' in first_line: separator = '\t'
        elif ' ' in first_line: separator = r'\s+'
        else: separator = ','
        try: df = pd.read_csv(data_io, sep=separator, skipinitialspace=True)
        except pd.errors.ParserError as pe: return None, f"❌ 解析數據時出錯:可能是分隔符錯誤或數據格式問題。檢測到的分隔符: '{separator}'. 錯誤: {pe}"
        except Exception as e: return None, f"❌ 解析數據時出錯: {e}"
        df.columns = df.columns.str.strip()
        return df, f"✅ 成功解析數據,共 {len(df)} 行,{len(df.columns)} 列。"
    except Exception as e: print(f"解析文本數據時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 解析數據時發生未預期錯誤: {e}"

def update_columns_as_radio(df):
    """更新列選擇為 Radio 選項,並為 Y/Group/Size 軸添加 '無'"""
    no_data_choices = [NO_DATA_STR]
    no_data_choices_with_none = [NONE_STR, NO_DATA_STR]

    if df is None or df.empty:
        no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
        no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
        return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none

    try:
        columns = df.columns.tolist()
        valid_columns = [str(col) for col in columns if col is not None and str(col) != ""]
        if not valid_columns:
             no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
             no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
             return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none

        x_default = valid_columns[0]
        y_default = NONE_STR if len(valid_columns) <= 1 else valid_columns[1]

        y_choices = [NONE_STR] + valid_columns
        group_choices = [NONE_STR] + valid_columns
        size_choices = [NONE_STR] + valid_columns

        return (gr.Radio(choices=valid_columns, value=x_default, label="X軸 / 類別"),
                gr.Radio(choices=y_choices, value=y_default, label="Y軸 / 數值"),
                gr.Radio(choices=group_choices, value=NONE_STR, label="分組列"),
                gr.Radio(choices=size_choices, value=NONE_STR, label="大小列"))
    except Exception as e:
        print(f"更新列選項 (Radio) 時出錯: {e}")
        no_data_update = gr.Radio(choices=no_data_choices, value=no_data_choices[0])
        no_data_update_with_none = gr.Radio(choices=no_data_choices_with_none, value=NONE_STR)
        return no_data_update, no_data_update_with_none, no_data_update_with_none, no_data_update_with_none


# =========================================
# == 圖表創建核心函數 (Core Plotting Function) ==
# =========================================
def create_plot(df, chart_type, x_column, y_column, group_column=None, size_column=None,
                color_scheme_name="預設 (Plotly)", patterns=[], title="", width=800, height=500,
                show_grid_str="是", show_legend_str="是", agg_func_name="計數", custom_colors_dict={}):
    """
    根據用戶選擇創建 Plotly 圖表 (已加入 Null/空白 過濾)。 V5.2 版
    """
    # --- 添加調試信息 ---
    print("-" * 20, file=sys.stderr)
    print(f"調用 create_plot:", file=sys.stderr)
    print(f"  - df type: {type(df)}", file=sys.stderr)
    if isinstance(df, pd.DataFrame):
        print(f"  - df empty: {df.empty}", file=sys.stderr)
        print(f"  - df shape: {df.shape}", file=sys.stderr)
    print(f"  - chart_type: {chart_type}", file=sys.stderr)
    print(f"  - x_column: {x_column}", file=sys.stderr)
    print(f"  - y_column: {y_column}", file=sys.stderr)
    print(f"  - group_column: {group_column}", file=sys.stderr)
    print(f"  - size_column: {size_column}", file=sys.stderr)
    print(f"  - agg_func_name: {agg_func_name}", file=sys.stderr)
    print(f"  - show_grid_str: {show_grid_str}", file=sys.stderr)
    print(f"  - show_legend_str: {show_legend_str}", file=sys.stderr)
    print("-" * 20, file=sys.stderr)
    # --- 結束調試信息 ---

    fig = go.Figure()
    try:
        # --- 0. 將 "是"/"否" 轉換為布林值 ---
        show_grid = True if show_grid_str == "是" else False
        show_legend = True if show_legend_str == "是" else False

        # --- 1. 輸入驗證 (更嚴格) ---
        if df is None or not isinstance(df, pd.DataFrame) or df.empty:
            raise ValueError("沒有有效的 DataFrame 數據可供繪圖。請先載入數據。")
        if not chart_type: raise ValueError("請選擇圖表類型。")
        if not agg_func_name: raise ValueError("請選擇聚合函數。")
        if not x_column or x_column == NO_DATA_STR: raise ValueError("請選擇有效的 X 軸或類別列。")

        # 檢查列是否存在
        if x_column not in df.columns: raise ValueError(f"X 軸列 '{x_column}' 不在數據中。可用列: {', '.join(df.columns)}")

        # 判斷是否需要 Y 軸 (修正 V5.1 錯誤: y_column 可能來自 Radio 且值為 NONE_STR)
        y_column_selected = None if y_column == NONE_STR or y_column == NO_DATA_STR or not y_column else y_column
        y_needed = agg_func_name != "計數" and chart_type not in ["直方圖"]

        if y_needed:
            if not y_column_selected: raise ValueError("此圖表類型和聚合函數需要選擇有效的 Y 軸或數值列 (不能選 '無')。")
            if y_column_selected not in df.columns: raise ValueError(f"Y 軸列 '{y_column_selected}' 不在數據中。可用列: {', '.join(df.columns)}")
        # else:
            # y_column_selected 保持為 None

        # 處理可選列 (從 Radio 傳來的值可能是 NONE_STR)
        group_col = None if group_column == NONE_STR or not group_column else group_column
        size_col = None if size_column == NONE_STR or not size_column else size_column

        if group_col and group_col not in df.columns: raise ValueError(f"分組列 '{group_col}' 不在數據中。可用列: {', '.join(df.columns)}")
        if size_col and size_col not in df.columns: raise ValueError(f"大小列 '{size_col}' 不在數據中。可用列: {', '.join(df.columns)}")
        if group_col == x_column: raise ValueError("分組列不能與 X 軸列相同。")

        df_processed = df.copy()
        print(f"原始數據行數: {len(df_processed)}", file=sys.stderr)

        # --- NEW: 過濾 Null/空白值 ---
        columns_to_filter = [x_column]
        if y_needed and y_column_selected: # Filter Y only if it's needed and selected
            columns_to_filter.append(y_column_selected)
        if group_col:
            columns_to_filter.append(group_col)

        valid_columns_to_filter = [col for col in columns_to_filter if col in df_processed.columns]
        if valid_columns_to_filter:
             original_rows = len(df_processed)
             df_processed.dropna(subset=valid_columns_to_filter, inplace=True)
             print(f"移除 Null ({', '.join(valid_columns_to_filter)}) 後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
        else:
             print("警告: 沒有有效的列用於 Null 值過濾。", file=sys.stderr)

        if x_column in df_processed.columns:
             try:
                original_rows = len(df_processed)
                df_processed = df_processed[df_processed[x_column].astype(str).str.strip() != '']
                print(f"移除 X 軸 '{x_column}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
             except Exception as e:
                 print(f"警告: 過濾 X 軸 '{x_column}' 空白字符串時出錯: {e}", file=sys.stderr)

        if group_col and group_col in df_processed.columns:
             try:
                original_rows = len(df_processed)
                df_processed = df_processed[df_processed[group_col].astype(str).str.strip() != '']
                print(f"移除分組列 '{group_col}' 空白字符串後行數: {len(df_processed)} (減少 {original_rows - len(df_processed)} 行)", file=sys.stderr)
             except Exception as e:
                 print(f"警告: 過濾分組列 '{group_col}' 空白字符串時出錯: {e}", file=sys.stderr)

        if df_processed.empty:
            raise ValueError("過濾掉 Null 或空白值後,沒有剩餘數據可供繪圖。")
        # --- END NEW ---


        # --- 2. 數據類型轉換與準備 ---
        df_processed[x_column] = df_processed[x_column].astype(str)
        if group_col:
            df_processed[group_col] = df_processed[group_col].astype(str)

        # 僅在需要時轉換 Y 軸和大小列
        if y_column_selected:
            try: df_processed[y_column_selected] = pd.to_numeric(df_processed[y_column_selected], errors='coerce')
            except Exception as e: print(f"警告:轉換 Y 軸列 '{y_column_selected}' 為數值時出錯: {e}")
        if size_col:
            try: df_processed[size_col] = pd.to_numeric(df_processed[size_col], errors='coerce')
            except Exception as e: print(f"警告:轉換大小列 '{size_col}' 為數值時出錯: {e}")


        # --- 3. 數據聚合 (如果需要) ---
        needs_aggregation = chart_type not in ["散點圖", "氣泡圖", "直方圖", "箱型圖", "小提琴圖", "甘特圖"]
        agg_df = None
        y_col_agg = y_column_selected # 使用處理過的 Y 軸列名 (可能為 None)
        if needs_aggregation:
            grouping_cols = [x_column] + ([group_col] if group_col else [])

            if agg_func_name == "計數":
                agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False).size().reset_index(name='__count__')
                y_col_agg = '__count__'
            else:
                agg_func_pd = agg_function_map(agg_func_name)
                if not y_column_selected: raise ValueError(f"聚合函數 '{agg_func_name}' 需要一個有效的 Y 軸數值列 (不能選 '無')。")
                # 確保 Y 軸是數值類型 (除非 first/last)
                if agg_func_pd not in ['first', 'last']:
                    if not pd.api.types.is_numeric_dtype(df_processed[y_column_selected]):
                        try: df_processed[y_column_selected] = pd.to_numeric(df_processed[y_column_selected], errors='raise')
                        except (ValueError, TypeError): raise ValueError(f"Y 軸列 '{y_column_selected}' 必須是數值類型才能執行聚合 '{agg_func_name}'。")
                    # 檢查轉換後是否有非 NaN 值
                    if df_processed[y_column_selected].isnull().all():
                         raise ValueError(f"Y 軸列 '{y_column_selected}' 在轉換為數值後全為無效值 (NaN),無法執行聚合 '{agg_func_name}'。")

                try:
                    # 執行聚合
                    agg_df = df_processed.groupby(grouping_cols, observed=False, dropna=False)[y_column_selected].agg(agg_func_pd)
                    agg_df = agg_df.reset_index()
                    y_col_agg = y_column_selected # 保持原始列名
                except Exception as agg_e:
                    raise ValueError(f"執行聚合 '{agg_func_name}' 時出錯: {agg_e}")
        else:
            agg_df = df_processed
            y_col_agg = y_column_selected # 保持處理過的 Y 軸列名 (可能為 None)

        if agg_df is None or agg_df.empty:
             raise ValueError("數據聚合或處理後沒有產生有效結果。")

        # 確保繪圖所需的列存在於 agg_df 中
        required_cols_for_plot = [x_column]
        # 修正:只有在 y_col_agg 實際有值(不是 None)時才加入檢查
        if y_col_agg is not None: required_cols_for_plot.append(y_col_agg)
        if group_col: required_cols_for_plot.append(group_col)
        if size_col: required_cols_for_plot.append(size_col)
        missing_cols = [col for col in required_cols_for_plot if col not in agg_df.columns]
        if missing_cols:
             raise ValueError(f"處理後的數據缺少繪圖所需的列: {', '.join(missing_cols)}")


        # --- 4. 獲取顏色方案 ---
        colors = COLOR_SCHEMES.get(color_scheme_name, px.colors.qualitative.Plotly)

        # --- 5. 創建圖表 (核心邏輯) ---
        fig_params = {"title": title, "color_discrete_sequence": colors, "width": width, "height": height}
        if group_col and custom_colors_dict: fig_params["color_discrete_map"] = custom_colors_dict
        effective_y = y_col_agg # 使用聚合後或處理過的 Y 軸列名

        # --- (繪圖邏輯開始) ---
        # 修正:繪圖時使用 y_column_selected (處理過的 Y 軸) 而不是原始 y_column
        # 修正:甘特圖需要原始 df,而不是 df_processed 或 agg_df
        if chart_type == "長條圖":
            if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
        elif chart_type == "堆疊長條圖":
            if not effective_y: raise ValueError("堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='stack', **fig_params)
        elif chart_type == "百分比堆疊長條圖":
             if not effective_y: raise ValueError("百分比堆疊長條圖需要 Y 軸數值或 '計數' 聚合。")
             fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='relative', text_auto='.1%', **fig_params)
             fig.update_layout(yaxis_title="百分比 (%)")
        elif chart_type == "群組長條圖":
            if not effective_y: raise ValueError("群組長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, barmode='group', **fig_params)
        elif chart_type == "水平長條圖":
            if not effective_y: raise ValueError("水平長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, y=x_column, x=effective_y, color=group_col, orientation='h', **fig_params)
        elif chart_type == "折線圖":
            if not effective_y: raise ValueError("折線圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
        elif chart_type == "多重折線圖":
            if not effective_y: raise ValueError("多重折線圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, markers=True, **fig_params)
        elif chart_type == "階梯折線圖":
            if not effective_y: raise ValueError("階梯折線圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.line(agg_df, x=x_column, y=effective_y, color=group_col, line_shape='hv', **fig_params)
        elif chart_type == "區域圖":
            if not effective_y: raise ValueError("區域圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
        elif chart_type == "堆疊區域圖":
            if not effective_y: raise ValueError("堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, groupnorm=None, **fig_params)
        elif chart_type == "百分比堆疊區域圖":
             if not effective_y: raise ValueError("百分比堆疊區域圖需要 Y 軸數值或 '計數' 聚合。")
             fig = px.area(agg_df, x=x_column, y=effective_y, color=group_col, groupnorm='percent', **fig_params)
             fig.update_layout(yaxis_title="百分比 (%)")
        elif chart_type == "圓餅圖":
            if not effective_y: raise ValueError("圓餅圖需要 Y 軸數值或 '計數' 聚合。")
            if group_col: print("警告:圓餅圖不支持分組列,已忽略。")
            fig = px.pie(agg_df, names=x_column, values=effective_y, **fig_params)
            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])]))
        elif chart_type == "環形圖":
            if not effective_y: raise ValueError("環形圖需要 Y 軸數值或 '計數' 聚合。")
            if group_col: print("警告:環形圖不支持分組列,已忽略。")
            fig = px.pie(agg_df, names=x_column, values=effective_y, hole=0.4, **fig_params)
            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])]))
        elif chart_type == "散點圖":
            if not y_column_selected: raise ValueError("散點圖需要選擇 Y 軸列。")
            fig = px.scatter(agg_df, x=x_column, y=y_column_selected, color=group_col, size=size_col, **fig_params)
        elif chart_type == "氣泡圖":
            if not y_column_selected: raise ValueError("氣泡圖需要選擇 Y 軸列。")
            if not size_col: raise ValueError("氣泡圖需要指定 '大小列'。")
            if not pd.api.types.is_numeric_dtype(agg_df[size_col]): raise ValueError(f"大小列 '{size_col}' 必須是數值類型。")
            fig = px.scatter(agg_df, x=x_column, y=y_column_selected, color=group_col, size=size_col, size_max=60, **fig_params)
        elif chart_type == "直方圖":
            if not pd.api.types.is_numeric_dtype(agg_df[x_column]): raise ValueError(f"直方圖的 X 軸列 '{x_column}' 必須是數值類型。")
            fig = px.histogram(agg_df, x=x_column, color=group_col, **fig_params); fig.update_layout(yaxis_title="計數")
        elif chart_type == "箱型圖":
            if not y_column_selected: raise ValueError("箱型圖需要選擇 Y 軸列。")
            if not pd.api.types.is_numeric_dtype(agg_df[y_column_selected]): raise ValueError(f"箱型圖的 Y 軸列 '{y_column_selected}' 必須是數值類型。")
            fig = px.box(agg_df, x=group_col, y=y_column_selected, color=group_col, **fig_params)
            if not group_col: fig = px.box(agg_df, y=y_column_selected, **fig_params)
        elif chart_type == "小提琴圖":
            if not y_column_selected: raise ValueError("小提琴圖需要選擇 Y 軸列。")
            if not pd.api.types.is_numeric_dtype(agg_df[y_column_selected]): raise ValueError(f"小提琴圖的 Y 軸列 '{y_column_selected}' 必須是數值類型。")
            fig = px.violin(agg_df, x=group_col, y=y_column_selected, color=group_col, box=True, points="all", **fig_params)
            if not group_col: fig = px.violin(agg_df, y=y_column_selected, box=True, points="all", **fig_params)
        elif chart_type == "熱力圖":
            if not effective_y: raise ValueError("熱力圖需要 Y 軸數值或 '計數' 聚合。")
            if not group_col: raise ValueError("熱力圖需要 X 軸、Y 軸 和一個 分組列。")
            try:
                if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"熱力圖的值列 '{effective_y}' 必須是數值類型。")
                pivot_df = pd.pivot_table(agg_df, values=effective_y, index=group_col, columns=x_column, aggfunc=agg_function_map(agg_func_name) if agg_func_name != "計數" else 'size')
                fig = px.imshow(pivot_df, color_continuous_scale=px.colors.sequential.Viridis, aspect="auto", text_auto=True, **fig_params);
                fig.update_layout(coloraxis_showscale=True)
            except Exception as pivot_e: raise ValueError(f"創建熱力圖的數據透視表時出錯: {pivot_e}")
        elif chart_type == "樹狀圖":
             if not effective_y: raise ValueError("樹狀圖需要 Y 軸數值或 '計數' 聚合。")
             path = [group_col, x_column] if group_col else [x_column]
             if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"樹狀圖的值列 '{effective_y}' 必須是數值類型。")
             fig = px.treemap(agg_df, path=path, values=effective_y, color=group_col if group_col else x_column, **fig_params)
        elif chart_type == "雷達圖":
            if not effective_y: raise ValueError("雷達圖需要 Y 軸數值或 '計數' 聚合。")
            fig = go.Figure()
            if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"雷達圖的徑向值列 '{effective_y}' 必須是數值類型。")
            if not group_col:
                 theta = agg_df[x_column].tolist(); r = agg_df[effective_y].tolist(); theta.append(theta[0]); r.append(r[0])
                 fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=effective_y if effective_y != '__count__' else '計數', line_color=colors[0]))
            else:
                 categories = agg_df[group_col].unique()
                 for i, category in enumerate(categories):
                     subset = agg_df[agg_df[group_col] == category]; theta = subset[x_column].tolist(); r = subset[effective_y].tolist(); theta.append(theta[0]); r.append(r[0])
                     color = custom_colors_dict.get(str(category), colors[i % len(colors)])
                     fig.add_trace(go.Scatterpolar(r=r, theta=theta, fill='toself', name=str(category), line_color=color))
            fig.update_layout(polar=dict(radialaxis=dict(visible=True)), showlegend=show_legend, title=title, width=width, height=height)
        elif chart_type == "漏斗圖":
            if not effective_y: raise ValueError("漏斗圖需要 Y 軸數值或 '計數' 聚合。")
            if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"漏斗圖的值列 '{effective_y}' 必須是數值類型。")
            sorted_df = agg_df.sort_values(by=effective_y, ascending=False)
            fig = px.funnel(sorted_df, x=effective_y, y=x_column, color=group_col, **fig_params)
        elif chart_type == "極座標圖":
            if not effective_y: raise ValueError("極座標圖需要 Y 軸數值或 '計數' 聚合。")
            if not pd.api.types.is_numeric_dtype(agg_df[effective_y]): raise ValueError(f"極座標圖的徑向值列 '{effective_y}' 必須是數值類型。")
            fig = px.bar_polar(agg_df, r=effective_y, theta=x_column, color=group_col if group_col else x_column, **fig_params)
        elif chart_type == "甘特圖":
             start_col_gantt = y_column_selected; end_col_gantt = group_col; task_col_gantt = x_column
             if not start_col_gantt or not end_col_gantt: raise ValueError("甘特圖需要指定 開始列 (Y軸) 和 結束列 (分組列)。")
             try:
                 df_gantt = df.copy() # 使用原始 df
                 if start_col_gantt not in df_gantt.columns: raise ValueError(f"開始列 '{start_col_gantt}' 不在數據中。")
                 if end_col_gantt not in df_gantt.columns: raise ValueError(f"結束列 '{end_col_gantt}' 不在數據中。")
                 if task_col_gantt not in df_gantt.columns: raise ValueError(f"任務列 '{task_col_gantt}' 不在數據中。")
                 df_gantt['_start_'] = pd.to_datetime(df_gantt[start_col_gantt], errors='coerce')
                 df_gantt['_end_'] = pd.to_datetime(df_gantt[end_col_gantt], errors='coerce')
                 if df_gantt['_start_'].isnull().any(): raise ValueError(f"開始列 '{start_col_gantt}' 包含無效或無法解析的日期時間格式。")
                 if df_gantt['_end_'].isnull().any(): raise ValueError(f"結束列 '{end_col_gantt}' 包含無效或無法解析的日期時間格式。")
                 fig = px.timeline(df_gantt, x_start='_start_', x_end='_end_', y=task_col_gantt, color=size_col if size_col else None, title=title, color_discrete_sequence=colors, width=width, height=height)
                 fig.update_layout(xaxis_type="date")
             except Exception as gantt_e: raise ValueError(f"創建甘特圖時出錯: {gantt_e}")
        else:
            print(f"警告:未知的圖表類型 '{chart_type}',使用長條圖代替。")
            if not effective_y: raise ValueError("長條圖需要 Y 軸數值或 '計數' 聚合。")
            fig = px.bar(agg_df, x=x_column, y=effective_y, color=group_col, **fig_params)
        # --- (繪圖邏輯結束) ---


        # --- 6. 應用圖案 (如果支持) ---
        if patterns:
            try:
                num_traces = len(fig.data)
                if chart_type in ["長條圖", "堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "圓餅圖", "環形圖"]:
                    for i, trace in enumerate(fig.data):
                        pattern_index = i % len(patterns)
                        if patterns[pattern_index] != "無": trace.marker.pattern.shape = patterns[pattern_index]; trace.marker.pattern.solidity = 0.4; trace.marker.pattern.fillmode = "replace"
                elif chart_type in ["散點圖", "氣泡圖"]:
                     symbol_map = {"/": "diamond", "\\": "square", "x": "x", "-": "line-ew", "|": "line-ns", "+": "cross", ".": "circle-dot"}
                     for i, trace in enumerate(fig.data):
                         pattern_index = i % len(patterns); symbol = symbol_map.get(patterns[pattern_index])
                         if symbol: trace.marker.symbol = symbol
                elif chart_type in ["折線圖", "多重折線圖", "階梯折線圖"]:
                     dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash", "|": "solid", "+": "solid", ".": "solid"}
                     for i, trace in enumerate(fig.data):
                         pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
                         if dash: trace.line.dash = dash
                elif chart_type in ["區域圖", "堆疊區域圖", "百分比堆疊區域圖"]:
                     print("提示:區域圖的圖案填充支持有限,將嘗試應用線型。")
                     dash_map = {"/": "dash", "\\": "dot", "x": "dashdot", "-": "longdash"}
                     for i, trace in enumerate(fig.data):
                         pattern_index = i % len(patterns); dash = dash_map.get(patterns[pattern_index])
                         if dash: trace.line.dash = dash; trace.fill = 'tonexty' if hasattr(trace, 'stackgroup') and trace.stackgroup else 'tozeroy'
            except Exception as pattern_e: print(f"應用圖案時出錯: {pattern_e}")

        # --- 7. 更新佈局 ---
        fig.update_layout(
            showlegend=show_legend, xaxis=dict(showgrid=show_grid), yaxis=dict(showgrid=show_grid),
            template="plotly_white", margin=dict(l=60, r=40, t=80 if title else 40, b=60),
            font=dict(family="Inter, sans-serif", size=12),
            hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter, sans-serif"),
            legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) if show_legend else None,
        )
        # 根據圖表類型更新軸標籤
        final_y_label = y_col_agg if y_col_agg != '__count__' else '計數'
        if chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]: fig.update_layout(xaxis_title=None, yaxis_title=None)
        elif chart_type == "水平長條圖": fig.update_layout(xaxis_title=final_y_label, yaxis_title=x_column)
        elif chart_type == "直方圖": fig.update_layout(xaxis_title=x_column, yaxis_title='計數')
        elif chart_type == "甘特圖": fig.update_layout(xaxis_title="時間", yaxis_title=x_column) # 使用 x_column 作為任務軸標籤
        else: fig.update_layout(xaxis_title=x_column, yaxis_title=final_y_label)

    except ValueError as ve:
        print(f"圖表創建錯誤 (ValueError): {ve}", file=sys.stderr); traceback.print_exc(file=sys.stderr)
        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)
    except Exception as e:
        error_message = f"❌ 創建圖表時發生未預期錯誤:\n{traceback.format_exc()}"; print(error_message, file=sys.stderr)
        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)

    print("create_plot 函數執行完畢。", file=sys.stderr) # 調試信息
    return fig


# =========================================
# == 導出與下載函數 (Export & Download Functions) ==
# =========================================
# (與 V4 相同)
def export_data(df, format_type):
    if df is None or df.empty: return None, "❌ 沒有數據可以導出。"
    try:
        if format_type == "CSV": filename = "exported_data.csv"; df.to_csv(filename, index=False, encoding='utf-8-sig')
        elif format_type == "Excel": filename = "exported_data.xlsx"; df.to_excel(filename, index=False)
        elif format_type == "JSON": filename = "exported_data.json"; df.to_json(filename, orient="records", indent=4, force_ascii=False)
        else: return None, f"❌ 不支持的導出格式: {format_type}"
        return filename, f"✅ 數據已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
    except Exception as e: print(f"導出數據時出錯: {e}"); traceback.print_exc(); return None, f"❌ 導出數據時出錯: {e}"

def download_figure(fig, format_type="PNG"):
    if fig is None or not fig.data: return None, "❌ 沒有圖表可以導出。"
    try:
        format_lower = format_type.lower(); filename = f"chart_export.{format_lower}"
        import kaleido # 確保導入
        fig.write_image(filename, format=format_lower)
        return filename, f"✅ 圖表已成功準備為 {format_type} 格式,點擊下方鏈接下載。"
    except ImportError: error_msg = "❌ 導出圖表失敗:需要 Kaleido 套件。請在環境中安裝 `pip install -U kaleido`。"; print(error_msg); return None, error_msg
    except ValueError as ve:
        if "kaleido" in str(ve).lower(): error_msg = "❌ 導出圖表失敗:Kaleido 套件無法運行。請檢查其依賴項或嘗試重新安裝。"; print(f"{error_msg}\n{ve}"); traceback.print_exc(); return None, error_msg
        else: print(f"導出圖表時出錯 (ValueError): {ve}"); traceback.print_exc(); return None, f"❌ 導出圖表時出錯: {ve}"
    except Exception as e: print(f"導出圖表時發生未預期錯誤: {e}"); traceback.print_exc(); return None, f"❌ 導出圖表時發生未預期錯誤: {e}"

# =========================================
# == 智能推薦函數 (Recommendation Function) ==
# =========================================
# (與 V4 相同)
def recommend_chart_settings(df):
    recommendation = {"chart_type": None, "x_column": None, "y_column": None, "group_column": "無", "agg_function": None, "message": "無法提供推薦。"}
    if df is None or df.empty: recommendation["message"] = "ℹ️ 請先上傳或輸入數據。"; return recommendation
    columns = df.columns.tolist(); num_cols = df.select_dtypes(include=np.number).columns.tolist(); cat_cols = df.select_dtypes(include=['object', 'category', 'string']).columns.tolist()
    date_cols = [col for col in columns if pd.api.types.is_datetime64_any_dtype(df[col]) or ('日期' in col or '時間' in col)]
    try:
        if date_cols and num_cols: recommendation.update({"chart_type": "折線圖", "x_column": date_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到時間列 '{date_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用折線圖顯示趨勢。"})
        elif len(num_cols) >= 2: recommendation.update({"chart_type": "散點圖", "x_column": num_cols[0], "y_column": num_cols[1], "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}' 和 '{num_cols[1]}',推薦使用散點圖分析相關性。"})
        elif cat_cols and num_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": num_cols[0], "agg_function": "平均值", "message": f"檢測到類別列 '{cat_cols[0]}' 和數值列 '{num_cols[0]}',推薦使用長條圖比較各類別的數值。"})
        elif len(cat_cols) >= 2: recommendation.update({"chart_type": "堆疊長條圖", "x_column": cat_cols[0], "y_column": None, "group_column": cat_cols[1], "agg_function": "計數", "message": f"檢測到多個類別列 ('{cat_cols[0]}', '{cat_cols[1]}', ...),推薦使用堆疊長條圖顯示計數分佈。"})
        elif cat_cols: recommendation.update({"chart_type": "長條圖", "x_column": cat_cols[0], "y_column": None, "agg_function": "計數", "message": f"檢測到類別列 '{cat_cols[0]}',推薦使用長條圖顯示其頻數分佈。"})
        elif num_cols: recommendation.update({"chart_type": "直方圖", "x_column": num_cols[0], "y_column": None, "agg_function": None, "message": f"檢測到數值列 '{num_cols[0]}',推薦使用直方圖查看其分佈。"})
        else: recommendation["message"] = "無法根據當前數據結構提供明確的圖表推薦。"
    except Exception as e: recommendation["message"] = f"❌ 推薦時出錯: {e}"; print(f"智能推薦時出錯: {e}"); traceback.print_exc()
    if recommendation["x_column"] and recommendation["x_column"] not in columns: recommendation["x_column"] = None
    if recommendation["y_column"] and recommendation["y_column"] not in columns: recommendation["y_column"] = None
    if recommendation["group_column"] != "無" and recommendation["group_column"] not in columns: recommendation["group_column"] = "無"
    if recommendation["agg_function"] and recommendation["agg_function"] != "計數" and not recommendation["y_column"]: recommendation["agg_function"] = None; recommendation["message"] += " (無法確定聚合的數值列)"
    if recommendation["agg_function"] == "計數": recommendation["y_column"] = None
    return recommendation

# =========================================
# == CSS 樣式 (CSS Styling) ==
# =========================================
# (移除 Dropdown CSS)
CUSTOM_CSS = """
/* --- 全局和容器 --- */
.gradio-container { font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
/* --- 應用程式標頭 --- */
.app-header { text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 25px 20px; border-radius: 12px; color: white; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); }
.app-title { font-size: 2.2em; font-weight: 700; margin: 0; letter-spacing: 1px; text-shadow: 1px 1px 3px rgba(0,0,0,0.2); }
.app-subtitle { font-size: 1.1em; color: #e0e0e0; margin-top: 8px; font-weight: 300; }
/* --- 區塊標題 --- */
.section-title { font-size: 1.4em; font-weight: 600; color: #343a40; border-bottom: 3px solid #7367f0; padding-bottom: 8px; margin-top: 25px; margin-bottom: 20px; }
/* --- 卡片樣式 --- */
.card { background-color: white; border-radius: 10px; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); margin-bottom: 20px; transition: transform 0.25s ease-out, box-shadow 0.25s ease-out; border: 1px solid #e0e0e0; }
.card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); }
/* --- 按鈕樣式 --- */
.primary-button { background: linear-gradient(to right, #667eea, #764ba2) !important; border: none !important; color: white !important; font-weight: 600 !important; padding: 10px 20px !important; border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
.primary-button:hover { background: linear-gradient(to right, #764ba2, #667eea) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
.secondary-button { background: linear-gradient(to right, #89f7fe, #66a6ff) !important; border: none !important; color: #333 !important; font-weight: 600 !important; padding: 8px 16px !important; border-radius: 8px !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
.secondary-button:hover { background: linear-gradient(to right, #66a6ff, #89f7fe) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; }
/* --- 下拉選單修正 (Dropdown Fix) --- */
/* 移除自定義下拉選單樣式,使用 Gradio 預設 */
/* --- 其他 UI 元素 --- */
.tips-box { background-color: #e7f3ff; border-left: 5px solid #66a6ff; padding: 15px 20px; border-radius: 8px; margin: 20px 0; font-size: 0.95em; color: #333; }
.tips-box code { background-color: #d1e7fd; padding: 2px 5px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; }
.chart-previewer { border: 2px dashed #ced4da; border-radius: 10px; padding: 15px; min-height: 400px; display: flex; justify-content: center; align-items: center; background-color: #ffffff; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); margin-top: 10px; }
.gradio-dataframe table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
.gradio-dataframe th, .gradio-dataframe td { border: 1px solid #dee2e6; padding: 8px 10px; text-align: left; }
.gradio-dataframe th { background-color: #f8f9fa; font-weight: 600; }
.gradio-dataframe tr:nth-child(even) { background-color: #f8f9fa; }
.color-customization-input textarea { font-family: 'Courier New', Courier, monospace; font-size: 0.9em; }
.gradio-tabs .tab-nav button { padding: 10px 20px !important; font-weight: 500 !important; border-radius: 8px 8px 0 0 !important; transition: background-color 0.2s ease, color 0.2s ease !important; }
.gradio-tabs .tab-nav button.selected { background-color: #667eea !important; color: white !important; border-bottom: none !important; }
.gradio-slider label { margin-bottom: 5px !important; }
.gradio-slider input[type="range"] { cursor: pointer !important; }
/* Radio Button 樣式調整 */
.gradio-radio fieldset { display: flex; flex-wrap: wrap; gap: 5px 15px; } /* 嘗試讓選項水平排列並換行 */
.gradio-radio label { margin-bottom: 0 !important; padding: 5px 0 !important; } /* 調整標籤間距 */
.gradio-radio input[type="radio"] { margin-right: 5px !important; }

.gradio-textbox textarea, .gradio-textbox input { border-radius: 6px !important; border: 1px solid #ced4da !important; padding: 10px !important; }
.gradio-textbox textarea:focus, .gradio-textbox input:focus { border-color: #80bdff !important; box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25) !important; }
.gradio-file .hidden-upload, .gradio-file .download-button { border-radius: 6px !important; }
.gradio-file .upload-button { border-radius: 6px !important; background: #6c757d !important; color: white !important; padding: 8px 15px !important; }
.gradio-file .upload-button:hover { background: #5a6268 !important; }
/* Accordion 樣式微調 (如果重新啟用) */
/* .gradio-accordion > .label { font-weight: 600 !important; font-size: 1.1em !important; padding: 10px 0 !important; } */
"""

# =========================================
# == Gradio UI 介面定義 (Gradio UI Definition) ==
# =========================================
with gr.Blocks(css=CUSTOM_CSS, title="數據視覺化工具_Eddie", theme=gr.themes.Soft()) as demo:

    # --- 應用程式標頭 ---
    gr.HTML("""
    <div class="app-header">
        <h1 class="app-title">📊 數據視覺化工具_Eddie</h1>
        <p class="app-subtitle">上傳或貼上數據,創建專業圖表 (極簡化測試版 - 修正)</p>
    </div>
    """)

    # --- 狀態變量 ---
    data_state = gr.State(None)
    custom_colors_state = gr.State({}) # 只保留一組狀態
    patterns_state = gr.State([])
    recommendation_state = gr.State({})

    # --- 主頁籤佈局 ---
    with gr.Tabs() as tabs:

        # --- 數據輸入頁籤 ---
        with gr.TabItem("📁 數據輸入與管理", id=0):
            with gr.Row():
                with gr.Column(scale=2): # 左側:數據輸入
                    gr.HTML('<div class="section-title">1. 上傳或輸入數據</div>')
                    with gr.Group(elem_classes=["card"]):
                        gr.Markdown("您可以上傳本地的 CSV 或 Excel 文件,或直接在下方的文本框中貼上數據。")
                        file_upload = gr.File(label="上傳 CSV / Excel 文件", type="filepath")
                        upload_button = gr.Button("⬆️ 載入文件數據", elem_classes=["primary-button"])
                        upload_status = gr.Textbox(label="載入狀態", lines=1, interactive=False)
                    with gr.Group(elem_classes=["card"]):
                        csv_input = gr.Textbox(label="或者,在此貼上數據 (逗號、Tab 或空格分隔)", placeholder="例如:\n類別,數值\nA,10\nB,20\nC,15...", lines=8, elem_classes=["data-input-textbox"])
                        parse_button = gr.Button("📝 解析貼上數據", elem_classes=["primary-button"])
                        parse_status = gr.Textbox(label="解析狀態", lines=1, interactive=False)
                with gr.Column(scale=3): # 右側:數據預覽與導出
                    gr.HTML('<div class="section-title">2. 數據預覽與導出</div>')
                    with gr.Group(elem_classes=["card"]):
                        gr.Markdown("下方將顯示載入或解析後的數據預覽。")
                        data_preview = gr.Dataframe(label="數據表格預覽", interactive=False)
                        with gr.Row():
                            export_format = gr.Radio(EXPORT_FORMATS_DATA, label="選擇導出格式", value="CSV") # Radio
                            export_button = gr.Button("⬇️ 導出預覽數據", elem_classes=["secondary-button"])
                        export_result = gr.File(label="導出文件下載", interactive=False)
                        export_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)

        # --- 圖表創建頁籤 (單圖表, 移除 Accordion, 使用 Radio) ---
        with gr.TabItem("📈 圖表創建", id=1):
            gr.HTML('<div class="section-title">創建圖表</div>')
            gr.Markdown("在此設置並生成圖表。")

            with gr.Row(): # 主 Row
                # --- 設定欄 (左側 Column) ---
                with gr.Column(scale=1):
                    gr.Markdown("### 📊 圖表設置")
                    with gr.Group(elem_classes=["card"]):
                        gr.Markdown("**基本設置**")
                        chart_type = gr.Radio(CHART_TYPES, label="圖表類型", value="長條圖", interactive=True) # Radio
                        recommend_button = gr.Button("🧠 智能推薦", elem_classes=["secondary-button"], size="sm")
                        chart_title = gr.Textbox(label="圖表標題", placeholder="我的圖表")
                        agg_function = gr.Radio(AGGREGATION_FUNCTIONS, label="聚合函數", value="計數") # Radio

                        gr.Markdown("**數據映射 (請選擇)**")
                        # 使用 Radio 進行欄位選擇 - 如果欄位過多會很長!
                        x_column = gr.Radio([NO_DATA_STR], label="X軸 / 類別", info="選擇圖表主要分類或 X 軸")
                        y_column = gr.Radio([NO_DATA_STR], label="Y軸 / 數值", info="選擇圖表數值或 Y 軸 (計數時可忽略)")
                        group_column = gr.Radio([NONE_STR, NO_DATA_STR], label="分組列", info="用於生成多系列或堆疊", value=NONE_STR)
                        size_column = gr.Radio([NONE_STR, NO_DATA_STR], label="大小列", info="用於氣泡圖等控制點的大小", value=NONE_STR)

                        gr.Markdown("**顯示選項**")
                        chart_width = gr.Slider(300, 1600, 700, step=50, label="寬度 (px)")
                        chart_height = gr.Slider(300, 1000, 450, step=50, label="高度 (px)")
                        show_grid = gr.Radio(YES_NO_CHOICES, label="顯示網格", value="是") # Radio
                        show_legend = gr.Radio(YES_NO_CHOICES, label="顯示圖例", value="是") # Radio
                        color_scheme = gr.Dropdown(list(COLOR_SCHEMES.keys()), label="顏色方案", value="預設 (Plotly)") # 保留 Dropdown
                        gr.HTML('<div style="margin-top: 10px;"><b>顏色參考</b> (點擊複製)</div>')
                        gr.HTML(generate_color_cards(), elem_id="color_display")

                        gr.Markdown("**圖案與自定義顏色**")
                        pattern1 = gr.Radio(PATTERN_TYPES, label="圖案1", value="無") # Radio
                        pattern2 = gr.Radio(PATTERN_TYPES, label="圖案2", value="無") # Radio
                        pattern3 = gr.Radio(PATTERN_TYPES, label="圖案3", value="無") # Radio
                        color_customization = gr.Textbox(label="自定義顏色", placeholder="類別A:#FF5733, 類別B:#33CFFF", info="格式: 類別名:十六進制顏色代碼, ...", elem_classes=["color-customization-input"])

                # --- 預覽與操作欄 (右側 Column) ---
                with gr.Column(scale=2):
                    # 操作按鈕 (預覽上方)
                    gr.HTML('<div class="section-title" style="margin-top:0; margin-bottom:10px;">操作</div>')
                    update_button = gr.Button("🔄 更新圖表", variant="primary", elem_classes=["primary-button"])
                    with gr.Row():
                        export_img_format = gr.Radio(EXPORT_FORMATS_IMG, label="導出格式", value="PNG", scale=1) # Radio
                        download_button = gr.Button("💾 導出圖表", elem_classes=["secondary-button"], scale=1)
                    export_chart = gr.File(label="圖表文件下載", interactive=False)
                    export_chart_status = gr.Textbox(label="導出狀態", lines=1, interactive=False)

                    # 預覽區域
                    gr.HTML('<div class="section-title" style="margin-top:20px; margin-bottom:10px;">圖表預覽</div>')
                    with gr.Group(elem_classes=["chart-previewer"]):
                        chart_output = gr.Plot(label="", elem_id="chart_preview")


        # --- 使用說明頁籤 ---
        with gr.TabItem("❓ 使用說明", id=2):
             with gr.Group(elem_classes=["card"]):
                gr.HTML("""
                <div class="section-title">使用說明 (V5 - 極簡測試版)</div>
                <h3>數據輸入</h3>
                <ul><li>點擊 "上傳 CSV / Excel 文件" 按鈕選擇本地文件,或在文本框中直接貼上數據。</li><li>支持逗號 (<code>,</code>)、製表符 (<code>Tab</code>) 或空格 (<code> </code>) 分隔的數據。</li><li>第一行通常被視為欄位名稱(表頭)。</li><li>數據載入或解析成功後,會在右側顯示預覽。</li><li>您可以使用 "導出預覽數據" 功能將處理後的數據保存為 CSV、Excel 或 JSON 格式。</li></ul>
                <h3>圖表創建</h3>
                <ul><li>此頁面僅提供一個圖表進行測試。</li><li><strong>智能推薦:</strong>點擊 "智能推薦" 按鈕,系統會根據數據結構嘗試推薦合適的設置。</li><li><strong>圖表類型/聚合函數/圖案等:</strong>使用點選按鈕 (Radio) 進行選擇。</li>
                    <ul><li><strong style="color: #7367f0;">【重要】單欄計數:</strong>若要統計某一欄位中各個項目出現的次數,請在 <strong>X軸/類別</strong> 選擇該欄位,並將 <strong>聚合函數</strong> 設為 <strong>計數</strong>,此時 <strong>無需選擇 Y軸/數值</strong>。然後選擇「長條圖」或「圓餅圖」。</li></ul>
                </li><li><strong>數據映射 (欄位選擇):</strong>使用點選按鈕 (Radio) 選擇 X軸、Y軸、分組列、大小列。<strong>注意:如果數據欄位過多,這裡會顯示得很長。</strong></li><li><strong>顯示選項/顏色/自定義:</strong>調整圖表外觀。顏色方案仍為下拉選單。</li><li>點擊 <strong>"更新圖表"</strong> 按鈕生成或刷新圖表預覽 (已移除自動更新)。</li><li>使用 "導出圖表" 功能將生成的圖表保存為圖片文件。</li></ul>
                <h3>提示</h3>
                <ul><li>如果圖表無法顯示或出現錯誤,請檢查數據格式、列選擇以及聚合函數是否合理。</li><li>如果欄位選擇的 Radio 按鈕區域過長或無法使用,表示此方法不適用於您的數據,且 Gradio 可能存在根本的元件衝突問題。</li></ul>
                """)

    # =========================================
    # == 事件處理 (Event Handling) ==
    # =========================================

    # --- 數據載入與更新 ---
    def load_data_and_update_ui_v5(df, status_msg):
        """輔助函數:更新數據狀態、預覽和所有列選擇 Radio"""
        print("調用 load_data_and_update_ui_v5...", file=sys.stderr)
        preview_df = df if df is not None else pd.DataFrame()
        # 更新列選擇 Radio
        col_updates = update_columns_as_radio(df)
        if col_updates is None or len(col_updates) != 4:
             print("警告: update_columns_as_radio 未返回預期的 4 個組件更新。", file=sys.stderr)
             # 返回空更新,避免錯誤 (狀態, 消息, 預覽, 4個Radio, 1個Plot)
             return [df, status_msg, preview_df] + [gr.update()] * 4 + [gr.Plot(value=None)]

        # 準備所有更新 (狀態, 消息, 預覽表格, 4 個 Radio)
        updates = [df, status_msg, preview_df] + list(col_updates)
        # 添加一個空的 Plot 更新
        updates.append(gr.Plot(value=None)) # 初始不繪圖

        print(f"load_data_and_update_ui_v5 返回 {len(updates)} 個更新。", file=sys.stderr)
        return updates

    # 綁定數據載入事件 - 移除初始繪圖
    upload_button.click(
        process_upload,
        inputs=[file_upload],
        outputs=[data_state, upload_status]
    ).then(
        load_data_and_update_ui_v5,
        inputs=[data_state, upload_status],
        outputs=[
            data_state, upload_status, data_preview,
            x_column, y_column, group_column, size_column, # 更新 Radio
            chart_output # 更新 Plot 為空
        ]
    )
    parse_button.click(
        parse_data,
        inputs=[csv_input],
        outputs=[data_state, parse_status]
    ).then(
        load_data_and_update_ui_v5,
        inputs=[data_state, parse_status],
        outputs=[
            data_state, parse_status, data_preview,
            x_column, y_column, group_column, size_column,
            chart_output
        ]
    )


    # --- 數據導出 ---
    export_button.click(export_data, inputs=[data_state, export_format], outputs=[export_result, export_status])

    # --- 顏色和圖案狀態 ---
    color_customization.change(parse_custom_colors, inputs=[color_customization], outputs=[custom_colors_state])
    patterns_inputs = [pattern1, pattern2, pattern3]
    for pattern_radio in patterns_inputs: # 改為 Radio
        pattern_radio.change(update_patterns, inputs=patterns_inputs, outputs=[patterns_state])

    # --- 更新圖表 (僅通過按鈕) ---
    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]

    def update_chart_action(*inputs):
        """按鈕點擊時的處理函數,包含調試信息"""
        print("="*30, file=sys.stderr)
        print("更新圖表按鈕點擊!", file=sys.stderr)
        # 打印傳入 create_plot 的數據狀態
        df_input = inputs[0] # data_state 在列表第一個
        print(f"  - data_state type in handler: {type(df_input)}", file=sys.stderr)
        if isinstance(df_input, pd.DataFrame):
            print(f"  - data_state empty in handler: {df_input.empty}", file=sys.stderr)
        else:
             print(f"  - data_state is not a DataFrame!", file=sys.stderr)
        print("="*30, file=sys.stderr)
        # 檢查 y_column 是否為 "無",如果是,傳遞 None 給 create_plot
        processed_inputs = list(inputs)
        if processed_inputs[3] == NONE_STR: # y_column 在列表索引 3 的位置
             processed_inputs[3] = None
        # 檢查 group_column 是否為 "無"
        if processed_inputs[4] == NONE_STR: # group_column 在列表索引 4 的位置
             processed_inputs[4] = None
        # 檢查 size_column 是否為 "無"
        if processed_inputs[5] == NONE_STR: # size_column 在列表索引 5 的位置
             processed_inputs[5] = None

        return create_plot(*processed_inputs) # 傳遞處理過的輸入

    update_button.click(update_chart_action, inputs=chart_inputs, outputs=[chart_output])

    # --- 導出圖表 ---
    download_button.click(download_figure, inputs=[chart_output, export_img_format], outputs=[export_chart, export_chart_status])

    # --- 智能推薦 ---
    def apply_recommendation_v5(rec_dict):
        if not isinstance(rec_dict, dict): print("警告:apply_recommendation 收到非字典輸入。"); return [gr.update()] * 5
        chart_type_val = rec_dict.get("chart_type"); x_col_val = rec_dict.get("x_column"); agg_func_val = rec_dict.get("agg_function")
        # Y 軸推薦值:如果是計數,則推薦 '無';否則推薦 Y 列名,如果 Y 列名為 None 也設為 '無'
        y_col_val = NONE_STR if agg_func_val == "計數" else rec_dict.get("y_column", NONE_STR)
        group_col_val = rec_dict.get("group_column", NONE_STR) # 默認為 "無"
        # 返回 Radio 的更新對象
        return [
            gr.Radio(value=chart_type_val),      # 更新 Chart Type Radio
            gr.Radio(value=x_col_val),           # 更新 X Column Radio
            gr.Radio(value=y_col_val),           # 更新 Y Column Radio
            gr.Radio(value=group_col_val),       # 更新 Group Column Radio
            gr.Radio(value=agg_func_val)         # 更新 Agg Function Radio
        ]

    recommend_button.click(recommend_chart_settings, inputs=[data_state], outputs=[recommendation_state]).then(
        apply_recommendation_v5, inputs=[recommendation_state], outputs=[chart_type, x_column, y_column, group_column, agg_function]
    ) # 應用推薦後不自動更新圖表,讓用戶點擊按鈕

    # --- 圖表類型改變時更新 UI 元素可見性 ---
    def update_element_visibility_v5(chart_type):
        try:
            # (與 V4 相同的邏輯)
            is_pie_like = chart_type in ["圓餅圖", "環形圖", "漏斗圖", "樹狀圖"]; is_histogram = chart_type == "直方圖"
            is_box_violin = chart_type in ["箱型圖", "小提琴圖"]; is_gantt = chart_type == "甘特圖"
            is_heatmap = chart_type == "熱力圖"; is_radar = chart_type == "雷達圖"
            y_label, y_needed = "Y軸 / 數值", True
            if is_histogram: y_label, y_needed = "Y軸 (自動計數)", False
            elif is_pie_like: y_label = "數值列 (用於大小/值)"
            elif is_box_violin: y_label = "數值列"
            elif is_gantt: y_label = "開始時間列"
            elif is_radar: y_label = "徑向值 (R)"
            group_label, group_needed = "分組列", chart_type in ["堆疊長條圖", "百分比堆疊長條圖", "群組長條圖", "水平長條圖", "折線圖", "多重折線圖", "階梯折線圖", "區域圖", "堆疊區域圖", "百分比堆疊區域圖", "散點圖", "氣泡圖", "箱型圖", "小提琴圖", "熱力圖", "雷達圖", "極座標圖"]
            if is_gantt: group_label, group_needed = "結束時間列", True
            elif is_heatmap: group_label, group_needed = "行/列 分組", True
            size_label, size_needed = "大小列", chart_type in ["氣泡圖", "散點圖"]
            if is_gantt: size_label, size_needed = "顏色列 (可選)", True
            # 返回 Radio 的更新對象
            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))
        except Exception as e: print(f"Error in update_element_visibility: {e}"); return (gr.update(), gr.update(), gr.update())

    # 綁定到 Radio chart_type 的 change 事件
    chart_type.change(update_element_visibility_v5, inputs=[chart_type], outputs=[y_column, group_column, size_column])

# =========================================
# == 應用程式啟動 (Launch Application) ==
# =========================================
if __name__ == "__main__":
    demo.launch(debug=True)