Execcomp-AI-Dashboard / index.html
pierjoe's picture
Filter parsing errors from top earners + add change_in_pension
1dc5ddb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Execcomp-AI Dashboard</title>
<script src="https://cdn.plot.ly/plotly-2.35.0.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* ── Reset & Base ────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #F8FAFC;
color: #1E293B;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a { color: #2563EB; text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── Layout ──────────────────────────────────────────── */
.container { max-width: 1140px; margin: 0 auto; padding: 0 24px; }
/* ── Header ──────────────────────────────────────────── */
.hero {
background: #FFFFFF;
border-bottom: 1px solid #E2E8F0;
padding: 48px 0 40px;
margin-bottom: 32px;
}
.hero-inner { text-align: center; }
.hero h1 {
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.03em;
color: #0F172A;
margin-bottom: 12px;
}
.hero p {
font-size: 1.05rem;
color: #64748B;
max-width: 620px;
margin: 0 auto 24px;
}
.badge-row { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.badge {
display: inline-flex; align-items: center; gap: 6px;
background: #F1F5F9; border: 1px solid #E2E8F0;
color: #475569; padding: 8px 18px; border-radius: 99px;
font-size: 13px; font-weight: 600; transition: all .15s;
}
.badge:hover { background: #E2E8F0; color: #1E293B; text-decoration: none; }
.badge.accent { background: #EFF6FF; border-color: #BFDBFE; color: #1D4ED8; }
/* ── Tabs ────────────────────────────────────────────── */
.tabs { display: flex; gap: 4px; background: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 12px; padding: 4px; margin-bottom: 28px; }
.tab-btn {
flex: 1; padding: 12px 8px; border: none; background: transparent;
font: 600 14px/1 'Inter', sans-serif; color: #64748B;
border-radius: 8px; cursor: pointer; transition: all .15s;
white-space: nowrap;
}
.tab-btn:hover { color: #1E293B; background: #F8FAFC; }
.tab-btn.active { background: #2563EB; color: #FFFFFF; box-shadow: 0 1px 3px rgba(37,99,235,.3); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Cards & KPIs ────────────────────────────────────── */
.card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 14px;
padding: 24px;
margin-bottom: 20px;
}
.card-title { font-size: 1rem; font-weight: 700; color: #0F172A; margin-bottom: 16px; }
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 14px; margin-bottom: 24px;
}
.kpi {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 14px;
padding: 20px 16px;
text-align: center;
border-top: 4px solid var(--accent, #2563EB);
transition: transform .15s, box-shadow .15s;
}
.kpi:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,.06); }
.kpi-icon { font-size: 24px; margin-bottom: 6px; }
.kpi-val { font-size: 1.4rem; font-weight: 800; color: #0F172A; letter-spacing: -0.02em; }
.kpi-label {
font-size: 11px; font-weight: 600; color: #94A3B8;
text-transform: uppercase; letter-spacing: 0.05em; margin-top: 4px;
}
/* ── Chart container ─────────────────────────────────── */
.chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.chart-full { margin-bottom: 20px; }
@media (max-width: 768px) { .chart-row { grid-template-columns: 1fr; } }
/* ── Tables ──────────────────────────────────────────── */
.data-table {
width: 100%; border-collapse: collapse; font-size: 14px;
background: #FFFFFF; border-radius: 12px; overflow: hidden;
border: 1px solid #E2E8F0;
}
.data-table th {
background: #F8FAFC; color: #64748B; font-weight: 600;
padding: 14px 18px; text-align: left;
border-bottom: 2px solid #E2E8F0;
font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
}
.data-table td {
padding: 14px 18px; border-bottom: 1px solid #F1F5F9;
color: #334155; vertical-align: middle;
}
.data-table tbody tr:hover { background: #F8FAFC; }
.data-table .total-row { background: #F0FDF4; }
.data-table .total-row td { font-weight: 700; color: #15803D; border-top: 2px solid #BBF7D0; }
.pill {
display: inline-block;
background: #EFF6FF; color: #1D4ED8;
padding: 3px 12px; border-radius: 99px;
font-size: 12px; font-weight: 600;
}
/* ── Info boxes ──────────────────────────────────────── */
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; margin-bottom: 20px; }
.info-stat {
background: #FFFFFF; border: 1px solid #E2E8F0;
border-radius: 12px; padding: 20px; text-align: center;
}
.info-stat.green { background: #F0FDF4; border-color: #BBF7D0; }
.info-stat.amber { background: #FFFBEB; border-color: #FDE68A; }
.info-stat-val { font-size: 1.6rem; font-weight: 800; color: #0F172A; }
.info-stat-label { font-size: 11px; font-weight: 600; color: #94A3B8; text-transform: uppercase; letter-spacing: .04em; margin-top: 4px; }
.info-stat.green .info-stat-val { color: #16A34A; }
.info-stat.amber .info-stat-val { color: #D97706; }
.tip-box {
background: #EFF6FF; border-left: 4px solid #2563EB;
border-radius: 0 10px 10px 0; padding: 16px 20px; font-size: 14px;
color: #1E40AF; margin-top: 16px;
}
.tip-box b { color: #1E3A5F; }
/* ── Pipeline info ───────────────────────────────────── */
.pipeline-flow {
background: #F1F5F9; border: 1px solid #E2E8F0; border-radius: 10px;
padding: 16px 20px; font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 13px; color: #334155; margin-bottom: 20px;
overflow-x: auto; white-space: nowrap;
}
.steps-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
.steps-grid b { color: #0F172A; display: block; margin-bottom: 4px; }
.steps-grid span { color: #64748B; font-size: 13px; }
@media (max-width: 768px) { .steps-grid { grid-template-columns: 1fr; } }
/* ── Footer ──────────────────────────────────────────── */
.footer { text-align: center; color: #94A3B8; font-size: 12px; padding: 32px 0 48px; }
/* ── Top Earners ─────────────────────────────────────── */
.top-controls {
background: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 12px;
padding: 16px 20px; margin-bottom: 20px; display: flex; align-items: center; gap: 16px;
}
.slider-wrap { flex: 1; }
.slider-wrap label { font-size: 13px; color: #64748B; font-weight: 600; margin-bottom: 6px; display: block; }
.slider-wrap label b { color: #2563EB; font-size: 15px; }
.slider-wrap input[type="range"] {
width: 100%; height: 6px; -webkit-appearance: none; appearance: none;
background: #E2E8F0; border-radius: 99px; outline: none;
}
.slider-wrap input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%;
background: #2563EB; cursor: pointer; box-shadow: 0 2px 6px rgba(37,99,235,.3);
}
.podium-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 24px; }
@media (max-width: 768px) { .podium-row { grid-template-columns: 1fr; } }
.podium-card {
background: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 16px;
padding: 28px 22px 22px; text-align: center; position: relative;
transition: transform .15s, box-shadow .15s;
}
.podium-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,.08); }
.podium-card.gold { border-top: 5px solid #F59E0B; background: linear-gradient(180deg,#FFFBEB 0%,#FFFFFF 40%); }
.podium-card.silver { border-top: 5px solid #94A3B8; background: linear-gradient(180deg,#F8FAFC 0%,#FFFFFF 40%); }
.podium-card.bronze { border-top: 5px solid #D97706; background: linear-gradient(180deg,#FFF7ED 0%,#FFFFFF 40%); }
.podium-medal { font-size: 40px; margin-bottom: 8px; }
.podium-name { font-size: 17px; font-weight: 800; color: #0F172A; margin-bottom: 4px; }
.podium-company { font-size: 12px; font-weight: 600; color: #64748B; margin-bottom: 4px; }
.podium-title-text { font-size: 11px; color: #94A3B8; margin-bottom: 12px; }
.podium-total { font-size: 24px; font-weight: 800; letter-spacing: -0.02em; margin-bottom: 12px; }
.podium-card.gold .podium-total { color: #D97706; }
.podium-card.silver .podium-total { color: #475569; }
.podium-card.bronze .podium-total { color: #B45309; }
.podium-bar { display: flex; height: 8px; border-radius: 99px; overflow: hidden; margin-bottom: 8px; }
.podium-bar div { height: 100%; }
.podium-legend { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.podium-legend span { font-size: 10px; color: #64748B; display: flex; align-items: center; gap: 3px; }
.podium-legend span::before { content:''; display:inline-block; width:8px; height:8px; border-radius:2px; }
.exec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
.exec-card {
background: #FAFAFA; border: 1px solid #F1F5F9; border-radius: 12px;
padding: 16px; cursor: default; transition: all .15s;
}
.exec-card:hover { background: #F1F5F9; border-color: #CBD5E1; }
.exec-card-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
.exec-rank { font-size: 12px; font-weight: 800; color: #94A3B8; background: #F1F5F9; border-radius: 6px; padding: 3px 8px; min-width: 30px; text-align: center; }
.exec-total { font-size: 15px; font-weight: 800; color: #16A34A; }
.exec-name { font-size: 14px; font-weight: 700; color: #0F172A; margin-bottom: 2px; }
.exec-meta { font-size: 11px; color: #94A3B8; }
.exec-bar { display: flex; height: 6px; border-radius: 99px; overflow: hidden; margin-top: 10px; }
.exec-bar div { height: 100%; }
.exec-comps { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.exec-comp-tag { font-size: 10px; color: #64748B; background: #F1F5F9; border-radius: 4px; padding: 2px 6px; }
/* ── Plotly overrides ────────────────────────────────── */
.js-plotly-plot .plotly .main-svg { border-radius: 8px; }
</style>
</head>
<body>
<!-- ═══════════════════ HERO ═══════════════════ -->
<header class="hero">
<div class="container hero-inner">
<h1>πŸ“Š Execcomp-AI Dashboard</h1>
<p>AI-extracted executive compensation from <b id="h-total"></b> SEC DEF 14A proxy statements (2005–2022)</p>
<div class="badge-row">
<a href="https://github.com/pierpierpy/Execcomp-AI" target="_blank" class="badge">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
GitHub
</a>
<a href="https://huggingface.co/datasets/pierjoe/execcomp-ai-sample" target="_blank" class="badge">πŸ€— Dataset</a>
<span class="badge accent">⚑ Qwen-VL-32B · MinerU</span>
</div>
</div>
</header>
<main class="container">
<!-- ═══════════════════ TABS ═══════════════════ -->
<div class="tabs" id="tab-bar">
<button class="tab-btn active" data-tab="pipeline">πŸ“ Pipeline</button>
<button class="tab-btn" data-tab="compensation">πŸ’° Compensation</button>
<button class="tab-btn" data-tab="top10">πŸ† Top 50 Earners</button>
<button class="tab-btn" data-tab="quality">🎯 Data Quality</button>
</div>
<!-- ═══════════ TAB 1: Pipeline ═══════════ -->
<div class="tab-panel active" id="panel-pipeline">
<div class="kpi-grid" id="kpi-pipeline"></div>
<div class="chart-row">
<div class="card"><div id="chart-donut" style="width:100%;height:380px"></div></div>
<div class="card"><div id="chart-by-year" style="width:100%;height:380px"></div></div>
</div>
<div class="card">
<div class="card-title">βš™οΈ How the pipeline works</div>
<div class="pipeline-flow">SEC EDGAR β†’ PDF Download β†’ MinerU Extraction β†’ Qwen3-VL-32B Classification &amp; Parsing β†’ Qwen3-VL-4B Verification β†’ HF Dataset</div>
<div class="steps-grid">
<div><b>1 Β· Vision Extraction</b><span>MinerU converts PDFs to structured images preserving table layouts.</span></div>
<div><b>2 Β· Classification + Parsing</b><span>Qwen3-VL-32B identifies the Summary Compensation Table and parses it into typed JSON.</span></div>
<div><b>3 Β· Quality Filtering</b><span>Fine-tuned Qwen3-VL-4B assigns a confidence score (0–1) for each extracted table.</span></div>
</div>
</div>
</div>
<!-- ═══════════ TAB 2: Compensation ═══════════ -->
<div class="tab-panel" id="panel-compensation">
<div class="kpi-grid" id="kpi-comp"></div>
<div class="card chart-full"><div id="chart-trends" style="width:100%;height:420px"></div></div>
<div class="card chart-full"><div id="chart-dist" style="width:100%;height:420px"></div></div>
<div class="card chart-full"><div id="chart-components" style="width:100%;height:420px"></div></div>
<div class="card chart-full"><div id="chart-comp-trends" style="width:100%;height:420px"></div></div>
<div class="card">
<div class="card-title">Compensation Breakdown</div>
<table class="data-table" id="table-breakdown"></table>
</div>
</div>
<!-- ═══════════ TAB 3: Top Earners ═══════════ -->
<div class="tab-panel" id="panel-top10">
<div class="top-controls">
<div class="slider-wrap">
<label for="top-n-slider">Showing top <b id="top-n-val">20</b> executives</label>
<input type="range" id="top-n-slider" min="5" max="50" value="20" step="5">
</div>
</div>
<div class="podium-row" id="podium"></div>
<div class="card chart-full"><div id="chart-stacked" style="width:100%;height:900px"></div></div>
<div class="card">
<div class="card-title">πŸ“‹ Detailed Breakdown</div>
<div class="exec-grid" id="exec-grid"></div>
</div>
</div>
<!-- ═══════════ TAB 4: Data Quality ═══════════ -->
<div class="tab-panel" id="panel-quality">
<div class="kpi-grid" id="kpi-quality"></div>
<div class="card chart-full"><div id="chart-prob-hist" style="width:100%;height:420px"></div></div>
<div class="card chart-full"><div id="chart-prob-pie" style="width:100%;height:420px"></div></div>
<div class="card" id="disambig-card"></div>
</div>
</main>
<footer class="footer"><span id="footer-text"></span></footer>
<!-- ═══════════════════ DATA + LOGIC ═══════════════════ -->
<script>
// Data loaded from file
let D;
// ── Colors ──
const C = {
blue:'#2563EB', green:'#16A34A', amber:'#D97706', red:'#DC2626',
violet:'#7C3AED', teal:'#0D9488', slate:'#64748B', gray:'#94A3B8',
bg:'#FFFFFF', grid:'#E2E8F0', text:'#1E293B',
};
const COLORS = [C.blue, C.teal, C.green, C.violet, C.amber, C.red, '#0EA5E9', '#84CC16'];
const plotCfg = {displayModeBar: false, responsive: true};
function baseLayout(extra={}) {
return Object.assign({
font: {family:'Inter, system-ui, sans-serif', size:13, color:C.text},
paper_bgcolor: C.bg, plot_bgcolor: C.bg,
margin: {l:55, r:30, t:50, b:50},
hoverlabel: {bgcolor:'#fff', font:{size:13, family:'Inter'}, bordercolor:C.grid},
}, extra);
}
// ── Helpers ──
const fmt = n => n.toLocaleString('en-US');
const fmtM = n => '$' + (n/1e6).toFixed(1) + 'M';
const fmtM2 = n => '$' + (n/1e6).toFixed(2) + 'M';
const fmtK = n => '$' + (n/1e3).toFixed(0) + 'K';
const fmtVal = v => v < 1e6 ? fmtK(v) : fmtM2(v);
function makeKpi(container, items) {
const el = document.getElementById(container);
el.innerHTML = items.map(([icon,val,label,color]) => `
<div class="kpi" style="--accent:${color}">
<div class="kpi-icon">${icon}</div>
<div class="kpi-val">${val}</div>
<div class="kpi-label">${label}</div>
</div>
`).join('');
}
// ── Tab switching ──
document.getElementById('tab-bar').addEventListener('click', e => {
const btn = e.target.closest('.tab-btn');
if (!btn) return;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
// Relayout all plotly charts in the newly visible panel for proper sizing
setTimeout(() => {
document.querySelectorAll('#panel-' + btn.dataset.tab + ' .js-plotly-plot').forEach(p => {
Plotly.Plots.resize(p);
});
}, 50);
});
// ── Load data and render ──
fetch('dashboard_data.json')
.then(r => r.json())
.then(data => {
D = data;
document.getElementById('h-total').textContent = fmt(D.pipeline.total_docs);
document.getElementById('footer-text').textContent =
`Indexed on ${D.generated_at.slice(0,10)} Β· Execcomp-AI Β· Pier Paolo Di Pasquale`;
renderPipeline();
renderCompensation();
renderTopEarners();
renderQuality();
});
// ═══════════════ 1. PIPELINE ═══════════════
function renderPipeline() {
const p = D.pipeline;
makeKpi('kpi-pipeline', [
['πŸ“', fmt(p.total_docs), 'Total Filings', C.blue],
['🏒', fmt(p.non_funds), 'Companies', C.teal],
['βœ…', fmt(p.with_sct), 'SCT Found', C.green],
['πŸ“‹', fmt(p.total_tables), 'SCT Tables', C.violet],
['❌', fmt(p.no_sct), 'No SCT', C.red],
['⏳', fmt(p.pending), 'Pending', C.amber],
]);
// Donut
Plotly.newPlot('chart-donut', [{
type:'pie', hole:.55,
labels: ['With SCT','No SCT','Funds (skipped)','Pending'],
values: [p.with_sct, p.no_sct, p.funds, p.pending],
marker: { colors:[C.green, C.red, C.slate, C.amber], line:{color:'#fff',width:2} },
textinfo:'percent', hovertemplate:'<b>%{label}</b><br>%{value:,} docs<br>%{percent}<extra></extra>',
}], baseLayout({
title:{text:'Document Breakdown',font:{size:16},x:.5,xanchor:'center'},
legend:{orientation:'h',y:-.06,x:.5,xanchor:'center'},
margin:{l:20,r:20,t:55,b:60},
}), plotCfg);
// Bar by year
const years = Object.keys(D.tables_by_year).sort();
const counts = years.map(y => D.tables_by_year[y]);
Plotly.newPlot('chart-by-year', [{
type:'bar', x:years, y:counts,
marker:{color:C.blue, line:{color:'#fff',width:1}},
text:counts, textposition:'outside', textfont:{size:10, color:C.text},
hovertemplate:'<b>Year %{x}</b><br>Tables: %{y:,}<extra></extra>',
}], baseLayout({
title:{text:'SCT Tables by Filing Year',font:{size:16},x:.5,xanchor:'center'},
xaxis:{title:'Filing Year',tickangle:-45,gridcolor:C.grid,linecolor:C.grid},
yaxis:{title:'Tables',gridcolor:C.grid,linecolor:C.grid},
margin:{l:55,r:25,t:55,b:70},
}), plotCfg);
}
// ═══════════════ 2. COMPENSATION ═══════════════
function renderCompensation() {
const c = D.compensation;
makeKpi('kpi-comp', [
['πŸ‘€', fmt(c.total_exec_records), 'Exec Records', C.blue],
['🏒', fmt(c.unique_companies), 'Companies', C.teal],
['πŸ’°', fmtM2(c.mean_total), 'Mean Comp', C.green],
['πŸ“Š', fmtM2(c.median_total), 'Median Comp', C.violet],
['πŸ†', fmtM(c.max_total), 'Max Comp', C.amber],
]);
// Trends (dual axis)
const t = D.trends;
const tYears = t.map(r=>r.year);
const tMeans = t.map(r=>r.mean/1e6);
const tMedians = t.map(r=>r.median/1e6);
const tCounts = t.map(r=>r.count);
Plotly.newPlot('chart-trends', [
{ type:'bar', x:tYears, y:tCounts, name:'Exec Count', yaxis:'y2',
marker:{color:C.grid}, opacity:.5, hoverinfo:'skip' },
{ type:'scatter', mode:'lines+markers', x:tYears, y:tMeans, name:'Mean',
line:{color:C.blue,width:3}, marker:{size:8,color:'#fff',line:{color:C.blue,width:2}},
hovertemplate:'<b>FY %{x}</b><br>Mean: $%{y:.2f}M<extra></extra>' },
{ type:'scatter', mode:'lines+markers', x:tYears, y:tMedians, name:'Median',
line:{color:C.amber,width:3,dash:'dot'}, marker:{size:8,color:'#fff',line:{color:C.amber,width:2}},
hovertemplate:'<b>FY %{x}</b><br>Median: $%{y:.2f}M<extra></extra>' },
], baseLayout({
title:{text:'Total Compensation Trends (2005–2022)',font:{size:16},x:.5,xanchor:'center'},
xaxis:{title:'Fiscal Year',gridcolor:C.grid,linecolor:C.grid},
yaxis:{title:'Total Comp ($M)',gridcolor:C.grid,linecolor:C.grid},
yaxis2:{overlaying:'y',side:'right',showgrid:false,showticklabels:false},
legend:{orientation:'h',y:1.08,x:.5,xanchor:'center'},
margin:{l:60,r:40,t:60,b:55},
}), plotCfg);
// Distribution
const d = D.distribution;
const mids=[],widths=[];
for(let i=0;i<d.values.length;i++){
mids.push((d.edges[i]+d.edges[i+1])/2);
widths.push(d.edges[i+1]-d.edges[i]);
}
Plotly.newPlot('chart-dist', [{
type:'bar', x:mids, y:d.values, width:widths,
marker:{color:C.teal,line:{width:0}}, opacity:.85,
hovertemplate:'<b>$%{x:.1f}M range</b><br>Executives: %{y:,}<extra></extra>',
}], baseLayout({
title:{text:`Distribution (≀99th pctl, ${fmt(d.n_outliers)} outliers excluded)`,font:{size:15},x:.5,xanchor:'center'},
xaxis:{title:'Total Compensation ($M)',gridcolor:C.grid,linecolor:C.grid},
yaxis:{title:'Executives',gridcolor:C.grid,linecolor:C.grid},
shapes:[{type:'line',x0:d.median/1e6,x1:d.median/1e6,y0:0,y1:1,yref:'paper',
line:{color:C.red,width:2,dash:'dash'}}],
annotations:[{x:d.median/1e6,y:1,yref:'paper',text:'Median $'+((d.median/1e6).toFixed(1))+'M',
showarrow:false,font:{color:C.red,size:12},xanchor:'left',xshift:6}],
margin:{l:60,r:25,t:55,b:55},
}), plotCfg);
// Components bar
const cc = D.comp_components;
const sorted = Object.entries(cc).sort((a,b)=>a[1]-b[1]);
Plotly.newPlot('chart-components', [{
type:'bar', orientation:'h',
y:sorted.map(([k])=>k), x:sorted.map(([,v])=>v/1e6),
marker:{color:COLORS.slice(0,sorted.length), line:{width:0}},
text:sorted.map(([,v])=>'$'+(v/1e6).toFixed(2)+'M'),
textposition:'outside', textfont:{size:12,color:C.text},
hovertemplate:'<b>%{y}</b><br>Average: $%{x:.2f}M<extra></extra>',
}], baseLayout({
title:{text:'Average Comp by Component',font:{size:16},x:.5,xanchor:'center'},
xaxis:{title:'Average ($M)',gridcolor:C.grid,linecolor:C.grid,range:[0,sorted[sorted.length-1][1]/1e6*1.25]},
yaxis:{automargin:true,gridcolor:C.grid,linecolor:C.grid},
margin:{l:10,r:80,t:55,b:55},
}), plotCfg);
// Component trends
const ct = D.comp_trends;
const traces = Object.entries(ct).map(([name,data],i) => ({
type:'scatter', mode:'lines+markers', name,
x:data.years, y:data.values.map(v=>v/1e6),
line:{color:COLORS[i%COLORS.length],width:2}, marker:{size:5},
hovertemplate:`<b>${name}</b><br>FY %{x}<br>$%{y:.2f}M<extra></extra>`,
}));
Plotly.newPlot('chart-comp-trends', traces, baseLayout({
title:{text:'Compensation Components Over Time',font:{size:16},x:.5,xanchor:'center'},
xaxis:{title:'Fiscal Year',gridcolor:C.grid,linecolor:C.grid},
yaxis:{title:'Average ($M)',gridcolor:C.grid,linecolor:C.grid},
legend:{orientation:'h',y:1.1,x:.5,xanchor:'center'},
margin:{l:55,r:30,t:70,b:55},
}), plotCfg);
// Breakdown table
const bd = D.compensation.breakdown;
let rows = `<thead><tr>
<th>Component</th><th style="text-align:right">Mean</th>
<th style="text-align:right">Median</th><th style="text-align:right">Max</th>
</tr></thead><tbody>`;
for (const [k,v] of Object.entries(bd)) {
const label = k.replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase());
const isTotal = k === 'total';
rows += `<tr class="${isTotal?'total-row':''}">
<td>${isTotal?'⭐ ':''}${label}</td>
<td style="text-align:right">${fmtVal(v.mean)}</td>
<td style="text-align:right">${fmtVal(v.median)}</td>
<td style="text-align:right">${fmtVal(v.max)}</td>
</tr>`;
}
rows += '</tbody>';
document.getElementById('table-breakdown').innerHTML = rows;
}
// ═══════════════ 3. TOP EARNERS ═══════════════
const COMP_KEYS = [
{key:'salary', label:'Salary', color:'#2563EB'},
{key:'stock_awards', label:'Stock Awards', color:'#0D9488'},
{key:'option_awards', label:'Option Awards', color:'#7C3AED'},
{key:'bonus', label:'Bonus', color:'#D97706'},
{key:'non_equity_incentive', label:'Non-Equity', color:'#16A34A'},
{key:'change_in_pension', label:'Pension Chg', color:'#F59E0B'},
{key:'other_compensation', label:'Other', color:'#94A3B8'},
];
function renderTopEarners() {
const all = D.top50;
const slider = document.getElementById('top-n-slider');
const valEl = document.getElementById('top-n-val');
function draw(n) {
const data = all.slice(0,n);
valEl.textContent = n;
renderPodium(data.slice(0,3));
renderStackedBar(data);
renderExecGrid(data);
}
slider.addEventListener('input', () => draw(+slider.value));
draw(+slider.value);
}
function renderPodium(top3) {
const medals = ['πŸ₯‡','πŸ₯ˆ','πŸ₯‰'];
const classes = ['gold','silver','bronze'];
const el = document.getElementById('podium');
el.innerHTML = top3.map((e,i) => {
const comps = COMP_KEYS.map(c => ({...c, v: e[c.key]||0})).filter(c => c.v > 0);
const tot = Math.max(comps.reduce((s,c)=>s+c.v,0), 1);
const barHtml = comps.map(c => `<div style="width:${c.v/tot*100}%;background:${c.color}"></div>`).join('');
const legendHtml = comps.slice(0,4).map(c =>
`<span style="--dot:${c.color}"><span style="background:${c.color};width:8px;height:8px;border-radius:2px;display:inline-block"></span>${c.label} $${(c.v/1e6).toFixed(1)}M</span>`
).join('');
return `
<div class="podium-card ${classes[i]}">
<div class="podium-medal">${medals[i]}</div>
<div class="podium-name">${e.name}</div>
<div class="podium-company">${e.company}</div>
<div class="podium-title-text">${e.title||''} Β· FY ${e.fiscal_year}</div>
<div class="podium-total">$${(e.total/1e6).toFixed(0)}M</div>
<div class="podium-bar">${barHtml}</div>
<div class="podium-legend">${legendHtml}</div>
</div>`;
}).join('');
}
function renderStackedBar(data) {
const rev = [...data].reverse();
const names = rev.map((e,i) => `#${data.length-i} ${e.name.slice(0,24)}`);
const traces = COMP_KEYS.map(comp => ({
type:'bar', orientation:'h', name: comp.label,
y: names,
x: rev.map(e => (e[comp.key]||0)/1e6),
marker: {color: comp.color, line:{width:0}},
hovertemplate: `<b>${comp.label}</b><br>$%{x:.1f}M<extra></extra>`,
}));
const height = Math.max(500, data.length * 28 + 80);
document.getElementById('chart-stacked').style.height = height + 'px';
Plotly.newPlot('chart-stacked', traces, baseLayout({
title:{text:`Top ${data.length} β€” Compensation Breakdown`,font:{size:16},x:.5,xanchor:'center'},
barmode:'stack',
xaxis:{title:'Total Compensation ($M)',gridcolor:C.grid,linecolor:C.grid},
yaxis:{automargin:true,gridcolor:C.grid,linecolor:C.grid,tickfont:{size:11}},
legend:{orientation:'h',y:1.02,x:.5,xanchor:'center',font:{size:12}},
margin:{l:10,r:30,t:55,b:55},
height: height,
}), plotCfg);
}
function renderExecGrid(data) {
const el = document.getElementById('exec-grid');
const medals = ['πŸ₯‡','πŸ₯ˆ','πŸ₯‰'];
el.innerHTML = data.map((e,i) => {
const comps = COMP_KEYS.map(c => ({...c, v: e[c.key]||0})).filter(c => c.v > 0);
const tot = Math.max(comps.reduce((s,c)=>s+c.v,0), 1);
const barHtml = comps.map(c => `<div style="width:${c.v/tot*100}%;background:${c.color}" title="${c.label}: $${(c.v/1e6).toFixed(1)}M"></div>`).join('');
const tagsHtml = comps.slice(0,4).map(c =>
`<span class="exec-comp-tag" style="border-left:3px solid ${c.color}">${c.label} $${(c.v/1e6).toFixed(1)}M</span>`
).join('');
const rankText = i<3 ? medals[i] : (i+1);
return `
<div class="exec-card">
<div class="exec-card-head">
<div class="exec-rank">${rankText}</div>
<div class="exec-total">$${(e.total/1e6).toFixed(1)}M</div>
</div>
<div class="exec-name">${e.name}</div>
<div class="exec-meta">${e.company} Β· ${e.title||''} Β· FY ${e.fiscal_year}</div>
<div class="exec-bar">${barHtml}</div>
<div class="exec-comps">${tagsHtml}</div>
</div>`;
}).join('');
}
// ═══════════════ 4. DATA QUALITY ═══════════════
function renderQuality() {
const p = D.probability;
const tot = p.total_tables;
const pct = n => (n/tot*100).toFixed(0) + '%';
makeKpi('kpi-quality', [
['πŸ“‹', fmt(tot), 'Tables Analyzed', C.blue],
['πŸ“„', fmt(p.unique_docs), 'Unique Documents', C.slate],
['βœ…', fmt(p.high_confidence)+' ('+pct(p.high_confidence)+')', 'High β‰₯0.7', C.green],
['⚠️', fmt(p.medium_confidence)+' ('+pct(p.medium_confidence)+')', 'Medium', C.amber],
['❌', fmt(p.low_confidence)+' ('+pct(p.low_confidence)+')', 'Low <0.3', C.red],
]);
// Histogram with colored bars
const mids=[], widths=[], colors=[];
for(let i=0;i<p.hist_values.length;i++){
const m = (p.hist_edges[i]+p.hist_edges[i+1])/2;
mids.push(m); widths.push(p.hist_edges[i+1]-p.hist_edges[i]);
colors.push(m>=.7 ? C.green : m>=.3 ? C.amber : C.red);
}
const ymax = Math.max(...p.hist_values);
Plotly.newPlot('chart-prob-hist', [{
type:'bar', x:mids, y:p.hist_values, width:widths,
marker:{color:colors,line:{width:0}}, opacity:.85,
hovertemplate:'<b>Score: %{x:.2f}</b><br>Tables: %{y:,}<extra></extra>',
}], baseLayout({
title:{text:'Confidence Score Distribution',font:{size:16},x:.5,xanchor:'center'},
xaxis:{title:'SCT Probability (0–1)',range:[0,1],gridcolor:C.grid,linecolor:C.grid},
yaxis:{title:'Tables',gridcolor:C.grid,linecolor:C.grid},
shapes:[
{type:'line',x0:.7,x1:.7,y0:0,y1:1,yref:'paper',line:{color:C.green,width:2,dash:'dash'}},
{type:'line',x0:.3,x1:.3,y0:0,y1:1,yref:'paper',line:{color:C.amber,width:2,dash:'dash'}},
],
annotations:[
{x:.85,y:ymax*.95,text:'Keep',showarrow:false,font:{color:C.green,size:13,weight:600}},
{x:.5,y:ymax*.95,text:'Review',showarrow:false,font:{color:C.amber,size:13}},
{x:.15,y:ymax*.95,text:'Filter out',showarrow:false,font:{color:C.red,size:13}},
],
margin:{l:60,r:25,t:55,b:55},
}), plotCfg);
// Pie
Plotly.newPlot('chart-prob-pie', [{
type:'pie', hole:.55,
labels:['High (β‰₯0.7)','Medium (0.3–0.7)','Low (<0.3)'],
values:[p.high_confidence, p.medium_confidence, p.low_confidence],
marker:{colors:[C.green, C.amber, C.red], line:{color:'#fff',width:2}},
textinfo:'percent',
hovertemplate:'<b>%{label}</b><br>%{value:,} tables<br>%{percent}<extra></extra>',
}], baseLayout({
title:{text:'Confidence Breakdown',font:{size:16},x:.5,xanchor:'center'},
legend:{orientation:'h',y:-.06,x:.5,xanchor:'center'},
margin:{l:20,r:20,t:55,b:60},
}), plotCfg);
// Disambiguation card
const disambPct = p.multi_table_docs ? (p.could_disambiguate/p.multi_table_docs*100).toFixed(0) : 0;
const remaining = p.multi_table_docs - p.could_disambiguate;
document.getElementById('disambig-card').innerHTML = `
<div class="card-title">πŸ” Duplicate / False-Positive Resolution</div>
<p style="color:${C.slate};font-size:14px;margin-bottom:20px">
Proxy filings often contain multiple tables resembling the Summary Compensation Table
(Director Compensation, Option Grant tables, etc.). A fine-tuned Qwen3-VL-4B binary classifier
assigns a confidence score to help filter them out.
</p>
<div class="info-grid">
<div class="info-stat">
<div class="info-stat-val">${fmt(p.multi_table_docs)}</div>
<div class="info-stat-label">Docs with duplicates</div>
</div>
<div class="info-stat green">
<div class="info-stat-val">${fmt(p.could_disambiguate)}</div>
<div class="info-stat-label">Auto-resolved (${disambPct}%)</div>
</div>
<div class="info-stat amber">
<div class="info-stat-val">${fmt(remaining)}</div>
<div class="info-stat-label">Still ambiguous</div>
</div>
</div>
<div class="tip-box">
<b>πŸ’‘ Tip:</b> Filter by <code>sct_probability β‰₯ 0.7</code> to keep only high-confidence SCTs.
</div>`;
}
</script>
</body>
</html>