Add deterministic exports, tooltips, and experimental relative ranking
Browse files- public_space_app.py +416 -79
public_space_app.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
import html
|
| 4 |
import json
|
|
|
|
| 5 |
import os
|
| 6 |
import re
|
|
|
|
|
|
|
| 7 |
import urllib.request
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Any, Dict, Tuple
|
|
@@ -21,6 +25,12 @@ try:
|
|
| 21 |
except ImportError as exc: # pragma: no cover - runtime dependency
|
| 22 |
raise RuntimeError("pyvis is required to run this Space bundle") from exc
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
def _read_json(source: str) -> Dict[str, Any]:
|
| 26 |
if source.startswith("http://") or source.startswith("https://"):
|
|
@@ -246,15 +256,16 @@ def _space_css() -> str:
|
|
| 246 |
margin: 0 auto !important;
|
| 247 |
padding-bottom: 48px !important;
|
| 248 |
}
|
| 249 |
-
.hero-panel {
|
| 250 |
background: linear-gradient(135deg, #161c24 0%, #202733 100%);
|
| 251 |
-
border: 1px solid rgba(212, 162, 74, 0.34);
|
| 252 |
border-radius: 24px;
|
| 253 |
padding: 28px;
|
| 254 |
margin: 6px 0 20px 0;
|
| 255 |
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34);
|
|
|
|
| 256 |
}
|
| 257 |
-
.hero-eyebrow {
|
| 258 |
font-size: 0.82rem;
|
| 259 |
font-weight: 700;
|
| 260 |
letter-spacing: 0.08em;
|
|
@@ -262,21 +273,21 @@ def _space_css() -> str:
|
|
| 262 |
color: #d4a24a;
|
| 263 |
margin-bottom: 8px;
|
| 264 |
}
|
| 265 |
-
.hero-title {
|
| 266 |
font-size: 2.2rem;
|
| 267 |
line-height: 1.1;
|
| 268 |
font-weight: 800;
|
| 269 |
color: #fff4e1;
|
| 270 |
margin: 0 0 12px 0;
|
| 271 |
}
|
| 272 |
-
.hero-lede {
|
| 273 |
font-size: 1.05rem;
|
| 274 |
line-height: 1.6;
|
| 275 |
color: #e2dacd;
|
| 276 |
margin: 0 0 10px 0;
|
| 277 |
max-width: 900px;
|
| 278 |
}
|
| 279 |
-
.hero-note {
|
| 280 |
font-size: 0.98rem;
|
| 281 |
line-height: 1.5;
|
| 282 |
color: #eee4d5;
|
|
@@ -296,24 +307,29 @@ def _space_css() -> str:
|
|
| 296 |
display: inline-block;
|
| 297 |
text-shadow: none !important;
|
| 298 |
}
|
| 299 |
-
.stat-grid, .story-grid {
|
| 300 |
display: grid;
|
| 301 |
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 302 |
gap: 14px;
|
| 303 |
margin-top: 18px;
|
| 304 |
}
|
| 305 |
-
.story-grid {
|
| 306 |
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 307 |
margin: 10px 0 22px 0;
|
| 308 |
}
|
| 309 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
background: #151b22;
|
| 311 |
-
border: 1px solid rgba(212, 162, 74, 0.22);
|
| 312 |
border-radius: 18px;
|
| 313 |
padding: 16px 18px;
|
| 314 |
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
|
|
|
|
| 315 |
}
|
| 316 |
-
.stat-label {
|
| 317 |
font-size: 0.82rem;
|
| 318 |
font-weight: 700;
|
| 319 |
text-transform: uppercase;
|
|
@@ -321,62 +337,64 @@ def _space_css() -> str:
|
|
| 321 |
color: #d4a24a;
|
| 322 |
margin-bottom: 8px;
|
| 323 |
}
|
| 324 |
-
.stat-value {
|
| 325 |
font-size: 1.9rem;
|
| 326 |
font-weight: 800;
|
| 327 |
color: #fff4e1;
|
| 328 |
line-height: 1;
|
| 329 |
margin-bottom: 6px;
|
| 330 |
}
|
| 331 |
-
.stat-help {
|
| 332 |
font-size: 0.92rem;
|
| 333 |
color: #d8cfbf;
|
| 334 |
line-height: 1.45;
|
| 335 |
}
|
| 336 |
-
.story-title, .source-title, .glossary-title {
|
| 337 |
font-size: 1rem;
|
| 338 |
font-weight: 800;
|
| 339 |
color: #fff4e1;
|
| 340 |
margin-bottom: 6px;
|
| 341 |
}
|
| 342 |
-
.story-body, .source-body, .glossary-body {
|
| 343 |
font-size: 0.95rem;
|
| 344 |
line-height: 1.55;
|
| 345 |
color: #ddd5c8;
|
| 346 |
}
|
| 347 |
-
.source-table {
|
| 348 |
width: 100%;
|
| 349 |
border-collapse: collapse;
|
| 350 |
margin-top: 8px;
|
| 351 |
font-size: 0.95rem;
|
|
|
|
| 352 |
}
|
| 353 |
-
.source-table th, .source-table td {
|
| 354 |
border-top: 1px solid rgba(212, 162, 74, 0.16);
|
| 355 |
padding: 12px 10px;
|
| 356 |
text-align: left;
|
| 357 |
vertical-align: top;
|
|
|
|
| 358 |
}
|
| 359 |
-
.source-table th {
|
| 360 |
color: #d4a24a;
|
| 361 |
font-size: 0.82rem;
|
| 362 |
text-transform: uppercase;
|
| 363 |
letter-spacing: 0.06em;
|
| 364 |
width: 32%;
|
| 365 |
}
|
| 366 |
-
.source-table td {
|
| 367 |
color: #ddd5c8;
|
| 368 |
}
|
| 369 |
-
.glossary-list {
|
| 370 |
display: grid;
|
| 371 |
gap: 10px;
|
| 372 |
margin-top: 8px;
|
| 373 |
}
|
| 374 |
-
.glossary-item strong {
|
| 375 |
display: block;
|
| 376 |
color: #fff4e1;
|
| 377 |
margin-bottom: 2px;
|
| 378 |
}
|
| 379 |
-
.section-kicker {
|
| 380 |
color: #d4a24a;
|
| 381 |
font-size: 0.84rem;
|
| 382 |
font-weight: 700;
|
|
@@ -384,19 +402,19 @@ def _space_css() -> str:
|
|
| 384 |
text-transform: uppercase;
|
| 385 |
margin-bottom: 6px;
|
| 386 |
}
|
| 387 |
-
.result-list {
|
| 388 |
display: flex;
|
| 389 |
flex-direction: column;
|
| 390 |
gap: 12px;
|
| 391 |
margin-top: 10px;
|
| 392 |
}
|
| 393 |
-
.result-head {
|
| 394 |
display: flex;
|
| 395 |
justify-content: space-between;
|
| 396 |
align-items: flex-start;
|
| 397 |
gap: 12px;
|
| 398 |
}
|
| 399 |
-
.result-rank {
|
| 400 |
font-size: 0.78rem;
|
| 401 |
font-weight: 700;
|
| 402 |
color: #d4a24a;
|
|
@@ -404,24 +422,24 @@ def _space_css() -> str:
|
|
| 404 |
letter-spacing: 0.06em;
|
| 405 |
margin-bottom: 4px;
|
| 406 |
}
|
| 407 |
-
.result-title {
|
| 408 |
font-size: 1.12rem;
|
| 409 |
font-weight: 800;
|
| 410 |
color: #fff4e1;
|
| 411 |
line-height: 1.2;
|
| 412 |
margin-bottom: 4px;
|
| 413 |
}
|
| 414 |
-
.result-subtitle {
|
| 415 |
color: #d5cbbb;
|
| 416 |
font-size: 0.93rem;
|
| 417 |
}
|
| 418 |
-
.metric-stack {
|
| 419 |
display: flex;
|
| 420 |
gap: 8px;
|
| 421 |
flex-wrap: wrap;
|
| 422 |
justify-content: flex-end;
|
| 423 |
}
|
| 424 |
-
.score-pill, .strength-pill, .chip {
|
| 425 |
display: inline-block;
|
| 426 |
border-radius: 999px;
|
| 427 |
padding: 5px 10px;
|
|
@@ -429,26 +447,26 @@ def _space_css() -> str:
|
|
| 429 |
font-weight: 700;
|
| 430 |
white-space: nowrap;
|
| 431 |
}
|
| 432 |
-
.score-pill {
|
| 433 |
background: #1f5f5b;
|
| 434 |
-
color: white;
|
| 435 |
}
|
| 436 |
-
.strength-pill {
|
| 437 |
background: rgba(212, 162, 74, 0.18);
|
| 438 |
color: #ffd47a;
|
| 439 |
border: 1px solid rgba(212, 162, 74, 0.32);
|
| 440 |
}
|
| 441 |
-
.chip-row {
|
| 442 |
display: flex;
|
| 443 |
flex-wrap: wrap;
|
| 444 |
gap: 8px;
|
| 445 |
margin: 12px 0 10px 0;
|
| 446 |
}
|
| 447 |
-
.chip {
|
| 448 |
background: rgba(255,255,255,0.08);
|
| 449 |
color: #ece3d5;
|
| 450 |
}
|
| 451 |
-
.meta-grid {
|
| 452 |
display: grid;
|
| 453 |
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 454 |
gap: 10px;
|
|
@@ -456,7 +474,7 @@ def _space_css() -> str:
|
|
| 456 |
font-size: 0.9rem;
|
| 457 |
color: #d6cec2;
|
| 458 |
}
|
| 459 |
-
.meta-grid strong {
|
| 460 |
display: block;
|
| 461 |
color: #fff4e1;
|
| 462 |
margin-bottom: 2px;
|
|
@@ -464,19 +482,28 @@ def _space_css() -> str:
|
|
| 464 |
text-transform: uppercase;
|
| 465 |
letter-spacing: 0.04em;
|
| 466 |
}
|
| 467 |
-
.result-hint {
|
| 468 |
margin-top: 12px;
|
| 469 |
font-size: 0.88rem;
|
| 470 |
color: #d4a24a;
|
| 471 |
}
|
| 472 |
-
.panel-note {
|
| 473 |
background: #151b22;
|
| 474 |
-
border: 1px solid rgba(212, 162, 74, 0.22);
|
| 475 |
border-radius: 18px;
|
| 476 |
padding: 14px 16px;
|
| 477 |
color: #ddd5c8;
|
| 478 |
margin-bottom: 12px;
|
| 479 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
.gradio-container .prose,
|
| 481 |
.gradio-container .prose p,
|
| 482 |
.gradio-container .prose li,
|
|
@@ -831,6 +858,44 @@ def _confidence_label(value: str) -> str:
|
|
| 831 |
}.get(normalized, normalized.title() or "Confidence not labeled")
|
| 832 |
|
| 833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
def _edge_evidence_chips(row: Dict[str, Any]) -> list[str]:
|
| 835 |
urls = _split_pipe_values(row.get("source_urls", ""), limit=12)
|
| 836 |
reason_codes = set(_split_pipe_values(row.get("reason_codes", ""), limit=20))
|
|
@@ -871,14 +936,28 @@ def _window_overlap_text(row: Dict[str, Any]) -> str:
|
|
| 871 |
return "not explicit in this row"
|
| 872 |
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
def _relationship_score(row: Dict[str, Any]) -> int:
|
| 875 |
status = str(row.get("relationship_status", "") or "")
|
| 876 |
-
|
| 877 |
-
stronger_support = int(
|
| 878 |
-
row.get("linked_count", 0) or 0
|
| 879 |
-
if family == "recipient"
|
| 880 |
-
else row.get("strong_event_count", 0) or 0
|
| 881 |
-
)
|
| 882 |
status_base = {
|
| 883 |
"linked": 78,
|
| 884 |
"release_ok": 74,
|
|
@@ -894,13 +973,34 @@ def _relationship_score(row: Dict[str, Any]) -> int:
|
|
| 894 |
return max(0, min(100, score))
|
| 895 |
|
| 896 |
|
| 897 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
columns = [
|
| 899 |
"rank",
|
| 900 |
"relationship_id",
|
| 901 |
"member",
|
| 902 |
"counterparty / sector",
|
| 903 |
"overall score",
|
|
|
|
|
|
|
| 904 |
"strength",
|
| 905 |
"evidence",
|
| 906 |
"time-window overlap",
|
|
@@ -912,26 +1012,30 @@ def _rank_relationships(edges: pd.DataFrame) -> pd.DataFrame:
|
|
| 912 |
]
|
| 913 |
if edges.empty:
|
| 914 |
return pd.DataFrame(columns=columns)
|
|
|
|
|
|
|
| 915 |
rows: list[dict[str, Any]] = []
|
| 916 |
for row in edges.to_dict("records"):
|
| 917 |
family = str(row.get("relationship_family", "") or "")
|
| 918 |
-
stronger_support =
|
| 919 |
-
row.get("linked_count", 0) or 0
|
| 920 |
-
if family == "recipient"
|
| 921 |
-
else row.get("strong_event_count", 0) or 0
|
| 922 |
-
)
|
| 923 |
caution_support = int(
|
| 924 |
row.get("review_count", 0) or 0
|
| 925 |
if family == "recipient"
|
| 926 |
else row.get("weak_event_count", 0) or 0
|
| 927 |
)
|
| 928 |
chips = _edge_evidence_chips(row)
|
|
|
|
|
|
|
|
|
|
| 929 |
rows.append(
|
| 930 |
{
|
| 931 |
"relationship_id": str(row.get("edge_id") or ""),
|
| 932 |
"member": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 933 |
"counterparty / sector": str(row.get("target_label") or ""),
|
| 934 |
-
"overall score":
|
|
|
|
|
|
|
|
|
|
| 935 |
"strength": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 936 |
"evidence": " | ".join(chips) if chips else "published source support",
|
| 937 |
"time-window overlap": _window_overlap_text(row),
|
|
@@ -957,6 +1061,7 @@ def _overview_summary_markdown(
|
|
| 957 |
family: str,
|
| 958 |
only_strong_links: bool,
|
| 959 |
top_n: int,
|
|
|
|
| 960 |
) -> str:
|
| 961 |
if ranked.empty:
|
| 962 |
return "\n".join(
|
|
@@ -975,6 +1080,7 @@ def _overview_summary_markdown(
|
|
| 975 |
"",
|
| 976 |
f"- Showing the top `{min(int(top_n), len(ranked))}` `{_plain_family_label(family).lower()}` for `{focus_label}`.",
|
| 977 |
f"- Filtered to stronger links only: `{str(bool(only_strong_links)).lower()}`.",
|
|
|
|
| 978 |
f"- Highest score in this view: `{int(ranked['overall score'].max())}`.",
|
| 979 |
"- Pick one relationship below to see the evidence breakdown and coarse evidence window.",
|
| 980 |
]
|
|
@@ -990,6 +1096,7 @@ def _overview_cards_html(
|
|
| 990 |
family: str,
|
| 991 |
only_strong_links: bool,
|
| 992 |
top_n: int,
|
|
|
|
| 993 |
) -> str:
|
| 994 |
if ranked.empty:
|
| 995 |
return (
|
|
@@ -1005,17 +1112,24 @@ def _overview_cards_html(
|
|
| 1005 |
f"<strong>Showing the top {min(int(top_n), len(ranked))} {_plain_family_label(family).lower()}</strong> "
|
| 1006 |
f"for <strong>{html.escape(focus_label)}</strong>. "
|
| 1007 |
f"Filtered to stronger links only: <strong>{'yes' if bool(only_strong_links) else 'no'}</strong>. "
|
| 1008 |
-
"
|
|
|
|
| 1009 |
"</div>"
|
| 1010 |
)
|
| 1011 |
cards: list[str] = []
|
| 1012 |
for row in ranked.head(int(top_n)).to_dict("records"):
|
| 1013 |
evidence_chips = [item.strip() for item in str(row.get("evidence", "") or "").split("|") if item.strip()]
|
| 1014 |
-
chip_html = "".join(
|
|
|
|
|
|
|
|
|
|
| 1015 |
supporting_rows = int(row.get("supporting rows", 0) or 0)
|
| 1016 |
stronger_support = int(row.get("stronger support", 0) or 0)
|
| 1017 |
needs_caution = int(row.get("needs caution", 0) or 0)
|
| 1018 |
unresolved_refs = int(row.get("unresolved refs", 0) or 0)
|
|
|
|
|
|
|
|
|
|
| 1019 |
cards.append(
|
| 1020 |
f"""
|
| 1021 |
<div class="result-card">
|
|
@@ -1026,8 +1140,8 @@ def _overview_cards_html(
|
|
| 1026 |
<div class="result-subtitle">For {html.escape(str(row.get("member", "") or ""))} in the {_plain_family_label(family).lower()} view.</div>
|
| 1027 |
</div>
|
| 1028 |
<div class="metric-stack">
|
| 1029 |
-
<span class="score-pill">Score {int(row.get("overall score", 0) or 0)}</span>
|
| 1030 |
-
<span class="strength-pill">{html.escape(str(row.get("strength", "") or ""))}</span>
|
| 1031 |
</div>
|
| 1032 |
</div>
|
| 1033 |
<div class="chip-row">{chip_html or '<span class="chip">published source support</span>'}</div>
|
|
@@ -1037,8 +1151,10 @@ def _overview_cards_html(
|
|
| 1037 |
<div><strong>Stronger support</strong>{stronger_support}</div>
|
| 1038 |
<div><strong>Needs caution</strong>{needs_caution}</div>
|
| 1039 |
<div><strong>Unresolved refs</strong>{unresolved_refs}</div>
|
|
|
|
|
|
|
| 1040 |
</div>
|
| 1041 |
-
<div class="result-hint">Use
|
| 1042 |
</div>
|
| 1043 |
"""
|
| 1044 |
)
|
|
@@ -1055,10 +1171,7 @@ def _relationship_options(ranked: pd.DataFrame) -> list[tuple[str, str]]:
|
|
| 1055 |
return []
|
| 1056 |
options: list[tuple[str, str]] = []
|
| 1057 |
for row in ranked.to_dict("records"):
|
| 1058 |
-
label = (
|
| 1059 |
-
f"#{int(row['rank'])} {row['counterparty / sector']} "
|
| 1060 |
-
f"— {row['strength']} (score {row['overall score']})"
|
| 1061 |
-
)
|
| 1062 |
options.append((label, str(row["relationship_id"])))
|
| 1063 |
return options
|
| 1064 |
|
|
@@ -1072,7 +1185,7 @@ def _select_edge_row(edges: pd.DataFrame, relationship_id: str) -> Dict[str, Any
|
|
| 1072 |
return matched.head(1).to_dict("records")[0]
|
| 1073 |
|
| 1074 |
|
| 1075 |
-
def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) -> str:
|
| 1076 |
row = _select_edge_row(edges, relationship_id)
|
| 1077 |
if not row:
|
| 1078 |
return "Select a relationship to inspect why it appears in this released slice."
|
|
@@ -1080,12 +1193,17 @@ def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) ->
|
|
| 1080 |
chips = _edge_evidence_chips(row)
|
| 1081 |
reason_codes = [_plain_reason_code(item) for item in _split_pipe_values(row.get("reason_codes", ""), limit=8)]
|
| 1082 |
urls = _split_pipe_values(row.get("source_urls", ""), limit=5)
|
|
|
|
|
|
|
|
|
|
| 1083 |
lines = [
|
| 1084 |
f"### {row.get('member_name') or row.get('member_slug')} -> {row.get('target_label')}",
|
| 1085 |
"",
|
| 1086 |
f"- Relationship view: `{_plain_family_label(family)}`",
|
| 1087 |
f"- Strength label: `{_plain_status_label(str(row.get('relationship_status', '') or ''))}`",
|
| 1088 |
-
f"-
|
|
|
|
|
|
|
| 1089 |
f"- Supporting relationship rows: `{int(row.get('link_count', 0) or 0)}`",
|
| 1090 |
f"- Stronger-support rows: `{int(row.get('linked_count', 0) or 0) if family == 'recipient' else int(row.get('strong_event_count', 0) or 0)}`",
|
| 1091 |
f"- Caution / weaker rows: `{int(row.get('review_count', 0) or 0) if family == 'recipient' else int(row.get('weak_event_count', 0) or 0)}`",
|
|
@@ -1108,9 +1226,189 @@ def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) ->
|
|
| 1108 |
"- `Integrity-checked` means the release includes a cryptographic fingerprint to help show a published record has not been altered.",
|
| 1109 |
]
|
| 1110 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
return "\n".join(lines)
|
| 1112 |
|
| 1113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
def _timeline_window_from_url(url: str) -> tuple[int, str, str]:
|
| 1115 |
normalized = str(url or "").strip()
|
| 1116 |
if not normalized:
|
|
@@ -1517,12 +1815,20 @@ def build_app(copy_path: str | Path):
|
|
| 1517 |
overview_member_limit,
|
| 1518 |
)
|
| 1519 |
|
| 1520 |
-
def _update_overview(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1521 |
filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
|
| 1522 |
-
ranked = _rank_relationships(filtered_edges)
|
| 1523 |
options = _relationship_options(ranked)
|
| 1524 |
valid_ids = {value for _, value in options}
|
| 1525 |
selected = relationship_id if relationship_id in valid_ids else (options[0][1] if options else None)
|
|
|
|
| 1526 |
return (
|
| 1527 |
_overview_summary_markdown(
|
| 1528 |
ranked,
|
|
@@ -1530,6 +1836,7 @@ def build_app(copy_path: str | Path):
|
|
| 1530 |
family=family,
|
| 1531 |
only_strong_links=only_strong,
|
| 1532 |
top_n=int(top_n),
|
|
|
|
| 1533 |
),
|
| 1534 |
_overview_cards_html(
|
| 1535 |
ranked,
|
|
@@ -1537,15 +1844,33 @@ def build_app(copy_path: str | Path):
|
|
| 1537 |
family=family,
|
| 1538 |
only_strong_links=only_strong,
|
| 1539 |
top_n=int(top_n),
|
|
|
|
| 1540 |
),
|
| 1541 |
gr.update(choices=options, value=selected),
|
| 1542 |
-
_relationship_detail_markdown(filtered_edges, selected or ""),
|
| 1543 |
_relationship_timeline_html(filtered_edges, selected or ""),
|
|
|
|
|
|
|
|
|
|
| 1544 |
)
|
| 1545 |
|
| 1546 |
-
def _update_overview_detail(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1547 |
filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
|
| 1548 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1549 |
|
| 1550 |
def _update_graph(member_query: str, family: str, only_strong: bool, top_n: int):
|
| 1551 |
review_status = "stronger" if only_strong else "all"
|
|
@@ -1660,6 +1985,14 @@ def build_app(copy_path: str | Path):
|
|
| 1660 |
choices=[("Sectors", "sector"), ("Funding recipients", "recipient")],
|
| 1661 |
value="sector",
|
| 1662 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1663 |
overview_only_strong = gr.Checkbox(label="Only stronger links", value=True)
|
| 1664 |
overview_top_n = gr.Dropdown(label="Show top results", choices=[5, 10, 15, 20], value=10)
|
| 1665 |
if example_member_choices:
|
|
@@ -1672,27 +2005,31 @@ def build_app(copy_path: str | Path):
|
|
| 1672 |
with gr.Row():
|
| 1673 |
overview_detail_md = gr.Markdown()
|
| 1674 |
overview_timeline_html = gr.HTML()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1675 |
|
| 1676 |
search_button.click(
|
| 1677 |
_update_overview,
|
| 1678 |
-
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1679 |
-
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1680 |
)
|
| 1681 |
overview_member.submit(
|
| 1682 |
_update_overview,
|
| 1683 |
-
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1684 |
-
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1685 |
)
|
| 1686 |
-
for control in (overview_family, overview_only_strong, overview_top_n):
|
| 1687 |
control.change(
|
| 1688 |
_update_overview,
|
| 1689 |
-
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1690 |
-
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1691 |
)
|
| 1692 |
relationship_choice.change(
|
| 1693 |
_update_overview_detail,
|
| 1694 |
-
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1695 |
-
[overview_detail_md, overview_timeline_html],
|
| 1696 |
)
|
| 1697 |
|
| 1698 |
with gr.Accordion("Explore the network map (optional)", open=False):
|
|
@@ -1789,8 +2126,8 @@ def build_app(copy_path: str | Path):
|
|
| 1789 |
|
| 1790 |
app.load(
|
| 1791 |
_update_overview,
|
| 1792 |
-
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1793 |
-
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1794 |
)
|
| 1795 |
app.load(
|
| 1796 |
_update_graph,
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import csv
|
| 4 |
import html
|
| 5 |
import json
|
| 6 |
+
import math
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
+
import tempfile
|
| 10 |
+
import textwrap
|
| 11 |
import urllib.request
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Any, Dict, Tuple
|
|
|
|
| 25 |
except ImportError as exc: # pragma: no cover - runtime dependency
|
| 26 |
raise RuntimeError("pyvis is required to run this Space bundle") from exc
|
| 27 |
|
| 28 |
+
try:
|
| 29 |
+
from reportlab.lib.pagesizes import LETTER
|
| 30 |
+
from reportlab.pdfgen import canvas
|
| 31 |
+
except ImportError as exc: # pragma: no cover - runtime dependency
|
| 32 |
+
raise RuntimeError("reportlab is required to run relationship evidence exports") from exc
|
| 33 |
+
|
| 34 |
|
| 35 |
def _read_json(source: str) -> Dict[str, Any]:
|
| 36 |
if source.startswith("http://") or source.startswith("https://"):
|
|
|
|
| 256 |
margin: 0 auto !important;
|
| 257 |
padding-bottom: 48px !important;
|
| 258 |
}
|
| 259 |
+
.gradio-container .hero-panel {
|
| 260 |
background: linear-gradient(135deg, #161c24 0%, #202733 100%);
|
| 261 |
+
border: 1px solid rgba(212, 162, 74, 0.34) !important;
|
| 262 |
border-radius: 24px;
|
| 263 |
padding: 28px;
|
| 264 |
margin: 6px 0 20px 0;
|
| 265 |
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34);
|
| 266 |
+
color: #ddd5c8 !important;
|
| 267 |
}
|
| 268 |
+
.gradio-container .hero-eyebrow {
|
| 269 |
font-size: 0.82rem;
|
| 270 |
font-weight: 700;
|
| 271 |
letter-spacing: 0.08em;
|
|
|
|
| 273 |
color: #d4a24a;
|
| 274 |
margin-bottom: 8px;
|
| 275 |
}
|
| 276 |
+
.gradio-container .hero-title {
|
| 277 |
font-size: 2.2rem;
|
| 278 |
line-height: 1.1;
|
| 279 |
font-weight: 800;
|
| 280 |
color: #fff4e1;
|
| 281 |
margin: 0 0 12px 0;
|
| 282 |
}
|
| 283 |
+
.gradio-container .hero-lede {
|
| 284 |
font-size: 1.05rem;
|
| 285 |
line-height: 1.6;
|
| 286 |
color: #e2dacd;
|
| 287 |
margin: 0 0 10px 0;
|
| 288 |
max-width: 900px;
|
| 289 |
}
|
| 290 |
+
.gradio-container .hero-note {
|
| 291 |
font-size: 0.98rem;
|
| 292 |
line-height: 1.5;
|
| 293 |
color: #eee4d5;
|
|
|
|
| 307 |
display: inline-block;
|
| 308 |
text-shadow: none !important;
|
| 309 |
}
|
| 310 |
+
.gradio-container .stat-grid, .gradio-container .story-grid {
|
| 311 |
display: grid;
|
| 312 |
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 313 |
gap: 14px;
|
| 314 |
margin-top: 18px;
|
| 315 |
}
|
| 316 |
+
.gradio-container .story-grid {
|
| 317 |
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 318 |
margin: 10px 0 22px 0;
|
| 319 |
}
|
| 320 |
+
.gradio-container .stat-card,
|
| 321 |
+
.gradio-container .story-card,
|
| 322 |
+
.gradio-container .source-card,
|
| 323 |
+
.gradio-container .glossary-card,
|
| 324 |
+
.gradio-container .result-card {
|
| 325 |
background: #151b22;
|
| 326 |
+
border: 1px solid rgba(212, 162, 74, 0.22) !important;
|
| 327 |
border-radius: 18px;
|
| 328 |
padding: 16px 18px;
|
| 329 |
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
|
| 330 |
+
color: #ddd5c8 !important;
|
| 331 |
}
|
| 332 |
+
.gradio-container .stat-label {
|
| 333 |
font-size: 0.82rem;
|
| 334 |
font-weight: 700;
|
| 335 |
text-transform: uppercase;
|
|
|
|
| 337 |
color: #d4a24a;
|
| 338 |
margin-bottom: 8px;
|
| 339 |
}
|
| 340 |
+
.gradio-container .stat-value {
|
| 341 |
font-size: 1.9rem;
|
| 342 |
font-weight: 800;
|
| 343 |
color: #fff4e1;
|
| 344 |
line-height: 1;
|
| 345 |
margin-bottom: 6px;
|
| 346 |
}
|
| 347 |
+
.gradio-container .stat-help {
|
| 348 |
font-size: 0.92rem;
|
| 349 |
color: #d8cfbf;
|
| 350 |
line-height: 1.45;
|
| 351 |
}
|
| 352 |
+
.gradio-container .story-title, .gradio-container .source-title, .gradio-container .glossary-title {
|
| 353 |
font-size: 1rem;
|
| 354 |
font-weight: 800;
|
| 355 |
color: #fff4e1;
|
| 356 |
margin-bottom: 6px;
|
| 357 |
}
|
| 358 |
+
.gradio-container .story-body, .gradio-container .source-body, .gradio-container .glossary-body {
|
| 359 |
font-size: 0.95rem;
|
| 360 |
line-height: 1.55;
|
| 361 |
color: #ddd5c8;
|
| 362 |
}
|
| 363 |
+
.gradio-container .source-table {
|
| 364 |
width: 100%;
|
| 365 |
border-collapse: collapse;
|
| 366 |
margin-top: 8px;
|
| 367 |
font-size: 0.95rem;
|
| 368 |
+
background: transparent !important;
|
| 369 |
}
|
| 370 |
+
.gradio-container .source-table th, .gradio-container .source-table td {
|
| 371 |
border-top: 1px solid rgba(212, 162, 74, 0.16);
|
| 372 |
padding: 12px 10px;
|
| 373 |
text-align: left;
|
| 374 |
vertical-align: top;
|
| 375 |
+
background: transparent !important;
|
| 376 |
}
|
| 377 |
+
.gradio-container .source-table th {
|
| 378 |
color: #d4a24a;
|
| 379 |
font-size: 0.82rem;
|
| 380 |
text-transform: uppercase;
|
| 381 |
letter-spacing: 0.06em;
|
| 382 |
width: 32%;
|
| 383 |
}
|
| 384 |
+
.gradio-container .source-table td {
|
| 385 |
color: #ddd5c8;
|
| 386 |
}
|
| 387 |
+
.gradio-container .glossary-list {
|
| 388 |
display: grid;
|
| 389 |
gap: 10px;
|
| 390 |
margin-top: 8px;
|
| 391 |
}
|
| 392 |
+
.gradio-container .glossary-item strong {
|
| 393 |
display: block;
|
| 394 |
color: #fff4e1;
|
| 395 |
margin-bottom: 2px;
|
| 396 |
}
|
| 397 |
+
.gradio-container .section-kicker {
|
| 398 |
color: #d4a24a;
|
| 399 |
font-size: 0.84rem;
|
| 400 |
font-weight: 700;
|
|
|
|
| 402 |
text-transform: uppercase;
|
| 403 |
margin-bottom: 6px;
|
| 404 |
}
|
| 405 |
+
.gradio-container .result-list {
|
| 406 |
display: flex;
|
| 407 |
flex-direction: column;
|
| 408 |
gap: 12px;
|
| 409 |
margin-top: 10px;
|
| 410 |
}
|
| 411 |
+
.gradio-container .result-head {
|
| 412 |
display: flex;
|
| 413 |
justify-content: space-between;
|
| 414 |
align-items: flex-start;
|
| 415 |
gap: 12px;
|
| 416 |
}
|
| 417 |
+
.gradio-container .result-rank {
|
| 418 |
font-size: 0.78rem;
|
| 419 |
font-weight: 700;
|
| 420 |
color: #d4a24a;
|
|
|
|
| 422 |
letter-spacing: 0.06em;
|
| 423 |
margin-bottom: 4px;
|
| 424 |
}
|
| 425 |
+
.gradio-container .result-title {
|
| 426 |
font-size: 1.12rem;
|
| 427 |
font-weight: 800;
|
| 428 |
color: #fff4e1;
|
| 429 |
line-height: 1.2;
|
| 430 |
margin-bottom: 4px;
|
| 431 |
}
|
| 432 |
+
.gradio-container .result-subtitle {
|
| 433 |
color: #d5cbbb;
|
| 434 |
font-size: 0.93rem;
|
| 435 |
}
|
| 436 |
+
.gradio-container .metric-stack {
|
| 437 |
display: flex;
|
| 438 |
gap: 8px;
|
| 439 |
flex-wrap: wrap;
|
| 440 |
justify-content: flex-end;
|
| 441 |
}
|
| 442 |
+
.gradio-container .score-pill, .gradio-container .strength-pill, .gradio-container .chip {
|
| 443 |
display: inline-block;
|
| 444 |
border-radius: 999px;
|
| 445 |
padding: 5px 10px;
|
|
|
|
| 447 |
font-weight: 700;
|
| 448 |
white-space: nowrap;
|
| 449 |
}
|
| 450 |
+
.gradio-container .score-pill {
|
| 451 |
background: #1f5f5b;
|
| 452 |
+
color: white !important;
|
| 453 |
}
|
| 454 |
+
.gradio-container .strength-pill {
|
| 455 |
background: rgba(212, 162, 74, 0.18);
|
| 456 |
color: #ffd47a;
|
| 457 |
border: 1px solid rgba(212, 162, 74, 0.32);
|
| 458 |
}
|
| 459 |
+
.gradio-container .chip-row {
|
| 460 |
display: flex;
|
| 461 |
flex-wrap: wrap;
|
| 462 |
gap: 8px;
|
| 463 |
margin: 12px 0 10px 0;
|
| 464 |
}
|
| 465 |
+
.gradio-container .chip {
|
| 466 |
background: rgba(255,255,255,0.08);
|
| 467 |
color: #ece3d5;
|
| 468 |
}
|
| 469 |
+
.gradio-container .meta-grid {
|
| 470 |
display: grid;
|
| 471 |
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 472 |
gap: 10px;
|
|
|
|
| 474 |
font-size: 0.9rem;
|
| 475 |
color: #d6cec2;
|
| 476 |
}
|
| 477 |
+
.gradio-container .meta-grid strong {
|
| 478 |
display: block;
|
| 479 |
color: #fff4e1;
|
| 480 |
margin-bottom: 2px;
|
|
|
|
| 482 |
text-transform: uppercase;
|
| 483 |
letter-spacing: 0.04em;
|
| 484 |
}
|
| 485 |
+
.gradio-container .result-hint {
|
| 486 |
margin-top: 12px;
|
| 487 |
font-size: 0.88rem;
|
| 488 |
color: #d4a24a;
|
| 489 |
}
|
| 490 |
+
.gradio-container .panel-note {
|
| 491 |
background: #151b22;
|
| 492 |
+
border: 1px solid rgba(212, 162, 74, 0.22) !important;
|
| 493 |
border-radius: 18px;
|
| 494 |
padding: 14px 16px;
|
| 495 |
color: #ddd5c8;
|
| 496 |
margin-bottom: 12px;
|
| 497 |
}
|
| 498 |
+
.gradio-container .hero-panel *,
|
| 499 |
+
.gradio-container .stat-card *,
|
| 500 |
+
.gradio-container .story-card *,
|
| 501 |
+
.gradio-container .source-card *,
|
| 502 |
+
.gradio-container .glossary-card *,
|
| 503 |
+
.gradio-container .result-card *,
|
| 504 |
+
.gradio-container .panel-note * {
|
| 505 |
+
text-shadow: none !important;
|
| 506 |
+
}
|
| 507 |
.gradio-container .prose,
|
| 508 |
.gradio-container .prose p,
|
| 509 |
.gradio-container .prose li,
|
|
|
|
| 858 |
}.get(normalized, normalized.title() or "Confidence not labeled")
|
| 859 |
|
| 860 |
|
| 861 |
+
def _evidence_chip_help(label: str) -> str:
|
| 862 |
+
normalized = str(label or "").strip().lower()
|
| 863 |
+
mapping = {
|
| 864 |
+
"trade disclosure": "Public periodic transaction reports or trade disclosures support this relationship.",
|
| 865 |
+
"annual disclosure": "Annual financial disclosure records support this relationship.",
|
| 866 |
+
"bill record": "Bill-status records help show legislative activity in the same topic area.",
|
| 867 |
+
"funding award": "Published federal award records support a funding-recipient link in this slice.",
|
| 868 |
+
"committee roster": "Committee records show committee context related to the same topic area.",
|
| 869 |
+
"vote activity": "Roll-call vote records add legislative activity in the same topic window.",
|
| 870 |
+
"lobbying activity": "Lobbying filings add public activity in the same issue area.",
|
| 871 |
+
"member profile": "Member-published profile or committee context contributes to this relationship summary.",
|
| 872 |
+
"published source support": "This relationship has published source support in the released slice.",
|
| 873 |
+
}
|
| 874 |
+
return mapping.get(normalized, "This chip names one kind of public-record support attached to this relationship.")
|
| 875 |
+
|
| 876 |
+
|
| 877 |
+
def _score_help_text(ranking_mode: str) -> str:
|
| 878 |
+
normalized = str(ranking_mode or "raw").strip().lower()
|
| 879 |
+
if normalized == "relative":
|
| 880 |
+
return (
|
| 881 |
+
"Experimental relative score. It compares this relationship with the same member's other visible "
|
| 882 |
+
"relationships in the current view so unusually strong links stand out against that member's baseline activity."
|
| 883 |
+
)
|
| 884 |
+
return (
|
| 885 |
+
"Raw score. It favors clearer public support, more supporting rows, more integrity-checked records, "
|
| 886 |
+
"and fewer unresolved references."
|
| 887 |
+
)
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
def _stronger_support_count(row: Dict[str, Any]) -> int:
|
| 891 |
+
family = str(row.get("relationship_family", "") or "")
|
| 892 |
+
return int(
|
| 893 |
+
row.get("linked_count", 0) or 0
|
| 894 |
+
if family == "recipient"
|
| 895 |
+
else row.get("strong_event_count", 0) or 0
|
| 896 |
+
)
|
| 897 |
+
|
| 898 |
+
|
| 899 |
def _edge_evidence_chips(row: Dict[str, Any]) -> list[str]:
|
| 900 |
urls = _split_pipe_values(row.get("source_urls", ""), limit=12)
|
| 901 |
reason_codes = set(_split_pipe_values(row.get("reason_codes", ""), limit=20))
|
|
|
|
| 936 |
return "not explicit in this row"
|
| 937 |
|
| 938 |
|
| 939 |
+
def _member_activity_baselines(edges: pd.DataFrame) -> Dict[str, Dict[str, float]]:
|
| 940 |
+
if edges.empty:
|
| 941 |
+
return {}
|
| 942 |
+
baselines: Dict[str, Dict[str, float]] = {}
|
| 943 |
+
for member_slug, group in edges.groupby("member_slug", dropna=False):
|
| 944 |
+
slug = str(member_slug or "")
|
| 945 |
+
records = group.to_dict("records")
|
| 946 |
+
raw_scores = [_relationship_score(row) for row in records]
|
| 947 |
+
stronger_counts = [_stronger_support_count(row) for row in records]
|
| 948 |
+
support_counts = [int(row.get("link_count", 0) or 0) for row in records]
|
| 949 |
+
count = max(len(records), 1)
|
| 950 |
+
baselines[slug] = {
|
| 951 |
+
"mean_raw_score": float(sum(raw_scores) / count),
|
| 952 |
+
"mean_stronger_support": float(sum(stronger_counts) / count),
|
| 953 |
+
"mean_support_count": float(sum(support_counts) / count),
|
| 954 |
+
}
|
| 955 |
+
return baselines
|
| 956 |
+
|
| 957 |
+
|
| 958 |
def _relationship_score(row: Dict[str, Any]) -> int:
|
| 959 |
status = str(row.get("relationship_status", "") or "")
|
| 960 |
+
stronger_support = _stronger_support_count(row)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 961 |
status_base = {
|
| 962 |
"linked": 78,
|
| 963 |
"release_ok": 74,
|
|
|
|
| 973 |
return max(0, min(100, score))
|
| 974 |
|
| 975 |
|
| 976 |
+
def _relative_relationship_score(row: Dict[str, Any], baselines: Dict[str, Dict[str, float]]) -> int:
|
| 977 |
+
member_slug = str(row.get("member_slug") or "")
|
| 978 |
+
baseline = baselines.get(member_slug) or {}
|
| 979 |
+
raw_score = _relationship_score(row)
|
| 980 |
+
mean_raw_score = float(baseline.get("mean_raw_score", raw_score) or raw_score)
|
| 981 |
+
mean_stronger_support = float(
|
| 982 |
+
baseline.get("mean_stronger_support", _stronger_support_count(row)) or _stronger_support_count(row)
|
| 983 |
+
)
|
| 984 |
+
mean_support_count = float(
|
| 985 |
+
baseline.get("mean_support_count", int(row.get("link_count", 0) or 0)) or int(row.get("link_count", 0) or 0)
|
| 986 |
+
)
|
| 987 |
+
relative = 50.0
|
| 988 |
+
relative += (raw_score - mean_raw_score) * 1.2
|
| 989 |
+
relative += (_stronger_support_count(row) - mean_stronger_support) * 5.0
|
| 990 |
+
relative += (int(row.get("link_count", 0) or 0) - mean_support_count) * 2.0
|
| 991 |
+
relative -= min(int(row.get("unresolved_source_ref_count", 0) or 0), 10) * 1.2
|
| 992 |
+
return max(0, min(100, int(round(relative))))
|
| 993 |
+
|
| 994 |
+
|
| 995 |
+
def _rank_relationships(edges: pd.DataFrame, ranking_mode: str = "raw") -> pd.DataFrame:
|
| 996 |
columns = [
|
| 997 |
"rank",
|
| 998 |
"relationship_id",
|
| 999 |
"member",
|
| 1000 |
"counterparty / sector",
|
| 1001 |
"overall score",
|
| 1002 |
+
"raw score",
|
| 1003 |
+
"relative score",
|
| 1004 |
"strength",
|
| 1005 |
"evidence",
|
| 1006 |
"time-window overlap",
|
|
|
|
| 1012 |
]
|
| 1013 |
if edges.empty:
|
| 1014 |
return pd.DataFrame(columns=columns)
|
| 1015 |
+
baselines = _member_activity_baselines(edges)
|
| 1016 |
+
normalized_mode = str(ranking_mode or "raw").strip().lower()
|
| 1017 |
rows: list[dict[str, Any]] = []
|
| 1018 |
for row in edges.to_dict("records"):
|
| 1019 |
family = str(row.get("relationship_family", "") or "")
|
| 1020 |
+
stronger_support = _stronger_support_count(row)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
caution_support = int(
|
| 1022 |
row.get("review_count", 0) or 0
|
| 1023 |
if family == "recipient"
|
| 1024 |
else row.get("weak_event_count", 0) or 0
|
| 1025 |
)
|
| 1026 |
chips = _edge_evidence_chips(row)
|
| 1027 |
+
raw_score = _relationship_score(row)
|
| 1028 |
+
relative_score = _relative_relationship_score(row, baselines)
|
| 1029 |
+
display_score = relative_score if normalized_mode == "relative" else raw_score
|
| 1030 |
rows.append(
|
| 1031 |
{
|
| 1032 |
"relationship_id": str(row.get("edge_id") or ""),
|
| 1033 |
"member": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 1034 |
"counterparty / sector": str(row.get("target_label") or ""),
|
| 1035 |
+
"overall score": display_score,
|
| 1036 |
+
"raw score": raw_score,
|
| 1037 |
+
"relative score": relative_score,
|
| 1038 |
+
"status_code": str(row.get("relationship_status", "") or ""),
|
| 1039 |
"strength": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 1040 |
"evidence": " | ".join(chips) if chips else "published source support",
|
| 1041 |
"time-window overlap": _window_overlap_text(row),
|
|
|
|
| 1061 |
family: str,
|
| 1062 |
only_strong_links: bool,
|
| 1063 |
top_n: int,
|
| 1064 |
+
ranking_mode: str,
|
| 1065 |
) -> str:
|
| 1066 |
if ranked.empty:
|
| 1067 |
return "\n".join(
|
|
|
|
| 1080 |
"",
|
| 1081 |
f"- Showing the top `{min(int(top_n), len(ranked))}` `{_plain_family_label(family).lower()}` for `{focus_label}`.",
|
| 1082 |
f"- Filtered to stronger links only: `{str(bool(only_strong_links)).lower()}`.",
|
| 1083 |
+
f"- Ranking mode: `{'experimental relative to this member baseline' if str(ranking_mode or 'raw').strip().lower() == 'relative' else 'raw score'}`.",
|
| 1084 |
f"- Highest score in this view: `{int(ranked['overall score'].max())}`.",
|
| 1085 |
"- Pick one relationship below to see the evidence breakdown and coarse evidence window.",
|
| 1086 |
]
|
|
|
|
| 1096 |
family: str,
|
| 1097 |
only_strong_links: bool,
|
| 1098 |
top_n: int,
|
| 1099 |
+
ranking_mode: str,
|
| 1100 |
) -> str:
|
| 1101 |
if ranked.empty:
|
| 1102 |
return (
|
|
|
|
| 1112 |
f"<strong>Showing the top {min(int(top_n), len(ranked))} {_plain_family_label(family).lower()}</strong> "
|
| 1113 |
f"for <strong>{html.escape(focus_label)}</strong>. "
|
| 1114 |
f"Filtered to stronger links only: <strong>{'yes' if bool(only_strong_links) else 'no'}</strong>. "
|
| 1115 |
+
f"Ranking mode: <strong>{'experimental relative to this member baseline' if str(ranking_mode or 'raw').strip().lower() == 'relative' else 'raw score'}</strong>. "
|
| 1116 |
+
"Hover over score badges and evidence chips for why they matter. Pick one relationship below to open the plain-English explanation and evidence window."
|
| 1117 |
"</div>"
|
| 1118 |
)
|
| 1119 |
cards: list[str] = []
|
| 1120 |
for row in ranked.head(int(top_n)).to_dict("records"):
|
| 1121 |
evidence_chips = [item.strip() for item in str(row.get("evidence", "") or "").split("|") if item.strip()]
|
| 1122 |
+
chip_html = "".join(
|
| 1123 |
+
f"<span class=\"chip\" title=\"{html.escape(_evidence_chip_help(chip))}\">{html.escape(chip)}</span>"
|
| 1124 |
+
for chip in evidence_chips[:6]
|
| 1125 |
+
)
|
| 1126 |
supporting_rows = int(row.get("supporting rows", 0) or 0)
|
| 1127 |
stronger_support = int(row.get("stronger support", 0) or 0)
|
| 1128 |
needs_caution = int(row.get("needs caution", 0) or 0)
|
| 1129 |
unresolved_refs = int(row.get("unresolved refs", 0) or 0)
|
| 1130 |
+
raw_score = int(row.get("raw score", 0) or 0)
|
| 1131 |
+
relative_score = int(row.get("relative score", 0) or 0)
|
| 1132 |
+
score_note = _score_help_text(ranking_mode)
|
| 1133 |
cards.append(
|
| 1134 |
f"""
|
| 1135 |
<div class="result-card">
|
|
|
|
| 1140 |
<div class="result-subtitle">For {html.escape(str(row.get("member", "") or ""))} in the {_plain_family_label(family).lower()} view.</div>
|
| 1141 |
</div>
|
| 1142 |
<div class="metric-stack">
|
| 1143 |
+
<span class="score-pill" title="{html.escape(score_note)}">Score {int(row.get("overall score", 0) or 0)}</span>
|
| 1144 |
+
<span class="strength-pill" title="{html.escape(_plain_status_explainer(str(row.get('status_code', '') or '')))}">{html.escape(str(row.get("strength", "") or ""))}</span>
|
| 1145 |
</div>
|
| 1146 |
</div>
|
| 1147 |
<div class="chip-row">{chip_html or '<span class="chip">published source support</span>'}</div>
|
|
|
|
| 1151 |
<div><strong>Stronger support</strong>{stronger_support}</div>
|
| 1152 |
<div><strong>Needs caution</strong>{needs_caution}</div>
|
| 1153 |
<div><strong>Unresolved refs</strong>{unresolved_refs}</div>
|
| 1154 |
+
<div><strong>Raw score</strong>{raw_score}</div>
|
| 1155 |
+
<div><strong>Relative score</strong>{relative_score}</div>
|
| 1156 |
</div>
|
| 1157 |
+
<div class="result-hint">Use Explain this link below to open the detailed breakdown and export files for this relationship.</div>
|
| 1158 |
</div>
|
| 1159 |
"""
|
| 1160 |
)
|
|
|
|
| 1171 |
return []
|
| 1172 |
options: list[tuple[str, str]] = []
|
| 1173 |
for row in ranked.to_dict("records"):
|
| 1174 |
+
label = f"#{int(row['rank'])} {row['counterparty / sector']} - {row['strength']} (score {row['overall score']})"
|
|
|
|
|
|
|
|
|
|
| 1175 |
options.append((label, str(row["relationship_id"])))
|
| 1176 |
return options
|
| 1177 |
|
|
|
|
| 1185 |
return matched.head(1).to_dict("records")[0]
|
| 1186 |
|
| 1187 |
|
| 1188 |
+
def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str, ranking_mode: str = "raw") -> str:
|
| 1189 |
row = _select_edge_row(edges, relationship_id)
|
| 1190 |
if not row:
|
| 1191 |
return "Select a relationship to inspect why it appears in this released slice."
|
|
|
|
| 1193 |
chips = _edge_evidence_chips(row)
|
| 1194 |
reason_codes = [_plain_reason_code(item) for item in _split_pipe_values(row.get("reason_codes", ""), limit=8)]
|
| 1195 |
urls = _split_pipe_values(row.get("source_urls", ""), limit=5)
|
| 1196 |
+
raw_score = _relationship_score(row)
|
| 1197 |
+
relative_score = _relative_relationship_score(row, _member_activity_baselines(edges))
|
| 1198 |
+
display_score = relative_score if str(ranking_mode or "raw").strip().lower() == "relative" else raw_score
|
| 1199 |
lines = [
|
| 1200 |
f"### {row.get('member_name') or row.get('member_slug')} -> {row.get('target_label')}",
|
| 1201 |
"",
|
| 1202 |
f"- Relationship view: `{_plain_family_label(family)}`",
|
| 1203 |
f"- Strength label: `{_plain_status_label(str(row.get('relationship_status', '') or ''))}`",
|
| 1204 |
+
f"- Displayed score in this view: `{display_score}`",
|
| 1205 |
+
f"- Raw score: `{raw_score}`",
|
| 1206 |
+
f"- Relative-to-baseline score (experimental): `{relative_score}`",
|
| 1207 |
f"- Supporting relationship rows: `{int(row.get('link_count', 0) or 0)}`",
|
| 1208 |
f"- Stronger-support rows: `{int(row.get('linked_count', 0) or 0) if family == 'recipient' else int(row.get('strong_event_count', 0) or 0)}`",
|
| 1209 |
f"- Caution / weaker rows: `{int(row.get('review_count', 0) or 0) if family == 'recipient' else int(row.get('weak_event_count', 0) or 0)}`",
|
|
|
|
| 1226 |
"- `Integrity-checked` means the release includes a cryptographic fingerprint to help show a published record has not been altered.",
|
| 1227 |
]
|
| 1228 |
)
|
| 1229 |
+
if str(ranking_mode or "raw").strip().lower() == "relative":
|
| 1230 |
+
lines.extend(
|
| 1231 |
+
[
|
| 1232 |
+
"",
|
| 1233 |
+
"#### Ranking note",
|
| 1234 |
+
"",
|
| 1235 |
+
"- This view is using the experimental relative score, which compares this relationship to the same member's other visible links in the current filtered view.",
|
| 1236 |
+
]
|
| 1237 |
+
)
|
| 1238 |
return "\n".join(lines)
|
| 1239 |
|
| 1240 |
|
| 1241 |
+
def _safe_export_stem(value: str) -> str:
|
| 1242 |
+
slug = re.sub(r"[^a-z0-9]+", "-", str(value or "").strip().lower()).strip("-")
|
| 1243 |
+
return slug or "relationship-export"
|
| 1244 |
+
|
| 1245 |
+
|
| 1246 |
+
def _relationship_export_rows(edges: pd.DataFrame, relationship_id: str, ranking_mode: str) -> list[dict[str, Any]]:
|
| 1247 |
+
row = _select_edge_row(edges, relationship_id)
|
| 1248 |
+
if not row:
|
| 1249 |
+
return []
|
| 1250 |
+
raw_score = _relationship_score(row)
|
| 1251 |
+
relative_score = _relative_relationship_score(row, _member_activity_baselines(edges))
|
| 1252 |
+
display_score = relative_score if str(ranking_mode or "raw").strip().lower() == "relative" else raw_score
|
| 1253 |
+
reason_codes = [_plain_reason_code(item) for item in _split_pipe_values(row.get("reason_codes", ""), limit=8)]
|
| 1254 |
+
urls = _split_pipe_values(row.get("source_urls", ""), limit=8)
|
| 1255 |
+
export_rows: list[dict[str, Any]] = [
|
| 1256 |
+
{
|
| 1257 |
+
"relationship_id": str(row.get("edge_id") or ""),
|
| 1258 |
+
"member_name": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 1259 |
+
"target_label": str(row.get("target_label") or ""),
|
| 1260 |
+
"relationship_family": _plain_family_label(str(row.get("relationship_family", "") or "")),
|
| 1261 |
+
"strength_label": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 1262 |
+
"ranking_mode": str(ranking_mode or "raw"),
|
| 1263 |
+
"displayed_score": display_score,
|
| 1264 |
+
"raw_score": raw_score,
|
| 1265 |
+
"relative_score": relative_score,
|
| 1266 |
+
"item_type": "summary",
|
| 1267 |
+
"item_label": "relationship summary",
|
| 1268 |
+
"item_detail": "Top-level relationship summary for export.",
|
| 1269 |
+
}
|
| 1270 |
+
]
|
| 1271 |
+
for chip in _edge_evidence_chips(row):
|
| 1272 |
+
export_rows.append(
|
| 1273 |
+
{
|
| 1274 |
+
"relationship_id": str(row.get("edge_id") or ""),
|
| 1275 |
+
"member_name": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 1276 |
+
"target_label": str(row.get("target_label") or ""),
|
| 1277 |
+
"relationship_family": _plain_family_label(str(row.get("relationship_family", "") or "")),
|
| 1278 |
+
"strength_label": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 1279 |
+
"ranking_mode": str(ranking_mode or "raw"),
|
| 1280 |
+
"displayed_score": display_score,
|
| 1281 |
+
"raw_score": raw_score,
|
| 1282 |
+
"relative_score": relative_score,
|
| 1283 |
+
"item_type": "evidence_chip",
|
| 1284 |
+
"item_label": chip,
|
| 1285 |
+
"item_detail": _evidence_chip_help(chip),
|
| 1286 |
+
}
|
| 1287 |
+
)
|
| 1288 |
+
for reason in sorted(reason_codes):
|
| 1289 |
+
export_rows.append(
|
| 1290 |
+
{
|
| 1291 |
+
"relationship_id": str(row.get("edge_id") or ""),
|
| 1292 |
+
"member_name": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 1293 |
+
"target_label": str(row.get("target_label") or ""),
|
| 1294 |
+
"relationship_family": _plain_family_label(str(row.get("relationship_family", "") or "")),
|
| 1295 |
+
"strength_label": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 1296 |
+
"ranking_mode": str(ranking_mode or "raw"),
|
| 1297 |
+
"displayed_score": display_score,
|
| 1298 |
+
"raw_score": raw_score,
|
| 1299 |
+
"relative_score": relative_score,
|
| 1300 |
+
"item_type": "reason",
|
| 1301 |
+
"item_label": reason,
|
| 1302 |
+
"item_detail": reason,
|
| 1303 |
+
}
|
| 1304 |
+
)
|
| 1305 |
+
for item in sorted(_plain_strengthener(value) for value in _split_pipe_values(row.get("missing_to_strengthen", ""), limit=12)):
|
| 1306 |
+
export_rows.append(
|
| 1307 |
+
{
|
| 1308 |
+
"relationship_id": str(row.get("edge_id") or ""),
|
| 1309 |
+
"member_name": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 1310 |
+
"target_label": str(row.get("target_label") or ""),
|
| 1311 |
+
"relationship_family": _plain_family_label(str(row.get("relationship_family", "") or "")),
|
| 1312 |
+
"strength_label": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 1313 |
+
"ranking_mode": str(ranking_mode or "raw"),
|
| 1314 |
+
"displayed_score": display_score,
|
| 1315 |
+
"raw_score": raw_score,
|
| 1316 |
+
"relative_score": relative_score,
|
| 1317 |
+
"item_type": "what_would_strengthen",
|
| 1318 |
+
"item_label": "What would strengthen it",
|
| 1319 |
+
"item_detail": item,
|
| 1320 |
+
}
|
| 1321 |
+
)
|
| 1322 |
+
for url in sorted(urls):
|
| 1323 |
+
export_rows.append(
|
| 1324 |
+
{
|
| 1325 |
+
"relationship_id": str(row.get("edge_id") or ""),
|
| 1326 |
+
"member_name": str(row.get("member_name") or row.get("member_slug") or ""),
|
| 1327 |
+
"target_label": str(row.get("target_label") or ""),
|
| 1328 |
+
"relationship_family": _plain_family_label(str(row.get("relationship_family", "") or "")),
|
| 1329 |
+
"strength_label": _plain_status_label(str(row.get("relationship_status", "") or "")),
|
| 1330 |
+
"ranking_mode": str(ranking_mode or "raw"),
|
| 1331 |
+
"displayed_score": display_score,
|
| 1332 |
+
"raw_score": raw_score,
|
| 1333 |
+
"relative_score": relative_score,
|
| 1334 |
+
"item_type": "source_url",
|
| 1335 |
+
"item_label": "Published source URL",
|
| 1336 |
+
"item_detail": url,
|
| 1337 |
+
}
|
| 1338 |
+
)
|
| 1339 |
+
return export_rows
|
| 1340 |
+
|
| 1341 |
+
|
| 1342 |
+
def _write_relationship_export_bundle(edges: pd.DataFrame, relationship_id: str, ranking_mode: str) -> tuple[str, str | None, str | None]:
|
| 1343 |
+
export_rows = _relationship_export_rows(edges, relationship_id, ranking_mode)
|
| 1344 |
+
if not export_rows:
|
| 1345 |
+
return "Pick one relationship to generate exportable evidence files.", None, None
|
| 1346 |
+
relationship_id_value = str(export_rows[0]["relationship_id"] or relationship_id)
|
| 1347 |
+
export_dir = Path(tempfile.gettempdir()) / "cmp_space_exports"
|
| 1348 |
+
export_dir.mkdir(parents=True, exist_ok=True)
|
| 1349 |
+
stem = _safe_export_stem(f"{relationship_id_value}-{ranking_mode}")
|
| 1350 |
+
csv_path = export_dir / f"{stem}.csv"
|
| 1351 |
+
pdf_path = export_dir / f"{stem}.pdf"
|
| 1352 |
+
|
| 1353 |
+
fieldnames = [
|
| 1354 |
+
"relationship_id",
|
| 1355 |
+
"member_name",
|
| 1356 |
+
"target_label",
|
| 1357 |
+
"relationship_family",
|
| 1358 |
+
"strength_label",
|
| 1359 |
+
"ranking_mode",
|
| 1360 |
+
"displayed_score",
|
| 1361 |
+
"raw_score",
|
| 1362 |
+
"relative_score",
|
| 1363 |
+
"item_type",
|
| 1364 |
+
"item_label",
|
| 1365 |
+
"item_detail",
|
| 1366 |
+
]
|
| 1367 |
+
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
| 1368 |
+
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
| 1369 |
+
writer.writeheader()
|
| 1370 |
+
for export_row in export_rows:
|
| 1371 |
+
writer.writerow({name: export_row.get(name, "") for name in fieldnames})
|
| 1372 |
+
|
| 1373 |
+
title = f"{export_rows[0]['member_name']} -> {export_rows[0]['target_label']}"
|
| 1374 |
+
pdf = canvas.Canvas(str(pdf_path), pagesize=LETTER, invariant=1)
|
| 1375 |
+
width, height = LETTER
|
| 1376 |
+
left = 54
|
| 1377 |
+
top = height - 54
|
| 1378 |
+
pdf.setTitle("Congress public records relationship export")
|
| 1379 |
+
pdf.setAuthor("Congress Public Records Slice")
|
| 1380 |
+
pdf.setSubject("Deterministic relationship evidence export")
|
| 1381 |
+
pdf.setFont("Helvetica-Bold", 14)
|
| 1382 |
+
pdf.drawString(left, top, title[:95])
|
| 1383 |
+
cursor_y = top - 24
|
| 1384 |
+
pdf.setFont("Helvetica", 10)
|
| 1385 |
+
wrapped_lines: list[str] = [
|
| 1386 |
+
f"Strength label: {export_rows[0]['strength_label']}",
|
| 1387 |
+
f"Ranking mode: {export_rows[0]['ranking_mode']}",
|
| 1388 |
+
f"Displayed score: {export_rows[0]['displayed_score']}",
|
| 1389 |
+
f"Raw score: {export_rows[0]['raw_score']}",
|
| 1390 |
+
f"Relative score: {export_rows[0]['relative_score']}",
|
| 1391 |
+
"",
|
| 1392 |
+
"Export rows included below in deterministic order:",
|
| 1393 |
+
]
|
| 1394 |
+
for export_row in export_rows:
|
| 1395 |
+
wrapped_lines.append(f"[{export_row['item_type']}] {export_row['item_label']}: {export_row['item_detail']}")
|
| 1396 |
+
for line in wrapped_lines:
|
| 1397 |
+
for wrapped in textwrap.wrap(str(line), width=98) or [""]:
|
| 1398 |
+
if cursor_y < 54:
|
| 1399 |
+
pdf.showPage()
|
| 1400 |
+
cursor_y = height - 54
|
| 1401 |
+
pdf.setFont("Helvetica", 10)
|
| 1402 |
+
pdf.drawString(left, cursor_y, wrapped)
|
| 1403 |
+
cursor_y -= 14
|
| 1404 |
+
pdf.save()
|
| 1405 |
+
note = (
|
| 1406 |
+
f"Prepared deterministic export files for `{relationship_id_value}`. "
|
| 1407 |
+
"The CSV keeps one row per exported evidence item, and the PDF mirrors the same content in a fixed order."
|
| 1408 |
+
)
|
| 1409 |
+
return note, str(csv_path), str(pdf_path)
|
| 1410 |
+
|
| 1411 |
+
|
| 1412 |
def _timeline_window_from_url(url: str) -> tuple[int, str, str]:
|
| 1413 |
normalized = str(url or "").strip()
|
| 1414 |
if not normalized:
|
|
|
|
| 1815 |
overview_member_limit,
|
| 1816 |
)
|
| 1817 |
|
| 1818 |
+
def _update_overview(
|
| 1819 |
+
member_query: str,
|
| 1820 |
+
family: str,
|
| 1821 |
+
only_strong: bool,
|
| 1822 |
+
top_n: int,
|
| 1823 |
+
ranking_mode: str,
|
| 1824 |
+
relationship_id: str | None = None,
|
| 1825 |
+
):
|
| 1826 |
filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
|
| 1827 |
+
ranked = _rank_relationships(filtered_edges, ranking_mode=ranking_mode)
|
| 1828 |
options = _relationship_options(ranked)
|
| 1829 |
valid_ids = {value for _, value in options}
|
| 1830 |
selected = relationship_id if relationship_id in valid_ids else (options[0][1] if options else None)
|
| 1831 |
+
export_note, export_csv, export_pdf = _write_relationship_export_bundle(filtered_edges, selected or "", ranking_mode)
|
| 1832 |
return (
|
| 1833 |
_overview_summary_markdown(
|
| 1834 |
ranked,
|
|
|
|
| 1836 |
family=family,
|
| 1837 |
only_strong_links=only_strong,
|
| 1838 |
top_n=int(top_n),
|
| 1839 |
+
ranking_mode=ranking_mode,
|
| 1840 |
),
|
| 1841 |
_overview_cards_html(
|
| 1842 |
ranked,
|
|
|
|
| 1844 |
family=family,
|
| 1845 |
only_strong_links=only_strong,
|
| 1846 |
top_n=int(top_n),
|
| 1847 |
+
ranking_mode=ranking_mode,
|
| 1848 |
),
|
| 1849 |
gr.update(choices=options, value=selected),
|
| 1850 |
+
_relationship_detail_markdown(filtered_edges, selected or "", ranking_mode),
|
| 1851 |
_relationship_timeline_html(filtered_edges, selected or ""),
|
| 1852 |
+
export_note,
|
| 1853 |
+
export_csv,
|
| 1854 |
+
export_pdf,
|
| 1855 |
)
|
| 1856 |
|
| 1857 |
+
def _update_overview_detail(
|
| 1858 |
+
member_query: str,
|
| 1859 |
+
family: str,
|
| 1860 |
+
only_strong: bool,
|
| 1861 |
+
top_n: int,
|
| 1862 |
+
ranking_mode: str,
|
| 1863 |
+
relationship_id: str,
|
| 1864 |
+
):
|
| 1865 |
filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
|
| 1866 |
+
export_note, export_csv, export_pdf = _write_relationship_export_bundle(filtered_edges, relationship_id, ranking_mode)
|
| 1867 |
+
return (
|
| 1868 |
+
_relationship_detail_markdown(filtered_edges, relationship_id, ranking_mode),
|
| 1869 |
+
_relationship_timeline_html(filtered_edges, relationship_id),
|
| 1870 |
+
export_note,
|
| 1871 |
+
export_csv,
|
| 1872 |
+
export_pdf,
|
| 1873 |
+
)
|
| 1874 |
|
| 1875 |
def _update_graph(member_query: str, family: str, only_strong: bool, top_n: int):
|
| 1876 |
review_status = "stronger" if only_strong else "all"
|
|
|
|
| 1985 |
choices=[("Sectors", "sector"), ("Funding recipients", "recipient")],
|
| 1986 |
value="sector",
|
| 1987 |
)
|
| 1988 |
+
overview_ranking_mode = gr.Radio(
|
| 1989 |
+
label="Rank by",
|
| 1990 |
+
choices=[
|
| 1991 |
+
("Raw score", "raw"),
|
| 1992 |
+
("Experimental: relative to this member baseline", "relative"),
|
| 1993 |
+
],
|
| 1994 |
+
value="raw",
|
| 1995 |
+
)
|
| 1996 |
overview_only_strong = gr.Checkbox(label="Only stronger links", value=True)
|
| 1997 |
overview_top_n = gr.Dropdown(label="Show top results", choices=[5, 10, 15, 20], value=10)
|
| 1998 |
if example_member_choices:
|
|
|
|
| 2005 |
with gr.Row():
|
| 2006 |
overview_detail_md = gr.Markdown()
|
| 2007 |
overview_timeline_html = gr.HTML()
|
| 2008 |
+
export_note_md = gr.Markdown()
|
| 2009 |
+
with gr.Row():
|
| 2010 |
+
export_csv_file = gr.File(label="Evidence breakdown CSV", interactive=False)
|
| 2011 |
+
export_pdf_file = gr.File(label="Evidence breakdown PDF", interactive=False)
|
| 2012 |
|
| 2013 |
search_button.click(
|
| 2014 |
_update_overview,
|
| 2015 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, overview_ranking_mode, relationship_choice],
|
| 2016 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html, export_note_md, export_csv_file, export_pdf_file],
|
| 2017 |
)
|
| 2018 |
overview_member.submit(
|
| 2019 |
_update_overview,
|
| 2020 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, overview_ranking_mode, relationship_choice],
|
| 2021 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html, export_note_md, export_csv_file, export_pdf_file],
|
| 2022 |
)
|
| 2023 |
+
for control in (overview_family, overview_ranking_mode, overview_only_strong, overview_top_n):
|
| 2024 |
control.change(
|
| 2025 |
_update_overview,
|
| 2026 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, overview_ranking_mode, relationship_choice],
|
| 2027 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html, export_note_md, export_csv_file, export_pdf_file],
|
| 2028 |
)
|
| 2029 |
relationship_choice.change(
|
| 2030 |
_update_overview_detail,
|
| 2031 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, overview_ranking_mode, relationship_choice],
|
| 2032 |
+
[overview_detail_md, overview_timeline_html, export_note_md, export_csv_file, export_pdf_file],
|
| 2033 |
)
|
| 2034 |
|
| 2035 |
with gr.Accordion("Explore the network map (optional)", open=False):
|
|
|
|
| 2126 |
|
| 2127 |
app.load(
|
| 2128 |
_update_overview,
|
| 2129 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, overview_ranking_mode, relationship_choice],
|
| 2130 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html, export_note_md, export_csv_file, export_pdf_file],
|
| 2131 |
)
|
| 2132 |
app.load(
|
| 2133 |
_update_graph,
|