feat(explorer): restructure SVD tab — pole-split motions, party axis chart, inline expanders with voting
This commit is contained in:
+61
-33
@@ -929,62 +929,90 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
)
|
)
|
||||||
comp_sel = comp_options[comp_sel_idx]
|
comp_sel = comp_options[comp_sel_idx]
|
||||||
|
|
||||||
# Show theme explanation + poles
|
# Show theme explanation
|
||||||
theme = SVD_THEMES.get(comp_sel, {})
|
theme = SVD_THEMES.get(comp_sel, {})
|
||||||
if theme:
|
if theme:
|
||||||
st.info(f"**{theme['label']}** — {theme['explanation']}")
|
st.info(f"**{theme['label']}** — {theme['explanation']}")
|
||||||
pos = theme.get("positive_pole", "")
|
|
||||||
neg = theme.get("negative_pole", "")
|
|
||||||
if pos or neg:
|
|
||||||
pcol, ncol = st.columns(2)
|
|
||||||
with pcol:
|
|
||||||
st.success(f"▲ **Positieve pool:** {pos}")
|
|
||||||
with ncol:
|
|
||||||
st.error(f"▼ **Negatieve pool:** {neg}")
|
|
||||||
|
|
||||||
motions = comp_map.get(comp_sel, [])
|
motions = comp_map.get(comp_sel, [])
|
||||||
|
|
||||||
col1, col2 = st.columns([1, 2])
|
# Party axis chart
|
||||||
with col1:
|
party_scores = load_party_axis_scores(db_path)
|
||||||
st.markdown("**Top-moties (titels)**")
|
_render_party_axis_chart(party_scores, comp_sel)
|
||||||
for m in motions:
|
|
||||||
mid = m.get("motion_id")
|
|
||||||
score = m.get("score", 0.0)
|
|
||||||
title = m.get("title") or f"Motie #{mid}"
|
|
||||||
sign = "▲" if score >= 0 else "▼"
|
|
||||||
if st.button(f"{sign} {mid}: {title[:72]}", key=f"btn_{comp_sel}_{mid}"):
|
|
||||||
st.session_state["svd_selected_mid"] = mid
|
|
||||||
|
|
||||||
with col2:
|
# Batch-fetch motion details (title, date, policy_area, url, body_text, voting_results)
|
||||||
sel_mid = st.session_state.get("svd_selected_mid")
|
motion_ids = [m.get("motion_id") for m in motions if m.get("motion_id") is not None]
|
||||||
if not sel_mid and motions:
|
motion_details: Dict[int, tuple] = {}
|
||||||
sel_mid = motions[0].get("motion_id")
|
if motion_ids:
|
||||||
if sel_mid:
|
|
||||||
# fetch motion metadata from DB for completeness
|
|
||||||
try:
|
try:
|
||||||
|
placeholders = ", ".join("?" for _ in motion_ids)
|
||||||
con = duckdb.connect(database=db_path, read_only=True)
|
con = duckdb.connect(database=db_path, read_only=True)
|
||||||
row = con.execute(
|
db_rows = con.execute(
|
||||||
"SELECT id, title, date, policy_area, url, body_text FROM motions WHERE id=?",
|
f"SELECT id, title, date, policy_area, url, body_text, voting_results "
|
||||||
[int(sel_mid)],
|
f"FROM motions WHERE id IN ({placeholders})",
|
||||||
).fetchone()
|
[int(mid) for mid in motion_ids],
|
||||||
|
).fetchall()
|
||||||
con.close()
|
con.close()
|
||||||
|
motion_details = {r[0]: r for r in db_rows}
|
||||||
except Exception:
|
except Exception:
|
||||||
row = None
|
logger.exception("Failed to batch-fetch motion details")
|
||||||
|
|
||||||
|
# Split motions by pole sign
|
||||||
|
pos_motions = [m for m in motions if float(m.get("score", 0.0)) >= 0]
|
||||||
|
neg_motions = [m for m in motions if float(m.get("score", 0.0)) < 0]
|
||||||
|
|
||||||
|
pos_pole = (
|
||||||
|
theme.get("positive_pole", "Positieve pool") if theme else "Positieve pool"
|
||||||
|
)
|
||||||
|
neg_pole = (
|
||||||
|
theme.get("negative_pole", "Negatieve pool") if theme else "Negatieve pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
pcol, ncol = st.columns(2)
|
||||||
|
|
||||||
|
with pcol:
|
||||||
|
st.success(f"▲ **Positieve pool:** {pos_pole}")
|
||||||
|
for m in pos_motions:
|
||||||
|
mid = m.get("motion_id")
|
||||||
|
raw_title = m.get("title") or f"Motie #{mid}"
|
||||||
|
with st.expander(f"▲ {raw_title[:80]}"):
|
||||||
|
row = motion_details.get(int(mid)) if mid is not None else None
|
||||||
if row:
|
if row:
|
||||||
st.markdown(f"### {row[1] or f'Motie #{row[0]}'}")
|
|
||||||
try:
|
try:
|
||||||
date_str = str(row[2])[:10]
|
date_str = str(row[2])[:10]
|
||||||
except Exception:
|
except Exception:
|
||||||
date_str = "?"
|
date_str = "?"
|
||||||
st.caption(f"📅 {date_str} | {row[3]}")
|
st.caption(f"📅 {date_str} | {row[3] or '—'}")
|
||||||
if row[4] and str(row[4]).startswith("http"):
|
if row[4] and str(row[4]).startswith("http"):
|
||||||
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
|
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
|
||||||
if row[5]:
|
if row[5]:
|
||||||
with st.expander("Toon volledige tekst"):
|
with st.expander("Toon volledige tekst"):
|
||||||
st.write(row[5])
|
st.write(row[5])
|
||||||
|
_render_voting_results(row[6])
|
||||||
else:
|
else:
|
||||||
st.info(f"Metadata not found in DB for motion {sel_mid}")
|
st.caption("_Geen metadata beschikbaar_")
|
||||||
|
|
||||||
|
with ncol:
|
||||||
|
st.error(f"▼ **Negatieve pool:** {neg_pole}")
|
||||||
|
for m in neg_motions:
|
||||||
|
mid = m.get("motion_id")
|
||||||
|
raw_title = m.get("title") or f"Motie #{mid}"
|
||||||
|
with st.expander(f"▼ {raw_title[:80]}"):
|
||||||
|
row = motion_details.get(int(mid)) if mid is not None else None
|
||||||
|
if row:
|
||||||
|
try:
|
||||||
|
date_str = str(row[2])[:10]
|
||||||
|
except Exception:
|
||||||
|
date_str = "?"
|
||||||
|
st.caption(f"📅 {date_str} | {row[3] or '—'}")
|
||||||
|
if row[4] and str(row[4]).startswith("http"):
|
||||||
|
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
|
||||||
|
if row[5]:
|
||||||
|
with st.expander("Toon volledige tekst"):
|
||||||
|
st.write(row[5])
|
||||||
|
_render_voting_results(row[6])
|
||||||
|
else:
|
||||||
|
st.caption("_Geen metadata beschikbaar_")
|
||||||
|
|
||||||
|
|
||||||
def build_mp_quiz_tab(db_path: str) -> None:
|
def build_mp_quiz_tab(db_path: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user