From 31416f0eb26029e474f92600fb843695fa1f6ae0 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Mar 2026 14:23:48 +0100 Subject: [PATCH] Polish DottedMap: glow effect, tooltips, better fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SVG filter for orange glow behind markers - Hover tooltips showing country name + pageview count - Reduced viewBox height (75→68) to fill card better - Bumped mapSamples to 8000 for crisper landmass - Centered map vertically with flexbox wrapper Co-Authored-By: Claude Opus 4.6 --- components/dashboard/DottedMap.tsx | 131 ++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 38 deletions(-) diff --git a/components/dashboard/DottedMap.tsx b/components/dashboard/DottedMap.tsx index dee2e67..f0799d8 100644 --- a/components/dashboard/DottedMap.tsx +++ b/components/dashboard/DottedMap.tsx @@ -1,8 +1,8 @@ 'use client' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { createMap } from 'svg-dotted-map' -import { cn } from '@ciphera-net/ui' +import { cn, formatNumber } from '@ciphera-net/ui' import { countryCentroids } from '@/lib/country-centroids' interface DottedMapProps { @@ -10,14 +10,24 @@ interface DottedMapProps { 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 = 75 + const height = 68 const dotRadius = 0.2 + const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null) - const { points, addMarkers } = createMap({ width, height, mapSamples: 5000 }) + const { points, addMarkers } = createMap({ width, height, mapSamples: 8000 }) - const markers = useMemo(() => { + const markerData = useMemo(() => { if (!data.length) return [] const max = Math.max(...data.map((d) => d.pageviews)) @@ -29,10 +39,16 @@ export default function DottedMap({ data, className }: DottedMapProps) { 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 processedMarkers = addMarkers(markers) + const markerInputs = useMemo( + () => markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size })), + [markerData], + ) + const processedMarkers = addMarkers(markerInputs) // Compute stagger helpers const { xStep, yToRowIndex } = useMemo(() => { @@ -59,37 +75,76 @@ export default function DottedMap({ data, className }: DottedMapProps) { }, [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 - 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)} +
+ )} +
) }