[object Object]

← back to Re Flyer Aggregator

live viz (reverted odd map -> clean XY bar chart w/ Z-depth): deals DROP from front into category columns (Type/Market/Source), bars build live, 'from where' ticker; verified render 0 console errors. + ad-crawl found 10 advertisers of top-100 CA brokers (TK-10708)

d5a10e2a29827c42d8ec9e51ee8bee4e5e11be5b · 2026-08-19 16:17:08 -0700 · Steve Abrams

Files touched

Diff

commit d5a10e2a29827c42d8ec9e51ee8bee4e5e11be5b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 16:17:08 2026 -0700

    live viz (reverted odd map -> clean XY bar chart w/ Z-depth): deals DROP from front into category columns (Type/Market/Source), bars build live, 'from where' ticker; verified render 0 console errors. + ad-crawl found 10 advertisers of top-100 CA brokers (TK-10708)
---
 public/flyers/live.html | 100 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)

diff --git a/public/flyers/live.html b/public/flyers/live.html
new file mode 100644
index 0000000..75042c8
--- /dev/null
+++ b/public/flyers/live.html
@@ -0,0 +1,100 @@
+<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>CRE Deals — Live · Abrams</title>
+<style>
+  html,body{margin:0;height:100%;background:#070a14;overflow:hidden;font-family:Inter,-apple-system,Helvetica,Arial,sans-serif;color:#eaf0ff}
+  #c{position:fixed;inset:0;display:block}
+  .hud{position:fixed;pointer-events:none;z-index:5}
+  #top{top:0;left:0;right:0;padding:16px 24px;display:flex;justify-content:space-between;align-items:flex-start}
+  #title{font-size:20px;font-weight:800;letter-spacing:.5px}#title small{display:block;color:#E8A81C;font-size:11px;letter-spacing:2px;margin-top:2px}
+  #stats{text-align:right;font-variant-numeric:tabular-nums}#stats b{font-size:26px;color:#7fd7ff}#stats span{color:#8aa;font-size:12px}
+  #ticker{bottom:0;left:0;right:0;padding:12px 24px;background:linear-gradient(0deg,#070a14ee,transparent);font-size:14px}
+  #ticker .lbl{color:#8aa;font-size:11px;letter-spacing:1px;text-transform:uppercase}
+  #ticker .row{margin-top:3px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
+  .chip{font-size:10px;font-weight:800;padding:2px 8px;border-radius:5px;color:#070a14}
+  #mode{position:fixed;top:64px;right:24px;z-index:6;pointer-events:auto;background:#111a30;color:#cfe0ff;border:1px solid #24304f;border-radius:7px;padding:6px 10px;font-size:12px}
+</style></head><body>
+<canvas id="c"></canvas>
+<div class="hud" id="top"><div id="title">CRE DEALS · LIVE<small>ABRAMS · FEEDS DROPPING INTO CATEGORIES</small></div>
+  <div id="stats"><b id="cnt">0</b> deals<br><span id="vol">$0</span> · <span id="mk">0 markets</span></div></div>
+<select id="mode" title="category axis"><option value="type">Group by: Type</option><option value="market">Group by: Market</option><option value="source">Group by: Source (from where)</option></select>
+<div class="hud" id="ticker"><div class="lbl">Latest — from where</div><div class="row" id="last">Loading deal flow…</div></div>
+<script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>
+<script type="module">
+import * as THREE from 'three';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+const TYPE={Sale:0x2ee6a0,Loan:0xf0b429,Lease:0x5b8bff,Development:0xb061ff,Filing:0x8aa0c0,CRE:0x8aa0c0};
+const money=n=>!n?'$0':n>=1e9?'$'+(n/1e9).toFixed(1)+'B':n>=1e6?'$'+Math.round(n/1e6)+'M':'$'+Math.round(n/1e3)+'K';
+const hex=c=>'#'+c.toString(16).padStart(6,'0');
+
+const scene=new THREE.Scene(); scene.fog=new THREE.FogExp2(0x070a14,0.004);
+const cam=new THREE.PerspectiveCamera(52,innerWidth/innerHeight,.1,2000); cam.position.set(0,42,86);
+const rnd=new THREE.WebGLRenderer({canvas:c,antialias:true}); rnd.setSize(innerWidth,innerHeight); rnd.setPixelRatio(Math.min(devicePixelRatio,2));
+const ctrl=new OrbitControls(cam,rnd.domElement); ctrl.enableDamping=true; ctrl.target.set(0,16,0); ctrl.autoRotate=true; ctrl.autoRotateSpeed=.25; ctrl.maxPolarAngle=Math.PI/2.05;
+scene.add(new THREE.AmbientLight(0x99aaff,.6)); const dl=new THREE.DirectionalLight(0xffffff,.8); dl.position.set(20,60,40); scene.add(dl);
+// floor (XZ plane) + back Y grid
+const floor=new THREE.GridHelper(200,40,0x1b2340,0x101830); scene.add(floor);
+const ywall=new THREE.GridHelper(200,20,0x141c33,0x0d1424); ywall.rotation.x=Math.PI/2; ywall.position.set(0,50,-40); scene.add(ywall);
+
+function label(t,color='#eaf0ff',size=40){const cv=document.createElement('canvas'),g=cv.getContext('2d');g.font=`bold ${size}px Inter,Arial`;cv.width=g.measureText(t).width+20;cv.height=size+16;g.font=`bold ${size}px Inter,Arial`;g.fillStyle=color;g.fillText(t,10,size);const tx=new THREE.CanvasTexture(cv);tx.anisotropy=4;const s=new THREE.Sprite(new THREE.SpriteMaterial({map:tx,transparent:true,depthTest:false}));s.scale.set(cv.width/13,cv.height/13,1);return s;}
+
+// ---- columns: one per category. Blocks DROP from front (+Z) into their column, stacking up (Y). ----
+const BW=7, GAP=6, BH=2.2;   // block width, gap, block height
+let cats=[], colX={}, stack={}, colGroup=new THREE.Group(); scene.add(colGroup);
+function layout(catList){
+  colGroup.clear(); cats=catList; stack={}; colX={};
+  const total=cats.length*(BW+GAP)-GAP; let x=-total/2+BW/2;
+  cats.forEach((cname,i)=>{ colX[cname]=x; stack[cname]=[];
+    // base pad + X-axis label
+    const pad=new THREE.Mesh(new THREE.BoxGeometry(BW+1.5,.4,BW+1.5),new THREE.MeshStandardMaterial({color:0x16203a})); pad.position.set(x,.2,0); colGroup.add(pad);
+    const l=label(cname,'#9fb4e6',30); l.position.set(x,-2.2,BW/2+1); colGroup.add(l);
+    x+=BW+GAP;
+  });
+}
+const drops=[];
+function catOf(d,mode){ return mode==='market'?(d.market||'US'):mode==='source'?(d.source||'CRE'):(d.type||'CRE'); }
+function drop(d){
+  const mode=document.getElementById('mode').value; let cat=catOf(d,mode);
+  if(colX[cat]===undefined){ // new category column appears -> re-layout keeping counts
+    layout([...cats,cat]); }
+  const x=colX[cat], k=stack[cat].length, landY=k*BH+BH/2+.4;
+  const col=TYPE[d.type]||0x8aa0c0;
+  const cube=new THREE.Mesh(new THREE.BoxGeometry(BW,BH,BW),new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:.55,transparent:true,opacity:.96}));
+  cube.position.set(x, landY+46, 30);   // spawn high + in FRONT (z=30)
+  colGroup.add(cube); stack[cat].push(cube);
+  drops.push({cube,x,landY,t:0});
+  // HUD
+  document.getElementById('cnt').textContent=(++S.cnt);
+  S.vol+=d.price||0; document.getElementById('vol').textContent=money(S.vol);
+  S.mk.add(d.market||'US'); document.getElementById('mk').textContent=S.mk.size+' markets';
+  document.getElementById('last').innerHTML=`<span class="chip" style="background:${hex(col)}">${(d.type||'CRE').toUpperCase()}</span> <b>${d.price_label||money(d.price)}</b> ${d.market?'· '+d.market:''} — ${(d.title||'').slice(0,64)} <span style="color:#E8A81C">◂ ${d.source}</span>`;
+}
+const S={cnt:0,vol:0,mk:new Set()};
+
+function anim(){ requestAnimationFrame(anim); ctrl.update();
+  for(let i=drops.length-1;i>=0;i--){ const f=drops[i]; f.t=Math.min(1,f.t+.035);
+    const e=f.t*f.t; // gravity ease-in
+    f.cube.position.y = (f.landY+46) + (f.landY-(f.landY+46))*e;
+    f.cube.position.z = 30*(1-e);           // come from front into the column
+    if(f.t>=1){ f.cube.position.set(f.x,f.landY,0); drops.splice(i,1); }
+  }
+  rnd.render(scene,cam);
+}
+layout(['Sale','Loan','Lease','Development']); anim();
+addEventListener('resize',()=>{cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix();rnd.setSize(innerWidth,innerHeight);});
+document.getElementById('mode').onchange=()=>{ // rebuild for new grouping
+  const base = document.getElementById('mode').value==='type'?['Sale','Loan','Lease','Development']:[];
+  layout(base); S.cnt=0;S.vol=0;S.mk=new Set(); document.getElementById('cnt').textContent=0; seen=new Set(); ALL.slice().reverse().forEach((d,i)=>setTimeout(()=>drop(d),i*120));
+};
+
+// ---- live populate from flyers.json: stream deals in, then poll for new ----
+let seen=new Set(), ALL=[];
+async function load(first){ try{
+  const j=await (await fetch('flyers.json?t='+Date.now())).json(); ALL=j;
+  const fresh=j.filter(d=>{const k=(d.price||0)+'|'+(d.title||'').slice(0,30);if(seen.has(k))return false;seen.add(k);return true;});
+  fresh.sort((a,b)=>(a.price||0)-(b.price||0)); // smallest first so big deals land on top
+  let i=0; const iv=setInterval(()=>{ if(i>=fresh.length){clearInterval(iv);return;} drop(fresh[i++]); }, first?280:800);
+}catch(e){ document.getElementById('last').textContent='feed error: '+e.message; } }
+load(true); setInterval(()=>load(false),30000);
+</script></body></html>

← fb372dc auto-data-snapshot: 2026-08-19T16:13:26 (1 data files) — dat  ·  back to Re Flyer Aggregator  ·  interactive live viz (click color bar -> enter its records) 3aabab2 →