Datenvisualisierung/interaktive Landkarten

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Bei einem Einsatz von Landkarten auf Webseiten bietet sich SVG als Format an, da Flächen und Orte aus gezeichneten Formen bestehen, die man dann mit CSS gestalten und sogar animieren kann.

Landkarten spielen eine große Rolle bei der Datenvisualisierung, da sie anschaulicher und besser als lange Tabellen sind.

Woher bekommt man SVG-Karten?

Natürlich musst du Karten nicht selbst zeichnen oder bei einem professionellen Kartendienst kaufen.

Es gibt auch Wege, um SVG-Karten aus openstreetmap[3] zu erzeugen, die dann eingebunden werden können.

Natural Earth ist eine Sammlung von geografischen Daten, die unter Public Domain stehen. Dieses Tutorial von smashing magazine zeigt, wie daraus mit dem Framework Kartograph.py eigene Karten erzeugt werden können.

interaktive Landkarte

Diese Karte zeigt Australien. Wenn man über die einzelnen Bundesstaaten mit der Maus fährt oder auf dem Touchscreen längere Zeit verweilt, erscheint deren Name.

Interaktive Karte ansehen …
a path {
  fill: #c82f04;
  stroke: #333;
  stroke-width: 0.4;
}

a:hover path,
a:focus path {
  fill: gold;
}
	
text {
  opacity: 0;
  fill: black;
  font-size: 8px;
	font-family: sans-serif,Arial;
  pointer-events: none;  
}
	
a:hover text,
a:focus text {
  opacity: 1;
}

Bei a:hover verändert das Pfad-Element seine Füllfarbe; zugleich wird der vorher mit opacity: 0 ausgeblendete Text sichtbar.

Der Selektor a:hover text blendet den vorher durchsichtigen Text ein.

Normalerweise würde der Pfad, wenn die Maus über dem Text schwebt, seinen :hover-Status verlieren. Durch die CSS-Eigenschaft pointer-events: none kann man dies abschalten.

Ausschnitt aus dem SVG-Markup:
<a id="ACT"  aria-labelledby="ACTInfo">
    <path d="M246 186C246 ...z"/>
    <text x="200" y="200" id="ACTInfo">Australian Capital Territory</text>  
</a>

Da sowohl das a-Element als auch der Pfad selbst keine semantische Bedeutung haben, wird mit einem aria-labelledby-Attribut auf das benachbarte text-Element hingewiesen.

Nachteilig an dieser Methode ist die Tatsache, dass Textfelder, die über den zugehörigen Pfad herausragen, durch nachfolgende andere Pfade überschrieben werden. Da es in SVG (werder in 1.1 noch in SVG2) noch kein z-index gibt, kann ein Objekt nur durch eine spätere Position im Markup nach vorne gebracht werden.

frei positionierbare Tooltipps

In diesem Beispiel wird die Textinformation in einen Tooltipp gelegt.

Beispiel
  <defs>
    <symbol id="tooltipp">
	  <path d="..." fill="#666" stroke="black" stroke-width=".25" />  
	</symbol>
  </defs>

...

<g class="info" id="NswInfo" transform="translate(200,120)">
    <use href="#tooltipp" />
    <text x="5" y="12">New South Wales</text> 
</g>

Die Form des Tooltips wird in einem wiederverwendbaren symbol angelegt und dann innerhalb eines g-Elements mit use wieder aufgerufen. Der Text wird nicht wie in HTML als Kindelement, sondern einfach als nächstes Geschwisterelement notiert.

Verknüpfung von Kartendaten und Tooltip
<a id="NSW" aria-labelledby="NswInfo" tabindex="0" >
    <path class="land" d="..." />
</a>

<g class="info" id="NswInfo" transform="translate(200,120)">
    <use href="#tooltipp" />
    <text x="5" y="12">New South Wales</text> 
</g>

Die Koordinaten der Landflächen werden zuerst, anschließend die einzelnen Tooltips notiert. So wird sichergestellt, dass die Tooltips über den Landflächen dargestellt werden.

Der Pfad ist in einem a-Element umschlossen, das durch tabIndex="0" (CamelCase) auch mit der -Taste anwählbar und über das aria-Attribut mit dem Tooltipp verbunden ist.


Und nun das fertige Beispiel:

Beispiel ansehen …
.land {
  fill: #fffbf0;
  stroke: #e7c157;
  stroke-width: 0.4;
}

.land:hover,
a:focus .land {
  fill: #dfac20;
  stroke:#333;
}
	
.info {
  opacity: 0;
  font-size: 8px;
  fill: white;
  pointer-events: none;  
}
	
#ACT:hover ~ #ActInfo,  #ACT:focus ~ #ActInfo,
#NSW:hover ~ #NswInfo,  #NSW:focus ~ #NswInfo,
#NT:hover ~  #NtInfo,   #NT:focus ~  #NtInfo,
#QLD:hover ~ #QldInfo,  #QLD:focus ~ #QldInfo,
#SA:hover ~  #SaInfo,   #SA:focus ~  #SaInfo,
#TAS:hover ~ #TasInfo,  #TAS:focus ~ #TasInfo,
#VIC:hover ~ #VicInfo,  #VIC:focus ~ #VicInfo,
#WA:hover ~  #WaInfo,   #WA:focus ~  #WaInfo {
  opacity: 1;
  transition: all 0.5s linear;
}

Bei :hover verändert die Landfläche ihre Füllfarbe; zugleich wird der vorher mit opacity: 0 ausgeblendete Tooltipp sichtbar.

Der Selektor #ACT:hover ~ #ActInfo wirkt dank des Geschwisterselektors auf alle Tooltipps, deren g-Element ja jüngeres Geschwister des Links mit der Landfläche ist. Da das a-Element den Pfad der Landfläche umfasst, kann man die aktive Fläche des Links pixelgenau ansprechen.

Sie können die Link-Elemente auch mittels eines internen Ankers mit den Tooltipps verbinden, sodass sie fest selektierbar sind:

Beispiel
<a id="ACT" href="#ACT-info">
  <path class="land" d="..."/>
</a>

<g class="info ACT" id="href-info" transform="translate(225,155)">
  <use href="#tooltipp" />
  <text x="5" y="12">Canberra</text> 
</g>

Fazit

  • Die mögliche Überlagerung des Tooltips durch nachfolgende Kartendaten wird so vermieden.
  • Allerdings sind die CSS-Regelsätze mit den Geschwisterselektoren grenzwertig. Für Australien mag das noch gehen; die 50 Bundestaaten der USA oder die 294 Landkreise Deutschlands können so nicht umgesetzt werden.

Dynamische Selektion von Kartenausschnitten

Im SELF-Forum wurde gefragt, wie man für einen Kartenausschnitt einen :hover-Effekt mit JavaScript auslösen kann.[4] Dies ist nicht möglich; mit focus kann man einem Element aber den Fokus geben und denselben Effekt erzielen.

Kartenausschnitte mit JavaScript markieren ansehen …
document.querySelector('#find').addEventListener('click', setFocus);

function setFocus() { 
    let state = document.querySelector('[type="search"]').value;
    console.log(state);
    document.getElementById(state).focus();
}

Das Beispiel enthält nun noch ein Suchformular, bei dem mit datalist Suchvorschläge angeboten werden. Um das Beispiel-Script so einfach wie möglich zu halten, werden nur die Abkürzungen, die sich als ID der Kartenausschnitte wiederfinden, aufgelistet.

Bei einem Klick auf den Finden-Button wird der Wert des Suchformulars ausgelesen und das entsprechende Element erhält mit document.getElementById(state).focus(); den Fokus.

Die Formatierung selbst wird wieder über CSS realisiert.

Choroplethenkarte

Eine Choroplethenkarte oder auch Flächenkartogramm ist eine Karte in der die Gebiete nicht willkürlich, sondern je nach zugehörenden Daten in unterschiedlichen Schattierungen gefärbt, schattiert, gepunktet oder schraffiert sind. Passende Daten wären keine absoluten Zahlen wie die Einwohnerzahl eines Gebietes, sondern auf die Fläche bezogene wie Bevölkerungsdichte, -wachstum oder die prozentualen Werte für Arbeitslosigkeit, Mietpreise etc.[5]

Für das Farbschema gibt es mit ColorBrewer ein bequemes Tool, das Ihnen passende Farbpaletten zusammenstellt.

Screenshot

Deutschland - Karte und Statistik

Die regionalen Unterschiede der 16 Bundesländer sollen in einer Infografik visualisiert werden. Dafür nehmen wir eine Karte aus Wikimedia Commons[6].

Anders als bei den vorherigen Beispielen integrieren wir diese Karte aber nicht in die Webseite, sondern laden sie auf unseren Webspace hoch (Deutschland-Karte.svg) und binden sie mit dem object-Element ein. So wird sie wie ein Bild dargestellt, kann aber - anders als ein img - auch durch ein Script manipuliert und eingefärbt werden.

Einbindung mit object
<object 
    data="Deutschland-Karte.svg" 
    type="image/svg+xml">

    <param name="src" value="/local/Deutschland-Karte.svg">
    <!-- Fallback -->
    <img src="Deutschland-Karte.png" alt="Choropletenkarte der Bundesländer">
</object>
Beachte: Wir können nicht die Originaldatei aus Wikimedia Commons verwenden. Die Same-Origin-Policy verhindert den Zugriff auf fremde Inhalte mit Scripten.
Webseite, Script und Mediendateien müssen sich auf demselben Server befinden und mit dem gleichen Protokoll geladen werden!

Die Idee stammt von Peter Collingridge, der die :hover-Effekte 2010 noch mit mouseover umsetzte, heute reicht dafür allein CSS.[7]

Karte und Statistik ansehen …
.land {
  fill: #fffbf0;
  stroke: #e7c157;
  stroke-width: 0.5;
}
.land:hover, .land:focus {
   stroke: #00000;
   stroke-width: 1.5;
}

Bei :hover und :focus wird die Randlinie schwarz und dicker dargestellt.

Empfehlung: Statt verschiedener Farben aus dem Color-Brewer kann man auch nur einen Farbton verwenden und die Helligkeit oder Sättigung stufenweise verändern.

Unsere Daten erhalten wir aus der in der Webseite vorhandenen Tabelle. Ein Script liest die Werte aus und sortiert sie in die verschiedenen Stufen. Nun wird auf das DOM des SVG zugegriffen und die Klasse .land durch eine Klasse ergänzt, die die Färbung anhand der Werte vornimmt.

Da diese CSS-Festlegungen ja noch nicht in der Karte vorhanden sind, müssen sie (und die Legende) ebenfalls durch das Script ins Style-Element des SVGs eingefügt werden:

Karte und Statistik - das fertige Projekt ansehen …
	var mySVG = document.getElementById('svgObject'),
	svgDoc;

	// Get the SVG document inside the Object tag - Wait until it's loaded!
	mySVG.addEventListener('load',function() {
		svgDoc = mySVG.contentDocument; console.log('SVG contentDocument ist geladen!');
		const table = document.getElementById("tabledata"); const rows = table.querySelectorAll('tr');

		// Initialize the result array for ranking const result = [];
		...

		//load stylesheet - only one rule! let sheet = svgDoc.styleSheets[0];
		sheet.insertRule('@media all { .level-1{fill: oklch(0.97 0.09 97.04);} .level-2{fill: oklch(0.93 0.14 89.69);} .level-3{fill: oklch(0.87 0.18 82.41);} .level-4{fill: oklch(0.79 0.23 75.32);} .level-5{fill: oklch(0.69 0.27 68.71);} .level-6{fill: oklch(0.55 0.38 57.14);} .level-7{fill: oklch(0.45 0.47 41.29);} .level-8{fill:oklch(0.36 0.53 29.12);} .level-9{fill:oklch(0.22 0.50 13.02);} }', 1);

	// Assign levels and classes result.forEach(entry => {
	const { bundesland, rate2016 } = entry;
	const svgItem = svgDoc.querySelector('#'+bundesland); 

	let levelClass;
	if (rate2016 < 4) levelClass = 'level-1'";
	else if (rate2016 < 5.5) levelClass = 'level-3';
	else if (rate2016 < 7) levelClass = 'level-4';
	else if (rate2016 < 8.5) levelClass = 'level-5';
	else if (rate2016 < 10) levelClass = 'level-7';
	else levelClass = "level-9";

	if (svgItem) {
	   svgItem.classList.add(levelClass);
 	}
});

Wenn man auf das DOM des SVG zuzugreifen versucht, bevor es vollständig geladen ist, wird dies einen Fehler produzieren. Dies wird durch die anonyme Funktion, die erst nach dem Laden auf das SVG (und sein DOM) zugreift, abgesichert.

Mit insertRule kann man eine CSS-Regel in das DOM einfügen. Deshalb wurden die verschiedenen Klassen in eine @media{}-Regel gepackt.

Anschließend werden die Werte in einer Kette von if und else if in Kategorien geteilt und mit ClassList.add entsprechende Klassen zugewiesen.

Um ein Bundesland mit Maus, Tastatur oder Touch auszuwählen, sollte es gekennzeichnet werden. Hier hat es sich als zweckmäßig herausgestellt, nur die Helligkeit der Füllung mit filter: brightness(1.1) zu verändern. Eine Änderung der Randlinie führt oft zu unerwünschten Ergebnissen, weil nachfolgende SVG-Elemente diese nach außen wachsende Randlinie wieder verdecken.

Ranking

Im letzten Beispiel wurde ja bereits ein (internes) Ranking erstellt und die einzelnen Bundesländer entsprechend eingefärbt. Könnte man diese Daten nicht für ein Balkendiagramm verwenden, das die alpabetische Sortierung aufhebt und die Zahlen anschaulich visualisiert?

Dabei ist die Frage, ob dieses Diagramm in das bestehende, externe SVG oder in einem Inline-SVG direkt in der Webseite eingefügt werden soll. Da es keinen direkten Bezug zur Karte hat, wollen wir es in der Webseite neu aufbauen:

Ein Balken mit Leerstellen für die Beschriftung ansehen …
<svg viewBox="0 0 500 500">
  <title>Deutschland - Arbeitslosenquote</title>
<defs>
<symbol id="dataset">
	<g class="dataset" transform="translate(210,50)">
		<text class="land-name" x="-10" y="17" >$land</text>	
		<rect class="bar" x="1" y="1" width="20" height="20" />
		<text class="value" x="10" y="17">$wert</text>
	</g>
</symbol>   
</defs>
 
<text x="20" y="40" role="heading">Arbeitslosenquote </text>

<g id="diagram" >
	<use href="#dataset" transform="translate(0,30)" />
	<use href="#dataset" transform="translate(0,60)" />
	...
</g>

Das SVG ist anfänglich nur mit Überschrift und leeren g-Element für das Diagramm angelegt.

Im Definitionsabschnitt wird ein Balken bereits vorher in einem symbol-Element angelegt und dann vom Script als use-Element mit den Werten aus der Tabelle gefüllt:


Fazit

Dieses Beispiel wirft die Frage auf, welchen Aufwand man treiben möchte. Für eine Karte mag es sich lohnen, eine solche Eigenlösung zu entwickeln. Tritt die Aufgabenstellung häufiger auf, sollte man ein Framework wie D3.js mit seinen Möglichkeiten nutzen.

Neben der Auswertung einer Datentabelle auf der Webseite selbst gibt es oft Daten, die aus Datenbanken, Messgeräten oder aus Excel-Tabellen, die einen Export von .csv-Daten ermöglichen[8] , importiert werden. Dies würde den Rahmen eines solchen Tutorials hier aber sprengen.


Siehe auch

  • Einstieg in Leaflet

    SELF-Blog-Artikel von Jürgen Berkemeier,
    Leaflet ist eine API, um Karten in Webseiten einzubinden

  • Geolocation API
  • responsive Imagemaps

    Klickflächen innerhalb eines Bilds? -


    Das wird jetzt mit SVG gemacht!


mit dem Viewport zoomen

Weblinks

  1. http://www.amcharts.com/svg-maps/
  2. wikimedia: Category:SVG_maps_by_country
  3. http://wiki.openstreetmap.org/wiki/SVG
  4. SELF-Forum: Frage zum Wiki-Artikel „SVG_und_JavaScript“ vom 09.12.2021
  5. flowingdata: How to Make a US County Thematic Map Using Free Tools
  6. File:Karte Bundesrepublik Deutschland.svg (commons.wikimedia.org)
  7. Peter Collingridge: SVG map
  8. How to Make a US County Thematic Map Using Free Tools (flowingdata.com)
    Dies ist ein sehr gutes Tutorial, wie man eine vorhandene SVG-Karte mit in einer Daten aus einer CSV-Datei mit aus der Wikipedia gewonnenen Daten dynamisch füllt. Die Skripte sind allerdings in Python geschrieben.