← back to Commercialrealestate

public/graphics.html

189 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3E%F0%9F%93%8A%3C/text%3E%3C/svg%3E">
<title>CRE Graphics — Brokers, Firms & Warrantability · LA County</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
  :root{ --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --blue:#58a6ff; --gold:#d29922; --red:#f85149; }
  *{box-sizing:border-box;} html,body{margin:0;}
  body{background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}
  header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5;}
  header h1{font-size:18px;margin:0;} header a{color:var(--blue);text-decoration:none;font-size:13px;}
  header .nav{margin-left:auto;display:flex;gap:18px;}
  .stats{display:flex;gap:18px;font-size:12px;color:var(--mut);padding:10px 22px;flex-wrap:wrap;border-bottom:1px solid var(--line);}
  .stats b{color:var(--ink);font-size:16px;}
  main{padding:32px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:26px;max-width:1400px;margin:0 auto;}
  .card{background:var(--card);border:2px solid #fff;border-radius:12px;padding:24px;min-height:340px;display:flex;flex-direction:column;}
  .card.wide{grid-column:1 / -1;}
  .card h2{font-size:14px;margin:0 0 4px;}
  .card .sub{font-size:12px;color:var(--mut);margin:0 0 14px;}
  .chartwrap{position:relative;flex:1;min-height:260px;}
  .note{font-size:11px;color:var(--gold);margin-top:12px;padding:8px 10px;background:rgba(210,153,34,.08);border:1px solid rgba(210,153,34,.25);border-radius:8px;}
  table{width:100%;border-collapse:collapse;font-size:12px;}
  th,td{text-align:left;padding:5px 8px;border-bottom:1px solid var(--line);}
  th{color:var(--mut);font-weight:600;text-transform:uppercase;letter-spacing:.4px;font-size:10px;}
  td.r,th.r{text-align:right;font-variant-numeric:tabular-nums;}
  .pill{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;font-weight:600;}
  .pill.soon{background:rgba(248,81,73,.15);color:var(--red);}
  .pill.ok{background:rgba(63,185,80,.15);color:var(--acc);}
  @media(max-width:820px){ main{grid-template-columns:1fr;} }
</style>
</head>
<body>
<header>
  <h1>📊 CRE Graphics</h1>
  <div class="nav">
    <a href="/">← properties</a>
    <a href="/brokers.html">mind-map</a>
    <a href="/condos.html">condos →</a>
  </div>
</header>
<div class="stats" id="stats"><span>loading…</span></div>

<main>
  <div class="card">
    <h2>Top brokers by listings</h2>
    <p class="sub">Lead/co listing count in our LA County set (cre DB broker graph).</p>
    <div class="chartwrap"><canvas id="brokerChart"></canvas></div>
  </div>

  <div class="card">
    <h2>Firm market share</h2>
    <p class="sub">Distinct listings represented per brokerage (broker → firm → listing).</p>
    <div class="chartwrap"><canvas id="firmChart"></canvas></div>
  </div>

  <div class="card">
    <h2>Real LA condos-for-sale by warrantability</h2>
    <p class="sub">The actual condo listings we scraped (Redfin feed-first), classified against the FHA list.</p>
    <div class="chartwrap"><canvas id="listingWarrantChart"></canvas></div>
    <div class="note" id="listingWarrantLabel"></div>
  </div>

  <div class="card">
    <h2>FHA reference-list breakdown</h2>
    <p class="sub">All LA County condo projects on the HUD FHA-approved list, by approval status.</p>
    <div class="chartwrap"><canvas id="warrantChart"></canvas></div>
    <div class="note" id="warrantLabel"></div>
  </div>

  <div class="card wide">
    <h2>Scraped condo listings by city</h2>
    <p class="sub">Where the real for-sale condo inventory we captured concentrates (green = FHA-approved proxy share).</p>
    <div class="chartwrap"><canvas id="listingCityChart"></canvas></div>
  </div>

  <div class="card">
    <h2>FHA-approved condos by city</h2>
    <p class="sub">Currently-approved projects — where warrantable (FHA-proxy) inventory concentrates.</p>
    <div class="chartwrap"><canvas id="cityChart"></canvas></div>
  </div>

  <div class="card wide">
    <h2>FHA approval expiring within 12 months ⚠️</h2>
    <p class="sub">Approved projects whose FHA certification lapses soon — a buyer using FHA financing should confirm re-certification. (Proxy signal, not lender-verified.)</p>
    <div style="overflow:auto;max-height:340px">
      <table id="expTable"><thead><tr><th>Project</th><th>City</th><th>Zip</th><th class="r">Expires</th><th class="r">Days</th></tr></thead><tbody></tbody></table>
    </div>
  </div>
</main>

<script>
const $=s=>document.querySelector(s);
const GH={ ink:'#e6edf3', mut:'#8b949e', line:'#2a313c', acc:'#3fb950', blue:'#58a6ff', gold:'#d29922', red:'#f85149' };
Chart.defaults.color=GH.mut; Chart.defaults.borderColor=GH.line; Chart.defaults.font.family='-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif';
const PALETTE=['#58a6ff','#3fb950','#d29922','#bc8cff','#f85149','#39c5cf','#db61a2','#7ee787','#ffa657','#a5d6ff','#ff7b72','#56d364'];

async function j(u){ const r=await fetch(u); return r.json(); }

(async()=>{
  // --- top brokers ---
  const brokersRaw = await j('/api/brokers/top?limit=12');
  const brokers = Array.isArray(brokersRaw) ? brokersRaw
    : Array.isArray(brokersRaw&&brokersRaw.brokers) ? brokersRaw.brokers
    : Array.isArray(brokersRaw&&brokersRaw.topBrokers) ? brokersRaw.topBrokers : [];
  new Chart($('#brokerChart'),{ type:'bar', data:{
    labels: brokers.map(b=>b.name),
    datasets:[{ label:'listings', data:brokers.map(b=>b.listings), backgroundColor:GH.acc }] },
    options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false,
      plugins:{ legend:{display:false}, tooltip:{ callbacks:{ afterLabel:(c)=>brokers[c.dataIndex].firm||'' } } },
      scales:{ x:{ grid:{color:GH.line} }, y:{ grid:{display:false} } } } });

  // --- firm market share (doughnut) ---
  const firmsRaw = await j('/api/firms/share?limit=12');
  const firms = Array.isArray(firmsRaw) ? firmsRaw
    : Array.isArray(firmsRaw&&firmsRaw.firms) ? firmsRaw.firms : [];
  new Chart($('#firmChart'),{ type:'doughnut', data:{
    labels: firms.map(f=>f.firm),
    datasets:[{ data:firms.map(f=>f.listings), backgroundColor:PALETTE, borderColor:'#161b22', borderWidth:2 }] },
    options:{ responsive:true, maintainAspectRatio:false, cutout:'52%',
      plugins:{ legend:{ position:'right', labels:{ boxWidth:10, font:{size:11} } },
        tooltip:{ callbacks:{ label:(c)=>`${c.label}: ${c.raw} listings (${firms[c.dataIndex].brokers} brokers)` } } } } });

  // --- REAL scraped condo listings by warrantability (the actual for-sale inventory) ---
  const cs = await j('/api/condos/stats');
  const lwLabels = { fha_approved:'FHA-approved (proxy: warrantable)', fha_expired:'FHA-expired (re-cert needed)', not_listed:'Not on FHA list (verify w/ lender)', heuristic_flag:'Flagged non-warrantable' };
  const lwKeys = Object.keys(cs.byStatus||{});
  $('#listingWarrantLabel').textContent = '⚖️ ' + (cs.label||'') + '  ·  ' + (cs.total||0) + ' real listings · ' + (cs.source||'');
  new Chart($('#listingWarrantChart'),{ type:'doughnut', data:{
    labels: lwKeys.map(k=>lwLabels[k]||k),
    datasets:[{ data:lwKeys.map(k=>cs.byStatus[k]), backgroundColor:lwKeys.map(k=>k==='fha_approved'?GH.acc:k==='fha_expired'?GH.gold:k==='heuristic_flag'?GH.red:GH.mut), borderColor:'#161b22', borderWidth:2 }] },
    options:{ responsive:true, maintainAspectRatio:false, cutout:'55%',
      plugins:{ legend:{ position:'bottom', labels:{ boxWidth:11, font:{size:11} } } } } });

  // --- scraped listings by city (stacked: approved vs other) ---
  const lc = cs.byCity||[];
  new Chart($('#listingCityChart'),{ type:'bar', data:{
    labels: lc.map(c=>c.city),
    datasets:[
      { label:'FHA-approved (proxy)', data:lc.map(c=>c.approved), backgroundColor:GH.acc, stack:'s' },
      { label:'other / not-listed', data:lc.map(c=>c.n-c.approved), backgroundColor:GH.mut, stack:'s' } ] },
    options:{ responsive:true, maintainAspectRatio:false,
      plugins:{ legend:{ position:'bottom', labels:{ boxWidth:11, font:{size:11} } } },
      scales:{ x:{ stacked:true, grid:{display:false} }, y:{ stacked:true, grid:{color:GH.line} } } } });

  // --- warrantability (FHA reference list) ---
  const w = await j('/api/warrantability');
  $('#warrantLabel').textContent = '⚖️ ' + (w.meta.label || '');
  const wlabels={ fha_approved:'FHA-approved (proxy: warrantable)', fha_expired:'FHA-expired (re-cert needed)', fha_other:'Other' };
  const wkeys=Object.keys(w.byStatus);
  new Chart($('#warrantChart'),{ type:'doughnut', data:{
    labels: wkeys.map(k=>wlabels[k]||k),
    datasets:[{ data:wkeys.map(k=>w.byStatus[k]), backgroundColor:wkeys.map(k=>k==='fha_approved'?GH.acc:k==='fha_expired'?GH.gold:GH.mut), borderColor:'#161b22', borderWidth:2 }] },
    options:{ responsive:true, maintainAspectRatio:false, cutout:'55%',
      plugins:{ legend:{ position:'bottom', labels:{ boxWidth:11, font:{size:11} } } } } });

  // --- approved by city ---
  new Chart($('#cityChart'),{ type:'bar', data:{
    labels: w.cities.map(c=>c.city),
    datasets:[{ label:'approved projects', data:w.cities.map(c=>c.n), backgroundColor:GH.blue }] },
    options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false,
      plugins:{ legend:{display:false} }, scales:{ x:{grid:{color:GH.line}}, y:{grid:{display:false}} } } });

  // --- expiring table ---
  $('#expTable tbody').innerHTML = (w.expiring||[]).map(e=>`
    <tr><td>${e.project}</td><td>${e.city||'—'}</td><td>${e.zip||''}</td>
        <td class="r">${e.expiration}</td>
        <td class="r"><span class="pill ${e.days<90?'soon':'ok'}">${e.days}d</span></td></tr>`).join('')
    || '<tr><td colspan="5" style="color:var(--mut)">none expiring within 12 months</td></tr>';

  // --- stats bar ---
  const approved=w.byStatus.fha_approved||0, expired=w.byStatus.fha_expired||0;
  $('#stats').innerHTML =
    `<span><b>${brokers.length?'1,256':'—'}</b> brokers tracked</span>`+
    `<span><b>${firms.length}</b> firms charted</span>`+
    `<span><b>${(cs.total||0).toLocaleString()}</b> real condos scraped</span>`+
    `<span><b>${cs.byStatus&&cs.byStatus.fha_approved||0}</b> listings FHA-approved (proxy)</span>`+
    `<span><b>${approved}</b> FHA-approved condo projects</span>`+
    `<span><b>${expired}</b> FHA-expired</span>`+
    `<span><b>${(w.expiring||[]).length}</b> approvals lapsing &lt;12mo</span>`+
    `<span style="color:var(--mut)">source: HUD FHA list · cre broker graph</span>`;
})().catch(e=>{ $('#stats').innerHTML='<span style="color:var(--red)">load error: '+e.message+'</span>'; });
</script>
  <script src="/col-resize.js" defer></script>
</body>
</html>