diff --git a/explorer.py b/explorer.py index dfe937a..4348ea7 100644 --- a/explorer.py +++ b/explorer.py @@ -1352,6 +1352,76 @@ def _render_party_axis_chart( st.plotly_chart(fig, use_container_width=True) +def _render_party_axis_chart_1d( + party_coords: Dict[str, Tuple[float, ...]], + comp_sel: int, + theme: dict, +) -> None: + """Render a 1D horizontal bar chart of party positions on SVD component `comp_sel`. + + Args: + party_coords: Dict mapping party name to tuple of scores (score_for_comp,) + comp_sel: SVD component number (1-indexed) + theme: Dict with label, positive_pole, negative_pole, flip + """ + import plotly.graph_objects as go + + if not party_coords: + st.caption("_Partijdata niet beschikbaar voor deze as._") + return + + # Extract scores and parties + parties = list(party_coords.keys()) + scores = [coords[0] for coords in party_coords.values()] + + # Apply flip if needed + flip = theme.get("flip", False) + if flip: + scores = [-s for s in scores] + + # Get party colors + party_colors = [PARTY_COLOURS.get(p, "#9E9E9E") for p in parties] + + # Sort by score for better visualization + sorted_indices = np.argsort(scores) + sorted_parties = [parties[i] for i in sorted_indices] + sorted_scores = [scores[i] for i in sorted_indices] + sorted_colors = [party_colors[i] for i in sorted_indices] + + # Create horizontal bar chart + fig = go.Figure() + + fig.add_trace( + go.Bar( + y=sorted_parties, + x=sorted_scores, + orientation="h", + marker_color=sorted_colors, + text=[f"{s:.2f}" for s in sorted_scores], + textposition="outside", + ) + ) + + # Update layout + label = theme.get("label", f"As {comp_sel}") + positive_pole = theme.get("positive_pole", "Positief") + negative_pole = theme.get("negative_pole", "Negatief") + + fig.update_layout( + title=f"Partijposities — {label}", + xaxis_title=f"{negative_pole} ← → {positive_pole}", + yaxis_title="", + height=max(400, len(parties) * 25), + margin=dict(l=150), + showlegend=False, + ) + + # Add vertical line at x=0 + fig.add_vline(x=0, line_dash="dash", line_color="gray", opacity=0.5) + + st.plotly_chart(fig, use_container_width=True) + + @st.cache_data(show_spinner="Moties laden…") def load_motions_df(db_path: str) -> pd.DataFrame: """Load the full motions table as a pandas DataFrame (read-only).""" @@ -2823,15 +2893,22 @@ def build_svd_components_tab(db_path: str) -> None: p: coords for p, coords in party_coords.items() if p in valid_parties } - # Only render party axis chart for components 1 and 2 (which have 2D coords) + # Render party axis chart if comp_sel in (1, 2): + # Components 1-2 use 2D coords from political compass _render_party_axis_chart( party_coords, comp_sel, theme, bootstrap_data=bootstrap_data ) else: - st.caption( - "_Partijposities zijn alleen beschikbaar voor de eerste twee SVD-assen._" - ) + # Components 3-10 use 1D scores from SVD + # Extract 1D scores for this component + party_1d_coords = {} + idx = comp_sel - 1 # Convert to 0-indexed + for party, scores in party_scores.items(): + if scores and len(scores) > idx: + party_1d_coords[party] = (scores[idx],) + + _render_party_axis_chart_1d(party_1d_coords, comp_sel, theme) # Batch-fetch motion details (title, date, policy_area, url, body_text, voting_results) motion_ids = [m.get("motion_id") for m in motions if m.get("motion_id") is not None]