← 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"> </div></div>
+ <div class="kpi"><div class="n" id="k2">0</div><div class="l">Attributed Sources</div><div class="s" id="k2s"> </div></div>
+ <div class="kpi"><div class="n" id="k3">0</div><div class="l">Total Square Feet</div><div class="s" id="k3s"> </div></div>
+ <div class="kpi"><div class="n" id="k4">0</div><div class="l">Top Market</div><div class="s" id="k4s"> </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 + ` · <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 →