You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
motief/docs/superpowers/specs/2026-03-24-svd-tab-redesign...

5.1 KiB

date topic status
2026-03-24 SVD Tab Redesign validated

SVD Tab Redesign

Problem Statement

The SVD components tab lists all motions in a single column, shows detail in a separate right-hand pane (session state driven), has no party-level spatial overview, and does not show voting breakdowns. Users need a cleaner split of positive/negative motions, inline expand/collapse, a party axis chart, and full voting data per motion.

Constraints

  • All UI text in Dutch
  • Use PARTY_COLOURS and plotly.express / plotly.graph_objects (already imported)
  • No new DB tables — reuse svd_vectors (entity_type='mp', window_id='2025') and motions.voting_results
  • No push to remote without explicit instruction

Approach

Restructure build_svd_components_tab in explorer.py — no new files, no new tables. Three additions to the function:

  1. New @st.cache_data helper load_party_axis_scores(db_path) inside the module (top-level, not nested) — queries party SVD vectors once, returns {party: [float * 50]}
  2. New private render helper _render_party_axis_chart(party_scores, comp_sel) — builds and returns a Plotly figure for the 1D horizontal scatter
  3. Replace motion list + session state + right column with st.expander per motion, split into two equal columns by pole sign

Architecture

Single file change: explorer.py.

  • load_party_axis_scores(db_path) — new @st.cache_data top-level function
  • _render_party_axis_chart(party_scores, comp_sel) — new private render helper
  • build_svd_components_tab(db_path) — restructured; no new function signature

Components

load_party_axis_scores(db_path: str) -> dict[str, list[float]]

  • @st.cache_data
  • Queries svd_vectors WHERE entity_type='mp' AND window_id='2025' AND entity_id IN (CURRENT_PARLIAMENT_PARTIES)
  • Parses JSON vector per row, returns {party_name: [float * 50]}
  • CURRENT_PARLIAMENT_PARTIES = frozenset of 15 parties: PVV, VVD, NSC, BBB, D66, GroenLinks-PvdA, CDA, SP, ChristenUnie, SGP, Volt, DENK, PvdD, JA21, FVD

_render_party_axis_chart(party_scores, comp_sel)

  • Extracts score at index comp_sel - 1 for each party
  • Creates a go.Figure with go.Scatter:
    • x = list of scores, y = list of zeros
    • mode='markers+text', text = party name, textposition='top center'
    • Marker colour from PARTY_COLOURS (with ChristenUnie alias → #0288D1)
    • Marker size = 12, hover text = "{party}: {score:.3f}"
  • Adds a thin horizontal baseline (go.Scatter with two x extremes, y=0, grey line, no hover)
  • Layout: height=180, y-axis hidden, x-axis labeled "← Negatieve pool | Positieve pool →", no legend, minimal margins
  • Renders via st.plotly_chart(fig, use_container_width=True)

build_svd_components_tab restructure

Remove:

  • col1, col2 = st.columns([1, 2]) motion layout
  • All st.button motion-list code
  • st.session_state["svd_selected_mid"] read/write
  • Right-column detail pane (single DB query per selected motion)

Add:

  1. Call load_party_axis_scores(db_path) → pass to _render_party_axis_chart
  2. Batch DB query: SELECT id, title, date, policy_area, url, body_text, voting_results FROM motions WHERE id IN (...) for all motion IDs of comp_sel — one query, results as dict[int, row]
  3. Split motions: pos_motions = [m for m in motions if m["score"] >= 0], neg_motions = [m for m in motions if m["score"] < 0]
  4. pcol, ncol = st.columns(2)
    • pcol: st.success("▲ Positieve pool: {positive_pole}") + expanders for pos_motions
    • ncol: st.error("▼ Negatieve pool: {negative_pole}") + expanders for neg_motions
  5. Each expander: st.expander(f"▲/▼ {title[:80]}")
    • Inside: st.caption(f"📅 {date} | {policy_area}"), URL link if present
    • st.expander("Toon volledige tekst")st.write(body_text)
    • _render_voting_results(voting_results_json)

Data Flow

DB: svd_vectors (entity_type='mp', window='2025')
  → load_party_axis_scores()  [cached]
    → _render_party_axis_chart(scores, comp_sel)

JSON: top_svd_top_motions.json
  → motion ids for comp_sel

DB: motions WHERE id IN (...)  [one batch query]
  → {motion_id: row} lookup

SVD_THEMES[comp_sel]
  → explanation, positive_pole, negative_pole

pos_motions / neg_motions split
  → st.columns(2)
    → st.expander per motion
      → metadata + voting_results → _render_voting_results()

Error Handling

  • load_party_axis_scores returns empty dict on any DB error → _render_party_axis_chart renders st.caption("Partijdata niet beschikbaar") and returns early
  • Batch motion query exception → motion_details = {} → expanders show title only, no metadata/voting
  • Individual missing voting_results_render_voting_results already handles None/empty gracefully

Testing Strategy

  • Existing syntax check (python3 -c "import ast; ast.parse(...)") after edit
  • Manual smoke test: load the tab, select each axis, expand a motion, verify voting data shows
  • No new unit tests required (UI-only change; underlying data functions are unchanged)

Open Questions

  • None. Design is complete and fully determined by codebase context.