[object Object]

← back to Nationalrealestate

USRE Map: left-rail county data-point filters in the drawer — dim counties by metric ranges + live match count (v0.8.0)

27ca20752e23791fd596788898c01257c66dbf8f · 2026-07-26 20:11:15 -0700 · Steve

Files touched

Diff

commit 27ca20752e23791fd596788898c01257c66dbf8f
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 26 20:11:15 2026 -0700

    USRE Map: left-rail county data-point filters in the drawer — dim counties by metric ranges + live match count (v0.8.0)
---
 package-lock.json |  4 +--
 package.json      |  2 +-
 public/index.html | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 78 insertions(+), 4 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index a3b7d3b..a16f3ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nationalrealestate",
-  "version": "0.7.1",
+  "version": "0.8.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nationalrealestate",
-      "version": "0.7.1",
+      "version": "0.8.0",
       "dependencies": {
         "better-sqlite3": "^13.0.1",
         "dotenv": "^16.4.5",
diff --git a/package.json b/package.json
index 23be7a3..aebddcc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nationalrealestate",
-  "version": "0.7.1",
+  "version": "0.8.0",
   "private": true,
   "type": "module",
   "description": "USRealEstate — national U.S. residential market explorer. Free-data v1 (Redfin Data Center, Zillow Research, Census ACS, FHFA). Internal analyst tool behind basic auth.",
diff --git a/public/index.html b/public/index.html
index 56f3db9..7ff7917 100644
--- a/public/index.html
+++ b/public/index.html
@@ -39,6 +39,20 @@
   .county-tip .nm{font-weight:700;} .county-tip .vl{color:var(--gold);font-variant-numeric:tabular-nums;}
   .leaflet-control-attribution{background:rgba(17,21,29,.8)!important;color:var(--muted)!important;}
   .leaflet-control-attribution a{color:var(--muted)!important;}
+  /* map county-filter panel (lives inside the hamburger drawer) */
+  #mapfilters{border-top:1px solid var(--line);padding-top:14px;margin-top:6px;}
+  #mapfilters .mf-h{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);font-weight:700;
+    display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;}
+  #mapfilters #mf-reset{background:none;border:1px solid var(--line);color:var(--muted);border-radius:6px;
+    font-size:10px;padding:2px 8px;cursor:pointer;text-transform:none;letter-spacing:0;}
+  #mapfilters #mf-reset:hover{color:var(--gold);border-color:var(--gold);}
+  #mapfilters .mf-row{margin-bottom:9px;}
+  #mapfilters .mf-row label{display:block;font-size:11px;color:var(--muted);margin-bottom:3px;}
+  #mapfilters .mf-in{display:flex;gap:6px;}
+  #mapfilters .mf-in input{width:100%;min-width:0;background:var(--bg);border:1px solid var(--line);color:var(--fg);
+    border-radius:6px;padding:5px 7px;font-size:12px;}
+  #mapfilters #mf-count{font-size:12px;color:var(--fg);margin-top:8px;font-variant-numeric:tabular-nums;}
+  #mapfilters #mf-count b{color:var(--gold);}
 </style>
 <link rel="stylesheet" href="/nav-drawer.css">
 </head>
@@ -115,11 +129,69 @@ function quantileBreaks(vals, n){
 }
 
 function styleFeature(f){
-  return { weight: .35, color: '#222936', fillOpacity: .82, fillColor: classColor(values[f.id]) };
+  const pass = filterPass(f.id);
+  return { weight: .35, color: '#222936',
+    fillOpacity: pass ? .82 : .05,
+    fillColor: pass ? classColor(values[f.id]) : NO_DATA };
 }
 
 function metricDef(){ return METRICS.find(m => m.key === currentMetric); }
 
+// ---- county data-point filters (left panel, inside the hamburger drawer) ----
+const metricVals = {};                                   // metric -> { countyId: value }
+const fRanges = {};                                      // metric -> {min,max}
+METRICS.forEach(m => fRanges[m.key] = { min: null, max: null });
+let filtersReady = false;
+
+function anyFilterActive(){ return METRICS.some(m => { const r = fRanges[m.key]; return r.min != null || r.max != null; }); }
+function filterPass(id){
+  if (!filtersReady || !anyFilterActive()) return true;
+  for (const m of METRICS){
+    const r = fRanges[m.key];
+    if (r.min == null && r.max == null) continue;
+    const v = (metricVals[m.key] || {})[id];
+    if (v == null || !Number.isFinite(v)) return false;   // no data can't satisfy an active range
+    if (r.min != null && !(v >= r.min)) return false;
+    if (r.max != null && !(v <= r.max)) return false;
+  }
+  return true;
+}
+async function loadAllMetricVals(){
+  const res = await Promise.all(METRICS.map(m =>
+    fetch('/api/choropleth?metric=' + m.key).then(r => r.json()).catch(() => ({}))));
+  METRICS.forEach((m, i) => metricVals[m.key] = res[i].values || {});
+  filtersReady = true;
+}
+function applyFilters(){
+  if (geoLayer) geoLayer.setStyle(styleFeature);
+  const el = document.getElementById('mf-count');
+  if (!el) return;
+  if (!anyFilterActive()){ el.textContent = ''; return; }
+  let n = 0, tot = 0;
+  (geojson && geojson.features || []).forEach(f => { tot++; if (filterPass(f.id)) n++; });
+  el.innerHTML = `<b>${n.toLocaleString()}</b> of ${tot.toLocaleString()} counties match`;
+}
+function buildMapFilters(){
+  const host = document.getElementById('navDrawerBody');
+  if (!host || document.getElementById('mapfilters')) return;
+  const wrap = document.createElement('div'); wrap.id = 'mapfilters';
+  wrap.innerHTML = `<div class="mf-h">Filter counties <button id="mf-reset" type="button">reset</button></div>` +
+    METRICS.map(m => `<div class="mf-row" data-k="${m.key}"><label>${m.label}</label>` +
+      `<div class="mf-in"><input type="number" class="mf-min" placeholder="min"><input type="number" class="mf-max" placeholder="max"></div></div>`).join('') +
+    `<div id="mf-count"></div>`;
+  host.appendChild(wrap);
+  wrap.querySelectorAll('.mf-row').forEach(row => {
+    const k = row.dataset.k;
+    row.querySelector('.mf-min').addEventListener('input', e => { fRanges[k].min = e.target.value ? +e.target.value : null; applyFilters(); });
+    row.querySelector('.mf-max').addEventListener('input', e => { fRanges[k].max = e.target.value ? +e.target.value : null; applyFilters(); });
+  });
+  wrap.querySelector('#mf-reset').addEventListener('click', () => {
+    METRICS.forEach(m => fRanges[m.key] = { min: null, max: null });
+    wrap.querySelectorAll('input').forEach(i => i.value = '');
+    applyFilters();
+  });
+}
+
 function renderLegend(count){
   const md = metricDef();
   const el = document.getElementById('legend');
@@ -217,8 +289,10 @@ async function loadTopMarkets(){
   ]);
   geojson = geo;
   geoLayer = L.geoJSON(geo, { style: styleFeature, onEachFeature }).addTo(map);
+  buildMapFilters();
   loadTopMarkets();
   loadNationalStrip();
+  loadAllMetricVals().then(applyFilters);   // enable county filtering once all metric values are in
 })();
 </script>
 </body>

← 46b0b58 USRE grid: polished empty state — icon + message + one-click  ·  back to Nationalrealestate  ·  parcels: add Salt Lake County UT (49035) — 393k parcels, mar b761f11 →