Phase E (substituted): Auswertungen-Drilldown-Modal (#59)

Sachsen-Adapter (#26/#38) ist Eigensystem mit ASP.NET-Webforms-Postbacks
(__VIEWSTATE/__CALLBACKID, siehe bundeslaender.py:343-348) und braucht
HAR-Aufnahme → Blocker für autonome Bearbeitung. Phase E entsprechend
substituiert mit der Frontend-Erweiterung der Auswertungen.

- Matrix-Zellen sind jetzt klickbar (`cell-with-data`-Klasse +
  hover-outline mit Blue-Border)
- Klick öffnet ein Modal, das `/api/auswertungen/zeitreihe?
  bundesland=...&partei=...` aufruft und die Score-Entwicklung dieser
  (BL, Partei)-Kombination über alle bekannten WPs als Tabelle rendert
- ESC-Taste oder Backdrop-Klick schließt das Modal
- Schließt damit den Frontend-Loop für die in Phase C gebauten
  Backend-Endpoints

(CLAUDE.md-Sync separat — die Datei liegt im Projekt-Root außerhalb
des Webapp-Git-Repos.)

Refs: #59 (Phase E substituted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-04-09 11:30:10 +02:00
parent 26f13bd29d
commit 7cf073122f

View File

@ -104,6 +104,59 @@
border-radius: 4px; border-radius: 4px;
color: var(--color-lightgray); color: var(--color-lightgray);
} }
table.matrix td.cell-with-data {
cursor: pointer;
}
table.matrix td.cell-with-data:hover {
outline: 2px solid var(--color-blue);
outline-offset: -2px;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-backdrop.show { display: flex; }
.modal {
background: white;
border-radius: 6px;
padding: 1.5rem 2rem;
min-width: 320px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.modal h2 {
color: var(--color-blue);
margin-bottom: 0.8rem;
font-size: 1.1rem;
}
.modal table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
}
.modal table th, .modal table td {
padding: 0.4rem 0.8rem;
border-bottom: 1px solid var(--color-bg);
text-align: left;
font-size: 0.85rem;
}
.modal table th { background: var(--color-bg); }
.modal-close {
float: right;
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
color: var(--color-lightgray);
}
.modal-close:hover { color: var(--color-darkgray); }
</style> </style>
</head> </head>
<body> <body>
@ -136,6 +189,15 @@
<div class="meta" id="meta"></div> <div class="meta" id="meta"></div>
</main> </main>
<!-- Drill-down modal: Zeitreihe für eine (BL, Partei)-Kombination -->
<div class="modal-backdrop" id="modal-backdrop" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2 id="modal-title">Zeitreihe</h2>
<div id="modal-body">Lade …</div>
</div>
</div>
<script> <script>
const wpFilter = document.getElementById('wp-filter'); const wpFilter = document.getElementById('wp-filter');
const reloadBtn = document.getElementById('reload'); const reloadBtn = document.getElementById('reload');
@ -174,7 +236,8 @@
for (const partei of data.parteien) { for (const partei of data.parteien) {
const cell = (data.cells[bl] || {})[partei]; const cell = (data.cells[bl] || {})[partei];
if (cell) { if (cell) {
html += `<td class="${scoreClass(cell.avg)}">${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small></td>`; // Click → drill-down auf die Zeitreihe für diese Zelle
html += `<td class="cell-with-data ${scoreClass(cell.avg)}" data-bl="${bl}" data-partei="${partei}" onclick="showZeitreihe(this.dataset.bl, this.dataset.partei)">${cell.avg.toFixed(1)}<br><small>n=${cell.n}</small></td>`;
} else { } else {
html += '<td class="empty"></td>'; html += '<td class="empty"></td>';
} }
@ -195,6 +258,45 @@
window.location.href = '/api/auswertungen/export.csv'; window.location.href = '/api/auswertungen/export.csv';
}); });
// Zeitreihen-Modal: zeigt die Score-Entwicklung einer (BL, Partei)-
// Kombination über alle bekannten Wahlperioden hinweg.
async function showZeitreihe(bundesland, partei) {
const backdrop = document.getElementById('modal-backdrop');
const title = document.getElementById('modal-title');
const body = document.getElementById('modal-body');
title.textContent = `${bundesland} × ${partei}`;
body.innerHTML = '<p>Lade Zeitreihe …</p>';
backdrop.classList.add('show');
try {
const r = await fetch(`/api/auswertungen/zeitreihe?bundesland=${encodeURIComponent(bundesland)}&partei=${encodeURIComponent(partei)}`);
const z = await r.json();
if (!z.wahlperioden || !z.wahlperioden.length) {
body.innerHTML = '<p style="color:#888;">Keine Daten für diese Kombination.</p>';
return;
}
let html = '<table><thead><tr><th>Wahlperiode</th><th>Anträge</th><th>Ø GWÖ-Score</th></tr></thead><tbody>';
for (const row of z.wahlperioden) {
html += `<tr><td>${row.wp}</td><td>${row.n}</td><td><strong>${row.avg.toFixed(2)}</strong></td></tr>`;
}
html += '</tbody></table>';
body.innerHTML = html;
} catch (e) {
body.innerHTML = `<p style="color:#d00;">Fehler: ${e}</p>`;
}
}
function closeModal(ev) {
// Klick aufs Backdrop schließt; Klicks im Modal nicht
if (!ev || ev.target.id === 'modal-backdrop') {
document.getElementById('modal-backdrop').classList.remove('show');
} else if (!ev.target) {
document.getElementById('modal-backdrop').classList.remove('show');
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('show');
});
loadMatrix(); loadMatrix();
</script> </script>
</body> </body>