190190 .region-map-wrap { width : 100% ; overflow : hidden; min-height : 280px ; }
191191 .region-map-wrap svg { display : block; width : 100% ; height : auto; }
192192 .region-map-wrap .region {
193- stroke : # fff ;
194- stroke-width : 0.6 px ;
193+ stroke : # 000 ;
194+ stroke-width : 0.2 px ;
195195 transition : fill 0.12s ;
196196 }
197+ .region-map-wrap .world-country {
198+ fill : # d3d3d3 ;
199+ stroke : # 000 ;
200+ stroke-width : 0.15px ;
201+ }
197202 .region-map-legend {
198203 display : flex;
199204 flex-wrap : wrap;
210215 height : 10px ;
211216 border-radius : 3px ;
212217 border : 1px solid var (--border );
213- background : linear-gradient (to right, # 440154 , # 31688e , # 35b779 , # fde725 );
218+ background : linear-gradient (to right, # f7fbff , # c6dbef , # 6baed6 , # 2171b5 , # 08306b );
214219 }
215220 .map-year-controls {
216221 display : flex;
@@ -345,22 +350,43 @@ <h3>Drill-down plots</h3>
345350 let mapYearSelection = MAP_YEAR_MEAN ;
346351 let mapFeaturesCache = null ;
347352 let mapFeaturesCacheCountry = null ;
353+ let mapWorldFeaturesCache = null ;
348354
349355 const HAS_DYNAMIC_MAPS = Boolean (
350356 MAP_DATA && MAP_DATA . datasets && Object . keys ( MAP_DATA . datasets ) . length
351357 && MAP_DATA . geojson_by_country && Object . keys ( MAP_DATA . geojson_by_country ) . length
352358 ) ;
353359 const geoCache = new Map ( ) ;
360+ const worldGeoCache = { href : null , features : null } ;
354361 const REGION_MAP_WIDTH = 520 ;
355362 const REGION_MAP_HEIGHT = 420 ;
356363 const REGION_MAP_MARGIN = 10 ;
357- const REGION_FILL_MISSING = "#f0f0f0 " ;
364+ const REGION_FILL_MISSING = "#d3d3d3 " ;
358365
359366 function fmtMap ( v , digits = 2 ) {
360367 if ( v === null || v === undefined || Number . isNaN ( v ) ) return "—" ;
361368 return Number ( v ) . toFixed ( digits ) ;
362369 }
363370
371+ async function loadWorldGeojson ( ) {
372+ const href = MAP_DATA . world_geojson_href ;
373+ if ( ! href ) return null ;
374+ if ( worldGeoCache . href === href && worldGeoCache . features ) {
375+ return worldGeoCache . features ;
376+ }
377+ try {
378+ const geo = await d3 . json ( href ) ;
379+ if ( ! geo || ! geo . features ) return null ;
380+ const features = geo . features . map ( f => rewindFeatureForSvg ( { ...f } ) ) ;
381+ worldGeoCache . href = href ;
382+ worldGeoCache . features = features ;
383+ return features ;
384+ } catch ( err ) {
385+ console . warn ( "World GeoJSON load failed:" , href , err ) ;
386+ return null ;
387+ }
388+ }
389+
364390 async function loadCountryGeojson ( country ) {
365391 if ( geoCache . has ( country ) ) return geoCache . get ( country ) ;
366392 const href = ( MAP_DATA . geojson_by_country || { } ) [ country ] ;
@@ -422,22 +448,17 @@ <h3>Drill-down plots</h3>
422448 } ;
423449 }
424450
425- function featuresForDataset ( features , ...valueMaps ) {
426- const locs = new Set ( ) ;
427- for ( const m of valueMaps ) {
428- if ( ! m ) continue ;
429- Object . keys ( m ) . forEach ( k => locs . add ( k ) ) ;
430- }
431- if ( ! locs . size ) return features || [ ] ;
432- return ( features || [ ] ) . filter ( f => locs . has ( f . properties . loc ) ) ;
451+ function regionsWithData ( features , valueByLoc ) {
452+ if ( ! features || ! valueByLoc ) return [ ] ;
453+ return features . filter ( f => valueByLoc [ f . properties . loc ] != null ) ;
433454 }
434455
435456 function yieldColor ( value , extent ) {
436457 if ( value == null || ! extent ) return REGION_FILL_MISSING ;
437458 const [ lo , hi ] = extent ;
438- if ( hi <= lo ) return d3 . interpolateViridis ( 0.5 ) ;
459+ if ( hi <= lo ) return d3 . interpolateBlues ( 0.55 ) ;
439460 const t = Math . max ( 0 , Math . min ( 1 , ( value - lo ) / ( hi - lo ) ) ) ;
440- return d3 . interpolateViridis ( t ) ;
461+ return d3 . interpolateBlues ( 0.15 + t * 0.85 ) ;
441462 }
442463
443464 function sharedYieldExtent ( actualMap , predMap ) {
@@ -464,18 +485,22 @@ <h3>Drill-down plots</h3>
464485
465486 function renderRegionChoropleth (
466487 container ,
467- features ,
488+ regionFeatures ,
468489 valueByLoc ,
469490 extent ,
470491 title ,
492+ worldFeatures ,
471493 ) {
472494 container . innerHTML = "" ;
473- if ( ! features || ! features . length ) {
495+ if ( ! regionFeatures || ! regionFeatures . length ) {
474496 container . innerHTML = `<p class="muted">${ title } : no map geometry.</p>` ;
475497 return ;
476498 }
477- let fitFeatures = featuresForProjectionFit ( features ) ;
478- if ( ! fitFeatures . length ) fitFeatures = features ;
499+ const dataFeatures = regionsWithData ( regionFeatures , valueByLoc ) ;
500+ let fitFeatures = featuresForProjectionFit (
501+ dataFeatures . length ? dataFeatures : regionFeatures ,
502+ ) ;
503+ if ( ! fitFeatures . length ) fitFeatures = regionFeatures ;
479504 const svg = d3 . select ( container ) . append ( "svg" )
480505 . attr ( "viewBox" , `0 0 ${ REGION_MAP_WIDTH } ${ REGION_MAP_HEIGHT } ` )
481506 . attr ( "role" , "img" )
@@ -492,8 +517,15 @@ <h3>Drill-down plots</h3>
492517 . attr ( "width" , REGION_MAP_WIDTH )
493518 . attr ( "height" , REGION_MAP_HEIGHT )
494519 . attr ( "fill" , "#f8f9fa" ) ;
520+ if ( worldFeatures && worldFeatures . length ) {
521+ svg . selectAll ( "path.world-country" )
522+ . data ( worldFeatures )
523+ . join ( "path" )
524+ . attr ( "class" , "world-country" )
525+ . attr ( "d" , path ) ;
526+ }
495527 svg . selectAll ( "path.region" )
496- . data ( features )
528+ . data ( regionFeatures )
497529 . join ( "path" )
498530 . attr ( "class" , "region" )
499531 . attr ( "d" , path )
@@ -502,15 +534,15 @@ <h3>Drill-down plots</h3>
502534 const v = valueByLoc ? valueByLoc [ loc ] : null ;
503535 return yieldColor ( v , extent ) ;
504536 } )
505- . attr ( "stroke" , "#fff" )
506- . attr ( "stroke-width" , 0.5 )
507537 . selectAll ( "title" )
508538 . data ( d => [ d ] )
509539 . join ( "title" )
510540 . text ( d => {
511541 const loc = d . properties . loc ;
512542 const v = valueByLoc ? valueByLoc [ loc ] : null ;
513- return v == null ? loc : `${ loc } : ${ fmtMap ( v ) } ` ;
543+ return v == null
544+ ? `${ loc } : outside benchmark`
545+ : `${ loc } : ${ fmtMap ( v ) } ` ;
514546 } ) ;
515547 }
516548
@@ -571,8 +603,10 @@ <h3>Drill-down plots</h3>
571603 const slice = ( MAP_DATA . datasets || { } ) [ dataset ] ;
572604 if ( ! slice || ! mapFeaturesCache ) return false ;
573605 const { actual, pred } = mapValuesForSelection ( slice , model , yearKey ) ;
574- const features = featuresForDataset ( mapFeaturesCache , actual , pred ) ;
575- if ( ! features . length ) return false ;
606+ if ( ! regionsWithData ( mapFeaturesCache , actual ) . length
607+ && ! regionsWithData ( mapFeaturesCache , pred ) . length ) {
608+ return false ;
609+ }
576610 const extent = pooledMapExtent ( slice , model ) ;
577611 const yearSuffix = yearKey === MAP_YEAR_MEAN ? "mean yield" : yearKey ;
578612 if ( mapCaptionLeftDyn ) {
@@ -583,17 +617,19 @@ <h3>Drill-down plots</h3>
583617 }
584618 renderRegionChoropleth (
585619 regionMapActual ,
586- features ,
620+ mapFeaturesCache ,
587621 actual ,
588622 extent ,
589623 `Ground truth (${ yearSuffix } )` ,
624+ mapWorldFeaturesCache ,
590625 ) ;
591626 renderRegionChoropleth (
592627 regionMapPred ,
593- features ,
628+ mapFeaturesCache ,
594629 pred ,
595630 extent ,
596631 `Model prediction (${ model } , ${ yearSuffix } )` ,
632+ mapWorldFeaturesCache ,
597633 ) ;
598634 if ( extent ) {
599635 const label = `${ fmtMap ( extent [ 0 ] ) } → ${ fmtMap ( extent [ 1 ] ) } ` ;
@@ -610,7 +646,10 @@ <h3>Drill-down plots</h3>
610646 setupMapYearSlider ( slice ) ;
611647 if ( mapFeaturesCacheCountry !== country ) {
612648 mapFeaturesCache = await loadCountryGeojson ( country ) ;
649+ mapWorldFeaturesCache = await loadWorldGeojson ( ) ;
613650 mapFeaturesCacheCountry = country ;
651+ } else if ( ! mapWorldFeaturesCache ) {
652+ mapWorldFeaturesCache = await loadWorldGeojson ( ) ;
614653 }
615654 if ( ! mapFeaturesCache ) return false ;
616655 return renderDynamicMapLayers ( dataset , model , mapYearSelection ) ;
0 commit comments