diff --git a/explorer.py b/explorer.py index 1bb973b..186ed13 100644 --- a/explorer.py +++ b/explorer.py @@ -251,21 +251,27 @@ def load_party_axis_scores(db_path: str) -> Dict[str, List[float]]: def _render_party_axis_chart( - party_scores: Dict[str, List[float]], comp_sel: int + party_scores: Dict[str, List[float]], comp_sel: int, theme: dict ) -> None: """Render a 1D horizontal Plotly scatter of party positions on SVD axis `comp_sel`. Each party is plotted at its score on a single horizontal axis (y=0). + When theme['flip'] is True the scores are negated so that the progressive/left + side always appears on the left of the chart. """ if not party_scores: st.caption("_Partijdata niet beschikbaar voor deze as._") return axis_idx = comp_sel - 1 # 0-based index into the 50-dim vector + flip = theme.get("flip", False) data: list[dict] = [] for party, vec in party_scores.items(): if axis_idx < len(vec): - data.append({"party": party, "score": vec[axis_idx]}) + score = vec[axis_idx] + if flip: + score = -score + data.append({"party": party, "score": score}) if not data: st.caption("_Geen partijscores voor deze as._") @@ -276,9 +282,17 @@ def _render_party_axis_chart( colours = [PARTY_COLOURS.get(p, "#9E9E9E") for p in parties] hover = [f"{p}: {s:.3f}" for p, s in zip(parties, scores)] + # Determine axis labels: left = progressive pole, right = conservative pole + pos_pole = theme.get("positive_pole", "") + neg_pole = theme.get("negative_pole", "") + left_label = pos_pole if flip else neg_pole + right_label = neg_pole if flip else pos_pole + fig = go.Figure() # Baseline x_min, x_max = min(scores) * 1.15, max(scores) * 1.15 + if x_min == x_max: + x_min, x_max = x_min - 1, x_max + 1 fig.add_trace( go.Scatter( x=[x_min, x_max], @@ -297,7 +311,7 @@ def _render_party_axis_chart( mode="markers+text", text=parties, textposition="top center", - marker={"size": 12, "color": colours}, + marker={"size": 18, "color": colours}, hovertext=hover, hoverinfo="text", showlegend=False, @@ -307,12 +321,13 @@ def _render_party_axis_chart( height=160, margin={"l": 10, "r": 10, "t": 10, "b": 30}, xaxis={ - "title": "← Negatieve pool | Positieve pool →", + "title": f"← {left_label} | {right_label} →", "zeroline": True, "zerolinecolor": "#aaaaaa", }, yaxis={"visible": False, "range": [-1, 2]}, - plot_bgcolor="white", + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", ) st.plotly_chart(fig, use_container_width=True) @@ -784,6 +799,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Breed coalitiebeleid: zorg, defensie, multilateralisme, inclusie", "negative_pole": "Radicale PVV-eis tot onmiddellijke uitzetting migranten", + "flip": True, }, 2: { "label": "Nationalistisch migratiebeleid versus progressief internationaal solidariteitsdenken", @@ -799,6 +815,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Asielbeperking, nationaal belang, restrictief migratiebeleid", "negative_pole": "Pro-Palestina, progressieve zorgrechten, anti-discriminatie minderheden", + "flip": False, }, 3: { "label": "Humanitaire solidariteit en inclusie versus nationalistische handhaving en deregulering", @@ -815,6 +832,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Internationale solidariteit, inclusie en pragmatische overheidsinterventie", "negative_pole": "Strikte handhaving, deregulering en nationalistisch eigenbelang boven humanitaire verplichtingen", + "flip": True, }, 4: { "label": "Publieke voorzieningen beschermen versus liberale marktwerking", @@ -830,8 +848,9 @@ def build_svd_components_tab(db_path: str) -> None: "voorzieningen betaalbaar en toegankelijk te houden, of dat vrije markt en open handel " "leidend moeten zijn." ), - "positive_pole": "Staatsbescherming van betaalbare publieke voorzieningen voor iedereen", - "negative_pole": "Vrije handel, open economie en marktgerichte arbeidsmigratie", + "positive_pole": "Vrije handel, open economie en marktgerichte arbeidsmigratie", + "negative_pole": "Staatsbescherming van betaalbare publieke voorzieningen voor iedereen", + "flip": False, }, 5: { "label": "Christelijk-conservatief sociaal beleid versus seculier progressief", @@ -844,8 +863,9 @@ def build_svd_components_tab(db_path: str) -> None: "consistent vanuit een christelijk-sociale visie stemmen tegenover partijen als D66, " "GroenLinks-PvdA en SP die een seculier-progressief beleid voorstaan." ), - "positive_pole": "Christelijk-conservatief: gezin, kerk, leven, traditionele waarden", - "negative_pole": "Seculier-progressief: individuele autonomie, progressieve sociale rechten", + "positive_pole": "Seculier-progressief: individuele autonomie, progressieve sociale rechten", + "negative_pole": "Christelijk-conservatief: gezin, kerk, leven, traditionele waarden", + "flip": True, }, 6: { "label": "Christelijk-sociaal beschermingsbeleid versus links-progressieve systeemkritiek", @@ -860,6 +880,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Christelijk-sociaal beschermingsbeleid voor pgb, kinderen en geloofsgroepen", "negative_pole": "Links-progressieve systeemkritiek op zorg, arbeid en internationale solidariteit", + "flip": False, }, 7: { "label": "Liberaal investeren en defensie versus linkse bescherming en controle", @@ -873,6 +894,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Gerichte liberale investeringen in sport, wetenschap en defensie", "negative_pole": "Collectieve bescherming, parlementaire controle en anti-marktwerking in zorg", + "flip": False, }, 8: { "label": "Confessioneel-sociaal coalitiebeleid versus procedurele blokkade en handhaving", @@ -888,6 +910,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Coalitie christelijk-sociaal beleid: defensie, stikstofmaatwerk, bouw en ethiek", "negative_pole": "Procedurele blokkade coffeeshop, handhavingsdoelstelling en topsportderegulering", + "flip": False, }, 9: { "label": "Brede coalitiemeerderheid versus links marktingrijpen zorg", @@ -903,6 +926,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Breed gedragen beleid door centrum-rechts meerderheidsstemmen", "negative_pole": "Socialistische inkomensregulering en marktingrijpen in de zorg", + "flip": False, }, 10: { "label": "Gereguleerde kennismigratie en natuur-landbouwtransitie versus institutionele veiligheid", @@ -918,6 +942,7 @@ def build_svd_components_tab(db_path: str) -> None: ), "positive_pole": "Beperkte kennismigratie, natuur-landbouwtransitie en Gaza-humanitair", "negative_pole": "Institutionele veiligheidssturing, economisch nationalisme en Woo-beperking", + "flip": True, }, } @@ -985,7 +1010,7 @@ def build_svd_components_tab(db_path: str) -> None: # Party axis chart party_scores = load_party_axis_scores(db_path) - _render_party_axis_chart(party_scores, comp_sel) + _render_party_axis_chart(party_scores, 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] @@ -1021,21 +1046,28 @@ def build_svd_components_tab(db_path: str) -> None: pos_motions = [m for m in motions if float(m.get("score", 0.0)) >= 0] neg_motions = [m for m in motions if float(m.get("score", 0.0)) < 0] - pos_pole = ( - theme.get("positive_pole", "Positieve pool") if theme else "Positieve pool" - ) - neg_pole = ( - theme.get("negative_pole", "Negatieve pool") if theme else "Negatieve pool" - ) + flip = theme.get("flip", False) if theme else False + pos_pole = theme.get("positive_pole", "") if theme else "" + neg_pole = theme.get("negative_pole", "") if theme else "" + + # Determine which pole goes left (progressive) and which goes right + if flip: + left_pole, right_pole = pos_pole, neg_pole + left_motions, right_motions = pos_motions, neg_motions + left_arrow, right_arrow = "▲", "▼" + else: + left_pole, right_pole = neg_pole, pos_pole + left_motions, right_motions = neg_motions, pos_motions + left_arrow, right_arrow = "▼", "▲" - pcol, ncol = st.columns(2) + lcol, rcol = st.columns(2) - with pcol: - st.success(f"▲ **Positieve pool:** {pos_pole}") - for m in pos_motions: + with lcol: + st.markdown(f"**← {left_pole}**") + for m in left_motions: mid = m.get("motion_id") raw_title = m.get("title") or f"Motie #{mid}" - with st.expander(f"▲ {raw_title[:80]}"): + with st.expander(f"{left_arrow} {raw_title[:80]}"): row = motion_details.get(int(mid)) if mid is not None else None if row: try: @@ -1052,12 +1084,12 @@ def build_svd_components_tab(db_path: str) -> None: else: st.caption("_Geen metadata beschikbaar_") - with ncol: - st.error(f"▼ **Negatieve pool:** {neg_pole}") - for m in neg_motions: + with rcol: + st.markdown(f"**{right_pole} →**") + for m in right_motions: mid = m.get("motion_id") raw_title = m.get("title") or f"Motie #{mid}" - with st.expander(f"▼ {raw_title[:80]}"): + with st.expander(f"{right_arrow} {raw_title[:80]}"): row = motion_details.get(int(mid)) if mid is not None else None if row: try: