@@ -20,6 +20,7 @@ import { apiPath } from '../../util/url'
2020import { MIN_HEIGHT } from '../reports/list'
2121import { MapTooltip } from './map-tooltip'
2222import { GeolocationNotice } from './geolocation-notice'
23+ import { DashboardQuery } from '../../query'
2324
2425const width = 475
2526const height = 335
@@ -32,6 +33,16 @@ type CountryData = {
3233}
3334type 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+
3546const 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
206243const 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
303321function setupProjetionPath ( ) {
0 commit comments