Einheit 07 — Leaflet.js, GeoJSON, Datenvisualisierung & Dashboard-Layout
Webentwicklung & Agentic Coding | Sommersemester 2026 | Universität Graz
Willkommen zur siebten Einheit. Heute verbinden wir JavaScript mit Karten, GeoJSON und Visualisierung. Am Ende bauen wir ein Dashboard.
const, let, Strings, Numbers, Arrays, ObjectsquerySelector(), textContent, classList, Event Listenerfetch(url) mit async/await zum Laden von Datenmap(), filter(), find(), forEach()Kurzer Rueckblick auf die Kernkonzepte aus Einheit 05 und 06.
Viele geisteswissenschaftliche Daten haben einen räumlichen Bezug:
Raeumliche Visualisierung ist ein Kernthema der Digital Humanities. Beispiel: Hugo Schuchardt Archiv der Uni Graz.
Leaflet.js ist eine schlanke Open-Source-Bibliothek für interaktive Karten. Zwei Zeilen im <head> genügen:
<!-- Leaflet CSS -->
<link rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" />
<!-- Leaflet JavaScript -->
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
integrity-Attribut stellt sicher, dass die Datei nicht manipuliert wurde.
CDN = Content Delivery Network. Leaflet ist ca. 40 KB gross und hat keine Abhaengigkeiten.
<!-- Container mit fester Hoehe -->
<div id="map"
style="height: 500px;">
</div>
// Karte initialisieren
const map = L.map('map')
.setView([47.07, 15.44], 13);
// Kartenkacheln laden
L.tileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{
maxZoom: 19,
attribution:
'© OpenStreetMap'
}
).addTo(map);
id und Höhe.
2. L.map() initialisiert die Karte.
3. L.tileLayer() lädt die Kartenkacheln.
setView nimmt Koordinaten [lat, lng] und ein Zoom-Level. 13 zeigt einen Stadtausschnitt.
Leaflet zeigt keine eigene Karte — es lädt Kacheln von einem Tile-Server:
https://tile.openstreetmap.org/
{z}/{x}/{y}.png
Detailliert, farbig, Community-gepflegt
https://{s}.basemaps.cartocdn.com/
light_all/{z}/{x}/{y}{r}.png
Minimalistisch, ideal für Overlays
Es gibt Dutzende Tile-Provider. Fuer akademische Projekte sind OpenStreetMap und CartoDB kostenlos nutzbar.
// Einen einzelnen Marker setzen
const marker = L.marker([47.0787, 15.4485])
.addTo(map);
// Marker mit Popup
L.marker([47.0787, 15.4485])
.addTo(map)
.bindPopup('<strong>Universität Graz</strong><br>Hauptgebäude');
// Marker mit sofort offenem Popup
L.marker([47.0787, 15.4485])
.addTo(map)
.bindPopup('Uni Graz')
.openPopup();
.addTo(map).bindPopup("...") verketten kann.
Koordinaten werden als Array [Breitengrad, Laengengrad] uebergeben. Popups koennen HTML enthalten.
const orte = [
{ name: 'Graz', lat: 47.07, lng: 15.44, briefe: 342 },
{ name: 'Wien', lat: 48.21, lng: 16.37, briefe: 218 },
{ name: 'Paris', lat: 48.86, lng: 2.35, briefe: 156 },
{ name: 'Rom', lat: 41.90, lng: 12.50, briefe: 87 },
{ name: 'Berlin', lat: 52.52, lng: 13.41, briefe: 134 }
];
// Fuer jeden Ort einen Marker erstellen
orte.forEach(ort => {
L.marker([ort.lat, ort.lng])
.addTo(map)
.bindPopup(
`<strong>${ort.name}</strong><br>` +
`${ort.briefe} Briefe`
);
});
forEach() →
für jedes Element einen Marker erstellen. Template Literals (`...${variable}...`)
machen den Popup-Inhalt dynamisch.
Dieses Muster ist fundamental: Daten-Array durchlaufen und fuer jeden Eintrag ein DOM-Element (hier: Marker) erzeugen.
GeoJSON ist ein offenes Format für geographische Daten, basierend auf JSON:
FeatureCollection → enthält ein Array von Feature-ObjektenFeature hat geometry (wo?) und properties (was?)
GeoJSON ist RFC 7946. Es ist das Standardformat fuer raeumliche Daten im Web und wird von Leaflet, Mapbox, Google Maps und vielen GIS-Tools unterstuetzt.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [15.4485, 47.0787]
},
"properties": {
"name": "Graz",
"institution": "Universität Graz",
"briefanzahl": 342,
"zeitraum": "1876-1927"
}
}
]
}
[Längengrad, Breitengrad] (also [lng, lat]) —
umgekehrt zu Leaflets [lat, lng]! Leaflet konvertiert das automatisch.
Die Koordinaten-Reihenfolge ist eine haeufige Fehlerquelle. GeoJSON folgt der mathematischen Konvention [x, y], also [lng, lat].
// GeoJSON-Daten als JavaScript-Objekt
const geojsonData = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [15.44, 47.07] },
"properties": { "name": "Graz", "briefe": 342 }
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [16.37, 48.21] },
"properties": { "name": "Wien", "briefe": 218 }
}
]
};
// Eine Zeile genuegt!
L.geoJSON(geojsonData).addTo(map);
L.geoJSON(data).addTo(map) — Leaflet erkennt automatisch
den Geometrietyp, erstellt Marker für Points, Linien für LineStrings und
Flächen für Polygons.
Das ist die Staerke von Leaflet: GeoJSON wird nativ unterstuetzt. Keine Transformation noetig.
// onEachFeature: Callback fuer jedes Feature
const onEachFeature = (feature, layer) => {
if (feature.properties) {
const p = feature.properties;
layer.bindPopup(
`<strong>${p.name}</strong><br>` +
`Briefe: ${p.briefe}<br>` +
`Zeitraum: ${p.zeitraum || 'unbekannt'}`
);
}
};
// GeoJSON mit Popup-Callback
L.geoJSON(geojsonData, {
onEachFeature: onEachFeature
}).addTo(map);
feature (mit Properties) und den layer
(das Leaflet-Objekt, auf dem man bindPopup() aufrufen kann).
onEachFeature ist das wichtigste Konfigurationsobjekt fuer GeoJSON in Leaflet. Damit verbindet man Geodaten mit der Darstellung.
Die Hugo-Schuchardt-Korrespondenz umfasst Briefe aus über 50 Städten in Europa. Mit Leaflet und GeoJSON können wir dieses Netzwerk visualisieren:
map.fitBounds() passt den Kartenausschnitt automatisch anfetch('korrespondenz.geojson') →
.then(r => r.json()) →
.then(data => L.geoJSON(data, {onEachFeature}).addTo(map))
In der Uebung werden die Studierenden genau dieses Muster umsetzen.
// Sidebar-Element
const sidebar = document.querySelector('#sidebar');
// GeoJSON mit Klick-Event
L.geoJSON(geojsonData, {
onEachFeature: (feature, layer) => {
// Popup auf der Karte
layer.bindPopup(feature.properties.name);
// Klick aktualisiert die Sidebar
layer.on('click', () => {
const p = feature.properties;
sidebar.innerHTML = `
<h3>${p.name}</h3>
<p>Briefe: ${p.briefe}</p>
<p>Partner: ${p.partner || '—'}</p>
<p>Zeitraum: ${p.zeitraum || '—'}</p>
`;
});
}
}).addTo(map);
Das Verknuepfen von Karten-Events mit DOM-Elementen ausserhalb der Karte ist ein zentrales Konzept fuer interaktive DH-Anwendungen.
const daten = [
{ label: '1870er', wert: 45 },
{ label: '1880er', wert: 187 },
{ label: '1890er', wert: 234 },
{ label: '1900er', wert: 156 }
];
const maxWert = Math.max(
...daten.map(d => d.wert)
);
const container =
document.querySelector('#chart');
daten.forEach(d => {
const hoehe =
(d.wert / maxWert) * 100;
container.innerHTML += `
<div class="bar"
style="height: ${hoehe}%">
<span>${d.wert}</span>
<label>${d.label}</label>
</div>`;
});
#chart {
display: flex;
align-items: flex-end;
gap: 0.5rem;
height: 200px;
border-bottom:
2px solid #333;
padding: 0 1rem;
}
.bar {
flex: 1;
background: #1a5276;
border-radius: 4px 4px 0 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding-bottom: 0.5rem;
position: relative;
}
.bar span {
color: white;
font-size: 0.8rem;
font-weight: bold;
}
.bar label {
position: absolute;
bottom: -1.5rem;
font-size: 0.75rem;
}
Keine Bibliothek noetig! Die Hoehe jedes Balkens wird als Prozent des Maximalwerts berechnet.
const daten = [
{ label: '1870er', wert: 45 },
{ label: '1880er', wert: 187 },
{ label: '1890er', wert: 234 },
{ label: '1900er', wert: 156 }
];
const svgBreite = 400, svgHoehe = 200;
const maxWert = Math.max(...daten.map(d => d.wert));
const barBreite = svgBreite / daten.length - 10;
let svgInhalt = '';
daten.forEach((d, i) => {
const barHoehe = (d.wert / maxWert) * (svgHoehe - 30);
const x = i * (barBreite + 10) + 5;
const y = svgHoehe - barHoehe - 20;
svgInhalt += `
<rect x="${x}" y="${y}" width="${barBreite}"
height="${barHoehe}" fill="#1a5276" rx="3" />
<text x="${x + barBreite / 2}" y="${y - 5}"
text-anchor="middle" font-size="12">${d.wert}</text>
<text x="${x + barBreite / 2}" y="${svgHoehe - 5}"
text-anchor="middle" font-size="11">${d.label}</text>`;
});
document.querySelector('#svg-chart').innerHTML =
`<svg width="${svgBreite}" height="${svgHoehe}">${svgInhalt}</svg>`;
SVG ist praeziser als CSS-Balken: volle Kontrolle ueber Position, Groesse und Text.
const briefe = [
{ datum: '1882-01-15',
partner: 'Ascoli' },
{ datum: '1885-05-03',
partner: 'Meyer' },
{ datum: '1890-11-22',
partner: 'Baudouin' },
];
const minJahr = 1880, maxJahr = 1895;
const timeline =
document.querySelector('#timeline');
briefe.forEach(b => {
const jahr = parseInt(b.datum);
const pos = ((jahr - minJahr) /
(maxJahr - minJahr)) * 100;
timeline.innerHTML += `
<div class="tl-item"
style="left: ${pos}%">
<span>${b.datum}</span>
<span>${b.partner}</span>
</div>`;
});
#timeline {
position: relative;
height: 80px;
border-bottom: 2px solid #333;
margin: 2rem 1rem;
}
.tl-item {
position: absolute;
bottom: 10px;
transform: translateX(-50%);
text-align: center;
font-size: 0.75rem;
line-height: 1.3;
}
.tl-item::before {
content: '';
display: block;
width: 8px;
height: 8px;
background: #1a5276;
border-radius: 50%;
margin: 0 auto 4px;
}
.tl-item span {
display: block;
}
Horizontale Zeitleiste: Position jedes Elements wird als Prozent der Gesamtspanne berechnet.
/* CSS Grid fuer Dashboard-Layout */
.dashboard {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"filter filter"
"karte tabelle";
gap: 1rem;
height: calc(100vh - 80px);
padding: 1rem;
}
.dashboard__filter { grid-area: filter; }
.dashboard__karte { grid-area: karte; }
.dashboard__tabelle { grid-area: tabelle; }
/* Responsive: auf kleinen Bildschirmen untereinander */
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
grid-template-areas:
"filter"
"karte"
"tabelle";
}
}
Grid-Areas sind ideal fuer Dashboards, weil man das Layout visuell im CSS sieht.
Das vollständige Dashboard besteht aus drei Komponenten:
<div class="dashboard">
<!-- Filter oben -->
<div class="dashboard__filter">
<input type="text"
id="suche"
placeholder="Ort suchen...">
</div>
<!-- Karte links -->
<div class="dashboard__karte">
<div id="map"></div>
</div>
<!-- Tabelle rechts -->
<div class="dashboard__tabelle">
<table id="tabelle">
<thead>...</thead>
<tbody></tbody>
</table>
</div>
</div>
input-Event ausDas Dashboard-Pattern: Eine Datenquelle (Array), mehrere Views (Karte, Tabelle), ein Filter. Aenderungen am Filter aktualisieren alle Views.
Ein Dashboard mit Karte, Tabelle und Filter ist zu komplex für einen einzigen Prompt. Stattdessen: Komponenten einzeln generieren und dann zusammenfügen.
Divide and conquer: Komplexe Aufgaben in ueberschaubare Teilaufgaben zerlegen. Das gilt fuer Agentic Coding genauso wie fuer klassische Softwareentwicklung.
Eine CLAUDE.md im Projektstamm gibt dem LLM Kontext über das gesamte Projekt:
# CLAUDE.md
## Projektbeschreibung
DH-Portfolio mit Korrespondenz-Visualisierung.
## Technische Regeln
- Vanilla HTML/CSS/JS, kein Framework
- Externe Abhaengigkeiten: Leaflet.js per CDN
- Alle Pfade relativ (GitHub Pages kompatibel)
- Deutsche Sprache in allen Inhalten
## Dateistruktur
- index.html (Startseite)
- karte.html (Korrespondenz-Karte)
- tabelle.html (Briefe-Tabelle)
- css/style.css (alle Styles)
- js/app.js (alle Skripte)
- data/briefe.json (Datenquelle)
## Konventionen
- const/let (kein var)
- Arrow Functions
- Semantische HTML-Elemente
- BEM-artige Klassennamen
CLAUDE.md wird von Claude Code automatisch gelesen. Sie ist das zentrale Dokument fuer Context Engineering in einem Projekt.
Context Engineering (CE) bedeutet: dem LLM alle Informationen geben, die es braucht, um konsistenten Code zu generieren.
Context Engineering ist die wichtigste Faehigkeit beim Agentic Coding. Es geht nicht darum, was man das LLM fragt, sondern welchen Kontext man mitliefert.
Wenn ein LLM ein Dashboard generiert, sollten Sie systematisch prüfen:
<th scope> versehen? Hat das Input ein Label?Code Review ist besonders wichtig bei generiertem Code, weil LLMs subtile Fehler machen koennen, die auf den ersten Blick nicht auffallen.
Erinnerung an die wichtigsten Anforderungen:
Assignment 2 baut auf den Einheiten 05-07 auf. Studierende koennen Leaflet.js optional einsetzen.
Assignment 3 wird in Einheit 09 vorgestellt — Ihr finales DH-Portfolio-Projekt.
Zusammenfassung und Ausblick. Hinweis auf Assignment 2 Abgabe und Assignment 3 Vorstellung.