Replace WorldMap with Magic UI DottedMap for visitor locations

- New DottedMap component using svg-dotted-map with country centroid markers
- Marker size scales by pageview proportion (brand orange)
- Static country-centroids.ts lookup (~200 ISO codes)
- Remove react-simple-maps, i18n-iso-countries, world-atlas CDN dependency
This commit is contained in:
Usman Baig
2026-03-09 14:17:35 +01:00
parent cbf48318ce
commit 6ccc26ab48
6 changed files with 309 additions and 349 deletions

View File

@@ -0,0 +1,95 @@
'use client'
import { useMemo } from 'react'
import { createMap } from 'svg-dotted-map'
import { cn } from '@ciphera-net/ui'
import { countryCentroids } from '@/lib/country-centroids'
interface DottedMapProps {
data: Array<{ country: string; pageviews: number }>
className?: string
}
export default function DottedMap({ data, className }: DottedMapProps) {
const width = 150
const height = 75
const dotRadius = 0.2
const { points, addMarkers } = createMap({ width, height, mapSamples: 5000 })
const markers = useMemo(() => {
if (!data.length) return []
const max = Math.max(...data.map((d) => d.pageviews))
if (max === 0) return []
return data
.filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country])
.map((d) => ({
lat: countryCentroids[d.country].lat,
lng: countryCentroids[d.country].lng,
size: 0.4 + (d.pageviews / max) * 0.8,
}))
}, [data])
const processedMarkers = addMarkers(markers)
// Compute stagger helpers
const { xStep, yToRowIndex } = useMemo(() => {
const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x)
const rowMap = new Map<number, number>()
let step = 0
let prevY = Number.NaN
let prevXInRow = Number.NaN
for (const p of sorted) {
if (p.y !== prevY) {
prevY = p.y
prevXInRow = Number.NaN
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
}
if (!Number.isNaN(prevXInRow)) {
const delta = p.x - prevXInRow
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
}
prevXInRow = p.x
}
return { xStep: step || 1, yToRowIndex: rowMap }
}, [points])
return (
<svg
viewBox={`0 0 ${width} ${height}`}
className={cn('text-neutral-300 dark:text-neutral-700', className)}
style={{ width: '100%', height: '100%' }}
>
{points.map((point, index) => {
const rowIndex = yToRowIndex.get(point.y) ?? 0
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
return (
<circle
cx={point.x + offsetX}
cy={point.y}
r={dotRadius}
fill="currentColor"
key={`${point.x}-${point.y}-${index}`}
/>
)
})}
{processedMarkers.map((marker, index) => {
const rowIndex = yToRowIndex.get(marker.y) ?? 0
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
return (
<circle
cx={marker.x + offsetX}
cy={marker.y}
r={marker.size ?? dotRadius}
fill="#FD5E0F"
key={`marker-${marker.x}-${marker.y}-${index}`}
/>
)
})}
</svg>
)
}

View File

@@ -6,7 +6,7 @@ import { formatNumber } from '@ciphera-net/ui'
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
import * as Flags from 'country-flag-icons/react/3x2'
import iso3166 from 'iso-3166-2'
import WorldMap from './WorldMap'
import DottedMap from './DottedMap'
import { Modal, GlobeIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons'
import { ShieldCheck, Detective, Broadcast } from '@phosphor-icons/react'
@@ -225,7 +225,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<p className="text-neutral-500 dark:text-neutral-400 text-sm">{getDisabledMessage()}</p>
</div>
) : activeTab === 'map' ? (
hasData ? <WorldMap data={filterUnknown(countries) as { country: string; pageviews: number }[]} /> : (
hasData ? <DottedMap data={filterUnknown(countries) as { country: string; pageviews: number }[]} /> : (
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />

View File

@@ -1,110 +0,0 @@
'use client'
import React, { memo, useMemo, useState } from 'react'
import { ComposableMap, Geographies, Geography } from 'react-simple-maps'
import countries from 'i18n-iso-countries'
import enLocale from 'i18n-iso-countries/langs/en.json'
import { useTheme } from '@ciphera-net/ui'
countries.registerLocale(enLocale)
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
interface WorldMapProps {
data: Array<{ country: string; pageviews: number }>
}
const WorldMap = ({ data }: WorldMapProps) => {
const { resolvedTheme } = useTheme()
const [tooltipContent, setTooltipContent] = useState<{ content: string; x: number; y: number } | null>(null)
const processedData = useMemo(() => {
const map = new Map<string, number>()
let max = 0
data.forEach(item => {
if (item.country === 'Unknown') return
// API returns 2 letter code. Convert to numeric (3 digits string)
const numericCode = countries.alpha2ToNumeric(item.country)
if (numericCode) {
map.set(numericCode, item.pageviews)
if (item.pageviews > max) max = item.pageviews
}
})
return { map, max }
}, [data])
// Plausible-like colors based on provided SVG snippet
const isDark = resolvedTheme === 'dark'
const defaultFill = isDark ? "var(--color-neutral-800)" : "var(--color-neutral-100)"
const defaultStroke = isDark ? "var(--color-neutral-900)" : "#ffffff"
const brandOrange = "var(--color-brand-orange)"
return (
<div className="relative w-full">
<ComposableMap
width={800}
height={400}
projectionConfig={{ rotate: [-10, 0, 0], scale: 170, center: [0, 10] }}
className="w-full h-auto"
>
<Geographies geography={geoUrl}>
{({ geographies }) =>
geographies
.filter(geo => geo.id !== "010") // Remove Antarctica
.map((geo) => {
const id = String(geo.id).padStart(3, '0')
const count = processedData.map.get(id) || 0
const fillColor = count > 0 ? brandOrange : defaultFill
return (
<Geography
key={geo.rsmKey}
geography={geo}
fill={fillColor}
stroke={defaultStroke}
strokeWidth={0.5}
style={{
default: { outline: "none", transition: "all 250ms" },
hover: {
fill: fillColor,
stroke: brandOrange,
strokeWidth: 2,
outline: "none",
cursor: 'pointer',
zIndex: 100 // Bring border to front
},
pressed: { outline: "none" },
}}
onMouseEnter={(evt) => {
const { name } = geo.properties
setTooltipContent({
content: `${name}: ${count} visitors`,
x: evt.clientX,
y: evt.clientY
})
}}
onMouseLeave={() => {
setTooltipContent(null)
}}
onMouseMove={(evt) => {
setTooltipContent(prev => prev ? { ...prev, x: evt.clientX, y: evt.clientY } : null)
}}
/>
)
})
}
</Geographies>
</ComposableMap>
{tooltipContent && (
<div
className="fixed z-50 px-2 py-1 text-xs font-medium text-white bg-black/80 backdrop-blur-sm rounded shadow pointer-events-none transform -translate-x-1/2 -translate-y-full -mt-2.5"
style={{ left: tooltipContent.x, top: tooltipContent.y }}
>
{tooltipContent.content}
</div>
)}
</div>
)
}
export default memo(WorldMap)

205
lib/country-centroids.ts Normal file
View File

@@ -0,0 +1,205 @@
/**
* Country centroids: ISO 3166-1 alpha-2 → { lat, lng }
* Used to place markers on the DottedMap for visitor locations.
*/
export const countryCentroids: Record<string, { lat: number; lng: number }> = {
AD: { lat: 42.5, lng: 1.5 },
AE: { lat: 24.0, lng: 54.0 },
AF: { lat: 33.0, lng: 65.0 },
AG: { lat: 17.1, lng: -61.8 },
AL: { lat: 41.0, lng: 20.0 },
AM: { lat: 40.0, lng: 45.0 },
AO: { lat: -12.5, lng: 18.5 },
AR: { lat: -34.0, lng: -64.0 },
AT: { lat: 47.3, lng: 13.3 },
AU: { lat: -25.0, lng: 134.0 },
AZ: { lat: 40.5, lng: 47.5 },
BA: { lat: 44.0, lng: 17.8 },
BB: { lat: 13.2, lng: -59.5 },
BD: { lat: 24.0, lng: 90.0 },
BE: { lat: 50.8, lng: 4.0 },
BF: { lat: 13.0, lng: -1.5 },
BG: { lat: 43.0, lng: 25.0 },
BH: { lat: 26.0, lng: 50.6 },
BI: { lat: -3.5, lng: 29.9 },
BJ: { lat: 9.3, lng: 2.3 },
BN: { lat: 4.5, lng: 114.7 },
BO: { lat: -17.0, lng: -65.0 },
BR: { lat: -10.0, lng: -55.0 },
BS: { lat: 24.3, lng: -76.0 },
BT: { lat: 27.5, lng: 90.5 },
BW: { lat: -22.0, lng: 24.0 },
BY: { lat: 53.0, lng: 28.0 },
BZ: { lat: 17.3, lng: -88.8 },
CA: { lat: 56.0, lng: -96.0 },
CD: { lat: -3.0, lng: 23.0 },
CF: { lat: 7.0, lng: 21.0 },
CG: { lat: -1.0, lng: 15.0 },
CH: { lat: 47.0, lng: 8.0 },
CI: { lat: 8.0, lng: -5.5 },
CL: { lat: -30.0, lng: -71.0 },
CM: { lat: 6.0, lng: 12.5 },
CN: { lat: 35.0, lng: 105.0 },
CO: { lat: 4.0, lng: -72.0 },
CR: { lat: 10.0, lng: -84.0 },
CU: { lat: 22.0, lng: -79.5 },
CV: { lat: 16.0, lng: -24.0 },
CY: { lat: 35.0, lng: 33.0 },
CZ: { lat: 49.8, lng: 15.5 },
DE: { lat: 51.2, lng: 10.4 },
DJ: { lat: 11.5, lng: 43.1 },
DK: { lat: 56.0, lng: 10.0 },
DM: { lat: 15.4, lng: -61.4 },
DO: { lat: 19.0, lng: -70.7 },
DZ: { lat: 28.0, lng: 3.0 },
EC: { lat: -2.0, lng: -77.5 },
EE: { lat: 59.0, lng: 26.0 },
EG: { lat: 27.0, lng: 30.0 },
ER: { lat: 15.0, lng: 39.0 },
ES: { lat: 40.0, lng: -4.0 },
ET: { lat: 8.0, lng: 38.0 },
FI: { lat: 64.0, lng: 26.0 },
FJ: { lat: -18.0, lng: 175.0 },
FM: { lat: 6.9, lng: 158.2 },
FR: { lat: 46.0, lng: 2.0 },
GA: { lat: -1.0, lng: 11.8 },
GB: { lat: 54.0, lng: -2.0 },
GD: { lat: 12.1, lng: -61.7 },
GE: { lat: 42.0, lng: 43.5 },
GH: { lat: 8.0, lng: -2.0 },
GM: { lat: 13.5, lng: -15.3 },
GN: { lat: 11.0, lng: -10.0 },
GQ: { lat: 2.0, lng: 10.0 },
GR: { lat: 39.0, lng: 22.0 },
GT: { lat: 15.5, lng: -90.3 },
GW: { lat: 12.0, lng: -15.0 },
GY: { lat: 5.0, lng: -59.0 },
HK: { lat: 22.3, lng: 114.2 },
HN: { lat: 15.0, lng: -86.5 },
HR: { lat: 45.2, lng: 15.5 },
HT: { lat: 19.0, lng: -72.4 },
HU: { lat: 47.0, lng: 20.0 },
ID: { lat: -5.0, lng: 120.0 },
IE: { lat: 53.0, lng: -8.0 },
IL: { lat: 31.5, lng: 34.8 },
IN: { lat: 20.0, lng: 77.0 },
IQ: { lat: 33.0, lng: 44.0 },
IR: { lat: 32.0, lng: 53.0 },
IS: { lat: 65.0, lng: -18.0 },
IT: { lat: 42.8, lng: 12.8 },
JM: { lat: 18.3, lng: -77.4 },
JO: { lat: 31.0, lng: 36.0 },
JP: { lat: 36.0, lng: 138.0 },
KE: { lat: 1.0, lng: 38.0 },
KG: { lat: 41.0, lng: 75.0 },
KH: { lat: 13.0, lng: 105.0 },
KI: { lat: 1.4, lng: 173.0 },
KM: { lat: -12.2, lng: 44.2 },
KN: { lat: 17.3, lng: -62.7 },
KP: { lat: 40.0, lng: 127.0 },
KR: { lat: 37.0, lng: 127.5 },
KW: { lat: 29.5, lng: 47.8 },
KZ: { lat: 48.0, lng: 68.0 },
LA: { lat: 18.0, lng: 105.0 },
LB: { lat: 33.9, lng: 35.8 },
LC: { lat: 13.9, lng: -61.0 },
LI: { lat: 47.2, lng: 9.5 },
LK: { lat: 7.0, lng: 81.0 },
LR: { lat: 6.5, lng: -9.5 },
LS: { lat: -29.5, lng: 28.5 },
LT: { lat: 56.0, lng: 24.0 },
LU: { lat: 49.8, lng: 6.2 },
LV: { lat: 57.0, lng: 25.0 },
LY: { lat: 25.0, lng: 17.0 },
MA: { lat: 32.0, lng: -5.0 },
MC: { lat: 43.7, lng: 7.4 },
MD: { lat: 47.0, lng: 29.0 },
ME: { lat: 42.5, lng: 19.3 },
MG: { lat: -20.0, lng: 47.0 },
MK: { lat: 41.8, lng: 22.0 },
ML: { lat: 17.0, lng: -4.0 },
MM: { lat: 22.0, lng: 98.0 },
MN: { lat: 46.0, lng: 105.0 },
MO: { lat: 22.2, lng: 113.5 },
MR: { lat: 20.0, lng: -12.0 },
MT: { lat: 35.9, lng: 14.4 },
MU: { lat: -20.3, lng: 57.6 },
MV: { lat: 3.2, lng: 73.2 },
MW: { lat: -13.5, lng: 34.0 },
MX: { lat: 23.0, lng: -102.0 },
MY: { lat: 2.5, lng: 112.5 },
MZ: { lat: -18.3, lng: 35.0 },
NA: { lat: -22.0, lng: 17.0 },
NE: { lat: 16.0, lng: 8.0 },
NG: { lat: 10.0, lng: 8.0 },
NI: { lat: 13.0, lng: -85.0 },
NL: { lat: 52.5, lng: 5.8 },
NO: { lat: 62.0, lng: 10.0 },
NP: { lat: 28.0, lng: 84.0 },
NR: { lat: -0.5, lng: 166.9 },
NZ: { lat: -41.0, lng: 174.0 },
OM: { lat: 21.0, lng: 57.0 },
PA: { lat: 9.0, lng: -80.0 },
PE: { lat: -10.0, lng: -76.0 },
PG: { lat: -6.0, lng: 147.0 },
PH: { lat: 13.0, lng: 122.0 },
PK: { lat: 30.0, lng: 70.0 },
PL: { lat: 52.0, lng: 20.0 },
PR: { lat: 18.3, lng: -66.6 },
PS: { lat: 31.9, lng: 35.2 },
PT: { lat: 39.5, lng: -8.0 },
PW: { lat: 7.5, lng: 134.6 },
PY: { lat: -23.0, lng: -58.0 },
QA: { lat: 25.5, lng: 51.3 },
RO: { lat: 46.0, lng: 25.0 },
RS: { lat: 44.0, lng: 21.0 },
RU: { lat: 60.0, lng: 100.0 },
RW: { lat: -2.0, lng: 29.9 },
SA: { lat: 24.0, lng: 45.0 },
SB: { lat: -8.0, lng: 159.0 },
SC: { lat: -4.7, lng: 55.5 },
SD: { lat: 15.0, lng: 30.0 },
SE: { lat: 62.0, lng: 15.0 },
SG: { lat: 1.4, lng: 103.8 },
SI: { lat: 46.1, lng: 15.0 },
SK: { lat: 48.7, lng: 19.5 },
SL: { lat: 8.5, lng: -11.8 },
SM: { lat: 43.9, lng: 12.4 },
SN: { lat: 14.5, lng: -14.5 },
SO: { lat: 5.0, lng: 46.0 },
SR: { lat: 4.0, lng: -56.0 },
SS: { lat: 7.0, lng: 30.0 },
ST: { lat: 1.0, lng: 7.0 },
SV: { lat: 13.8, lng: -88.9 },
SY: { lat: 35.0, lng: 38.0 },
SZ: { lat: -26.5, lng: 31.5 },
TD: { lat: 15.0, lng: 19.0 },
TG: { lat: 8.0, lng: 1.2 },
TH: { lat: 15.0, lng: 100.0 },
TJ: { lat: 39.0, lng: 71.0 },
TL: { lat: -8.8, lng: 126.0 },
TM: { lat: 40.0, lng: 60.0 },
TN: { lat: 34.0, lng: 9.0 },
TO: { lat: -20.0, lng: -175.0 },
TR: { lat: 39.0, lng: 35.0 },
TT: { lat: 10.5, lng: -61.3 },
TV: { lat: -8.0, lng: 178.0 },
TW: { lat: 23.5, lng: 121.0 },
TZ: { lat: -6.0, lng: 35.0 },
UA: { lat: 49.0, lng: 32.0 },
UG: { lat: 1.0, lng: 32.0 },
US: { lat: 39.8, lng: -98.5 },
UY: { lat: -33.0, lng: -56.0 },
UZ: { lat: 41.0, lng: 64.0 },
VA: { lat: 41.9, lng: 12.5 },
VC: { lat: 13.3, lng: -61.2 },
VE: { lat: 8.0, lng: -66.0 },
VN: { lat: 16.0, lng: 108.0 },
VU: { lat: -16.0, lng: 167.0 },
WS: { lat: -13.8, lng: -172.1 },
XK: { lat: 42.6, lng: 21.0 },
YE: { lat: 15.0, lng: 48.0 },
ZA: { lat: -29.0, lng: 24.0 },
ZM: { lat: -15.0, lng: 28.0 },
ZW: { lat: -20.0, lng: 30.0 },
}

240
package-lock.json generated
View File

@@ -19,7 +19,6 @@
"d3-scale": "^4.0.2",
"framer-motion": "^12.23.26",
"html-to-image": "^1.11.13",
"i18n-iso-countries": "^7.14.0",
"iso-3166-2": "^1.0.0",
"jspdf": "^4.0.0",
"jspdf-autotable": "^5.0.7",
@@ -27,9 +26,9 @@
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-markdown": "^10.1.0",
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",
"sonner": "^2.0.7",
"svg-dotted-map": "^2.0.1",
"swr": "^2.3.3",
"xlsx": "^0.18.5"
},
@@ -41,7 +40,6 @@
"@types/node": "^20.14.12",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-simple-maps": "^3.0.6",
"@vitejs/plugin-react": "^5.1.4",
"autoprefixer": "^10.4.19",
"eslint": "^9.39.2",
@@ -4045,26 +4043,6 @@
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-geo": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-2.0.7.tgz",
"integrity": "sha512-RIXlxPdxvX+LAZFv+t78CuYpxYag4zuw9mZc+AwfB8tZpKU90rMEn2il2ADncmeZlb7nER9dDsJpRisA3lRvjA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz",
"integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-color": "^2"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
@@ -4080,13 +4058,6 @@
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.5.tgz",
"integrity": "sha512-71BorcY0yXl12S7lvb01JdaN9TpeUHBDb4RRhSq8U8BEkX/nIk5p7Byho+ZRTsx5nYLMpAbY3qt5EhqFzfGJlw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
@@ -4108,17 +4079,6 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/d3-zoom": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.7.tgz",
"integrity": "sha512-JWke4E8ZyrKUQ68ESTWSK16fVb0OYnaiJ+WXJRYxKLn4aXU0o4CLYxMWBEiouUfO3TTCoyroOrGPcBG6u1aAxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "^2",
"@types/d3-selection": "^2"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4170,13 +4130,6 @@
"@types/estree": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -4257,19 +4210,6 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-simple-maps": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/react-simple-maps/-/react-simple-maps-3.0.6.tgz",
"integrity": "sha512-hR01RXt6VvsE41FxDd+Bqm1PPGdKbYjCYVtCgh38YeBPt46z3SwmWPWu2L3EdCAP6bd6VYEgztucihRw1C0Klg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-geo": "^2",
"@types/d3-zoom": "^2",
"@types/geojson": "*",
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -6320,28 +6260,6 @@
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
"integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==",
"license": "BSD-3-Clause"
},
"node_modules/d3-drag": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz",
"integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dispatch": "1 - 2",
"d3-selection": "2"
}
},
"node_modules/d3-ease": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz",
"integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
@@ -6351,30 +6269,6 @@
"node": ">=12"
}
},
"node_modules/d3-geo": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz",
"integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^2.5.0"
}
},
"node_modules/d3-geo/node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"license": "BSD-3-Clause",
"dependencies": {
"internmap": "^1.0.0"
}
},
"node_modules/d3-geo/node_modules/internmap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
"license": "ISC"
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
@@ -6412,13 +6306,6 @@
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
"integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==",
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
@@ -6455,71 +6342,6 @@
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz",
"integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==",
"license": "BSD-3-Clause"
},
"node_modules/d3-transition": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz",
"integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1 - 2",
"d3-dispatch": "1 - 2",
"d3-ease": "1 - 2",
"d3-interpolate": "1 - 2",
"d3-timer": "1 - 2"
},
"peerDependencies": {
"d3-selection": "2"
}
},
"node_modules/d3-transition/node_modules/d3-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-transition/node_modules/d3-interpolate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1 - 2"
}
},
"node_modules/d3-zoom": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz",
"integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dispatch": "1 - 2",
"d3-drag": "2",
"d3-interpolate": "1 - 2",
"d3-selection": "2",
"d3-transition": "2"
}
},
"node_modules/d3-zoom/node_modules/d3-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-zoom/node_modules/d3-interpolate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1 - 2"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6764,12 +6586,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/diacritics": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==",
"license": "MIT"
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -8387,18 +8203,6 @@
"node": ">= 14"
}
},
"node_modules/i18n-iso-countries": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz",
"integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==",
"license": "MIT",
"dependencies": {
"diacritics": "1.3.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
@@ -11140,23 +10944,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-simple-maps": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-simple-maps/-/react-simple-maps-3.0.0.tgz",
"integrity": "sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==",
"license": "MIT",
"dependencies": {
"d3-geo": "^2.0.2",
"d3-selection": "^2.0.0",
"d3-zoom": "^2.0.0",
"topojson-client": "^3.1.0"
},
"peerDependencies": {
"prop-types": "^15.7.2",
"react": "^16.8.0 || 17.x || 18.x",
"react-dom": "^16.8.0 || 17.x || 18.x"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@@ -12275,6 +12062,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-dotted-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/svg-dotted-map/-/svg-dotted-map-2.0.1.tgz",
"integrity": "sha512-eeI2XzIKm23gmSVr7ASTMNVJvxAvBfyL30tN33Y/DcZCJXvC/Br/cxQp9Ts6jDK/e7fkE5TpZStEfduPqPXrIw=="
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
@@ -12613,26 +12405,6 @@
"node": ">=8.0"
}
},
"node_modules/topojson-client": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
"license": "ISC",
"dependencies": {
"commander": "2"
},
"bin": {
"topo2geo": "bin/topo2geo",
"topomerge": "bin/topomerge",
"topoquantize": "bin/topoquantize"
}
},
"node_modules/topojson-client/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",

View File

@@ -23,7 +23,6 @@
"d3-scale": "^4.0.2",
"framer-motion": "^12.23.26",
"html-to-image": "^1.11.13",
"i18n-iso-countries": "^7.14.0",
"iso-3166-2": "^1.0.0",
"jspdf": "^4.0.0",
"jspdf-autotable": "^5.0.7",
@@ -31,9 +30,9 @@
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-markdown": "^10.1.0",
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",
"sonner": "^2.0.7",
"svg-dotted-map": "^2.0.1",
"swr": "^2.3.3",
"xlsx": "^0.18.5"
},
@@ -51,7 +50,6 @@
"@types/node": "^20.14.12",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-simple-maps": "^3.0.6",
"@vitejs/plugin-react": "^5.1.4",
"autoprefixer": "^10.4.19",
"eslint": "^9.39.2",