Skip to content

Commit

Permalink
feat: global map
Browse files Browse the repository at this point in the history
  • Loading branch information
hamster1963 committed Dec 4, 2024
1 parent 535e9f6 commit 8228fab
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 4 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-query-devtools": "^5.62.0",
"@tanstack/react-table": "^8.20.5",
"@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.13",
"d3-geo": "^3.1.1",
"framer-motion": "^11.12.0",
"i18next": "^24.0.2",
"lucide-react": "^0.460.0",
Expand All @@ -34,7 +36,6 @@
"react-dom": "^18.3.1",
"react-i18next": "^15.1.3",
"react-router-dom": "^7.0.1",
"react-use-websocket": "^4.11.1",
"recharts": "^2.13.3",
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.5",
Expand Down
210 changes: 210 additions & 0 deletions src/components/GlobalMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { geoJsonString } from "@/lib/geo-json-string";
import { NezhaServer } from "@/types/nezha-api";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { AnimatePresence, m } from "framer-motion";
import { geoEquirectangular, geoPath } from "d3-geo";
import { countryCoordinates } from "@/lib/geo-limit";

export default function GlobalMap({
serverList,
}: {
serverList: NezhaServer[];
}) {
const { t } = useTranslation();
const countryList: string[] = [];
const serverCounts: { [key: string]: number } = {};

console.log(serverList);

serverList.forEach((server) => {
if (server.country_code) {
const countryCode = server.country_code.toUpperCase();
if (!countryList.includes(countryCode)) {
countryList.push(countryCode);
}
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
}
});

const width = 900;
const height = 500;

const geoJson = JSON.parse(geoJsonString);
const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);

return (
<section className="flex flex-col gap-4 mt-8">
<p className="text-sm font-medium opacity-40">
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
</p>
<div className="w-full overflow-x-auto">
<InteractiveMap
countries={countryList}
serverCounts={serverCounts}
width={width}
height={height}
filteredFeatures={filteredFeatures}
/>
</div>
</section>
);
}

interface InteractiveMapProps {
countries: string[];
serverCounts: { [key: string]: number };
width: number;
height: number;
filteredFeatures: {
type: "Feature";
properties: {
iso_a2_eh: string;
[key: string]: string;
};
geometry: never;
}[];
}

function InteractiveMap({
countries,
serverCounts,
width,
height,
filteredFeatures,
}: InteractiveMapProps) {
const { t } = useTranslation();
const [tooltipData, setTooltipData] = useState<{
centroid: [number, number];
country: string;
count: number;
} | null>(null);

const projection = geoEquirectangular()
.scale(140)
.translate([width / 2, height / 2])
.rotate([-12, 0, 0]);

const path = geoPath().projection(projection);

return (
<div className="relative w-full aspect-[2/1]">
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
>
<defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
</pattern>
</defs>
<g>
{filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(
feature.properties.iso_a2_eh,
);

if (isHighlighted) {
console.log(feature.properties.iso_a2_eh);
}

const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;

return (
<path
key={index}
d={path(feature) || ""}
className={
isHighlighted
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
}
onMouseEnter={() => {
if (isHighlighted && path.centroid(feature)) {
setTooltipData({
centroid: path.centroid(feature),
country: feature.properties.name,
count: serverCount,
});
}
}}
onMouseLeave={() => setTooltipData(null)}
/>
);
})}

{/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);

// 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null;

// 获取国家的经纬度
const coords = countryCoordinates[countryCode];
if (!coords) return null;

// 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0;

return (
<g
key={countryCode}
onMouseEnter={() => {
setTooltipData({
centroid: [x, y],
country: coords.name,
count: serverCount,
});
}}
onMouseLeave={() => setTooltipData(null)}
className="cursor-pointer"
>
<circle
cx={x}
cy={y}
r={4}
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/>
</g>
);
})}
</g>
</svg>
<AnimatePresence mode="wait">
{tooltipData && (
<m.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
className="absolute hidden lg:block pointer-events-none bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700"
key={tooltipData.country}
style={{
left: tooltipData.centroid[0],
top: tooltipData.centroid[1],
transform: "translate(-50%, -50%)",
}}
>
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400">
{tooltipData.count} {t("map.Servers")}
</p>
</m.div>
)}
</AnimatePresence>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/ServerOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ export default function ServerOverview({
{t("serverOverview.network")}
</p>
<section className="flex flex-row z-[999] sm:items-center items-start pr-2 sm:pr-0 gap-1 ml-auto">
<p className="sm:text-[12px] text-[10px] text-blue-800 text-nowrap font-medium">
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
{formatBytes(up)}
</p>
<p className="sm:text-[12px] text-[10px] text-purple-800 text-nowrap font-medium">
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
{formatBytes(down)}
</p>
</section>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/geo-json-string.ts

Large diffs are not rendered by default.

Loading

0 comments on commit 8228fab

Please sign in to comment.