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();
});