cjc0013 commited on
Commit
17a4606
·
verified ·
1 Parent(s): e7a04a6

Redesign public disclosure explorer landing page

Browse files
Files changed (3) hide show
  1. README.md +3 -3
  2. app.py +287 -55
  3. space_bundle_manifest.json +5 -5
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Senator Wealth And Trading Dataset
3
  emoji: 📊
4
  colorFrom: green
5
  colorTo: yellow
@@ -10,8 +10,8 @@ pinned: false
10
  license: cc-by-4.0
11
  ---
12
 
13
- # Senator Wealth And Trading Dataset
14
 
15
- Public-interest explorer for official Senate disclosure-derived wealth intervals, disclosed trades, filing behavior, and no-cost exploratory return-test summaries.
16
 
17
  This Space does not claim intent, illegality, causality, insider trading, realized private returns, or release-grade above-market alpha.
 
1
  ---
2
+ title: Senator Financial Disclosure Explorer
3
  emoji: 📊
4
  colorFrom: green
5
  colorTo: yellow
 
10
  license: cc-by-4.0
11
  ---
12
 
13
+ # Senator Financial Disclosure Explorer
14
 
15
+ Public-interest explorer for official Senate financial disclosures: reported wealth ranges, disclosed transactions, filing delays, and cautious market-context checks.
16
 
17
  This Space does not claim intent, illegality, causality, insider trading, realized private returns, or release-grade above-market alpha.
app.py CHANGED
@@ -61,11 +61,41 @@ def pct(value) -> float | None:
61
  return None
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  RETURN_TESTS = pd.read_csv(DATASET / "return_tests.csv")
65
  RETURN_TESTS["equal_weight_alpha_pct"] = RETURN_TESTS["alpha_equal_weight"].map(pct)
66
  RETURN_TESTS["midpoint_weight_alpha_pct"] = RETURN_TESTS["alpha_midpoint"].map(pct)
67
  RETURN_TESTS["ci_low_pct"] = RETURN_TESTS["bootstrap_ci_low"].map(pct)
68
  RETURN_TESTS["ci_high_pct"] = RETURN_TESTS["bootstrap_ci_high"].map(pct)
 
 
 
69
 
70
  RETURN_HANDOFF = pd.read_csv(DATA / "reporter_handoff" / "return_test_summary_handoff.csv")
71
  TRADES = read_jsonl_gz("trades.jsonl.gz")
@@ -81,53 +111,148 @@ for col in ["net_worth_min", "net_worth_midpoint", "net_worth_max", "uncertainty
81
  WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce")
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  def metric_html() -> str:
85
  counts = MANIFEST.get("counts", {})
86
  dataset = counts.get("dataset", {})
87
  returns = counts.get("return_tests", {})
88
  items = [
89
- ("Trades", dataset.get("trades", "")),
90
- ("Filings", dataset.get("filings", "")),
91
- ("Asset rows", dataset.get("annual_assets", "")),
92
- ("Wealth intervals", dataset.get("net_worth_intervals", "")),
93
- ("Return events", returns.get("return_test_events", "")),
94
- ("Alpha claim", "Blocked"),
95
  ]
96
  cells = "".join(
97
- f"<div class='metric'><div class='metric-value'>{value}</div><div class='metric-label'>{label}</div></div>"
98
- for label, value in items
 
 
 
 
 
 
99
  )
100
- return f"<div class='metrics'>{cells}</div>"
101
 
102
 
103
  def boundary_html() -> str:
104
- return f"""
105
- <div class='boundary'>
106
- <strong>Claim boundary:</strong> {MANIFEST.get('claim_boundary', '')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  </div>
108
  """
109
 
110
 
111
  def return_chart():
112
  fig = go.Figure()
113
- for date_basis, frame in RETURN_TESTS.groupby("event_date_source"):
114
  fig.add_trace(
115
  go.Bar(
116
- name=date_basis.replace("_", " ").title(),
117
- x=frame["window"],
118
  y=frame["equal_weight_alpha_pct"],
119
- text=frame["claim_status"],
120
- hovertemplate="%{x}<br>Equal-weight alpha: %{y:.3f}%<br>%{text}<extra></extra>",
 
 
 
 
 
 
121
  )
122
  )
123
  fig.add_hline(y=0, line_width=1, line_color="#555")
124
  fig.update_layout(
125
- title="Exploratory abnormal return summaries",
126
- yaxis_title="Equal-weight signed abnormal return (%)",
127
- xaxis_title="Window",
128
  barmode="group",
129
- margin=dict(l=40, r=20, t=60, b=50),
130
- legend_title_text="Date basis",
131
  )
132
  return style_figure(fig)
133
 
@@ -136,13 +261,13 @@ def filing_delay_chart():
136
  frame = TRADES.dropna(subset=["filing_lag_days_num"]).copy()
137
  frame = frame[(frame["filing_lag_days_num"] >= 0) & (frame["filing_lag_days_num"] <= 800)]
138
  bins = [-1, 30, 45, 90, 180, 365, 800]
139
- labels = ["0-30", "31-45", "46-90", "91-180", "181-365", "366+"]
140
  frame["lag_bucket"] = pd.cut(frame["filing_lag_days_num"], bins=bins, labels=labels)
141
  counts = frame["lag_bucket"].value_counts().reindex(labels).reset_index()
142
  counts.columns = ["lag_bucket", "rows"]
143
  fig = px.bar(counts, x="lag_bucket", y="rows", color="lag_bucket", color_discrete_sequence=px.colors.qualitative.Safe)
144
  fig.update_layout(
145
- title="Disclosure filing lag buckets",
146
  xaxis_title="Days between transaction and filing",
147
  yaxis_title="Disclosed trade rows",
148
  showlegend=False,
@@ -158,7 +283,7 @@ def trade_type_chart():
158
  yearly = yearly[(yearly["transaction_year"] >= 2012) & (yearly["transaction_year"] <= 2026)]
159
  fig = px.area(yearly, x="transaction_year", y="rows", color="kind", color_discrete_sequence=px.colors.qualitative.Safe)
160
  fig.update_layout(
161
- title="Disclosed trades by year and type",
162
  xaxis_title="Transaction year",
163
  yaxis_title="Rows",
164
  margin=dict(l=40, r=20, t=60, b=50),
@@ -172,7 +297,7 @@ def top_ticker_chart():
172
  counts.columns = ["ticker", "rows"]
173
  fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis")
174
  fig.update_layout(
175
- title="Most common ticker candidates in disclosures",
176
  xaxis_title="Rows",
177
  yaxis_title="Ticker candidate",
178
  margin=dict(l=80, r=20, t=60, b=50),
@@ -224,9 +349,9 @@ def wealth_chart(senator_id: str):
224
  )
225
  )
226
  fig.update_layout(
227
- title=f"Wealth interval timeline: {senator_id}",
228
  xaxis_title="Report year",
229
- yaxis_title="Net-worth interval dollars",
230
  margin=dict(l=70, r=20, t=60, b=50),
231
  )
232
  return style_figure(fig)
@@ -243,7 +368,17 @@ def wealth_table(senator_id: str) -> pd.DataFrame:
243
  "calculation_note",
244
  ]
245
  frame = WEALTH[WEALTH["senator_id"] == senator_id][cols].sort_values("report_year")
246
- return frame
 
 
 
 
 
 
 
 
 
 
247
 
248
 
249
  def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
@@ -269,7 +404,62 @@ def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
269
  "amount_max",
270
  "what_this_does_not_prove",
271
  ]
272
- return frame[cols].head(int(max_rows))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
 
275
  def source_gap_table() -> pd.DataFrame:
@@ -300,6 +490,23 @@ CSS = """
300
  --soft: #f8fafc;
301
  --font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
302
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  .gradio-container,
304
  .gradio-container * {
305
  font-family: var(--font) !important;
@@ -314,6 +521,7 @@ CSS = """
314
  color: var(--ink);
315
  font-size: 16px;
316
  line-height: 1.48;
 
317
  }
318
  .gradio-container h1,
319
  .gradio-container .prose h1,
@@ -343,11 +551,24 @@ CSS = """
343
  color: var(--accent) !important;
344
  font-weight: 800 !important;
345
  }
 
 
 
346
  .gradio-container input,
347
  .gradio-container textarea,
348
  .gradio-container select {
349
  color: #111827 !important;
350
  font-size: 1rem !important;
 
 
 
 
 
 
 
 
 
 
351
  }
352
  .metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
353
  .metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; }
@@ -384,53 +605,64 @@ CSS = """
384
  .gradio-container .table-wrap {
385
  border-color: var(--line) !important;
386
  }
387
- @media (max-width: 900px) { .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
 
 
 
 
 
 
388
  """
389
 
390
 
391
- with gr.Blocks(title="Senator Wealth And Trading Dataset") as demo:
392
- gr.Markdown("# Senator Wealth And Trading Dataset")
 
393
  gr.HTML(metric_html())
394
  gr.HTML(boundary_html())
395
 
396
  with gr.Tabs():
397
- with gr.Tab("Overview"):
 
398
  with gr.Row():
399
- gr.Plot(value=return_chart(), label="Exploratory returns")
400
- with gr.Row():
401
- gr.Plot(value=filing_delay_chart(), label="Filing delay")
402
- gr.Plot(value=top_ticker_chart(), label="Ticker candidates")
403
- gr.Markdown(
404
- "The data is useful as public disclosure evidence and exploratory market context. The public alpha claim is blocked because this no-pay bundle does not use a licensed security-level total-return feed with delisting handling."
 
405
  )
406
 
407
- with gr.Tab("Wealth Intervals"):
408
  choices = wealth_senators()
409
- selector = gr.Dropdown(choices=choices, value=choices[0] if choices else None, label="Senator/filer id")
410
- wealth_plot = gr.Plot(value=wealth_chart(choices[0]) if choices else go.Figure(), label="Wealth interval")
411
- wealth_rows = gr.Dataframe(value=wealth_table(choices[0]) if choices else pd.DataFrame(), label="Interval rows", interactive=False, wrap=True)
 
412
  selector.change(wealth_chart, inputs=selector, outputs=wealth_plot)
413
  selector.change(wealth_table, inputs=selector, outputs=wealth_rows)
414
 
415
- with gr.Tab("Disclosed Trades"):
 
416
  with gr.Row():
417
- search_box = gr.Textbox(label="Search asset, ticker, or filer id", value="")
418
  min_lag = gr.Slider(0, 400, value=0, step=5, label="Minimum filing lag days")
419
  max_rows = gr.Slider(10, 250, value=50, step=10, label="Rows")
420
- trade_rows = gr.Dataframe(value=trade_search("", 0, 50), label="Trade rows", interactive=False, wrap=True)
421
  search_box.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
422
  min_lag.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
423
  max_rows.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
424
- gr.Plot(value=trade_type_chart(), label="Trade type by year")
425
 
426
- with gr.Tab("Return Tests"):
427
- gr.Plot(value=return_chart(), label="Exploratory return-test chart")
428
- gr.Dataframe(value=RETURN_HANDOFF, label="Plain-language return-test summaries", interactive=False, wrap=True)
429
- gr.Markdown(
430
- "Rows are blocked from public alpha claims. A positive cell is not proof of intent, illegality, causality, or realized private return."
431
- )
432
 
433
- with gr.Tab("Receipts And Downloads"):
 
434
  gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True)
435
  gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True)
436
  gr.File(
 
61
  return None
62
 
63
 
64
+ def fmt_int(value) -> str:
65
+ try:
66
+ return f"{int(value):,}"
67
+ except (TypeError, ValueError):
68
+ return str(value or "")
69
+
70
+
71
+ def basis_label(value: str) -> str:
72
+ labels = {
73
+ "filing_date": "after public filing date",
74
+ "transaction_date": "after reported transaction date",
75
+ }
76
+ return labels.get(str(value), str(value).replace("_", " "))
77
+
78
+
79
+ def window_label(value: str) -> str:
80
+ return str(value).replace("_trading_days", " trading days").replace("_", " ")
81
+
82
+
83
+ def claim_label(value: str) -> str:
84
+ labels = {
85
+ "blocked_market_data_policy": "Not a public alpha claim: stronger market data required",
86
+ "blocked": "Not a public alpha claim",
87
+ }
88
+ return labels.get(str(value), str(value).replace("_", " "))
89
+
90
+
91
  RETURN_TESTS = pd.read_csv(DATASET / "return_tests.csv")
92
  RETURN_TESTS["equal_weight_alpha_pct"] = RETURN_TESTS["alpha_equal_weight"].map(pct)
93
  RETURN_TESTS["midpoint_weight_alpha_pct"] = RETURN_TESTS["alpha_midpoint"].map(pct)
94
  RETURN_TESTS["ci_low_pct"] = RETURN_TESTS["bootstrap_ci_low"].map(pct)
95
  RETURN_TESTS["ci_high_pct"] = RETURN_TESTS["bootstrap_ci_high"].map(pct)
96
+ RETURN_TESTS["date_basis_label"] = RETURN_TESTS["event_date_source"].map(basis_label)
97
+ RETURN_TESTS["window_label"] = RETURN_TESTS["window"].map(window_label)
98
+ RETURN_TESTS["claim_status_label"] = RETURN_TESTS["claim_status"].map(claim_label)
99
 
100
  RETURN_HANDOFF = pd.read_csv(DATA / "reporter_handoff" / "return_test_summary_handoff.csv")
101
  TRADES = read_jsonl_gz("trades.jsonl.gz")
 
111
  WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce")
112
 
113
 
114
+ def hero_html() -> str:
115
+ return """
116
+ <section class="hero-panel" style="background:#ffffff;border:1px solid #cbd5e1;border-radius:12px;padding:22px 24px;margin:12px 0 16px;box-shadow:0 16px 40px rgba(15,23,42,.08);">
117
+ <div style="display:flex;gap:22px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;">
118
+ <div style="min-width:280px;flex:1 1 620px;">
119
+ <div style="color:#0f766e;font-size:.88rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase;margin-bottom:8px;">Official Senate financial disclosure data</div>
120
+ <h1 style="color:#0f172a;font-size:2.35rem;line-height:1.08;font-weight:850;margin:0 0 10px;">Senator Financial Disclosure Explorer</h1>
121
+ <p style="color:#334155;font-size:1.08rem;line-height:1.6;max-width:850px;margin:0;">
122
+ This turns public Senate disclosure forms into searchable visuals: reported wealth ranges, disclosed transactions,
123
+ filing delays, and cautious market-context checks. It is built to show what the records say, where the uncertainty is,
124
+ and what cannot be concluded from the disclosures alone.
125
+ </p>
126
+ </div>
127
+ <div style="background:#f8fafc;border:1px solid #d7dee8;border-radius:10px;padding:14px 16px;min-width:250px;flex:0 1 310px;">
128
+ <div style="color:#64748b;font-size:.84rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;">Current evidence status</div>
129
+ <div style="color:#0f172a;font-size:1.35rem;font-weight:850;line-height:1.2;margin-top:5px;">Disclosure explorer: ready</div>
130
+ <div style="color:#8a4b00;font-size:1rem;font-weight:800;margin-top:10px;">Market-beating proof: not established</div>
131
+ <p style="color:#475569;font-size:.95rem;line-height:1.45;margin:8px 0 0;">The market checks are exploratory because the public bundle does not use a licensed total-return source with delisting handling.</p>
132
+ </div>
133
+ </div>
134
+ </section>
135
+ """
136
+
137
+
138
+ def plain_english_cards_html() -> str:
139
+ cards = [
140
+ (
141
+ "What this is",
142
+ "A public-records browser for Senate financial disclosures: filings, reported asset ranges, liabilities, income rows, and disclosed transactions.",
143
+ "#e8f7f3",
144
+ "#0f766e",
145
+ ),
146
+ (
147
+ "Why wealth is shown as ranges",
148
+ "Disclosures use broad dollar brackets. The app shows low, midpoint, and high estimates so the uncertainty stays visible.",
149
+ "#eef4ff",
150
+ "#3155b7",
151
+ ),
152
+ (
153
+ "What the market charts mean",
154
+ "They compare disclosed timing to simple benchmarks under stated assumptions. They do not prove motive, private profit, illegality, or insider trading.",
155
+ "#fff7e8",
156
+ "#8a4b00",
157
+ ),
158
+ ]
159
+ cells = "".join(
160
+ f"""
161
+ <div style="background:{bg};border:1px solid rgba(15,23,42,.12);border-radius:10px;padding:15px 16px;min-height:142px;">
162
+ <div style="color:{accent};font-size:1.02rem;font-weight:850;margin-bottom:8px;">{title}</div>
163
+ <div style="color:#243447;font-size:1rem;line-height:1.5;">{body}</div>
164
+ </div>
165
+ """
166
+ for title, body, bg, accent in cards
167
+ )
168
+ return f"<div class='plain-cards' style='display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:14px 0 18px;'>{cells}</div>"
169
+
170
+
171
  def metric_html() -> str:
172
  counts = MANIFEST.get("counts", {})
173
  dataset = counts.get("dataset", {})
174
  returns = counts.get("return_tests", {})
175
  items = [
176
+ ("Disclosed transaction rows", fmt_int(dataset.get("trades", "")), "transactions listed in Senate reports"),
177
+ ("Financial disclosure filings", fmt_int(dataset.get("filings", "")), "annual reports, PTRs, and amendments"),
178
+ ("Reported asset rows", fmt_int(dataset.get("annual_assets", "")), "asset bracket rows parsed from filings"),
179
+ ("Wealth range estimates", fmt_int(dataset.get("net_worth_intervals", "")), "low/mid/high disclosure estimates"),
180
+ ("Market-context event rows", fmt_int(returns.get("return_test_events", "")), "exploratory benchmark comparisons"),
181
+ ("Public alpha claim", "Not established", "blocked by market-data standard"),
182
  ]
183
  cells = "".join(
184
+ f"""
185
+ <div class='metric' style='border:1px solid #cbd5e1;border-radius:10px;padding:13px 14px;background:#ffffff;box-shadow:0 6px 18px rgba(15,23,42,.05);'>
186
+ <div class='metric-value' style='font-size:1.38rem;font-weight:850;color:#0f766e;line-height:1.15;overflow-wrap:anywhere;'>{value}</div>
187
+ <div class='metric-label' style='font-size:.92rem;color:#0f172a;font-weight:800;margin-top:5px;line-height:1.25;'>{label}</div>
188
+ <div style='font-size:.82rem;color:#475569;font-weight:600;margin-top:4px;line-height:1.25;'>{note}</div>
189
+ </div>
190
+ """
191
+ for label, value, note in items
192
  )
193
+ return f"<div class='metrics' style='display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin:14px 0 18px;'>{cells}</div>"
194
 
195
 
196
  def boundary_html() -> str:
197
+ return """
198
+ <div class='boundary' style='border-left:5px solid #8a4b00;background:#fff7e8;color:#3f2a00;padding:14px 16px;border-radius:8px;margin:12px 0 20px;font-size:1rem;line-height:1.55;'>
199
+ <strong style='color:#2f1d00;font-weight:850;'>Important boundary:</strong>
200
+ This is a public disclosure explorer, not an accusation tool. It does not claim intent, illegality, causality,
201
+ insider trading, realized private returns, or proven above-market performance.
202
+ </div>
203
+ """
204
+
205
+
206
+ def overview_intro_html() -> str:
207
+ return """
208
+ <div class="section-intro" style="background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:16px 18px;margin:8px 0 14px;">
209
+ <div style="color:#0f172a;font-size:1.18rem;font-weight:850;margin-bottom:6px;">Start with the records, then the timing</div>
210
+ <div style="color:#334155;font-size:1rem;line-height:1.55;">
211
+ The first charts show filing behavior and what assets appear most often in disclosed transactions.
212
+ The return chart is deliberately lower on the page because it is context, not a verdict.
213
+ </div>
214
+ </div>
215
+ """
216
+
217
+
218
+ def market_note_html() -> str:
219
+ return """
220
+ <div class="section-intro" style="background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;">
221
+ <div style="color:#0f172a;font-size:1.08rem;font-weight:850;margin-bottom:5px;">How to read the market-context chart</div>
222
+ <div style="color:#334155;font-size:1rem;line-height:1.55;">
223
+ Bars above zero mean the disclosed transaction set moved more than a simple benchmark in that window under one assumption.
224
+ Bars below zero mean it moved less. Because this public bundle uses no-cost adjusted-close data, every row remains exploratory.
225
+ </div>
226
  </div>
227
  """
228
 
229
 
230
  def return_chart():
231
  fig = go.Figure()
232
+ for date_basis, frame in RETURN_TESTS.groupby("date_basis_label"):
233
  fig.add_trace(
234
  go.Bar(
235
+ name=str(date_basis).capitalize(),
236
+ x=frame["window_label"],
237
  y=frame["equal_weight_alpha_pct"],
238
+ customdata=frame[["claim_status_label", "n_events", "q_value"]],
239
+ hovertemplate=(
240
+ "%{x}<br>"
241
+ "Benchmark difference: %{y:.3f}%<br>"
242
+ "Rows: %{customdata[1]}<br>"
243
+ "%{customdata[0]}<br>"
244
+ "q-value: %{customdata[2]}<extra></extra>"
245
+ ),
246
  )
247
  )
248
  fig.add_hline(y=0, line_width=1, line_color="#555")
249
  fig.update_layout(
250
+ title="Exploratory market check: disclosed transactions vs benchmark",
251
+ yaxis_title="Benchmark difference (%)",
252
+ xaxis_title="Market window after date",
253
  barmode="group",
254
+ margin=dict(l=64, r=20, t=70, b=62),
255
+ legend_title_text="Measured from",
256
  )
257
  return style_figure(fig)
258
 
 
261
  frame = TRADES.dropna(subset=["filing_lag_days_num"]).copy()
262
  frame = frame[(frame["filing_lag_days_num"] >= 0) & (frame["filing_lag_days_num"] <= 800)]
263
  bins = [-1, 30, 45, 90, 180, 365, 800]
264
+ labels = ["0-30 days", "31-45 days", "46-90 days", "91-180 days", "181-365 days", "366+ days"]
265
  frame["lag_bucket"] = pd.cut(frame["filing_lag_days_num"], bins=bins, labels=labels)
266
  counts = frame["lag_bucket"].value_counts().reindex(labels).reset_index()
267
  counts.columns = ["lag_bucket", "rows"]
268
  fig = px.bar(counts, x="lag_bucket", y="rows", color="lag_bucket", color_discrete_sequence=px.colors.qualitative.Safe)
269
  fig.update_layout(
270
+ title="How long after a transaction was it filed?",
271
  xaxis_title="Days between transaction and filing",
272
  yaxis_title="Disclosed trade rows",
273
  showlegend=False,
 
283
  yearly = yearly[(yearly["transaction_year"] >= 2012) & (yearly["transaction_year"] <= 2026)]
284
  fig = px.area(yearly, x="transaction_year", y="rows", color="kind", color_discrete_sequence=px.colors.qualitative.Safe)
285
  fig.update_layout(
286
+ title="Disclosed transaction rows by year and type",
287
  xaxis_title="Transaction year",
288
  yaxis_title="Rows",
289
  margin=dict(l=40, r=20, t=60, b=50),
 
297
  counts.columns = ["ticker", "rows"]
298
  fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis")
299
  fig.update_layout(
300
+ title="Ticker candidates that appear most often",
301
  xaxis_title="Rows",
302
  yaxis_title="Ticker candidate",
303
  margin=dict(l=80, r=20, t=60, b=50),
 
349
  )
350
  )
351
  fig.update_layout(
352
+ title=f"Reported wealth range over time: {senator_id}",
353
  xaxis_title="Report year",
354
+ yaxis_title="Estimated range from disclosure brackets ($)",
355
  margin=dict(l=70, r=20, t=60, b=50),
356
  )
357
  return style_figure(fig)
 
368
  "calculation_note",
369
  ]
370
  frame = WEALTH[WEALTH["senator_id"] == senator_id][cols].sort_values("report_year")
371
+ return frame.rename(
372
+ columns={
373
+ "report_year": "Report year",
374
+ "net_worth_min": "Low estimate",
375
+ "net_worth_midpoint": "Midpoint estimate",
376
+ "net_worth_max": "High estimate",
377
+ "uncertainty_width": "Range width",
378
+ "top_coded": "Open-ended top bracket",
379
+ "calculation_note": "How to read it",
380
+ }
381
+ )
382
 
383
 
384
  def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
 
404
  "amount_max",
405
  "what_this_does_not_prove",
406
  ]
407
+ return frame[cols].head(int(max_rows)).rename(
408
+ columns={
409
+ "senator_id": "Senator/Filer ID",
410
+ "transaction_date": "Reported transaction date",
411
+ "filing_date": "Public filing date",
412
+ "filing_lag_days": "Filing lag days",
413
+ "transaction_type": "Disclosure transaction type",
414
+ "owner": "Reported owner",
415
+ "asset_name": "Asset name from filing",
416
+ "ticker_candidate": "Ticker candidate",
417
+ "amount_min": "Amount low",
418
+ "amount_max": "Amount high",
419
+ "what_this_does_not_prove": "What this row does not prove",
420
+ }
421
+ )
422
+
423
+
424
+ def return_handoff_display() -> pd.DataFrame:
425
+ frame = RETURN_HANDOFF.copy()
426
+ if "date_basis" in frame:
427
+ frame["date_basis"] = frame["date_basis"].map(basis_label)
428
+ if "window" in frame:
429
+ frame["window"] = frame["window"].map(window_label)
430
+ if "claim_status" in frame:
431
+ frame["claim_status"] = frame["claim_status"].map(claim_label)
432
+ cols = [
433
+ "date_basis",
434
+ "window",
435
+ "events",
436
+ "senators",
437
+ "tickers",
438
+ "equal_weight_alpha_pct",
439
+ "midpoint_weight_alpha_pct",
440
+ "bootstrap_ci_low_pct",
441
+ "bootstrap_ci_high_pct",
442
+ "q_value",
443
+ "claim_status",
444
+ "plain_english_status",
445
+ ]
446
+ frame = frame[[col for col in cols if col in frame.columns]]
447
+ return frame.rename(
448
+ columns={
449
+ "date_basis": "Measured from",
450
+ "window": "Window",
451
+ "events": "Transaction-event rows",
452
+ "senators": "Senators/filer IDs",
453
+ "tickers": "Ticker candidates",
454
+ "equal_weight_alpha_pct": "Equal-weight benchmark difference (%)",
455
+ "midpoint_weight_alpha_pct": "Midpoint-weight benchmark difference (%)",
456
+ "bootstrap_ci_low_pct": "Bootstrap low (%)",
457
+ "bootstrap_ci_high_pct": "Bootstrap high (%)",
458
+ "q_value": "FDR-adjusted q-value",
459
+ "claim_status": "Claim status",
460
+ "plain_english_status": "Plain English status",
461
+ }
462
+ )
463
 
464
 
465
  def source_gap_table() -> pd.DataFrame:
 
490
  --soft: #f8fafc;
491
  --font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
492
  }
493
+ html,
494
+ body,
495
+ #root,
496
+ .gradio-container,
497
+ .gradio-container .main,
498
+ .gradio-container .wrap,
499
+ .gradio-container .contain {
500
+ background: #f6f8fb !important;
501
+ color: var(--ink) !important;
502
+ }
503
+ .dark .gradio-container,
504
+ .dark .gradio-container .main,
505
+ .dark .gradio-container .wrap,
506
+ .dark .gradio-container .contain {
507
+ background: #f6f8fb !important;
508
+ color: var(--ink) !important;
509
+ }
510
  .gradio-container,
511
  .gradio-container * {
512
  font-family: var(--font) !important;
 
521
  color: var(--ink);
522
  font-size: 16px;
523
  line-height: 1.48;
524
+ padding: 0 18px 28px;
525
  }
526
  .gradio-container h1,
527
  .gradio-container .prose h1,
 
551
  color: var(--accent) !important;
552
  font-weight: 800 !important;
553
  }
554
+ .gradio-container button[role="tab"] {
555
+ min-height: 42px !important;
556
+ }
557
  .gradio-container input,
558
  .gradio-container textarea,
559
  .gradio-container select {
560
  color: #111827 !important;
561
  font-size: 1rem !important;
562
+ background: #ffffff !important;
563
+ }
564
+ .gradio-container .block,
565
+ .gradio-container .form,
566
+ .gradio-container .panel,
567
+ .gradio-container .plot-container,
568
+ .gradio-container .table-wrap,
569
+ .gradio-container .dataframe {
570
+ background: #ffffff !important;
571
+ color: #111827 !important;
572
  }
573
  .metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
574
  .metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; }
 
605
  .gradio-container .table-wrap {
606
  border-color: var(--line) !important;
607
  }
608
+ @media (max-width: 900px) {
609
+ .metrics, .plain-cards { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
610
+ .hero-panel h1 { font-size: 1.8rem !important; }
611
+ }
612
+ @media (max-width: 620px) {
613
+ .metrics, .plain-cards { grid-template-columns: 1fr !important; }
614
+ }
615
  """
616
 
617
 
618
+ with gr.Blocks(title="Senator Financial Disclosure Explorer") as demo:
619
+ gr.HTML(hero_html())
620
+ gr.HTML(plain_english_cards_html())
621
  gr.HTML(metric_html())
622
  gr.HTML(boundary_html())
623
 
624
  with gr.Tabs():
625
+ with gr.Tab("Start Here"):
626
+ gr.HTML(overview_intro_html())
627
  with gr.Row():
628
+ gr.Plot(value=filing_delay_chart(), label="Filing delay: how late were public reports?")
629
+ gr.Plot(value=top_ticker_chart(), label="Ticker candidates: what appears in filings?")
630
+ gr.HTML(
631
+ "<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;color:#334155;font-size:1rem;line-height:1.55;'>"
632
+ "<strong style='color:#0f172a;'>Where to go next:</strong> use <strong>Wealth Ranges</strong> to inspect bracket-based wealth estimates, "
633
+ "<strong>Disclosed Transactions</strong> to search filings, <strong>Market Checks</strong> for cautious benchmark context, and <strong>Receipts</strong> for the audit trail."
634
+ "</div>"
635
  )
636
 
637
+ with gr.Tab("Wealth Ranges"):
638
  choices = wealth_senators()
639
+ gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>Read this as a range, not a precise fortune.</strong> Senate forms use value brackets, so the ribbon shows low-to-high estimates from reported assets and liabilities.</div>")
640
+ selector = gr.Dropdown(choices=choices, value=choices[0] if choices else None, label="Choose a senator/filer ID")
641
+ wealth_plot = gr.Plot(value=wealth_chart(choices[0]) if choices else style_figure(go.Figure()), label="Reported wealth range")
642
+ wealth_rows = gr.Dataframe(value=wealth_table(choices[0]) if choices else pd.DataFrame(), label="Range rows from disclosure brackets", interactive=False, wrap=True)
643
  selector.change(wealth_chart, inputs=selector, outputs=wealth_plot)
644
  selector.change(wealth_table, inputs=selector, outputs=wealth_rows)
645
 
646
+ with gr.Tab("Disclosed Transactions"):
647
+ gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>These are disclosed transaction rows.</strong> A row can be a purchase, sale, exchange, or another reported transaction type. The table does not know execution price, exact trade size, or private realized return.</div>")
648
  with gr.Row():
649
+ search_box = gr.Textbox(label="Search asset, ticker, or filer ID", value="")
650
  min_lag = gr.Slider(0, 400, value=0, step=5, label="Minimum filing lag days")
651
  max_rows = gr.Slider(10, 250, value=50, step=10, label="Rows")
652
+ trade_rows = gr.Dataframe(value=trade_search("", 0, 50), label="Disclosed transaction rows", interactive=False, wrap=True)
653
  search_box.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
654
  min_lag.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
655
  max_rows.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
656
+ gr.Plot(value=trade_type_chart(), label="Transaction type by year")
657
 
658
+ with gr.Tab("Market Checks"):
659
+ gr.HTML(market_note_html())
660
+ gr.Plot(value=return_chart(), label="Exploratory market-context chart")
661
+ gr.Dataframe(value=return_handoff_display(), label="Plain-English market-check rows", interactive=False, wrap=True)
662
+ gr.HTML("<div class='boundary' style='border-left:5px solid #8a4b00;background:#fff7e8;color:#3f2a00;padding:14px 16px;border-radius:8px;margin:12px 0 20px;font-size:1rem;line-height:1.55;'><strong style='color:#2f1d00;'>Bottom line:</strong> these rows are blocked from public alpha claims. A positive cell is not proof of intent, illegality, causality, insider trading, or realized private return.</div>")
 
663
 
664
+ with gr.Tab("Receipts"):
665
+ gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>Receipts are the audit trail.</strong> The files below show what was included, what was intentionally omitted, and where source or market-data gaps remain.</div>")
666
  gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True)
667
  gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True)
668
  gr.File(
space_bundle_manifest.json CHANGED
@@ -8,8 +8,8 @@
8
  "files": [
9
  {
10
  "path": "app.py",
11
- "sha256": "95b3e4f476a8708cbf5892ab0b788b176224373631492978b9bf9f72a5adac20",
12
- "byte_count": 17143
13
  },
14
  {
15
  "path": "data/audit/claim_boundary.json",
@@ -98,8 +98,8 @@
98
  },
99
  {
100
  "path": "README.md",
101
- "sha256": "9fb415862b6c5f0372606d363866e329b4b69aa5f7a4632e4a05461417be0e10",
102
- "byte_count": 535
103
  },
104
  {
105
  "path": "requirements.txt",
@@ -107,5 +107,5 @@
107
  "byte_count": 47
108
  }
109
  ],
110
- "manifest_hash": "0b3c002cb32fe8dfd5f891d084ad18701d28ebb250e71b4f5606f85ce2172660"
111
  }
 
8
  "files": [
9
  {
10
  "path": "app.py",
11
+ "sha256": "8c303e8e6ee0e11d29512e99f663d5454048db85bbe7a016182df47db4756f24",
12
+ "byte_count": 30041
13
  },
14
  {
15
  "path": "data/audit/claim_boundary.json",
 
98
  },
99
  {
100
  "path": "README.md",
101
+ "sha256": "e52e46bb2b6cc3dd1c334a0ce3465e5aa0d8c97329a4e0107afccad082f32156",
102
+ "byte_count": 544
103
  },
104
  {
105
  "path": "requirements.txt",
 
107
  "byte_count": 47
108
  }
109
  ],
110
+ "manifest_hash": "a0663b17e37274c65850f2c9e89b588acb6dac6dc3d88e7f74c2407c4408893c"
111
  }