aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/filings.py424
1 files changed, 349 insertions, 75 deletions
diff --git a/components/filings.py b/components/filings.py
index 9e3b156..4343a07 100644
--- a/components/filings.py
+++ b/components/filings.py
@@ -1,8 +1,14 @@
-"""SEC filings — recent 10-K, 10-Q, 8-K and other forms with direct links."""
+"""SEC filings tab rendered as a client-side HTML surface."""
+from datetime import date as _date
+from datetime import datetime as _dt
+from html import escape as _esc
+
import streamlit as st
+import streamlit.components.v1 as components
-from services.data_service import get_sec_filings
+from services.data_service import get_company_info, get_latest_price, get_sec_filings
+_XMAP = {"NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", "NCM": "NASDAQ", "ASE": "AMEX"}
_FORM_DESCRIPTIONS = {
"10-K": "Annual report",
@@ -16,93 +22,361 @@ _FORM_DESCRIPTIONS = {
"SC 13D": "Beneficial ownership (active)",
}
-_FORM_COLORS = {
- "10-K": "rgba(74,120,181,0.15)",
- "10-Q": "rgba(79,140,94,0.15)",
- "8-K": "rgba(196,149,69,0.15)",
-}
+
+def _normalize_date(raw) -> str | None:
+ if raw is None:
+ return None
+ if isinstance(raw, _dt):
+ return raw.strftime("%Y-%m-%d")
+ if isinstance(raw, _date):
+ return raw.strftime("%Y-%m-%d")
+
+ text = str(raw).strip()
+ if not text:
+ return None
+
+ if " " in text:
+ text = text.split(" ", 1)[0]
+ if "T" in text:
+ text = text.split("T", 1)[0]
+
+ for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%Y/%m/%d"):
+ try:
+ return _dt.strptime(text, fmt).strftime("%Y-%m-%d")
+ except Exception:
+ pass
+
+ if len(text) >= 10:
+ cand = text[:10]
+ try:
+ return _dt.strptime(cand, "%Y-%m-%d").strftime("%Y-%m-%d")
+ except Exception:
+ pass
+
+ return None
def render_filings(ticker: str):
- with st.spinner("Loading SEC filings…"):
- filings = get_sec_filings(ticker)
+ import json as _json
+
+ info = get_company_info(ticker) or {}
+ price = get_latest_price(ticker)
+ filings = get_sec_filings(ticker)
if not filings:
st.info("No SEC filing data available for this ticker.")
return
- # yfinance returns: date (datetime.date), type, title, edgarUrl, exhibits (dict)
- form_types = sorted({str(f.get("type", "")).strip() for f in filings if f.get("type")})
- priority = [t for t in ["10-K", "10-Q", "8-K"] if t in form_types]
- other = [t for t in form_types if t not in ("10-K", "10-Q", "8-K")]
- filter_options = ["All"] + priority + other
+ rows = []
+ for filing in filings:
+ filing = filing or {}
+ form_raw = filing.get("type")
+ form = str(form_raw).strip() if form_raw is not None else ""
+ if not form:
+ form = "Other"
+
+ date_str = _normalize_date(filing.get("date"))
+ month_str = date_str[:7] if date_str else None
- filter_col, _ = st.columns([1, 3])
- with filter_col:
- selected_type = st.selectbox("Form type", options=filter_options, index=0, key="filings_filter")
+ title_raw = filing.get("title")
+ title = str(title_raw).strip() if title_raw is not None else ""
+ if not title:
+ title = _FORM_DESCRIPTIONS.get(form)
- # Summary counts
- counts = {}
- for f in filings:
- ft = str(f.get("type", "Other")).strip()
- counts[ft] = counts.get(ft, 0) + 1
+ exhibits = filing.get("exhibits")
+ url = None
+ if isinstance(exhibits, dict):
+ direct = exhibits.get(form)
+ if direct:
+ url = str(direct).strip() or None
- if priority:
- cols = st.columns(len(priority))
- for col, ft in zip(cols, priority):
- col.metric(ft, counts.get(ft, 0))
- st.write("")
+ if url is None:
+ edgar = filing.get("edgarUrl")
+ if edgar:
+ url = str(edgar).strip() or None
- filtered = filings if selected_type == "All" else [
- f for f in filings if str(f.get("type", "")).strip() == selected_type
- ]
+ rows.append(
+ {
+ "date": date_str,
+ "month": month_str,
+ "form": form,
+ "title": title or None,
+ "url": url,
+ "has_exhibits": bool(isinstance(exhibits, dict) and len(exhibits) > 0),
+ }
+ )
- if not filtered:
- st.info("No filings match the current filter.")
+ rows.sort(key=lambda r: (r.get("date") is not None, r.get("date") or ""), reverse=True)
+
+ if not rows:
+ st.info("No SEC filing data available for this ticker.")
return
- for item in filtered[:40]:
- form = str(item.get("type", "—")).strip()
- date = str(item.get("date", ""))[:10]
- title = item.get("title") or _FORM_DESCRIPTIONS.get(form, "")
- edgar_url = item.get("edgarUrl", "")
- exhibits = item.get("exhibits") or {}
+ n_rows = max(len(rows), 20)
+ height = 1280 + n_rows * 30
+
+ def _safe_float(value):
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+ current_price = info.get("currentPrice") or info.get("regularMarketPrice") or price
+ prev_close = info.get("previousClose")
+ cur_num = _safe_float(current_price)
+ prev_num = _safe_float(prev_close)
+
+ if cur_num is not None and prev_num is not None and prev_num > 0:
+ chg_pct = (cur_num - prev_num) / prev_num * 100.0
+ chg_sign = "+" if chg_pct >= 0 else ""
+ chg_arrow = "▲" if chg_pct >= 0 else "▼"
+ chg_str = chg_arrow + " " + chg_sign + "{:.2f}%".format(chg_pct)
+ chg_cls = "chg-pos" if chg_pct >= 0 else "chg-neg"
+ else:
+ chg_str = "—"
+ chg_cls = ""
+
+ exchange_raw = str(info.get("exchange") or "")
+ exchange = _XMAP.get(exchange_raw, exchange_raw) or "—"
+ co_name = _esc(info.get("longName") or info.get("shortName") or ticker.upper())
+ price_str = "${:,.2f}".format(cur_num) if cur_num is not None else "—"
+
+ rows_js = "const FILINGS=" + _json.dumps(rows) + ";"
+ meta_js = "const FILINGS_META=" + _json.dumps({"ticker": ticker.upper(), "rows": len(rows)}) + ";"
+
+ plotly_cdn = "<script src='https://cdn.plot.ly/plotly-2.27.0.min.js'></script>"
+ _ROOT = (
+ "<style>*,*::before,*::after{box-sizing:border-box}"
+ ":root{"
+ "--ink-0:#0B0E13;--ink-1:#11151C;--ink-2:#181D26;--ink-3:#222934;--ink-4:#2C3340;"
+ "--line-1:#232934;--line-2:#2E3645;--line-3:#3D4658;"
+ "--fg-1:#F2ECDC;--fg-2:#C7C0AE;--fg-3:#8E8676;--fg-4:#5E5849;"
+ "--brass:#C2AA7A;--brass-bright:#DCC79E;--brass-deep:#8F7A50;--brass-ink:#17120A;"
+ "--oxford:#1F3D5C;--oxford-light:#2E5A87;"
+ "--positive:#4F8C5E;--positive-bg:#15241A;--negative:#B5494B;--negative-bg:#2A1517;"
+ "--warning:#C49545;--warning-bg:#2A1F0F;--info:#4A78B5;--info-bg:#11202E;"
+ "--font-display:'EB Garamond',Georgia,serif;"
+ "--font-sans:'IBM Plex Sans','Helvetica Neue',system-ui,sans-serif;"
+ "--font-mono:'IBM Plex Mono','SF Mono',Menlo,monospace;"
+ "--fs-12:0.75rem;--fs-13:0.8125rem;--fs-14:0.875rem;--fs-16:1rem;--fs-18:1.125rem;"
+ "--fs-20:1.25rem;--fs-24:1.5rem;--fs-30:1.875rem;--fs-38:2.375rem;"
+ "--tr-wider:0.12em;--tr-wide:0.04em;--tr-tight:-0.02em;"
+ "--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:16px;--sp-5:24px;--sp-6:32px;--sp-7:48px;"
+ "--r-1:2px;--r-2:4px;--r-3:6px;--r-full:999px;"
+ "--shadow-1:0 1px 3px rgba(0,0,0,0.4);"
+ "}"
+ "html,body{margin:0;padding:0;background:var(--ink-0);color:var(--fg-2);"
+ "font-family:var(--font-sans);font-size:14px;-webkit-font-smoothing:antialiased}"
+ "</style>"
+ )
+
+ fonts_link = (
+ "<link rel='preconnect' href='https://fonts.googleapis.com'>"
+ "<link href='https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;1,400;1,500"
+ "&family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap'"
+ " rel='stylesheet'>"
+ )
+
+ _FI_CSS = """<style>
+.fi-wrap{background:var(--ink-0);min-height:100vh}
+.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
+.eyebrow-lbl{font-family:var(--font-sans);font-size:var(--fs-12);text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-3);font-weight:600}
+.val-ctx{display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-3) var(--sp-5);border-bottom:1px solid var(--line-1);background:var(--ink-1)}
+.val-ctx .sym{font-family:var(--font-display);font-size:var(--fs-24);font-weight:500;letter-spacing:var(--tr-tight)}
+.val-ctx .name{font-family:var(--font-display);font-style:italic;font-size:var(--fs-16);color:var(--fg-2);margin-left:calc(-1 * var(--sp-1));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:48ch}
+.val-ctx .eyebrow-ctx{font-family:var(--font-sans);font-size:var(--fs-12);text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-3);font-weight:600;white-space:nowrap}
+.val-ctx .meta{display:flex;gap:var(--sp-4);margin-left:auto;font-family:var(--font-mono);font-size:var(--fs-12);color:var(--fg-3)}
+.val-ctx .meta span{white-space:nowrap}
+.val-ctx .meta .px{color:var(--fg-1);font-size:var(--fs-14)}
+.val-ctx .meta .chg-pos{color:var(--positive)}
+.val-ctx .meta .chg-neg{color:var(--negative)}
+.fi-body{padding:var(--sp-5);display:flex;flex-direction:column;gap:var(--sp-5)}
+.fi-lede{display:grid;grid-template-columns:1.6fr 1fr;gap:var(--sp-5);align-items:stretch;background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-5)}
+.fi-lede .left{display:flex;flex-direction:column;gap:var(--sp-2)}
+.fi-lede .ttl{font-family:var(--font-display);font-size:var(--fs-30);font-weight:500;letter-spacing:var(--tr-tight);line-height:1.1;color:var(--fg-1);margin:var(--sp-1) 0 0}
+.fi-lede .sub{font-family:var(--font-sans);font-size:var(--fs-13);color:var(--fg-2);line-height:1.55;max-width:68ch;margin:0}
+.fi-lede .right{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-3);align-content:end}
+.kr-source{display:flex;flex-direction:column;gap:2px;padding:var(--sp-3) var(--sp-4);background:var(--ink-2);border:1px solid var(--line-1);border-radius:var(--r-2)}
+.kr-source .lbl{font-family:var(--font-sans);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-3);font-weight:600}
+.kr-source .v{font-family:var(--font-mono);font-size:var(--fs-14);font-variant-numeric:tabular-nums;color:var(--fg-1);font-weight:500}
+.fi-controls{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-4);padding:var(--sp-3) var(--sp-4);background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3)}
+.ctl-group{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
+.ctl-lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:var(--tr-wider);text-transform:uppercase;color:var(--fg-4);margin-right:var(--sp-2)}
+.ctl-btn{font-family:var(--font-sans);font-size:var(--fs-12);font-weight:500;padding:5px 14px;border-radius:var(--r-2);border:1px solid var(--line-2);background:var(--ink-2);color:var(--fg-3);cursor:pointer;letter-spacing:var(--tr-wide)}
+.ctl-btn:hover{background:var(--ink-3);color:var(--fg-2)}
+.ctl-btn.active{background:var(--ink-3);color:var(--brass);border-color:var(--brass-deep)}
+.fi-kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--sp-3)}
+.fi-kpi{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-3) var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-1)}
+.fi-kpi .lbl{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-4);font-weight:500}
+.fi-kpi .v{font-family:var(--font-mono);font-size:var(--fs-24);line-height:1.1;font-variant-numeric:tabular-nums;color:var(--fg-1)}
+.fi-analysis-grid{display:grid;grid-template-columns:1.25fr 1fr;gap:var(--sp-4)}
+.fi-card{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);padding:var(--sp-3)}
+.fi-card-hd{font-family:var(--font-sans);font-size:var(--fs-12);font-weight:600;letter-spacing:var(--tr-wider);text-transform:uppercase;color:var(--fg-3);padding:0 var(--sp-1) var(--sp-2)}
+#fi-monthly-chart{height:308px}
+.fi-mix-head,.fi-mix-row{display:grid;grid-template-columns:1.6fr 0.8fr 0.8fr}
+.fi-mix-head{border-bottom:1px solid var(--line-1)}
+.fi-mix-head div{padding:9px var(--sp-2);font-family:var(--font-sans);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-4);font-weight:600}
+.fi-mix-head div:nth-child(2),.fi-mix-head div:nth-child(3),.fi-mix-row div:nth-child(2),.fi-mix-row div:nth-child(3){text-align:right}
+.fi-mix-row{border-bottom:1px solid var(--line-1)}
+.fi-mix-row:last-child{border-bottom:none}
+.fi-mix-row div{padding:10px var(--sp-2);font-family:var(--font-mono);font-size:var(--fs-13);font-variant-numeric:tabular-nums;color:var(--fg-2)}
+.fi-mix-row .f{font-family:var(--font-sans);color:var(--fg-1)}
+.fi-readout{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-2);padding:var(--sp-3) var(--sp-4);font-size:var(--fs-13);line-height:1.5;color:var(--fg-2)}
+.fi-table-card{background:var(--ink-1);border:1px solid var(--line-1);border-radius:var(--r-3);overflow:hidden}
+.fi-table-head{padding:var(--sp-3) var(--sp-4);border-bottom:1px solid var(--line-1);font-family:var(--font-sans);font-size:var(--fs-12);font-weight:600;letter-spacing:var(--tr-wider);text-transform:uppercase;color:var(--fg-3)}
+.fi-grid-head,.fi-grid-row{display:grid;grid-template-columns:0.8fr 0.8fr 2.3fr 0.7fr}
+.fi-grid-head{background:var(--ink-2);border-bottom:1px solid var(--line-1)}
+.fi-grid-head div{padding:8px var(--sp-3);font-family:var(--font-sans);font-size:10px;text-transform:uppercase;letter-spacing:var(--tr-wider);color:var(--fg-4);font-weight:600}
+.fi-grid-head div:last-child,.fi-grid-row div:last-child{text-align:right}
+.fi-grid-row{border-bottom:1px solid var(--line-1)}
+.fi-grid-row:last-child{border-bottom:none}
+.fi-grid-row.info{border-left:3px solid var(--oxford-light)}
+.fi-grid-row.warn{border-left:3px solid var(--warning)}
+.fi-grid-row.neutral{border-left:3px solid var(--line-2)}
+.fi-grid-row div{padding:10px var(--sp-3);font-family:var(--font-sans);font-size:var(--fs-13);color:var(--fg-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.fi-grid-row .mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
+.fi-grid-row .title{white-space:normal;line-height:1.4}
+.form-badge{display:inline-flex;align-items:center;justify-content:center;padding:2px 8px;border-radius:var(--r-full);font-family:var(--font-mono);font-size:11px;font-weight:500}
+.form-badge.info{background:var(--info-bg);color:var(--info)}
+.form-badge.warn{background:var(--warning-bg);color:var(--warning)}
+.form-badge.neutral{background:var(--ink-2);color:var(--fg-3);border:1px solid var(--line-2)}
+.link-view{font-family:var(--font-sans);font-size:var(--fs-12);font-weight:500;color:var(--brass);text-decoration:none;border-bottom:1px solid var(--brass-deep)}
+.link-view:hover{color:var(--brass-bright)}
+.fi-empty{padding:var(--sp-6);text-align:center;color:var(--fg-3);font-size:var(--fs-14);font-family:var(--font-sans)}
+.va-foot{font-family:var(--font-sans);font-size:var(--fs-12);color:var(--fg-3);line-height:1.6;padding:var(--sp-3) var(--sp-5);border:1px solid var(--line-1);border-radius:var(--r-2);background:var(--ink-1)}
+@media (max-width:1100px){
+ .fi-lede,.fi-analysis-grid{grid-template-columns:1fr}
+ .fi-lede .right{grid-template-columns:1fr}
+ .fi-controls{grid-template-columns:1fr}
+ .fi-kpis{grid-template-columns:1fr 1fr}
+ .fi-grid-head,.fi-grid-row{grid-template-columns:0.9fr 0.9fr 2fr 0.8fr}
+}
+</style>"""
+
+ ctx_html = (
+ '<div class="val-ctx">'
+ + '<span class="sym">'
+ + _esc(ticker.upper())
+ + "</span>"
+ + '<span class="name">'
+ + co_name
+ + "</span>"
+ + '<span class="eyebrow-ctx" style="margin-left:12px">Filings</span>'
+ + '<div class="meta">'
+ + "<span>"
+ + _esc(exchange)
+ + "</span>"
+ + '<span class="px num">'
+ + price_str
+ + "</span>"
+ + '<span class="'
+ + chg_cls
+ + ' num">'
+ + chg_str
+ + "</span>"
+ + "</div></div>"
+ )
+
+ lede_html = (
+ '<section class="fi-lede">'
+ + '<div class="left">'
+ + '<span class="eyebrow-lbl">Regulatory</span>'
+ + '<div class="ttl">How the company is reporting</div>'
+ + '<p class="sub">SEC filing flow for '
+ + _esc(ticker.upper())
+ + ' across annual, quarterly, and event-driven forms. Data is sourced from Yahoo Finance via yfinance and links route to EDGAR documents when available.</p>'
+ + "</div>"
+ + '<div class="right">'
+ + '<div class="kr-source"><span class="lbl">Source</span><span class="v">yfinance</span></div>'
+ + '<div class="kr-source"><span class="lbl">Feed</span><span class="v">SEC filings</span></div>'
+ + '<div class="kr-source"><span class="lbl">Filings</span><span class="v num">'
+ + str(len(rows))
+ + "</span></div>"
+ + "</div></section>"
+ )
+
+ controls_html = (
+ '<div class="fi-controls">'
+ + '<div class="ctl-group">'
+ + '<span class="ctl-lbl">Range</span>'
+ + '<button class="ctl-btn active" data-range="6m" onclick="setRange(\'6m\',this)">6M</button>'
+ + '<button class="ctl-btn" data-range="12m" onclick="setRange(\'12m\',this)">12M</button>'
+ + '<button class="ctl-btn" data-range="all" onclick="setRange(\'all\',this)">All</button>'
+ + "</div>"
+ + '<div class="ctl-group" id="fi-form-buttons">'
+ + '<span class="ctl-lbl">Form</span>'
+ + "</div>"
+ + "</div>"
+ )
+
+ foot_html = (
+ '<div class="va-foot">'
+ + "<span>SEC filing data provided by Yahoo Finance via yfinance · Filing links route to EDGAR documents when available · Form labels and counts reflect the current feed returned for this ticker</span>"
+ + "</div>"
+ )
+
+ js = (
+ "<script>"
+ + rows_js
+ + meta_js
+ + "var activeRange='6m';var activeForm='All';"
+ + "function cssVar(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim();}"
+ + "var C_BRASS=cssVar('--brass');var C_LINE=cssVar('--line-1');var C_FG3=cssVar('--fg-3');"
+ + "function esc(s){if(s===null||s===undefined)return '';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}"
+ + "function escAttr(s){return esc(s).replace(/\\\"/g,'&quot;').replace(/'/g,'&#39;');}"
+ + "function parseDate(s){if(!s)return null;var d=new Date(s+'T00:00:00');return isNaN(d.getTime())?null:d;}"
+ + "function monthsBack(n){var d=new Date();d.setHours(0,0,0,0);d.setMonth(d.getMonth()-n);return d;}"
+ + "function formPriorityOrder(forms){var priority=['10-K','10-Q','8-K'];var out=['All'];priority.forEach(function(f){if(forms.indexOf(f)>=0)out.push(f);});forms.filter(function(f){return priority.indexOf(f)<0;}).sort().forEach(function(f){out.push(f);});return out;}"
+ + "function collectForms(){var set={};(FILINGS||[]).forEach(function(r){var f=r.form||'Other';set[f]=1;});return Object.keys(set);}"
+ + "function buildFormButtons(){var host=document.getElementById('fi-form-buttons');if(!host)return;host.innerHTML='<span class=\"ctl-lbl\">Form</span>';var forms=formPriorityOrder(collectForms());forms.forEach(function(f,idx){var b=document.createElement('button');b.className='ctl-btn'+(idx===0?' active':'');b.setAttribute('data-form',f);b.textContent=f;b.onclick=function(){setForm(f,b);};host.appendChild(b);});}"
+ + "function applyFilters(){return (FILINGS||[]).filter(function(r){if(activeRange!=='all'){var cutoff=monthsBack(activeRange==='6m'?6:12);var dt=parseDate(r.date);if(!dt||dt<cutoff)return false;}if(activeForm!=='All'&&(r.form||'Other')!==activeForm)return false;return true;});}"
+ + "function countForms(rows){var m={};rows.forEach(function(r){var f=r.form||'Other';m[f]=(m[f]||0)+1;});return m;}"
+ + "function renderKPIs(rows){var m=countForms(rows);var distinct=Object.keys(m).length;var html='';"
+ + "html+='<div class=\"fi-kpi\"><div class=\"lbl\">Total Filings</div><div class=\"v\">'+rows.length.toLocaleString()+'</div></div>';"
+ + "html+='<div class=\"fi-kpi\"><div class=\"lbl\">10-K</div><div class=\"v\">'+String(m['10-K']||0)+'</div></div>';"
+ + "html+='<div class=\"fi-kpi\"><div class=\"lbl\">10-Q</div><div class=\"v\">'+String(m['10-Q']||0)+'</div></div>';"
+ + "html+='<div class=\"fi-kpi\"><div class=\"lbl\">8-K</div><div class=\"v\">'+String(m['8-K']||0)+'</div></div>';"
+ + "html+='<div class=\"fi-kpi\"><div class=\"lbl\">Distinct Forms</div><div class=\"v\">'+distinct.toLocaleString()+'</div></div>';"
+ + "document.getElementById('fi-kpis').innerHTML=html;}"
+ + "function renderCadenceChart(rows){if(typeof Plotly==='undefined')return;var monthMap={};rows.forEach(function(r){if(!r.month)return;monthMap[r.month]=(monthMap[r.month]||0)+1;});var months=Object.keys(monthMap).sort();var counts=months.map(function(m){return monthMap[m];});"
+ + "var data=[{x:months,y:counts,type:'bar',marker:{color:C_BRASS}}];"
+ + "var layout={plot_bgcolor:'rgba(0,0,0,0)',paper_bgcolor:'rgba(0,0,0,0)',margin:{l:44,r:12,t:16,b:42},height:300,font:{family:'IBM Plex Mono',color:C_FG3},xaxis:{title:'Month',gridcolor:C_LINE,zerolinecolor:C_LINE},yaxis:{title:'Filings',gridcolor:C_LINE,zerolinecolor:C_LINE},showlegend:false};"
+ + "Plotly.react('fi-monthly-chart',data,layout,{displayModeBar:false,responsive:true});}"
+ + "function renderFormMix(rows){var head='<div class=\"fi-mix-head\"><div>Form</div><div>Count</div><div>Share</div></div>';var m=countForms(rows);var total=rows.length;var forms=Object.keys(m).map(function(f){return {form:f,count:m[f],share:total>0?(m[f]*100/total):0};});forms.sort(function(a,b){if(b.count!==a.count)return b.count-a.count;return a.form.localeCompare(b.form);});forms=forms.slice(0,8);"
+ + "if(!forms.length){document.getElementById('fi-form-mix').innerHTML=head+'<div class=\"fi-empty\">No form mix for current filters.</div>';return;}var body='';forms.forEach(function(r){body+='<div class=\"fi-mix-row\"><div class=\"f\">'+esc(r.form)+'</div><div>'+String(r.count)+'</div><div>'+r.share.toFixed(1)+'%</div></div>';});document.getElementById('fi-form-mix').innerHTML=head+body;}"
+ + "function badgeTone(form){var f=(form||'').toUpperCase();if(f.indexOf('10-K')===0||f.indexOf('10-Q')===0)return 'info';if(f.indexOf('8-K')===0)return 'warn';return 'neutral';}"
+ + "function renderReadout(rows){var m=countForms(rows);var total=rows.length;var topForm='—';var topCount=0;Object.keys(m).forEach(function(f){if(m[f]>topCount){topCount=m[f];topForm=f;}});var topShare=(total>0&&topCount>0)?Math.round((topCount*100)/total):0;var latest='—';var latestForm='—';if(rows.length){latest=rows[0].date||'—';latestForm=rows[0].form||'—';}var msg1=topForm==='—'?'No filings in selected window.':(topForm+' filings represented '+String(topShare)+'% of the selected window.');var msg2='Most recent filing: '+latest+' ('+latestForm+').';var msg3='Coverage includes '+Object.keys(m).length+' distinct form types.';document.getElementById('fi-readout').innerHTML=esc(msg1)+' · '+esc(msg2)+' · '+esc(msg3);}"
+ + "function renderTable(rows){var head='<div class=\"fi-table-head\">Recent filings</div><div class=\"fi-grid-head\"><div>Date</div><div>Form</div><div>Title</div><div>Link</div></div>';if(!rows.length){document.getElementById('fi-table').innerHTML=head+'<div class=\"fi-empty\">No filings match current filters.</div>';return;}var body='';rows.forEach(function(r){var tone=badgeTone(r.form||'Other');var rowTone=tone==='warn'?'warn':(tone==='info'?'info':'neutral');var title=r.title||'—';var date=r.date||'—';var link='—';if(r.url){link='<a class=\"link-view\" href=\"'+escAttr(r.url)+'\" target=\"_blank\" rel=\"noopener noreferrer\">View</a>';}body+='<div class=\"fi-grid-row '+rowTone+'\">';body+='<div class=\"mono\">'+esc(date)+'</div>';body+='<div><span class=\"form-badge '+tone+'\">'+esc(r.form||'Other')+'</span></div>';body+='<div class=\"title\">'+esc(title)+'</div>';body+='<div>'+link+'</div>';body+='</div>';});document.getElementById('fi-table').innerHTML=head+body;}"
+ + "function refreshAll(){var filtered=applyFilters();renderKPIs(filtered);renderCadenceChart(filtered);renderFormMix(filtered);renderReadout(filtered);renderTable(filtered);}"
+ + "function setRange(range,btn){activeRange=range;document.querySelectorAll('.ctl-btn[data-range]').forEach(function(b){b.classList.remove('active');});if(btn)btn.classList.add('active');refreshAll();}"
+ + "function setForm(form,btn){activeForm=form;document.querySelectorAll('.ctl-btn[data-form]').forEach(function(b){b.classList.remove('active');});if(btn)btn.classList.add('active');refreshAll();}"
+ + "function bootFilings(){buildFormButtons();refreshAll();if(typeof Plotly==='undefined'){setTimeout(refreshAll,350);setTimeout(refreshAll,950);}}"
+ + "if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',bootFilings);}else{bootFilings();}"
+ + "</script>"
+ )
- color = _FORM_COLORS.get(form, "rgba(255,255,255,0.05)")
+ doc = (
+ "<!doctype html><html><head><meta charset='utf-8'>"
+ + plotly_cdn
+ + fonts_link
+ + _ROOT
+ + _FI_CSS
+ + "</head><body><div class='fi-wrap'>"
+ + ctx_html
+ + '<div class="fi-body">'
+ + lede_html
+ + controls_html
+ + '<div id="fi-kpis" class="fi-kpis"></div>'
+ + '<div class="fi-analysis-grid">'
+ + '<div class="fi-card"><div class="fi-card-hd">Monthly filing cadence</div><div id="fi-monthly-chart"></div></div>'
+ + '<div class="fi-card"><div class="fi-card-hd">Form mix</div><div id="fi-form-mix"></div></div>'
+ + "</div>"
+ + '<div id="fi-readout" class="fi-readout"></div>'
+ + '<div class="fi-table-card"><div id="fi-table"></div></div>'
+ + foot_html
+ + "</div></div>"
+ + js
+ + "</body></html>"
+ )
- with st.container():
- left, right = st.columns([5, 1])
- with left:
- st.markdown(
- f"<div style='"
- f"background:{color};"
- f"border:1px solid rgba(194,170,122,0.12);"
- f"padding:8px 12px;border-radius:2px;margin-bottom:2px;"
- f"display:flex;align-items:baseline;gap:10px;"
- f"'>"
- f"<span style='"
- f"font-family:IBM Plex Mono,monospace;"
- f"font-size:11px;color:#C2AA7A;"
- f"background:rgba(194,170,122,0.07);"
- f"border:1px solid rgba(194,170,122,0.25);"
- f"padding:2px 6px;border-radius:2px;"
- f"white-space:nowrap;"
- f"'>{form}</span>"
- f"<span style='font-family:IBM Plex Sans,sans-serif;font-size:0.8125rem;color:#F2ECDC;'>{title}</span>"
- f"<span style='font-family:IBM Plex Mono,monospace;font-size:11px;color:#5E5849;margin-left:auto;white-space:nowrap;'>{date}</span>"
- f"</div>",
- unsafe_allow_html=True,
- )
- with right:
- doc_url = exhibits.get(form) or edgar_url
- if doc_url:
- st.markdown(
- f"<div style='padding-top:8px;text-align:right;'>"
- f"<a href='{doc_url}' target='_blank' style='"
- f"font-family:IBM Plex Sans,sans-serif;"
- f"font-size:11px;color:#C2AA7A;"
- f"text-decoration:none;"
- f"border-bottom:1px solid #8F7A50;"
- f"'>View ↗</a></div>",
- unsafe_allow_html=True,
- )
+ components.html(doc, height=height, scrolling=False)