Interaktive Karten mit d3.js
Siehe auch
D3.js - Daten visualisieren
D3.js ist ein Tool zur Visualisierung von Daten. Man kann auch interaktive Karten damit erstellen. Der Prozess von Gis Shape Files bis zur fertigen Karte wird hier erklärt.
Links:[Bearbeiten]
http://www.tnoda.com/blog/2013-12-07 (Vorbild für dieses Tutorial) http://www.naturalearthdata.com/ (Geodaten) http://www.gdal.org/ (Geospatial Data Abstraction Library - GDAL, Tools zum Arbeiten mit Geodaten) https://mapshaper.org/ (Online Editor für Kartendaten. Akzeptiert GeoJSON, TopoJSON, DBF und csv Dateien)
Beispieldateien[Bearbeiten]
File:ne_10m_populated_places.zip File:ne_10m_admin_1_states_provinces_lakes.zip File:ne_10m_admin_0_countries_lakes.zip File:interactive_d3_map-master.zip (Fertiges Beispiel Projekt)
Geodaten konvertieren - Tools installieren[Bearbeiten]
Wir gehen von Shapefiles aus (.shp / Quelle s.o.). Damit man die Daten für eine WebApp nutzen kann wandeln wir sie in GeoJSON Daten um. Diese wiederum in TopoJSON um die Größe zu veringern (Kurven werden dadurch etwas weniger detailiert).
Größenvergleich ausgehend von Shapefile mit ca. 9MB
Shapefile: 9MB GeoJSON: 25 MB Topojson: 2,5 MB
Tipp: Viele Daten gibt es mittlerweile direkt als GeoJSON. So spart man sich den ersten Schritt.
.shp -> geojson[Bearbeiten]
Geht mit dem Command Line Tool ogr2ogr aus der GDAL (siehe Links)
Installation unter MacOS (mit brew):
brew install gdal
GeoJSON -> Topojson[Bearbeiten]
Für die Konvertierung nach Topojson gibt es das topojson Tool. Es läßt sich über node.js installieren: Installation
sudo npm install -g topojson
Konvertieren und Vereinfachen[Bearbeiten]
Konvertierung von .shp nach .json[Bearbeiten]
ogr2ogr -f GeoJSON meinJsonFile.json meinShapeFile.shp
Wir machen das Beispiel aus dem in den Links genannten Tutorial.
Beginnend mit dem Country Shapefile. Im ne_10m_admin_0_countries_lakes Verzeichnis:
ogr2ogr -f GeoJSON countries.json ne_10m_admin_0_countries_lakes.shp
Dies erzeugt ein GeoJSON File "countries.json" from the shapefile. Wenn man sich die Datei anschaut sieht man eine Menge Zahlen und Attriubute wie:
ADMIN, SU_A3, NAME
Außerdem alle Länder inklusive der Antarktis
Antarktis ausschließen[Bearbeiten]
Als Übung entfernen wir die Antarktis. In ogr2ogr kann man ähnlich wie in SQL Conditional Statements nutzen:
ogr2ogr -f GeoJSON -where "SU_A3 <> 'ATA'" countries.json ne_10m_admin_0_countries_lakes.shp
entfernt die Antarktis.
Umwandlung nach Topojson Attribute auswählen[Bearbeiten]
Das GeoJSON file ist unteranderem so groß, da es eine Menge Attribute enthält. Viele davon benötigen wir nicht. Wenn wir im nächsten Schritt nach Topjson umwandeln suchen wir uns nur die nötigsten heraus: SU_A3 und NAME
topojson --id-property SU_A3 -p name=NAME -p name -o countries.topo.json countries.json
Das erste -p konvertiert die Attribute von uppercase to lowercase Schreibweise und das zweite -p wählt 'name' als ein Attribute. Topojson hat zwar weniger als 10% der GeoJSON Datei aber immer noch 2.5MB. Wir wollen die Größe weiter reduzieren.
Hinweis zu Topojson: Topojson ist eine GeoJSON Erweiterung. Im Prinzip arbeiten wir also immer noch mit GeoJSON Daten, das Datenformat ist jedoch komplexer. Topojson kann Daten in vielen Fällen stark reduzieren. Das funktioniert vor allem bei angrenzenden Formen (z.B. Bundesländer) In anderen Fällen wie z.B. unorganisierten Punktdaten (z.B. Städte) bringt es nichts. Topojson hat noch einige weitere Funktionen, die mit reinen GeoJSON Daten nicht möglich sind.
Formen vereinfachen[Bearbeiten]
Als nächstes vereinfachen wir die Polygone in unserer Datei.
Methode 1 mit ogr2ogr[Bearbeiten]
Mit der -simplyfy Methode von ogr2ogr kann man Polygone vereinfachen wir fangen nochmal mit dem GeoJSON an:
ogr2ogr -f GeoJSON -simplify 0.2 -where "SU_A3 <> 'ATA'" countries.json ne_10m_admin_0_countries_lakes.shp topojson --id-property SU_A3 -p name=NAME -p name -o countries.topo.json countries.json
Größenmäßig macht das viel aus, aber die Grenzen sind nicht mehr sauber aufeineander. Es entstehen Lücken zwischen den Ländern.
Methode 2 mit Mapeshaper[Bearbeiten]
Das Online Tool Mapshaper.org hat bessere Algorhythmen (Douglas-Peucker and Visvalingam) und kann Layer Topologien bewahren. Außerdem unterstützt es viele Formate (s.o.).
Mehr Layer, mehr Daten, mehr GeoJSON[Bearbeiten]
Staatentopologie[Bearbeiten]
Wir verfahren bei den Staaten wie mit den Ländern. Wir erzeugen ein Staate und Provinzen TopoJSON Hier im Beispiel mit USA und Japan.
Da die Staaten später über lazy load bei Bedarf geladen werden lassen wir die Länder in separaten .json Dateien. So wird immer nur das benötigte geladen.
ogr2ogr -f GeoJSON -where "gu_a3 = 'USA'" states.json ne_10m_admin_1_states_provinces_lakes.shp mv states.json states_usa.json ogr2ogr -f GeoJSON -where "gu_a3 = 'JPN'" states.json ne_10m_admin_1_states_provinces_lakes.shp mv states.json states_jpn.json
Trotzdem verwenden wir für beide Länder den Dateinamen states.json. Der Name wird auch als Objektname in den Daten genutzt. So ist später einfacher darauf zuzugreifen da für jedes Land der Name gleich bleibt.
Wieder die GeoJSON Files mit mapshaper vereinfachen dann zu TopoJSON konvertieren:
topojson --id-property adm1_cod_1 -p name -o states_usa.topo.json states_usa.json topojson --id-property adm1_cod_1 -p name -o states_jpn.topo.json states_jpn.json
Die Dateien kann man damit jeweils auf 5-20kB reduzieren.
Städte[Bearbeiten]
Die Städte sind Punktdaten. Wir holen uns die Hauptstädte in USA und Japan. Die Popularität der Städte is im scalerank skaliert. Das ist die Skala für den Map Zoom. Dies steuert die Sichtbarkeit in verschidenen Zoom-Stufen. Man kann scalerank = 10 nur sehen wenn man 10 Schritte eingezoomt hat.
Bei der Extrahierung der Städte nehmen wir die Staaten mit. Diese brauchen wir später zum Filtern. Die Befehle:
ogr2ogr -f GeoJSON -where "ADM0_A3 = 'USA' and SCALERANK <= 4" cities.json ne_10m_populated_places.shp topojson -p name=NAME -p state=ADM1NAME -p name -p state -o cities_usa.topo.json cities.json
...
ogr2ogr -f GeoJSON -where "ADM0_A3 = 'JPN' and SCALERANK <= 6" cities.json ne_10m_populated_places.shp topojson -p name=NAME -p state=ADM1NAME -p name -p state -o cities_jpn.topo.json cities.json
Visualisierung mit d3[Bearbeiten]
Jetzt können die Daten mit d3.js visualisiert werden.
Das Beispiel nutzt hauptsächlich die d3 geo library. Der Code ist einfach gehalten und kann deutlich verbessert werden.
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var m_width = $("#map").width(),
width = 938,
height = 500,
country,
state;
var projection = d3.geo.mercator()
.scale(150)
.translate([width / 2, height / 1.5]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#map").append("svg")
.attr("preserveAspectRatio", "xMidYMid")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("width", m_width)
.attr("height", m_width * height / width);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", country_clicked);
var g = svg.append("g");
d3.json("/json/countries.topo.json", function(error, us) {
g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(topojson.feature(us, us.objects.countries).features)
.enter()
.append("path")
.attr("id", function(d) { return d.id; })
.attr("d", path)
.on("click", country_clicked);
});
function zoom(xyz) {
g.transition()
.duration(750)
.attr("transform", "translate(" + projection.translate() + ")scale(" + xyz[2] + ")translate(-" + xyz[0] + ",-" + xyz[1] + ")")
.selectAll(["#countries", "#states", "#cities"])
.style("stroke-width", 1.0 / xyz[2] + "px")
.selectAll(".city")
.attr("d", path.pointRadius(20.0 / xyz[2]));
}
function get_xyz(d) {
var bounds = path.bounds(d);
var w_scale = (bounds[1][0] - bounds[0][0]) / width;
var h_scale = (bounds[1][1] - bounds[0][1]) / height;
var z = .96 / Math.max(w_scale, h_scale);
var x = (bounds[1][0] + bounds[0][0]) / 2;
var y = (bounds[1][1] + bounds[0][1]) / 2 + (height / z / 6);
return [x, y, z];
}
function country_clicked(d) {
g.selectAll(["#states", "#cities"]).remove();
state = null;
if (country) {
g.selectAll("#" + country.id).style('display', null);
}
if (d && country !== d) {
var xyz = get_xyz(d);
country = d;
if (d.id == 'USA' || d.id == 'JPN') {
d3.json("/json/states_" + d.id.toLowerCase() + ".topo.json", function(error, us) {
g.append("g")
.attr("id", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter()
.append("path")
.attr("id", function(d) { return d.id; })
.attr("class", "active")
.attr("d", path)
.on("click", state_clicked);
zoom(xyz);
g.selectAll("#" + d.id).style('display', 'none');
});
} else {
zoom(xyz);
}
} else {
var xyz = [width / 2, height / 1.5, 1];
country = null;
zoom(xyz);
}
}
function state_clicked(d) {
g.selectAll("#cities").remove();
if (d && state !== d) {
var xyz = get_xyz(d);
state = d;
country_code = state.id.substring(0, 3).toLowerCase();
state_name = state.properties.name;
d3.json("/json/cities_" + country_code + ".topo.json", function(error, us) {
g.append("g")
.attr("id", "cities")
.selectAll("path")
.data(topojson.feature(us, us.objects.cities).features.filter(function(d) { return state_name == d.properties.state; }))
.enter()
.append("path")
.attr("id", function(d) { return d.properties.name; })
.attr("class", "city")
.attr("d", path.pointRadius(20 / xyz[2]));
zoom(xyz);
});
} else {
state = null;
country_clicked(country);
}
}
$(window).resize(function() {
var w = $("#map").width();
svg.attr("width", w);
svg.attr("height", w * height / width);
});
</script>