Dessiner des graphiques avec d3js

Voir la vidéo

Dans ce tutoriel je vous propose de découvrir la librairie d3 qui permet de générer des graphiques en tout genre.

00:00 Présentation
00:50 Installation et utilisation du SVG
03:22 Découverte du principe de sélection
11:20 Présentation de l'objectif
11:50 Chargement du CSV
14:09 Les échelles
19:40 Les axes
25:26 Créer une ligne
28:21 Histogramme

Pourquoi d3 plutôt que chart.js ou autre ?

Avant de commencer à découvrir la librairie, il est intéressant de voir la différence par rapport à ce qui existe déjà (comme ChartJS ou ApexCharts). Même si au premier abord ces librairies peuvent sembler similaires la portée et le cas d'utilisation de d3 est très différent :

  • Chart.js et autre propose une liste de représentation prédéfinies (ligne, area, bar...) avec des options de personnalisation plus ou moins avancées. Convient pour des cas classiques mais s'avère rapidement limité pour des représentations complexes.
  • D3 est plus bas niveau et offre des fonctions pour créer les différents éléments associés à des graphiques (ligne, courbes, axes...).

D3 offre ainsi plus de flexibilité pour créer une représentation qui correspond à des besoins spécifiques, mais nécessite d'écrire plus de code pour définir tous les éléments que l'on souhaite avoir (à titre d'exemple [voici le code permettant de tracer une ligne)[https://observablehq.com/@d3/line-chart/2?intent=fork]).

Concepts de base

D3 est une librairie qui contient de nombreuses fonctions qui correspondent à différents cas d'utilisations. La meilleur façon de découvrir ces fonctions est de jetter un coup d'oeil aux exemples de visualisation. Cependant je vous propose de nous attarder sur quelques notions spécifiques qui vous permettra de mieux comprendre la librairie.

Manipuler le DOM

Pour créer notre visualisation on peut utiliser un canvas ou un SVG. Dans le cas du SVG d3 offrira une API permettant de simplifier la création / modification d'éléments dans le DOM.

const $svg = d3.create('svg')
    .attr('width', width)
    .attr('height', height)
    .attr('viewBox', `0 0 ${width} ${height}`)

const $points = $svg.append('g')
    .attr('class', 'points')

$points.append('circle')
    .attr('r', 10)
    .attr('cx', 100)
    .attr('cy', 100)
    .attr('fill', 'red')

document.body.appendChild($svg.node())

En soit, ce n'est pas révolutionnaire mais les méthodes peuvent être chaînées, ce qui rend l'écriture plus pratique que les API natives. Il est aussi possible de manipuler plusieurs éléments ensembles ce qui permet un code plus concis et plus lisible.

Les échelles

Pour créer notre visualisation on cherchera souvent à représenter des données dans notre échelle (le domaine) vers l'échelle de notre graphique (zone de dessin). On peut pour cela utiliser les méthodes d'échelle :

const y = d3.scaleLinear()
    .domain([0, maxPopulation]) 
    .range([height - marginBottom, marginTop])

Cette méthode renvoie une fonction que l'on pourra utiliser pour convertir une valeur dans l'échelle de notre graphique.

const positionY = y(60_000_000) // 300

En créant 2 échelles (en X et en Y) il est ensuite possible de calculer la position des points à placer dans notre graphique. Par exemple on peut utiliser nos échelles pour créer une ligne représentant nos données :

const line = d3.line()
    .x(d => x(d.year))
    .y(d => y(d.population))

line(data) // M 10 20 L 30 0 ....

Les axes

Il est aussi possible à partir de ces échelles de générer des axes que l'on pourra placer dans notre SVG.

svg.append("g") 
    // On place notre Axe X en bas du graphique
    .attr("transform", `translate(0,${height - marginBottom})`) 
    // Cette méthode se charge de générer les éléments de l'axe
    .call(d3.axisBottom(x));

Il existe des méthodes pour les différentes positions (haut, bas, droit, gauche) mais aussi pour personnaliser le rendu (nombre de repères, longueur des traits...)

Sélections

Lorsque l'on souhaite créer plusieurs éléments (comme des points) on pourra utiliser le système de sélection de D3 qui permet de sélectionner plusieurs éléments en y associant nos données.

$svg.selectAll('circle')
    .data(data, d => d.id)

La méthode selectAll() permet de créer une sélection contenant tous les éléments correspondant au sélecteur (dans notre cas, vu qu'il n'y a pas de cercle la sélection sera vide). La méthode data() va comparer les données associées à notre sélection aux nouvelle données et former 3 groupes :

  • enter, représente les nouveaux éléments
  • update, représente les éléments qui sont déjà présent dans notre sélection
  • exit, représente les éléments qui ne sont plus dans notre sélection

Pour effectuer des actions sur ces différents groupes on pourra utiliser la méthode join

$svg.selectAll('circle')
    .data(data, d => d.id)
    .join(
        enter => enter.append('circle')
            .attr('cx', d => d.x)
            .attr('cy', d => d.y)
            .attr('r', 10),
        update => update
            .attr('cx', d => d.x)
            .attr('cy', d => d.y)
        exit => exit.remove()
    )

Le retour de cette fonction va renvoyer une sélection qui contiendra les éléments ajoutés et mis à jour (on peut donc écrire le code suivant pour éviter la répétition).

$svg.selectAll('circle')
    .data(data, d => d.id)
    .join(
        enter => enter.append('circle').attr('r', 10),
        update => update
        exit => exit.remove()
    )
    .attr('cx', d => d.x)
    .attr('cy', d => d.y)

Lorsqu'un élément est ajouté ou mis à jour d3 attache une propriété __data__ sur l'élément qui contient la ligne associé. C'est cette données qui est lue lorsque l'on passe un callback avec la méthode attr par exemple. Cela permet aussi de récupérer la données à partir d'une sélection.

$svg.selectAll('circle').data() // Renvoie la donnée

Mais cela permet aussi de mettre à jour le DOM lorsque la donnée change

$svg.selectAll('circle')
    .data(newData, d => d.id)
    .join(
        enter => enter.append('circle').attr('r', 10),
        update => update
        exit => exit.remove()
    )
    // On déplace les points aux nouvelles position
    .transition()
    .duration(500)
    .attr('cx', d => d.x)
    .attr('cy', d => d.y)

La librairie va détecter les différences entre les données et va modifier le DOM en fonction.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager