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