@@ -2011,6 +2011,7 @@ let currentMapGroup = "all"; // Currently selected filter group
20112011let d3MapSvg = null ; // D3 SVG element
20122012let d3MapPath = null ; // D3 geo path generator
20132013let stateDataLookup = { } ; // Quick lookup: state_fips -> data for current group
2014+ let renderableFipsSet = new Set ( ) ; // Set of FIPS codes that can be rendered on map
20142015let currentColorScaleMin = 0 ; // Current color scale min (dynamic per group)
20152016let currentColorScaleMax = 1 ; // Current color scale max (dynamic per group)
20162017
@@ -2144,10 +2145,6 @@ function buildStateDataLookup(group) {
21442145 hasRecord : true // Flag to indicate record exists in data
21452146 } ;
21462147 } ) ;
2147-
2148- // Debug: Log Nevada (state_fips="32") data for verification
2149- const nevadaData = stateDataLookup [ "32" ] ;
2150- console . log ( `[Map Debug] Nevada (32) for group "${ group } ":` , nevadaData ) ;
21512148
21522149 console . log ( `[Map] Built lookup for group "${ group } ": ${ Object . keys ( stateDataLookup ) . length } states` ) ;
21532150
@@ -2225,6 +2222,9 @@ function initializeD3Map() {
22252222 // Add a group for states
22262223 d3MapSvg . append ( "g" ) . attr ( "class" , "states" ) ;
22272224
2225+ // Add a group for Puerto Rico inset
2226+ d3MapSvg . append ( "g" ) . attr ( "class" , "puerto-rico-inset" ) ;
2227+
22282228 console . log ( `[Map] D3 SVG initialized: ${ width } x${ height } ` ) ;
22292229}
22302230
@@ -2245,9 +2245,19 @@ function renderStateMap() {
22452245 // Build lookup for current group (just indexing, no computation)
22462246 buildStateDataLookup ( currentMapGroup ) ;
22472247
2248- // Helper function to get fill color for a state
2248+ // Filter out features that can't be rendered by geoAlbersUsa projection
2249+ // This includes Puerto Rico (72) and other non-continental territories
2250+ const renderableFeatures = stateGeoData . features . filter ( f => {
2251+ const path = d3MapPath ( f ) ;
2252+ return path && path . length > 0 ;
2253+ } ) ;
2254+
2255+ // Update global set of renderable FIPS codes for statistics
2256+ renderableFipsSet = new Set ( renderableFeatures . map ( f => String ( f . id ) . padStart ( 2 , '0' ) ) ) ;
2257+
2258+ // Helper function to get fill color for a state (works for both main map and inset)
22492259 const getFillColor = ( d ) => {
2250- const fips = String ( d . id ) . padStart ( 2 , '0' ) ;
2260+ const fips = typeof d === 'object' ? String ( d . id ) . padStart ( 2 , '0' ) : d ;
22512261 const data = stateDataLookup [ fips ] ;
22522262
22532263 if ( ! data ) return "#dfe6e9" ;
@@ -2257,16 +2267,23 @@ function renderStateMap() {
22572267 return "#dfe6e9" ;
22582268 } ;
22592269
2260- // Helper to check low reliability
2270+ // Helper to check low reliability (n < 50) - works for both feature objects and FIPS strings
22612271 const isLowReliability = ( d ) => {
2262- const fips = String ( d . id ) . padStart ( 2 , '0' ) ;
2272+ const fips = typeof d === 'object' ? String ( d . id ) . padStart ( 2 , '0' ) : d ;
22632273 const data = stateDataLookup [ fips ] ;
22642274 return data && typeof data . n === 'number' && data . n > 0 && data . n < 50 ;
22652275 } ;
22662276
2267- // Bind data and render states
2277+ // Helper to check if a state is small (DC, Rhode Island, etc.) - needs larger stroke
2278+ const isSmallState = ( d ) => {
2279+ const fips = typeof d === 'object' ? String ( d . id ) . padStart ( 2 , '0' ) : d ;
2280+ // DC (11), Rhode Island (44), Delaware (10) are very small on the map
2281+ return [ '11' , '44' , '10' ] . includes ( fips ) ;
2282+ } ;
2283+
2284+ // Bind data and render states (only renderable features)
22682285 const states = statesGroup . selectAll ( "path" )
2269- . data ( stateGeoData . features , d => d . id ) ;
2286+ . data ( renderableFeatures , d => d . id ) ;
22702287
22712288 // Enter: New states (first render)
22722289 const statesEnter = states . enter ( )
@@ -2279,14 +2296,20 @@ function renderStateMap() {
22792296 . style ( "cursor" , "pointer" ) ;
22802297
22812298 // Merge enter + update and apply transitions
2299+ // For small states like DC, use thicker stroke to make dashed border visible
22822300 statesEnter . merge ( states )
22832301 . transition ( )
22842302 . duration ( MAP_TRANSITION_DURATION )
22852303 . ease ( d3 . easeCubicInOut )
22862304 . attr ( "fill" , getFillColor )
22872305 . attr ( "stroke" , d => isLowReliability ( d ) ? "#e67e22" : "#fff" )
2288- . attr ( "stroke-width" , d => isLowReliability ( d ) ? 1.5 : 1 )
2289- . attr ( "stroke-dasharray" , d => isLowReliability ( d ) ? "4,2" : "none" ) ;
2306+ . attr ( "stroke-width" , d => {
2307+ if ( isLowReliability ( d ) ) {
2308+ return isSmallState ( d ) ? 3.5 : 2.5 ; // Thicker for small states
2309+ }
2310+ return 1 ;
2311+ } )
2312+ . attr ( "stroke-dasharray" , d => isLowReliability ( d ) ? "8,4" : "none" ) ;
22902313
22912314 // Add event handlers (only need to set once on enter, but merge ensures all have them)
22922315 statesEnter . merge ( states )
@@ -2295,19 +2318,15 @@ function renderStateMap() {
22952318 const data = stateDataLookup [ fips ] ;
22962319 const stateName = stateFipsToName [ fips ] || d . properties ?. name || "Unknown State" ;
22972320
2298- // Debug: Log Nevada data when hovering
2299- if ( fips === "32" ) {
2300- console . log ( `[Map Debug] Tooltip for Nevada (32), group="${ currentMapGroup } ":` , JSON . stringify ( data ) ) ;
2301- }
2302-
23032321 // Highlight state (preserve dash for low reliability: n < 50)
23042322 const lowReliability = isLowReliability ( d ) ;
2323+ const smallState = isSmallState ( d ) ;
23052324 d3 . select ( this )
23062325 . transition ( )
23072326 . duration ( 150 )
23082327 . attr ( "stroke" , "#2c3e50" )
2309- . attr ( "stroke-width" , 2 )
2310- . attr ( "stroke-dasharray" , lowReliability ? "4,2 " : "none" ) ;
2328+ . attr ( "stroke-width" , smallState ? 4 : 3 )
2329+ . attr ( "stroke-dasharray" , lowReliability ? "8,4 " : "none" ) ;
23112330
23122331 // Build tooltip content
23132332 let tooltipHtml = `<strong>${ stateName } </strong>` ;
@@ -2365,34 +2384,165 @@ function renderStateMap() {
23652384 } )
23662385 . on ( "mouseleave" , function ( event , d ) {
23672386 const lowReliability = isLowReliability ( d ) ;
2387+ const smallState = isSmallState ( d ) ;
23682388 d3 . select ( this )
23692389 . transition ( )
23702390 . duration ( 150 )
23712391 . attr ( "stroke" , lowReliability ? "#e67e22" : "#fff" )
2372- . attr ( "stroke-width" , lowReliability ? 1.5 : 1 )
2373- . attr ( "stroke-dasharray" , lowReliability ? "4,2 " : "none" ) ;
2392+ . attr ( "stroke-width" , lowReliability ? ( smallState ? 3.5 : 2.5 ) : 1 )
2393+ . attr ( "stroke-dasharray" , lowReliability ? "8,4 " : "none" ) ;
23742394 tooltip . style . display = "none" ;
23752395 } ) ;
23762396
23772397 // Remove old states
23782398 states . exit ( ) . remove ( ) ;
23792399
2400+ // Render Puerto Rico inset (not supported by geoAlbersUsa projection)
2401+ renderPuertoRicoInset ( tooltip , getFillColor , isLowReliability ) ;
2402+
23802403 // Update statistics display
23812404 updateMapStatistics ( ) ;
23822405}
23832406
2407+ /**
2408+ * Render Puerto Rico as an inset in the bottom right corner
2409+ * Since geoAlbersUsa doesn't include PR, we render it separately
2410+ */
2411+ function renderPuertoRicoInset ( tooltip , getFillColor , isLowReliability ) {
2412+ const prFips = "72" ;
2413+ const prData = stateDataLookup [ prFips ] ;
2414+
2415+ // Only render if we have data for PR
2416+ if ( ! prData ) {
2417+ console . log ( "[Map] No data for Puerto Rico, skipping inset" ) ;
2418+ return ;
2419+ }
2420+
2421+ const insetGroup = d3MapSvg . select ( "g.puerto-rico-inset" ) ;
2422+
2423+ // Get SVG dimensions
2424+ const svgWidth = + d3MapSvg . attr ( "width" ) ;
2425+ const svgHeight = + d3MapSvg . attr ( "height" ) ;
2426+
2427+ // Inset position and size (bottom left, to avoid legend overlay)
2428+ const insetWidth = 60 ;
2429+ const insetHeight = 30 ;
2430+ const insetX = 20 ; // Position at bottom left
2431+ const insetY = svgHeight - 60 ;
2432+
2433+ // Clear previous content
2434+ insetGroup . selectAll ( "*" ) . remove ( ) ;
2435+
2436+ // Add background/border for the inset box
2437+ insetGroup . append ( "rect" )
2438+ . attr ( "x" , insetX - 5 )
2439+ . attr ( "y" , insetY - 5 )
2440+ . attr ( "width" , insetWidth + 10 )
2441+ . attr ( "height" , insetHeight + 20 )
2442+ . attr ( "fill" , "#f8fafc" )
2443+ . attr ( "stroke" , "#e2e8f0" )
2444+ . attr ( "stroke-width" , 1 )
2445+ . attr ( "rx" , 3 ) ;
2446+
2447+ // Add label
2448+ insetGroup . append ( "text" )
2449+ . attr ( "x" , insetX + insetWidth / 2 )
2450+ . attr ( "y" , insetY + insetHeight + 12 )
2451+ . attr ( "text-anchor" , "middle" )
2452+ . attr ( "font-size" , "8px" )
2453+ . attr ( "fill" , "#64748b" )
2454+ . text ( "Puerto Rico" ) ;
2455+
2456+ // Determine fill color and stroke style
2457+ const fillColor = getFillColor ( prFips ) ;
2458+ const lowReliability = isLowReliability ( prFips ) ;
2459+
2460+ // Draw PR as a simple rectangle (stylized representation)
2461+ const prRect = insetGroup . append ( "rect" )
2462+ . attr ( "class" , "puerto-rico" )
2463+ . attr ( "x" , insetX )
2464+ . attr ( "y" , insetY )
2465+ . attr ( "width" , insetWidth )
2466+ . attr ( "height" , insetHeight )
2467+ . attr ( "rx" , 2 )
2468+ . attr ( "fill" , fillColor )
2469+ . attr ( "stroke" , lowReliability ? "#e67e22" : "#fff" )
2470+ . attr ( "stroke-width" , lowReliability ? 2.5 : 1 )
2471+ . attr ( "stroke-dasharray" , lowReliability ? "6,3" : "none" )
2472+ . style ( "cursor" , "pointer" ) ;
2473+
2474+ // Add hover interactions
2475+ prRect . on ( "mouseenter" , function ( event ) {
2476+ d3 . select ( this )
2477+ . transition ( )
2478+ . duration ( 150 )
2479+ . attr ( "stroke" , "#2c3e50" )
2480+ . attr ( "stroke-width" , 3 ) ;
2481+
2482+ // Show tooltip
2483+ let tooltipHtml = `<strong>Puerto Rico</strong>` ;
2484+ tooltipHtml += `<br/><span style="color: #7f8c8d; font-size: 11px;">FIPS: ${ prFips } </span>` ;
2485+ tooltipHtml += `<hr style="margin: 8px 0; border: none; border-top: 1px solid #ecf0f1;">` ;
2486+
2487+ if ( typeof prData . avg_zs === 'number' && ! isNaN ( prData . avg_zs ) ) {
2488+ tooltipHtml += `<span style="font-size: 18px; font-weight: 600; color: #2c3e50;">${ prData . avg_zs . toFixed ( 2 ) } </span>` ;
2489+ tooltipHtml += `<br/><span style="color: #7f8c8d; font-size: 10px;">Zero-Sum Index</span>` ;
2490+ }
2491+
2492+ tooltipHtml += `<hr style="margin: 8px 0; border: none; border-top: 1px solid #ecf0f1;">` ;
2493+ tooltipHtml += `<span>Sample Size (n):</span>` ;
2494+ tooltipHtml += ` <strong>${ prData . n } </strong>` ;
2495+ if ( lowReliability ) {
2496+ tooltipHtml += ` <span style="color: #e67e22;">⚠️ Low reliability</span>` ;
2497+ }
2498+
2499+ tooltip . innerHTML = tooltipHtml ;
2500+ tooltip . style . display = "block" ;
2501+ } )
2502+ . on ( "mousemove" , function ( event ) {
2503+ const container = document . getElementById ( "d3-map-container" ) ;
2504+ const rect = container . getBoundingClientRect ( ) ;
2505+ const x = event . clientX - rect . left + 15 ;
2506+ const y = event . clientY - rect . top + 15 ;
2507+
2508+ const tooltipRect = tooltip . getBoundingClientRect ( ) ;
2509+ const maxX = rect . width - tooltipRect . width - 10 ;
2510+ const maxY = rect . height - tooltipRect . height - 10 ;
2511+
2512+ tooltip . style . left = Math . min ( x , maxX ) + "px" ;
2513+ tooltip . style . top = Math . min ( y , maxY ) + "px" ;
2514+ } )
2515+ . on ( "mouseleave" , function ( ) {
2516+ d3 . select ( this )
2517+ . transition ( )
2518+ . duration ( 150 )
2519+ . attr ( "stroke" , lowReliability ? "#e67e22" : "#fff" )
2520+ . attr ( "stroke-width" , lowReliability ? 2.5 : 1 ) ;
2521+ tooltip . style . display = "none" ;
2522+ } ) ;
2523+
2524+ console . log ( `[Map] Puerto Rico inset rendered: avg_zs=${ prData . avg_zs } , n=${ prData . n } , lowReliability=${ lowReliability } ` ) ;
2525+ }
2526+
23842527/**
23852528 * Update the statistics panel with current data
2386- * Just displays the preprocessed values, no computation
2529+ * Now includes Puerto Rico in the count since we render it as an inset
23872530 */
23882531function updateMapStatistics ( ) {
2389- const dataPoints = Object . values ( stateDataLookup ) ;
2532+ // Include Puerto Rico (72) in statistics since we now render it as an inset
2533+ const allDataFips = new Set ( [ ...renderableFipsSet , "72" ] ) ;
2534+ const renderableDataPoints = Object . entries ( stateDataLookup )
2535+ . filter ( ( [ fips , _ ] ) => allDataFips . has ( fips ) )
2536+ . map ( ( [ fips , data ] ) => ( { fips, ...data } ) ) ;
2537+
23902538 // Valid data = avg_zs is a number (record exists and has value)
2391- const validData = dataPoints . filter ( d => typeof d . avg_zs === 'number' && ! isNaN ( d . avg_zs ) ) ;
2539+ const validData = renderableDataPoints . filter ( d => typeof d . avg_zs === 'number' && ! isNaN ( d . avg_zs ) ) ;
23922540 // Low reliability = record exists AND 0 < n < 50
2393- const lowReliabilityCount = dataPoints . filter ( d => typeof d . n === 'number' && d . n > 0 && d . n < 50 ) . length ;
2541+ const lowReliabilityData = renderableDataPoints . filter ( d => typeof d . n === 'number' && d . n > 0 && d . n < 50 ) ;
2542+ const lowReliabilityCount = lowReliabilityData . length ;
2543+
23942544 // No data count for states in lookup but missing avg_zs
2395- const noDataCount = dataPoints . length - validData . length ;
2545+ const noDataCount = renderableDataPoints . length - validData . length ;
23962546
23972547 const countEl = document . getElementById ( "map-state-count" ) ;
23982548 const meanEl = document . getElementById ( "map-mean" ) ;
0 commit comments