feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
{% extends "v2/base.html" %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block title %}Auswertungen — GWÖ-Antragsprüfer{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
{% set v2_active_nav = "auswertungen" %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block head_extra %}
|
|
|
|
|
|
<script src="/static/chart.umd.min.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.auswert-tabs {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
border-bottom: 2px solid var(--ecg-border);
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.auswert-tab {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--ecg-dark);
|
|
|
|
|
|
opacity: 0.55;
|
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
|
margin-bottom: -2px;
|
|
|
|
|
|
transition: opacity 0.1s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.auswert-tab:hover { opacity: 0.85; }
|
|
|
|
|
|
.auswert-tab.active {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
border-bottom-color: var(--ecg-teal);
|
|
|
|
|
|
color: var(--ecg-teal);
|
|
|
|
|
|
}
|
|
|
|
|
|
.auswert-panel { display: none; }
|
|
|
|
|
|
.auswert-panel.active { display: block; }
|
|
|
|
|
|
.controls-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls-bar select {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
padding: 5px 8px;
|
|
|
|
|
|
border: 1px solid var(--ecg-border);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
background: var(--ecg-card-bg);
|
|
|
|
|
|
color: var(--ecg-dark);
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls-bar button {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
padding: 5px 12px;
|
|
|
|
|
|
border: 1px solid var(--ecg-border);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
background: var(--ecg-card-bg);
|
|
|
|
|
|
color: var(--ecg-dark);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls-bar button.primary {
|
|
|
|
|
|
background: var(--ecg-teal);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
border-color: var(--ecg-teal);
|
|
|
|
|
|
}
|
|
|
|
|
|
.matrix-wrap {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
background: var(--ecg-card-bg);
|
|
|
|
|
|
border: 1px solid var(--ecg-border);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
table.gwoe-matrix {
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
min-width: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
table.gwoe-matrix th, table.gwoe-matrix td {
|
|
|
|
|
|
border: 1px solid var(--ecg-border);
|
|
|
|
|
|
padding: 5px 8px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
table.gwoe-matrix th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
|
|
|
|
|
table.gwoe-matrix th.row-h { text-align: left; }
|
|
|
|
|
|
table.gwoe-matrix .s-high { background: rgba(136,158,51,0.22); font-weight: 700; }
|
|
|
|
|
|
table.gwoe-matrix .s-mid { background: rgba(247,148,29,0.15); }
|
|
|
|
|
|
table.gwoe-matrix .s-low { background: rgba(200,0,0,0.13); font-weight: 700; }
|
|
|
|
|
|
table.gwoe-matrix .empty { color: var(--ecg-dark); opacity: 0.3; }
|
|
|
|
|
|
table.gwoe-matrix td.clickable { cursor: pointer; }
|
|
|
|
|
|
table.gwoe-matrix td.clickable:hover { background: rgba(0,157,165,0.1); }
|
|
|
|
|
|
.meta-line {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* Modal */
|
|
|
|
|
|
.v2-modal-backdrop {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(0,0,0,0.45);
|
|
|
|
|
|
z-index: 500;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.v2-modal-backdrop.show { display: flex; }
|
|
|
|
|
|
.v2-modal {
|
|
|
|
|
|
background: var(--ecg-card-bg);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
max-width: 580px;
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
max-height: 80vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
.v2-modal h2 {
|
|
|
|
|
|
font-family: var(--font-display);
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: var(--ecg-teal);
|
|
|
|
|
|
margin: 0 0 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.v2-modal-close {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 12px;
|
|
|
|
|
|
right: 14px;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--ecg-dark);
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.v2-modal-close:hover { opacity: 1; }
|
|
|
|
|
|
table.modal-table {
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
table.modal-table th, table.modal-table td {
|
|
|
|
|
|
border: 1px solid var(--ecg-border);
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block main %}
|
|
|
|
|
|
<div style="padding:0 0 1.5rem;">
|
|
|
|
|
|
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">Auswertungen</h1>
|
|
|
|
|
|
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
Bundesland × Partei · Thema × Fraktion · Stimmverhalten · Cluster
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Tabs -->
|
|
|
|
|
|
<div class="auswert-tabs" role="tablist">
|
|
|
|
|
|
<button class="auswert-tab active" role="tab" onclick="switchTab('bl-partei', this)">BL × Partei</button>
|
|
|
|
|
|
<button class="auswert-tab" role="tab" onclick="switchTab('themen', this)">Thema × Fraktion</button>
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
<button class="auswert-tab" role="tab" onclick="switchTab('stimmverhalten', this)">Stimmverhalten</button>
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
<button class="auswert-tab" role="tab" onclick="switchTab('cluster', this)">Cluster</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Panel 1: BL × Partei -->
|
|
|
|
|
|
<div class="auswert-panel active" id="panel-bl-partei">
|
|
|
|
|
|
<div class="controls-bar">
|
|
|
|
|
|
<label for="wp-filter">Wahlperiode:</label>
|
|
|
|
|
|
<select id="wp-filter">
|
|
|
|
|
|
<option value="">— alle WPs —</option>
|
|
|
|
|
|
{% for wp in wahlperioden %}
|
|
|
|
|
|
<option value="{{ wp }}">{{ wp }}</option>
|
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button class="primary" onclick="loadBlMatrix()">Laden</button>
|
|
|
|
|
|
<button onclick="window.location.href='/api/auswertungen/export.csv'">CSV-Export</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="bl-matrix-wrap" class="matrix-wrap">
|
|
|
|
|
|
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Matrix …</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="bl-matrix-meta" class="meta-line"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Panel 2: Thema × Fraktion -->
|
|
|
|
|
|
<div class="auswert-panel" id="panel-themen">
|
|
|
|
|
|
<div class="controls-bar">
|
|
|
|
|
|
<div id="themen-matrix-wrap" class="matrix-wrap">
|
|
|
|
|
|
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Wählen Sie den Tab „Thema × Fraktion".</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
<!-- Panel 3: Stimmverhalten × Gemeinwohl -->
|
|
|
|
|
|
<div class="auswert-panel" id="panel-stimmverhalten">
|
|
|
|
|
|
<div class="v2-kasten outline-blue" style="margin-bottom:1rem;">
|
|
|
|
|
|
<h4 style="margin-top:0;">Stimmverhalten × Gemeinwohl-Orientierung</h4>
|
|
|
|
|
|
<p style="font-size:12px;line-height:1.5;">
|
|
|
|
|
|
Verschneidung von <strong>GWÖ-Bewertung pro Antrag</strong> mit dem
|
|
|
|
|
|
tatsächlichen <strong>Plenum-Stimmverhalten der Fraktionen</strong>.
|
|
|
|
|
|
Beantwortet die Frage: Welche Fraktion stimmt häufiger Anträgen mit
|
|
|
|
|
|
hoher Gemeinwohl-Bewertung zu, welche lehnt sie eher ab?
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p style="font-size:11px;line-height:1.5;opacity:0.8;">
|
|
|
|
|
|
<strong>Datenbasis:</strong> Anträge, die sowohl eine GWÖ-Bewertung
|
|
|
|
|
|
<em>als auch</em> einen Plenum-Vote haben. Heute noch dünn — wächst
|
|
|
|
|
|
mit der Anzahl Bewertungen.
|
|
|
|
|
|
<strong>Eigene Anträge sind per Default ausgeschlossen</strong>, weil
|
|
|
|
|
|
Antragsteller-Fraktionen quasi immer „ja" stimmen — das würde den Index
|
|
|
|
|
|
verzerren.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<label style="display:inline-flex;align-items:center;gap:6px;font-size:11px;
|
|
|
|
|
|
font-family:var(--font-mono);margin-top:4px;cursor:pointer;">
|
|
|
|
|
|
<input type="checkbox" id="sv-exclude-antragsteller" checked
|
|
|
|
|
|
onchange="loadStimmverhalten()" />
|
|
|
|
|
|
Eigene Anträge ausschließen
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Sub 1: Stimm-Index Bar Chart -->
|
|
|
|
|
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
|
|
|
|
|
margin:1.5rem 0 0.5rem;">1. Gemeinwohl-Stimm-Index pro Fraktion</h3>
|
|
|
|
|
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
|
|
|
|
|
Ø-GWÖ-Score der <em>Ja</em>-Stimmen <strong>minus</strong> Ø-GWÖ-Score der
|
|
|
|
|
|
<em>Nein</em>-Stimmen. Positiv = stimmt eher Gemeinwohl-affinen Anträgen zu.
|
|
|
|
|
|
Domain: −10..+10.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="matrix-wrap" style="padding:14px;">
|
|
|
|
|
|
<canvas id="sv-index-chart" style="max-height:380px;"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="sv-index-meta" class="meta-line"></div>
|
|
|
|
|
|
<div id="sv-index-insufficient" style="font-size:11px;font-family:var(--font-mono);
|
|
|
|
|
|
opacity:0.6;margin-top:6px;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Sub 2: Heuchelei-Score Bar Chart -->
|
|
|
|
|
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
|
|
|
|
|
margin:1.5rem 0 0.5rem;">2. Heuchelei-Quote pro Fraktion</h3>
|
|
|
|
|
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
|
|
|
|
|
Anteil der Anträge mit <strong>Wahlprogramm-Treue ≥ 7/10</strong>
|
|
|
|
|
|
(passt inhaltlich zum Wahlprogramm der Fraktion), bei denen die Fraktion
|
|
|
|
|
|
trotzdem <em>Nein</em> gestimmt hat. Hohe Werte = häufige Inkonsistenz
|
|
|
|
|
|
zwischen Wahlversprechen und Stimmverhalten.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="matrix-wrap" style="padding:14px;">
|
|
|
|
|
|
<canvas id="sv-heuchelei-chart" style="max-height:380px;"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="sv-heuchelei-meta" class="meta-line"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Sub 3: Pro GWÖ-Wert Heatmap -->
|
|
|
|
|
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
|
|
|
|
|
margin:1.5rem 0 0.5rem;">3. Stimm-Index pro GWÖ-Wert</h3>
|
|
|
|
|
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
|
|
|
|
|
Aufschlüsselung nach den fünf GWÖ-Werten (Würde, Solidarität,
|
|
|
|
|
|
Nachhaltigkeit, Gerechtigkeit, Demokratie). Pro Zelle: Stimm-Index analog
|
|
|
|
|
|
Aussage 1, aber gegen den Wert-Score statt den Gesamt-Score. Domain
|
|
|
|
|
|
pro Zelle: −5..+5.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div id="sv-wert-heatmap" class="matrix-wrap"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Sub 4: Cross-BL Grouped Bar -->
|
|
|
|
|
|
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
|
|
|
|
|
|
margin:1.5rem 0 0.5rem;">4. Stimm-Index pro Bundesland (Cross-BL)</h3>
|
|
|
|
|
|
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
|
|
|
|
|
|
Gleiche Fraktion in mehreren Ländern. Macht regionale Drift sichtbar
|
|
|
|
|
|
— gleiche Partei, unterschiedlicher Stimm-Index? Nur Fraktionen in ≥2 BL
|
|
|
|
|
|
mit ausreichender Datenbasis.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="matrix-wrap" style="padding:14px;">
|
|
|
|
|
|
<canvas id="sv-cross-bl-chart" style="max-height:380px;"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="sv-cross-bl-meta" class="meta-line"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Panel 4: Cluster-Link -->
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
<div class="auswert-panel" id="panel-cluster">
|
|
|
|
|
|
<div class="v2-kasten outline-blue">
|
|
|
|
|
|
<h4>Cluster-Ansicht</h4>
|
|
|
|
|
|
<p style="font-size:12px;">
|
|
|
|
|
|
Die interaktive Cluster-Übersicht finden Sie unter
|
|
|
|
|
|
<a href="/v2/cluster" style="color:var(--ecg-teal);">/v2/cluster</a>.
|
|
|
|
|
|
Sie zeigt thematisch ähnliche Anträge als redaktionelle Liste und verlinkt
|
|
|
|
|
|
zur Force-Graph-Visualisierung.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<a href="/v2/cluster"
|
|
|
|
|
|
style="display:inline-block;margin-top:8px;font-family:var(--font-mono);font-size:11px;
|
|
|
|
|
|
padding:6px 14px;background:var(--ecg-teal);color:#fff;border-radius:3px;text-decoration:none;">
|
|
|
|
|
|
Zur Cluster-Ansicht →
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Zeitreihen-Modal -->
|
|
|
|
|
|
<div class="v2-modal-backdrop" id="modal-backdrop" onclick="closeModal(event)">
|
|
|
|
|
|
<div class="v2-modal" onclick="event.stopPropagation()">
|
|
|
|
|
|
<button class="v2-modal-close" onclick="closeModal()">×</button>
|
|
|
|
|
|
<h2 id="modal-title">Zeitreihe</h2>
|
|
|
|
|
|
<div id="modal-body" style="font-size:12px;">Lade …</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block body_scripts %}
|
|
|
|
|
|
<script>
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
let _tabLoaded = { 'bl-partei': false, 'themen': false, 'stimmverhalten': false };
|
|
|
|
|
|
let _svCharts = { index: null, heuchelei: null, crossBl: null };
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
|
|
|
|
|
|
function switchTab(id, btn) {
|
|
|
|
|
|
document.querySelectorAll('.auswert-tab').forEach(b => b.classList.remove('active'));
|
|
|
|
|
|
document.querySelectorAll('.auswert-panel').forEach(p => p.classList.remove('active'));
|
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
|
document.getElementById('panel-' + id).classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
if (id === 'themen' && !_tabLoaded.themen) {
|
|
|
|
|
|
loadThemenMatrix();
|
|
|
|
|
|
_tabLoaded.themen = true;
|
|
|
|
|
|
}
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
if (id === 'stimmverhalten' && !_tabLoaded.stimmverhalten) {
|
|
|
|
|
|
loadStimmverhalten();
|
|
|
|
|
|
_tabLoaded.stimmverhalten = true;
|
|
|
|
|
|
}
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 21:50:36 +02:00
|
|
|
|
// Bei BL-Wechsel aktive Panels neu laden
|
|
|
|
|
|
window.addEventListener('v2-bl-changed', function () {
|
|
|
|
|
|
var activePanel = document.querySelector('.auswert-panel.active');
|
|
|
|
|
|
if (!activePanel) return;
|
|
|
|
|
|
if (activePanel.id === 'panel-bl-partei') loadBlMatrix();
|
|
|
|
|
|
if (activePanel.id === 'panel-themen') loadThemenMatrix();
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
if (activePanel.id === 'panel-stimmverhalten') loadStimmverhalten();
|
2026-04-25 21:50:36 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () { loadBlMatrix(); });
|
|
|
|
|
|
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
function scoreClass(avg) {
|
|
|
|
|
|
if (avg == null) return '';
|
|
|
|
|
|
if (avg >= 6) return 's-high';
|
|
|
|
|
|
if (avg >= 3) return 's-mid';
|
|
|
|
|
|
return 's-low';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadBlMatrix() {
|
|
|
|
|
|
const wrap = document.getElementById('bl-matrix-wrap');
|
|
|
|
|
|
const metaEl = document.getElementById('bl-matrix-meta');
|
|
|
|
|
|
const wp = document.getElementById('wp-filter').value;
|
2026-04-25 21:50:36 +02:00
|
|
|
|
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
|
|
|
|
|
const bl = (blRaw === 'ALL') ? '' : blRaw;
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
|
|
|
|
|
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Matrix …</div>';
|
|
|
|
|
|
metaEl.textContent = '';
|
|
|
|
|
|
|
|
|
|
|
|
let url = '/api/auswertungen/matrix';
|
|
|
|
|
|
const params = [];
|
|
|
|
|
|
if (wp) params.push('wahlperiode=' + encodeURIComponent(wp));
|
|
|
|
|
|
if (bl) params.push('bundesland=' + encodeURIComponent(bl));
|
|
|
|
|
|
if (params.length) url += '?' + params.join('&');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(url);
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
if (!data.bundeslaender || !data.bundeslaender.length) {
|
|
|
|
|
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Keine Assessments für diesen Filter.</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<table class="gwoe-matrix"><thead><tr><th class="row-h">Bundesland</th>';
|
|
|
|
|
|
for (const partei of data.parteien) html += `<th>${partei}</th>`;
|
|
|
|
|
|
html += '</tr></thead><tbody>';
|
|
|
|
|
|
|
|
|
|
|
|
for (const bundesland of data.bundeslaender) {
|
|
|
|
|
|
html += `<tr><th class="row-h">${bundesland}</th>`;
|
|
|
|
|
|
for (const partei of data.parteien) {
|
|
|
|
|
|
const cell = (data.cells[bundesland] || {})[partei];
|
|
|
|
|
|
if (cell) {
|
|
|
|
|
|
html += `<td class="clickable ${scoreClass(cell.avg)}"
|
|
|
|
|
|
onclick="showZeitreihe('${bundesland.replace(/'/g,"\\'")}','${partei.replace(/'/g,"\\'")}')">
|
|
|
|
|
|
${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small>
|
|
|
|
|
|
</td>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += '<td class="empty">—</td>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</tr>';
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</tbody></table>';
|
|
|
|
|
|
wrap.innerHTML = html;
|
|
|
|
|
|
metaEl.textContent = `${data.total} Assessments | Filter: ${data.filter_wp || 'alle WPs'}`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
wrap.innerHTML = `<div style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadThemenMatrix() {
|
2026-04-25 21:50:36 +02:00
|
|
|
|
const wrap = document.getElementById('themen-matrix-wrap');
|
|
|
|
|
|
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
|
|
|
|
|
const bl = (blRaw === 'ALL') ? '' : blRaw;
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Themen-Matrix …</div>';
|
|
|
|
|
|
|
|
|
|
|
|
let url = '/api/auswertungen/themen-matrix';
|
|
|
|
|
|
if (bl) url += '?bundesland=' + encodeURIComponent(bl);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(url);
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
if (!data.themen || !data.themen.length) {
|
|
|
|
|
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Noch zu wenige Assessments für Themen-Analyse.</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<table class="gwoe-matrix"><thead><tr><th class="row-h">Thema</th>';
|
|
|
|
|
|
for (const frak of data.fraktionen) html += `<th>${frak}</th>`;
|
|
|
|
|
|
html += '</tr></thead><tbody>';
|
|
|
|
|
|
|
|
|
|
|
|
for (const thema of data.themen) {
|
|
|
|
|
|
html += `<tr><th class="row-h">${thema}</th>`;
|
|
|
|
|
|
for (const frak of data.fraktionen) {
|
|
|
|
|
|
const cell = (data.cells[thema] || {})[frak];
|
|
|
|
|
|
if (cell) {
|
|
|
|
|
|
html += `<td class="${scoreClass(cell.avg)}" title="${thema} × ${frak}: Ø ${cell.avg}/10 (${cell.n} Anträge)">
|
|
|
|
|
|
${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small>
|
|
|
|
|
|
</td>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += '<td class="empty">—</td>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</tr>';
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</tbody></table>';
|
|
|
|
|
|
wrap.innerHTML = html;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
wrap.innerHTML = `<div style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 style="font-family:var(--font-mono);font-size:12px;opacity:0.6;">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="font-family:var(--font-mono);font-size:12px;opacity:0.6;">Keine Daten für diese Kombination.</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body.innerHTML =
|
|
|
|
|
|
'<canvas id="zeitreihe-chart" style="max-height:260px;margin-bottom:1rem;"></canvas>' +
|
|
|
|
|
|
'<table class="modal-table"><thead><tr><th>Wahlperiode</th><th>Anträge</th><th>Ø GWÖ-Score</th></tr></thead><tbody>' +
|
|
|
|
|
|
z.wahlperioden.map(row => `<tr><td>${row.wp}</td><td>${row.n}</td><td><strong>${row.avg.toFixed(2)}</strong></td></tr>`).join('') +
|
|
|
|
|
|
'</tbody></table>';
|
|
|
|
|
|
|
|
|
|
|
|
if (window.Chart) {
|
|
|
|
|
|
const ctx = document.getElementById('zeitreihe-chart');
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: z.wahlperioden.map(r => 'WP ' + r.wp),
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: `Ø GWÖ-Score ${partei} (${bundesland})`,
|
|
|
|
|
|
data: z.wahlperioden.map(r => r.avg),
|
|
|
|
|
|
borderColor: '#009da5',
|
|
|
|
|
|
backgroundColor: 'rgba(0,157,165,0.1)',
|
|
|
|
|
|
fill: true,
|
|
|
|
|
|
tension: 0.3,
|
|
|
|
|
|
pointRadius: 5,
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: { min: 0, max: 10, title: { display: true, text: 'GWÖ-Score' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
callbacks: {
|
|
|
|
|
|
afterLabel: (ctx) => `n=${z.wahlperioden[ctx.dataIndex].n} Anträge`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
body.innerHTML = `<p style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</p>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeModal(ev) {
|
|
|
|
|
|
if (!ev || ev.target.id === 'modal-backdrop') {
|
|
|
|
|
|
document.getElementById('modal-backdrop').classList.remove('show');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
|
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('show');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:30:02 +02:00
|
|
|
|
// ─── Stimmverhalten × Gemeinwohl ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
async function loadStimmverhalten() {
|
|
|
|
|
|
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
|
|
|
|
|
const bl = (blRaw === 'ALL') ? '' : blRaw;
|
|
|
|
|
|
const exclude = document.getElementById('sv-exclude-antragsteller').checked ? '1' : '0';
|
|
|
|
|
|
|
|
|
|
|
|
loadStimmIndex(bl, exclude);
|
|
|
|
|
|
loadHeuchelei(bl);
|
|
|
|
|
|
loadStimmIndexProWert(bl, exclude);
|
|
|
|
|
|
loadStimmIndexCrossBl(exclude);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function svColor(idx) {
|
|
|
|
|
|
// Diverging color: positiv = teal/grün, negativ = rot, null = grau
|
|
|
|
|
|
if (idx == null) return 'rgba(120,120,120,0.4)';
|
|
|
|
|
|
if (idx >= 0) return `rgba(0,157,165,${Math.min(0.85, 0.3 + Math.abs(idx) / 10)})`;
|
|
|
|
|
|
return `rgba(200,30,30,${Math.min(0.85, 0.3 + Math.abs(idx) / 10)})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadStimmIndex(bl, exclude) {
|
|
|
|
|
|
const meta = document.getElementById('sv-index-meta');
|
|
|
|
|
|
const insufficientEl = document.getElementById('sv-index-insufficient');
|
|
|
|
|
|
|
|
|
|
|
|
let url = `/api/auswertungen/stimm-index?exclude_antragsteller=${exclude}&min_n=5`;
|
|
|
|
|
|
if (bl) url += '&bundesland=' + encodeURIComponent(bl);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(url);
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
if (_svCharts.index) _svCharts.index.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
const ausreichend = data.fraktionen.filter(f => f.ausreichend && f.stimm_index != null);
|
|
|
|
|
|
const nicht = data.fraktionen.filter(f => !f.ausreichend);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ausreichend.length) {
|
|
|
|
|
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge — keine Fraktion erreicht das Mindest-N (5 Ja UND 5 Nein).`;
|
|
|
|
|
|
insufficientEl.textContent = '';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = document.getElementById('sv-index-chart');
|
|
|
|
|
|
_svCharts.index = new Chart(ctx, {
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: ausreichend.map(f => f.partei),
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: 'Stimm-Index',
|
|
|
|
|
|
data: ausreichend.map(f => f.stimm_index),
|
|
|
|
|
|
backgroundColor: ausreichend.map(f => svColor(f.stimm_index)),
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
indexAxis: 'y',
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
x: { min: -10, max: 10, title: { display: true, text: 'Stimm-Index (−10..+10)' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: { display: false },
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
callbacks: {
|
|
|
|
|
|
afterLabel: (ctx) => {
|
|
|
|
|
|
const f = ausreichend[ctx.dataIndex];
|
|
|
|
|
|
return [
|
|
|
|
|
|
`Ø GWÖ Ja: ${f.avg_gwoe_ja}/10 (n=${f.n_ja})`,
|
|
|
|
|
|
`Ø GWÖ Nein: ${f.avg_gwoe_nein}/10 (n=${f.n_nein})`,
|
|
|
|
|
|
`Enthaltung: n=${f.n_enth}`,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge mit GWÖ-Bewertung × Plenum-Vote.`;
|
|
|
|
|
|
if (nicht.length) {
|
|
|
|
|
|
insufficientEl.innerHTML = '<strong>Nicht aussagekräftig (N<5):</strong> '
|
|
|
|
|
|
+ nicht.map(f => `${f.partei} (n_ja=${f.n_ja}, n_nein=${f.n_nein})`).join(' · ');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
insufficientEl.textContent = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
meta.textContent = 'Fehler: ' + e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadHeuchelei(bl) {
|
|
|
|
|
|
const meta = document.getElementById('sv-heuchelei-meta');
|
|
|
|
|
|
|
|
|
|
|
|
let url = '/api/auswertungen/heuchelei?score_threshold=7&min_n=5';
|
|
|
|
|
|
if (bl) url += '&bundesland=' + encodeURIComponent(bl);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(url);
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
if (_svCharts.heuchelei) _svCharts.heuchelei.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
const filtered = data.fraktionen.filter(f => f.ausreichend && f.heuchelei_quote != null);
|
|
|
|
|
|
if (!filtered.length) {
|
|
|
|
|
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge — zu wenige Anträge mit Wahlprogramm-Treue ≥ 7.`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = document.getElementById('sv-heuchelei-chart');
|
|
|
|
|
|
_svCharts.heuchelei = new Chart(ctx, {
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: filtered.map(f => f.partei),
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: 'Heuchelei-Quote',
|
|
|
|
|
|
data: filtered.map(f => Math.round(f.heuchelei_quote * 1000) / 10),
|
|
|
|
|
|
backgroundColor: filtered.map(f => `rgba(200,30,30,${Math.min(0.85, 0.25 + f.heuchelei_quote)})`),
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
indexAxis: 'y',
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
x: { min: 0, max: 100, title: { display: true, text: 'Heuchelei-Quote (%)' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: { display: false },
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
callbacks: {
|
|
|
|
|
|
afterLabel: (ctx) => {
|
|
|
|
|
|
const f = filtered[ctx.dataIndex];
|
|
|
|
|
|
return [
|
|
|
|
|
|
`Anträge passend zum Programm: ${f.n_im_programm}`,
|
|
|
|
|
|
`davon Nein gestimmt: ${f.n_nein_trotz_programm}`,
|
|
|
|
|
|
`davon Ja gestimmt: ${f.n_ja_passt}`,
|
|
|
|
|
|
`davon Enthaltung: ${f.n_enth_passt}`,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge — Threshold: Wahlprogramm-Treue ≥ 7/10.`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
meta.textContent = 'Fehler: ' + e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function wertHeatColor(idx) {
|
|
|
|
|
|
// Wert-Spalten haben Domain -5..+5 (rating-Skala der Matrix)
|
|
|
|
|
|
if (idx == null) return 'rgba(120,120,120,0.1)';
|
|
|
|
|
|
if (idx >= 0) return `rgba(136,158,51,${Math.min(0.7, 0.15 + Math.abs(idx) / 5)})`;
|
|
|
|
|
|
return `rgba(200,0,0,${Math.min(0.7, 0.15 + Math.abs(idx) / 5)})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadStimmIndexProWert(bl, exclude) {
|
|
|
|
|
|
const wrap = document.getElementById('sv-wert-heatmap');
|
|
|
|
|
|
let url = `/api/auswertungen/stimm-index-pro-wert?exclude_antragsteller=${exclude}&min_n=3`;
|
|
|
|
|
|
if (bl) url += '&bundesland=' + encodeURIComponent(bl);
|
|
|
|
|
|
|
|
|
|
|
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Heatmap …</div>';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(url);
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (!data.fraktionen.length) {
|
|
|
|
|
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Keine Daten für diesen Filter.</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<table class="gwoe-matrix"><thead><tr><th class="row-h">Fraktion</th>';
|
|
|
|
|
|
for (const w of data.werte) html += `<th>${w}</th>`;
|
|
|
|
|
|
html += '</tr></thead><tbody>';
|
|
|
|
|
|
|
|
|
|
|
|
for (const partei of data.fraktionen) {
|
|
|
|
|
|
html += `<tr><th class="row-h">${partei}</th>`;
|
|
|
|
|
|
for (const wert of data.werte) {
|
|
|
|
|
|
const cell = (data.cells[partei] || {})[wert];
|
|
|
|
|
|
if (cell && cell.stimm_index != null && cell.ausreichend) {
|
|
|
|
|
|
const color = wertHeatColor(cell.stimm_index);
|
|
|
|
|
|
const sign = cell.stimm_index >= 0 ? '+' : '';
|
|
|
|
|
|
html += `<td style="background:${color};" title="${partei} × ${wert}: Index ${sign}${cell.stimm_index} (n_ja=${cell.n_ja}, n_nein=${cell.n_nein})">
|
|
|
|
|
|
${sign}${cell.stimm_index.toFixed(1)}
|
|
|
|
|
|
</td>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
html += '<td class="empty">—</td>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</tr>';
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</tbody></table>';
|
|
|
|
|
|
html += `<div class="meta-line">Datenbasis: ${data.n_assessments_matched} Anträge.
|
|
|
|
|
|
Mindest-N pro Zelle: 3. Skala −5..+5 (Wert-Score-Differenz Ja minus Nein).</div>`;
|
|
|
|
|
|
wrap.innerHTML = html;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
wrap.innerHTML = `<div style="color:#c00;font-family:var(--font-mono);font-size:12px;">Fehler: ${e}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadStimmIndexCrossBl(exclude) {
|
|
|
|
|
|
const meta = document.getElementById('sv-cross-bl-meta');
|
|
|
|
|
|
let url = `/api/auswertungen/stimm-index-cross-bl?exclude_antragsteller=${exclude}&min_n=3`;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(url);
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
if (_svCharts.crossBl) _svCharts.crossBl.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
if (!data.fraktionen.length) {
|
|
|
|
|
|
meta.textContent = `Keine Fraktion in ≥2 Bundesländern mit ausreichender Datenbasis.`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Datasets: pro Bundesland eine Bar-Reihe
|
|
|
|
|
|
const datasets = data.bundeslaender.map((bl, i) => {
|
|
|
|
|
|
const colors = ['rgba(0,157,165,0.7)', 'rgba(247,148,29,0.7)', 'rgba(136,158,51,0.7)',
|
|
|
|
|
|
'rgba(200,30,30,0.7)', 'rgba(150,100,200,0.7)', 'rgba(80,80,80,0.7)',
|
|
|
|
|
|
'rgba(50,150,50,0.7)', 'rgba(220,180,20,0.7)'];
|
|
|
|
|
|
return {
|
|
|
|
|
|
label: bl,
|
|
|
|
|
|
data: data.fraktionen.map(p => {
|
|
|
|
|
|
const cell = (data.cells[p] || {})[bl];
|
|
|
|
|
|
return (cell && cell.ausreichend) ? cell.stimm_index : null;
|
|
|
|
|
|
}),
|
|
|
|
|
|
backgroundColor: colors[i % colors.length],
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = document.getElementById('sv-cross-bl-chart');
|
|
|
|
|
|
_svCharts.crossBl = new Chart(ctx, {
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: { labels: data.fraktionen, datasets: datasets },
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: { min: -10, max: 10, title: { display: true, text: 'Stimm-Index (−10..+10)' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
callbacks: {
|
|
|
|
|
|
afterLabel: (ctx) => {
|
|
|
|
|
|
const partei = data.fraktionen[ctx.dataIndex];
|
|
|
|
|
|
const bl = ctx.dataset.label;
|
|
|
|
|
|
const cell = (data.cells[partei] || {})[bl];
|
|
|
|
|
|
if (!cell) return '';
|
|
|
|
|
|
return `n_ja=${cell.n_ja}, n_nein=${cell.n_nein}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge. ${data.fraktionen.length} Fraktion(en) in ≥2 BL mit n≥3 ja UND n≥3 nein.`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
meta.textContent = 'Fehler: ' + e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
(ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze
Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
/api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie
Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00
|
|
|
|
// Load BL-Matrix on init
|
|
|
|
|
|
loadBlMatrix();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
{% endblock %}
|