Skip to content

Commit 03fa177

Browse files
authored
Fix map on Chrome (#6037)
* Fix map on Chrome * Add changelog * Make sure only one country can be highlighted
1 parent 3355b5f commit 03fa177

File tree

2 files changed

+74
-55
lines changed

2 files changed

+74
-55
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
1818
### Fixed
1919

2020
- Fixed issue with all non-interactive events being counted as interactive
21+
- Fixed countries map countries staying highlighted on Chrome
2122

2223
## v3.2.0 - 2026-01-16
2324

assets/js/dashboard/stats/locations/map.tsx

Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { apiPath } from '../../util/url'
2020
import { MIN_HEIGHT } from '../reports/list'
2121
import { MapTooltip } from './map-tooltip'
2222
import { GeolocationNotice } from './geolocation-notice'
23+
import { DashboardQuery } from '../../query'
2324

2425
const width = 475
2526
const height = 335
@@ -32,6 +33,16 @@ type CountryData = {
3233
}
3334
type WorldJsonCountryData = { properties: { name: string; a3: string } }
3435

36+
function getMetricLabel(query: DashboardQuery) {
37+
if (hasConversionGoalFilter(query)) {
38+
return { singular: 'Conversion', plural: 'Conversions' }
39+
}
40+
if (isRealTimeDashboard(query)) {
41+
return { singular: 'Current visitor', plural: 'Current visitors' }
42+
}
43+
return { singular: 'Visitor', plural: 'Visitors' }
44+
}
45+
3546
const WorldMap = ({
3647
onCountrySelect,
3748
afterFetchData
@@ -50,15 +61,7 @@ const WorldMap = ({
5061
hoveredCountryAlpha3Code: string | null
5162
}>({ x: 0, y: 0, hoveredCountryAlpha3Code: null })
5263

53-
const labels = (() => {
54-
if (hasConversionGoalFilter(query)) {
55-
return { singular: 'Conversion', plural: 'Conversions' }
56-
}
57-
if (isRealTimeDashboard(query)) {
58-
return { singular: 'Current visitor', plural: 'Current visitors' }
59-
}
60-
return { singular: 'Visitor', plural: 'Visitors' }
61-
})()
64+
const metricLabel = useMemo(() => getMetricLabel(query), [query])
6265

6366
const { data, refetch, isFetching, isError } = useQuery({
6467
queryKey: ['countries', 'map', query],
@@ -127,7 +130,28 @@ const WorldMap = ({
127130
return
128131
}
129132

130-
const svg = drawInteractiveCountries(svgRef.current, setTooltip)
133+
const { svg, countriesSelection } = drawInteractiveCountries(svgRef.current)
134+
const highlightSelection = drawHighlightedCountryOutline(svgRef.current)
135+
136+
countriesSelection
137+
.on('mouseover', function (event, country) {
138+
const [x, y] = d3.pointer(event, svg.node()?.parentNode)
139+
setTooltip({ x, y, hoveredCountryAlpha3Code: country.properties.a3 })
140+
141+
highlightSelection
142+
.attr('d', this.getAttribute('d'))
143+
.attr('class', hoveredOutlineClass)
144+
})
145+
146+
.on('mousemove', function (event) {
147+
const [x, y] = d3.pointer(event, svg.node()?.parentNode)
148+
setTooltip((currentState) => ({ ...currentState, x, y }))
149+
})
150+
151+
.on('mouseout', function () {
152+
setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null })
153+
highlightSelection.attr('d', null).attr('class', initialOutlineClass)
154+
})
131155

132156
return () => {
133157
svg.selectAll('*').remove()
@@ -178,7 +202,9 @@ const WorldMap = ({
178202
name={hoveredCountryData.name}
179203
value={numberShortFormatter(hoveredCountryData.visitors)}
180204
label={
181-
labels[hoveredCountryData.visitors === 1 ? 'singular' : 'plural']
205+
hoveredCountryData.visitors === 1
206+
? metricLabel.singular
207+
: metricLabel.plural
182208
}
183209
/>
184210
)}
@@ -201,26 +227,41 @@ const colorScales = {
201227
[UIMode.light]: ['#e0e7ff', '#818cf8'] // indigo-100, indigo-400
202228
}
203229

204-
const sharedCountryClass = classNames('transition-colors')
230+
const countryElementClass = 'country'
231+
const countrySelector = `path.${countryElementClass}`
232+
const initialStroke = classNames(
233+
'stroke-white',
234+
'dark:stroke-gray-900',
235+
'stroke-1px'
236+
)
237+
const hoveredStroke = classNames(
238+
'stroke-[1.5px]',
239+
'stroke-indigo-400',
240+
'dark:stroke-indigo-500'
241+
)
205242

206243
const countryClass = classNames(
207-
sharedCountryClass,
244+
countryElementClass,
245+
initialStroke,
246+
'transition-colors',
208247
'stroke-1',
209248
'fill-gray-150',
210-
'stroke-white',
211-
'dark:fill-gray-750',
212-
'dark:stroke-gray-900'
249+
'dark:fill-gray-750'
213250
)
214251

215-
const highlightedCountryClass = classNames(
216-
sharedCountryClass,
217-
'stroke-[1.5px]',
218-
'fill-gray-150',
219-
'stroke-indigo-400',
220-
'dark:fill-gray-750',
221-
'dark:stroke-indigo-500'
252+
const sharedOutlineClass = classNames(
253+
'transition-colors',
254+
'fill-none',
255+
'pointer-events-none'
222256
)
223257

258+
const initialOutlineClass = classNames(
259+
sharedOutlineClass,
260+
initialStroke,
261+
'opacity-0'
262+
)
263+
const hoveredOutlineClass = classNames(sharedOutlineClass, hoveredStroke)
264+
224265
/**
225266
* Used to color the countries
226267
* @returns the svg elements represeting countries
@@ -239,7 +280,7 @@ function colorInCountriesWithValues(
239280
const svg = d3.select(element)
240281

241282
return svg
242-
.selectAll('path')
283+
.selectAll(countrySelector)
243284
.style('fill', (countryPath) => {
244285
const country = getCountryByCountryPath(countryPath)
245286
if (!country?.visitors) {
@@ -256,48 +297,25 @@ function colorInCountriesWithValues(
256297
})
257298
}
258299

300+
function drawHighlightedCountryOutline(element: SVGSVGElement) {
301+
return d3.select(element).append('path').attr('class', initialOutlineClass)
302+
}
303+
259304
/** @returns the d3 selected svg element */
260-
function drawInteractiveCountries(
261-
element: SVGSVGElement,
262-
setTooltip: React.Dispatch<
263-
React.SetStateAction<{
264-
x: number
265-
y: number
266-
hoveredCountryAlpha3Code: string | null
267-
}>
268-
>
269-
) {
305+
function drawInteractiveCountries(element: SVGSVGElement) {
270306
const path = setupProjetionPath()
271307
const data = parseWorldTopoJsonToGeoJsonFeatures()
272308
const svg = d3.select(element)
273309

274-
svg
275-
.selectAll('path')
310+
const countriesSelection = svg
311+
.selectAll(countrySelector)
276312
.data(data)
277313
.enter()
278314
.append('path')
279315
.attr('class', countryClass)
280316
.attr('d', path as never)
281317

282-
.on('mouseover', function (event, country) {
283-
const [x, y] = d3.pointer(event, svg.node()?.parentNode)
284-
setTooltip({ x, y, hoveredCountryAlpha3Code: country.properties.a3 })
285-
// brings country to front
286-
this.parentNode?.appendChild(this)
287-
d3.select(this).attr('class', highlightedCountryClass)
288-
})
289-
290-
.on('mousemove', function (event) {
291-
const [x, y] = d3.pointer(event, svg.node()?.parentNode)
292-
setTooltip((currentState) => ({ ...currentState, x, y }))
293-
})
294-
295-
.on('mouseout', function () {
296-
setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null })
297-
d3.select(this).attr('class', countryClass)
298-
})
299-
300-
return svg
318+
return { svg, countriesSelection }
301319
}
302320

303321
function setupProjetionPath() {

0 commit comments

Comments
 (0)