Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
<!DOCTYPE html>
< html lang = "de" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Auswertungen — {{ app_name }}< / title >
< style >
:root {
--color-darkgray: #5a5a5a;
--color-green: #889e33;
--color-blue: #009da5;
--color-lightgray: #bfbfbf;
--color-bg: #f5f5f5;
--color-orange: #F7941D;
--color-red: #d00000;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Avenir', 'Segoe UI', sans-serif;
color: var(--color-darkgray);
line-height: 1.6;
background: var(--color-bg);
}
.header {
background: white;
padding: 1rem 2rem;
border-bottom: 1px solid var(--color-lightgray);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.header h1 { color: var(--color-blue); font-size: 1.3rem; }
.header nav a {
color: var(--color-blue);
text-decoration: none;
margin-right: 1rem;
font-size: 0.9rem;
}
.header nav a:hover { text-decoration: underline; }
main { max-width: 1400px; margin: 1.5rem auto; padding: 0 2rem; }
.controls {
background: white;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.controls label { font-size: 0.9rem; }
.controls select, .controls button {
padding: 0.4rem 0.7rem;
border: 1px solid var(--color-lightgray);
border-radius: 3px;
font-size: 0.9rem;
background: white;
color: var(--color-darkgray);
cursor: pointer;
}
.controls button.export {
background: var(--color-blue);
color: white;
border-color: var(--color-blue);
}
.matrix-wrap { background: white; padding: 1rem; border-radius: 4px; overflow-x: auto; }
table.matrix {
border-collapse: collapse;
width: 100%;
font-size: 0.85rem;
}
table.matrix th, table.matrix td {
border: 1px solid var(--color-lightgray);
padding: 0.4rem 0.6rem;
text-align: center;
}
table.matrix th {
background: var(--color-bg);
font-weight: 600;
color: var(--color-darkgray);
}
table.matrix th.row-header {
background: var(--color-blue);
color: white;
text-align: left;
position: sticky;
left: 0;
}
table.matrix .empty { color: var(--color-lightgray); }
table.matrix .score-high { background: rgba(136, 158, 51, 0.25); font-weight: 600; }
table.matrix .score-mid { background: rgba(247, 148, 29, 0.18); }
table.matrix .score-low { background: rgba(208, 0, 0, 0.18); font-weight: 600; }
.meta {
font-size: 0.8rem;
color: var(--color-lightgray);
margin-top: 0.6rem;
}
.empty-state {
background: white;
padding: 2rem;
text-align: center;
border-radius: 4px;
color: var(--color-lightgray);
}
2026-04-09 11:30:10 +02:00
table.matrix td.cell-with-data {
cursor: pointer;
}
table.matrix td.cell-with-data:hover {
outline: 2px solid var(--color-blue);
outline-offset: -2px;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-backdrop.show { display: flex; }
.modal {
background: white;
border-radius: 6px;
padding: 1.5rem 2rem;
min-width: 320px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.modal h2 {
color: var(--color-blue);
margin-bottom: 0.8rem;
font-size: 1.1rem;
}
.modal table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
}
.modal table th, .modal table td {
padding: 0.4rem 0.8rem;
border-bottom: 1px solid var(--color-bg);
text-align: left;
font-size: 0.85rem;
}
.modal table th { background: var(--color-bg); }
.modal-close {
float: right;
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
color: var(--color-lightgray);
}
.modal-close:hover { color: var(--color-darkgray); }
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
< / style >
< / head >
< body >
< div class = "header" >
< h1 > Auswertungen — Bundesland × Partei × Wahlperiode< / h1 >
< nav >
< a href = "/" > ← zurück zur Suche< / a >
< a href = "/quellen" > Quellen< / a >
< / nav >
< / div >
< main >
< div class = "controls" >
< label > Wahlperiode:
< select id = "wp-filter" >
< option value = "" > — alle WPs —< / option >
{% for wp in wahlperioden %}
< option value = "{{ wp }}" > {{ wp }}< / option >
{% endfor %}
< / select >
< / label >
< button id = "reload" > Anwenden< / button >
< button class = "export" id = "export-csv" > CSV-Export (alle Daten)< / button >
< / div >
< div id = "matrix-container" class = "matrix-wrap" >
< div class = "empty-state" > Lade Matrix …< / div >
< / div >
< div class = "meta" id = "meta" > < / div >
< / main >
2026-04-09 11:30:10 +02:00
<!-- Drill - down modal: Zeitreihe für eine (BL, Partei) - Kombination -->
< div class = "modal-backdrop" id = "modal-backdrop" onclick = "closeModal(event)" >
< div class = "modal" onclick = "event.stopPropagation()" >
< button class = "modal-close" onclick = "closeModal()" > × < / button >
< h2 id = "modal-title" > Zeitreihe< / h2 >
< div id = "modal-body" > Lade …< / div >
< / div >
< / div >
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
< script >
const wpFilter = document.getElementById('wp-filter');
const reloadBtn = document.getElementById('reload');
const exportBtn = document.getElementById('export-csv');
const container = document.getElementById('matrix-container');
const meta = document.getElementById('meta');
function scoreClass(avg) {
if (avg === null || avg === undefined) return '';
if (avg >= 6) return 'score-high';
if (avg >= 3) return 'score-mid';
return 'score-low';
}
async function loadMatrix() {
const wp = wpFilter.value;
const url = wp
? `/api/auswertungen/matrix?wahlperiode=${encodeURIComponent(wp)}`
: '/api/auswertungen/matrix';
container.innerHTML = '< div class = "empty-state" > Lade Matrix …< / div > ';
try {
const r = await fetch(url);
const data = await r.json();
if (!data.bundeslaender.length) {
container.innerHTML = '< div class = "empty-state" > Keine Assessments für diesen Filter.< / div > ';
meta.textContent = '';
return;
}
let html = '< table class = "matrix" > < thead > < tr > < th class = "row-header" > Bundesland< / th > ';
for (const partei of data.parteien) {
html += `< th > ${partei}< / th > `;
}
html += '< / tr > < / thead > < tbody > ';
for (const bl of data.bundeslaender) {
html += `< tr > < th class = "row-header" > ${bl}< / th > `;
for (const partei of data.parteien) {
const cell = (data.cells[bl] || {})[partei];
if (cell) {
2026-04-09 11:30:10 +02:00
// Click → drill-down auf die Zeitreihe für diese Zelle
html += `< td class = "cell-with-data ${scoreClass(cell.avg)}" data-bl = "${bl}" data-partei = "${partei}" onclick = "showZeitreihe(this.dataset.bl, this.dataset.partei)" > ${cell.avg.toFixed(1)}< br > < small > n=${cell.n}< / small > < / td > `;
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
} else {
html += '< td class = "empty" > —< / td > ';
}
}
html += '< / tr > ';
}
html += '< / tbody > < / table > ';
container.innerHTML = html;
meta.textContent = `${data.total} Assessment(s) | Filter: ${data.filter_wp || 'alle WPs'}`;
} catch (e) {
container.innerHTML = `< div class = "empty-state" > Fehler beim Laden: ${e}< / div > `;
}
}
reloadBtn.addEventListener('click', loadMatrix);
wpFilter.addEventListener('change', loadMatrix);
exportBtn.addEventListener('click', () => {
window.location.href = '/api/auswertungen/export.csv';
});
2026-04-09 11:30:10 +02:00
// Zeitreihen-Modal: zeigt die Score-Entwicklung einer (BL, Partei)-
// Kombination über alle bekannten Wahlperioden hinweg.
async function showZeitreihe(bundesland, partei) {
const backdrop = document.getElementById('modal-backdrop');
const title = document.getElementById('modal-title');
const body = document.getElementById('modal-body');
title.textContent = `${bundesland} × ${partei}`;
body.innerHTML = '< p > Lade Zeitreihe …< / p > ';
backdrop.classList.add('show');
try {
const r = await fetch(`/api/auswertungen/zeitreihe?bundesland=${encodeURIComponent(bundesland)}&partei=${encodeURIComponent(partei)}`);
const z = await r.json();
if (!z.wahlperioden || !z.wahlperioden.length) {
body.innerHTML = '< p style = "color:#888;" > Keine Daten für diese Kombination.< / p > ';
return;
}
let html = '< table > < thead > < tr > < th > Wahlperiode< / th > < th > Anträge< / th > < th > Ø GWÖ-Score< / th > < / tr > < / thead > < tbody > ';
for (const row of z.wahlperioden) {
html += `< tr > < td > ${row.wp}< / td > < td > ${row.n}< / td > < td > < strong > ${row.avg.toFixed(2)}< / strong > < / td > < / tr > `;
}
html += '< / tbody > < / table > ';
body.innerHTML = html;
} catch (e) {
body.innerHTML = `< p style = "color:#d00;" > Fehler: ${e}< / p > `;
}
}
function closeModal(ev) {
// Klick aufs Backdrop schließt; Klicks im Modal nicht
if (!ev || ev.target.id === 'modal-backdrop') {
document.getElementById('modal-backdrop').classList.remove('show');
} else if (!ev.target) {
document.getElementById('modal-backdrop').classList.remove('show');
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('show');
});
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
loadMatrix();
< / script >
< / body >
< / html >