"""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( "Hoe het Overton-venster verschuift: de relatie tussen centristisch stemgedrag " "en de beweging van partijen op het politieke kompas." ) 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_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_motion_browser(con: duckdb.DuckDBPyConnection) -> None: st.subheader("Rechtse Moties Browser") df = con.execute(""" SELECT year, title, centrist_support_strict, category FROM right_wing_motions WHERE classified = TRUE ORDER BY centrist_support_strict DESC LIMIT 50 """).fetchdf() if df.empty: st.info("Geen rechtse moties gevonden.") return df["title"] = df["title"].str.slice(0, 80) st.dataframe(df, use_container_width=True) 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" )