1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="utf-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1"> 6<title data-id="th-modified">Rail-accessible census blocks â 40 units/acre analysis</title> 7<link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet">
8<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
8 9<style> 10 html,body{margin:0;height:100%;font-family:-apple-system,Segoe UI,Roboto,sans-serif} 11 #map{position:absolute;top:0;bottom:0;left:0;right:0} 12 #panel{position:absolute;top:12px;left:12px;z-index:2;background:#fff;padding:14px 16px; 13 border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,.18);width:270px;font-size:13px;max-height:92%;overflow:auto} 14 #panel h1{font-size:15px;margin:0 0 4px} #panel p.sub{margin:0 0 10px;color:#555;font-size:12px;line-height:1.35} 15 .grp{border-top:1px solid #eee;padding-top:9px;margin-top:9px} 16 label{display:block;margin:3px 0;cursor:pointer} .sw{display:inline-block;width:11px;height:11px;border-radius:2px;margin-right:6px;vertical-align:middle} 17 .leg{display:flex;align-items:center;margin:2px 0;font-size:12px} 18 select{width:100%;padding:4px;margin-top:4px} .muted{color:#777;font-size:11px} 19 .tool-link{display:inline-block;margin-top:2px;color:#2c6faa;text-decoration:none;font-weight:600} 20 .tool-link:hover{text-decoration:underline} 21 .maplibregl-popup-content{font-size:12.5px;line-height:1.5;padding:10px 12px} 22 .cnt{font-weight:600} 23</style> 24<meta property="og:title" content="Rail-accessible census blocks â 40 units/acre analysis" data-id="th-modified" ><meta name="description" content="TOD-primary-tools-tiiny-toggle" data-id="th-modified" ><meta property="og:description" content="TOD-primary-tools-tiiny-toggle" data-id="th-modified" ><meta property="og:image" content="https://tiiny.host/assets/logo-purple-bg.png" data-id="th-modified" ><meta property="og:image:width" content="1501" data-id="th-modified" ><meta property="og:image:height" content="1501" data-id="th-modified" ><meta property="og:image:type" content="image/png" data-id="th-modified" ><meta property="og:url" content="https://tod-primary-tools-tiiny-toggle.tiiny.site/" data-id="th-modified" ><meta property="og:type" content="website" data-id="th-modified" >
24<script defer data-domain="tod-primary-tools-tiiny-toggle.tiiny.site" src="https://analytics.tiiny.net/js/plausible.js"></script>
24<script type="text/javascript" src="https://tiiny.host/ad-script.js"></script>
24</head> 25<body> 26<div id="map"></div> 27<div id="panel"> 28 <h1>Rail-accessible census blocks</h1> 29 <p class="sub">Blocks (with people or homes) whose centroid is within ½ mile of a rail station. 30 <span class="cnt" id="cnt"></span></p> 31 <a class="tool-link" href="analysis.html">Open analysis tables →</a> 32 33 <div class="grp"> 34 <b>Color blocks by</b> 35 <select id="metric"> 36 <option value="hot">Development hotness (5 tiers)</option> 37 <option value="peer">Above/below national peers (binary)</option> 38 <option value="upa">Current density (units/acre)</option> 39 <option value="need">New units needed → 40/acre (per-block floor)</option> 40 <option value="nn">New units needed → 40/acre (station-area avg)</option> 41 <option value="val">Median home value (ACS block group)</option> 42 <option value="rent">Median gross rent (ACS block group)</option> 43 </select> 44 <div id="legend" style="margin-top:8px"></div> 45 </div> 46 47 <div class="grp"> 48 <b>Layers</b> 49 <label><input type="checkbox" id="tBlocks" checked> Census blocks</label> 50 <label><input type="checkbox" id="tLines" checked> Rail lines</label> 51 <label><input type="checkbox" id="tStations"> Rail stations</label> 52 </div> 53 54 <div class="grp"> 55 <b>Filter blocks by market tier</b> 56 <div class="muted" style="margin:2px 0 4px">Pair with "units needed" to see e.g. hot blocks' gap.</div> 57 <div id="tierFilter"></div> 58 </div> 59 60 <div class="grp"> 61 <b>Rail mode</b> <span class="muted">(lines & stations)</span> 62 <div id="modeLegend"></div> 63 </div> 64 <div class="grp muted">Click any feature for its characteristics. Data: TIGER 2020 blocks · BTS National Transit Map · 40/acre floor analysis.</div> 65</div> 66
67<script src="data/blocks.js"></script>
vendor: 1 bytes, line 67
67
68<script src="data/stations.js"></script>
vendor: 1 bytes, line 68
68
69<script src="data/lines.js"></script>
vendor: 1 bytes, line 69
69
70<script> 71const MODE_COLORS = { 72 "Heavy rail":"#e6194B","Commuter rail":"#4363d8","Light rail / streetcar":"#3cb44b", 73 "Monorail":"#f58231","Cable car":"#911eb4","Aerial tramway":"#42d4f4","Funicular":"#a9a9a9"}; 74 75// density (units/acre) color steps â red below 40, blue/green at/above 76// under 40 u/ac: darkest red = farthest below the 40 threshold, fading to pale near 40; 77// at/above 40 = blue. (Bright red = "warning, far from target".) 78const UPA_STOPS=[[0,"#99000d"],[2,"#cb181d"],[5,"#ef3b2c"],[15,"#fc9272"],[30,"#fee0d2"], 79 [40,"#2c7fb8"],[75,"#253494"]]; 80const UPA_LABELS=["0â2","2â5","5â15","15â30","30â40","40â75","75+"]; 81// units-needed color steps 82const NEED_STOPS=[[0,"#e8e8e8"],[1,"#ffffcc"],[100,"#c2e699"],[300,"#78c679"],[600,"#31a354"],[1200,"#006837"]]; 83const NEED_LABELS=["0 (already â¥40)","1â100","100â300","300â600","600â1200","1200+"]; 84// ACS median home value (purples) & median gross rent (oranges); -1 = no data (grey) 85const VAL_STOPS=[[0,"#f2f0f7"],[150000,"#dadaeb"],[300000,"#bcbddc"],[500000,"#9e9ac8"],[750000,"#756bb1"],[1200000,"#54278f"]]; 86const VAL_LABELS=["<$150k","$150â300k","$300â500k","$500â750k","$750kâ1.2M","$1.2M+"]; 87const RENT_STOPS=[[0,"#feedde"],[900,"#fdd0a2"],[1300,"#fdae6b"],[1700,"#fd8d3c"],[2200,"#e6550d"],[3000,"#a63603"]]; 88const RENT_LABELS=["<$900","$900â1,300","$1,300â1,700","$1,700â2,200","$2,200â3,000","$3,000+"]; 89// Development "hotness": block-group home value vs ~$250k marginal hard build cost. 90// blue = value below build cost (unlikely to pencil) -> red = far above (very likely). 91// Hotness tier precomputed per block from VALUE if present, else RENT (rent cuts 92// derived from the valueâ17.6Ãannual-rent relationship, so the OR is one scale). 93const HOT_C=["#2c7bb6","#abd9e9","#ffffbf","#fdae61","#d7191c"]; // coldâred-hot 94const HOT_STOPS=HOT_C.map((c,i)=>[i,c]); 95const HOT_LABELS=["Cold · value <$150k / rent <$710","Cool · $150â300k / $710â1,420", 96 "Warm · $300â500k / $1,420â2,360","Hot · $500â800k / $2,360â3,780", 97 "Red-hot · >$800k / >$3,780"]; 98function tierName(t){return ["Cold","Cool","Warm","Hot","Red-hot"][t]||"no data";} 99// binary: above national median value OR rent 100const PEER_STOPS=[[1,"#d7191c"],[0,"#2c7bb6"]]; 101const PEER_LABELS=["Above US median ($360,600 value or $1,487 rent)","At / below US median"]; 102 103function colorExpr(m){ 104 if(m==="upa") return stepExpr("upa",UPA_STOPS); 105 if(m==="need") return stepExpr("need",NEED_STOPS); 106 if(m==="nn") return stepExpr("nn",NEED_STOPS); 107 if(m==="hot") return ["case",["<",["get","t"],0],"#cccccc", 108 ["step",["get","t"],HOT_C[0],1,HOT_C[1],2,HOT_C[2],3,HOT_C[3],4,HOT_C[4]]]; 109 if(m==="peer") return ["case",["<",["get","pr"],0],"#cccccc", 110 ["match",["get","pr"],1,"#d7191c",0,"#2c7bb6","#cccccc"]]; 111 const stops = m==="val"?VAL_STOPS:RENT_STOPS; 112 return ["case",["<",["get",m],0],"#cccccc",stepExpr(m,stops)]; // grey = no ACS data 113} 114 115function stepExpr(prop,stops){ 116 const e=["step",["get",prop],stops[0][1]]; 117 for(let i=1;i<stops.length;i++){e.push(stops[i][0],stops[i][1]);} 118 return e; 119} 120// build GeoJSON from compact arrays 121function blocksGeo(){ 122 return {type:"FeatureCollection",features:BLOCKS.map(r=>({type:"Feature", 123 geometry:{type:"Point",coordinates:[r[0],r[1]]}, 124 properties:{hu:r[2],upa:r[3],need:r[4],s:r[5],z:r[6],val:r[7],rent:r[8],t:r[9],pr:r[12],nn:r[13]}}))}; 125} 126function stationsGeo(){ 127 return {type:"FeatureCollection",features:STATIONS.map(r=>({type:"Feature", 128 geometry:{type:"Point",coordinates:[r[0],r[1]]}, 129 properties:{mode:SMODES[r[2]],s:r[3]}}))}; 130} 131const modeMatch=["match",["get","mode"]]; 132Object.entries(MODE_COLORS).forEach(([k,v])=>{modeMatch.push(k,v)}); 133modeMatch.push("#888"); 134 135const map=new maplibregl.Map({ 136 container:"map", 137 style:{version:8, 138 sources:{carto:{type:"raster",tiles:["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", 139 "https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"],tileSize:256, 140 attribution:"© OpenStreetMap © CARTO"}}, 141 layers:[{id:"bg",type:"raster",source:"carto"}]}, 142 center:[-96,38],zoom:3.6 143}); 144map.addControl(new maplibregl.NavigationControl()); 145document.getElementById("cnt").textContent=BLOCKS.length.toLocaleStr
145ing()+" blocks · "+STATIONS.length.toLocaleString()+" stations."; 146 147map.on("load",()=>{ 148 map.addSource("blocks",{type:"geojson",data:blocksGeo()}); 149 map.addSource("lines",{type:"geojson",data:LINES}); 150 map.addSource("stations",{type:"geojson",data:stationsGeo()}); 151 152 map.addLayer({id:"blocks",type:"circle",source:"blocks",paint:{ 153 "circle-color":colorExpr("hot"), 154 "circle-radius":["interpolate",["linear"],["zoom"],4,1.4,8,3,12,6,15,10], 155 "circle-opacity":0.75,"circle-stroke-width":0}}); 156 157 map.addLayer({id:"lines",type:"line",source:"lines",paint:{ 158 "line-color":modeMatch,"line-width":["interpolate",["linear"],["zoom"],5,1,12,3]}}); 159 160 map.addLayer({id:"stations",type:"circle",source:"stations",layout:{visibility:"none"},paint:{ 161 "circle-color":modeMatch,"circle-radius":["interpolate",["linear"],["zoom"],4,2,10,5,14,8], 162 "circle-stroke-color":"#111","circle-stroke-width":0.8}}); 163 164 // popups 165 map.on("click","blocks",e=>{const p=e.features[0].properties; 166 new maplibregl.Popup().setLngLat(e.lngLat).setHTML( 167 `<b>Census block</b><br>Existing housing units: <b>${(+p.hu).toLocaleString()}</b>`+ 168 `<br>Current density: <b>${(+p.upa).toFixed(1)}</b> units/acre`+ 169 `<br>New units to reach 40/acre: <b>${(+p.need).toLocaleString()}</b> <span class="muted">(per-block)</span>`+ 170 `<br> station-area-avg share: <b>${(+p.nn).toLocaleString()}</b>`+ 171 `<br>Median home value: <b>${p.val<0?"n/a":"$"+(+p.val).toLocaleString()}</b> <span class="muted">(block group)</span>`+ 172 `<br>Median gross rent: <b>${p.rent<0?"n/a":"$"+(+p.rent).toLocaleString()}</b> <span class="muted">(block group)</span>`+ 173 `<br>Development hotness: <b>${p.t<0?"no data":tierName(p.t)}</b> `+ 174 `<span class="muted">${p.val>0?"(by value)":p.rent>0?"(by rent)":""}</span>`+ 175 `<br>vs. national peers: <b>${p.pr<0?"no data":(p.pr?"Above median":"At/below median")}</b>`+ 176 `<br>Nearest system: ${SYS[p.s]||"â"}<br>UZA: ${UZA[p.z]||"â"}`).addTo(map);}); 177 map.on("click","stations",e=>{const p=e.features[0].properties; 178 new maplibregl.Popup().setLngLat(e.lngLat).setHTML( 179 `<b>Rail station</b><br>Mode: ${p.mode}<br>System: ${SYS2[p.s]||"â"}`).addTo(map);}); 180 map.on("click","lines",e=>{new maplibregl.Popup().setLngLat(e.lngLat) 181 .setHTML(`<b>Rail line</b><br>Mode: ${e.features[0].properties.m}`).addTo(map);}); 182 ["blocks","stations","lines"].forEach(l=>{ 183 map.on("mouseenter",l,()=>map.getCanvas().style.cursor="pointer"); 184 map.on("mouseleave",l,()=>map.getCanvas().style.cursor="");}); 185 186 drawLegend("hot"); drawModeLegend(); 187}); 188 189const LEG={upa:[UPA_STOPS,UPA_LABELS],need:[NEED_STOPS,NEED_LABELS],nn:[NEED_STOPS,NEED_LABELS], 190 val:[VAL_STOPS,VAL_LABELS],rent:[RENT_STOPS,RENT_LABELS],hot:[HOT_STOPS,HOT_LABELS], 191 peer:[PEER_STOPS,PEER_LABELS]}; 192function drawLegend(metric){ 193 const [stops,labels]=LEG[metric]; 194 let html=stops.map((s,i)=>`<div class="leg"><span class="sw" style="background:${s[1]}"></span>${labels[i]}</div>`).join(""); 195 if(["val","rent","hot","peer"].includes(metric)) html+=`<div class="leg"><span class="sw" style="background:#cccccc"></span>no ACS data</div>`; 196 document.getElementById("legend").innerHTML=html; 197} 198function drawModeLegend(){ 199 document.getElementById("modeLegend").innerHTML=Object.entries(MODE_COLORS).map(([k,v])=> 200 `<div class="leg"><span class="sw" style="background:${v}"></span>${k}</div>`).join(""); 201} 202 203document.getElementById("metric").onchange=e=>{ 204 const m=e.target.value; 205 map.setPaintProperty("blocks","circle-color",colorExpr(m)); 206 drawLegend(m); 207}; 208const tog=(id,layer)=>document.getElementById(id).onchange=e=>
209 map.setLayoutProperty(layer,"visibility",e.target.checked?"visible":"none"); 210tog("tBlocks","blocks"); tog("tLines","lines"); tog("tStations","stations"); 211 212// ---- market-tier filter for the blocks layer ---- 213const TIERS=[[3,"Hot","#fdae61"],[4,"Red-hot","#d7191c"],[2,"Warm","#ffffbf"], 214 [1,"Cool","#abd9e9"],[0,"Cold","#2c7bb6"],[-1,"No data","#cccccc"]]; 215const tierSel=new Set(TIERS.map(t=>t[0])); 216function applyTierFilter(){ 217 const f=(tierSel.size===TIERS.length)?null:["in",["get","t"],["literal",[...tierSel]]]; 218 if(map.getLayer("blocks")) map.setFilter("blocks",f); 219} 220const tf=document.getElementById("tierFilter"); 221TIERS.forEach(([code,name,color])=>{ 222 const l=document.createElement("label"); 223 l.innerHTML=`<input type="checkbox" checked> <span class="sw" style="background:${color}"></span>${name}`; 224 l.querySelector("input").onchange=e=>{e.target.checked?tierSel.add(code):tierSel.delete(code);applyTierFilter();}; 225 tf.appendChild(l); 226}); 227</script>
227 228</body> 229</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.