6.0 KiB
| date | topic | status |
|---|---|---|
| 2026-03-28 | Compass UI Improvements | validated |
Compass UI Improvements
Problem Statement
Three separate issues degrade the political compass UI:
-
SVD axis descriptions (axes 3–5) are outdated. The current
label,explanation,positive_pole, andnegative_polestrings inSVD_THEMESwere written for an earlier dataset and no longer match the structural patterns the axes actually capture. Using single-year (2024) centroid snapshots to verify this was misleading — the fix must be grounded in multi-year averages and motion-level content. -
Y-axis direction indicators are broken. The current
"Progressief ↑ / Conservatief ↓"string is passed as the Plotly Y-axis title. Plotly rotates axis titles 90° counter-clockwise, so the arrows end up pointing sideways instead of up/down. This appears in the two compass scatter plots and in the trajectories tab. -
No voting discipline context. The compass shows where parties sit spatially but gives no sense of whether a party votes as a bloc. This context would make the compass more interpretable.
Constraints
- No new DB tables or schema changes.
compute_party_disciplinereadsmp_voteswheremp_name LIKE '%,%'(individual MP rows only — party-aggregate rows are excluded).- Skip discipline section if fewer than 5 roll-call motions in the selected window.
- Follow existing patterns in
explorer.pyanddatabase.py. - Tests use
uv run pytest.
Approach
Change 1 — Axis descriptions: Derive corrected descriptions from multi-year party centroid averages (all annual windows, not just 2024) and from the motion-level content that loads high/low on each axis. Update only label, explanation, positive_pole, negative_pole in SVD_THEMES entries for axes 3, 4, and 5. The flip boolean is not changed.
Change 2 — Y-axis arrows: Replace the ↑/↓ characters from the axis title string (set to plain "Progressief / Conservatief"). Add two fig.add_annotation calls per chart: top-center "▲ Progressief" and bottom-center "▼ Conservatief", using xref="paper", yref="paper", showarrow=False, styled to be subtle (small font, muted color). Apply to both compass scatter plots and the trajectories chart.
Change 3 — Voting discipline: Add a compute_party_discipline(db_path, start_date, end_date) function in explorer.py that queries individual MP votes, computes per-party Rice index (fraction voting with party majority), and returns a DataFrame with columns [party, n_motions, discipline]. In build_compass_tab, after rendering the compass chart, call this function with the window's date range, and render: (a) a horizontal bar chart sorted ascending (least disciplined at top), and (b) a small table showing the three most and three least disciplined parties. If fewer than 5 motions, show a brief explanatory message instead.
Architecture
All changes are confined to explorer.py. No changes to analysis/, database.py, or test files (the discipline function is a read-only helper, not shared infrastructure).
Components
SVD_THEMES dict (explorer.py ~line 1156)
- Entries for axes 3, 4, 5 updated in-place.
- New text is based on multi-year patterns (see Data Flow below).
Y-axis annotation helper (explorer.py)
- Small inline helper or inline code block that adds the two direction annotations to any given
fig. - Called once after each
px.scatter(...)and once after the trajectoriesfigis built.
compute_party_discipline(db_path, start_date, end_date) (explorer.py)
- Connects to DuckDB read-only.
- Queries
mp_votesfiltered to individual MPs (mp_name LIKE '%,%') and date range. - Groups by
(motion_id, party), counts votes per token, determines majority token, computes Rice index per motion per party. - Averages Rice index across motions per party.
- Returns
pd.DataFrame(columns=["party", "n_motions", "discipline"])or empty DataFrame.
build_compass_tab additions (explorer.py ~line 841+)
- After
st.plotly_chart(fig, ...), map the currentwindow_idxto a(start_date, end_date)range. - Call
compute_party_discipline(...). - If result has ≥ 5 motions: render bar chart + extremes table under a
st.subheader("Stemgedrag cohesie"). - If not:
st.caption("Te weinig hoofdelijke stemmingen voor cohesieanalyse.").
Data Flow
Axis description research (prior to implementation): Multi-year centroid averages are computed by averaging each party's SVD vector across all annual windows in which it appears. The axis 3/4/5 descriptions are updated to reflect these stable patterns rather than any single year's snapshot.
Discipline computation:
mp_votes (individual MPs, date range)
→ GROUP BY (motion_id, party, vote) → vote counts
→ determine majority_vote per (motion_id, party)
→ Rice index = (count voting with majority) / (total voting) per motion per party
→ average Rice index across motions → per-party score
→ return DataFrame
Error Handling
compute_party_disciplinereturns an empty DataFrame on any DB exception (logged, not raised), following the pattern of other read helpers inexplorer.py.- Empty DataFrame → show the "too few motions" caption (same path as < 5 motions).
- The Y-axis annotation is purely visual — no error paths needed.
- Axis description changes are static strings — no runtime risk.
Testing Strategy
- The
compute_party_disciplinefunction is tested with a small in-memory DuckDB fixture intests/test_political_compass.py:- Construct a fixture with 6 motions, 2 parties, each with varying vote splits.
- Assert returned DataFrame has correct columns and that discipline scores are in [0, 1].
- Assert empty DataFrame is returned when date range has 0 motions.
- Y-axis annotation: no unit test needed (visual only, trivially correct).
- Axis description changes: no unit test needed (static strings).
- Run all tests with
uv run pytest tests/test_political_compass.py -vafter each change.
Open Questions
None. All design decisions are resolved.