Published on

Mit Observable Kurvendaten visualisieren

Authors

D3.js (oder auch einfach D3, Abkürzung für "Data-Driven Documents") ist eine beliebte und leistungsfähige Javascript Library für die Visualisierung von Daten jeglicher Art.

D3.js is a JavaScript library for manipulating documents based on data. D3 helps you bring data to life using HTML, SVG, and CSS. D3’s emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, combining powerful visualization components and a data-driven approach to DOM manipulation. (D3.js - Data-Driven Documents)

D3.js ist und wird von Michael Bostock entwickelt, ein Doktorand der Stanford University und ehemaliger Mitarbeiter der New York Times.

Ich selber nutzte D3.js seit 2015 für die Visualisierung von Kurvendaten und ist meine absolute Lieblingsbibliothek für solche Aufgaben.

Seit ein paar Jahren schon gibt es die Plattform Observablehq, welche von Michael Bostock gegründet wurde. Observablehq ist ein sehr beliebtes Notebook zum Schreiben von Code, der die Library D3.js nutzt. Ein Notebook kombiniert Code, Gleichungen, erzählenden Text, Visualisierungen und mehr. Wer sich mit Jupyter Notebook auskennt, der wird sich hier direkt wie zuhause fühlen.

Observable, like Jupyter, is a computational notebook that’s great for doing data science and visualization, where “notebook” refers to a series of cells containing prose, code, and visualizations. Observable runs a superset of JavaScript entirely in your browser, so it draws on a different ecosystem than the Python one you may be used to. (Observable for Jupyter Users, Published Mar 2, 2020 by Observablehq)

Ohne Daten geht nichts

Als Daten sollen uns für dieses Beispiel die Sensordaten eines Temperatursensor dienen. Dabei wird die Temperatur mit einem Zeitstempel geloggt. Der Zeitstempel selber ist als "Epoch time" formatiert.

curveData = [
  {
    date: 1662213247,
    temperature: 25.08,
  },
  {
    date: 1662214447,
    temperature: 24.98,
  },
  {
    date: 1662215647,
    temperature: 24.41,
  },

  //...

  {
    date: 1662298446,
    temperature: 28.33,
  },
]

Epoch time - Was ist das?

Die "Epoch time" oder auch Unixzeit ist eine Zeitdefinition, die für das Bergtriebsystem Unix entwickelt wurde. Die Unixzeit zählt die vergangenen Sekunden seit einem bestimmten Datum, dem Startdatum, das auch als Epoch bezeichnet wird. Diese Zeitformat kann einfach in andere Darstellungen umgewandelt werden. Es hat einige Vorteile z.B. für die Skalierung der X-Achse.

Die Plot-Funktion Plot.plot

Als erstes erstellen wir ein neues leeres Notebook und ändern den Text in der ersten Zelle zum Beispiel in "Kurvendiagramm" um.

Anschließend klicken wir auf das Plus-Zeichen unterhalb der ersten Zelle. Aus der sich öffnenden Liste wählen wir JavaScript aus. Es öffnet sich eine neue Zelle. In diese Zelle fügen wir die Kurvendaten ein.

Nun erzeugen wir eine weitere Zelle, diesmal jedoch wählen wir "Line chart" aus der Auswahlliste (nach unten scrollen) aus.

Jetzt noch den Namen der Kurvendaten und die der Accessoren (für die X- und Y-Daten) anpassen. Anschließend den Play-Button der Zelle klicken.

Mit der Funktion Plot.plot ein Kurvendiagramm erstellen
Mit der Funktion Plot.plot ein Kurvendiagramm erstellen

Anpassen der Kurvendarstellung

Soweit so gut. Mit ein paar Klick sind wir fertig. Aber es geht natürlich noch besser.

Z.B. können wir die Skalierung der Y-Achse so verändern, das die Kurve das Diagramm besser ausnutzt sowie die Beschriftung der Y-Achse anpassen. Hierfür verwenden wird "Scales". In unserem Beispiel heißt der "Scale" für die Y-Achse "y", da dieser bereits durch den "Mark channel" festgelegt ist.

Plot doesn’t have chart types. Instead, it has marks: geometric shapes such as bars, dots, and lines. Yet unlike a conventional graphics system, Plot marks are not positioned in pixels or colored literally. You draw with abstract data! (Plot: Marks and Channels, Published May 4, 2021 by Mike Bostock)

Plot.plot({
  y: {
    ticks: 8,
    label: '↑ Temperature  (°C)',
    domain: [14, 32],
  },
  marks: [
    Plot.ruleY([0]),
    Plot.lineY(curveData, {
      x: 'date',
      y: 'temperature',
    }),
  ],

  //Remaining code omitted for clarity
  //...
})
Line chart mit formatierter Y-Achse
Line chart mit formatierter Y-Achse

Das selbe führen wir dann für die X-Achse durch und wandeln dabei das Epoch-Format in eine für Menschen lesbare Form um. Wegen der besseren Übersichtlichkeit fügen wir auch noch ein Grid hinzu.

dataPlot = Plot.plot({
  //Add vertical and horizontal grid lines
  grid: true,

  x: {
    ticks: 10,
    tickFormat: (d) => new Date(d * 1000).toLocaleString('de-DE'),
    tickRotate: 45,
    label: 'Date and time →',
  },
  y: {
    ticks: 8,
    label: '↑ Temperature  (°C)',
    domain: [14, 32],
  },

  //Remaining code omitted for clarity
  //...

  //Add some space below the chart
  marginBottom: 100,
})
Line chart mit Grid
Line chart mit Grid

Das sieht schon viel besser aus. Die Marker der X-Achse sind um 45° gedreht (tickRotate: 45), da diese sich sonst überlappen würden.

"Datum und Uhrzeit"-Markers der X-Achse zweizeilig darstellen

Die Darstellung der Achsenbeschriftung der X-Achse ist, so wie diese jetzt ist, unübersichtlich.

Auf Grund der Länge des Textes (Datum und Uhrzeit) müssen die Marker gedreht werden, ansonsten überlappen diese sich.

Besser wäre es, wenn Datum und Uhrzeit jeweils übereinander stehen würden, also die Ausgabe zweizeilig ist. Die Plot-Funktion liefert für diese Art der Darstellung keine eingebaute Möglichkeit. Man kann aber mit ein wenig D3-Code die X-Achse entsprechend modifizieren. Wie geht das?

Die Plot-Funktion liefert uns ein SVG-Element zurück, welches wir dann mit D3-Code modifizieren können.

The returned SVG-Element
The returned SVG-Element

Hierfür speichern wir das zurückgebende SVG-Element in einer Variablen, die wir zum Beisiel "dataPlot" nennen.

dataPlot = Plot.plot({
  grid: true,

  //Remaining code omitted for clarity
  //...
})

Nun können wir das SVG selektieren und modifizieren. Dafür fügen wir in einer neuen Zelle, hinter der Zelle mit der Plot-Funktion, den folgenden Code ein. Anschließend noch den Play-Button der Zelle anklicken.

d3.select(dataPlot)
  .select('[aria-label="x-axis"]')
  .selectAll('text')
  .call(function (t) {
    t.each(function (d) {
      var self = d3.select(this)
      var s = new Date(d * 1000).toLocaleString('de-DE').split(',')
      self.text(null)
      self.append('tspan').attr('x', 0).attr('dy', '.8em').text(s[0])
      self.append('tspan').attr('x', 0).attr('dy', '1.2em').text(s[1])
    })
  })

Mit d3.select(dataPlot) selektieren wir das SVG-Element. Weiter wird dann die X-Achse ausgewählt und dann alle "Text-Elemente", welches dann die Marker der X-achse sind. Nun können die Labels modifiziert werden.

Das Ergebnis sieht dann wie folgt aus.

Line chart mit Grid
Line chart mit Grid

Was jetzt hier auffällt, zumindest mir, es fehlt auf einmal das Label der X-Achse - "Date and time ->".

Da wir alle Text-Elemente der X-Achse selektieren, wird auch das Text-Element der X-Achse selbst selektiert und dieses dann gelöscht. Wenn man zuerst alle Elemente mit der Klasse "tick" selektiert und dann jeweils das zugehörige Text-Element, bleibt das Label der X-Achse erhalten. Der modifiziert Code sieht dann wie folgt aus.

d3.select(dataPlot)
  .select('[aria-label="x-axis"]')
  .selectAll('.tick')
  .select('text')
  .call(function (t) {
    t.each(function (d) {
      var self = d3.select(this)
      var s = new Date(d * 1000).toLocaleString('de-DE').split(',')
      self.text(null)
      self.append('tspan').attr('x', 0).attr('dy', '.8em').text(s[0])
      self.append('tspan').attr('x', 0).attr('dy', '1.2em').text(s[1])
    })
  })
Line chart mit Grid
Line chart mit Grid

Zusammenfassung der Plot-Funktion und des D3-Code in eine einzige Zelle

Man kann die Diagrammerstellung und -änderung in einer Zelle kombinieren. Der Vorteil ist, das dadurch eventuelle Nebeneffekte vermieden werden können. (Side Effects, Published Jan 30 2022 by Observable)

Die zusammengefasste Zelle sieht dann so aus.

plot = {
  let dataPlot = Plot.plot({
    grid: true,
    x: {
      ticks: 10,
      tickFormat: (d) => new Date(d * 1000).toLocaleString("de-DE"),
      tickRotate: 0,
      label: "Date and time →"
    },
    y: {
      ticks: 8,
      label: "↑ Temperature  (°C)",
      domain: [14, 32]
    },
    marks: [
      Plot.ruleY([0]),
      Plot.lineY(curveData, {
        x: "date",
        y: "temperature",
        curve: "catmull-rom",
        marker: "circle"
      })
    ],
    marginTop: 20,
    marginBottom: 50
  });

  //Add Multi-line tick labels to the chart
d3
  .select(dataPlot)
  .select('[aria-label="x-axis"]')
  .selectAll(".tick")
  .select("text")
  .call(function (t) {
    t.each(function (d) {
      var self = d3.select(this);
      var s = new Date(d * 1000).toLocaleString("de-DE").split(",");
      self.text(null);
      self.append("tspan").attr("x", 0).attr("dy", ".8em").text(s[0]);
      self.append("tspan").attr("x", 0).attr("dy", "1.2em").text(s[1]);
    });
  })

  return dataPlot
}

Das fertige "Line chart" kann sich jeder hier ansehen. Interesse geweckt? Dann erstellen Sie ein (kostenloses) Konto bei Observable und beginnen Sie mit der Erstellung Ihrer eigenen Diagramme.