Introduction à la datavisualisation sous D3JS

Réalisez un diagramme en barre avec D3JS alimenté par une api sous Express

Notre objectif, dans cet article, est de mettre à profit quelques notions que nous avons étudiées précédemment (ci-dessous les liens vers nos articles traitant de ces sujets) :
- le chargement de données depuis un fichier CSV (ici),
- la mise en place d'un serveur web sous Express (ici),
- l'implémentation d'une API (ici),
- le rendu via le moteur de template Handlebars (ici)

Concrètement nous voulons cette fois dresser un diagramme en barre (bar chart) via la librairie D3JS. Nos données nous seront envoyées depuis une API du serveur qui sera chargée de les récupérer d'un fichier CSV.

Voici le résultat attendu :

nodejs express serveur template handlebars api d3js barchart

Nos données de travail

Nous allons travailler avec une petite série de données agrégées listant le salaire médian pour quelques secteurs d'activité. Voici les données :

Id,Salaire,Secteur
1,22000,Restauration
2,21000,Transport
3,25000,Construction
4,35000,Technologie

Ces données sont enregistrées dans un fichier CSV nommé salaires_par_secteur.csv

Installation des packages

Les packages nécessaires doivent être installés. Il nous faut le framework Express, le moteur de template Handlebars, le parser de fichiers csv-parser et enfin le client http axios.


npm install express express-handlebars csv-parser axios

Arborescence du projet

Puisque nous avons opté pour un serveur Express et un moteur de template Handlebars, nous allons organiser à minima notre projet afin de nous conformer à la structure attendue.
Nous allons revenir au fur et à mesure sur le contenu des fichiers mentionnés.

serveur.js
views
 |
 |-- dashboard.handlebars
public
 |
 |--static
      |
      |--js
          |
          |-- salaires.js
data
 |
 |-- salaires_par_secteur.csv

Comme vous le voyez, nous n'avons besoin de 4 fichiers dont le fichier de données salaires_par_secteur.csv évoqué précédemment et placé dans un répertoire data à la racine de projet.
Commençons par voir l'implémentation du serveur.

Le serveur Express

Nous avons déjà évoqué au cours de précédents articles l'implémentation d'un serveur Express, aussi nous n'allons pas revenir en détails dessus. Sur cette base, comme nous l'avions déjà fait également, nous greffons le moteur de template Handlebars.
Ce qui est nouveau dans le cadre de cette version et, ce sur quoi nous allons nous attarder un peu plus, ce sont les 2 routes api/salaires et dashboard qui, respectivement, vont pointer sur le chargement des données et sur le rendu du diagramme en barre.
Voici le code complet du serveur :


//Chargement du systeme de fichier et du parser
const File = require('fs')
const csv = require('csv-parser');

//Chargement express et instanciation du serveur
const express = require('express')
const server = express()

//Chargement du moteur de template
const { engine } = require('express-handlebars');

//Definition de l'adresse et du port du serveur
const host = '127.0.0.1';
const port = 3280;

//Declaration des parametres de lecture du fichier d'entree
const filepath = "./data/salaires_par_secteur2.csv"
const separator = ",";
const skipLines = 1;

const csvOptions = {'separator': separator,
                    'skipLines': skipLines,
                    'headers': false};
var df = [];

//Paramatrage serveur
server.use(express.static('public'));
server.engine('handlebars', engine());
server.set('view engine', 'handlebars');
server.set('views', './views');

server.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  next();
});

//Route API salaires
server.get('/api/salaires', (req, res) => {
File.createReadStream(filepath)
  .pipe(csv(csvOptions))
  .on('data', (data) => df.push({'Id' : String(data[0]), 
                                 'Salaire': parseInt(data[1]),
                                 'Secteur'  : String(data[2]),}))
  .on('end', () => res.send(df))
})

//Route restitution dashboard
server.get('/dashboard', (req, res) => {
  res.render('dashboard', {layout: false});
})

//Information log
server.listen(port, host, () => {
  console.log(`Server running at http://${host}:${port}/`);
});

API des salaires :
Nous ouvrons un flux de lecture du fichier csv puis chargeons un dataframe dans un style clé/valeur que D3JS va être en mesure de traiter. Le dataframe obtenu est au final redirigé vers la réponse au requêteur.
Pour information, une fois le serveur lancé, un appel direct depuis l'explorateur à la route api/salaires affichera ceci :

[{"Id":"1","Salaire":22000,"Secteur":"Restauration"},
 {"Id":"2","Salaire":21000,"Secteur":"Transport"},
 {"Id":"3","Salaire":25000,"Secteur":"Construction"},
 {"Id":"4","Salaire":35000,"Secteur":"Technologie"}]

Dashboard :
La route dashboard sera celle requêtée par l'utilisateur. Elle pointe vers une vue Handlebars du nom de dashboard.handlebars sur laquelle nous allons revenir dans le point suivant.

Vue dashboard

Il s'agit de la seule vue HandleBars de ce projet. Vous constatez que nous n'utilisons pas le layout comme nous l'avons fait dans un précèdent article, c'est pourquoi nous précisons le paramètre {layout: false} lors l'appel à la méthode render. Voici le code de la vue :


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .barre {
            fill: orange;
        }
    </style>
</head>
<body>
    <svg width="600" height="500"></svg>
</body>
</html>

<script src="https://d3js.org/d3.v4.js"></script> 
<script type="module" src="./static/js/salaires.js"></script>

Quelques précisions sur ce code :
- nous définissons un style css pour la classe barre qui sera par la suite générée par D3JS,
- le corps de la page est composé d'un seul conteneur svg qui va abriter le diagramme,
- outre la librairie D3JS, nous chargeons également le script associé à la génération du diagramme, point sur lequel nous allons nous attarder dans la partie suivante.

Génération du diagramme

Nous avons fait appel à un fichier javascript salaires.js depuis la vue dashboard. Ce fichier va contenir le code javascript nous permettant d'appeler l'API afin de récupérer les salaires médians par secteur, puis d'en dresser, via D3JS, le diagramme en barre. Voici ci-dessous le code complet. Vous noterez que les différentes parties du code ont été numérotées afin que les commentaires à suivre soient plus compréhensibles :


//1. Import des librairies d3 et axios
import * as d3 from "https://cdn.skypack.dev/d3@7";
import axios from 'https://cdn.skypack.dev/axios';

//2. Declaration de l'appel asynchrone a l'API 
const apidata = async function getData() {
    return await axios.get('/api/salaires');
}

//3. Fonction principale de generation du diagramme
async function salairesDiagramme() {
    
    //3.1. Appel a l'API, recuperation des donnees
    const dataset = await apidata();

    //3.2. Formatage du canvas svg
    var svg = d3.select("svg"),
            margin = 200,
            width = svg.attr("width") - margin,
            height = svg.attr("height") - margin;

    //3.3. Titre du diagramme
       svg.append("text")
           .attr("transform", "translate(100,0)")
           .attr("x", 70)
           .attr("y", 50)
           .attr("font-size", "22px")
           .text("Salaires par secteur")
    
    //3.4. Mise a l'echelle des axes par rapport au canvas
    var xScale = d3.scaleBand().range ([0, width]).padding(0.4),
        yScale = d3.scaleLinear().range ([height, 0]);

    //3.5. Dimensionnement des axes en fonctions des donnees
    var g = svg.append("g")
               .attr("transform", "translate(" + 100 + "," + 100 + ")");
        xScale.domain(dataset.data.map(function(d) { return d.Secteur; }));
        yScale.domain([0, d3.max(dataset.data, function(d) { return d.Salaire; })]);
    
    //3.6. Edition des labels sur les axes
        g.append("g")
         .attr("transform", "translate(0," + height + ")")
         .call(d3.axisBottom(xScale));

        g.append("g")
         .call(d3.axisLeft(yScale).tickFormat(function(d){
             return d;
         }).ticks(10))
         .append("text")
         .attr("y", 6)
         .attr("dy", "0.70em")
         .attr("text-anchor", "end");
     
     //3.7. Tracage des barres
        g.selectAll(".barre")
         .data(dataset.data)
         .enter().append("rect")
         .attr("class", "barre")
         .attr("x", function(d) { return xScale(d.Secteur); })
         .attr("y", function(d) { return yScale(d.Salaire); })
         .attr("width", xScale.bandwidth())
         .attr("height", function(d) { return height - yScale(d.Salaire); });
}

//4. Appel de la fonction principale
salairesDiagramme();

Déroulons dans l'ordre les différentes étapes qui constituent ce code :
1. Nous importons tout d'abord les librairies d3js et axios depuis le cdn,
2. Nous déclarons ensuite une fonction asynchrone qui va se charger de requêter notre API afin de récupérer le contenu du fichier CSV,
3. Nous avons choisi de dérouler le programme principal dans une fonction même si nous aurions pu nous en passer, il s'agit essentiellement d'une préparation pour de futures évolutions,
3.1. Nous commençons par charger les données en appelant notre API,
3.2. Le canvas déclaré dans la vue Handlebars est récupérer puis ses attributs sont initialisés, il s'agit principalement de sa taille,
3.3. Le titre du graphique est ajouté sur le canvas,
3.4. L'échelle des axes est initialisée, leur longueur est fonction de la taille du canvas,
3.5. L'étendue des axes est fonction des données, un mapping des secteurs pour les abscisses, de 0 au salaire maximum pour les ordonnées,
3.6. Les labels sont édités sur les axes,
3.7. Enfin les barres sont tracées en bouclant via la méthode enter() sur les données. Vous noterez par ailleurs l'attribution de la classe barre qui va nous permettre d'appliquer un style dessus,
4. Il suffit d'appeler pour finir la fonction principale.

Lancement du serveur / requetage

Tout est en place. Il ne reste plus qu'à lancer le serveur via la ligne de commande :


node serveur.js

Puis requêter, depuis un navigateur, l'adresse : http://127.0.0.1:3280/dashboard pour obtenir le résultat suivant :

nodejs express serveur template handlebars api d3js barchart

Retrouvez dans la rubrique "Nos datasets" toutes les données dont vous aurez besoin pour tester et pratiquer !