Skip to content

Commit 69a0d85

Browse files
Merge pull request #74 from code4policy/finaledits0119v2
Finalv2edition
2 parents 107761e + 9c44ddd commit 69a0d85

2 files changed

Lines changed: 186 additions & 65 deletions

File tree

app.js

Lines changed: 176 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,6 +2011,7 @@ let currentMapGroup = "all"; // Currently selected filter group
20112011
let d3MapSvg = null; // D3 SVG element
20122012
let d3MapPath = null; // D3 geo path generator
20132013
let stateDataLookup = {}; // Quick lookup: state_fips -> data for current group
2014+
let renderableFipsSet = new Set(); // Set of FIPS codes that can be rendered on map
20142015
let currentColorScaleMin = 0; // Current color scale min (dynamic per group)
20152016
let 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
*/
23882531
function 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");

index.html

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,7 +1636,7 @@ <h2 style="margin: 0 0 8px 0;">State-Level Zero-Sum Thinking Map</h2>
16361636
<span>No data</span>
16371637
</div>
16381638
<div style="display: flex; align-items: center; gap: 3px;">
1639-
<span style="width: 10px; height: 10px; background: #f8fafc; border: 1.5px dashed #e67e22; border-radius: 2px;"></span>
1639+
<span style="width: 12px; height: 12px; background: #f8fafc; border: 2px dashed #e67e22; border-radius: 2px;"></span>
16401640
<span>n&lt;50</span>
16411641
</div>
16421642
</div>
@@ -1675,6 +1675,15 @@ <h3 style="margin-top: 0; font-size: 14px;">Subgroup Statistics</h3>
16751675
<strong>⚠️ Notice:</strong> <span id="map-warning-text"></span>
16761676
</div>
16771677

1678+
<!-- DC and Puerto Rico note -->
1679+
<div style="margin-top: 12px; padding: 10px; background: rgba(108, 117, 125, 0.06); border-left: 3px solid #6c757d; border-radius: 6px; font-size: 11px; line-height: 1.6; color: var(--text);">
1680+
<strong>📍 Note:</strong>
1681+
<ul style="margin: 4px 0 0 0; padding-left: 16px;">
1682+
<li><strong>DC</strong> = Washington, D.C. (the U.S. capital, not Washington State). It is very small and located on the East Coast between Maryland and Virginia.</li>
1683+
<li><strong>PR</strong> = Puerto Rico (a U.S. territory in the Caribbean Sea, not a state). It is shown as an inset box in the bottom-left corner of the map.</li>
1684+
</ul>
1685+
</div>
1686+
16781687
<div style="margin-top: 16px; padding: 10px; background: rgba(108, 117, 125, 0.04); border-radius: 6px; font-size: 11px; color: var(--muted); line-height: 1.5;">
16791688
Hover over states for details.
16801689
</div>
@@ -1936,44 +1945,6 @@ <h2 style="margin: 0 0 12px 0; color: var(--text); font-size: 1.2em;">Disclaimer
19361945
</div>
19371946
</div>
19381947

1939-
<!-- Contact Section -->
1940-
<div class="card" style="margin-bottom: 24px; background: linear-gradient(135deg, #a51c30 0%, #7e1424 100%); border: none; color: white;" id="about-contact">
1941-
<h2 style="color: white; font-size: 1.6em; margin-bottom: 8px; text-align: center;">Contact & Feedback</h2>
1942-
<p style="text-align: center; color: rgba(255,255,255,0.85); margin-bottom: 28px; font-size: 15px;">We welcome feedback from researchers, educators, policymakers, and the public.</p>
1943-
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
1944-
<a href="mailto:jwang@hks.harvard.edu" style="display: flex; flex-direction: column; align-items: center; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); border-radius: 12px; padding: 24px 16px; text-decoration: none; transition: all 0.3s;" onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-4px)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0)'">
1945-
<div style="width: 52px; height: 52px; background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 14px;">
1946-
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#a51c30" stroke-width="2">
1947-
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
1948-
<polyline points="22,6 12,13 2,6"></polyline>
1949-
</svg>
1950-
</div>
1951-
<span style="color: white; font-weight: 600; font-size: 15px;">Email Us</span>
1952-
<span style="color: rgba(255,255,255,0.75); font-size: 12px; margin-top: 4px;">jwang@hks.harvard.edu</span>
1953-
</a>
1954-
<a href="survey.html" target="_blank" style="display: flex; flex-direction: column; align-items: center; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); border-radius: 12px; padding: 24px 16px; text-decoration: none; transition: all 0.3s;" onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-4px)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0)'">
1955-
<div style="width: 52px; height: 52px; background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 14px;">
1956-
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#a51c30" stroke-width="2">
1957-
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1958-
</svg>
1959-
</div>
1960-
<span style="color: white; font-weight: 600; font-size: 15px;">Feedback Form</span>
1961-
<span style="color: rgba(255,255,255,0.75); font-size: 12px; margin-top: 4px;">Share your thoughts</span>
1962-
</a>
1963-
<a href="report-issue.html" target="_blank" style="display: flex; flex-direction: column; align-items: center; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); border-radius: 12px; padding: 24px 16px; text-decoration: none; transition: all 0.3s;" onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-4px)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0)'">
1964-
<div style="width: 52px; height: 52px; background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 14px;">
1965-
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#a51c30" stroke-width="2">
1966-
<circle cx="12" cy="12" r="10"></circle>
1967-
<line x1="12" y1="8" x2="12" y2="12"></line>
1968-
<line x1="12" y1="16" x2="12.01" y2="16"></line>
1969-
</svg>
1970-
</div>
1971-
<span style="color: white; font-weight: 600; font-size: 15px;">Report Issues</span>
1972-
<span style="color: rgba(255,255,255,0.75); font-size: 12px; margin-top: 4px;">Technical problems</span>
1973-
</a>
1974-
</div>
1975-
</div>
1976-
19771948
<!-- Back to Home -->
19781949
<div style="text-align: center; margin-top: 40px; padding-top: 32px; border-top: 1px solid #e5e7eb;">
19791950
<a href="#" onclick="setRoute('learn'); return false;" style="display: inline-flex; align-items: center; gap: 8px; padding: 14px 36px; background: var(--text); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 15px; transition: all 0.2s; cursor: pointer;" onmouseover="this.style.background='#a51c30'; this.style.transform='translateY(-2px)'" onmouseout="this.style.background='var(--text)'; this.style.transform='translateY(0)'">

0 commit comments

Comments
 (0)