@@ -1025,27 +1025,34 @@ function showLandingView() {
10251025async function showCityView ( slug ) {
10261026 const city = CITIES_BY_SLUG . get ( slug ) ;
10271027 currentCity = city ;
1028-
10291028 document . getElementById ( "view-landing" ) . style . display = "none" ;
10301029 document . getElementById ( "view-city" ) . style . display = "" ;
10311030 document . getElementById ( "view-city" ) . classList . add ( "active" ) ;
10321031 document . getElementById ( "view-stats" ) . style . display = "none" ;
10331032 document . getElementById ( "back-btn" ) . style . display = "inline-block" ;
10341033 document . getElementById ( "header-sub" ) . textContent = city . name ;
1035-
10361034 if ( window . innerWidth > 1024 ) {
10371035 document . getElementById ( "view-city" ) . style . gridTemplateColumns = `var(--sidebar-w) 1fr 8px 1fr` ;
10381036 }
1037+
1038+ // ── Reset state and tear down the previous city IMMEDIATELY (before any await).
10391039 features = [ ] ; mapFeatures = [ ] ; cartogramFeatures = null ; viewMode = "map" ;
10401040 selectedId = null ; hoveredId = null ;
1041+
1042+ if ( cityLeaflet ) {
1043+ cityLeaflet . remove ( ) ;
1044+ cityLeaflet = null ;
1045+ }
1046+ if ( canvas && ctx ) {
1047+ ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
1048+ }
1049+
10411050 document . getElementById ( "view-city" ) . classList . remove ( "side-closed" ) ;
10421051 citySideToggle . textContent = "‹" ;
1043- // Reset filter
10441052 cdiMinSlider . value = - 1 ; cdiMaxSlider . value = 1 ; updateRangeUI ( ) ;
10451053 updateInfoBox ( null ) ;
10461054 updateCityStats ( ) ;
10471055 syncViewToggleUI ( ) ;
1048-
10491056 document . querySelectorAll ( ".mobile-tab" ) . forEach ( ( b ) => b . classList . remove ( "active" ) ) ;
10501057 document . querySelector ( '.mobile-tab[data-panel="map"]' ) ?. classList . add ( "active" ) ;
10511058 if ( window . innerWidth <= 1024 ) {
@@ -1054,38 +1061,81 @@ async function showCityView(slug) {
10541061 document . getElementById ( "city-sidebar" ) . style . display = "none" ;
10551062 }
10561063
1064+ document . getElementById ( "info-box" ) . innerHTML =
1065+ `<p style="color:var(--muted);font-size:.8rem">Loading ${ city . name } …</p>` ;
1066+
1067+ // ── Yield once so the cleared state paints, then build the empty Leaflet map
1068+ // centered on the city's known coordinates BEFORE the fetch starts.
1069+ await new Promise ( ( r ) => requestAnimationFrame ( r ) ) ;
1070+ initCityMap ( ) ;
1071+ cityLeaflet . invalidateSize ( { animate : false } ) ;
1072+ if ( city . center && Number . isFinite ( city . zoom ) ) {
1073+ cityLeaflet . setView ( city . center , city . zoom , { animate : false } ) ;
1074+ }
1075+
1076+ // Show the loading overlay over the (now-centered, empty) map.
1077+ showMapLoading ( true ) ;
1078+
1079+ const loadStartedFor = slug ;
10571080 const loaded = await loadCityFeatures ( slug ) ;
1081+
1082+ // Bail out if the user clicked another city while this one was loading.
1083+ if ( currentCity ?. slug !== loadStartedFor ) {
1084+ showMapLoading ( false ) ;
1085+ return ;
1086+ }
1087+
10581088 if ( ! loaded ) {
1089+ showMapLoading ( false ) ;
10591090 document . getElementById ( "info-box" ) . innerHTML =
10601091 `<p style="color:#b03a2e">Could not load data for ${ city . name } .</p>` ;
10611092 return ;
10621093 }
1094+ // Data is here — hide the spinner now, even before rendering kicks in.
1095+ // For very large cities (Paris FUA), drawCanvas() can take seconds; we don't
1096+ // want the spinner to obscure the map while hexes are progressively painting.
1097+ showMapLoading ( false ) ;
10631098
1064- await new Promise ( ( r ) => requestAnimationFrame ( ( ) => requestAnimationFrame ( r ) ) ) ;
1065- initCityMap ( ) ;
10661099 if ( window . innerWidth <= 1024 ) {
10671100 document . getElementById ( "map-panel" ) . style . display = "block" ;
10681101 await new Promise ( ( r ) => requestAnimationFrame ( ( ) => requestAnimationFrame ( r ) ) ) ;
1102+ cityLeaflet . invalidateSize ( { animate : false } ) ;
10691103 }
1070- cityLeaflet . invalidateSize ( { animate : false } ) ;
1104+
10711105 mapFeatures = loaded ;
10721106 features = mapFeatures ;
10731107
10741108 // Prefetch the cartogram in the background so the toggle is instant.
1075- // Don't await — the geographic view should render immediately.
10761109 loadCartogramFeatures ( slug ) . then ( ( cg ) => {
1077- if ( currentCity ?. slug !== slug ) return ; // user moved on
1078- cartogramFeatures = cg ; // array on success, false if missing/failed
1110+ if ( currentCity ?. slug !== slug ) return ;
1111+ cartogramFeatures = cg ;
10791112 syncViewToggleUI ( ) ;
10801113 } ) ;
10811114
1115+ // Now that data is here, refit precisely to the hex bounds.
10821116 const lngs = features . flatMap ( ( f ) => f . geometry . coordinates . flatMap ( ( r ) => r . map ( ( p ) => p [ 0 ] ) ) ) ;
10831117 const lats = features . flatMap ( ( f ) => f . geometry . coordinates . flatMap ( ( r ) => r . map ( ( p ) => p [ 1 ] ) ) ) ;
10841118 cityLeaflet . fitBounds ( [ [ Math . min ( ...lats ) , Math . min ( ...lngs ) ] , [ Math . max ( ...lats ) , Math . max ( ...lngs ) ] ] ,
10851119 { padding : [ 10 , 10 ] , animate : false } ) ;
1086-
10871120 resizeCanvas ( ) ; drawCanvas ( ) ; buildScatter ( ) ; updateCityStats ( ) ;
10881121 syncViewToggleUI ( ) ;
1122+ updateInfoBox ( null ) ;
1123+
1124+ }
1125+
1126+ function showMapLoading ( on ) {
1127+ let el = document . getElementById ( "map-loading" ) ;
1128+ if ( on ) {
1129+ if ( ! el ) {
1130+ el = document . createElement ( "div" ) ;
1131+ el . id = "map-loading" ;
1132+ el . innerHTML = `<div class="map-loading-spinner"></div><span>${ t ( "loading" ) || "Loading…" } </span>` ;
1133+ document . getElementById ( "map-panel" ) . appendChild ( el ) ;
1134+ }
1135+ el . style . display = "flex" ;
1136+ } else if ( el ) {
1137+ el . style . display = "none" ;
1138+ }
10891139}
10901140
10911141// ─────────────────────────────────────────────────────────────────────────────
0 commit comments