Als Vorlage für das HTML Grundgerüst verwenden wir template.zip -> auspacken als username.github.io/world
COVID-19 Data Repository vom Center for Systems Science and Engineering (CSSE) at Johns Hopkins University ist die Datenquelle
Raw klicken um zu den CSV Rohdaten zu gelangenCSV in JSON und dann JS konvertieren
CSV To JSON Array als Choose Conversion Typeund fügen eine Variablendeklaration mit const dazu - data.js sieht nach Konvertierung der drei CSV-Files dann so aus:
const CONFIRMED=,// konvertierter Inhalt von confirmed
const DEATHS=,// konvertierter Inhalt von deaths
const RECOVERED=// konvertierter Inhalt von recovered
diese drei Konstanten sind die Grundlage für unsere Visualisierung
wir binden data.js als Skript in index.html ein. In welcher Reihenfolge wird data.js einbinden ist egal, denn über das defer-Attribut beim main.js ist sichergestellt, dass alle Daten geladen sind bevor wir die Karte zeichnen
<script src="data.js"></script>
und testen sie mit console.log in main.js
console.log(CONFIRMED);
console.log(DEATHS);
console.log(RECOVERED);
wir implementieren das Zeichnen in einer eigenen Funktion drawMarker
for-Schleife arbeiten wir die einzelnen Datensätze der Konstanten CONFIRMED ab
i=1 weil bei i=0 der Datenheader steht, den wir erst später brauchenrowlet reg) im ersten und zweiten Eintrag von row mit den Indizes 0 und 1row mit den Indizes 2 (let lat) und 3 (let lng)row-Arrays und kann über row.length - 1 angesprochen werden (let val)der komplette Code unsere drawMarker-Funktion, die wir ganz zum Schluss natürlich aufrufen müssen um die Marker auch wirklich zu zeichnen, sieht damit so aus:
let drawMarker = function () {
for (let i = 1; i < CONFIRMED.length; i++) {
let row = CONFIRMED[i];
let reg = `${row[0]} ${row[1]}`;
let lat = row[2];
let lng = row[3];
let val = row[row.length - 1];
let mrk = L.marker([lat, lng]).addTo(map);
mrk.bindPopup(`${reg} ${val}`);
}
}
drawMarker();
aus den Markern machen wir jetzt Flächen-proportionale Größenkreise mit zusätzlicher Skalierung - deshalb benennen wir die Funktion drawMarker in drawCircles um
Fläche = Radius² * PI berechnen wir den Radius (let r)let s=0.5) let drawCircles = function () {
for (let i = 1; i < CONFIRMED.length; i++) {
// reg, lat, lng, val definieren ...
let s = 0.5;
let r = Math.sqrt(val * s / Math.PI);
let circle = L.circleMarker([lat, lng], {
radius: r
}).addTo(map);
circle.bindPopup(`${reg}: ${val}`);
}
}
die Kreise zeichnen wir in ein eigenes ein-/ausschaltbares Overlay
oberhalb von L.control.layers eine neue L.featureGroup hinzufügen und an die Karte hängen
let circleGroup = L.featureGroup().addTo(map);
in L.control.layers das neue Overlay einbauen
L.control.layers({
// baselayers
}, {
"Thematische Darstellung" : circleGroup
}).addTo(map);
bei L.circleMarker die Kreise ans Overlay hängen
let circle = L.circleMarker([lat, lng], {
radius: r
}).addTo(circleGroup);
in drawCircles ist sowohl der zu visualisierende Datensatz als auch der anzuzeigende Datenwert fix eingestellt
CONFIRMEDder Datenwert ist das letzte Element des jeweiligen Datenarrays -> row[row.length - 1]
um später alle drei Themen und die Daten zu allen Zeitpunkten visualisieren zu können ist es besser, diese Informationen in Variablen festzuhalten, die wir dann leicht an einer einzigen Stelle ändern können. Deshalb führen wir am Beginn der drawCircles Funktion zwei neue Variablen ein - eine bestimmt das Thema (let data), die zweite den Index des Datenwerts (let index) den wir anzeigen wollen - wir nehmen dazu den Index des letzte Elements des ersten Datensatzes
let data = CONFIRMED;
let index = CONFIRMED[0].length - 1;
zusätzlich merken wir uns den Header mit den Zeitstempeln im ersten Datensatz von CONFIRMED gleich mit - der Header ist bei allen Themen gleich, also nehmen wir einfach einen davon
let header = CONFIRMED[0];
drawCircles nur alle Vorkommen von CONFIRMED mit data ersetzen und statt row.length - 1 die Variable index verwendenWir sehen zwar schon Kreise, wissen aber nicht, welchen Datenwert sie repräsentieren. Deshalb zeigen wir das Thema und Datum des Datenwerts beim Header der Karte an. Der Vorgang dabei:
in index.html ein span-Element mit id="datum" zum H2-Element im header-Bereich hinzufügen
in main.js mit document.querySelector eine Referenz auf diesen Span erzeugen und dessen Inhalt setzen
header[index]das Thema setzen wir unterhalb von let header vorerst fix auf “bestätigte Fälle”
let topic = "bestätigte Fälle";
mit Template-Syntax setzen wir unterhalb der for-Schleife das .innerHTML unseres Spans
document.querySelector("#datum").innerHTML = `am ${header[index]} - ${topic}`;
im index.html unterhalb der Karte ein select-Element mit der ID pulldown einfügen - siehe auch MDN <select>
<select id="pulldown">
<option value="confirmed" selected>bestätigte Fälle</option>
<option value="deaths">Verstorbene</option>
<option value="recovered">Genesene</option>
</select>
selected bestimmt, welcher Wert voreingestellt werden soll
in main.js auf Änderungen im Auswahlmenü reagieren - dazu verwenden wir einen onchange Eventhandler auf unser Pulldown mit der ID pulldown
oberhalb von drawCircles(); fügen wir diesen Codeblock ein
document.querySelector("#pulldown").onchange = function() {
drawCircles();
}
damit können wir schon mehrmals zeichnen (die Kreise werden immer dunkler weil sie sich überlagern), haben aber noch keinen Wechsel der Daten
den Datensatz aus dem Auswahlmenü bestimmen und neu zeichnen
in drawCircles speichern wir zuerst eine Referenz auf die Optionen des Pulldowns
let options = document.querySelector("#pulldown").options;
in der Variablen options finden wir jetzt eine HTMLOptionsCollection die die drei Einträge des Pulldowns enthält und in options.selectedIndex den Index des ausgewählten Eintrags speichert
über .value und .text des selektierten Eintrags (i.e. options[options.selectedIndex]) können wir auf den Wert (confirmed, deaths oder recovered) und den Label des Eintrags (bestätigte Fälle, Verstorbene oder Genesene) zugreifen
let value = options[options.selectedIndex].value;
let label = options[options.selectedIndex].text;
label ersetzt unseren hard-gecodeten topic
value erlaubt uns vor der for-Schleife den Datensatz in einer if-Abfrage entsprechend zu setzen
if (value === "confirmed") {
data = CONFIRMED;
} else if (value === "deaths") {
data = DEATHS;
} else {
data = RECOVERED;
}
dann müssen wir nur noch die bestehenden Kreise vor dem Neuzeichnen mit clearLayers löschen. Nachdem unsere Kreise immer in die circleGroup gezeichnet werden ist das nach der if-Abfrage einfach zu lösen
circleGroup.clearLayers();
slider hinzu - siehe auch MDN <input type=”range”>
<input id="slider" type="range">
#slider {
width: 80%;
}
in main.js (ab jetzt) initialisieren wir die Konfiguration des Sliders direkt vor dem letzten drawCircles() Aufruf. Bei Slidern kann man Minimum, Maximum, Schrittweite und den aktuellen Wert definieren.
das Minimum (min) entspricht dem Index des ersten Datenwerts eines Datensatzes - bei uns ist das 4, denn davor stehen noch zwei Spalten mit administrativen Einheiten sowie Lat/Lng
das Maximum (max) entspricht dem Index des letzten Datenwerts - nachdem alle Datensätze gleich lang sind können wir ihn vom ersten Datensatz direkt ableiten
als Schrittweite step verwenden wir 1 - damit können wir von einem Index zum nächsten wechseln
als voreingestellten Wert (value) verwenden schließlich den letzten, sprich neuesten Datensatz - das ist also slider.max
let slider = document.querySelector("#slider");
slider.min = 4;
slider.max = CONFIRMED[0].length - 1;
slider.step = 1;
slider.value = slider.max;
auf Änderungen im Auswahlmenü reagieren wir wieder in einem onchange-Event-Listener den wir direkt unter den Code der Initialisierung schreiben
slider.onchange = function() {
drawCircles();
};
drawCircles müssen wir dann nur mehr den aktuellen Wert des Sliders berücksichtigen indem wir index entsprechend setzen
let index = document.querySelector("#slider").value;
Tipp: Wenn wir den Slider anklicken, können wir auch über die Pfeiltasten links, rechts zu den einzelnen Zeitpunkten wie bei einer Animation wechseln
Unterschiedliche Farben bei den Kreisen nach Thema lassen sich in der if-Abfrage bei L.circleMarker definieren. Wir verwenden dabei Farben von https://clrs.cc/
let color;
if (value === "confirmed") {
data = CONFIRMED;
color = "#0074D9"; // Blue
} else if (value === "deaths") {
data = DEATHS;
color = "#B10DC9"; // PURPLE
} else {
data = RECOVERED;
color = "#2ECC40"; // GREEN
}
Bei L.circleMarker müssen wir dann noch die Farbe mit color : color unterhalb von radius : r setzen
Schönheitsfehler: leider überdecken die großen Kreise die kleinen Kreise (z.B. US) was dazu führt, dass wir Popups der darunter liegenden Länder nicht öffnen können. Deshalb müssen wir die Daten absteigend innerhalb der aktuellen Datenspalte sortieren womit große Kreise zuerst gezeichnet und kleine Kreise später darüber gelegt werden.
direkt vor der for-Schleife sortieren wir data mit Hilfe einer Sortierfunktion - siehe MDN Array.prototype.sort()
data.sort(function compareNumbers(row1, row2) {
//console.log(index, row1[index], row2[index])
return row2[index] - row1[index];
});
Was passiert dabei?
index ist der Index der aktuellen Datenspalte der weiter oben in der drawCircles Funktion aus dem Slider abgeleitet wurderow1 und row2 beinhalten die zwei aufeinanderfolgenden Datenarrays die ich vergleichen willrow2[index] - row1[index] ist entweder kleiner 0, 0 oder größer als 0
row1 auf einen niedrigeren Index als row2 sortiert, d. h. row1 kommt zuerst0 bleibt es unverändert0, wird row2 auf einen niedrigeren Index als row1 sortiert, d. h. row2 kommt zuerstWir starten und pausieren die Animation mit einem HTML input-Element vom Typ button den wir in index.html nach dem Pulldownmenü einfügen. Als Label für den Button verwenden wir ein passendes Symbol von https://en.wikipedia.org/wiki/Media_control_symbols
<input id="play" type="button" value="▶">
in main.js (ab jetzt) speichern wir eine Referenz auf den Button und reagieren mit einem onclick Event-Listener auf Klicks auf den Button. In dieser Funktion ermitteln wir zuerst den aktuellen Wert des Sliders und setzen ihn auf den ersten Datensatz, wenn wir schon beim letzten Datensatz sind. Unsere Animation wird damit entweder am Anfang oder der aktuellen Position gestartet.
let playButton = document.querySelector("#play");
playButton.onclick = function () {
let value;
if (slider.value == slider.max) {
value = slider.min;
} else {
value = slider.value;
}
}
Animieren mit Hilfe von window.setInterval und window.clearInterval
Der Code zum Animieren der Kreisgrößen sieht in seiner ersten Version so aus:
let runningAnimation = null;
playButton.onclick = function () {
// Wert des Sliders in value ermitteln
runningAnimation = window.setInterval(function () {
slider.value = value;
drawCircles();
value++;
if (value > slider.max) {
window.clearInterval(runningAnimation);
runningAnimation = null;
}
}, 250)
}
was passiert dabei?
onclick Event-Listeners definieren wir die Variable runningAnimation die uns helfen wird festzustellen, ob gerade eine Animation läuft oder nichtonclick Event-Listeners wiederholt window.setInterval alle 250 Millisekunden den Codeblock seiner Funktion und speichert die ID dieser Animation in runningAnimation ab - siehe auch MDN .setInterval(). In der Funktion passiert Folgendes:
window.clearInterval stoppen - siehe auch MDN .clearInterval()window.clearInterval benötigt dazu die ID der laufenden Animation in runningAnimation und setzt selbige danach wieder auf null um anzuzeigen, dass keine Animation mehr läuftDamit läuft unsere Animation vom Start-, bzw. aktuell beim Slider eingestellten Zeitpunkt bis zum letzten Datensatz durch und stoppt dann
Pausieren der Animation
Die Implementierung der Pause-Taste erfolgt in einer if-Abfrage die ermittelt, ob beim Klick auf den Button gerade eine Animation läuft, oder nicht. Ob, oder ob nicht entscheidet der aktuelle Wert von runningAnimation. Ist er nicht null (die ID von setInterval ist übrigens eine Zahl) läuft die Animation und wir stoppen sie mit clearInterval und setzen danach runningAnimation auf null, ist er null läuft keine Animation und wir starten die Animation neu.
if (runningAnimation) {
window.clearInterval(runningAnimation);
runningAnimation = null;
} else {
runningAnimation = window.setInterval(function () {
// Animation
}, 250)
}
Damit können wir die Animation an jeder beliebigen Stelle durch Klick auf den Button stoppen, bzw. starten. Als optisches Feedback bleibt noch, den Label des Buttons auf play oder pause zu setzen:
vor der if (runningAnimation) {...} Abfrage setzen wir per default ein Pause-Zeichen
playButton.value = "⏸";
innerhalb von if (runningAnimation) {...} beim Stoppen setzen wir wieder ein Play-Zeichen
playButton.value = "▶";
innerhalb von if (value > slider.max) {...} beim automatischen Beenden setzen wir wieder ein Play-Zeichen
playButton.value = "▶";