Board: fix the county colors

The county fills were technically correct but visually near-black: linear
min-max scaling crammed a right-skewed distribution into the dark end, so
the map read as empty. Fixes:

  - Rank (percentile) coloring — spread the full palette evenly across
    counties, the standard choropleth technique, so every lens shows rich
    variation instead of a few bright outliers on black.
  - Brighter per-lens ramps (income green, population teal, poverty amber).
  - Coverage amber lifted so Florida's 67 lit counties are unmistakable
    against the fog-of-war country.
  - Default lens is now Median income, so the map opens full of color;
    Coverage (the fog of war) is one click away.

Verified in-browser: full green income choropleth, Florida clearly lit in
coverage, hover readout prints each county's demographics + officeholders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Fabio
2026-07-04 09:11:44 -04:00
parent e126d3e374
commit 4e0c559227
+21 -14
View File
@@ -123,28 +123,35 @@ function pathFor(g){let d="";(g.type==="Polygon"?[g.coordinates]:g.coordinates).
// ---- lenses ---- // ---- lenses ----
const LENSES={ const LENSES={
coverage:{label:"Coverage", kind:"cov"}, coverage:{label:"Coverage", kind:"cov"},
pop:{label:"Population", field:"pop", scale:"sqrt", fmt:fmtN, hi:[67,214,196]}, pop:{label:"Population", field:"pop", fmt:fmtN, lo:[22,48,60], hi:[96,236,244]},
income:{label:"Median income", field:"income", scale:"lin", fmt:fmt$, hi:[122,208,140]}, income:{label:"Median income", field:"income", fmt:fmt$, lo:[28,54,42], hi:[150,246,142]},
poverty:{label:"Poverty rate", field:"poverty", scale:"lin", fmt:fmtP, hi:[244,120,90]}, poverty:{label:"Poverty rate", field:"poverty", fmt:fmtP, lo:[48,40,36], hi:[252,150,96]},
}; };
const ORDER=["coverage","pop","income","poverty"]; const ORDER=["coverage","pop","income","poverty"];
const BASE=[20,27,37]; // --dark-terr-ish const BASE=[26,34,46]; // --dark-terr-ish
let cur="coverage", domains={}, maxOH=0; let cur="income", domains={}, maxOH=0;
Object.values(D).forEach(v=>{ if(v.oh>maxOH)maxOH=v.oh; }); Object.values(D).forEach(v=>{ if(v.oh>maxOH)maxOH=v.oh; });
const ranks={}; // sorted value arrays, for percentile (rank) coloring
for(const key of ORDER){ const L=LENSES[key]; if(!L.field)continue; for(const key of ORDER){ const L=LENSES[key]; if(!L.field)continue;
let lo=1e15,hi=-1e15; Object.values(D).forEach(v=>{const x=v[L.field]; if(x!=null){if(x<lo)lo=x;if(x>hi)hi=x;}}); let lo=1e15,hi=-1e15; const vals=[];
Object.values(D).forEach(v=>{const x=v[L.field]; if(x!=null){vals.push(x); if(x<lo)lo=x;if(x>hi)hi=x;}});
domains[key]=[lo,hi]; domains[key]=[lo,hi];
ranks[key]=vals.sort((a,b)=>a-b);
} }
function rankT(key,x){ const a=ranks[key]; let lo=0,hi=a.length;
while(lo<hi){const m=(lo+hi)>>1; if(a[m]<x)lo=m+1; else hi=m;}
return a.length>1 ? lo/(a.length-1) : 0.5; }
function mix(a,b,t){return `rgb(${a.map((x,i)=>Math.round(x+(b[i]-x)*t)).join(",")})`;} function mix(a,b,t){return `rgb(${a.map((x,i)=>Math.round(x+(b[i]-x)*t)).join(",")})`;}
function colorFor(fips){ function colorFor(fips){
const v=D[fips]; const L=LENSES[cur]; const v=D[fips]; const L=LENSES[cur];
if(!v) return "#0d1219"; if(!v) return "#101720";
if(L.kind==="cov") return v.oh>0 ? mix([60,44,20],[244,178,78],Math.sqrt(v.oh/maxOH)) : "#141b25"; if(L.kind==="cov"){
const x=v[L.field]; if(x==null) return "#141b25"; if(!(v.oh>0)) return "#18212e";
const[lo,hi]=domains[cur]; let t=(x-lo)/(hi-lo||1); return mix([120,82,34],[250,201,105], 0.45+0.55*Math.sqrt(v.oh/maxOH));
if(L.scale==="sqrt") t=Math.sqrt(t); }
return mix(BASE,L.hi,Math.max(0,Math.min(1,t))); const x=v[L.field]; if(x==null) return "#18212e";
return mix(L.lo, L.hi, 0.14+0.86*rankT(cur,x)); // percentile spread, per-lens ramp
} }
// ---- render (single innerHTML string; event delegation for hover) ---- // ---- render (single innerHTML string; event delegation for hover) ----
@@ -190,7 +197,7 @@ function renderLegend(){
}else{ }else{
const[lo,hi]=domains[cur]; const[lo,hi]=domains[cur];
el.innerHTML=` el.innerHTML=`
<div class="scalebar" style="background:linear-gradient(90deg,rgb(${BASE}),rgb(${L.hi}))"></div> <div class="scalebar" style="background:linear-gradient(90deg,rgb(${L.lo}),rgb(${L.hi}))"></div>
<div class="scaleends"><span>${L.fmt(lo)}</span><span>${L.fmt(hi)}</span></div> <div class="scaleends"><span>${L.fmt(lo)}</span><span>${L.fmt(hi)}</span></div>
<div class="lgrow" style="margin-top:9px"><span class="sw" style="background:#141b25"></span>no data</div>`; <div class="lgrow" style="margin-top:9px"><span class="sw" style="background:#141b25"></span>no data</div>`;
} }
@@ -223,6 +230,6 @@ document.getElementById("t-people").textContent="11,285";
document.getElementById("t-counties").textContent=Object.keys(D).length.toLocaleString(); document.getElementById("t-counties").textContent=Object.keys(D).length.toLocaleString();
document.getElementById("t-lit").textContent=nLit; document.getElementById("t-lit").textContent=nLit;
render(); renderLegend(); document.getElementById("stagefoot").textContent=foot.coverage; render(); renderLegend(); document.getElementById("stagefoot").textContent=foot[cur];
requestAnimationFrame(()=>requestAnimationFrame(()=>document.getElementById("board").classList.remove("booting"))); requestAnimationFrame(()=>requestAnimationFrame(()=>document.getElementById("board").classList.remove("booting")));
</script> </script>