diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-05-15 23:47:50 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-05-15 23:47:50 -0700 |
| commit | 870f8e6c8b88d61d0f7183b938b9a496c193b141 (patch) | |
| tree | f6b6e7c6b7b719ae13e85d4b7a94f03ae990163b /components/filings.py | |
| parent | 551e4019b78f418af8fb8ea941ad8d0dac00eecc (diff) | |
Redesign filings tab as client-side surface
Diffstat (limited to 'components/filings.py')
| -rw-r--r-- | components/filings.py | 424 |
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,'&').replace(/</g,'<').replace(/>/g,'>');}" + + "function escAttr(s){return esc(s).replace(/\\\"/g,'"').replace(/'/g,''');}" + + "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) |
