"""Generate additional blog charts: controversy trend + party alignment heatmap.""" from __future__ import annotations import os, sys ROOT = os.path.dirname(os.path.abspath(__file__)) if ROOT not in sys.path: sys.path.insert(0, ROOT) import duckdb import plotly.graph_objects as go import plotly.express as px import numpy as np DB = "data/motions.db" OUT = "outputs/blog-charts" os.makedirs(OUT, exist_ok=True) con = duckdb.connect(DB, read_only=True) # ─── 1. Controversy trend (bar chart, 2019-2026, quarterly) ────────────────── rows = con.execute(""" SELECT YEAR(date) || '-Q' || QUARTER(date) as wid, YEAR(date) as yr, QUARTER(date) as q, COUNT(*) as n, ROUND(AVG(controversy_score), 3) as avg_c, COUNT(*) FILTER (WHERE controversy_score >= 0.7) as high_c FROM motions WHERE controversy_score IS NOT NULL AND date >= '2019-01-01' AND date < '2026-04-01' GROUP BY wid, yr, q ORDER BY yr, q """).fetchall() windows = [r[0] for r in rows] avg_c = [r[4] for r in rows] high_pct = [round(100.0 * r[5] / r[3], 1) if r[3] else 0 for r in rows] fig = go.Figure() fig.add_trace( go.Bar( x=windows, y=high_pct, name="% highly contested (score ≥ 0.7)", marker_color="#00d9a3", opacity=0.85, ) ) fig.add_trace( go.Scatter( x=windows, y=[v * 100 for v in avg_c], name="avg controversy × 100", mode="lines+markers", line=dict(color="#e6edf3", width=2), marker=dict(size=4), ) ) fig.update_layout( title="Political controversy per quarter (Tweede Kamer, 2019–2026)", xaxis_title="Quarter", yaxis_title="% of motions", plot_bgcolor="#161b22", paper_bgcolor="#0d1117", font=dict(color="#e6edf3", family="Inter, system-ui"), legend=dict(bgcolor="rgba(0,0,0,0)", bordercolor="#30363d", borderwidth=1), xaxis=dict(tickangle=-45, gridcolor="#30363d"), yaxis=dict(gridcolor="#30363d", range=[0, 55]), bargap=0.15, ) out1 = os.path.join(OUT, "controversy_trend.html") fig.write_html(out1, include_plotlyjs="cdn", full_html=True) print(f"Wrote {out1}") # ─── 2. Party alignment heatmap ────────────────────────────────────────────── # Only include major parties with sufficient data MAJOR = [ "VVD", "PVV", "D66", "CDA", "PvdA", "GroenLinks", "SP", "ChristenUnie", "SGP", "FVD", "BBB", "PvdD", "Volt", "GroenLinks-PvdA", "Nieuw Sociaal Contract", "DENK", "JA21", ] rows = con.execute(""" WITH pv AS ( SELECT motion_id, party, CASE WHEN SUM(CASE WHEN vote='voor' THEN 1 ELSE 0 END) > SUM(CASE WHEN vote='tegen' THEN 1 ELSE 0 END) THEN 'voor' WHEN SUM(CASE WHEN vote='tegen' THEN 1 ELSE 0 END) > SUM(CASE WHEN vote='voor' THEN 1 ELSE 0 END) THEN 'tegen' ELSE 'split' END as pv FROM mp_votes WHERE party IS NOT NULL AND vote IN ('voor','tegen') GROUP BY motion_id, party ), d AS (SELECT * FROM pv WHERE pv != 'split') SELECT a.party, b.party, COUNT(*) as shared, ROUND(100.0 * SUM(CASE WHEN a.pv = b.pv THEN 1 ELSE 0 END) / COUNT(*), 1) as pct FROM d a JOIN d b ON a.motion_id = b.motion_id AND a.party != b.party GROUP BY a.party, b.party HAVING COUNT(*) >= 100 """).fetchall() # Build matrix agree = {} for a, b, _, pct in rows: agree[(a, b)] = pct # Filter to parties that have data present = set() for a, b in agree: if a in MAJOR: present.add(a) if b in MAJOR: present.add(b) parties = [p for p in MAJOR if p in present] n = len(parties) matrix = np.full((n, n), np.nan) for i, a in enumerate(parties): matrix[i, i] = 100.0 for j, b in enumerate(parties): if i != j and (a, b) in agree: matrix[i, j] = agree[(a, b)] fig2 = go.Figure( data=go.Heatmap( z=matrix, x=parties, y=parties, colorscale=[[0, "#6e40c9"], [0.5, "#30363d"], [1, "#00d9a3"]], zmid=70, zmin=35, zmax=100, text=[[f"{v:.0f}%" if not np.isnan(v) else "" for v in row] for row in matrix], texttemplate="%{text}", textfont=dict(size=9), hoverongaps=False, showscale=True, colorbar=dict(title="Agreement %", tickfont=dict(color="#e6edf3")), ) ) fig2.update_layout( title="Cross-party vote alignment (all years combined)", plot_bgcolor="#161b22", paper_bgcolor="#0d1117", font=dict(color="#e6edf3", family="Inter, system-ui", size=11), xaxis=dict(tickangle=-45, side="bottom", gridcolor="#30363d"), yaxis=dict(autorange="reversed", gridcolor="#30363d"), height=600, ) out2 = os.path.join(OUT, "party_alignment.html") fig2.write_html(out2, include_plotlyjs="cdn", full_html=True) print(f"Wrote {out2}") con.close() print("Done.")