'use client' import { useMemo, useState } from 'react' import { createMap } from 'svg-dotted-map' import { cn, formatNumber } from '@ciphera-net/ui' import { countryCentroids } from '@/lib/country-centroids' interface DottedMapProps { data: Array<{ country: string; pageviews: number }> className?: string } function getCountryName(code: string): string { try { const regionNames = new Intl.DisplayNames(['en'], { type: 'region' }) return regionNames.of(code) || code } catch { return code } } export default function DottedMap({ data, className }: DottedMapProps) { const width = 150 const height = 68 const dotRadius = 0.25 const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null) const { points, addMarkers } = useMemo( () => createMap({ width, height, mapSamples: 8000 }), [width, height], ) const markerData = 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, country: d.country, pageviews: d.pageviews, })) }, [data]) const markerInputs = useMemo( () => markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size })), [markerData], ) const processedMarkers = useMemo(() => addMarkers(markerInputs), [addMarkers, markerInputs]) // 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() 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 (
{points.map((point, index) => { const rowIndex = yToRowIndex.get(point.y) ?? 0 const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0 return ( ) })} {processedMarkers.map((marker, index) => { const rowIndex = yToRowIndex.get(marker.y) ?? 0 const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0 const info = markerData[index] return ( { if (info) { const rect = (e.target as SVGCircleElement).closest('svg')!.getBoundingClientRect() const svgX = marker.x + offsetX const svgY = marker.y setTooltip({ x: rect.left + (svgX / width) * rect.width, y: rect.top + (svgY / height) * rect.height, country: info.country, pageviews: info.pageviews, }) } }} onMouseLeave={() => setTooltip(null)} /> ) })} {tooltip && (
{getCountryName(tooltip.country)} {formatNumber(tooltip.pageviews)}
)}
) }