Comment travailler avec le modèle de mise à jour générale de D3.js

Une visite guidée de la mise en œuvre de modules de visualisation avec des jeux de données dynamiques

Il est courant de supprimer l'élément SVG (Scalable Vector Graphics) existant en appelant d3.select('#chart').remove(), avant de rendre un nouveau graphique.

Cependant, il peut y avoir des scénarios où vous devez produire des visualisations dynamiques à partir de sources telles que des API externes. Cet article vous montrera comment faire cela en utilisant D3.js.

D3.js gère les données dynamiques en adoptant le modèle de mise à jour générale. Ceci est généralement décrit comme une jointure de données, suivie d'opérations sur les sélections d'entrée, de mise à jour et de sortie. La maîtrise de ces méthodes de sélection vous permettra de produire des transitions transparentes entre les états, vous permettant de raconter des histoires significatives avec des données.

Commencer

Exigences

Nous allons construire un graphique qui illustre le mouvement de quelques fonds négociés en bourse (ETF) au cours du second semestre de 2018. Le graphique comprend les outils suivants:

  1. Graphique linéaire des prix de clôture
  2. Graphique à barres du volume des échanges
  3. Moyenne mobile simple sur 50 jours
  4. Bandes de Bollinger (moyenne mobile simple sur 20 jours, avec un écart type fixé à 2,0)
  5. Graphique Open-High-Low-Close (OHLC)
  6. Chandeliers

Ces outils sont couramment utilisés dans l'analyse technique des actions, des matières premières et d'autres titres. Par exemple, les commerçants peuvent utiliser les bandes de Bollinger et les chandeliers pour dériver des modèles qui représentent des signaux d'achat ou de vente.

Voici à quoi ressemblera le graphique:

Cet article vise à vous équiper des théories fondamentales des jointures de données et du modèle entrée-mise à jour-sortie afin de vous permettre de visualiser facilement des ensembles de données dynamiques. De plus, nous couvrirons selection.join, qui est introduit dans la version v5.8.0 de D3.js.

Le modèle de mise à jour générale

L'essentiel du modèle de mise à jour générale est la sélection des éléments DOM (Document Object Model), suivie de la liaison des données à ces éléments. Ces éléments sont ensuite créés, mis à jour ou supprimés pour représenter les données nécessaires.

Rejoindre de nouvelles données

La jointure de données est le mappage du nnombre d'éléments dans l'ensemble de données avec le nnombre de nœuds DOM (Document Object Model) sélectionnés, spécifiant l'action requise au DOM à mesure que les données changent.

Nous utilisons la data()méthode pour mapper chaque point de données à un élément correspondant dans la sélection DOM. De plus, il est recommandé de maintenir la constance des objets en spécifiant une clé comme identifiant unique dans chaque point de données. Jetons un coup d'œil à l'exemple suivant, qui est la première étape vers le rendu des barres de volume des échanges:

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

La ligne de code ci-dessus sélectionne tous les éléments avec la classe vol, puis mappe le this.currentDatatableau avec la sélection des éléments DOM à l'aide de la data()méthode.

Le deuxième argument facultatif de data()prend un point de données comme entrée et renvoie la datepropriété comme clé sélectionnée pour chaque point de données.

Entrer / Mettre à jour la sélection

.enter()renvoie une sélection d'entrée qui représente les éléments qui doivent être ajoutés lorsque le tableau joint est plus long que la sélection. Ceci est suivi d'un appel .append(), qui crée ou met à jour des éléments sur le DOM. Nous pouvons l'implémenter de la manière suivante:

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()fusionne la mise à jour et saisit les sélections, avant d'appliquer les chaînes de méthodes suivantes pour créer des animations entre les transitions et pour mettre à jour leurs attributs associés. Le bloc de code ci-dessus vous permet d'effectuer les actions suivantes sur les éléments DOM sélectionnés:

  1. La sélection de mise à jour, qui se compose de points de données représentés par les éléments sur le graphique, verra ses attributs mis à jour en conséquence.
  2. La création d' éléments avec la classe vol, avec les attributs ci-dessus définis dans chaque élément lorsque la sélection d'entrée se compose de points de données qui ne sont pas représentés sur le graphique.

Quitter la sélection

Supprimez des éléments de notre ensemble de données en suivant les étapes simples ci-dessous: bars.exit (). Remove ();

.exit()renvoie une sélection de sortie, qui spécifie les points de données à supprimer. La .remove()méthode supprime ensuite la sélection du DOM.

Voici comment les barres de la série de volumes répondront aux changements de données:

Notez comment le DOM et les attributs respectifs de chaque élément sont mis à jour lorsque nous sélectionnons un ensemble de données différent:

Selection.join (à partir de la v5.8.0)

L'introduction de la selection.joinv5.8.0 de D3.js a simplifié l'ensemble du processus de jointure de données. Fonctions distinctes sont transmises à gérer entrer , mise à jour , et la sortie qui revient à son tour l' a fusionné entrer et sélections de mise à jour.

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

Dans le cas des barres de séries de volumes, l'application de selection.joinentraînera les modifications suivantes sur notre 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'])) );

Notez également que nous avons apporté quelques modifications à l'animation des barres. Au lieu de transmettre la transition()méthode aux sélections d'entrée et de mise à jour fusionnées, elle est maintenant utilisée dans la sélection de mise à jour de sorte que les transitions ne seront appliquées que lorsque l'ensemble de données a changé.

Les sélections d'entrée et de mise à jour renvoyées sont ensuite fusionnées et renvoyées par selection.join.

Bandes de Bollinger

De même, nous pouvons appliquer selection.joinsur le rendu des bandes de Bollinger. Avant de rendre les bandes, nous devons calculer les propriétés suivantes de chaque point de données:

  1. Moyenne mobile simple sur 20 jours.
  2. Les bandes supérieure et inférieure, qui ont un écart type de 2,0 au-dessus et au-dessous de la moyenne mobile simple de 20 jours, respectivement.

Voici la formule pour calculer l'écart type:

Maintenant, nous allons traduire la formule ci-dessus en code JavaScript:

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);

Une explication rapide du calcul de l'écart type et des valeurs de la bande de Bollinger sur le bloc de code ci-dessus est la suivante:

Pour chaque itération,

  1. Calculez la moyenne du prix de clôture.
  2. Trouvez la différence entre la valeur moyenne et le prix de clôture pour ce point de données.
  3. Mettez au carré le résultat de chaque différence.
  4. Trouvez la somme des différences au carré.
  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