PageSourceSearch

https://tod-primary-tools-tiiny-toggle.tiiny.site/

html tod-primary-tools-tiiny-toggle.tiiny.site collected 2026-10-03 05:07:51 UTC 13,362 bytes, 229 lines download raw bytes

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 &frac12; mile of a rail station.
30  <span class="cnt" id="cnt"></span></p>
31  <a class="tool-link" href="analysis.html">Open analysis tables &rarr;</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 &rarr; 40/acre (per-block floor)</option>
40      <option value="nn">New units needed &rarr; 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 &amp; 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>&nbsp;&nbsp;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.