feat(overton): coherent narrative architecture — Quarto article, Explorer Overton tab, report cleanup
- U1: Remove stale findings_report.md and blog_post.html, add cross-reference headers to all 13 appendix reports, switch HTML report to canonical 4-party centrist definition - U2: Create Quarto narrative spine (overton_window.qmd) with 9 sections and 6 interactive Plotly charts. Includes 'About Stemwijzer' platform section. - U3: Add Overton tab to Explorer (centrist support trend, right-wing motion browser, explore-further links). Add Overton context expander to Kompas tab and 2024 breakpoint annotation to Trajectories tab. - U4: Create build_all_reports.py master regeneration script (3-phase, dependency-ordered, --skip-llm support) - U5: Update README with Research section, create reports/overton_window/README.md reading guide, update STATUS.md with broader platform framing Plan: docs/plans/2026-06-06-001-overton-coherent-narrative-plan.md 282 tests pass.
This commit is contained in:
@@ -8,10 +8,12 @@ from analysis.tabs.compass import build_compass_tab
|
||||
from analysis.tabs.trajectories import build_trajectories_tab
|
||||
from analysis.tabs.components import build_svd_components_tab
|
||||
from analysis.tabs.quiz import build_mp_quiz_tab
|
||||
from analysis.tabs.overton import build_overton_tab
|
||||
|
||||
__all__ = [
|
||||
"build_compass_tab",
|
||||
"build_trajectories_tab",
|
||||
"build_svd_components_tab",
|
||||
"build_mp_quiz_tab",
|
||||
"build_overton_tab",
|
||||
]
|
||||
|
||||
@@ -183,7 +183,18 @@ def build_compass_tab(db_path: str, window_size: str) -> None:
|
||||
):
|
||||
st.caption(_x_interp)
|
||||
|
||||
# Voting discipline analysis
|
||||
with st.expander("Overton Window Context"):
|
||||
st.markdown(
|
||||
"Het SVD-kompas visualiseert direct de dynamiek achter de "
|
||||
"Overton-verschuiving in de Tweede Kamer.\n\n"
|
||||
"**Centristische partijen** (D66, CDA, CU, NSC) zijn op **beide assen naar"
|
||||
" links** verschoven, terwijl rechtse partijen stabiel bleven. Dit patroon"
|
||||
' van "acceptatie zonder conversie" betekent dat centristen meer met'
|
||||
" rechtse moties meestemmen terwijl ze ideologisch verder van rechts af"
|
||||
" komen te staan.\n\n"
|
||||
"[Lees de volledige analyse](../reports/overton_window/overton_window.qmd)\n\n"
|
||||
"Probeer de **Stemwijzer-quiz** om te zien welke MP bij jouw standpunten past."
|
||||
)
|
||||
st.markdown("---")
|
||||
st.markdown(
|
||||
"**Stemdiscipline analyse:** De Rice-index meet hoe eensgezind partijen stemmen "
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""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"
|
||||
)
|
||||
@@ -666,5 +666,9 @@ def build_trajectories_tab(db_path: str, window_size: str) -> None:
|
||||
else:
|
||||
try:
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
st.info(
|
||||
"**Overton shift:** centrist support for right-wing motions surged "
|
||||
"after PVV's Nov 2023 election win."
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Trajectories rendering failed: {e}")
|
||||
|
||||
Reference in New Issue
Block a user