J's prompt: shoot for the stars, frame the data corpus's value as a
predictive signal, not just a contractor directory. The thesis is
that every name in this corpus is also a forward indicator on public
equities — permits filed today predict construction starts in ~45
days, staffing in ~30, revenue recognition months later. The
associated-ticker network surfaces this signal before any 10-Q does.
Two new layers above the basket:
1. HERO THESIS PANEL — "Chicago Construction Activity Signal Engine"
header + 3-line value statement, then 4 live metrics:
- BAI (Building Activity Index) — attribution-weighted average of
day-change % across surfaced issuers. Weight = attribution count
so issuers we have more depth on count more. Today: +0.76%
(9 issuers · top contributors FCBC +2.4%, ACRE +1.7%, JPM +1.5%).
Color-coded green/red.
- Indexed build value — total $ of permits attributable to ANY
public issuer in this view. Today: $344M.
- Network depth — issuers / attribution edges. Today: 9 / 15.
This is the "we see what nobody else sees" metric: how many
contractors are bridges from a private builder back to a public
equity holder.
- Market replication roadmap — chips showing "Chicago — live ·
NYC DOB — adapter ready · LA County · Houston BCD · Boston ISD
· DC DCRA". Frames the corpus as metro-agnostic from day one.
2. PER-TICKER ACTIVITY MAP — when a basket card is clicked, a leaflet
map appears below the basket plotting that ticker's geocoded permit
activity. Pulls /intelligence/contractor_profile for up to 6
attributed contractors, merges their geocoded permits, plots on a
dark Chicago tile layer. Color-banded by permit cost (green <$100K,
amber $100K-$1M, red ≥$1M). Click TGT → 23 Target permits across
Chicago; click JPM → JPMorgan-adjacent contractor activity. Cached
per ticker so toggling is instant.
Verified end-to-end on devop.live/lakehouse/profiler:
Default load: hero panel renders with all 4 metrics, basket strip
with 9 issuers + live prices in 669ms.
Click TGT : signal map activates, "23 geocoded permits across
1 contractor", table filters to 2 rows.
Tooltip on basket cards: full reason path including matched name +
contributors attributed to that ticker.
Architecture-side: zero new server code — all metrics computed
client-side from the existing profiler_index + ticker_quotes payloads.
The corpus already had the value; the page just needed to articulate it.
600 lines
31 KiB
HTML
600 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Profiler Index · Staffing Co-Pilot</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
html,body{overflow-x:hidden}
|
|
body{font-family:'Inter',-apple-system,system-ui,sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.6}
|
|
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center}
|
|
.bar h1{font-size:14px;font-weight:600;color:#e6edf3}
|
|
.bar nav a{color:#545d68;text-decoration:none;font-size:12px;padding:6px 14px;border-radius:6px;margin-left:4px}
|
|
.bar nav a:hover{color:#e6edf3;background:#161b22}
|
|
.content{max-width:1200px;margin:0 auto;padding:24px 20px 40px}
|
|
.controls{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
.controls input,.controls select{padding:9px 12px;background:#161b22;border:1px solid #21262d;border-radius:6px;color:#e6edf3;font-size:13px;outline:none}
|
|
.controls input:focus,.controls select:focus{border-color:#388bfd}
|
|
.controls input.s{flex:1;min-width:240px}
|
|
.controls .meta{font-size:11px;color:#8b949e;margin-left:auto}
|
|
.summary{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px 16px;margin-bottom:14px;font-size:12px;color:#8b949e}
|
|
.summary b{color:#e6edf3;font-weight:600}
|
|
table{width:100%;border-collapse:collapse;background:#0d1117;border:1px solid #171d27;border-radius:10px;overflow:hidden}
|
|
th{font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;font-weight:600;text-align:left;padding:12px;background:#0a0d12;border-bottom:1px solid #171d27;cursor:pointer;user-select:none}
|
|
th:hover{color:#e6edf3}
|
|
th .arrow{font-size:9px;margin-left:4px;color:#388bfd}
|
|
td{padding:11px 12px;border-bottom:1px solid #1f2631;font-size:13px}
|
|
tr:last-child td{border-bottom:none}
|
|
tr:hover td{background:#0a0d12}
|
|
td.name a{color:#58a6ff;text-decoration:none;font-weight:600}
|
|
td.name a:hover{text-decoration:underline}
|
|
td.right{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}
|
|
td.role{font-size:10px;color:#8b949e}
|
|
td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#161b22;border:1px solid #21262d;color:#8b949e;margin-right:4px;text-transform:uppercase;letter-spacing:0.5px}
|
|
.tickers{display:flex;gap:4px;flex-wrap:wrap;margin-top:3px}
|
|
.ticker-pill{display:inline-block;padding:1px 7px;border-radius:5px;font-size:10px;font-weight:700;font-family:ui-monospace,SFMono-Regular,monospace;letter-spacing:0.3px;cursor:help}
|
|
.ticker-pill.direct{background:#0d2818;border:1px solid #2ea04388;color:#3fb950}
|
|
.ticker-pill.parent{background:#1a1410;border:1px solid #d2992288;color:#d29922}
|
|
.ticker-pill.associated{background:#0d1830;border:1px solid #58a6ff66;color:#58a6ff}
|
|
.ticker-pill.exact{background:#0d2818;border:1px solid #2ea043;color:#3fb950}
|
|
|
|
/* Hero — the thesis panel that frames the data corpus's value. */
|
|
.thesis{background:linear-gradient(135deg,#0d2818 0%,#0d1830 50%,#1a1410 100%);border:1px solid #2ea04344;border-radius:12px;padding:18px 22px;margin-bottom:14px;position:relative;overflow:hidden}
|
|
.thesis::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#3fb950 0%,#58a6ff 50%,#d29922 100%)}
|
|
.thesis h2{font-size:18px;color:#e6edf3;font-weight:700;letter-spacing:-0.4px;margin-bottom:6px}
|
|
.thesis .sub{font-size:12px;color:#8b949e;line-height:1.6;margin-bottom:14px;max-width:880px}
|
|
.thesis .sub b{color:#3fb950;font-weight:600}
|
|
.bai-row{display:flex;gap:24px;align-items:baseline;flex-wrap:wrap;margin-bottom:14px}
|
|
.bai-block{display:flex;flex-direction:column;gap:2px}
|
|
.bai-label{font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;font-weight:700}
|
|
.bai-value{font-size:26px;font-weight:700;color:#e6edf3;font-family:ui-monospace,monospace;letter-spacing:-0.5px;font-variant-numeric:tabular-nums}
|
|
.bai-value.up{color:#3fb950}
|
|
.bai-value.down{color:#f85149}
|
|
.bai-sub{font-size:10px;color:#8b949e;margin-top:1px}
|
|
.markets-strip{display:flex;gap:6px;flex-wrap:wrap;font-size:10px}
|
|
.market-pill{padding:3px 9px;border-radius:9px;font-weight:600;border:1px solid;letter-spacing:0.4px}
|
|
.market-pill.live{background:#0d2818;border-color:#3fb950;color:#3fb950}
|
|
.market-pill.next{background:#0d1830;border-color:#58a6ff;color:#58a6ff}
|
|
.market-pill.queue{background:#161b22;border-color:#21262d;color:#545d68}
|
|
.market-pill.queue::before{content:'· '}
|
|
|
|
/* Map panel below basket — populates when a ticker is selected. */
|
|
.signal-map-wrap{display:none;background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px;margin-bottom:14px}
|
|
.signal-map-wrap.active{display:block}
|
|
.signal-map-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px}
|
|
.signal-map-title{font-size:11px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;font-weight:600}
|
|
.signal-map-title b{color:#58a6ff;font-family:ui-monospace,monospace}
|
|
.signal-map-meta{font-size:11px;color:#8b949e}
|
|
.signal-map{height:340px;border-radius:8px;border:1px solid #1f2631;overflow:hidden}
|
|
.signal-map .leaflet-container{background:#0a0d12}
|
|
|
|
/* Scrolling ticker basket — top strip showing every public issuer
|
|
the profiler index has surfaced, with live price + day-change. */
|
|
.basket-wrap{background:#0a0d12;border:1px solid #171d27;border-radius:10px;margin-bottom:14px;overflow:hidden;position:relative}
|
|
.basket-label{padding:10px 16px 4px;font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.3px;font-weight:600;display:flex;justify-content:space-between;align-items:baseline}
|
|
.basket-label .meta{font-weight:400;color:#3d444d;font-size:10px;text-transform:none;letter-spacing:0}
|
|
.basket-track{display:flex;gap:0;overflow-x:auto;scroll-behavior:smooth;padding:6px 8px 12px;scrollbar-width:thin;scrollbar-color:#21262d transparent}
|
|
.basket-track::-webkit-scrollbar{height:6px}
|
|
.basket-track::-webkit-scrollbar-thumb{background:#21262d;border-radius:3px}
|
|
.basket-track::-webkit-scrollbar-thumb:hover{background:#388bfd}
|
|
.bk-card{flex:0 0 auto;min-width:140px;background:#0d1117;border:1px solid #21262d;border-radius:8px;padding:10px 12px;margin:0 4px;cursor:pointer;transition:all 0.12s;position:relative}
|
|
.bk-card:hover{border-color:#58a6ff;background:#0d1a30;transform:translateY(-1px)}
|
|
.bk-card.selected{border-color:#58a6ff;background:#0d1a30;box-shadow:0 0 0 1px #58a6ff;}
|
|
.bk-card .tk{font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;font-weight:700;color:#e6edf3;letter-spacing:0.3px}
|
|
.bk-card .px{font-family:ui-monospace,SFMono-Regular,monospace;font-size:14px;font-weight:600;color:#e6edf3;margin-top:3px;font-variant-numeric:tabular-nums}
|
|
.bk-card .ch{font-family:ui-monospace,monospace;font-size:11px;margin-top:1px;font-variant-numeric:tabular-nums}
|
|
.bk-card .ch.up{color:#3fb950}
|
|
.bk-card .ch.down{color:#f85149}
|
|
.bk-card .ch.flat{color:#545d68}
|
|
.bk-card .meta{font-size:9px;color:#545d68;margin-top:5px;text-transform:uppercase;letter-spacing:0.6px}
|
|
.bk-card .kind-bar{position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px}
|
|
.bk-card .kind-bar.exact,.bk-card .kind-bar.direct{background:#3fb950}
|
|
.bk-card .kind-bar.parent{background:#d29922}
|
|
.bk-card .kind-bar.associated{background:#58a6ff}
|
|
.bk-card .kind-bar.mixed{background:linear-gradient(180deg,#3fb950 0%,#58a6ff 100%)}
|
|
.bk-card.no-quote .px{color:#545d68}
|
|
.basket-empty{padding:18px;font-size:11px;color:#545d68;font-style:italic;text-align:center}
|
|
.basket-clear{margin-left:8px;font-size:10px;color:#58a6ff;cursor:pointer;border:none;background:none;text-decoration:underline}
|
|
.cost-band-1{color:#3fb950}
|
|
.cost-band-2{color:#d29922}
|
|
.cost-band-3{color:#f85149}
|
|
.loading{text-align:center;padding:60px;font-size:13px;color:#3d444d}
|
|
.empty{text-align:center;padding:40px;font-size:12px;color:#545d68;font-style:italic}
|
|
.foot{margin-top:14px;font-size:10px;color:#484f58;line-height:1.6}
|
|
@media(max-width:640px){.bar{padding:0 14px}.content{padding:14px}th,td{padding:8px 6px;font-size:11px}}
|
|
</style>
|
|
</head><body>
|
|
<div class="bar">
|
|
<h1>Staffing Co-Pilot · Profiler Index</h1>
|
|
<nav>
|
|
<a href="" id="back-dashboard">← Dashboard</a>
|
|
<a href="" id="back-console">Console</a>
|
|
</nav>
|
|
</div>
|
|
<div class="content">
|
|
<!-- Hero thesis — frames what this data corpus actually is. The
|
|
profiler index isn't just a contractor directory; it's a
|
|
construction-activity signal that surfaces public issuers months
|
|
before quarterly earnings does. Each metric here is computed
|
|
from the live data, not pre-baked. -->
|
|
<div class="thesis" id="thesis">
|
|
<h2>Chicago Construction Activity Signal Engine</h2>
|
|
<div class="sub">
|
|
Every contractor name in this corpus is also a forward indicator on the public equities they touch. Permits filed today predict construction starts ~45 days out, staffing windows ~2 weeks before that, and revenue recognition months later. The associated-ticker network surfaces this signal <b>before</b> it lands in any 10-Q.
|
|
</div>
|
|
<div class="bai-row">
|
|
<div class="bai-block">
|
|
<span class="bai-label">Building Activity Index — today</span>
|
|
<span class="bai-value" id="bai-value">—</span>
|
|
<span class="bai-sub" id="bai-sub">awaiting basket prices</span>
|
|
</div>
|
|
<div class="bai-block">
|
|
<span class="bai-label">Indexed build value</span>
|
|
<span class="bai-value" id="bav-value">—</span>
|
|
<span class="bai-sub" id="bav-sub">across surfaced issuers</span>
|
|
</div>
|
|
<div class="bai-block">
|
|
<span class="bai-label">Network depth</span>
|
|
<span class="bai-value" id="net-value">—</span>
|
|
<span class="bai-sub" id="net-sub">issuers · attributions</span>
|
|
</div>
|
|
<div class="bai-block" style="flex:1;min-width:240px">
|
|
<span class="bai-label">Market replication roadmap</span>
|
|
<div class="markets-strip" style="margin-top:4px">
|
|
<span class="market-pill live">Chicago — live</span>
|
|
<span class="market-pill next">NYC DOB — adapter ready</span>
|
|
<span class="market-pill queue">LA County · Houston BCD · Boston ISD · DC DCRA</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="basket-wrap" id="basket-wrap" style="display:none">
|
|
<div class="basket-label">
|
|
<span><span id="bk-count">0</span> public issuers in this view <span class="meta" id="bk-meta"></span></span>
|
|
<button class="basket-clear" id="bk-clear" style="display:none" type="button">clear filter</button>
|
|
</div>
|
|
<div class="basket-track" id="basket"></div>
|
|
</div>
|
|
|
|
<!-- Per-ticker permit map — shows where the selected issuer's
|
|
attributed contractor activity is actually happening. Same
|
|
leaflet pattern as the contractor profile, scoped to one ticker. -->
|
|
<div class="signal-map-wrap" id="signal-map-wrap">
|
|
<div class="signal-map-header">
|
|
<span class="signal-map-title">Where <b id="signal-map-ticker">—</b> activity is happening</span>
|
|
<span class="signal-map-meta" id="signal-map-meta">—</span>
|
|
</div>
|
|
<div class="signal-map" id="signal-map"></div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<input class="s" id="q" type="text" placeholder="Filter by contractor name (e.g., Target, Turner)" autocomplete="off">
|
|
<select id="since">
|
|
<option value="2025-06-01">Since June 2025</option>
|
|
<option value="2024-01-01">Since 2024</option>
|
|
<option value="2020-01-01">Since 2020 (deeper history)</option>
|
|
</select>
|
|
<select id="min-cost">
|
|
<option value="500000">$500K+</option>
|
|
<option value="250000" selected>$250K+</option>
|
|
<option value="100000">$100K+</option>
|
|
<option value="50000">$50K+</option>
|
|
</select>
|
|
<span class="meta" id="meta">Loading…</span>
|
|
</div>
|
|
<div class="summary" id="summary" style="display:none"></div>
|
|
<div id="result"><div class="loading">Loading the directory from Chicago Socrata…</div></div>
|
|
<div class="foot">Aggregations sourced live from data.cityofchicago.org (Building Permits dataset ydr8-5enu). Contractor names appear when listed as contact_1 or contact_2 on a permit. Click any name to open the full profile — heat map, project index, history, 12 awaiting public-data sources.</div>
|
|
</div>
|
|
<script>
|
|
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
|
document.getElementById('back-dashboard').href = P+'/';
|
|
document.getElementById('back-console').href = P+'/console';
|
|
|
|
var sortKey='total_cost', sortDir='desc';
|
|
var lastRows=[];
|
|
var tickerFilter=null; // selected ticker for filtering the table
|
|
var lastQuotes={}; // ticker → quote (price, day_change_pct)
|
|
var lastBasket=[]; // basket rows aggregated from lastRows
|
|
var signalMap=null; // leaflet map instance for the per-ticker view
|
|
|
|
function clearChildren(el){ while(el.firstChild) el.removeChild(el.firstChild); }
|
|
function fmt$(n){
|
|
if(n>=1e9) return '$'+(n/1e9).toFixed(2)+'B';
|
|
if(n>=1e6) return '$'+(n/1e6).toFixed(1)+'M';
|
|
if(n>=1e3) return '$'+(n/1e3).toFixed(0)+'K';
|
|
return '$'+Math.round(n||0).toLocaleString();
|
|
}
|
|
function costClass(n){
|
|
if(n>=1e7) return 'cost-band-3';
|
|
if(n>=1e6) return 'cost-band-2';
|
|
return 'cost-band-1';
|
|
}
|
|
|
|
function load(){
|
|
var search=document.getElementById('q').value.trim();
|
|
var since=document.getElementById('since').value;
|
|
var minCost=parseInt(document.getElementById('min-cost').value,10);
|
|
document.getElementById('meta').textContent='Loading…';
|
|
var host=document.getElementById('result'); clearChildren(host);
|
|
var loading=document.createElement('div'); loading.className='loading';
|
|
loading.textContent='Aggregating from Chicago Socrata…';
|
|
host.appendChild(loading);
|
|
|
|
fetch(P+'/intelligence/profiler_index',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({since:since,min_cost:minCost,search:search,limit:200})
|
|
}).then(function(r){return r.json()}).then(function(d){
|
|
lastRows = d.contractors||[];
|
|
document.getElementById('meta').textContent=lastRows.length+' contractors · '+(d.duration_ms||0)+'ms';
|
|
// Build the ticker basket from the surfaced rows
|
|
buildBasket();
|
|
var totalCost = lastRows.reduce(function(s,r){return s+(r.total_cost||0)},0);
|
|
var totalPermits = lastRows.reduce(function(s,r){return s+(r.permits||0)},0);
|
|
var sumDiv=document.getElementById('summary');
|
|
sumDiv.style.display='block';
|
|
clearChildren(sumDiv);
|
|
var b1=document.createElement('b'); b1.textContent=lastRows.length.toLocaleString();
|
|
sumDiv.appendChild(b1);
|
|
sumDiv.appendChild(document.createTextNode(' contractors · '));
|
|
var b2=document.createElement('b'); b2.textContent=totalPermits.toLocaleString();
|
|
sumDiv.appendChild(b2);
|
|
sumDiv.appendChild(document.createTextNode(' total permits · '));
|
|
var b3=document.createElement('b'); b3.textContent=fmt$(totalCost);
|
|
sumDiv.appendChild(b3);
|
|
sumDiv.appendChild(document.createTextNode(' aggregate value · since '+(d.since||'?')+' · min permit cost '+fmt$(d.min_cost||0)));
|
|
render();
|
|
}).catch(function(e){
|
|
document.getElementById('meta').textContent='error';
|
|
var host=document.getElementById('result'); clearChildren(host);
|
|
var er=document.createElement('div'); er.className='empty'; er.style.color='#f85149';
|
|
er.textContent='Profiler index error: '+e.message;
|
|
host.appendChild(er);
|
|
});
|
|
}
|
|
|
|
// Aggregate every public ticker the profiler index surfaced, with a
|
|
// kind hierarchy (exact > direct > parent > associated) and the count
|
|
// of contractors each ticker is attributed to. Then fetch live quotes
|
|
// in one batch and render the scrolling basket.
|
|
function buildBasket(){
|
|
var byTicker = {};
|
|
lastRows.forEach(function(r){
|
|
var ts = (r.tickers && r.tickers.direct ? r.tickers.direct : []).concat(r.tickers && r.tickers.associated ? r.tickers.associated : []);
|
|
ts.forEach(function(t){
|
|
if(!t || !t.ticker) return;
|
|
if(!byTicker[t.ticker]) byTicker[t.ticker] = {ticker:t.ticker, kinds:new Set(), count:0, contractors:[], matched_name:t.matched_name||t.partner_name||null};
|
|
byTicker[t.ticker].kinds.add(t.via);
|
|
byTicker[t.ticker].count++;
|
|
if(byTicker[t.ticker].contractors.length < 5) byTicker[t.ticker].contractors.push(r.name);
|
|
});
|
|
});
|
|
var basketRows = Object.values(byTicker)
|
|
.map(function(b){
|
|
// Pick a single 'kind' for the bar color: direct/exact wins, then parent, then associated.
|
|
var k = b.kinds.has('exact')?'exact':b.kinds.has('direct')?'direct':b.kinds.has('parent')?'parent':b.kinds.has('associated')?'associated':'mixed';
|
|
if(b.kinds.size>1 && (b.kinds.has('exact')||b.kinds.has('direct')) && b.kinds.has('associated')) k='mixed';
|
|
return Object.assign({}, b, {kinds:Array.from(b.kinds), kind:k});
|
|
})
|
|
.sort(function(a,b){return b.count - a.count});
|
|
var wrap = document.getElementById('basket-wrap');
|
|
var track = document.getElementById('basket');
|
|
clearChildren(track);
|
|
if(!basketRows.length){
|
|
wrap.style.display='block';
|
|
var emp=document.createElement('div'); emp.className='basket-empty';
|
|
emp.textContent='No public issuers in this view. Try a wider filter or "since 2020" history.';
|
|
track.appendChild(emp);
|
|
document.getElementById('bk-count').textContent='0';
|
|
document.getElementById('bk-meta').textContent='';
|
|
return;
|
|
}
|
|
wrap.style.display='block';
|
|
document.getElementById('bk-count').textContent=basketRows.length;
|
|
document.getElementById('bk-meta').textContent='loading prices…';
|
|
// Render shells immediately, then fill in prices when the batch returns
|
|
basketRows.forEach(function(b){
|
|
var card=document.createElement('div'); card.className='bk-card no-quote';
|
|
card.dataset.ticker=b.ticker;
|
|
var bar=document.createElement('div'); bar.className='kind-bar '+b.kind; card.appendChild(bar);
|
|
var tk=document.createElement('div'); tk.className='tk'; tk.textContent=b.ticker; card.appendChild(tk);
|
|
var px=document.createElement('div'); px.className='px'; px.textContent='—'; card.appendChild(px);
|
|
var ch=document.createElement('div'); ch.className='ch flat'; ch.textContent=' '; card.appendChild(ch);
|
|
var meta=document.createElement('div'); meta.className='meta';
|
|
meta.textContent=b.count+' attribution'+(b.count===1?'':'s')+' · '+b.kinds.join('+');
|
|
card.appendChild(meta);
|
|
card.title=(b.matched_name||b.ticker)+'\n'+b.contractors.slice(0,5).join('\n')+(b.count>5?'\n…':'');
|
|
card.onclick=function(){
|
|
tickerFilter = (tickerFilter===b.ticker) ? null : b.ticker;
|
|
Array.prototype.forEach.call(track.children, function(c){
|
|
c.classList.toggle('selected', c.dataset && c.dataset.ticker===tickerFilter);
|
|
});
|
|
document.getElementById('bk-clear').style.display = tickerFilter ? 'inline' : 'none';
|
|
showSignalMap(tickerFilter);
|
|
render();
|
|
};
|
|
track.appendChild(card);
|
|
});
|
|
lastBasket = basketRows;
|
|
// Update the hero panel right away with what we know without prices
|
|
updateThesisMetrics();
|
|
// Batch-fetch quotes and update each card + thesis
|
|
fetch(P+'/intelligence/ticker_quotes',{
|
|
method:'POST',headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({tickers:basketRows.map(function(b){return b.ticker})})
|
|
}).then(function(r){return r.json()}).then(function(qd){
|
|
var quotes=qd.quotes||{};
|
|
lastQuotes = quotes;
|
|
document.getElementById('bk-meta').textContent='quotes via Stooq · '+(qd.duration_ms||0)+'ms';
|
|
Array.prototype.forEach.call(track.children, function(card){
|
|
var t=card.dataset.ticker; var q=quotes[t];
|
|
if(!q || !q.price) return;
|
|
card.classList.remove('no-quote');
|
|
var px=card.querySelector('.px'); px.textContent='$'+q.price.toFixed(2);
|
|
var ch=card.querySelector('.ch');
|
|
if(q.day_change_pct==null){ ch.textContent='close '+(q.price_date||''); ch.className='ch flat'; }
|
|
else if(q.day_change_pct>=0){ ch.textContent='+'+q.day_change_pct.toFixed(2)+'%'; ch.className='ch up'; }
|
|
else { ch.textContent=q.day_change_pct.toFixed(2)+'%'; ch.className='ch down'; }
|
|
});
|
|
updateThesisMetrics();
|
|
}).catch(function(){
|
|
document.getElementById('bk-meta').textContent='quote fetch failed';
|
|
});
|
|
}
|
|
|
|
// Compute the Building Activity Index and update the hero panel.
|
|
// BAI = attribution-weighted day-change % across surfaced issuers.
|
|
// "Indexed build value" = total dollars of permits attributable to
|
|
// any public issuer in this view (sum across attributing contractors).
|
|
// "Network depth" = issuer count + total attributions.
|
|
function updateThesisMetrics(){
|
|
if(!lastBasket.length){
|
|
document.getElementById('bai-value').textContent='—';
|
|
document.getElementById('bai-sub').textContent='awaiting basket data';
|
|
return;
|
|
}
|
|
// BAI: weighted average of day_change_pct, weight = attribution count.
|
|
var weightedSum=0, weightTotal=0, contributors=[];
|
|
lastBasket.forEach(function(b){
|
|
var q = lastQuotes[b.ticker];
|
|
if(q && q.day_change_pct!=null){
|
|
var w = b.count || 1;
|
|
weightedSum += q.day_change_pct * w;
|
|
weightTotal += w;
|
|
contributors.push({ticker:b.ticker, day:q.day_change_pct, weight:w});
|
|
}
|
|
});
|
|
var bai = weightTotal>0 ? (weightedSum/weightTotal) : null;
|
|
var baiEl = document.getElementById('bai-value');
|
|
var baiSub = document.getElementById('bai-sub');
|
|
if(bai==null){
|
|
baiEl.textContent='—'; baiSub.textContent='no quotes settled yet';
|
|
baiEl.className='bai-value';
|
|
} else {
|
|
var sign = bai>=0 ? '+' : '';
|
|
baiEl.textContent = sign + bai.toFixed(2) + '%';
|
|
baiEl.className = 'bai-value ' + (bai>=0?'up':'down');
|
|
contributors.sort(function(a,b){return Math.abs(b.day*b.weight) - Math.abs(a.day*a.weight)});
|
|
var top = contributors.slice(0,3).map(function(c){return c.ticker+' '+(c.day>=0?'+':'')+c.day.toFixed(1)+'%'}).join(' · ');
|
|
baiSub.textContent = contributors.length+' issuers contributing · top: '+top;
|
|
}
|
|
// Indexed build value
|
|
var totalCost = 0;
|
|
lastRows.forEach(function(r){
|
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
|
if(ts.length>0) totalCost += (r.total_cost||0);
|
|
});
|
|
var bav = totalCost>=1e9 ? '$'+(totalCost/1e9).toFixed(2)+'B' : totalCost>=1e6 ? '$'+(totalCost/1e6).toFixed(0)+'M' : '$'+Math.round(totalCost/1e3)+'K';
|
|
document.getElementById('bav-value').textContent = bav;
|
|
document.getElementById('bav-sub').textContent = lastBasket.length+' issuers in scope';
|
|
// Network depth
|
|
var totalAttrib = lastBasket.reduce(function(s,b){return s + (b.count||0)},0);
|
|
document.getElementById('net-value').textContent = lastBasket.length + ' / ' + totalAttrib;
|
|
document.getElementById('net-sub').textContent = 'issuers / attribution edges';
|
|
}
|
|
|
|
// Per-ticker map: when a ticker is selected, plot the contractor
|
|
// permit locations attributed to that ticker. Pulls lat/lng for each
|
|
// attributed contractor from the contractor profile endpoint and
|
|
// merges. Caches per-ticker so toggling is instant.
|
|
var mapCache = {};
|
|
function showSignalMap(ticker){
|
|
var wrap=document.getElementById('signal-map-wrap');
|
|
if(!ticker){ wrap.classList.remove('active'); if(signalMap){signalMap.remove(); signalMap=null;} return; }
|
|
wrap.classList.add('active');
|
|
document.getElementById('signal-map-ticker').textContent = ticker;
|
|
document.getElementById('signal-map-meta').textContent = 'loading permits…';
|
|
// Find the contractors attributed to this ticker
|
|
var attrib = lastRows.filter(function(r){
|
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
|
return ts.some(function(t){return t.ticker===ticker});
|
|
});
|
|
if(!attrib.length){
|
|
document.getElementById('signal-map-meta').textContent='no attributed contractors';
|
|
return;
|
|
}
|
|
// Use the contractor_profile endpoint per attributed contractor (cap at 6)
|
|
// to pull their geocoded permits, then render. Cached per ticker.
|
|
if(mapCache[ticker]){
|
|
drawSignalMap(ticker, mapCache[ticker]);
|
|
return;
|
|
}
|
|
var names = attrib.slice(0,6).map(function(r){return r.name});
|
|
Promise.all(names.map(function(n){
|
|
return fetch(P+'/intelligence/contractor_profile',{
|
|
method:'POST',headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({name:n})
|
|
}).then(function(r){return r.json()}).then(function(d){
|
|
var perms = (d.history && d.history.recent_permits) || [];
|
|
return perms.filter(function(p){return p.lat&&p.lng}).map(function(p){
|
|
return Object.assign({contractor:n}, p);
|
|
});
|
|
}).catch(function(){return []});
|
|
})).then(function(arrs){
|
|
var all = arrs.reduce(function(a,b){return a.concat(b)},[]);
|
|
mapCache[ticker] = all;
|
|
drawSignalMap(ticker, all);
|
|
});
|
|
}
|
|
function drawSignalMap(ticker, permits){
|
|
if(signalMap){ signalMap.remove(); signalMap=null; }
|
|
if(!permits.length){
|
|
document.getElementById('signal-map-meta').textContent='0 geocoded permits across attributed contractors';
|
|
return;
|
|
}
|
|
document.getElementById('signal-map-meta').textContent = permits.length + ' geocoded permits across ' + new Set(permits.map(function(p){return p.contractor})).size + ' contractors';
|
|
signalMap = L.map('signal-map',{zoomControl:true, attributionControl:false}).setView([41.88,-87.63], 11);
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{maxZoom:19}).addTo(signalMap);
|
|
var bounds=[];
|
|
var maxCost = Math.max.apply(null, permits.map(function(p){return Number(p.cost)||1}));
|
|
permits.forEach(function(p){
|
|
var c=Number(p.cost)||0;
|
|
var radius = 4 + (c/maxCost)*14;
|
|
var color = c>=1000000?'#f85149':c>=100000?'#d29922':'#3fb950';
|
|
var marker = L.circleMarker([p.lat,p.lng],{radius:radius,color:color,weight:1,fillOpacity:0.55});
|
|
var pop=document.createElement('div');
|
|
pop.style.cssText='font-family:ui-monospace,monospace;font-size:11px;color:#0a0d12;min-width:200px';
|
|
var top=document.createElement('div'); top.style.cssText='font-weight:700;margin-bottom:3px;color:#1f6feb';
|
|
top.textContent=ticker+' attribution';
|
|
pop.appendChild(top);
|
|
var con=document.createElement('div'); con.textContent=p.contractor; con.style.fontWeight='600';
|
|
pop.appendChild(con);
|
|
var meta=document.createElement('div'); meta.style.color='#545d68';
|
|
meta.textContent='$'+c.toLocaleString()+' · '+(p.date||'?')+' · '+(p.work_type||'?');
|
|
pop.appendChild(meta);
|
|
var addr=document.createElement('div'); addr.style.color='#545d68';
|
|
addr.textContent=p.address||'?';
|
|
pop.appendChild(addr);
|
|
marker.bindPopup(pop);
|
|
marker.addTo(signalMap);
|
|
bounds.push([p.lat,p.lng]);
|
|
});
|
|
if(bounds.length>1) signalMap.fitBounds(bounds,{padding:[28,28]});
|
|
}
|
|
|
|
function render(){
|
|
var host=document.getElementById('result');
|
|
clearChildren(host);
|
|
// Apply ticker filter if set: keep only rows whose tickers include the selected one
|
|
var pool = tickerFilter ? lastRows.filter(function(r){
|
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
|
return ts.some(function(t){return t.ticker===tickerFilter});
|
|
}) : lastRows;
|
|
if(!pool.length){
|
|
var emp=document.createElement('div'); emp.className='empty';
|
|
emp.textContent='No contractors match the current filter.';
|
|
host.appendChild(emp);
|
|
return;
|
|
}
|
|
var rows = pool.slice().sort(function(a,b){
|
|
var av=a[sortKey], bv=b[sortKey];
|
|
if(typeof av==='string'){ av=(av||'').toUpperCase(); bv=(bv||'').toUpperCase(); }
|
|
if(av<bv) return sortDir==='asc'?-1:1;
|
|
if(av>bv) return sortDir==='asc'?1:-1;
|
|
return 0;
|
|
});
|
|
|
|
var t=document.createElement('table');
|
|
var thead=document.createElement('thead'); var hr=document.createElement('tr');
|
|
var cols=[
|
|
{k:'name', label:'Contractor'},
|
|
{k:'permits', label:'Permits', right:true},
|
|
{k:'total_cost', label:'Total Value', right:true},
|
|
{k:'last_filed', label:'Last Filed', right:true},
|
|
{k:'roles', label:'Listed As'},
|
|
];
|
|
cols.forEach(function(c){
|
|
var h=document.createElement('th');
|
|
h.textContent=c.label;
|
|
if(c.right) h.style.textAlign='right';
|
|
if(sortKey===c.k){
|
|
var ar=document.createElement('span'); ar.className='arrow';
|
|
ar.textContent = sortDir==='asc' ? '▲' : '▼';
|
|
h.appendChild(ar);
|
|
}
|
|
h.onclick=function(){
|
|
if(sortKey===c.k) sortDir = sortDir==='asc' ? 'desc' : 'asc';
|
|
else { sortKey=c.k; sortDir = (c.k==='name') ? 'asc' : 'desc'; }
|
|
render();
|
|
};
|
|
hr.appendChild(h);
|
|
});
|
|
thead.appendChild(hr); t.appendChild(thead);
|
|
|
|
var tb=document.createElement('tbody');
|
|
rows.forEach(function(r){
|
|
var tr=document.createElement('tr');
|
|
var ntd=document.createElement('td'); ntd.className='name';
|
|
var a=document.createElement('a');
|
|
a.href = P+'/contractor?name='+encodeURIComponent(r.name);
|
|
a.target='_blank'; a.rel='noopener';
|
|
a.textContent = r.name;
|
|
ntd.appendChild(a);
|
|
// Ticker association pills — direct (green) = the contractor is a
|
|
// public issuer; parent (amber) = subsidiary of a public parent;
|
|
// associated (blue) = co-appears on permits with a public entity.
|
|
// Shows the correlation indicator J described — when Bob's Electric
|
|
// works permits with Target, TGT renders here as associated.
|
|
var t = r.tickers || {direct:[], associated:[]};
|
|
var allTk = (t.direct||[]).concat(t.associated||[]);
|
|
if(allTk.length){
|
|
var trk = document.createElement('div'); trk.className='tickers';
|
|
allTk.forEach(function(x){
|
|
var p = document.createElement('span');
|
|
p.className = 'ticker-pill ' + (x.via||'direct');
|
|
p.textContent = x.ticker;
|
|
// Tooltip shows the full reason path
|
|
var hint = x.via === 'associated'
|
|
? 'Associated via co-permits with '+x.partner_name+' ('+(x.co_permits||0)+' shared permits)' + (x.matched_name ? ' — '+x.matched_name : '')
|
|
: x.via === 'parent'
|
|
? 'Subsidiary of '+(x.matched_name||x.ticker) + (x.exchange ? ' · '+x.exchange : '')
|
|
: 'Direct match: '+(x.matched_name||r.name);
|
|
p.title = hint;
|
|
trk.appendChild(p);
|
|
});
|
|
ntd.appendChild(trk);
|
|
}
|
|
tr.appendChild(ntd);
|
|
var ptd=document.createElement('td'); ptd.className='right';
|
|
ptd.textContent=(r.permits||0).toLocaleString();
|
|
tr.appendChild(ptd);
|
|
var ctd=document.createElement('td'); ctd.className='right '+costClass(r.total_cost||0);
|
|
ctd.textContent=fmt$(r.total_cost||0);
|
|
tr.appendChild(ctd);
|
|
var ltd=document.createElement('td'); ltd.className='right';
|
|
ltd.textContent=(r.last_filed||'').slice(0,10) || '—';
|
|
tr.appendChild(ltd);
|
|
var rtd=document.createElement('td'); rtd.className='role';
|
|
(r.roles||[]).forEach(function(role){
|
|
var pill=document.createElement('span'); pill.className='pill'; pill.textContent=role;
|
|
rtd.appendChild(pill);
|
|
});
|
|
tr.appendChild(rtd);
|
|
tb.appendChild(tr);
|
|
});
|
|
t.appendChild(tb);
|
|
host.appendChild(t);
|
|
}
|
|
|
|
var sDeb;
|
|
document.getElementById('q').addEventListener('input',function(){
|
|
clearTimeout(sDeb);
|
|
sDeb=setTimeout(load,400);
|
|
});
|
|
document.getElementById('since').addEventListener('change',load);
|
|
document.getElementById('min-cost').addEventListener('change',load);
|
|
document.getElementById('bk-clear').addEventListener('click',function(){
|
|
tickerFilter=null;
|
|
document.getElementById('bk-clear').style.display='none';
|
|
Array.prototype.forEach.call(document.querySelectorAll('.bk-card.selected'), function(c){c.classList.remove('selected')});
|
|
showSignalMap(null);
|
|
render();
|
|
});
|
|
|
|
window.addEventListener('load',load);
|
|
</script>
|
|
</body></html>
|