01 / 25

JavaScript-Grundlagen III

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.

02 / 25 Recap

Recap: JavaScript-Grundlagen I & II

Heute: Wir nutzen all das, um räumliche Daten auf Karten darzustellen und ein interaktives Dashboard zu bauen!

Kurzer Rueckblick auf die Kernkonzepte aus Einheit 05 und 06.

03 / 25 Karten

Warum Karten? Räumliche Daten in den DH

Viele geisteswissenschaftliche Daten haben einen räumlichen Bezug:

Hugo Schuchardt korrespondierte mit über 1.100 Personen in ganz Europa. Eine Karte zeigt sein Netzwerk auf einen Blick — das kann keine Tabelle leisten.

Raeumliche Visualisierung ist ein Kernthema der Digital Humanities. Beispiel: Hugo Schuchardt Archiv der Uni Graz.

04 / 25 Leaflet.js

Leaflet.js: Einbindung per CDN

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>
Wichtig: Das CSS muss vor dem JavaScript geladen werden. Das integrity-Attribut stellt sicher, dass die Datei nicht manipuliert wurde.

CDN = Content Delivery Network. Leaflet ist ca. 40 KB gross und hat keine Abhaengigkeiten.

05 / 25 Leaflet.js

Erste Karte: map, tileLayer, setView

HTML

<!-- Container mit fester Hoehe -->
<div id="map"
  style="height: 500px;">
</div>

JavaScript

// 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:
      '&copy; OpenStreetMap'
  }
).addTo(map);
Drei Schritte: 1. HTML-Container mit 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.

06 / 25 Leaflet.js

Tile-Provider: Verschiedene Kartenstile

Leaflet zeigt keine eigene Karte — es lädt Kacheln von einem Tile-Server:

OpenStreetMap (Standard)

https://tile.openstreetmap.org/
  {z}/{x}/{y}.png

Detailliert, farbig, Community-gepflegt

CartoDB Positron (hell)

https://{s}.basemaps.cartocdn.com/
  light_all/{z}/{x}/{y}{r}.png

Minimalistisch, ideal für Overlays

Tipp für DH-Projekte: Helle, dezente Karten (CartoDB Positron) eignen sich besser für Datenvisualisierung, weil die Marker und Daten im Vordergrund stehen.

Es gibt Dutzende Tile-Provider. Fuer akademische Projekte sind OpenStreetMap und CartoDB kostenlos nutzbar.

07 / 25 Leaflet.js

Marker setzen

// 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();
Methoden-Verkettung: Leaflet nutzt das Builder-Pattern. Jede Methode gibt das Objekt zurück, sodass man .addTo(map).bindPopup("...") verketten kann.

Koordinaten werden als Array [Breitengrad, Laengengrad] uebergeben. Popups koennen HTML enthalten.

08 / 25 Leaflet.js

Mehrere Marker aus einem Array

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`
    );
});
Muster: Daten als Array von Objekten → 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.

09 / 25 GeoJSON

GeoJSON: Was ist das Format?

GeoJSON ist ein offenes Format für geographische Daten, basierend auf JSON:

Aufbau:
FeatureCollection → enthält ein Array von Feature-Objekten
Jedes Feature 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.

10 / 25 GeoJSON

GeoJSON Beispiel: Ein Point Feature

{
  "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"
      }
    }
  ]
}
Achtung: Bei GeoJSON ist die Reihenfolge der Koordinaten [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].

11 / 25 GeoJSON

GeoJSON auf Leaflet: Eine Zeile Code!

// 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.

12 / 25 GeoJSON

Popups aus GeoJSON-Properties

// 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);
onEachFeature wird für jedes Feature aufgerufen und erhält zwei Parameter: das 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.

13 / 25 Praxis

Praxis: Korrespondenz-Orte auf der Karte

Die Hugo-Schuchardt-Korrespondenz umfasst Briefe aus über 50 Städten in Europa. Mit Leaflet und GeoJSON können wir dieses Netzwerk visualisieren:

Code-Muster: fetch('korrespondenz.geojson').then(r => r.json()).then(data => L.geoJSON(data, {onEachFeature}).addTo(map))

In der Uebung werden die Studierenden genau dieses Muster umsetzen.

14 / 25 Interaktion

Interaktion: Klick auf Marker zeigt Details

// 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);
Pattern: Karte und Sidebar sind verbunden. Ein Klick auf der Karte aktualisiert Inhalte ausserhalb der Karte — das ist die Basis für Dashboards.

Das Verknuepfen von Karten-Events mit DOM-Elementen ausserhalb der Karte ist ein zentrales Konzept fuer interaktive DH-Anwendungen.

15 / 25 Visualisierung

Einfache Visualisierung: Bar Chart mit CSS

HTML + JavaScript

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>`;
});

CSS

#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.

16 / 25 Visualisierung

SVG-basierter Bar Chart

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.

17 / 25 Visualisierung

Zeitleiste: Briefe chronologisch darstellen

HTML/JS

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>`;
});

CSS

#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.

18 / 25 Dashboard

Dashboard-Layout: CSS Grid für Multi-Panel

/* 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-template-areas macht das Layout lesbar: Man sieht auf einen Blick, wo jede Komponente sitzt.

Grid-Areas sind ideal fuer Dashboards, weil man das Layout visuell im CSS sieht.

19 / 25 Dashboard

Alles zusammen: Karte + Tabelle + Filter

Das vollständige Dashboard besteht aus drei Komponenten:

Layout-Struktur

<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>

Verbindung der Komponenten

  • Filter-Input löst input-Event aus
  • Event Handler filtert das Daten-Array
  • Gefilterte Daten aktualisieren:
    • Die Marker auf der Karte
    • Die Zeilen in der Tabelle
  • Klick auf Marker markiert Zeile in Tabelle
  • Klick auf Tabellenzeile zoomt auf Marker
Prinzip: Eine Datenquelle,
mehrere Darstellungen, alles synchron.

Das Dashboard-Pattern: Eine Datenquelle (Array), mehrere Views (Karte, Tabelle), ein Filter. Aenderungen am Filter aktualisieren alle Views.

20 / 25 Agentic Coding

Agentic Coding: Komplexe Projekte strukturieren

Ein Dashboard mit Karte, Tabelle und Filter ist zu komplex für einen einzigen Prompt. Stattdessen: Komponenten einzeln generieren und dann zusammenfügen.

  1. Schritt 1: Karte mit Markern erstellen lassen (eigener Prompt)
  2. Schritt 2: Tabelle mit Daten generieren lassen (eigener Prompt)
  3. Schritt 3: Filter-Logik implementieren lassen (eigener Prompt)
  4. Schritt 4: CSS Grid Layout für das Dashboard (eigener Prompt)
  5. Schritt 5: Komponenten verbinden und testen (eigener Prompt)
Regel: Je kleiner und präziser der Prompt, desto besser das Ergebnis. Lieber 5 kleine Prompts als 1 riesiger.

Divide and conquer: Komplexe Aufgaben in ueberschaubare Teilaufgaben zerlegen. Das gilt fuer Agentic Coding genauso wie fuer klassische Softwareentwicklung.

21 / 25 Agentic Coding

CLAUDE.md: Projektregeln für dein Repository

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.

22 / 25 Agentic Coding

Kontext-Engineering: Genug Kontext für ein ganzes Projekt

Context Engineering (CE) bedeutet: dem LLM alle Informationen geben, die es braucht, um konsistenten Code zu generieren.

Was dem LLM mitgeben?

  • Projektstruktur (Ordner, Dateien)
  • Bestehenden Code (relevante Dateien)
  • Datenbeispiele (JSON, GeoJSON)
  • Coding-Konventionen (Variablen, Klassen)
  • Design-Tokens (Farben, Schriften)

Wie?

  • CLAUDE.md: Wird automatisch gelesen
  • Im Prompt: „Hier ist mein bestehendes CSS:“
  • Datei referenzieren: „Orientiere dich an karte.html“
  • Beispiel zeigen: „Die JSON-Daten sehen so aus: ...“
Je mehr relevanter Kontext, desto konsistenter der generierte Code.

Context Engineering ist die wichtigste Faehigkeit beim Agentic Coding. Es geht nicht darum, was man das LLM fragt, sondern welchen Kontext man mitliefert.

23 / 25 Agentic Coding

Code-Review: Generierten Dashboard-Code prüfen

Wenn ein LLM ein Dashboard generiert, sollten Sie systematisch prüfen:

Code Review (RV) ist keine Kritik, sondern eine systematische Prüfung. Nutzen Sie Checklisten und testen Sie in verschiedenen Browsern.

Code Review ist besonders wichtig bei generiertem Code, weil LLMs subtile Fehler machen koennen, die auf den ersten Blick nicht auffallen.

24 / 25 Assignment

Assignment 2: Letzte Hinweise

Abgabe: 18.05.2026

Erinnerung an die wichtigsten Anforderungen:

Tipp: Nutzen Sie die heutigen Übungen als Basis für Ihr Assignment. Die Korrespondenz-Karte lässt sich gut erweitern!

Assignment 2 baut auf den Einheiten 05-07 auf. Studierende koennen Leaflet.js optional einsetzen.

25 / 25

Vorschau & Wrap-Up

Heute gelernt:
  • Leaflet.js: Karten, Marker, Tile-Layer
  • GeoJSON: Format, Properties, L.geoJSON()
  • CSS/SVG Bar Charts und Zeitleisten
  • Dashboard-Layout mit CSS Grid
  • CLAUDE.md und Context Engineering
Nächste Einheit (08):
  • Präsentation Assignment 2
  • Feedback und Diskussion

Assignment 3 wird in Einheit 09 vorgestellt — Ihr finales DH-Portfolio-Projekt.

Zusammenfassung und Ausblick. Hinweis auf Assignment 2 Abgabe und Assignment 3 Vorstellung.

1 / 25
F = Vollbild | N = Notizen | Pfeiltasten = Navigation