D3.js

De BdC de chez Wam...


Cette page est une copie de : Visualisation de données - d3.js

d3.js est une librairie javascript très complète avec beaucoup d’exemples à disposition, avec une personnalisation totale possible. Elle permet l’accès à des primitives SVG permettant toute innovation. Malheureusement, elle est peu accessible directement et assez technique.

Fonctionnement typique

L’idée principale est de lier les données au DOM (Document Object Model), et d’appliquer des transformations, basées sur les données, au document.

Il y a plusieurs concepts spécifiques à bien comprendre pour l’utiliser pleinement :

  • Sélection, modification, ajout et insertion d’éléments
  • Ajout de données au DOM
  • Propriété dynamique, et Transformation
  • Chaînage des fonctions

Base

Sélection

Il existe deux fonctions de sélection respectivement d’un seul élément (avec select()) et de l’ensemble des éléments (avec selectAll()) correspondant à la définition passée en paramètre :

var selection1 = d3.select("selecteur");
var selection2 = d3.selectAll("selecteur");

Quand on utilise une sélection d’un seul élément (avec select() donc), mais qu’il existe plusieurs éléments de ce type dans le document, c’est le premier qui est renvoyé.

La sélection se fait de manière identique que pour le CSS :

  • "balise" : pour tout ce qui est balise html (div, h1, body, …)
  • ".maclasse" : pour le ou les objets de la classe indiquée (indiquée via "class='maclasse'")
  • "#identifiant" : pour l’objet (unique normalement) avec cet identifiant (indiquée via "id='identifiant'")

Ces sélecteurs permettent ensuite d’accéder à plusieurs fonctions utiles pour manipuler les objets, comme nous le verrons. Voici trois premières fonctions :

  • size() : taille de la sélection
  • empty() : sélection vide ou non
  • html() : contenu html de la sélection

Modification, ajout et insertion d’éléments

Plusieurs fonctions permettent de modifier les éléments sélectionnés (comme par exemple style() pour appliquer des règles CSS ou html() pour modifier le contenu de la balise). Le code suivant permet de mettre le texte en rouge pour tout le corps de la page

var corps = d3.select("body");
corps.style("color", "red");

Deux fonctions sont utiles pour respectivement insérer un élément HTML fils à la fin (append()) ou au début (insert()) d’un élément père, qui s’utilisent comme suit :

selection.append("balise");
selection.insert("balise");

Exemple

Ici, on sélectionne la balise <body> (qui est vide au départ). Dans cette sélection, on ajoute deux balises div, pour mettre dans la première la taille de la sélection (1 normalement) et dans la seconde le test si elle est vide ou non (normalement false). Enfin, on met le texte de la sélection en rouge (donc tout).

// Sélection de la balise body
var corps = d3.select("body");

// Ajout de deux balises div au corps
var div1 = corps.append("div");
var div2 = corps.append("div");

// Définition du contenu des deux balises
div1.html("Je met du texte ici.");
div2.html("Nombre de div : " + d3.selectAll("div").size());

// Modification de la couleur de la police
corps.style("color", "red");
div2.style("color", "steelblue");

Ajout de données au DOM

Avec la fonction data() sur une sélection, il est possible de lier les données passées en paramètres (ce doit être un tableau) au DOM à la sélection en question. Le code suivant affecte chaque élément du tableau à chaque élément renvoyé par le sélecteur précédent

var selection = d3.selectAll("selecteur");
selection.data(tableau);

S’il y a différence entre la taille de la sélection et la taille du tableau passé en paramètre de data(), il existe deux fonctions utiles pour gérer ces cas :

  • enter() : pour gérer les éléments du tableau en plus
  • exit() : pour gérer les éléments de la sélection en plus

Propriété dynamique

Sur chaque sélection, on peut appliquer des modifications de style ou de contenu (voire autre), en fonction des données sont liées au DOM. On passe par l’utilisation d’une fonction anonyme, dont les paramètres peuvent être, dans cet ordre :

  • la valeur de l’élément du tableau affectée à l’élément
  • la position de la valeur dans le tableau
  • il est possible de n’utiliser que la valeur, voire aucun paramètre si besoin

var selection = d3.selectAll("selecteur");
selection.data(tableau);
selection.html(function(d, i) {
    return "position = " + i + ", valeur = " + d;
})

Exemples

Ajout de données + propriété dynamique

Dans cet exemple, on affecte les données du tableau (qui contient des couleurs) à chaque div du body. Et on affecte un style CSS (la couleur), en prenant comme valeur celle contenu dans le tableau.

HTML

Pomme
Tomate
Fleur

JS

var div = d3.selectAll("div");
div.data(["green", "red", "blue"])
div.style("color", function (d) { return d; });

fonction enter()

Le tableau passé en paramètre de data() est ici plus long que la sélection. Pour les valeurs supplémentaires du tableau (sélectionnées avec le enter()), on ajoute des div (avec append()). HTML

Pomme
Tomate
Fleur

JS

// div : liste des div présents dans le HTML
var div = d3.select("body").selectAll("div");
/* Le select("body") n'est la que pour s'assurer que les div soient bien
   ajoutée dans le body et non après */
// div_data : div avec les données associées
var div_data = div.data(["green", "red", "blue", "orange", "purple"]);
// affectation de la couleur à chaque div
div.style("color", function (d) { return d; });
// div_enter : données en trop
var div_enter = div_data.enter();
// div_nv : nouvelles div ajoutée à partir des données en trop
var div_nv = div_enter.append("div");
// définition du contenu des nouvelles div
div_nv.html("div ajoutée");
// affectation de la couleur à chaque nouvelle div
div_nv.style("color", function (d) { return d; });

fonction exit()

C’est ici le contraire, le tableau est plus petit que la sélection. Pour les éléments de la sélection en trop (sélectionnés avec le exit()), on les supprime (avec remove()). HTML

Pomme
Tomate
Fleur

JS

// div : liste des div présents dans le HTML
var div = d3.select("body").selectAll("div");
// div_data : div avec les données associées
var div_data = div.data(["green", "red"]);
// affectation de la couleur à chaque div
div.style("color", function (d) { return d; });
// div_exit : div en trop
var div_exit = div_data.exit();
// suppression de ces div en trop
div_exit.remove();
// on peut remplacer par cette ci-dessous, qui change le contenu de la div en trop
// div_exit.html("en trop");

Chaînage des fonctions

Il faut absolument comprendre le principe généralement appliqué en JS orienté objet :

Toute fonction d’un objet renvoie cet objet

Ceci est vrai sauf si la fonction a pour but de renvoyer un résultat spécifique. Et cela ne concerne donc que les procédures (qui sont aussi des fonctions en JS).

Le corollaire de ce principe est intéressant :

Il est possible d’enchaîner un grand nombre de fonctions directement

Dans l’exemple ci-dessous, on utilise ce principe pour créer autant de div qu’il y a de couleurs dans le tableau, en indiquant le contenu HTML de celles-ci, et en leur appliquant un style CSS spécifique.

var couleur = ["green", "red", "blue", "orange", "purple"];
d3.select("body").selectAll("div")
  .data(couleur)
  .enter().append("div")
    .html(function(d,i) { return "Div n°" + (i + 1) + " : " + d; })
    .style("color", function(d, i) { return d; });

Lecture de données

Il existe plusieurs fonctions dans la librairie D3 pour charger des données de tout type (JSON, CSV, TSV, XML, …). Les fonctions pour le faire sont toutes de type d3.xxx() (xxx étant remplacé par le format approprié - d3.json() pour du JSON par exemple).

L’exemple ci-dessous charge les données contenues dans le fichier mpg.csv (qui recense un ensemble de voitures, avec plusieurs caractéristiques).

d3.csv("https://fxjollois.github.io/donnees/mpg.csv",
       function(err, don) {
  console.log(don);
  if (err)
    d3.select("body").html("Erreur : " + err)
  else {
    d3.select("body").selectAll("div")
      .data(don)
      .enter()
      .append("div")
      .html(function(e, i) {
        var r = "" + e.manufacturer + ", " +
            e.model +
            " (" + e.year + ")";
        return r;
     });
   }
})

Ajout d’interactivité

Il est possible d’ajouter des gestions d’événements sur les objets créés via la fonction on(). Celle-ci prend en premier paramètre l’événement (par exemple mouseover pour gérer le positionnement de la souris sur l’objet), et en deuxième paramètre la fonction à appliquer lors de la survenue de cet évènement.

Dans cette fonction, il n’y aucun paramètre possible. Mais nous avons accès à l’objet via this. Si de plus, nous avons pris le soin d’ajouter des propriétés à cet objet (via la fonction property("nom", valeur)), nous pouvons y accéder à la valeur via this.nom.

Dans l’exemple ci-dessous, nous ajoutons à chaque div la gestion du passage de la souris sur celle-ci (ainsi que sa sortie). Pour cela, nous ajoutons une propriété couleur à chacune, qui prendra la valeur de la couleur dans le tableau. Ensuite, on indique que lorsque la souris passe sur la div (on("mouseover", ...)), on change la couleur de la police par celle spécifique à la div.

Si nous ne gérons pas la sortie de la souris, la couleur ne sera jamais rechangé. Nous gérons donc ce cas (via on("mouseout, ...)) en indiquant que la couleur redevient noire.

HTML

Première div
Deuxième div
Troisième div

JS

d3.selectAll("div")
   .data(["green", "red", "blue"])
   .property("couleur", function(d) { return d;})
   .on("mouseover", function () {
      d3.select(this).style("color", this.couleur);
   })
   .on("mouseout", function () {
      d3.select(this).style("color", "black");
   });

Pour récupérer les informations de la souris, il existe l’objet d3.event qui contient en particulier les informations suivantes :

  • clientX et clientY : position relative à la partie visible du navigateur
  • screenX et screenY : position relative au moniteur
  • offsetX et offsetY : position relative à l’objet sur lequel la souris est (implémentation variable entres les navigateurs)
  • pageX et pageY : position relative au document HTML

Voici un petit exemple de ce qu’on peut récupérer comme informations.

HTML

<svg width=200 height=100></svg>
<div id = "infos">
   <div id = "client"></div>
   <div id = "screen"></div>
   <div id = "page"></div>
  <div id = "offset"></div>
</div>

CSS

svg {
   border: solid 1px black;
   margin-left: 50px;
   margin-top: 50px;
}
#infos {
   width: 200px;
   float: right;
}
#client::before {
   content: "client : "
}
#screen::before {
   content: "screen : "
}
#page::before {
   content: "page : "
}
#offset::before {
   content: "offset : "
}

JS

d3.select("svg")
   .on("mousemove", function () {
       m = d3.event;
       d3.select("#client").html(m.clientX + "-" + m.clientY);
       d3.select("#screen").html(m.screenX + "-" + m.screenY);
       d3.select("#page").html(m.pageX + "-" + m.pageY);
       d3.select("#offset").html(m.offsetX + "-" + m.offsetY);
   })
   .on("mouseout", function () {
       d3.select("#infos").selectAll("div").html("");
   });

Graphique SVG

La librairie d3 permet de créer des graphiques au format SVG (Scalable Vector Graphics), et c’est régulièrement dans ce cadre qu’on l’utilise.

Ces graphiques sont définis dans un langage de type XML (et donc similaire à HTML). C’est un langage de définition basé sur des primitives de dessin (rectangle, cercle, ligne, texte, …), qui permet de produire tout type de graphique. L’un des gros avantages est qu’ils sont zoomable sans perte de définition.

Vous pouvez trouver dans les liens qui suivent un certain nombre d’informations sur ces graphiques :

Dans l’exemple ci-dessous, nous créons un graphique de largeur 200 pixels et de hauteur 100 pixels. Une fois créé, on ajoute une transformation (via la balise g ajoutée). Celle-ci est une translation de 10 pixels en x et de 10 pixels en y. C’est le résultat de la translation qui est renvoyé, ce qui veut dire que tout ce qu’on ajoute intégrera donc cette première translation.

On ajoute ensuite un rectangle dont le point haut gauche est situé en (0,0). Notez donc que le point origine est donc situé en haut à gauche sur un écran. Ce rectangle est un carré de 50 pixels, rempli en rouge. On ajoute finalement un texte.

CSS

svg {
   border: solid 1px black;
}

JS

var graph = d3.select("body").append("svg")
    .attr("width", 200)
    .attr("height", 100)
  .append("g")
    .attr("transform", "translate(10, 10)");

graph.append("rect")
  .attr("x", 0).attr("y", 0)
  .attr("width", 50).attr("height", 50)
  .style("fill", "red");

graph.append("text")
  .attr("x", 75).attr("y", 50)
  .text("Voici un graphique")
  .style("font-size", ".75em");

Echelles

Dans un graphique, nous devons faire un passage d’échelle entre les données et la zone graphique. Par exemple, si l’on doit afficher des valeurs entre -1000 et 1000 sur l’axe x, il nous faut une fonction pour les transformer dans l’intervalle [0,largeur] (où largeur représente la largeur du graphique SVG produit).

Les fonctions dans D3 pour réaliser cela ont toutes comme nom d3.scaleXxx(), où Xxx est à remplacer par le type de changement d’échelle qu’on souhaite. Il faut notre que ces fonctions renvoient elle-même une fonction de changement d’échelle. Il faut de plus déterminer deux éléments importants :

  • Le domaine (ou domain) : la plage des données d’origine
  • L’étendu (ou range) : la plage de ce qu’on doit obtenir au fina

Quantitatif

Vers du numérique

L’exemple proposé ci-dessus est typiquement un problème de changement d’échelle linéaire. Il existe pour cela la fonction d3.scaleLinear().

var echelle = d3.scaleLinear()
        .domain([-1000, 1000])
        .range([0, 100]);
console.log(echelle(-1000)) // renvoie 0
console.log(echelle(0)) // renvoie 50
console.log(echelle(1000)) // renvoie 100

Vers des couleurs

L’intérêt de ces échelles réside aussi dans la possibilité de passer de valeurs numériques à des couleurs par exemple. On doit juste définir dans l’étendu les couleurs de début et de fin (et éventuellement certaines intermédiaires).

var echelle = d3.scaleLinear()
        .domain([-1000, 1000])
        .range(["red", "green"]);
console.log(echelle(-1000)) // renvoie "#ff0000"
console.log(echelle(0)) // renvoie "#804000"
console.log(echelle(1000)) // renvoie "#008000"

Qualitatif

Vers du numérique

Un autre changement d’échelle classique est le passage d’un ensemble de modalités à une plage de valeurs. Pour cela, on utilise la fonction d3.scaleBand(). Ici, nous définissons l’étendu par bandes ("A" sera sur la bande ainsi [0, 33.33…])

var echelle = d3.scaleBand()
        .domain(["A", "B", "Z"])
        .range([0, 90]);
console.log(echelle("A")) // renvoie 0
console.log(echelle("B")) // renvoie 30
console.log(echelle("Z")) // renvoie 60
console.log(echelle.bandwidth()) // renvoie 30

Vers des couleurs

Dans ce cas aussi, on peut affecter une couleur à chaque modalité, toujours en définissant des couleurs dans l’étendu. Il existe de plus des fonctions spécifiques pour cela dans d3, comme d3.scaleOrdinal(), dans lesquelles il n’y a pas d’étendu à définir.

var echelle = d3.scaleOrdinal(d3["schemeSet1"])
    .domain(["A", "B", "Z"]);
console.log(echelle("A")) // renvoie "#1f77b4"
console.log(echelle("B")) // renvoie "#ff7f0e"
console.log(echelle("Z")) // renvoie "#2ca02c"

Exemple

Dans l’exemple ci-dessous, nous allons utiliser les fonctions de D3 pour faire tous les changements d’échelle vu ci-dessus. Ceux-ci sont classiques dans la création d’un graphique avec cette librairie.

Nos données concernent la répartition des logements en location par AirBnB sur Paris le 2 septembre 2015 (voir les données ici), par type (logement complet, chambre privée, chambre partagée). Nous avons le décompte et le prix moyen comme information.

HTML

<table>
   <theader>
      <tr>
         <th colspan=3>Type</th>
         <th></th>
         <th colspan=3>Price</th>
      </tr>
      <tr>
         <td colspan=3>(qualitative)</td>
         <td></td>
         <td colspan=3>(quantitative)</td>
      </tr>
      <tr>
         <th>original</th>
         <th>en numérique</th>
         <th>en couleur</th>
         <th width = 50></th>
         <th>original</th>
         <th>numérique</th>
         <th>couleur</th>
      </tr>
   </theader>
   <tbody id = "tab">
   </tbody>
</table>

CSS

td {
   text-align: center;
}

JS

don = [
   { type: "Entire home/apt", count: 35185, price: 106 },
   { type: "Private room", count: 5827, price: 56 },
   { type: "Shared room", count: 464, price: 40 },
   { type: "Other type", count: 10, price: 200}
];

var type_modalites = don.map(function(d) { return d.type; });
var prices = don.map(function(d) { return d.price; });
var price_min = d3.min(prices);
var price_max = d3.max(prices);

var qt2num = d3.scaleLinear()
    .domain([price_min, price_max])
    .range([0, 100]);
var qt2col = d3.scaleLinear()
    .domain([price_min, price_max])
    .range(["lightgreen", "steelblue"]);
var ql2num = d3.scaleBand()
    .domain(type_modalites)
    .range([0, 100]);
var ql2col = d3.scaleOrdinal(d3["schemeSet1"])
    .domain(type_modalites);

var tbody = d3.select("#tab").selectAll("tr")
  .data(don)
  .enter().append("tr")
  .html(function(d) {

    var chaine = "" + d.type + "" +
        "" + ql2num(d.type) + "" +
        "" +
        "" + d.price + "" +
        "" + qt2num(d.price) + "" +
        "";

    return chaine;
  });

Création d’un graphique

Dans l’exemple ci-dessous, nous utilisons l’ensemble des éléments vu ci-dessous pour créer un diagramme en barres, pour les données suivantes :

Type Nombre Prix moyen
Entire home/apt 35185 106
Private room 5827 56
Shared room 464 40

Le code est commenté pour expliquer ce que chaque partie permet de réaliser dans le graphique.

index.html

<!DOCTYPE html>
<html>

  <head>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <link rel="stylesheet" href="style.css">
  </head>

  <body>
    <p>Graphique</p>
    <div id="graph"></div>
    <script src="script.js"></script>
  </body>

</html>

style.css

.bar {
  fill: orange;
}

.axis {
  font-size: 10px;
}

.axis line {
  stroke: black;
  fill: none;
}

.axis path, .x.axis line {
  display: none;
}

script.js

// Tableau de données obtenu sur les données AirBnB
donnees = [
  { type: "Entire home/apt", count: 35185, price: 106 },
  { type: "Private room", count: 5827, price: 56 },
  { type: "Shared room", count: 464, price: 40 }
];

// Liste des modalités de la variable type
var type_modalites = donnees.map(function(d) { return d.type; });
// Prix (moyen) maximum
var prix_max = d3.max(donnees, function(d) { return d.price; });

// Définition des marges et de la taille du graphique
var marges = {haut: 20, droit: 20, bas: 30, gauche: 40},
    largeurTotale = 400,
    hauteurTotale = 300,
    largeurInterne = largeurTotale - marges.gauche - marges.droit,
    hauteurInterne = hauteurTotale - marges.haut - marges.bas;

// Echelle pour les prix sur l'axe Y
var echelleY = d3.scaleLinear()
    .domain([0, prix_max])
    .range([hauteurInterne, 0]);
// Echelle pour le type sur l'axe X
var echelleX = d3.scaleBand()
    .domain(type_modalites)
    .range([0, largeurInterne])
.padding(0.2);
// Echelle pour le type affectant une couleur automatique à chaque type
var echelleCouleur = d3.scaleOrdinal(d3["schemeSet1"])
    .domain(type_modalites);

// Création de l'axe X
var axeX = d3.axisBottom()
    .scale(echelleX);

// Création de l'axe Y
var axeY = d3.axisLeft()
    .scale(echelleY);

// Création du graphique
var graphique = d3.select("#graph").append("svg")
    .attr("width", largeurTotale)
    .attr("height", hauteurTotale)
  .append("g")
    .attr("transform", "translate(" + marges.gauche + "," + marges.haut + ")");

// Ajout de l'axe X au graphique
graphique.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + hauteurInterne + ")")
  .call(axeX);

// Ajout de l'axe Y au graphique
graphique.append("g")
    .attr("class", "y axis")
  .call(axeY);

graphique.append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Prix moyen");
    
// Ajout d'une barre pour chaque type de logement, avec une taille fonction du prix moyen
graphique.selectAll(".bar")
  .data(donnees)
  .enter()
  .append("rect")
  .attr("class", "bar")
  .attr("x", function(d) { return echelleX(d.type); })
  .attr("width", echelleX.bandwidth())
  .attr("y", function(d) { return echelleY(d.price); })
  .attr("height", function(d) { return hauteurInterne - echelleY(d.price); })
  .style("fill", function(d) { return echelleCouleur(d.type); });