Spaces:
Running
Running
| <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 & 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> | |