[object Object]

← back to Rentv

News map: market-activity stats panel (count + deal volume + top metros)

27f2cd0484e0f275b3baa2e9734d8722ab89c304 · 2026-08-06 09:24:21 -0700 · Steve Abrams

Side panel now shows a live dashboard over the current filtered set: story count,
total priced deal volume ($T/$B/$M), and the 6 most-active metros as click-to-fly
bars. /api/map-articles now returns numeric amount to power the volume sum.

TK-10284

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 27f2cd0484e0f275b3baa2e9734d8722ab89c304
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 09:24:21 2026 -0700

    News map: market-activity stats panel (count + deal volume + top metros)
    
    Side panel now shows a live dashboard over the current filtered set: story count,
    total priced deal volume ($T/$B/$M), and the 6 most-active metros as click-to-fly
    bars. /api/map-articles now returns numeric amount to power the volume sum.
    
    TK-10284
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/map.html | 43 +++++++++++++++++++++++++++++++++++++++++++
 server.js       |  1 +
 2 files changed, 44 insertions(+)

diff --git a/public/map.html b/public/map.html
index 8987de74..419262e1 100644
--- a/public/map.html
+++ b/public/map.html
@@ -34,6 +34,17 @@ a{color:inherit;text-decoration:none}.wrap{max-width:1100px;margin:0 auto;paddin
 #map{flex:1 1 auto;min-width:0;height:100%;background:#e8ecef}
 aside{flex:0 0 340px;border-left:1px solid var(--line);display:flex;flex-direction:column;min-height:0;background:#fff}
 aside .ah{padding:12px 16px;border-bottom:1px solid var(--line);font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);display:flex;justify-content:space-between;align-items:center}
+.stats{padding:12px 16px;border-bottom:1px solid var(--line);background:var(--wash);flex:0 0 auto}
+.stats .big{display:flex;gap:14px;margin-bottom:6px}
+.stats .kpi{flex:1}
+.stats .kpi .v{font-family:'Playfair Display',serif;font-size:22px;font-weight:700;line-height:1;color:var(--ink)}
+.stats .kpi .l{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);margin-top:3px}
+.stats .mt{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);margin:8px 0 3px}
+.stats .mrow{display:flex;align-items:center;gap:8px;font-size:12px;padding:3px 5px;cursor:pointer;border-radius:5px}
+.stats .mrow:hover{background:#fff}
+.stats .mrow .nm{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
+.stats .mrow .bar{height:6px;background:var(--red);border-radius:3px;opacity:.85;flex:0 0 auto}
+.stats .mrow .ct{color:var(--sub);font-weight:800;min-width:24px;text-align:right}
 #list{overflow:auto;flex:1 1 auto}
 .it{display:block;padding:12px 16px;border-bottom:1px solid var(--line);transition:background .12s}
 .it:hover{background:var(--wash)}
@@ -83,6 +94,7 @@ aside .ah{padding:12px 16px;border-bottom:1px solid var(--line);font-size:12px;f
 <div class="stage">
   <div id="map"></div>
   <aside>
+    <div class="stats" id="stats"></div>
     <div class="ah"><span id="listhd">Stories in view</span><span id="listn"></span></div>
     <div id="list"><p class="empty">Loading the newsroom map…</p></div>
   </aside>
@@ -154,9 +166,40 @@ function buildMarkers(){
   });
   cluster.addLayers(markers);
   $('cnt').textContent='· '+shown.length.toLocaleString()+' stories';
+  renderStats(shown);
   refreshList();
 }
 
+function fmtUSD(n){ return n>=1e12?'$'+(n/1e12).toFixed(2)+'T' : n>=1e9?'$'+(n/1e9).toFixed(1)+'B' : n>=1e6?'$'+Math.round(n/1e6)+'M' : '$'+Math.round(n).toLocaleString(); }
+// Market-activity summary over the current filtered set — story count, total priced
+// deal volume, and the most-active metros (click a metro to fly there).
+function renderStats(arr){
+  var vol=0, priced=0, metro={};
+  arr.forEach(function(a){
+    if(a.amount){vol+=a.amount;priced++;}
+    var k=a.city+(a.state?', '+a.state:'');
+    if(!metro[k])metro[k]={n:0,vol:0,lat:a.lat,lng:a.lng};
+    metro[k].n++; metro[k].vol+=a.amount||0;
+  });
+  var tops=Object.keys(metro).map(function(k){return {k:k,n:metro[k].n,vol:metro[k].vol,lat:metro[k].lat,lng:metro[k].lng};})
+    .sort(function(x,y){return y.n-x.n;}).slice(0,6);
+  var max=tops.length?tops[0].n:1;
+  var h='<div class="big"><div class="kpi"><div class="v">'+arr.length.toLocaleString()+'</div><div class="l">Stories</div></div>'+
+    '<div class="kpi"><div class="v">'+(vol?fmtUSD(vol):'—')+'</div><div class="l">Deal volume · '+priced+' priced</div></div></div>'+
+    '<div class="mt">Most active metros</div>';
+  tops.forEach(function(t){
+    h+='<div class="mrow" data-lat="'+t.lat+'" data-lng="'+t.lng+'" title="'+esc(t.k)+(t.vol?' · '+fmtUSD(t.vol):'')+'">'+
+      '<span class="nm">'+esc(t.k)+'</span><span class="bar" style="width:'+Math.max(8,Math.round(64*t.n/max))+'px"></span><span class="ct">'+t.n+'</span></div>';
+  });
+  $('stats').innerHTML=h;
+}
+// Click a metro row → fly the map there.
+$('stats').addEventListener('click',function(e){
+  var el=e.target.closest('.mrow'); if(!el)return;
+  var la=parseFloat(el.getAttribute('data-lat')), ln=parseFloat(el.getAttribute('data-lng'));
+  if(isFinite(la)&&isFinite(ln)) map.setView([la,ln],10);
+});
+
 // Haversine miles
 function miles(a,b){var R=3959,dLat=(b[0]-a[0])*Math.PI/180,dLng=(b[1]-a[1])*Math.PI/180,
   s=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(a[0]*Math.PI/180)*Math.cos(b[0]*Math.PI/180)*Math.sin(dLng/2)*Math.sin(dLng/2);
diff --git a/server.js b/server.js
index 191f3556..1db5f58e 100644
--- a/server.js
+++ b/server.js
@@ -372,6 +372,7 @@ app.get('/api/map-articles', (req, r) => {
       city, state,
       lat: g.lat, lng: g.lng,
       txn: a.txn_type || '', ptype: a.property_type || '', amt: a.amount_label || '',
+      amount: (typeof a.amount === 'number' && a.amount > 0) ? a.amount : null,
     });
   }
   out.sort((x, y) => String(y.date).localeCompare(String(x.date)));

← ca159afb PR intel: correspondence timeline on the contact drawer  ·  back to Rentv  ·  fix(boomer-voice): correct the stale comment naming the WRON 749cf6e9 →