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)}
/>
);
});