← back to Sublease Leaderboard
index.html
586 lines
<!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 = '';
/* sort sources but always pin the un-sourced bucket to the bottom —
it's not a broker, so it shouldn't compete for rank #1 */
function sortedSources(){
return [...SOURCES].sort((a,b)=>{
if(!a.dom !== !b.dom) return a.dom ? -1 : 1;
const av=a[sortKey], bv=b[sortKey];
if(typeof av==='string') return sortDir*av.localeCompare(bv);
return sortDir*(av-bv);
});
}
/* ---- render leaderboard table ---- */
function renderTable(){
const rows=sortedSources();
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=sortedSources();
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>