Weiter geht es mit meiner aktuellen Lieblings Javascript Bibliothek – Heute möchte ich euch zeigen wie wir ein Balkendiagramm mit D3 erstellen können.

Zunächst möchte ich auf mein erstes Posting zum Thema D3 hinweisen. Es ist wichtig zu wissen wie D3 mit Daten umgeht – wenn auch nur grob. In diesem Posting werden wir die Arbeitsweise von D3 auf eine Visualisierung anwenden.

Wetterdaten als Datenbasis

Um ein Balkendiagramm mit D3 darzustellen benötigen wir in erster Linie Daten. Als Datenbasis benutzen wir eine CSV Datei mit Wetterdaten aus dem Jahr 2019. Hier habe ich eine kostenlose „Probe“ bekommen: https://www.meteoblue.com/de/wetter/archive/export/basel_schweiz_2661604 – ich habe die Inhalte der Datei etwas „gekürzt“ damit wir nur relevante Daten erhalten – die Datei heißt wetterdaten.csv und schaut so aus:

Year,Month,Day,maxTemp,minTemp
2019,1,1,7.10,3.84
2019,1,2,4.55,0.08
2019,1,3,2.72,-1.65
2019,1,4,4.74,-1.50
2019,1,5,3.10,0.26
2019,1,6,5.46,1.29
2019,1,7,4.55,1.19
2019,1,8,5.80,2.38
2019,1,9,3.75,0.48
2019,1,10,2.70,-1.33
2019,1,11,3.70,-1.50
2019,1,12,5.70,1.76
2019,1,13,7.68,4.81
2019,1,14,7.15,3.89
2019,1,15,6.53,0.17
2019,1,16,10.56,0.13
2019,1,17,8.28,2.62
2019,1,18,3.91,-2.02
2019,1,19,2.80,-1.49
2019,1,20,6.45,-0.57
2019,1,21,2.25,-1.44
2019,1,22,3.34,-2.34
2019,1,23,4.75,-0.71
2019,1,24,0.57,-3.75
2019,1,25,0.94,-2.80
2019,1,26,8.49,0.33
2019,1,27,9.78,2.67
2019,1,28,3.58,1.27
2019,1,29,5.17,-0.73
2019,1,30,2.78,-0.31
2019,1,31,6.86,-2.26

Wir benötigen für das Balkendiagramm mit D3 aus technischer Sicht nur zwei Dinge:

  • Je eine Achse für Datum und Temperatur
  • Die „Balken“ zum repräsentieren der Daten

Um ein Balkendiagramm mit D3 zu erstellen benutzen wir SVG. Bei „Scalable Vector Graphics“ handelt es sich nämlich in unserem Fall um Elemente im DOM. Das wiederum bringt einige andere Vorteile mit sich die ich in der Vergangenheit bereits beschrieben habe (vielleicht aktualisiere ich das Posting demnächst mal).

<!-- Das HTML mit dem wir arbeiten werden -->
<svg id="canvas" style="width:1200px; height:400px; border: 1px solid #000;"></svg>
// Vorbereiten des SVG und einiger Maße
const svg = d3.select('#canvas');
const width = parseInt(svg.style('width'));
const height = parseInt(svg.style('height'));
const margin = {top: 20, right: 10, bottom: 30, left: 40};

// Vorbereiten der Daten
const data = d3.csv('wetterdaten.csv', d => ({
    // Dieser erste Callback lässt uns die Daten Zeilenweise bearbeiten.
    // Jede Zeile besteht aus einem Datum und min/max Temperatur.
    date: new Date(d.Year + '-' + d.Month + '-' + d.Day),
    max: +d.maxTemp,
    min: +d.minTemp
})).then( /* ... */ );

Koordinaten nach Datum berechnen

Okay, die Daten sind geladen, vorbereitet und können an unsere Logik übermittelt werden um tatsächlich etwas zu zeichnen! Zuerst müssen wir zwei Dinge herausfinden um etwas zeichnen zu können: die minimalen und maximalen Werte für die X und Y Koordinaten. In unserem Fall sind das Start- und Enddatum so wie Höchst- und Tiefsttemperaturen.

D3 bringt hier ein paar Helfer mit die wir zu diesem Zweck nutzen können. Zuerst ermitteln wir die Zeitspanne:

const x = d3.scaleTime() // Alternativ d3.scaleLinear()
    .domain(d3.extent(data, d => d.date))
    .range([margin.left, width - margin.right]);

Ich möchte kurz erklären was hier passiert. d3.scaleTime bereitet eine Zeit-Skala vor. Alternativ könnten wir d3.scaleLinear verwenden, doch da wir explizit mit Zeit-Daten arbeiten bietet sich d3.scaleTime hier besser an 🙂

Es gibt darüber hinaus noch etwa fünf weitere Skala-Typen, jeweils für spezifische Anwendungsfälle.

Mittels domain definieren wir den kleinsten und größten Wert den wir für diese Skala setzen möchten, in unserem Fall der 1.Januar 2019 und 31.Dezember 2019. Technisch wird diese Aufgabe von d3.extent übernommen. Hier übergeben wir die Rohdaten data und eine Funktion die den gewünschten Wert zurückliefert d => d.date.

Um die Werte unserer Skala auf den Bildschirm zu zaubern benötigen wir eine weitere Angabe, und zwar die range. Im Grunde definieren wir hierüber die Koordinate für den 1.Januar (= margin.left oder 40) und den 31.Dezember (= width - margin.right oder 1190).

D3 wird anschließend in der Lage sein ein Datum zu einer Koordinate umzurechnen:

x(new Date('2019-01-01')) // = 40
x(new Date('2019-06-17')) // = 567.6811
x(new Date('2019-12-31')) // = 1190

Koordinaten nach Temperatur berechnen

Was wir auf der X-Achse für das Datum gemacht haben, machen wir nun gleichermaßen für die Y-Achse und die min/max Temperaturen:

const y = d3.scaleLinear()
    .domain([d3.min(data, d => d.min), d3.max(data, d => d.max)]).nice()
    .range([height - margin.bottom, margin.top])
    .nice();

Hier können wir nicht d3.extent benutze, da unsere „min/max Temperatur“ Werte in verschiedenen Variablen liegen. Daher die etwas aufwändigere Logik im „domain“ Part.

Ich möchte kurz auf den Zusatz nice() eingehen. D3 wird die Angaben aus „domain“ und „range“ 1:1 übernehmen, das heißt wenn wir die niedrigste Temperatur darstellen klebt diese mit 0 Pixel Abstand am Boden. Gleiches gilt für die höchste Temperatur und die Decke.
Die nice() Methode wird dafür Sorgen das die Werte „hübsch“ auf- bzw. abgerundet werden – es wird ein zusätzlicher Abstand hinzugefügt um aus einer krummen Zahl wie 37 und -4.7 etwas zu machen wie 40 und -5.

Daten übermitteln und Balken zeichnen

Wir haben unser Koordinatensystem fertig und können jetzt starten das Balkendiagramm mit D3 zeichnen zu lassen! Dazu machen wir uns die Informationen aus meinem letzten Posting zu nutze und übermitteln die Daten an eine Kollektion:

// Wir bereiten ein neues <g> Element vor und übergeben die Daten.
const rectGroup = svg.append("g")
    .attr("fill", "#f80")
    .selectAll("rect")
    .data(data);

// Wir erstellen eine "enter" Kollektion und stellen diese dar.
rectGroup
    .enter()
    .append('rect')
    .attr("x", (d, i) => x(d.date))
    .attr("y", d => y(d.max))
    .attr("height", d => y(d.min) - y(d.max))
    .attr("width", 2);

Öffnen wir die Seite in diesem Zustand im Browser sollten wir etwa so etwas sehen:

Gezeichnete Balken, allerdings noch ohne Achsen

Das schaut schon ganz gut aus, aber es fehlen wichtige Informationen: unsere Achsen! Diese können wir mit Hilfe unserer x und y Skalen zeichnen lassen:

svg.append("g")
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x).tickFormat(d3.timeFormat('%B')));

svg.append("g")
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y));

Hier fügen wir je Achse ein neues „Gruppen“ Element (g) zu und lassen D3 mittels d3.axisBottom(x) bzw d3.axisLeft(y) die Achsen zeichnen. Optional können wir hier noch die „ticks“ formatieren – in diesem Fall um die Namen der Monate darzustellen… Und damit erhalten wir folgende Darstellung:

Gezeichnete Balken mit Achsen

Aufgrund der Art und Weise mit D3 mit Daten umgeht brauchen wir keine Loops oder sonstige komplexe Logik!

Die tatsächliche Logik (ohne Bootstrapping und vorbereiten der Daten) beträgt rund 30 Zeilen und bietet eine sehr solide Basis auf der wir aufbauen können, wenn uns das aktuelle Ergebnis noch nicht ausreicht.

Das komplette Skript

<svg id="canvas" style="width:1200px; height:400px; border: 1px solid #000;"></svg>
const svg = d3.select('#canvas');
const width = parseInt(svg.style('width'));
const height = parseInt(svg.style('height'));
const margin = {top: 20, right: 10, bottom: 30, left: 40};

const data = d3.csv('wetterdaten.csv', d => ({
    date: new Date(d.Year + '-' + d.Month + '-' + d.Day),
    max: +d.maxTemp,
    min: +d.minTemp
})).then(d => drawChart(d));

const drawChart = data => {
    const x = d3.scaleTime()
        .domain(d3.extent(data, d => d.date))
        .range([margin.left, width - margin.right]);
        
    const y = d3.scaleLinear()
        .domain([d3.min(data, d => d.min), d3.max(data, d => d.max)])
        .range([height - margin.bottom, margin.top])
        .nice();
        
    const rectGroup = svg.append("g")
        .attr("fill", "#f80")
        .selectAll("rect")
        .data(data);

    rectGroup
        .enter()
        .append('rect')
        .attr("x", (d, i) => x(d.date))
        .attr("y", d => y(d.max))
        .attr("height", d => y(d.min) - y(d.max))
        .attr("width", 2);

    svg.append("g")
        .attr("transform", `translate(0,${height - margin.bottom})`)
        .call(d3.axisBottom(x).tickFormat(d3.timeFormat('%B')));

    svg.append("g")
        .attr("transform", `translate(${margin.left},0)`)
        .call(d3.axisLeft(y));
};

Weitere Infos, bitte!

Ich möchte in einigen Stichpunkten auf einzelne Zeilen des Skripts eingehen:

  • Die ersten paar Zeilen sind eigentlich unnötig, oder?
    Ja das ist richtig – die Variablen mit Informationen zur Breite, Höhe und Abständen kann man sich „theoretisch“ sparen wenn man genau weiß mit welchen Maßen man arbeitet. Dann ist man allerdings nicht länger responsive. Wir können den Code außerdem als „Boilerplate Code“ immer wieder verwenden 🙂
  • CSV Daten bearbeiten?
    Auch das ist ein optionaler Schritt, es kommt hierbei natürlich immer auf die Daten an die in der CSV stehen – aber auch auf das gewünschte Ergebnis. In unserem Fall war es notwendig mit echten Date Objekten zu arbeiten. Und numerische Werte zu casten kann nie schaden 😉
  • drawChart als Funktion auslagern?
    Auch dieser Schritt ist in unserem Beispiel Optional, wird aber spätestens dann weiterhelfen wenn sich die Daten (oder Maße der Visualisierung) ändern können – in diesem Fall müsste allerdings noch etwas mehr Code eingebaut werden um auf Daten-Änderungen zu reagieren.
  • Achsen anpassen?
    Das ist super einfach möglich, da wir ja im Endeffekt mit Elementen im DOM arbeiten. Das heißt wir können diese sowohl mit CSS stylen als auch manipulieren.

Ich glaube D3 wird ein wiederkehrendes Thema im Blog werden – einfach weil ich es sowohl beruflich als auch Privat immer wieder nutze um mal mehr und mal weniger komplexe Visualisierungen zu erstellen 🙂

Ich hoffe ich konnte euch dieses Thema etwas näher bringen und vielleicht sogar überzeugen selbst mal mit D3 zu arbeiten. Bis zum nächsten mal Wünsche ich euch viel Erfolg mit euren Projekten und natürlich Happy Coding!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.