Improve text contrast and horizontal table scrolling in Space UI
Browse files- public_space_app.py +136 -9
public_space_app.py
CHANGED
|
@@ -463,6 +463,67 @@ def _space_css() -> str:
|
|
| 463 |
color: #4b4b4b;
|
| 464 |
margin-bottom: 12px;
|
| 465 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
"""
|
| 467 |
|
| 468 |
|
|
@@ -1091,6 +1152,38 @@ def _graph_table(edges: pd.DataFrame) -> pd.DataFrame:
|
|
| 1091 |
]
|
| 1092 |
|
| 1093 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
def _filter_events(events: pd.DataFrame, member_query: str, event_type: str, score_label: str, text_query: str) -> pd.DataFrame:
|
| 1095 |
filtered = events.copy()
|
| 1096 |
if member_query.strip():
|
|
@@ -1441,7 +1534,15 @@ def build_app(copy_path: str | Path):
|
|
| 1441 |
review_status=review_status,
|
| 1442 |
max_edges=int(top_n),
|
| 1443 |
)
|
| 1444 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1445 |
|
| 1446 |
def _reset_graph(member_query: str):
|
| 1447 |
default_family = str(graph_defaults.get("relationship_family", "sector"))
|
|
@@ -1474,11 +1575,21 @@ def build_app(copy_path: str | Path):
|
|
| 1474 |
gr.update(value=int(default_top_n)),
|
| 1475 |
summary,
|
| 1476 |
_render_graph(filtered_nodes, filtered_edges),
|
| 1477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1478 |
)
|
| 1479 |
|
| 1480 |
def _update_events(member_query: str, event_type: str, score_label: str, text_query: str):
|
| 1481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1482 |
|
| 1483 |
with gr.Blocks(title=copy_payload.get("title", "Congress Public Records Slice"), css=_space_css()) as app:
|
| 1484 |
gr.HTML(_hero_html(manifest))
|
|
@@ -1565,7 +1676,7 @@ def build_app(copy_path: str | Path):
|
|
| 1565 |
graph_summary_md = gr.Markdown()
|
| 1566 |
graph_html = gr.HTML()
|
| 1567 |
with gr.Accordion("Current relationships in this map", open=False):
|
| 1568 |
-
graph_df = gr.
|
| 1569 |
|
| 1570 |
for control in (graph_family, graph_only_strong, graph_top_n):
|
| 1571 |
control.change(
|
|
@@ -1600,20 +1711,36 @@ def build_app(copy_path: str | Path):
|
|
| 1600 |
event_type = gr.Dropdown(label="Event type", choices=event_type_choices, value="all")
|
| 1601 |
score_label = gr.Dropdown(label="Score label", choices=score_label_choices, value="all")
|
| 1602 |
text_query = gr.Textbox(label="Issuer or sector search")
|
| 1603 |
-
explore_df = gr.
|
| 1604 |
for control in (member_query, event_type, score_label, text_query):
|
| 1605 |
control.change(_update_events, [member_query, event_type, score_label, text_query], explore_df)
|
| 1606 |
|
| 1607 |
with gr.Accordion("Inspect one released event row", open=False):
|
| 1608 |
event_id = gr.Dropdown(label="Event id", choices=event_id_choices, value=event_id_choices[0] if event_id_choices else None)
|
| 1609 |
event_detail_md = gr.Markdown()
|
| 1610 |
-
event_detail_df = gr.
|
| 1611 |
-
|
| 1612 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1613 |
|
| 1614 |
with gr.Accordion("Integrity-checked source records and audit summary", open=False):
|
| 1615 |
gr.Markdown(_consistency_summary_markdown(data["consistency"]))
|
| 1616 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1617 |
|
| 1618 |
with gr.Accordion("Methodology, limits, and downloads", open=False):
|
| 1619 |
gr.Markdown(copy_payload.get("landing_markdown", ""))
|
|
|
|
| 463 |
color: #4b4b4b;
|
| 464 |
margin-bottom: 12px;
|
| 465 |
}
|
| 466 |
+
.gradio-container .prose,
|
| 467 |
+
.gradio-container .prose p,
|
| 468 |
+
.gradio-container .prose li,
|
| 469 |
+
.gradio-container .prose strong,
|
| 470 |
+
.gradio-container .prose h1,
|
| 471 |
+
.gradio-container .prose h2,
|
| 472 |
+
.gradio-container .prose h3,
|
| 473 |
+
.gradio-container .prose h4,
|
| 474 |
+
.gradio-container .prose code {
|
| 475 |
+
color: var(--body-text-color) !important;
|
| 476 |
+
}
|
| 477 |
+
.table-shell {
|
| 478 |
+
background: var(--block-background-fill);
|
| 479 |
+
border: 1px solid var(--border-color-primary);
|
| 480 |
+
border-radius: 18px;
|
| 481 |
+
overflow: hidden;
|
| 482 |
+
margin-top: 10px;
|
| 483 |
+
}
|
| 484 |
+
.table-scroll {
|
| 485 |
+
overflow-x: auto;
|
| 486 |
+
overflow-y: auto;
|
| 487 |
+
max-height: 520px;
|
| 488 |
+
}
|
| 489 |
+
.public-table {
|
| 490 |
+
border-collapse: collapse;
|
| 491 |
+
width: max-content;
|
| 492 |
+
min-width: 100%;
|
| 493 |
+
font-size: 0.92rem;
|
| 494 |
+
}
|
| 495 |
+
.public-table thead th {
|
| 496 |
+
position: sticky;
|
| 497 |
+
top: 0;
|
| 498 |
+
z-index: 1;
|
| 499 |
+
background: var(--block-title-background-fill, var(--block-background-fill));
|
| 500 |
+
color: var(--body-text-color);
|
| 501 |
+
text-align: left;
|
| 502 |
+
padding: 10px 12px;
|
| 503 |
+
border-bottom: 1px solid var(--border-color-primary);
|
| 504 |
+
white-space: nowrap;
|
| 505 |
+
}
|
| 506 |
+
.public-table tbody td {
|
| 507 |
+
padding: 10px 12px;
|
| 508 |
+
border-bottom: 1px solid var(--border-color-primary);
|
| 509 |
+
color: var(--body-text-color);
|
| 510 |
+
white-space: nowrap;
|
| 511 |
+
max-width: none;
|
| 512 |
+
}
|
| 513 |
+
.public-table tbody tr:nth-child(even) td {
|
| 514 |
+
background: color-mix(in srgb, var(--block-background-fill) 88%, var(--body-background-fill) 12%);
|
| 515 |
+
}
|
| 516 |
+
.public-table a {
|
| 517 |
+
color: #c67f00 !important;
|
| 518 |
+
text-decoration: underline;
|
| 519 |
+
}
|
| 520 |
+
.table-note {
|
| 521 |
+
padding: 10px 12px;
|
| 522 |
+
font-size: 0.88rem;
|
| 523 |
+
color: var(--body-text-color-subdued);
|
| 524 |
+
border-top: 1px solid var(--border-color-primary);
|
| 525 |
+
background: var(--body-background-fill);
|
| 526 |
+
}
|
| 527 |
"""
|
| 528 |
|
| 529 |
|
|
|
|
| 1152 |
]
|
| 1153 |
|
| 1154 |
|
| 1155 |
+
def _format_table_cell(value: Any) -> str:
|
| 1156 |
+
text = "" if value is None else str(value)
|
| 1157 |
+
if not text:
|
| 1158 |
+
return ""
|
| 1159 |
+
escaped = html.escape(text)
|
| 1160 |
+
if text.startswith("http://") or text.startswith("https://"):
|
| 1161 |
+
label = escaped if len(text) <= 90 else html.escape(text[:87] + "...")
|
| 1162 |
+
return f'<a href="{escaped}" target="_blank" rel="noopener noreferrer">{label}</a>'
|
| 1163 |
+
display = escaped if len(text) <= 120 else html.escape(text[:117] + "...")
|
| 1164 |
+
return f'<span title="{escaped}">{display}</span>'
|
| 1165 |
+
|
| 1166 |
+
|
| 1167 |
+
def _table_html(frame: pd.DataFrame, *, empty_message: str, note: str = "", max_rows: int | None = None) -> str:
|
| 1168 |
+
if frame is None or frame.empty:
|
| 1169 |
+
return f'<div class="panel-note">{html.escape(empty_message)}</div>'
|
| 1170 |
+
preview = frame.head(int(max_rows)) if max_rows is not None else frame
|
| 1171 |
+
headers = "".join(f"<th>{html.escape(str(col))}</th>" for col in preview.columns)
|
| 1172 |
+
body_rows: list[str] = []
|
| 1173 |
+
for row in preview.fillna("").astype(str).to_dict("records"):
|
| 1174 |
+
body_cells = "".join(f"<td>{_format_table_cell(value)}</td>" for value in row.values())
|
| 1175 |
+
body_rows.append(f"<tr>{body_cells}</tr>")
|
| 1176 |
+
note_html = f'<div class="table-note">{html.escape(note)}</div>' if note else ""
|
| 1177 |
+
return (
|
| 1178 |
+
'<div class="table-shell">'
|
| 1179 |
+
'<div class="table-scroll">'
|
| 1180 |
+
f'<table class="public-table"><thead><tr>{headers}</tr></thead><tbody>{"".join(body_rows)}</tbody></table>'
|
| 1181 |
+
'</div>'
|
| 1182 |
+
f"{note_html}"
|
| 1183 |
+
'</div>'
|
| 1184 |
+
)
|
| 1185 |
+
|
| 1186 |
+
|
| 1187 |
def _filter_events(events: pd.DataFrame, member_query: str, event_type: str, score_label: str, text_query: str) -> pd.DataFrame:
|
| 1188 |
filtered = events.copy()
|
| 1189 |
if member_query.strip():
|
|
|
|
| 1534 |
review_status=review_status,
|
| 1535 |
max_edges=int(top_n),
|
| 1536 |
)
|
| 1537 |
+
return (
|
| 1538 |
+
summary,
|
| 1539 |
+
_render_graph(filtered_nodes, filtered_edges),
|
| 1540 |
+
_table_html(
|
| 1541 |
+
_graph_table(filtered_edges),
|
| 1542 |
+
empty_message="No relationships match the current graph filters.",
|
| 1543 |
+
note="Scroll sideways if you want to inspect every column in the current graph view.",
|
| 1544 |
+
),
|
| 1545 |
+
)
|
| 1546 |
|
| 1547 |
def _reset_graph(member_query: str):
|
| 1548 |
default_family = str(graph_defaults.get("relationship_family", "sector"))
|
|
|
|
| 1575 |
gr.update(value=int(default_top_n)),
|
| 1576 |
summary,
|
| 1577 |
_render_graph(filtered_nodes, filtered_edges),
|
| 1578 |
+
_table_html(
|
| 1579 |
+
_graph_table(filtered_edges),
|
| 1580 |
+
empty_message="No relationships match the current graph filters.",
|
| 1581 |
+
note="Scroll sideways if you want to inspect every column in the current graph view.",
|
| 1582 |
+
),
|
| 1583 |
)
|
| 1584 |
|
| 1585 |
def _update_events(member_query: str, event_type: str, score_label: str, text_query: str):
|
| 1586 |
+
filtered = _filter_events(events, member_query, event_type, score_label, text_query)
|
| 1587 |
+
display = filtered.head(150)
|
| 1588 |
+
return _table_html(
|
| 1589 |
+
display,
|
| 1590 |
+
empty_message="No released event rows match the current filters.",
|
| 1591 |
+
note=f"Showing {len(display)} of {len(filtered)} matching released event rows." if len(filtered) > len(display) else f"Showing {len(display)} released event rows.",
|
| 1592 |
+
)
|
| 1593 |
|
| 1594 |
with gr.Blocks(title=copy_payload.get("title", "Congress Public Records Slice"), css=_space_css()) as app:
|
| 1595 |
gr.HTML(_hero_html(manifest))
|
|
|
|
| 1676 |
graph_summary_md = gr.Markdown()
|
| 1677 |
graph_html = gr.HTML()
|
| 1678 |
with gr.Accordion("Current relationships in this map", open=False):
|
| 1679 |
+
graph_df = gr.HTML()
|
| 1680 |
|
| 1681 |
for control in (graph_family, graph_only_strong, graph_top_n):
|
| 1682 |
control.change(
|
|
|
|
| 1711 |
event_type = gr.Dropdown(label="Event type", choices=event_type_choices, value="all")
|
| 1712 |
score_label = gr.Dropdown(label="Score label", choices=score_label_choices, value="all")
|
| 1713 |
text_query = gr.Textbox(label="Issuer or sector search")
|
| 1714 |
+
explore_df = gr.HTML(value=_table_html(events.head(100), empty_message="No released event rows are available."))
|
| 1715 |
for control in (member_query, event_type, score_label, text_query):
|
| 1716 |
control.change(_update_events, [member_query, event_type, score_label, text_query], explore_df)
|
| 1717 |
|
| 1718 |
with gr.Accordion("Inspect one released event row", open=False):
|
| 1719 |
event_id = gr.Dropdown(label="Event id", choices=event_id_choices, value=event_id_choices[0] if event_id_choices else None)
|
| 1720 |
event_detail_md = gr.Markdown()
|
| 1721 |
+
event_detail_df = gr.HTML()
|
| 1722 |
+
|
| 1723 |
+
def _event_detail_view(events_state: pd.DataFrame, prov_state: pd.DataFrame, event_id_value: str):
|
| 1724 |
+
detail_md, prov_rows = _event_detail(events_state, prov_state, event_id_value)
|
| 1725 |
+
table_html = _table_html(
|
| 1726 |
+
prov_rows,
|
| 1727 |
+
empty_message="No provenance rows are attached to this released event row.",
|
| 1728 |
+
note="Scroll sideways to inspect all provenance columns and URLs.",
|
| 1729 |
+
)
|
| 1730 |
+
return detail_md, table_html
|
| 1731 |
+
|
| 1732 |
+
event_id.change(_event_detail_view, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
|
| 1733 |
+
app.load(_event_detail_view, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
|
| 1734 |
|
| 1735 |
with gr.Accordion("Integrity-checked source records and audit summary", open=False):
|
| 1736 |
gr.Markdown(_consistency_summary_markdown(data["consistency"]))
|
| 1737 |
+
gr.HTML(
|
| 1738 |
+
_table_html(
|
| 1739 |
+
data["artifact_index"].head(200),
|
| 1740 |
+
empty_message="No source artifact rows are available in the audit index.",
|
| 1741 |
+
note="Scroll sideways to inspect long URLs and SHA-256 values.",
|
| 1742 |
+
)
|
| 1743 |
+
)
|
| 1744 |
|
| 1745 |
with gr.Accordion("Methodology, limits, and downloads", open=False):
|
| 1746 |
gr.Markdown(copy_payload.get("landing_markdown", ""))
|