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 brauchenrow
let reg
) im ersten und zweiten Eintrag von row
mit den Indizes 0
und 1
row
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
CONFIRMED
der 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 = "▶";