From 82abf60357a02a5971f6f5c7b443345fa2ddce67 Mon Sep 17 00:00:00 2001 From: Sajid Mehmood Date: Fri, 14 Aug 2026 14:18:46 +0000 Subject: [PATCH] Filter by country when clicking the world map Clicking a country on the world map applies the same country=eq.XX filter that the country metrics table produces. Clicking the selected country again clears the filter. Countries with data get a pointer cursor; while a filter is active any country can be clicked so you can switch selections. Clicks that are the tail end of a pan are ignored, and filtering is disabled on the realtime map, which does not read query filters. Co-Authored-By: Claude Opus 5 --- .../[websiteId]/realtime/RealtimePage.tsx | 2 +- src/components/metrics/WorldMap.tsx | 44 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx index 6220c6957..d9933d0e6 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx @@ -50,7 +50,7 @@ export function RealtimePage({ websiteId }: { websiteId: string }) { - + diff --git a/src/components/metrics/WorldMap.tsx b/src/components/metrics/WorldMap.tsx index 3c8fadb80..3a99e4100 100644 --- a/src/components/metrics/WorldMap.tsx +++ b/src/components/metrics/WorldMap.tsx @@ -1,11 +1,12 @@ import { Column, type ColumnProps, FloatingTooltip, useTheme } from '@umami/react-zen'; import { colord } from 'colord'; -import { useMemo, useState } from 'react'; +import { type MouseEvent, useMemo, useRef, useState } from 'react'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import { useCountryNames, useLocale, useMessages, + useNavigation, useWebsiteMetricsQuery, } from '@/components/hooks'; import { getThemeColors } from '@/lib/colors'; @@ -16,17 +17,21 @@ import { formatLongNumber } from '@/lib/format'; export interface WorldMapProps extends ColumnProps { websiteId?: string; data?: any[]; + allowFilter?: boolean; } -export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { +export function WorldMap({ websiteId, data, allowFilter = true, ...props }: WorldMapProps) { const [tooltip, setTooltipPopup] = useState(); + const pointerStart = useRef<{ x: number; y: number }>(null); const { theme } = useTheme(); const { colors } = getThemeColors(theme); const { locale } = useLocale(); const { formatMessage, labels } = useMessages(); const { countryNames } = useCountryNames(locale); + const { router, query, updateParams } = useNavigation(); const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale); const unknownLabel = formatMessage(labels.unknown); + const selectedCountry = (query.country as string)?.replace(/^eq\./, ''); const { data: mapData } = useWebsiteMetricsQuery(websiteId, { type: 'country', @@ -54,6 +59,31 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { return code === 'AQ' ? 0 : 1; }; + const isClickable = (code: string) => { + if (!allowFilter || !code || code === 'AQ') return false; + + // when filtered, the map only has data for the selected country, so allow switching to any other + return !!selectedCountry || !!metrics?.find(({ x }) => x === code); + }; + + const handlePointerDown = (e: MouseEvent) => { + pointerStart.current = { x: e.clientX, y: e.clientY }; + }; + + const handleClick = (code: string, e: MouseEvent) => { + const start = pointerStart.current; + pointerStart.current = null; + + // ignore clicks that are the tail end of a pan + if (start && Math.hypot(e.clientX - start.x, e.clientY - start.y) > 5) return; + + if (!isClickable(code)) return; + + router.replace( + updateParams({ country: code === selectedCountry ? undefined : `eq.${code}` }) as string, + ); + }; + const handleHover = (code: string) => { if (code === 'AQ') return; const country = metrics?.find(({ x }) => x === code); @@ -77,6 +107,8 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { {({ geographies }) => { return geographies.map(geo => { const code = ISO_COUNTRIES[geo.id]; + const clickable = isClickable(code); + const cursor = clickable ? 'pointer' : 'default'; return ( handleHover(code)} onMouseOut={() => setTooltipPopup(null)} + onMouseDown={handlePointerDown} + onClick={(e: MouseEvent) => handleClick(code, e)} /> ); });