|
|
|
|
@ -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] |
|
|
|
|
|