gwoe-antragspruefer/app/templates/v2/screens/auswertungen.html

958 lines
36 KiB
HTML
Raw Normal View History

{% 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
</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>
<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>
<div style="display:flex;align-items:center;gap:1rem;margin-top:6px;flex-wrap:wrap;">
<label style="display:inline-flex;align-items:center;gap:6px;font-size:11px;
font-family:var(--font-mono);cursor:pointer;">
<input type="checkbox" id="sv-exclude-antragsteller" checked
onchange="loadStimmverhalten()" />
Eigene Anträge ausschließen
</label>
<button type="button" onclick="downloadStimmverhaltenCsv()"
style="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);cursor:pointer;">
CSV-Export (Long-Format)
</button>
</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
</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 / Pro Berührungsgruppe (Toggle) -->
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
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
margin:1.5rem 0 0.5rem;">3. Stimm-Index pro Matrix-Achse</h3>
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
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
Aufschlüsselung der Stimm-Indizes pro <strong>GWÖ-Wert</strong>
(Spalten: Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit,
Demokratie) oder pro <strong>Berührungsgruppe</strong> (Zeilen AE:
Lieferant:innen, Finanzpartner, Politik, Bürger, Staat). Pro Zelle:
Stimm-Index analog Aussage 1, aber gegen den Achsen-Score statt den
Gesamt-Score. Domain pro Zelle: 5..+5.
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
</p>
<div style="display:inline-flex;gap:4px;margin-bottom:0.5rem;">
<button type="button" id="sv-axis-werte" class="sv-axis-btn active"
onclick="setMatrixAxis('werte')"
style="font-family:var(--font-mono);font-size:11px;padding:5px 12px;
border:1px solid var(--ecg-border);border-radius:3px;
cursor:pointer;background:var(--ecg-teal);color:#fff;">
Pro GWÖ-Wert
</button>
<button type="button" id="sv-axis-gruppen" class="sv-axis-btn"
onclick="setMatrixAxis('gruppen')"
style="font-family:var(--font-mono);font-size:11px;padding:5px 12px;
border:1px solid var(--ecg-border);border-radius:3px;
cursor:pointer;background:var(--ecg-card-bg);color:var(--ecg-dark);">
Pro Berührungsgruppe
</button>
</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
<div id="sv-wert-heatmap" class="matrix-wrap"></div>
<!-- Sub 4: Empfehlungs-Konsistenz Bar Chart -->
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
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
margin:1.5rem 0 0.5rem;">4. Empfehlungs-Konsistenz (gegen GWÖ-Empfehlung)</h3>
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.7;margin:0 0 0.5rem;">
Anteil der Anträge mit GWÖ-Empfehlung „Uneingeschränkt unterstützen" oder
„Unterstützen mit Änderungen", bei denen die Fraktion trotzdem
<em>Nein</em> gestimmt hat. Orthogonal zur Heuchelei-Quote — prüft NICHT
gegen Wahlprogramm-Treue, sondern gegen die GWÖ-Empfehlung des Systems.
</p>
<div class="matrix-wrap" style="padding:14px;">
<canvas id="sv-empfehlung-chart" style="max-height:380px;"></canvas>
</div>
<div id="sv-empfehlung-meta" class="meta-line"></div>
<!-- Sub 5: Cross-BL Grouped Bar -->
<h3 style="font-family:var(--font-display);font-size:15px;color:var(--ecg-teal);
margin:1.5rem 0 0.5rem;">5. Stimm-Index pro Bundesland (Cross-BL)</h3>
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
<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 -->
<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()">&times;</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, empfehlung: null, crossBl: null };
let _svMatrixAxis = 'werte'; // 'werte' or 'gruppen'
function setMatrixAxis(axis) {
_svMatrixAxis = axis;
const werteBtn = document.getElementById('sv-axis-werte');
const gruppenBtn = document.getElementById('sv-axis-gruppen');
if (axis === 'werte') {
werteBtn.style.background = 'var(--ecg-teal)';
werteBtn.style.color = '#fff';
gruppenBtn.style.background = 'var(--ecg-card-bg)';
gruppenBtn.style.color = 'var(--ecg-dark)';
} else {
gruppenBtn.style.background = 'var(--ecg-teal)';
gruppenBtn.style.color = '#fff';
werteBtn.style.background = 'var(--ecg-card-bg)';
werteBtn.style.color = 'var(--ecg-dark)';
}
loadMatrixHeatmap();
}
function loadMatrixHeatmap() {
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const bl = (blRaw === 'ALL') ? '' : blRaw;
const exclude = document.getElementById('sv-exclude-antragsteller').checked ? '1' : '0';
if (_svMatrixAxis === 'gruppen') {
loadStimmIndexProGruppe(bl, exclude);
} else {
loadStimmIndexProWert(bl, exclude);
}
}
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;
}
}
// 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();
});
document.addEventListener('DOMContentLoaded', function () { loadBlMatrix(); });
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;
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const bl = (blRaw === 'ALL') ? '' : blRaw;
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() {
const wrap = document.getElementById('themen-matrix-wrap');
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const bl = (blRaw === 'ALL') ? '' : blRaw;
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);
loadMatrixHeatmap();
loadEmpfehlungsKonsistenz(bl);
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
loadStimmIndexCrossBl(exclude);
}
function downloadStimmverhaltenCsv() {
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
const bl = (blRaw === 'ALL') ? '' : blRaw;
const exclude = document.getElementById('sv-exclude-antragsteller').checked ? '1' : '0';
let url = `/api/auswertungen/stimmverhalten.csv?exclude_antragsteller=${exclude}`;
if (bl) url += '&bundesland=' + encodeURIComponent(bl);
window.location.href = url;
}
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
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&lt;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 loadStimmIndexProGruppe(bl, exclude) {
const wrap = document.getElementById('sv-wert-heatmap');
let url = `/api/auswertungen/stimm-index-pro-gruppe?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 g of data.gruppen) html += `<th style="font-size:10px;max-width:120px;white-space:normal;">${g}</th>`;
html += '</tr></thead><tbody>';
for (const partei of data.fraktionen) {
html += `<tr><th class="row-h">${partei}</th>`;
for (const gruppe of data.gruppen) {
const cell = (data.cells[partei] || {})[gruppe];
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} × ${gruppe}: 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 (Gruppen-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 loadEmpfehlungsKonsistenz(bl) {
const meta = document.getElementById('sv-empfehlung-meta');
let url = '/api/auswertungen/empfehlungs-konsistenz?min_n=3';
if (bl) url += '&bundesland=' + encodeURIComponent(bl);
try {
const r = await fetch(url);
const data = await r.json();
if (_svCharts.empfehlung) _svCharts.empfehlung.destroy();
const filtered = data.fraktionen.filter(f => f.ausreichend && f.konsistenz_quote != null);
if (!filtered.length) {
meta.textContent = `Datenbasis: ${data.n_assessments_matched} positiv-empfohlene Anträge — keine Fraktion erreicht das Mindest-N (3).`;
return;
}
const ctx = document.getElementById('sv-empfehlung-chart');
_svCharts.empfehlung = new Chart(ctx, {
type: 'bar',
data: {
labels: filtered.map(f => f.partei),
datasets: [{
label: 'Inkonsistenz-Quote',
data: filtered.map(f => Math.round(f.konsistenz_quote * 1000) / 10),
backgroundColor: filtered.map(f => `rgba(200,30,30,${Math.min(0.85, 0.25 + f.konsistenz_quote)})`),
}]
},
options: {
indexAxis: 'y',
responsive: true,
scales: {
x: { min: 0, max: 100, title: { display: true, text: 'Inkonsistenz-Quote (%) — NEIN trotz GWÖ-Empfehlung' } }
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
afterLabel: (ctx) => {
const f = filtered[ctx.dataIndex];
return [
`Anträge mit GWÖ-Empfehlung+: ${f.n_empfohlen}`,
`davon Nein gestimmt: ${f.n_nein_trotz_empfehlung}`,
`davon Ja gestimmt: ${f.n_ja}`,
`davon Enthaltung: ${f.n_enth}`,
];
}
}
}
}
}
});
meta.textContent = `Datenbasis: ${data.n_assessments_matched} Anträge mit GWÖ-Empfehlung „Uneingeschränkt unterstützen" oder „Unterstützen mit Änderungen".`;
} catch (e) {
meta.textContent = 'Fehler: ' + e;
}
}
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
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;
}
}
// Load BL-Matrix on init
loadBlMatrix();
</script>
{% endblock %}