Comment construire des graphiques de prix historiques avec D3.js

Une approche pas à pas pour visualiser les jeux de données financières

Photo de Kevin Ku de Pexels

Il est difficile de communiquer des données et d’afficher ces visualisations sur plusieurs appareils et plates-formes.

«Les données ressemblent au brut. C’est précieux, mais s’il n’est pas raffiné, il ne peut pas vraiment être utilisé. »- Michael Palmer

D3 (Data-Driven Documents) résout ce dilemme séculaire. Il offre aux développeurs et aux analystes la possibilité de créer des visualisations personnalisées pour le Web en toute liberté. D3.js nous permet de lier des données au DOM (Document Object Model). Appliquez ensuite des transformations basées sur les données pour créer des visualisations affinées des données.

Dans ce tutoriel, nous allons comprendre comment nous pouvons utiliser la bibliothèque D3.js.

Commencer

Nous allons construire un graphique qui illustre l'évolution d'un instrument financier au cours d'une période donnée. Cette visualisation ressemble aux tableaux de prix fournis par Yahoo Finance. Nous allons décomposer les divers composants nécessaires au rendu d'un graphique de prix interactif qui suit un stock particulier.

Composants requis:

  1. Chargement et analyse des données
  2. Élément SVG
  3. Axes X et Y
  4. Fermer la courbe des prix
  5. Courbe de moyenne mobile simple avec quelques calculs
  6. Diagramme à barres de série de volume
  7. Règle de souris et légende

Chargement et analyse des données

const loadData = d3.json ('sample-data.json'). then (data => {
  const chartResultsData = data ['chart'] ['result'] [0];
  const quoteData = chartResultsData ['indicateurs'] ['quote'] [0];
  return chartResultsData ['timestamp']. map ((heure, index) => ({
    date: nouvelle date (heure * 1000),
    high: quoteData ['high'] [index],
    low: quoteData ['low'] [index],
    open: quoteData ['open'] [index],
    close: quoteData ['close'] [index],
    volume: quoteData ['volume'] [index]
  }));
});

Premièrement, nous allons utiliser le module de récupération pour charger nos exemples de données. D3-fetch prend également en charge d'autres formats tels que les fichiers TSV et CSV. Les données seront ensuite traitées pour renvoyer un tableau d'objets. Chaque objet contient l'horodatage d'échange, le prix élevé, le prix d'ouverture, le prix d'ouverture, le prix de clôture et le volume d'échange.

corps {
  arrière-plan: # 00151c;
}
#chart {
  arrière-plan: # 0e3040;
  couleur: # 67809f;
}

Ajoutez les propriétés CSS de base ci-dessus pour personnaliser le style de votre graphique afin d'obtenir un attrait visuel maximal.

Ajouter l'élément SVG

const initialiseChart = data => {
  marge constante = {en haut: 50, à droite: 50, en bas: 50, à gauche: 50};
  const width = window.innerWidth - margin.left - margin.right;
  const height = window.innerHeight - margin.top - margin.bottom;
  // ajoute SVG à la page
  const svg = d3
    .select ('# chart')
    .append ('svg')
    .attr ('largeur', largeur + marge ['gauche'] + marge ['droite'])
    .attr ('hauteur', hauteur + marge ['haut'] + marge ['bas'])
    .call (responsivefy)
    .append ('g')
    .attr ('transform', `translate ($ {margin ['left']}, $ {margin ['top']})`);

Par la suite, nous pouvons utiliser la méthode append () pour ajouter l'élément SVG à l'élément

avec l'id, chart. Ensuite, nous utilisons la méthode attr () pour attribuer la largeur et la hauteur de l’élément SVG. Nous appelons ensuite la méthode responsivefy () (écrite à l'origine par Brendan Sudol). Cela permet à l'élément SVG d'avoir des capacités réactives en écoutant les événements de redimensionnement de fenêtre.

N'oubliez pas d'ajouter l'élément de groupe SVG à l'élément SVG ci-dessus avant de le traduire à l'aide des valeurs de la constante de marge.

Rendu des axes X et Y

Avant de rendre le composant axes, nous devrons définir notre domaine et notre plage, qui seront ensuite utilisés pour créer nos échelles pour les axes.

// recherche une plage de données
const xMin = d3.min (data, d => {
  renvoyer d ['date'];
});
const xMax = d3.max (data, d => {
  renvoyer d ['date'];
});
const yMin = d3.min (données, d => {
  retour d ['close'];
});
const yMax = d3.max (data, d => {
  retour d ['close'];
});
// échelles pour les graphiques
const xScale = d3
  .scaleTime ()
  .domain ([xMin, xMax])
  .range ([0, width]);
const yScale = d3
  .scaleLinear ()
  .domain ([yMin - 5, yMax])
  .range ([height, 0]);

Les axes x et y du graphique de la ligne de prix de clôture comprennent respectivement la date de transaction et le prix de clôture. Par conséquent, nous devons définir les valeurs x et y minimales et maximales en utilisant d3.max () et d3.min (). Nous pouvons ensuite utiliser scaleTime () et scaleLinear () pour créer l’échelle de temps sur l’axe des x et l’échelle linéaire sur l’axe des y, respectivement. La plage des échelles est définie par la largeur et la hauteur de notre élément SVG.

// crée le composant d'axes
svg
  .append ('g')
  .attr ('id', 'xAxis')
  .attr ('transform', `translate (0, $ {height})`)
  .call (d3.axisBottom (xScale));
svg
  .append ('g')
  .attr ('id', 'yAxis')
  .attr ('transform', `translate ($ {width}, 0)`)
  .call (d3.axisRight (yScale));

Après cette étape, nous devons ajouter le premier élément g à l'élément SVG, qui appelle la méthode d3.axisBottom (), en prenant xScale comme paramètre pour générer l'axe des x. L'axe des x est ensuite traduit en bas de la zone de graphique. De la même manière, l'axe des y est généré en ajoutant l'élément g, en appelant d3.axisRight () avec yScale comme paramètre, avant de traduire l'axe des y à la droite de la zone de graphique.

Rendu du graphique de la ligne de prix de fermeture

// génère un graphique de ligne de prix proche lorsque appelé
ligne constante = d3
  .ligne()
  .x (d => {
    renvoyer xScale (d ['date']);
  })
  .y (d => {
    retourne yScale (d ['close']);
  });
// Ajoute le chemin et les données de liaison
svg
 .append ('chemin')
 .data ([données])
 .style ('remplir', 'aucun')
 .attr ('id', 'priceChart')
 .attr ('stroke', 'steelblue')
 .attr ('largeur du trait', '1,5')
 .attr ('d', ligne);

Maintenant, nous pouvons ajouter l’élément path dans notre élément SVG principal, puis passer à notre ensemble de données analysé, data. Nous définissons l'attribut d avec notre fonction d'assistance, line. qui appelle la méthode d3.line (). Les attributs x et y de la ligne acceptent les fonctions anonymes et renvoient respectivement la date et le prix de clôture.

Voici comment votre graphique devrait se présenter:

Point de contrôle n ° 1: fermez la courbe des prix avec les axes X et Y.

Rendu de la courbe de moyenne mobile simple

Au lieu de nous fier uniquement au prix de clôture comme unique indicateur technique, nous utilisons la moyenne mobile simple. Cette moyenne identifie les tendances à la hausse et à la baisse pour la sécurité donnée.

const movingAverage = (data, numberOfPricePoints) => {
  retourne data.map ((ligne, index, total) => {
    const start = Math.max (0, index-numberOfPricePoints);
    const end = index;
    const sous-ensemble = total.slice (début, fin + 1);
    const sum = subset.reduce ((a, b) => {
      retourne a + b ['fermer'];
    }, 0);
    revenir {
      date: rangée ['date'],
      moyenne: somme / sous-ensemble.longueur
    };
  });
};

Nous définissons notre fonction d'assistance, movingAverage pour calculer la moyenne mobile simple. Cette fonction accepte deux paramètres, à savoir l'ensemble de données et le nombre de prix, ou périodes. Il retourne ensuite un tableau d'objets, chaque objet contenant la date et la moyenne de chaque point de données.

// calcule la moyenne mobile simple sur 50 jours
const movingAverageData = movingAverage (données, 49);
// génère une courbe de moyenne mobile lorsque appelé
const movingAverageLine = d3
 .ligne()
 .x (d => {
  renvoyer xScale (d ['date']);
 })
 .y (d => {
  retourne yScale (d ['moyenne']);
 })
  .curve (d3.curveBasis);
svg
  .append ('chemin')
  .data ([movingAverageData])
  .style ('remplir', 'aucun')
  .attr ('id', 'movingAverageLine')
  .attr ('stroke', '# FF8900')
  .attr ('d', movingAverageLine);

MovingAverage () calcule la moyenne mobile simple sur une période de 50 jours. Semblable au graphique de la ligne de prix de clôture, nous ajoutons l'élément path dans notre élément SVG principal, suivons du passage de notre ensemble de données de moyenne mobile et définissons l'attribut d avec notre fonction d'assistance, movingAverageLine. La seule différence par rapport à ce qui précède est que nous avons passé d3.curveBasis à d3.line (). Curve () afin de réaliser une courbe.

Il en résulte une simple courbe de moyenne mobile superposée au-dessus de notre graphique actuel:

Point de contrôle n ° 2: courbe orange, qui représente la moyenne mobile simple. Cela nous donne une meilleure idée du mouvement des prix.

Rendu du graphique à barres de la série de volumes

Pour ce composant, nous allons restituer le volume commercial sous la forme d'un diagramme à barres codées par couleur occupant le même élément SVG. Les barres sont vertes lorsque le titre se ferme plus haut que le cours de clôture de la veille. Ils sont rouges lorsque le cours de clôture est inférieur au prix de clôture du jour précédent. Ceci illustre le volume échangé pour chaque date de transaction. Ceci peut ensuite être utilisé à côté du graphique ci-dessus pour analyser les mouvements de prix.

/ * Barres de série de volume * /
const volData = data.filter (d => d ['volume']! == null && d ['volume']! == 0);
const yMinVolume = d3.min (volData, d => {
  retourne Math.min (d ['volume']);
});
const yMaxVolume = d3.max (volData, d => {
  retourne Math.max (d ['volume']);
});
const yVolumeScale = d3
  .scaleLinear ()
  .domain ([yMinVolume, yMaxVolume])
  .range ([height, 0]);

Les axes x et y du graphique à barres des séries de volumes comprennent respectivement la date de transaction et le volume. Ainsi, nous devrons redéfinir les valeurs y minimale et maximale et utiliser scaleLinear () sur l’axe des y. La plage de ces échelles est définie par la largeur et la hauteur de notre élément SVG. Nous allons réutiliser xScale car l’axe des x du graphique à barres correspond de manière similaire à la date de transaction.

svg
  .tout sélectionner()
  .data (volData)
  .entrer()
  .append ('rect')
  .attr ('x', d => {
    renvoyer xScale (d ['date']);
  })
  .attr ('y', d => {
    retourne yVolumeScale (d ['volume']);
  })
  .attr ('remplir', (d, i) => {
    si (i === 0) {
      retourne '# 03a678';
    } autre {
      retourne volData [i - 1] .close> d.close? '# c0392b': '# 03a678';
    }
  })
  .attr ('largeur', 1)
  .attr ('hauteur', d => {
    hauteur de retour - yVolumeScale (d ['volume']);
  });

Cette section repose sur votre compréhension du fonctionnement de la méthode selectAll () avec les méthodes enter () et append (). Vous voudrez peut-être lire ceci (écrit par Mike Bostock lui-même) si vous ne connaissez pas ces méthodes. Cela peut être important car ces méthodes sont utilisées dans le cadre du modèle enter-update-exit, que je pourrais couvrir dans un tutoriel ultérieur.

Pour rendre les barres, nous allons d’abord utiliser .selectAll () pour retourner une sélection vide ou un tableau vide. Ensuite, nous passons à volData pour définir la hauteur de chaque barre. La méthode enter () compare le jeu de données volData à la sélection de selectAll (), qui est actuellement vide. Actuellement, le DOM ne contient aucun élément . Ainsi, la méthode append () accepte un argument ‘rect’, qui crée un nouvel élément dans le DOM pour chaque objet de volData.

Voici une ventilation des attributs des barres. Nous allons utiliser les attributs suivants: x, y, remplissage, largeur et hauteur.

.attr ('x', d => {
  renvoyer xScale (d ['date']);
})
.attr ('y', d => {
  retourne yVolumeScale (d ['volume']);
})

La première méthode attr () définit la coordonnée x. Il accepte une fonction anonyme qui retourne la date. De même, la seconde méthode attr () définit la coordonnée y. Il accepte une fonction anonyme qui renvoie le volume. Ceux-ci définiront la position de chaque barre.

.attr ('largeur', 1)
.attr ('hauteur', d => {
  hauteur de retour - yVolumeScale (d ['volume']);
});

Nous assignons une largeur de 1 pixel à chaque barre. Pour que la barre s'étire du haut (défini par y) à l'axe des x, déduisez simplement la hauteur avec la valeur y.

.attr ('remplir', (d, i) => {
  si (i === 0) {
    retourne '# 03a678';
  } autre {
    retourne volData [i - 1] .close> d.close? '# c0392b': '# 03a678';
  }
})

Rappelez-vous la façon dont les barres seront codées par couleur? Nous utiliserons l'attribut de remplissage pour définir les couleurs de chaque barre. Pour les actions dont la clôture est supérieure au cours de clôture du jour précédent, la barre sera de couleur verte. Sinon, la barre sera rouge.

Voici à quoi devrait ressembler votre graphique actuel:

Point de contrôle n ° 3: Graphique de la série de volumes, représenté par les barres rouge et verte.

Rendu de la croix et de la légende pour l'interactivité

Nous avons atteint la dernière étape de ce didacticiel, dans lequel nous allons générer un réticule de souris qui affiche des lignes de chute. Si vous passez la souris sur les différents points du graphique, les légendes seront mises à jour. Cela nous fournit les informations complètes (prix d'ouverture, prix de clôture, prix élevé, prix bas et volume) pour chaque date de transaction.

La section suivante est tirée de l’excellent exemple de Micah Stubb.

// rend x et y réticule
const focus = svg
  .append ('g')
  .attr ('classe', 'focus')
  .style ('display', 'none');
focus.append ('cercle'). attr ('r', 4,5);
focus.append ('line'). classed ('x', true);
focus.append ('line'). classed ('y', true);
svg
  .append ('rect')
  .attr ('classe', 'superposition')
  .attr ('largeur', largeur)
  .attr ('hauteur', hauteur)
  .on ('mouseover', () => focus.style ('display', null))
  .on ('mouseout', () => focus.style ('display', 'none'))
  .on ('mousemove', génèreCroosshair);
d3.select ('. superposition'). style ('fill', 'none');
d3.select ('. superposition'). style ('pointeur-événements', 'tous');
d3.selectAll ('. ligne de mise au point'). style ('fill', 'none');
d3.selectAll ('. focus line'). style ('stroke', '# 67809f');
d3.selectAll ('. ligne de mise au point'). style ('largeur du trait', '1.5px');
d3.selectAll ('. ligne de focus'). style ('stroke-dasharray', '3 3');

Le réticule est constitué d'un cercle translucide avec des lignes de chute composées de tirets. Le bloc de code ci-dessus fournit le style des éléments individuels. Au passage de la souris, il générera le réticule basé sur la fonction ci-dessous.

const bisectDate = d3.bisector (d => d.date) .left;
function generateCrosshair () {
  // retourne la valeur correspondante du domaine
  const matchingDate = xScale.invert (d3.mouse (this) [0]);
  // obtient le point d'insertion
  const i = bisectDate (data, correspondantesDate, 1);
  const d0 = données [i - 1];
  const d1 = données [i];
  const currentPoint = correspondantesDate - d0 ['date']> d1 ['date'] - correspondantesDate? d1: d0;
  
  focus.attr ('transform', `translate ($ {xScale (currentPoint ['date'])}}, $ {yScale (currentPoint ['close'])})`);
concentrer
  .select ('line.x')
  .attr ('x1', 0)
  .attr ('x2', width - xScale (currentPoint ['date']))
  .attr ('y1', 0)
  .attr ('y2', 0);
concentrer
  .select ('line.y')
  .attr ('x1', 0)
  .attr ('x2', 0)
  .attr ('y1', 0)
  .attr ('y2', height - yScale (currentPoint ['close']));
 updateLegends (currentPoint);
}

Nous pouvons ensuite utiliser la méthode d3.bisector () pour localiser le point d’insertion, ce qui mettra en surbrillance le point de données le plus proche du graphique de la ligne de prix de fermeture. Après avoir déterminé le currentPoint, les lignes de chute seront mises à jour. La méthode updateLegends () utilise le currentPoint comme paramètre.

const updateLegends = currentData => {
  d3.selectAll ('. lineLegend'). remove ();
  const legendKeys = Object.keys (data [0]);
  const lineLegend = svg
    .selectAll ('. lineLegend')
    .data (legendKeys)
    .entrer()
    .append ('g')
    .attr ('classe', 'lineLegend')
    .attr ('transformer', (d, i) => {
      return `translate (0, $ {i * 20})`;
    });
  lineLegend
    .append ('text')
    .text (d => {
      si (d === 'date') {
        return `$ {d}: $ {currentData [d] .toLocaleDateString ()}`;
      } sinon si (d === 'haut' || d === 'bas' || d === 'ouvert' || d === 'fermé') {
        return `$ {d}: $ {currentData [d] .toFixed (2)}`;
      } autre {
        return `$ {d}: $ {currentData [d]}`;
      }
    })
    .style ('remplir', 'blanc')
    .attr ('transformer', 'traduire (15,9)');
  };

La méthode updateLegends () met à jour la légende en affichant la date, le prix d'ouverture, le prix de clôture, le prix le plus élevé, le prix le plus bas et le volume du point de survol sélectionné sur le graphique de la ligne de fermeture. Semblable aux graphiques à barres Volume, nous allons utiliser la méthode selectAll () avec les méthodes enter () et append ().

Pour rendre les légendes, nous allons utiliser .selectAll ('. LineLegend') pour sélectionner les légendes, puis appeler la méthode remove () pour les supprimer. Ensuite, nous passons les clés des légendes, legendKeys, qui serviront à définir la hauteur de chaque barre. La méthode enter () est appelée, elle compare le jeu de données volData et à la sélection de selectAll (), qui est actuellement vide. Actuellement, le DOM ne contient aucun élément . Ainsi, la méthode append () accepte un argument ‘rect’, qui crée un nouvel élément dans le DOM pour chaque objet de volData.

Ensuite, ajoutez les légendes avec leurs propriétés respectives. Nous traitons ensuite les valeurs en convertissant les prix en deux décimales. Nous avons également défini l'objet de date sur les paramètres régionaux par défaut pour assurer la lisibilité.

Ce sera le résultat final:

Point de contrôle n ° 4: Passez la souris sur une partie du graphique!

Pensées de clôture

Toutes nos félicitations! Vous avez atteint la fin de ce tutoriel. Comme démontré ci-dessus, D3.js est simple mais dynamique. Il vous permet de créer des visualisations personnalisées pour tous vos ensembles de données. Dans les semaines à venir, je publierai la deuxième partie de cette série qui plongera dans le modèle enter-update-exit de D3.js. En attendant, vous pouvez consulter la documentation de l’API, d’autres didacticiels et d’autres visualisations intéressantes construites avec D3.js.

N'hésitez pas à consulter le code source ainsi que la démonstration complète de ce tutoriel. Merci et j'espère que vous avez appris quelque chose de nouveau aujourd'hui!

Un merci spécial à Debbie Leong pour la révision de cet article.