So arbeiten Sie mit dem allgemeinen Aktualisierungsmuster von D3.js.

Eine Führung zur Implementierung von Visualisierungsmodulen mit dynamischen Datensätzen

Es ist üblich, das vorhandene SVG-Element (Scalable Vector Graphics) durch Aufrufen zu entfernen d3.select('#chart').remove(), bevor ein neues Diagramm gerendert wird.

Es kann jedoch Szenarien geben, in denen Sie dynamische Visualisierungen aus Quellen wie externen APIs erstellen müssen. Dieser Artikel zeigt Ihnen, wie Sie dies mit D3.js tun.

D3.js verarbeitet dynamische Daten unter Verwendung des allgemeinen Aktualisierungsmusters. Dies wird üblicherweise als Datenverknüpfung beschrieben, gefolgt von Vorgängen beim Eingeben, Aktualisieren und Beenden. Wenn Sie diese Auswahlmethoden beherrschen, können Sie nahtlose Übergänge zwischen Zuständen erstellen und so aussagekräftige Geschichten mit Daten erzählen.

Loslegen

Bedarf

Wir werden ein Diagramm erstellen, das die Bewegung einiger Exchange Traded Funds (ETFs) im zweiten Halbjahr 2018 veranschaulicht. Das Diagramm besteht aus den folgenden Tools:

  1. Schlusskurs-Liniendiagramm
  2. Balkendiagramm des Handelsvolumens
  3. 50 Tage einfacher gleitender Durchschnitt
  4. Bollinger-Bänder (einfacher gleitender 20-Tage-Durchschnitt mit einer Standardabweichung von 2,0)
  5. Open-High-Low-Close-Diagramm (OHLC)
  6. Kerzenhalter

Diese Tools werden üblicherweise bei der technischen Analyse von Aktien, Rohstoffen und anderen Wertpapieren verwendet. Zum Beispiel können Händler die Bollinger-Bänder und Kerzenhalter verwenden, um Muster abzuleiten, die Kauf- oder Verkaufssignale darstellen.

So sieht das Diagramm aus:

Dieser Artikel soll Sie mit den grundlegenden Theorien der Datenverknüpfungen und dem Enter-Update-Exit-Muster ausstatten, damit Sie dynamische Datensätze einfach visualisieren können. Darüber hinaus werden wir uns mit selection.join befassen, das in der Version 5.5.0 von D3.js eingeführt wird.

Das allgemeine Aktualisierungsmuster

Der Kern des allgemeinen Aktualisierungsmusters ist die Auswahl von DOM-Elementen (Document Object Model), gefolgt von der Bindung von Daten an diese Elemente. Diese Elemente werden dann erstellt, aktualisiert oder entfernt, um die erforderlichen Daten darzustellen.

Neue Daten verbinden

Datenverknüpfung ist die Zuordnung der nAnzahl der Elemente im Dataset zur nAnzahl der ausgewählten DOM-Knoten (Document Object Model), wobei die erforderliche Aktion für das DOM angegeben wird, wenn sich die Daten ändern.

Wir verwenden die data()Methode, um jeden Datenpunkt einem entsprechenden Element in der DOM-Auswahl zuzuordnen. Darüber hinaus empfiehlt es sich, die Objektkonstanz aufrechtzuerhalten, indem in jedem Datenpunkt ein Schlüssel als eindeutige Kennung angegeben wird. Schauen wir uns das folgende Beispiel an, das der erste Schritt zum Rendern der Handelsvolumenbalken ist:

const bars = d3 .select('#volume-series') .selectAll(.'vol') .data(this.currentData, d => d['date']);

In der obigen Codezeile werden alle Elemente mit der Klasse ausgewählt vol, gefolgt von der Zuordnung des this.currentDataArrays zur Auswahl von DOM-Elementen mithilfe der data()Methode.

Das zweite optionale Argument von verwendet data()einen Datenpunkt als Eingabe und gibt die dateEigenschaft als ausgewählten Schlüssel für jeden Datenpunkt zurück.

Auswahl eingeben / aktualisieren

.enter()Gibt eine Eingabeauswahl zurück, die die Elemente darstellt, die hinzugefügt werden müssen, wenn das verknüpfte Array länger als die Auswahl ist. Anschließend wird aufgerufen .append(), wodurch Elemente im DOM erstellt oder aktualisiert werden. Wir können dies folgendermaßen umsetzen:

bars .enter() .append('rect') .attr('class', 'vol') .merge(bars) .transition() .duration(750) .attr('x', d => this.xScale(d['date'])) .attr('y', d => yVolumeScale(d['volume'])) .attr('fill', (d, i) => { if (i === 0) { return '#03a678'; } else { // green bar if price is rising during that period, and red when price is falling return this.currentData[i - 1].close > d.close ? '#c0392b' : '#03a678'; } }) .attr('width', 1) .attr('height', d => this.height - yVolumeScale(d['volume']));

.merge()führt die Aktualisierung zusammen und gibt eine Auswahl ein, bevor die nachfolgenden Methodenketten angewendet werden, um Animationen zwischen Übergängen zu erstellen und die zugehörigen Attribute zu aktualisieren. Mit dem obigen Codeblock können Sie die folgenden Aktionen für die ausgewählten DOM-Elemente ausführen:

  1. Bei der Aktualisierungsauswahl, die aus Datenpunkten besteht, die durch die Elemente im Diagramm dargestellt werden, werden die Attribute entsprechend aktualisiert.
  2. Die Erstellung von Elementen mit der Klasse vol, wobei die obigen Attribute in jedem Element als Eingabeauswahl definiert sind, besteht aus Datenpunkten, die nicht im Diagramm dargestellt werden.

Auswahl beenden

Entfernen Sie Elemente aus unserem Datensatz, indem Sie die folgenden einfachen Schritte ausführen: Bars.exit (). Remove ();

.exit()Gibt eine Exit-Auswahl zurück, die die Datenpunkte angibt, die entfernt werden müssen. Die .remove()Methode löscht anschließend die Auswahl aus dem DOM.

So reagieren die Volumenreihenbalken auf Datenänderungen:

Beachten Sie, wie das DOM und die jeweiligen Attribute jedes Elements aktualisiert werden, wenn wir einen anderen Datensatz auswählen:

Selection.join (ab Version 5.8.0)

Die Einführung von selection.joinD3.js in Version 5.8.0 hat den gesamten Datenverbindungsprozess vereinfacht. Separate Funktionen sind nun vergangen zu handhaben eingeben , aktualisieren , und verlassen die wiederum gibt die Eingabe und Aktualisierung Auswahlen verschmolzen.

selection.join( enter => // enter.. , update => // update.. , exit => // exit.. ) // allows chained operations on the returned selections

Im Fall der Volumenserienbalken führt die Anwendung von zu selection.joinfolgenden Änderungen an unserem Code:

//select, followed by updating data join const bars = d3 .select('#volume-series') .selectAll('.vol') .data(this.currentData, d => d['date']); bars.join( enter => enter .append('rect') .attr('class', 'vol') .attr('x', d => this.xScale(d['date'])) .attr('y', d => yVolumeScale(d['volume'])) .attr('fill', (d, i) => { if (i === 0) { return '#03a678'; } else { return this.currentData[i - 1].close > d.close ? '#c0392b' : '#03a678'; } }) .attr('width', 1) .attr('height', d => this.height - yVolumeScale(d['volume'])), update => update .transition() .duration(750) .attr('x', d => this.xScale(d['date'])) .attr('y', d => yVolumeScale(d['volume'])) .attr('fill', (d, i) => { if (i === 0) { return '#03a678'; } else { return this.currentData[i - 1].close > d.close ? '#c0392b' : '#03a678'; } }) .attr('width', 1) .attr('height', d => this.height - yVolumeScale(d['volume'])) );

Beachten Sie außerdem, dass wir einige Änderungen an der Animation der Balken vorgenommen haben. Anstatt die transition()Methode an die zusammengeführte Eingabe- und Aktualisierungsauswahl zu übergeben, wird sie jetzt in der Aktualisierungsauswahl verwendet, sodass Übergänge nur angewendet werden, wenn sich das Dataset geändert hat.

Die zurückgegebenen Eingabe- und Aktualisierungsauswahlen werden dann zusammengeführt und von zurückgegeben selection.join.

Bollinger Bands

Ebenso können wir selection.joindas Rendern von Bollinger-Bändern anwenden . Vor dem Rendern der Bänder müssen die folgenden Eigenschaften jedes Datenpunkts berechnet werden:

  1. Einfacher gleitender Durchschnitt von 20 Tagen.
  2. Die oberen und unteren Bänder, die eine Standardabweichung von 2,0 über bzw. unter dem einfachen gleitenden 20-Tage-Durchschnitt aufweisen.

Dies ist die Formel zur Berechnung der Standardabweichung:

Nun werden wir die obige Formel in JavaScript-Code übersetzen:

calculateBollingerBands(data, numberOfPricePoints) { let sumSquaredDifference = 0; return data.map((row, index, total) => { const start = Math.max(0, index - numberOfPricePoints); const end = index; // divide the sum with subset.length to obtain moving average const subset = total.slice(start, end + 1); const sum = subset.reduce((a, b) => { return a + b['close']; }, 0); const sumSquaredDifference = subset.reduce((a, b) => { const average = sum / subset.length; const dfferenceFromMean = b['close'] - average; const squaredDifferenceFromMean = Math.pow(dfferenceFromMean, 2); return a + squaredDifferenceFromMean; }, 0); const variance = sumSquaredDifference / subset.length; return { date: row['date'], average: sum / subset.length, standardDeviation: Math.sqrt(variance), upperBand: sum / subset.length + Math.sqrt(variance) * 2, lowerBand: sum / subset.length - Math.sqrt(variance) * 2 }; }); } . . // calculates simple moving average, and standard deviation over 20 days this.bollingerBandsData = this.calculateBollingerBands(validData, 19);

Eine kurze Erläuterung der Berechnung der Standardabweichung und der Bollinger-Band-Werte im obigen Codeblock lautet wie folgt:

Für jede Iteration

  1. Berechnen Sie den Durchschnitt des Schlusskurses.
  2. Ermitteln Sie die Differenz zwischen dem Durchschnittswert und dem Schlusskurs für diesen Datenpunkt.
  3. Quadrieren Sie das Ergebnis jeder Differenz.
  4. Finden Sie die Summe der quadratischen Differenzen.
  5. Calculate the mean of the squared differences to get the variance
  6. Get the square root of the variance to obtain the standard deviation for each data point.
  7. Multiply the standard deviation by 2. Calculate the upper and lower band values by adding or subtracting the average with the multiplied value.

With the data points defined, we can then make use of selection.join to render Bollinger Bands:

// code not shown: rendering of upper and lower bands . . // bollinger bands area chart const area = d3 .area() .x(d => this.xScale(d['date'])) .y0(d => this.yScale(d['upperBand'])) .y1(d => this.yScale(d['lowerBand'])); const areaSelect = d3 .select('#chart') .select('svg') .select('g') .selectAll('.band-area') .data([this.bollingerBandsData]); areaSelect.join( enter => enter .append('path') .style('fill', 'darkgrey') .style('opacity', 0.2) .style('pointer-events', 'none') .attr('class', 'band-area') .attr('clip-path', 'url(#clip)') .attr('d', area), update => update .transition() .duration(750) .attr('d', area) );

This renders the area chart which denotes the area filled by the Bollinger Bands. On the update function, we can use the selection.transition()method to provide animated transitions on the update selection.

Candlesticks

The candlesticks chart displays the high, low, open and close prices of a stock for a specific period. Each candlestick represents a data point. Green represents when the stock closes higher while red represents when the stock closes at a lower value.

Unlike the Bollinger Bands, there is no need for additional calculations, as the prices are available in the existing dataset.

const bodyWidth = 5; const candlesticksLine = d3 .line() .x(d => d['x']) .y(d => d['y']); const candlesticksSelection = d3 .select('#chart') .select('g') .selectAll('.candlesticks') .data(this.currentData, d => d['volume']); candlesticksSelection.join(enter => { const candlesticksEnter = enter .append('g') .attr('class', 'candlesticks') .append('g') .attr('class', 'bars') .classed('up-day', d => d['close'] > d['open']) .classed('down-day', d => d['close'] <= d['open']); 

On the enter function, each candlestick is rendered based on its individual properties.

First and foremost, each candlestick group element is assigned a class of up-day if the close price is higher than the open price, and down-day if the close price is lower than or equal to the open-price.

candlesticksEnter .append('path') .classed('high-low', true) .attr('d', d => { return candlesticksLine([ { x: this.xScale(d['date']), y: this.yScale(d['high']) }, { x: this.xScale(d['date']), y: this.yScale(d['low']) } ]); });

Next, we append the path element, which represents the highest and lowest price of that day, to the above selection.

 candlesticksEnter .append('rect') .attr('x', d => this.xScale(d.date) - bodyWidth / 2) .attr('y', d => { return d['close'] > d['open'] ? this.yScale(d.close) : this.yScale(d.open); }) .attr('width', bodyWidth) .attr('height', d => { return d['close'] > d['open'] ? this.yScale(d.open) - this.yScale(d.close) : this.yScale(d.close) - this.yScale(d.open); }); });

This is followed by appending the rect element to the selection. The height of each rect element is directly proportionate to its day range, derived by subtracting the open price with the close price.

On our stylesheets, we will define the following CSS properties to our classes making the candlesticks red or green:

.bars.up-day path { stroke: #03a678; } .bars.down-day path { stroke: #c0392b; } .bars.up-day rect { fill: #03a678; } .bars.down-day rect { fill: #c0392b; }

This results in the rendering of the Bollinger Bands and candlesticks:

The new syntax has proven to be simpler and more intuitive than explicitly calling selection.enter, selection.append, selection.merge, and selection.remove.

Note that for those who are developing with D3.js’s v5.8.0 and beyond, it has been recommended by Mike Bostock that these users start using selection.join due to the above advantages.

Conclusion

The potential of D3.js is limitless and the above illustrations are merely the tip of the iceberg. Many satisfied users have created visualizations which are vastly more complex and sophisticated than the one show above. This list of free APIs may interest you if you are keen to embark on your own data visualization projects.

Feel free to check out the source code and the full demonstration of this project.

Thank you very much for reading this article. If you have any questions or suggestions, feel free to leave them on the comments below!

New to D3.js? You may refer to this article on the basics of implementing common chart components.

Special thanks to Debbie Leong for reviewing this article.

Additional references:

  1. D3.js API documentation
  2. Interactive demonstration of selection.join