Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function RealtimePage({ websiteId }: { websiteId: string }) {
<RealtimeCountries data={countries} />
</Panel>
<Panel gridColumn={isMobile ? null : 'span 2'} padding="0">
<WorldMap data={countries} />
<WorldMap data={countries} allowFilter={false} />
</Panel>
</GridRow>
</Grid>
Expand Down
44 changes: 39 additions & 5 deletions src/components/metrics/WorldMap.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -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);
Expand All @@ -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 (
<Geography
Expand All @@ -86,12 +118,14 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) {
stroke={colors.map.strokeColor}
opacity={getOpacity(code)}
style={{
default: { outline: 'none' },
hover: { outline: 'none', fill: colors.map.hoverColor },
pressed: { outline: 'none' },
default: { outline: 'none', cursor },
hover: { outline: 'none', cursor, fill: colors.map.hoverColor },
pressed: { outline: 'none', cursor },
}}
onMouseOver={() => handleHover(code)}
onMouseOut={() => setTooltipPopup(null)}
onMouseDown={handlePointerDown}
onClick={(e: MouseEvent) => handleClick(code, e)}
/>
);
});
Expand Down