← back to Rentv 2026
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
M public/map.htmlM server.js
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 2026
·
fix(boomer-voice): correct the stale comment naming the WRON 749cf6e9 →