From 7b28e195c78686040bd6f0ce0fe701c653d2417b Mon Sep 17 00:00:00 2001 From: Sajid Mehmood Date: Mon, 3 Aug 2026 19:21:00 +0000 Subject: [PATCH] Filter by country when clicking the world map Clicking a country on the world map now applies the same country=eq.XX filter that the country metrics table produces. Clicking the currently selected country clears the filter, and the selected country renders in the full base color so the active selection is visible on the map. Only countries with data are clickable (cursor: pointer); clicks that are the tail end of a pan/drag are ignored. Filtering is disabled on the realtime map, which does not read query filters. Run-on: Niteshift Co-Authored-By: Claude Opus 5 --- .../[websiteId]/realtime/RealtimePage.tsx | 2 +- src/components/metrics/WorldMap.tsx | 42 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 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..0e82abb48 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, updateParams, query } = useNavigation(); const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale); const unknownLabel = formatMessage(labels.unknown); + const selectedCountry = query.country?.startsWith('eq.') ? query.country.slice(3) : null; const { data: mapData } = useWebsiteMetricsQuery(websiteId, { type: 'country', @@ -45,6 +50,10 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { return colors.map.fillColor; } + if (selectedCountry === code) { + return colors.map.baseColor; + } + return colord(colors.map.baseColor) [theme === 'light' ? 'lighten' : 'darken'](0.4 * (1.0 - country.z / 100)) .toHex(); @@ -54,6 +63,10 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { return code === 'AQ' ? 0 : 1; }; + const isClickable = (code: string) => { + return allowFilter && code && code !== 'AQ' && metrics?.some(({ x }) => x === code); + }; + const handleHover = (code: string) => { if (code === 'AQ') return; const country = metrics?.find(({ x }) => x === code); @@ -64,12 +77,27 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { ); }; + const handleClick = (code: string, e: MouseEvent) => { + if (!isClickable(code)) return; + + // ignore clicks that are the tail end of a pan/drag on the map + const { x, y } = pointerStart.current || { x: e.clientX, y: e.clientY }; + if (Math.abs(e.clientX - x) > 3 || Math.abs(e.clientY - y) > 3) return; + + router.replace( + updateParams({ country: selectedCountry === code ? undefined : `eq.${code}` }) as string, + ); + }; + return ( { + pointerStart.current = { x: e.clientX, y: e.clientY }; + }} > @@ -77,6 +105,7 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) { {({ geographies }) => { return geographies.map(geo => { const code = ISO_COUNTRIES[geo.id]; + const clickable = isClickable(code); return ( handleHover(code)} onMouseOut={() => setTooltipPopup(null)} + onClick={(e: MouseEvent) => handleClick(code, e)} /> ); });