parent
c96c681641
commit
8c79aaf917
@ -0,0 +1,113 @@ |
|||||||
|
--- |
||||||
|
date: 2026-03-24 |
||||||
|
topic: "SVD Tab Redesign" |
||||||
|
status: 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. |
||||||
Loading…
Reference in new issue