@@ -213,20 +213,65 @@ const countryCentroids = {
213213 LI : [ 9.5554 , 47.1660 ] , // Liechtenstein
214214} ;
215215
216- const getCountryCentroid = ( countryCode ) => countryCentroids [ countryCode ] || [ 0 , 0 ] ;
216+ const getCountryCentroid = ( countryCode ) => {
217+ if ( ! countryCode ) return null ;
218+
219+ // Normalize country code to uppercase
220+ const normalizedCode = countryCode . toUpperCase ( ) ;
221+
222+ // Direct lookup
223+ if ( countryCentroids [ normalizedCode ] ) {
224+ return countryCentroids [ normalizedCode ] ;
225+ }
226+
227+ // Handle common variations
228+ const codeVariations = {
229+ 'GERMANY' : 'DE' ,
230+ 'DEUTSCHLAND' : 'DE' ,
231+ 'UNITED STATES' : 'US' ,
232+ 'USA' : 'US' ,
233+ 'UNITED KINGDOM' : 'GB' ,
234+ 'UK' : 'GB' ,
235+ 'CHINA' : 'CN' ,
236+ 'JAPAN' : 'JP' ,
237+ 'FRANCE' : 'FR' ,
238+ 'ITALY' : 'IT' ,
239+ 'SPAIN' : 'ES' ,
240+ 'NETHERLANDS' : 'NL' ,
241+ 'SWEDEN' : 'SE' ,
242+ 'SWITZERLAND' : 'CH' ,
243+ 'CANADA' : 'CA' ,
244+ 'AUSTRALIA' : 'AU'
245+ } ;
246+
247+ // Try variations
248+ if ( codeVariations [ normalizedCode ] ) {
249+ return countryCentroids [ codeVariations [ normalizedCode ] ] ;
250+ }
251+
252+ // Try partial matches
253+ for ( const [ variation , code ] of Object . entries ( codeVariations ) ) {
254+ if ( normalizedCode . includes ( variation ) || variation . includes ( normalizedCode ) ) {
255+ return countryCentroids [ code ] ;
256+ }
257+ }
258+
259+ return null ;
260+ } ;
217261
218262const OPENALEX_API_BASE = 'https://api.openalex.org' ;
219263
220264const WorldMapPapers = ( { searchQuery, onPaperSelect, onApiCallsUpdate, triggerSearch = false , searchResults = null } ) => {
221265 const [ papers , setPapers ] = useState ( [ ] ) ;
222266 const [ loading , setLoading ] = useState ( false ) ;
223- const [ tooltipContent , setTooltipContent ] = useState ( '' ) ;
224- const [ mapError , setMapError ] = useState ( false ) ;
225267 const [ fetchError , setFetchError ] = useState ( null ) ;
268+ const [ mapError , setMapError ] = useState ( false ) ;
269+ const [ tooltipContent , setTooltipContent ] = useState ( null ) ;
270+ const [ zoom , setZoom ] = useState ( 1 ) ;
226271
227272 useEffect ( ( ) => {
228- // If search results are provided from parent, use those
229273 if ( searchResults && searchResults . length > 0 ) {
274+ // Process searchResults directly
230275 const mapped = searchResults . map ( ( work , idx ) => {
231276 // Try to get first author institution country and coordinates
232277 let country = null ;
@@ -264,24 +309,84 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
264309 institution : institution || null
265310 } ;
266311 } ) . filter ( Boolean ) ;
267- setPapers ( mapped ) ;
268- setLoading ( false ) ;
269- setFetchError ( null ) ;
270- } else if ( triggerSearch && searchQuery && searchQuery . trim ( ) . length > 0 ) {
271- // Fallback to own API call if no results provided
272- fetchPapersByQuery ( searchQuery . trim ( ) ) ;
273- } else if ( ! triggerSearch ) {
274- // Reset to empty state when not searching
312+
313+ // Ensure all countries from leadership analysis have markers
314+ const countriesWithMarkers = new Set ( mapped . map ( p => p . country ) ) ;
315+ const additionalMarkers = [ ] ;
316+
317+ // Get all unique countries from the search results
318+ const allCountriesInData = new Set ( ) ;
319+ searchResults . forEach ( ( work ) => {
320+ let country = null ;
321+ if ( work . authorships && work . authorships . length > 0 ) {
322+ const firstAuth = work . authorships [ 0 ] ;
323+ if ( firstAuth . institutions && firstAuth . institutions . length > 0 ) {
324+ const inst = firstAuth . institutions [ 0 ] ;
325+ country = inst . country_code || inst . country || null ;
326+ }
327+ }
328+ if ( ! country && work . country_code ) {
329+ country = work . country_code ;
330+ }
331+ if ( country ) {
332+ allCountriesInData . add ( country ) ;
333+ }
334+ } ) ;
335+
336+ // Create markers for ALL countries that appear in the data
337+ allCountriesInData . forEach ( ( country ) => {
338+ const coordinates = getCountryCentroid ( country ) ;
339+ if ( coordinates ) {
340+ // If country doesn't have any markers yet, create one
341+ if ( ! countriesWithMarkers . has ( country ) ) {
342+ additionalMarkers . push ( {
343+ id : `additional-${ country } ` ,
344+ title : `Research papers from ${ country } ` ,
345+ authors : [ ] ,
346+ citations : 0 ,
347+ country,
348+ coordinates,
349+ year : null ,
350+ institution : null ,
351+ isAdditional : true
352+ } ) ;
353+ countriesWithMarkers . add ( country ) ;
354+ }
355+ }
356+ } ) ;
357+ const allMarkers = [ ...mapped , ...additionalMarkers ] ;
358+ setPapers ( allMarkers ) ;
359+ } else if ( triggerSearch && searchQuery ) {
360+ fetchPapersByQuery ( searchQuery ) ;
361+ } else {
275362 setPapers ( [ ] ) ;
276- setFetchError ( null ) ;
277- setLoading ( false ) ;
278- // Update API calls for disclaimer
279- if ( onApiCallsUpdate ) {
280- onApiCallsUpdate ( [ ] ) ;
281- }
282363 }
283- // eslint-disable-next-line
284- } , [ triggerSearch , searchQuery , searchResults ] ) ;
364+ } , [ searchResults , triggerSearch , searchQuery ] ) ;
365+
366+ // Add global event listeners to prevent zoom
367+ useEffect ( ( ) => {
368+ const handleWheel = ( e ) => {
369+ if ( e . ctrlKey ) {
370+ e . preventDefault ( ) ; // Prevent zoom on ctrl + wheel
371+ }
372+ } ;
373+
374+ const handleGesture = ( e ) => {
375+ e . preventDefault ( ) ; // Prevent pinch zoom on Mac trackpad
376+ } ;
377+
378+ window . addEventListener ( 'wheel' , handleWheel , { passive : false } ) ;
379+ window . addEventListener ( 'gesturestart' , handleGesture , { passive : false } ) ;
380+ window . addEventListener ( 'gesturechange' , handleGesture , { passive : false } ) ;
381+ window . addEventListener ( 'gestureend' , handleGesture , { passive : false } ) ;
382+
383+ return ( ) => {
384+ window . removeEventListener ( 'wheel' , handleWheel ) ;
385+ window . removeEventListener ( 'gesturestart' , handleGesture ) ;
386+ window . removeEventListener ( 'gesturechange' , handleGesture ) ;
387+ window . removeEventListener ( 'gestureend' , handleGesture ) ;
388+ } ;
389+ } , [ ] ) ;
285390
286391 const fetchPapersByQuery = async ( query ) => {
287392 const trimmed = ( query || '' ) . trim ( ) ;
@@ -301,7 +406,7 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
301406 const filter = `title_and_abstract.search:"${ trimmed . replace ( / " / g, '\\"' ) } "` ;
302407 const params = new URLSearchParams ( {
303408 filter,
304- per_page : 20
409+ per_page : 100
305410 } ) ;
306411 const apiUrl = `${ OPENALEX_API_BASE } /works?${ params . toString ( ) } ` ;
307412
@@ -372,6 +477,7 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
372477
373478 // Helper to offset markers with the same coordinates
374479 function offsetMarkers ( papers ) {
480+
375481 // Group by coordinates as string
376482 const groups = { } ;
377483 papers . forEach ( ( paper ) => {
@@ -381,6 +487,7 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
381487 groups [ key ] . push ( paper ) ;
382488 }
383489 } ) ;
490+
384491 // Offset each group
385492 const R = 2.5 ; // increased degrees offset radius
386493 const result = [ ] ;
@@ -399,6 +506,7 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
399506 } ) ;
400507 }
401508 } ) ;
509+
402510 return result ;
403511 }
404512
@@ -447,6 +555,19 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
447555 setMapError ( true ) ;
448556 } ;
449557
558+ // Zoom control functions
559+ const handleZoomIn = ( ) => {
560+ setZoom ( prevZoom => Math . min ( prevZoom + 0.5 , 4 ) ) ;
561+ } ;
562+
563+ const handleZoomOut = ( ) => {
564+ setZoom ( prevZoom => Math . max ( prevZoom - 0.5 , 0.8 ) ) ;
565+ } ;
566+
567+ const handleZoomReset = ( ) => {
568+ setZoom ( 1 ) ;
569+ } ;
570+
450571 return (
451572 < div className = { styles . worldMapContainer } >
452573 < div className = { styles . header } >
@@ -520,7 +641,34 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
520641 </ button >
521642 </ div >
522643 ) : papers . length > 0 && (
523- < div className = { styles . mapContainer } >
644+ < div
645+ className = { styles . mapContainer }
646+ >
647+ { /* Zoom Controls */ }
648+ < div className = { styles . zoomControls } >
649+ < button
650+ className = { styles . zoomButton }
651+ onClick = { handleZoomIn }
652+ title = "Zoom In"
653+ >
654+ +
655+ </ button >
656+ < button
657+ className = { styles . zoomButton }
658+ onClick = { handleZoomOut }
659+ title = "Zoom Out"
660+ >
661+ −
662+ </ button >
663+ < button
664+ className = { styles . zoomButton }
665+ onClick = { handleZoomReset }
666+ title = "Reset Zoom"
667+ >
668+ ⌂
669+ </ button >
670+ </ div >
671+
524672 < ComposableMap
525673 projection = "geoEqualEarth"
526674 projectionConfig = { {
@@ -534,9 +682,11 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
534682 >
535683 < ZoomableGroup
536684 center = { [ 0 , 0 ] }
537- zoom = { 1 }
685+ zoom = { zoom }
538686 maxZoom = { 4 }
539687 minZoom = { 0.8 }
688+ disablePanning = { false }
689+ disableZooming = { true }
540690 >
541691 < Geographies
542692 geography = "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson"
@@ -559,23 +709,29 @@ const WorldMapPapers = ({ searchQuery, onPaperSelect, onApiCallsUpdate, triggerS
559709 ) ) ;
560710 } }
561711 </ Geographies >
562- { offsetMarkers ( papers . slice ( 0 , 20 ) ) . map ( ( paper ) => (
563- < Marker
564- key = { paper . id }
565- coordinates = { paper . coordinates }
566- onClick = { ( ) => handleMarkerClick ( paper ) }
567- onMouseEnter = { e => handleMarkerMouseEnter ( paper , e ) }
568- onMouseLeave = { handleMarkerMouseLeave }
569- >
570- < circle
571- r = { getMarkerSize ( paper . citations ) }
572- fill = { getMarkerColor ( paper . citations ) }
573- stroke = "#fff"
574- strokeWidth = { 2 }
575- className = { styles . marker }
576- />
577- </ Marker >
578- ) ) }
712+ { ( ( ) => {
713+ const markersToRender = offsetMarkers ( papers . slice ( 0 , 100 ) ) ;
714+
715+ return markersToRender . map ( ( paper ) => {
716+ return (
717+ < Marker
718+ key = { paper . id }
719+ coordinates = { paper . coordinates }
720+ onClick = { ( ) => handleMarkerClick ( paper ) }
721+ onMouseEnter = { e => handleMarkerMouseEnter ( paper , e ) }
722+ onMouseLeave = { handleMarkerMouseLeave }
723+ >
724+ < circle
725+ r = { getMarkerSize ( paper . citations ) }
726+ fill = { getMarkerColor ( paper . citations ) }
727+ stroke = "#fff"
728+ strokeWidth = { 2 }
729+ className = { styles . marker }
730+ />
731+ </ Marker >
732+ ) ;
733+ } ) ;
734+ } ) ( ) }
579735 </ ZoomableGroup >
580736 </ ComposableMap >
581737 </ div >
0 commit comments