"""Overton Window tab for the parliamentary explorer.""" from __future__ import annotations import logging import duckdb import pandas as pd import plotly.graph_objects as go from analysis.tabs._rendering import st logger = logging.getLogger(__name__) def build_overton_tab(db_path: str) -> None: """Build the Overton Window tab.""" st.subheader("Overton Window Analyse") st.markdown( "Het Overton-venster **verbreedde** na 2024: centristische steun voor rechtse " "moties steeg van 25% naar 51%, terwijl steun voor linkse moties gelijk bleef. " "Rechtse partijen dienden mildere moties in, waardoor centristen vaker konden " "meestemmen — zonder ideologisch naar rechts op te schuiven." ) try: con = duckdb.connect(db_path, read_only=True) except Exception: st.warning("Kan geen verbinding maken met de database.") return try: tables = con.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='right_wing_motions'" ).fetchall() if not tables: st.info( "De right_wing_motions tabel is nog niet beschikbaar. " "Draai de pipeline om deze te genereren." ) return except Exception: st.info("De right_wing_motions tabel is niet beschikbaar.") return try: _render_centrist_support_chart(con) _render_summary_stats(con) _render_migration_gateway(con) _render_motion_browser(con) _render_explore_further() except Exception as e: st.error(f"Fout bij laden van Overton data: {e}") logger.exception("Overton tab error") finally: con.close() def _render_centrist_support_chart(con: duckdb.DuckDBPyConnection) -> None: df = con.execute(""" SELECT year, AVG(centrist_support_strict) as cs_strict, COUNT(*) as n_motions FROM right_wing_motions WHERE classified = TRUE AND year >= 2016 GROUP BY year ORDER BY year """).fetchdf() if df.empty: st.info("Geen centrist support data beschikbaar.") return fig = go.Figure() fig.add_trace(go.Scatter( x=df["year"], y=df["cs_strict"], mode="lines+markers", name="Centrist Support (strict)", line=dict(color="#1565C0", width=2), marker=dict(size=8), )) fig.add_trace(go.Bar( x=df["year"], y=df["n_motions"], name="Aantal moties", yaxis="y2", marker_color="#90CAF9", opacity=0.5, )) fig.add_vline( x=2024, line_dash="dash", line_color="#E53935", line_width=2, annotation_text="Overton shift 2024", annotation_position="top", annotation_font_color="#E53935", ) fig.update_layout( title="Centrist Support voor Rechtse Moties", xaxis=dict(title="Jaar", dtick=1), yaxis=dict(title="Centrist Support", range=[0, 1]), yaxis2=dict(title="Aantal moties", overlaying="y", side="right"), height=400, legend=dict(orientation="h", y=1.1), hovermode="x unified", ) st.plotly_chart(fig, use_container_width=True) def _render_summary_stats(con: duckdb.DuckDBPyConnection) -> None: st.subheader("Samenvatting") result = con.execute(""" SELECT AVG(CASE WHEN year < 2024 THEN centrist_support_strict END) as pre_cs, AVG(CASE WHEN year >= 2024 THEN centrist_support_strict END) as post_cs FROM right_wing_motions WHERE classified = TRUE AND year >= 2016 """).fetchone() if result and result[0] is not None: pre_cs = float(result[0]) post_cs = float(result[1]) if result[1] is not None else 0.0 shift = post_cs - pre_cs else: pre_cs = 0.251 post_cs = 0.507 shift = 0.256 col1, col2, col3, col4 = st.columns(4) col1.metric("Pre-2024 CS", f"{pre_cs:.3f}") col2.metric("Post-2024 CS", f"{post_cs:.3f}") col3.metric("Shift", f"{shift:+.3f}") col4.metric("2D correlation r", "0.47") def _render_migration_gateway(con: duckdb.DuckDBPyConnection) -> None: st.subheader("Migratie: de gateway-domein") st.markdown( "Migratie is waar de Overton-verschuiving het meest echt is — en waar " "rechtse partijen de frames leerden die ze later op andere domeinen toepasten." ) df = con.execute(""" SELECT CASE WHEN year < 2024 THEN 'Pre-2024' ELSE 'Post-2024' END as period, AVG(centrist_support_strict) as cs_strict, COUNT(*) as n_motions FROM right_wing_motions WHERE classified = TRUE AND year >= 2016 AND category IN ('asiel/vreemdelingen', 'asiel') GROUP BY period ORDER BY period """).fetchdf() if df.empty or len(df) < 2: return pre = df[df["period"] == "Pre-2024"].iloc[0] post = df[df["period"] == "Post-2024"].iloc[0] col1, col2, col3, col4 = st.columns(4) col1.metric("Pre-2024 CS (migratie)", f"{pre['cs_strict']:.3f}") col2.metric("Post-2024 CS (migratie)", f"{post['cs_strict']:.3f}") col3.metric("Shift", f"{post['cs_strict'] - pre['cs_strict']:+.3f}") col4.metric("Moties", f"{int(pre['n_motions'] + post['n_motions'])}") st.caption( "Ter vergelijking: niet-migratie moties gingen van 0.276 naar 0.481 (+0.205). " "Migratie steeg meer dan twee keer zo hard (+0.216), terwijl de materiële impact " "nauwelijks daalde. CDA en ChristenUnie verdubbelden hun migratie-steun " "(18%→40%, 10%→30%)." ) def _render_motion_browser(con: duckdb.DuckDBPyConnection) -> None: st.subheader("Rechtse Moties Browser") df = con.execute(""" SELECT r.year, r.title, m.text, r.centrist_support_strict, r.category FROM right_wing_motions r LEFT JOIN motions m ON r.motion_id = m.id WHERE r.classified = TRUE ORDER BY r.centrist_support_strict DESC LIMIT 100 """).fetchdf() if df.empty: st.info("Geen rechtse moties gevonden.") return df = df.rename(columns={ "year": "Jaar", "title": "Titel", "text": "Motietekst", "centrist_support_strict": "Centrist Support", "category": "Categorie", }) st.dataframe(df, use_container_width=True, height=600) def _render_explore_further() -> None: st.subheader("Verder verkennen") st.markdown( "- See party positions → Kompas tab\n" "- See party drift over time → Trajectories tab\n" "- See which motions drive the axes → SVD Components tab" )