Reiserouten mit DataMaps illustriert

Reiserouten mit DataMaps illustriert

Im letzten Beitrag habe ich versucht geografische Daten mit Vega-Lite zu illustrieren. Ein Problem bei Vega-Lite war, dass die Visualisierung von Reiserouten nicht optimal lösbar war. Zur Erinnerung: die Verbindungen zwischen zwei geografischen Punkten wurden durch gerade Linien dargestellt. Diese wurden also nicht der geografischen Projektion unterworfen, und lieferten so eine ungenaue Vorstellung über die zurück gelegten Strecken und deren Verlauf.

Das Projekt DataMaps hat sich als eine Möglichkeit herausgestellt, diesem Ungemach aus dem Weg zu gehen. Allerdings muss man dafür etwas mehr tun als nur konfigurieren, aber nicht viel. DataMaps beruht auf D3.js und stellt auf hoher Ebene einfache Möglichkeiten bereit geografische Bereiche, Punkte und Strecken zu visualisieren.

Wie man sehen wird beschränkt die Programmierung im Wesentlichen auf:

  • Konfiguration der Karte und der Symbole
  • Festlegung der interessanten Punkte
  • Festlegung der interessanten Strecken

Das ist nicht so komfortabel wie die reine Konfiguration bei Vega-Lite, aber der Aufwand hält sich immer noch in Grenzen, da die Bibliothek schon viele Dinge mitbringt und vorkonfiguriert, die man sonst manuell erledigen müsste.

Das Ganze beginnt mit einer HTML-Datei, die letztendlich unsere Karte zur Anzeige bringen wird. Dort wird ein Element definiert, das die Karte enthalten soll:

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DataMaps-Illustration</title>
</head>
<body>
  <div id="root" class="center-align"></div>
</body>
</html>

Im Javascript erstellt man eine Instanz des Typs Datamap und beginnt mit der Konfiguration. Als erstes das Element der HTML-Seite definiert, in dem die Karte landen soll:

var map = new Datamap({
 element: document.getElementById('root'),

Wie bei Vega-Lite definiert man die Größe der generierten Karte und die geografische Projektion:

 height: 800,
 width: 1600,
 projection: 'equirectangular',

Hier habe ich eine Projektion gewählt, die eine querrechteckige Grafik erzeugt. D3.js bietet eine Vielzahl von geografischen Projektionen an, welche man wählt hängt natürlich vom Zweck der Darstellung ab.

Weitere geografische Grundlagen werden in der Struktur geographyConfig festgelegt.

 geographyConfig: {
   scope: 'world'
   // Standardmäßig wird die Antarktis ausgeblendet
   hideAntarctica: false,
   // Heutige Grenzen werden angezeigt -- daher ausgeblendet
   borderWidth: 1,
   borderColor: '#AAA',
 },

Mit dem Element scope wird festgelegt, welcher Bereich der Karte angezeigt wird. Die DataMaps-Bibliothek bringt die benötigten Kartendateien gleich mit. In diesem Fall möchte ich eine Weltkarte. Standardmäßig wird die Antarktis bei der Anzeige ausgeblendet, was ich aber hier nicht möchte — obwohl sie wegen der gewählten Projektion ungewohnt breit wirken wird.

Einer der Vorannahmen von DataMaps ist, dass man aktuelle Ländergrenzen anzeigen möchte. Diese lassen sich leider nicht einfach ausschalten, daher werden sie in der obigen Konfiguration einfach unsichtbar gemacht. Die Farbe der Markierungen für Ländergrenzen wird einfach der Farbe der Kontinente angeglichen. So erhält man eine einfache Karte mit Kontinenten, die gut als Hintergrund fungiert.

Farben für Kartenelemente können in der Struktur fills auf verschiedene Weise konfiguriert werden. In diesem Fall definieren wir mit defaultFill die Standardfarbe für Kartenelemente.

 fills: {
   defaultFill: '#AAA',
   lt50: 'rgba(0,244,244,0.9)',
   gt50: 'red'
 }

Mit den Strukturen für geografische Punkte (bubblesConfig) und Strecken (arcConfig) kann man diese statisch definieren.

 arcConfig: {
    greatArc: true
 },
 bubblesConfig: {},

Man könnte in diesen Abschnitten die Farben und das Verhalten für alle festlegen. Für meinen Anwendungsfall, die Generierung einer Buch-Illustration, brauche ich kein dynamisches Verhalten nicht, und die Farben möchte ich dynamisch festlegen. Daher bleibt die bubblesConfig für die geografischen Punkte hier leer.

Die Konfiguration für Strecken, arcConfig enthält eine Einstellung, die bei weltumspannenden Reiserouten sehr nützlich ist: greatArc. Gibt man eine Strecke mit mehreren Punkten an, generiert die Bibliothek standardmäßig eine Route innerhalb der Karte, d.h. die kürzeste Verbindung wird gesucht. Mit der Einstellung greatArc: true weiß DataMaps, dass die Routen quasi außen herum generiert werden sollen.

DataMaps-Beispiel: Weltumspannende Reiseroute
DataMaps-Beispiel: Weltumspannende Reiseroute

Gebe ich als Route zum Beispiel Berlin, Los Angeles und Sydney an, wird die generierte Route bis zum linken Kartenrand gehen, um am rechten Kartenrand wieder zu erscheinen und in Sydney zu enden. Mit Vega-Lite musste ich dieses Verhalten durch Einfügen von entsprechenden Punkten manuell simulieren.

Ab hier beginnt die JavaScript-Welt. Einige wenige Funktionen werden benötigt, um die Routendaten zu laden und in eine geeignete Form für die Anzeige zu bringen. In diesem Fall habe ich das auf vier Funktionen verteilt:

  • Strecken generieren
  • Punkte generieren
  • Annotationen für Punkte anbringen
  • Daten laden

Die Funktionen zeige ich hier nur auszugsweise, wer mag kann sich den vollständigen Code hier ansehen.

Die Funktionen gehen davon aus, dass Reiserouten mehrerer Teams in einer CSV-Datei gespeichert sind. Pro Team gibt es eine Route mit mehreren Stationen. In diesem Fall wird davon ausgegangen, dass die Routen geschlossen sind, d.h. von der letzten Station geht es zurück zum Startpunkt. Der Einfachheit halber nutze ich im Beispiel nur die Route eines Teams.

d3.csv('/data/reiserouten.csv', (error, dataset) => {
 const points = [];
 // Punkte auslesen
 dataset.forEach((data) => {
   const pt = {
     team: data['team'],
     longitude: data['länge'],
     latitude: data['breite'],
     type: data['typ'],
     name: data['name'],
     txt: data['text']
   };
   points.push(pt);
 });
 const npoints = d3.nest()
   .key(d =&gt; { return d.team; })
   .entries(points);
 ...
});

Dieser Teil des Codes nutzt D3-Funktionen, um die CSV-Datei auszulesen, und fasst dann die Datensätze eines Teams anhand der Team-ID zusammen. npoints enthält danach einen Eintrag pro Team, der wiederum einen Array mit den einzelnen Streckenpunkten enthält.

Mit diesen Daten ausgerüstet, kann es endlich ans Zeichnen gehen.

const arcs1 = makeArc(npoints[0].values, {strokeWidth: 3, strokeColor: '#d53e4f'});
map.arc(arcs1);

Die Funktion makeArc bekommt jeweils die Streckenpunkte eines Teams übergeben, zusammen mit Breite und Farbe für diese Route, und generiert eine D3-Strecke. Diese wird zum Zeichnen schließlich an die Funktion map.arc übergeben.

Sollen mehrere Routen gezeichnet werden, wird makeArc mehrfach aufgerufen, die Ergebnisse per concat verbunden, und dann map.arc mit der Zusammenfassung aufgerufen. Mehrere Aufrufe von map.arc hintereinander funktionieren nicht.

const bubbles1 = makeBubbles(npoints[0].values);
map.bubbles(bubbles1);

Ganz ähnlich geht man beim Einzeichnen der Streckenpunkte vor. Die Streckenpunkte der einzelnen Teams werden an die Funktion makeBubbles übergeben, die sie entsprechend formatiert. In diesem Fall wird der Startpunkt größer und anders eingefärbt angezeigt, als die anderen Streckenpunkte, wie im Bild zu sehen ist. Das Ergebnis wandert in map.bubbles und wird angezeigt. Für mehrere Routen gilt dasselbe wie bei den Strecken.

annotateRoute(route, bubbles);

Da ich manche der angezeigten Stationen noch gern annotieren möchte, habe ich auf die Bibliothek d3-annotation zurück gegriffen. Die Funktion annotateRoute geht die Punkte einer Strecke durch und versieht die Startpunkte mit den Texten aus der CSV-Datei.

Damit ist die Karte generiert und wird als SVG im Browser angezeigt. Wie bekommt man sie nun in eine Datei? Dafür füge ich einen Button ein, der die Funktion writeDownloadLink aufruft. Diese Funktion selektiert das von D3.js erzeugte SVG, löscht einige unnötige Elemente, und speichert die Karte als SVG-Datei ab. Somit kann die generierte Karte bearbeitet und in andere Formate umgewandelt werden.

Wer das Vorgehen genauer nachvollziehen möchte, der sei wie gesagt auf das Github-Projekt verwiesen.