const SHEET_ID = "1sNlDhNaoVSUn8AXPA3FTzUfLAHN0MuLYHhHVST8biTg"; const DATA_SHEET_NAME = "partners"; const CODE_SHEET_NAME = "country_region_map"; const DATA_URL = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?sheet=${DATA_SHEET_NAME}&tqx=out:json`; const CODE_URL = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?sheet=${CODE_SHEET_NAME}&tqx=out:json`; let jsonData = [], filteredData = [], currentPage = 1, rowsPerPage = 15, countryCounts = {}, countryCodeMap = {}, countryCnToIso = {}; Promise.all([ fetch(DATA_URL).then(res => res.text()), fetch(CODE_URL).then(res => res.text()) ]) .then(([dataText, codeText]) => { const dataJson = JSON.parse(dataText.substring(47, dataText.length - 2)); const codeJson = JSON.parse(codeText.substring(47, codeText.length - 2)); codeJson.table.rows.forEach(row => { const countryCn = row.c[2]?.v?.trim(); const countryEn = row.c[4]?.v?.trim(); const isoCode = row.c[7]?.v?.trim(); if (isoCode && countryEn) countryCodeMap[isoCode] = countryEn; if (isoCode && countryCn) countryCnToIso[countryCn] = isoCode; }); jsonData = dataJson.table.rows.slice(1).map(row => { const countryCn = row.c[2]?.v || ""; const code = countryCnToIso[countryCn] || ""; return { RegionCn: row.c[0]?.v || "", RegionEn: row.c[1]?.v || "", CountryCn: countryCn, CountryEn: row.c[3]?.v || "", SchoolCn: row.c[4]?.v || "", SchoolEn: row.c[5]?.v || "", SchoolWeb: row.c[6]?.v || "", CountryCode: code }; }).filter(entry => entry.CountryCode); jsonData.sort((a, b) => { if (a.RegionEn !== b.RegionEn) return a.RegionEn.localeCompare(b.RegionEn); if (a.CountryEn !== b.CountryEn) return a.CountryEn.localeCompare(b.CountryEn); return a.SchoolEn.localeCompare(b.SchoolEn); }); filteredData = [...jsonData]; calculateCountryCounts(); populateFilters(); displayResults(); initMap(); document.getElementById("search-btn")?.addEventListener("click", () => { filterAndDisplay(); }); document.getElementById("region-filter")?.addEventListener("change", () => { updateCountryOptions(); }); }); function calculateCountryCounts() { countryCounts = {}; filteredData.forEach(entry => { const countryId = entry.CountryCode; countryCounts[countryId] = (countryCounts[countryId] || 0) + 1; }); } function populateFilters() { const regionFilter = document.getElementById("region-filter"); const countryFilter = document.getElementById("country-filter"); regionFilter.innerHTML = ''; countryFilter.innerHTML = ''; const regionSet = new Set(jsonData.map(i => i.RegionEn)); regionSet.forEach(region => { const opt = document.createElement("option"); opt.value = region; opt.textContent = region; regionFilter.appendChild(opt); }); updateCountryOptions(); } function updateCountryOptions() { const region = document.getElementById("region-filter").value; const countryFilter = document.getElementById("country-filter"); countryFilter.innerHTML = ''; const countrySet = new Set(jsonData .filter(item => !region || item.RegionEn === region) .map(i => i.CountryEn)); countrySet.forEach(c => { const opt = document.createElement("option"); opt.value = c; opt.textContent = c; countryFilter.appendChild(opt); }); } function filterAndDisplay() { const region = document.getElementById("region-filter").value; const country = document.getElementById("country-filter").value; const keyword = document.getElementById("keyword")?.value.trim().toLowerCase() || ""; filteredData = jsonData.filter(entry => (!region || entry.RegionEn === region) && (!country || entry.CountryEn === country) && (!keyword || Object.values(entry).some(val => val.toLowerCase().includes(keyword))) ); currentPage = 1; calculateCountryCounts(); displayResults(); } function displayResults() { const tbody = document.getElementById("table-body"); const totalCount = document.getElementById("total-count"); const totalPages = Math.ceil(filteredData.length / rowsPerPage); tbody.innerHTML = ""; const pageData = filteredData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage); pageData.forEach(item => { const row = document.createElement("tr"); row.innerHTML = ` ${item.CountryEn} ${item.SchoolEn} ${item.RegionEn} Click here `; tbody.appendChild(row); }); totalCount.textContent = `Total ${filteredData.length}`; updatePagination(totalPages); } function updatePagination(totalPages) { if (totalPages < 1) return; const pageNumbers = document.getElementById("page-numbers"); pageNumbers.innerHTML = ""; const maxPagesToShow = 5; let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); if (endPage - startPage + 1 < maxPagesToShow) { startPage = Math.max(1, endPage - maxPagesToShow + 1); } const createBtn = (text, page, disabled = false, isActive = false) => { const btn = document.createElement("button"); btn.textContent = text; btn.classList.add("page-btn"); if (isActive) btn.classList.add("active"); if (disabled) btn.disabled = true; btn.addEventListener("click", () => { currentPage = page; displayResults(); }); return btn; }; pageNumbers.appendChild(createBtn("?", 1, currentPage === 1)); pageNumbers.appendChild(createBtn("?", currentPage - 1, currentPage === 1)); for (let i = startPage; i <= endPage; i++) { pageNumbers.appendChild(createBtn(i, i, false, i === currentPage)); } pageNumbers.appendChild(createBtn("?", currentPage + 1, currentPage === totalPages)); pageNumbers.appendChild(createBtn("?", totalPages, currentPage === totalPages)); } function getColorByCount(value) { if (value >= 20) return am5.color(0x6d9e6d); if (value >= 10) return am5.color(0x90b990); if (value >= 5) return am5.color(0xb9d4b9); if (value > 0) return am5.color(0xd3e0d3); return am5.color(0xeceeea); } function initMap() { const customLabelCoordinates = { "HK": { lat: 22.3, lon: 114.2 }, // 香港 "MO": { lat: 22.2, lon: 113.5 }, // 澳門 "SG": { lat: 1.3, lon: 103.8 }, // 新加坡 "PH": { lat: 13.4, lon: 122.9 }, // 菲律賓 "SG": { lat: 1.3, lon: 103.8 }, // 新加坡 "KR": { lat: 36.5, lon: 127.8 }, // 南韓 "TH": { lat: 15.8, lon: 100.9 }, // 泰國 "VN": { lat: 14.1, lon: 108.3 }, // 越南 "MY": { lat: 4.2, lon: 101.9 }, // 馬來西亞 "BE": { lat: 50.5, lon: 4.5 }, // 比利時 "CZ": { lat: 49.8, lon: 15.5 }, // 捷克 "FR": { lat: 46.2, lon: 2.2 }, // 法國 "DE": { lat: 51.2, lon: 10.4 }, // 德國 "HU": { lat: 47.1, lon: 19.4 }, // 匈牙利 "IT": { lat: 41.9, lon: 12.6 }, // 義大利 "NL": { lat: 52.1, lon: 5.2 }, // 荷蘭 "PL": { lat: 52.1, lon: 19.3 }, // 波蘭 "ES": { lat: 40.5, lon: -3.7 }, // 西班牙 "SE": { lat: 60.1, lon: 18.6 }, // 瑞典 "CH": { lat: 46.8, lon: 8.2 }, // 瑞士 "GB": { lat: 55.3, lon: -3.4 }, // 英國 "JP": { lat: 36.2, lon: 140.3 }, "KR": { lat: 36.5, lon: 127.8 }, "CN": { lat: 35.9, lon: 104.2 }, "IN": { lat: 20.6, lon: 78.9 }, "TH": { lat: 15.8, lon: 100.9 }, "VN": { lat: 14.1, lon: 108.3 }, "MY": { lat: 4.2, lon: 101.9 }, "ID": { lat: -0.7, lon: 113.9 }, "PH": { lat: 13.4, lon: 122.9 }, // 澳洲&紐西蘭 "AU": { lat: -25.3, lon: 133.8 }, "NZ": { lat: -40.9, lon: 174.9 }, // 北美&歐洲常見 "US": { lat: 37.1, lon: -95.7 }, "CA": { lat: 56.1, lon: -106.3 }, "GB": { lat: 55.3, lon: -3.4 }, "FR": { lat: 46.2, lon: 2.2 }, "DE": { lat: 51.2, lon: 10.4 }, "NL": { lat: 52.1, lon: 5.2 }, "BE": { lat: 50.5, lon: 4.5 }, "CH": { lat: 46.8, lon: 8.2 }, "AT": { lat: 47.5, lon: 14.5 }, "IT": { lat: 41.9, lon: 12.6 }, "ES": { lat: 40.5, lon: -3.7 }, "SE": { lat: 60.1, lon: 18.6 }, "FI": { lat: 64.0, lon: 26.0 }, "PL": { lat: 52.1, lon: 19.3 }, "BR": { lat: -14.2, lon: -51.9 }, "RO": { lat: 45.9, lon: 24.9 } }; let root = am5.Root.new("map-container"); root.setThemes([am5themes_Animated.new(root)]); let chart = root.container.children.push(am5map.MapChart.new(root, { panX: "translateX", panY: "translateY", projection: am5map.geoMercator() })); let polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, { geoJSON: am5geodata_worldLow, exclude: ["AQ"] })); polygonSeries.mapPolygons.template.setAll({ interactive: true }); polygonSeries.mapPolygons.template.adapters.add("tooltipText", (text, target) => { const value = target.dataItem?.dataContext?.value; }); polygonSeries.mapPolygons.template.adapters.add("fill", (fill, target) => { const data = target.dataItem?.dataContext; return getColorByCount(data?.value || 0); }); const allCountryIds = am5geodata_worldLow.features.map(f => f.id); const dataForAllCountries = allCountryIds.map(id => ({ id, value: countryCounts[id] || 0 })); polygonSeries.data.setAll(dataForAllCountries); let pointSeries = chart.series.push(am5map.MapPointSeries.new(root, { latitudeField: "latitude", longitudeField: "longitude" })); const labels = Object.entries(countryCounts).map(([code, count]) => { const custom = customLabelCoordinates[code]; let lat, lon; if (custom) { lat = custom.lat; lon = custom.lon; } else { const feature = am5geodata_worldLow.features.find(f => f.id === code); const coordinates = am5map.getGeoCentroid(feature); if (!coordinates || !Array.isArray(coordinates)) return null; [lon, lat] = coordinates; } const countryName = countryCodeMap[code] || code; return { id: code, latitude: lat, longitude: lon, value: count, title: String(count), CountryName: countryName }; }).filter(item => item !== null); pointSeries.bullets.push((root, dataItem) => { const context = dataItem.dataContext || {}; const value = Number(context.value || 1); const radius = 10 + Math.pow(value, 0.9) * 4.5; const container = am5.Container.new(root, { centerX: am5.p50, centerY: am5.p50, width: radius * 2, height: radius * 2, }); const circle = am5.Circle.new(root, { radius: 20, fill: am5.color(0x4E79A7), fillOpacity: 0.85, tooltipText: "{CountryName}: ", populateText: true, // ? 啟用綁定 shadowColor: am5.color(0x000000), shadowBlur: 10, shadowOffsetX: 0, shadowOffsetY: 4, shadowOpacity: 0.25 }); const label = am5.Label.new(root, { text: "", // ? 改為綁定 populateText: true, // ? 啟用 data binding centerX: am5.p50, centerY: am5.p50, fontSize: Math.max(12, radius * 0.5), fontWeight: "normal", fill: am5.color(0xffffff) }); // ? 同時調整字體、大小、顏色 label.adapters.add("fontSize", (fontSize, target) => { const context = target.dataItem?.dataContext; const value = parseInt(context?.title || context?.value || "0"); if (!isNaN(value)) { if (value > 19) { circle.set("radius", 28); circle.set("fill", am5.color(0xf16f28)); return 24; } else if (value < 6) { circle.set("radius", 14); circle.set("fill", am5.color(0xffd060)); return 14; } else { circle.set("radius", 20); circle.set("fill", am5.color(0xf19e28)); return 18; } } return 18; }); container.children.push(circle); container.children.push(label); return am5.Bullet.new(root, { sprite: container }); }); pointSeries.data.setAll(labels); chart.seriesContainer.toFront(); } document.addEventListener("keydown", function(event) { if (event.key === "F12" || (event.ctrlKey && event.shiftKey && (event.key === "I" || event.key === "J")) || (event.ctrlKey && event.key === "U")) { event.preventDefault(); } }); // **禁用右鍵** document.addEventListener("contextmenu", function(event) { event.preventDefault(); });