Jetzt da wir wissen wie man eine Karte in D3 rendert können wir eine Schweizerkarte der Kantone mit D3 generieren. Den Code für dieses Modul kannst du auf Github finden.
- Einführung und Ziel
- Base map
- Choroplethenkarte
- Interaktivität
- Bonus: Hover events
Diese Karte der Schweiz werden wir von Grund auf in D3 gestalten. Die geografischen Daten wurden auf ein WGS84 Koordinatensystem umgewandelt damit sie einfacher zu benutzen sind mit d3-geo. Das Geojson file findest du in diesem Github Repo.
Die Kantone im Geojson enthalten auch metadaten als "properties" und können verwendet werden, um eine Visualisierung zu machen. In diesem Fall geht es um verschiedene Wahlresultate von Volksabstimmungen.
Um eine Schweizerkarte zu rendern müssen wir zuerst die geografischen Daten laden.
// map.js
document.addEventListener("DOMContentLoaded", () => {
d3.json("/ch.json")
.then(ch => {
// Visualisierung
})
})
Die Daten sind bereits im geojson Format und somit vorbereitet für die Visualisierung mit d3-geo. Wir müssen also nur die Kartenprojektion und den Pfadgenerator definieren.
d3.json("/ch.json")
.then(ch => {
const width = 800
const height = 600
const svg = d3.select("svg")
// https://www.swisstopo.admin.ch/en/knowledge-facts/surveying-geodesy/reference-systems/map-projections.html
const projection = d3.geoMercator()
.translate([width/2, height/2])
.rotate([-7.43864, -46.95108, 0])
.center([0.54, -0.1])
.scale(13000)
const path = d3.geoPath().projection(projection)
const cantons = svg.selectAll("path")
.data(ch.features)
.enter()
.append("path")
.attr("d", path)
.attr("stroke", "#FFF")
.attr("stroke-width", 1)
})
Jetzt haben wir eine Base-map die noch keine Farben zeigt aber bereits eine Schweiz mit Kantonsgrenzen. Im nächsten Schritt müssen wir die Abstimmungen definieren und dann die Daten an eine Farbskala binden damit wir die Kantone als eine Choroplethenkarte rendern können.
Weil wir zwischen beiden Abstimmungen hin und her wechseln wollen definieren wir auch eine currentInitiative
Variabel,die die im Moment ausgewählte Abstimmung beinhaltet.
const initiatives = [
{
id: 0,
name: "Erleichterte Einbürgerung der dritten Ausländergeneration",
datum: "12.02.2017",
},
{
id: 1,
name: "Atomausstiegsinitiative",
datum: "27.11.2016",
},
]
let currentInitiative = initiatives[0]
// ...
Der obige Code kann am Anfang des Dokuments definiert werden und ist nicht von den Daten, die asynchron geladen werden abhängig.
Jetzt können wir die Farbskala und die Farbe unserer Kantone definieren.
// ...
const colorScale = d3.scaleLinear()
.domain([20, 50, 50.01, 80])
.range(["#FF585D", "#FFF6F6", "#F2F9F9", "#008C95"])
cantons
.attr("fill", d => {
return colorScale(d.properties.initiatives[currentInitiative.id].ja)
})
Hier definieren wir unsere Farbskala von 20-80 (unsere Abstimmungsresultate sind nie grösser oder kleiner als 80% und 20%). Obwohl es sich hier um eine divergierende Skala handelt definieren wir 4 Schnittstellen weil in einer Abstimmung 50% als nicht angenommen gilt. Somit muss 50 + 1 Stimme vorhanden sein um ein positives Resultat zu erzielen. In diesem Fall benutzen wir 50.01 um diese eine Stimme zu markieren (unsere Resultate sind nicht so genau und beschränken sich auf eine Dezimalstelle).
Wir bedienen uns der cantons
Variabel, welche uns erlaubt unsere Kantone weiter zu definieren. Im Attribut fill
berechnen wir unsere Farbe dynamisch mittels colorScale
und dem Ja-Stimmenanteil.
Um unseren Benutzern zu erlauben zwischen den beiden Abstimmungen zu wechseln müssen wir ein Select-Element in HTML definieren und dann das Verhalten dieses Select-Elements in Javascript definieren.
<div id="map">
<!-- Select-Element -->
<div class="select">
<select id="initiativeSelector">
<option value="0">
Erleichterte Einbürgerung der dritten Ausländergeneration — 12.02.2017
</option>
<option value="1">
Atomausstiegsinitiative — 27.11.2016
</option>
</select>
</div>
<!-- Select-Element -->
<svg viewBox="0 0 800 600"></svg>
</div>
Jetzt da wir ein Select-Element haben können wir in Javascript einen sogenannten onChange
handler registrieren. Mit d3 sieht das so aus:
// ...
d3.select("#initiativeSelector")
.on("change", e => {
currentInitiative = initiatives[e.target.value]
cantons
.transition()
.duration(250)
.attr("fill", d => {
return colorScale(d.properties.initiatives[currentInitiative.id].ja)
})
})
Wir holen uns das Select aus dem DOM mittels d3.select()
und benutzen .on()
um einen "change"
handler zu definieren. In diesem handler ersetzen wir die currentInitiative
Variabel mit der neu ausgewählten Abstimmung und benutzen diese neue currentInitiative
Variabel um unsere Farben wiederum dynamisch zu definieren. Also bonus benutzen wir .transition()
und duration(250)
um eine mini-animation mit d3 hinzuzufügen.
Jetzt haben wir eine Karte mit einem Toggle zwischen zwei Abstimmungsresultaten.
Als bonus können wir jetzt einen Hover-Event hinzufügen der uns erlaubt die Resultate für jeden Kanton unter der Karte zu sehen wenn wir den Mauszeiger über den Kanton auf der Karte halten.
Wieder müssen wir zuerst einen Paragraph für unseren Text in HTML definieren und dann können wir diesen Paragraph benutzen um den Text dynamisch in Javascript einzusetzen.
<div class="map">
<div class="select">
<select id="initiativeSelector">
<option value="0">
Erleichterte Einbürgerung der dritten Ausländergeneration — 12.02.2017
</option>
<option value="1">
Atomausstiegsinitiative — 27.11.2016
</option>
</select>
</div>
<svg viewBox="0 0 800 600"></svg>
<!-- Paragraph -->
<p id="info"></p>
<!-- Paragraph -->
</div>
In Javascript können wir jetzt das Verhalten des Texts im Paragraph definieren.
// ...
cantons
.on("mouseenter", (e, d) => {
const initiativeData = d.properties.initiatives[currentInitiative.id]
const passed = initiativeData.passed
const ja = initiativeData.ja
const cantonName = d.properties.name
d3.select("#info")
.style("color", passed ? "#008C95" : "#FF585D")
.text(cantonName + " — " + (passed ? ja + "% Ja" : 100 - ja + "% Nein"))
})
.on("mouseleave", () => {
d3.select("#info").text("")
})
Das sieht vielleicht ein bisschen einschüchternd aus, aber das Prinzip ist relativ einfach. Wie zuvor bedienen wir uns der cantons
Variabel um einen mouseenter
und einen mouseleave
Event mit .on()
zu definieren. Das erlaubt uns auf den Mauszeiger zu reagieren.
Wenn wir mit dem Mauszeiger auf einem Kanton sind möchten wir unter der Karte den Namen und den Ja/Nein Stimmenanteil anzeigen, je nachdem ob im Kanton mehr Ja- oder Nein-Stimmen vorhanden waren.
Wir haben in unserem Datensatz den Ja-Stimmenanteil und ein Ja/Nein Feld, das uns sagt ob der Kanton Ja oder Nein gestimmt hat. Den Nein-Stimmenanteil berechnen wir in Fällen wo die passed
Variabel false
ist.
Im mouseleave
Event müssen wir dann den ganzen Text wieder aus dem Infoparagraph entfernen.
Nun haben wir unsere erste Interaktive Karte in d3. In den nächsten Schritten würden wir eine Legende hinzufügen und noch weitere Informationen anzeigen (z.B. Stimmbeteiligung). Karten wie vieles Andere in Design sind halt nie wirklich fertig...