// USA Choropleth — real state geography via us-atlas TopoJSON + d3-geo. // Continuous diverging palette + distinct "no data" treatment. { const { STATES, scoreFor, lensMetaFor } = window.PL_DATA; // Five-stop continuous diverging palette: rust → terracotta → slate → jade → teal. // Continuous (not bucketed) so subtle differences between states are visible. const PALETTE_STOPS = [ { at: 0, c: "#5a2a1f" }, // rust — most restrictive { at: 25, c: "#b85a4a" }, // terracotta { at: 50, c: "#475569" }, // slate — neutral { at: 75, c: "#6b9b94" }, // jade { at: 100, c: "#2a6b66" }, // teal — most protective ]; const _palette = window.d3.scaleLinear() .domain(PALETTE_STOPS.map(s => s.at)) .range(PALETTE_STOPS.map(s => s.c)) .interpolate(window.d3.interpolateRgb) .clamp(true); function colorForScore(s) { return { color: _palette(s), text: "#fff" }; } // A state has "real data" for a lens only if at least one bill was scored // against that lens for that state. Without this distinction, unscored states // would look identical to scored-as-neutral states. function stateHasData(code, lensId) { if (!lensMetaFor) return true; const meta = lensMetaFor(code, lensId); return !!(meta && meta.bill_count > 0); } window.PL_COLOR = colorForScore; window.PL_HAS_DATA = stateHasData; // Map state-name (as used in us-atlas) → 2-letter code we score with. const NAME_TO_CODE = { "Alabama":"AL","Alaska":"AK","Arizona":"AZ","Arkansas":"AR","California":"CA", "Colorado":"CO","Connecticut":"CT","Delaware":"DE","Florida":"FL","Georgia":"GA", "Hawaii":"HI","Idaho":"ID","Illinois":"IL","Indiana":"IN","Iowa":"IA", "Kansas":"KS","Kentucky":"KY","Louisiana":"LA","Maine":"ME","Maryland":"MD", "Massachusetts":"MA","Michigan":"MI","Minnesota":"MN","Mississippi":"MS","Missouri":"MO", "Montana":"MT","Nebraska":"NE","Nevada":"NV","New Hampshire":"NH","New Jersey":"NJ", "New Mexico":"NM","New York":"NY","North Carolina":"NC","North Dakota":"ND","Ohio":"OH", "Oklahoma":"OK","Oregon":"OR","Pennsylvania":"PA","Rhode Island":"RI","South Carolina":"SC", "South Dakota":"SD","Tennessee":"TN","Texas":"TX","Utah":"UT","Vermont":"VT", "Virginia":"VA","Washington":"WA","West Virginia":"WV","Wisconsin":"WI","Wyoming":"WY", "District of Columbia":"DC", }; // Approx label centroids for state-abbr overlays (lon,lat) — used in projected space at draw time. // We compute centroids from the geometry instead of hard-coding. // Single shared promise for the topojson fetch. let _atlasPromise = null; function loadAtlas() { if (!_atlasPromise) { _atlasPromise = fetch("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json") .then(r => r.json()); } return _atlasPromise; } function Choropleth({ lensId, mode = "passed", onSelect, activeState, showLabels = "always" }) { const [atlas, setAtlas] = React.useState(null); const [hover, setHover] = React.useState(null); // {code, name, score, x, y} const [transform, setTransform] = React.useState({ k: 1, x: 0, y: 0 }); const wrapRef = React.useRef(null); const svgRef = React.useRef(null); const zoomBehaviorRef = React.useRef(null); React.useEffect(() => { loadAtlas().then(setAtlas).catch(err => { console.warn("atlas load failed", err); }); }, []); // Wire d3 zoom + pan once the atlas + svg are ready React.useEffect(() => { if (!atlas || !svgRef.current) return; const svg = window.d3.select(svgRef.current); const z = window.d3.zoom() .scaleExtent([1, 6]) .on("zoom", (e) => setTransform({ k: e.transform.k, x: e.transform.x, y: e.transform.y })); zoomBehaviorRef.current = z; svg.call(z); return () => svg.on(".zoom", null); }, [atlas]); const resetZoom = () => { if (svgRef.current && zoomBehaviorRef.current) { window.d3.select(svgRef.current).transition().duration(280) .call(zoomBehaviorRef.current.transform, window.d3.zoomIdentity); } }; if (!atlas) { return (