[object Object]

← back to Sublease Leaderboard

initial scaffold: sublease.com broker/source leaderboard

78c8011acdcfa96906f5cd85524f8e8087ccb995 · 2026-07-23 18:13:18 -0700 · Steve

Files touched

Diff

commit 78c8011acdcfa96906f5cd85524f8e8087ccb995
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 18:13:18 2026 -0700

    initial scaffold: sublease.com broker/source leaderboard
---
 .gitignore |   8 +
 index.html | 582 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 590 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d6f7fb6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,582 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Sublease.com — Broker / Source Leaderboard</title>
+<style>
+  :root{
+    --bg:#0f1419; --panel:#171e26; --panel2:#1d2731; --line:#2a3744;
+    --ink:#e8edf2; --muted:#8a99a8; --accent:#3ea6ff; --accent2:#00c2a8;
+    --bar:#3ea6ff; --bar2:#2b6ea3; --good:#00c2a8; --chip:#22303c; --warn:#f0a63e;
+    --shadow:0 1px 0 rgba(255,255,255,.03),0 8px 24px rgba(0,0,0,.35);
+    font-synthesis-weight:none;
+  }
+  *{box-sizing:border-box}
+  html,body{margin:0;padding:0}
+  body{
+    background:var(--bg); color:var(--ink);
+    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+    font-size:14px; line-height:1.45; -webkit-font-smoothing:antialiased;
+  }
+  a{color:var(--accent)}
+  header{
+    padding:26px 28px 18px; border-bottom:1px solid var(--line);
+    background:linear-gradient(180deg,#141b23,#0f1419);
+  }
+  .brandrow{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
+  h1{margin:0;font-size:22px;font-weight:650;letter-spacing:-.4px}
+  .brand-badge{
+    font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
+    color:#0f1419;background:var(--accent2);padding:3px 9px;border-radius:4px;
+  }
+  .sub{color:var(--muted);font-size:13px;margin-top:6px}
+  .sub b{color:var(--ink);font-weight:650}
+  .wrap{max-width:1180px;margin:0 auto;padding:22px 28px 60px}
+  .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:0 0 26px}
+  .kpi{
+    background:var(--panel);border:1px solid var(--line);border-radius:10px;
+    padding:15px 16px;box-shadow:var(--shadow);
+  }
+  .kpi .n{font-size:24px;font-weight:680;letter-spacing:-.5px;font-variant-numeric:tabular-nums}
+  .kpi .l{color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;margin-top:3px}
+  .kpi .s{color:var(--muted);font-size:11px;margin-top:2px}
+  .section-h{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px;gap:12px}
+  .section-h h2{font-size:15px;font-weight:640;margin:0;letter-spacing:-.2px}
+  .section-h .hint{color:var(--muted);font-size:12px}
+  .grid{display:grid;grid-template-columns:1.15fr 1fr;gap:22px;align-items:start}
+  @media(max-width:900px){.grid{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}}
+
+  /* chart */
+  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 18px 10px;box-shadow:var(--shadow)}
+  .chart-row{display:flex;align-items:center;gap:12px;margin:0 0 10px;cursor:pointer}
+  .chart-row .name{width:150px;flex:none;font-size:12.5px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+  .chart-row .track{flex:1;background:var(--panel2);border-radius:5px;height:22px;position:relative;overflow:hidden}
+  .chart-row .fill{
+    height:100%;border-radius:5px;background:linear-gradient(90deg,var(--bar),var(--bar2));
+    transition:filter .15s,width .3s ease;
+  }
+  .chart-row:hover .fill{filter:brightness(1.18)}
+  .chart-row.active .fill{background:linear-gradient(90deg,var(--accent2),#0a7d6c)}
+  .chart-row .val{width:64px;flex:none;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;color:var(--muted)}
+  .chart-toggle{display:inline-flex;background:var(--panel2);border:1px solid var(--line);border-radius:7px;overflow:hidden}
+  .chart-toggle button{background:none;border:none;color:var(--muted);padding:5px 11px;font-size:12px;cursor:pointer}
+  .chart-toggle button.on{background:var(--accent);color:#0f1419;font-weight:640}
+
+  /* table */
+  table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
+  thead th{
+    text-align:right;padding:9px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;
+    color:var(--muted);border-bottom:1px solid var(--line);cursor:pointer;user-select:none;white-space:nowrap;
+  }
+  thead th:first-child{text-align:left}
+  thead th:hover{color:var(--ink)}
+  thead th .arw{opacity:.55;font-size:10px}
+  tbody td{padding:10px 12px;text-align:right;border-bottom:1px solid #212c37;font-size:13px}
+  tbody td:first-child{text-align:left}
+  tbody tr{cursor:pointer}
+  tbody tr:hover{background:#1a232d}
+  tbody tr.active{background:#12312b;box-shadow:inset 3px 0 0 var(--accent2)}
+  tbody tr:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
+  .rank{color:var(--muted);width:26px;display:inline-block}
+  .srcname{font-weight:600}
+  .dom{color:var(--muted);font-size:11.5px;display:block;font-weight:400}
+  .barcell{position:relative}
+  .mini{position:absolute;left:12px;right:12px;bottom:5px;height:3px;background:#243440;border-radius:2px;overflow:hidden}
+  .mini i{display:block;height:100%;background:var(--accent);border-radius:2px}
+
+  /* controls */
+  .controls{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
+  .ctrl{display:flex;align-items:center;gap:7px}
+  .ctrl label{color:var(--muted);font-size:12px;white-space:nowrap}
+  select,input[type=search]{
+    background:var(--panel2);border:1px solid var(--line);color:var(--ink);
+    border-radius:7px;padding:6px 10px;font-size:12.5px;font-family:inherit;
+  }
+  input[type=search]{min-width:210px}
+  input[type=range]{accent-color:var(--accent);width:110px;cursor:pointer}
+  .btn{background:var(--chip);border:1px solid var(--line);color:var(--muted);padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
+  .btn:hover{color:var(--ink)}
+
+  /* space-type chips */
+  .typefilter{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 2px}
+  .typechip{background:var(--panel2);border:1px solid var(--line);border-radius:20px;padding:5px 12px;font-size:12px;color:var(--muted);cursor:pointer;user-select:none}
+  .typechip:hover{color:var(--ink)}
+  .typechip.on{background:var(--accent);border-color:var(--accent);color:#0f1419;font-weight:640}
+  .typechip .c{opacity:.7;font-size:11px}
+
+  /* listings panel */
+  #listings{margin-top:30px}
+  .listbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px;flex-wrap:wrap}
+  .listbar .who{font-size:15px;font-weight:640}
+  .listbar .who span{color:var(--accent2)}
+  .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--cardmin,280px),1fr));gap:14px;margin-top:14px}
+  .lc{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 15px;box-shadow:var(--shadow);display:flex;flex-direction:column}
+  .lc .t{font-weight:620;font-size:14px;margin-bottom:4px;line-height:1.3}
+  .lc .loc{color:var(--muted);font-size:12px;margin-bottom:10px}
+  .lc .meta{display:flex;gap:8px;flex-wrap:wrap}
+  .tag{background:var(--chip);border:1px solid var(--line);border-radius:20px;padding:3px 10px;font-size:11.5px;color:var(--ink)}
+  .tag.sf{color:var(--accent2);border-color:#1c4038}
+  .lc .src{margin-top:auto;padding-top:10px;margin-top:11px;border-top:1px solid #212c37;font-size:11.5px;color:var(--muted);display:flex;justify-content:space-between;align-items:center}
+  .lc .src a{text-decoration:none}
+  .lc .src a:hover{text-decoration:underline}
+  .empty{color:var(--muted);padding:30px;text-align:center;border:1px dashed var(--line);border-radius:10px}
+  .foot{margin-top:40px;color:var(--muted);font-size:11.5px;border-top:1px solid var(--line);padding-top:16px;line-height:1.6}
+  .foot code{background:var(--panel2);padding:1px 5px;border-radius:3px;color:#b9c6d2}
+</style>
+</head>
+<body>
+<header>
+  <div class="brandrow">
+    <h1>Sublease.com — Broker / Source Leaderboard</h1>
+    <span class="brand-badge">CRE Intelligence</span>
+  </div>
+  <div class="sub">Aggregating <b id="totListings">0</b> listings across <b id="totSources">0</b> sources · <b id="totSF">0</b> SF of sublet space indexed · directory of <b>3,161</b> live sublease listings</div>
+</header>
+
+<div class="wrap">
+  <div class="kpis">
+    <div class="kpi"><div class="n" id="k1">0</div><div class="l">Sample Listings</div><div class="s" id="k1s">&nbsp;</div></div>
+    <div class="kpi"><div class="n" id="k2">0</div><div class="l">Attributed Sources</div><div class="s" id="k2s">&nbsp;</div></div>
+    <div class="kpi"><div class="n" id="k3">0</div><div class="l">Total Square Feet</div><div class="s" id="k3s">&nbsp;</div></div>
+    <div class="kpi"><div class="n" id="k4">0</div><div class="l">Top Market</div><div class="s" id="k4s">&nbsp;</div></div>
+  </div>
+
+  <div class="grid">
+    <div>
+      <div class="section-h">
+        <h2>Source Leaderboard</h2>
+        <span class="hint">click a row to filter listings ↓</span>
+      </div>
+      <div class="card" style="padding:4px 6px 2px">
+        <table id="lbTable">
+          <thead>
+            <tr>
+              <th data-k="name" title="Sort by source name">Source <span class="arw"></span></th>
+              <th data-k="count" title="Sort by listing count">Listings <span class="arw">▼</span></th>
+              <th data-k="sf" title="Sort by total square footage">Total SF <span class="arw"></span></th>
+              <th data-k="avg" title="Sort by average square footage">Avg SF <span class="arw"></span></th>
+              <th data-k="cities" title="Sort by distinct markets covered">Markets <span class="arw"></span></th>
+            </tr>
+          </thead>
+          <tbody id="lbBody"></tbody>
+        </table>
+      </div>
+      <div style="margin-top:10px"><button class="btn" id="csvBtn">⇩ Export leaderboard CSV</button></div>
+    </div>
+
+    <div>
+      <div class="section-h">
+        <h2>Top Markets by <span id="chTitle">Listing Count</span></h2>
+        <div class="chart-toggle" id="chToggle">
+          <button data-m="count" class="on">Count</button>
+          <button data-m="sf">Total SF</button>
+        </div>
+      </div>
+      <div class="card" id="chart"></div>
+      <div class="section-h" style="margin:18px 0 0"><span class="hint">click a bar to filter listings by that market</span></div>
+    </div>
+  </div>
+
+  <div id="listings">
+    <div class="listbar">
+      <div class="who" id="listWho">All listings</div>
+      <button class="btn" id="clearBtn" style="display:none">✕ Clear filter</button>
+    </div>
+
+    <div class="typefilter" id="typeFilter"></div>
+
+    <div class="section-h" style="margin:16px 0 0">
+      <div class="controls">
+        <div class="ctrl">
+          <label for="searchBox">Search</label>
+          <input type="search" id="searchBox" placeholder="title, city, source…" autocomplete="off">
+        </div>
+      </div>
+      <div class="controls">
+        <div class="ctrl">
+          <label for="sortSel">Sort</label>
+          <select id="sortSel">
+            <option value="sf-desc">Size (SF) ↓</option>
+            <option value="sf-asc">Size (SF) ↑</option>
+            <option value="city-az">City A→Z</option>
+            <option value="source-az">Source A→Z</option>
+            <option value="type-az">Space type A→Z</option>
+            <option value="title-az">Title A→Z</option>
+          </select>
+        </div>
+        <div class="ctrl">
+          <label for="densRange">Density</label>
+          <input type="range" id="densRange" min="220" max="440" step="20" value="280">
+        </div>
+      </div>
+    </div>
+
+    <div class="cards" id="cards"></div>
+  </div>
+
+  <div class="foot">
+    Source is derived from each listing's <code>source_url</code> domain. The first 8 records are the verbatim
+    real sample from sublease.com (every field <code>null</code> — no data was captured for those rows); they are
+    counted honestly as <b>Unattributed</b> and excluded from market/SF math. The remaining records are synthesized
+    in the identical schema across varied U.S. markets and brokerages to make the leaderboard meaningful.
+    Self-contained · no external libraries · no network requests.
+  </div>
+</div>
+
+<script>
+/* ---- 1) The REAL sublease.com sample, embedded verbatim ---- */
+const REAL_SAMPLE = [{"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}];
+
+/* ---- 2) Synthesized listings in the SAME shape (varied cities / sources / types) ---- */
+const SYNTH = [
+  {title:"Full Floor Sublease, Class A Tower", city:"New York", state:"NY", space_type:"Office", size_sf:24500, term:"36 mo", source_url:"https://www.cbre.com/properties/389201"},
+  {title:"Creative Loft Sublease, Flatiron", city:"New York", state:"NY", space_type:"Creative/Office", size_sf:8200, term:"24 mo", source_url:"https://www.jll.com/en/listings/nyc-flatiron-8200"},
+  {title:"Partial Floor Available Immediately", city:"New York", state:"NY", space_type:"Office", size_sf:6100, term:"18 mo", source_url:"https://www.cushmanwakefield.com/listings/nyc-6100"},
+  {title:"Midtown Sublet, Furnished + Wired", city:"New York", state:"NY", space_type:"Office", size_sf:13400, term:"30 mo", source_url:"https://www.newmark.com/listings/nyc-midtown-13400"},
+  {title:"Plug-and-Play Tech Space, SoMa", city:"San Francisco", state:"CA", space_type:"Office", size_sf:15300, term:"30 mo", source_url:"https://www.jll.com/en/listings/sf-soma-15300"},
+  {title:"Furnished Sublease, Financial District", city:"San Francisco", state:"CA", space_type:"Office", size_sf:9800, term:"24 mo", source_url:"https://www.cbre.com/properties/sf-fd-9800"},
+  {title:"R&D / Lab Sublet, Mission Bay", city:"San Francisco", state:"CA", space_type:"Lab/R&D", size_sf:22000, term:"48 mo", source_url:"https://www.newmark.com/listings/sf-missionbay"},
+  {title:"Loop Office Sublease, Move-In Ready", city:"Chicago", state:"IL", space_type:"Office", size_sf:11750, term:"36 mo", source_url:"https://www.cushmanwakefield.com/listings/chi-loop"},
+  {title:"West Loop Creative Sublet", city:"Chicago", state:"IL", space_type:"Creative/Office", size_sf:5400, term:"12 mo", source_url:"https://www.colliers.com/en-us/listings/chi-westloop"},
+  {title:"River North Sublease, 2 Floors", city:"Chicago", state:"IL", space_type:"Office", size_sf:19800, term:"42 mo", source_url:"https://www.cbre.com/properties/chi-rivernorth"},
+  {title:"Downtown High-Rise Sublease", city:"Austin", state:"TX", space_type:"Office", size_sf:13200, term:"30 mo", source_url:"https://www.cbre.com/properties/atx-9921"},
+  {title:"Domain District Office Sublet", city:"Austin", state:"TX", space_type:"Office", size_sf:7600, term:"24 mo", source_url:"https://www.avisonyoung.com/listings/atx-domain"},
+  {title:"East Austin Flex Sublease", city:"Austin", state:"TX", space_type:"Flex", size_sf:12900, term:"36 mo", source_url:"https://www.leeassociates.com/listings/atx-east-flex"},
+  {title:"Warehouse / Flex Sublease", city:"Dallas", state:"TX", space_type:"Industrial", size_sf:41000, term:"60 mo", source_url:"https://www.jll.com/en/listings/dfw-flex-41000"},
+  {title:"Uptown Sublease, Furnished", city:"Dallas", state:"TX", space_type:"Office", size_sf:9200, term:"18 mo", source_url:"https://www.transwestern.com/listings/dfw-uptown"},
+  {title:"Legacy West Sublet, Corporate HQ", city:"Dallas", state:"TX", space_type:"Office", size_sf:28600, term:"48 mo", source_url:"https://www.streamrealty.com/listings/dfw-legacy"},
+  {title:"Class A Office Sublet, Buckhead", city:"Atlanta", state:"GA", space_type:"Office", size_sf:16800, term:"36 mo", source_url:"https://www.colliers.com/en-us/listings/atl-buckhead"},
+  {title:"Midtown Sublease Opportunity", city:"Atlanta", state:"GA", space_type:"Office", size_sf:5900, term:"24 mo", source_url:"https://www.newmark.com/listings/atl-midtown"},
+  {title:"West Midtown Industrial Sublet", city:"Atlanta", state:"GA", space_type:"Industrial", size_sf:33500, term:"60 mo", source_url:"https://www.leeassociates.com/listings/atl-westmid"},
+  {title:"Seaport Office Sublet", city:"Boston", state:"MA", space_type:"Office", size_sf:12400, term:"30 mo", source_url:"https://www.cbre.com/properties/bos-seaport"},
+  {title:"Cambridge Lab Sublease", city:"Boston", state:"MA", space_type:"Lab/R&D", size_sf:18600, term:"48 mo", source_url:"https://www.cushmanwakefield.com/listings/bos-cambridge"},
+  {title:"Back Bay Boutique Sublet", city:"Boston", state:"MA", space_type:"Office", size_sf:4700, term:"12 mo", source_url:"https://www.jll.com/en/listings/bos-backbay"},
+  {title:"Century City Sublet, Partial Floor", city:"Los Angeles", state:"CA", space_type:"Office", size_sf:10200, term:"24 mo", source_url:"https://www.jll.com/en/listings/la-centurycity"},
+  {title:"Culver City Creative Space", city:"Los Angeles", state:"CA", space_type:"Creative/Office", size_sf:7300, term:"18 mo", source_url:"https://www.savills.us/listings/la-culvercity"},
+  {title:"DTLA Historic Core Sublease", city:"Los Angeles", state:"CA", space_type:"Office", size_sf:14800, term:"36 mo", source_url:"https://www.cbre.com/properties/la-dtla"},
+  {title:"Denver Tech Center Sublease", city:"Denver", state:"CO", space_type:"Office", size_sf:8800, term:"24 mo", source_url:"https://www.avisonyoung.com/listings/den-dtc"},
+  {title:"RiNo Creative Sublet", city:"Denver", state:"CO", space_type:"Creative/Office", size_sf:6200, term:"18 mo", source_url:"https://www.kidder.com/listings/den-rino"},
+  {title:"South Lake Union Sublet", city:"Seattle", state:"WA", space_type:"Office", size_sf:14100, term:"36 mo", source_url:"https://www.colliers.com/en-us/listings/sea-slu"},
+  {title:"Retail Sublease, Ballard", city:"Seattle", state:"WA", space_type:"Retail", size_sf:3200, term:"12 mo", source_url:"https://www.savills.us/listings/sea-ballard"},
+  {title:"Pioneer Square Office Sublet", city:"Seattle", state:"WA", space_type:"Office", size_sf:9600, term:"30 mo", source_url:"https://www.kidder.com/listings/sea-pioneer"},
+  {title:"Brickell Office Sublet", city:"Miami", state:"FL", space_type:"Office", size_sf:9600, term:"30 mo", source_url:"https://www.transwestern.com/listings/mia-brickell"},
+  {title:"Wynwood Creative Sublease", city:"Miami", state:"FL", space_type:"Creative/Office", size_sf:5100, term:"18 mo", source_url:"https://www.cushmanwakefield.com/listings/mia-wynwood"},
+  {title:"Downtown Sublease, Skyline Views", city:"Phoenix", state:"AZ", space_type:"Office", size_sf:11200, term:"36 mo", source_url:"https://www.leeassociates.com/listings/phx-downtown"},
+  {title:"Tempe Flex Sublet", city:"Phoenix", state:"AZ", space_type:"Flex", size_sf:17400, term:"48 mo", source_url:"https://www.colliers.com/en-us/listings/phx-tempe"},
+  {title:"Nashville Gulch Office Sublease", city:"Nashville", state:"TN", space_type:"Office", size_sf:8300, term:"24 mo", source_url:"https://www.avisonyoung.com/listings/bna-gulch"},
+  {title:"Old City Retail Sublet", city:"Philadelphia", state:"PA", space_type:"Retail", size_sf:2800, term:"12 mo", source_url:"https://www.jll.com/en/listings/phl-oldcity"},
+  {title:"Center City Office Sublease", city:"Philadelphia", state:"PA", space_type:"Office", size_sf:12700, term:"36 mo", source_url:"https://www.newmark.com/listings/phl-centercity"}
+];
+
+const DATA = REAL_SAMPLE.concat(SYNTH);
+
+/* ---- helpers ---- */
+const fmt = n => (n==null?0:n).toLocaleString('en-US');
+const NONE = '__none__';
+function domainOf(url){
+  if(!url) return null;
+  try{ return new URL(url).hostname.replace(/^www\./,''); }catch(e){ return null; }
+}
+const SOURCE_NAMES = {
+  'cbre.com':'CBRE','jll.com':'JLL','cushmanwakefield.com':'Cushman & Wakefield',
+  'newmark.com':'Newmark','colliers.com':'Colliers','avisonyoung.com':'Avison Young',
+  'transwestern.com':'Transwestern','savills.us':'Savills','leeassociates.com':'Lee & Associates',
+  'streamrealty.com':'Stream Realty','kidder.com':'Kidder Mathews'
+};
+function sourceName(dom){ return SOURCE_NAMES[dom] || (dom || 'Unattributed'); }
+const marketKey = l => (l.city ? l.city + ', ' + l.state : null);
+
+/* ---- aggregate by source ---- */
+function aggregate(){
+  const m = new Map();
+  DATA.forEach(l=>{
+    const dom = domainOf(l.source_url);
+    const key = dom || NONE;
+    if(!m.has(key)) m.set(key,{dom, name:sourceName(dom), count:0, sf:0, cities:new Set()});
+    const s = m.get(key);
+    s.count++;
+    s.sf += (l.size_sf||0);
+    const mk = marketKey(l); if(mk) s.cities.add(mk);
+  });
+  return [...m.values()].map(s=>({
+    dom:s.dom, name:s.name, count:s.count, sf:s.sf,
+    avg: s.count ? Math.round(s.sf/s.count) : 0,
+    cities: s.cities.size
+  }));
+}
+const SOURCES = aggregate();
+
+/* ---- markets (by city) for chart ---- */
+function markets(){
+  const m = new Map();
+  DATA.forEach(l=>{
+    const k = marketKey(l); if(!k) return;
+    if(!m.has(k)) m.set(k,{name:k,count:0,sf:0});
+    const x=m.get(k); x.count++; x.sf+=(l.size_sf||0);
+  });
+  return [...m.values()];
+}
+const MARKETS = markets();
+
+/* ---- space types for chip filter ---- */
+function spaceTypes(){
+  const m = new Map();
+  DATA.forEach(l=>{ if(!l.space_type) return; m.set(l.space_type,(m.get(l.space_type)||0)+1); });
+  return [...m.entries()].sort((a,b)=>b[1]-a[1]);
+}
+const TYPES = spaceTypes();
+
+/* ---- KPIs ---- */
+const totSF = DATA.reduce((a,l)=>a+(l.size_sf||0),0);
+const attributedSources = SOURCES.filter(s=>s.dom).length;
+const topSrc = [...SOURCES].filter(s=>s.dom).sort((a,b)=>b.count-a.count)[0];
+const topMkt = [...MARKETS].sort((a,b)=>b.count-a.count)[0];
+const unattributed = DATA.filter(l=>!l.source_url).length;
+document.getElementById('totListings').textContent = fmt(DATA.length);
+document.getElementById('totSources').textContent = attributedSources;
+document.getElementById('totSF').textContent = fmt(totSF);
+document.getElementById('k1').textContent = fmt(DATA.length);
+document.getElementById('k1s').textContent = unattributed + ' unattributed (real sample)';
+document.getElementById('k2').textContent = attributedSources;
+document.getElementById('k2s').textContent = topSrc ? 'Top: ' + topSrc.name : ' ';
+document.getElementById('k3').textContent = fmt(totSF);
+document.getElementById('k3s').textContent = 'across ' + MARKETS.length + ' markets';
+document.getElementById('k4').textContent = topMkt ? topMkt.name.split(',')[0] : '—';
+document.getElementById('k4s').textContent = topMkt ? topMkt.count + ' listings · ' + fmt(topMkt.sf) + ' SF' : ' ';
+
+/* ---- state ---- */
+let sortKey='count', sortDir=-1;
+let activeSource=null, cityFilter=null, typeFilter=null;
+let listSort = localStorage.getItem('sub_listsort') || 'sf-desc';
+let searchQ = '';
+
+/* ---- render leaderboard table ---- */
+function renderTable(){
+  const rows=[...SOURCES].sort((a,b)=>{
+    const av=a[sortKey], bv=b[sortKey];
+    if(typeof av==='string') return sortDir*av.localeCompare(bv);
+    return sortDir*(av-bv);
+  });
+  const maxCount=Math.max(...rows.map(r=>r.count),1);
+  const body=document.getElementById('lbBody');
+  body.innerHTML='';
+  rows.forEach((r,i)=>{
+    const tr=document.createElement('tr');
+    const key=r.dom||NONE;
+    tr.tabIndex=0;
+    tr.setAttribute('role','button');
+    if(activeSource===key) tr.className='active';
+    tr.innerHTML=`
+      <td class="barcell">
+        <span class="rank">${i+1}</span>
+        <span class="srcname">${r.name}</span>
+        <span class="dom">${r.dom||'no source url'}</span>
+        <span class="mini"><i style="width:${(r.count/maxCount*100).toFixed(1)}%"></i></span>
+      </td>
+      <td>${fmt(r.count)}</td>
+      <td>${fmt(r.sf)}</td>
+      <td>${fmt(r.avg)}</td>
+      <td>${r.cities}</td>`;
+    tr.onclick=()=>toggleSource(key);
+    tr.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggleSource(key); } };
+    body.appendChild(tr);
+  });
+  document.querySelectorAll('#lbTable thead th').forEach(th=>{
+    const k=th.dataset.k, a=th.querySelector('.arw');
+    a.textContent = (k===sortKey) ? (sortDir<0?'▼':'▲') : '';
+  });
+}
+document.querySelectorAll('#lbTable thead th').forEach(th=>{
+  th.onclick=()=>{
+    const k=th.dataset.k;
+    if(k===sortKey) sortDir*=-1;
+    else { sortKey=k; sortDir=(k==='name')?1:-1; }
+    renderTable();
+  };
+});
+
+/* ---- chart ---- */
+let chartMode='count';
+function renderChart(){
+  const rows=[...MARKETS].sort((a,b)=>b[chartMode]-a[chartMode]).slice(0,8);
+  const max=Math.max(...rows.map(r=>r[chartMode]),1);
+  const c=document.getElementById('chart');
+  c.innerHTML='';
+  rows.forEach(r=>{
+    const div=document.createElement('div');
+    div.className='chart-row' + (cityFilter===r.name?' active':'');
+    div.tabIndex=0;
+    div.setAttribute('role','button');
+    const val = chartMode==='sf' ? fmt(r.sf) : r.count;
+    div.innerHTML=`
+      <span class="name" title="${r.name}">${r.name}</span>
+      <span class="track"><span class="fill" style="width:${(r[chartMode]/max*100).toFixed(1)}%"></span></span>
+      <span class="val">${val}</span>`;
+    div.onclick=()=>filterByCity(r.name);
+    div.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); filterByCity(r.name); } };
+    c.appendChild(div);
+  });
+  document.getElementById('chTitle').textContent = chartMode==='sf'?'Total SF':'Listing Count';
+}
+document.querySelectorAll('#chToggle button').forEach(b=>{
+  b.onclick=()=>{
+    document.querySelectorAll('#chToggle button').forEach(x=>x.classList.remove('on'));
+    b.classList.add('on'); chartMode=b.dataset.m; renderChart();
+  };
+});
+
+/* ---- space-type chip filter ---- */
+function renderTypeChips(){
+  const wrap=document.getElementById('typeFilter');
+  wrap.innerHTML='';
+  TYPES.forEach(([t,n])=>{
+    const chip=document.createElement('div');
+    chip.className='typechip'+(typeFilter===t?' on':'');
+    chip.tabIndex=0;
+    chip.setAttribute('role','button');
+    chip.innerHTML=`${t} <span class="c">${n}</span>`;
+    chip.onclick=()=>{ typeFilter = (typeFilter===t)?null:t; renderTypeChips(); renderListings(); };
+    chip.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); chip.onclick(); } };
+    wrap.appendChild(chip);
+  });
+}
+
+/* ---- listings ---- */
+function toggleSource(key){
+  activeSource = (activeSource===key)?null:key;
+  cityFilter=null;
+  renderTable(); renderChart(); renderListings();
+}
+function filterByCity(city){
+  cityFilter = (cityFilter===city)?null:city;
+  activeSource=null;
+  renderTable(); renderChart(); renderListings();
+}
+function clearFilters(){
+  activeSource=null; cityFilter=null; typeFilter=null;
+  renderTable(); renderChart(); renderTypeChips(); renderListings();
+}
+
+function sortListings(list){
+  const by=listSort;
+  const arr=[...list];
+  const sfv = l => (l.size_sf==null ? -1 : l.size_sf);
+  const s = (a,b,f)=> (a[f]||'').localeCompare(b[f]||'');
+  arr.sort((a,b)=>{
+    if(by==='sf-desc') return sfv(b)-sfv(a);
+    if(by==='sf-asc')  return sfv(a)-sfv(b);
+    if(by==='city-az') return s(a,b,'city');
+    if(by==='type-az') return s(a,b,'space_type');
+    if(by==='title-az')return s(a,b,'title');
+    if(by==='source-az') return sourceName(domainOf(a.source_url)).localeCompare(sourceName(domainOf(b.source_url)));
+    return 0;
+  });
+  return arr;
+}
+
+function renderListings(){
+  let list=DATA, whoTxt='All listings', showClear=false;
+  if(activeSource){
+    list=list.filter(l=>(domainOf(l.source_url)||NONE)===activeSource);
+    whoTxt=`Listings from <span>${sourceName(activeSource===NONE?null:activeSource)}</span>`;
+    showClear=true;
+  } else if(cityFilter){
+    list=list.filter(l=>marketKey(l)===cityFilter);
+    whoTxt=`Listings in <span>${cityFilter}</span>`;
+    showClear=true;
+  }
+  if(typeFilter){
+    list=list.filter(l=>l.space_type===typeFilter);
+    whoTxt += ` · <span>${typeFilter}</span>`;
+    showClear=true;
+  }
+  if(searchQ){
+    const q=searchQ.toLowerCase();
+    list=list.filter(l=>{
+      const hay=[l.title,l.city,l.state,l.space_type,sourceName(domainOf(l.source_url))].filter(Boolean).join(' ').toLowerCase();
+      return hay.includes(q);
+    });
+    showClear=true;
+  }
+
+  list=sortListings(list);
+  document.getElementById('listWho').innerHTML = whoTxt + ` &nbsp;·&nbsp; <span style="color:var(--muted);font-weight:400">${list.length}</span>`;
+  document.getElementById('clearBtn').style.display = showClear?'inline-block':'none';
+
+  const cards=document.getElementById('cards');
+  cards.innerHTML='';
+  if(!list.length){
+    cards.innerHTML='<div class="empty" style="grid-column:1/-1">No listings match this filter.</div>';
+    return;
+  }
+  const frag=document.createDocumentFragment();
+  list.forEach(l=>{
+    const title = l.title || '<span style="color:var(--muted)">Untitled listing — no data</span>';
+    const loc = (l.city? (l.city+', '+l.state) : '<span style="color:var(--muted)">Location unavailable</span>');
+    const dom = domainOf(l.source_url);
+    const div=document.createElement('div');
+    div.className='lc';
+    let tags='';
+    if(l.space_type) tags+=`<span class="tag">${l.space_type}</span>`;
+    if(l.size_sf) tags+=`<span class="tag sf">${fmt(l.size_sf)} SF</span>`;
+    if(l.term) tags+=`<span class="tag">${l.term}</span>`;
+    if(!tags) tags='<span class="tag" style="color:var(--muted)">no specs</span>';
+    div.innerHTML=`
+      <div class="t">${title}</div>
+      <div class="loc">${loc}</div>
+      <div class="meta">${tags}</div>
+      <div class="src">
+        <span>${sourceName(dom)}</span>
+        ${l.source_url?`<a href="${l.source_url}" target="_blank" rel="noopener noreferrer">view ↗</a>`:'<span style="color:var(--muted)">—</span>'}
+      </div>`;
+    frag.appendChild(div);
+  });
+  cards.appendChild(frag);
+}
+document.getElementById('clearBtn').onclick=clearFilters;
+
+/* ---- listings controls ---- */
+const sortSel=document.getElementById('sortSel');
+sortSel.value=listSort;
+sortSel.onchange=()=>{ listSort=sortSel.value; localStorage.setItem('sub_listsort',listSort); renderListings(); };
+
+const densRange=document.getElementById('densRange');
+const savedDens=localStorage.getItem('sub_density');
+if(savedDens){ densRange.value=savedDens; }
+function applyDensity(){ document.documentElement.style.setProperty('--cardmin', densRange.value+'px'); }
+densRange.oninput=()=>{ applyDensity(); localStorage.setItem('sub_density',densRange.value); };
+applyDensity();
+
+const searchBox=document.getElementById('searchBox');
+searchBox.oninput=()=>{ searchQ=searchBox.value.trim(); renderListings(); };
+
+/* ---- CSV export of leaderboard ---- */
+document.getElementById('csvBtn').onclick=()=>{
+  const rows=[...SOURCES].sort((a,b)=>{
+    const av=a[sortKey], bv=b[sortKey];
+    if(typeof av==='string') return sortDir*av.localeCompare(bv);
+    return sortDir*(av-bv);
+  });
+  const head=['Rank','Source','Domain','Listings','Total SF','Avg SF','Markets'];
+  const lines=[head.join(',')];
+  rows.forEach((r,i)=>{
+    const cells=[i+1,`"${r.name}"`,r.dom||'',r.count,r.sf,r.avg,r.cities];
+    lines.push(cells.join(','));
+  });
+  const blob=new Blob([lines.join('\n')],{type:'text/csv'});
+  const a=document.createElement('a');
+  a.href=URL.createObjectURL(blob);
+  a.download='sublease-source-leaderboard.csv';
+  a.click();
+  URL.revokeObjectURL(a.href);
+};
+
+/* ---- init ---- */
+renderTable();
+renderChart();
+renderTypeChips();
+renderListings();
+</script>
+</body>
+</html>

(oldest)  ·  back to Sublease Leaderboard  ·  pin unattributed bucket last; add search, sort+density, type 178f481 →