D3 Pas à pas

2016, dans le bureau du fond d’un openspace.

– Tiens regarde mes beaux tableaux de données… Un peu austère mais c’est ce que le client demande…

tableau

– Un peu austère en effet ! Ne pourrais-tu pas mettre tout ça en forme avec D3 ?

– D3 ? À quoi ça sert ?

D3 signifie Data Driven Documents, c’est-à-dire documents orientés données. C’est une librairie Javascript utilisée la plupart du temps pour transformer des tableaux en jolis graphes SVG.

Beaucoup d’exemples sont disponibles sur le site de D3, mais pour en voir des plus originaux, Mike Bostok recense sur son blog tous les articles du New-York Times qu’il enrichit grâce à D3. Il y a des cartes classiques rouges et bleues des élection américaines, des tableaux de victoires sur les Jeux olympiques mais aussi des ajouts plus originaux, comme cet article sur un voyage où le lecteur voit le chemin avancer au fur et à mesure de la lecture.

– Ah en effet c’est très joli mais… comment cela fonctionne-t’il ?

Fondamentalement, D3 est une librairie de transformation de données.

En entrée : du Json ou du CSV. Et en sortie : tout ce qu’il est possible de faire en Javascript (Json ou CSV, et en pratique, souvent du Html ou du SVG).

1Ces données peuvent être un relevé quotidiens de séances de pompes.
Notez la possibilité de créer du Html.

Il est même possible (pour les tordus) de dessiner dans un Canvas !

Pour ce qui est de la transformation vers Json ou CSV, vos librairies ou frameworks préférés auront probablement déjà des fonctionnalités. Mais là où D3 brille le plus, ce sont les documents dynamiques avec la modification du DOM en temps réel. Et pour cela, il y a plusieurs concepts de D3 à comprendre avant de pouvoir faire son premier graphe.

– Hum… Il me faut quelque chose de très simple, quelles sont les alternatives ?

Il y a dimple, C3 ou une des vingtaines de librairies qui encapsulent D3 pour des diagrammes prêts-à-l’emploi et déjà forts jolis.

– Ah bah, je vais en choisir une !…

Pourquoi pas… Mais un problème surviendra quand on te demandera d’ajouter une notification sur un passage de souris. Puis une ligne supplémentaire là. Et l’aire entre ici et ici, ça serait bien de la mettre en grisé, avec une photo de produit quand on passe au dessus de la légende, sauf pour les mois à 31 jours où on remplace les lignes par des pointillés, et les nuits de pleine lune, la police doit être jaune à pois rouge, etc.

Tu devras probablement t’aventurer hors des cas permis par ces librairies. Et à ce moment, il te faudra connaître le fonctionnement interne de D3. Autant s’y mettre directement puis décider si la surcouche suffit.

– Va pour l’original ! Mais… le SVG c’est un peu loin pour moi… Peut-on réviser le format ?

Ce serait un peu hors du cadre de l’article, je vais donc aller vite : SVG permet de décrire des graphiques vectoriels en XML. Un document SVG peut être intégré dans du XHTML. Un document SVG peut contenir des objets simples : lignes, rectangles, cercles, polygones. Ces objets peuvent être animés avec des transitions et avoir des styles CSS. Le document SVG peut aussi contenir du texte, malheureusement la plupart des navigateurs n’ont pas un support complet de SVG 1.2, excluant souvent le contrôle de flot, ce petit truc pratique qui met des retours à la ligne dans les textes de vos sites Web… Et donc vous êtes prévenus, la gestion de gros blocs de texte multilignes en SVG sera un bon casse-tête.

goflow

Sur le site w3.org , une belle démonstration d’un texte bien inséré dans une forme complexe, possible seulement en SVG 1.2. Tous mes navigateurs échouent à l’afficher et affichent juste le code source.

– Ah… Mais on peut quand même faire des graphes sympas ?

Oui bien sûr ! Et là encore, Mike Bostock nous donne plein d’exemples de code ou de projections de cartes assez complexes. Pour notre apprentissage, rassure-toi, nous allons partir de la base. Tout commence par D3 et un sélecteur du DOM (qui reprend les sélecteurs jQuery).

d3.select("body")<br></br>
.append("p")<br></br>
.text("hello world");

hello world

Ad augusta per angusta.

– Hum… Ceci ajoute un paragraphe à l’élément body et insère le texte incontournable “hello world” dans ce paragraphe ?

Tout à fait ! Note que D3 fait du chaînage de fonction et donc l’ordre est important.

“select” retourne l’élément sélectionné. “append” ajoute un élément à l’élément sélectionné et retourne le nouvel élément. Enfin, “selectAll” retourne la liste des éléments sélectionnés.

Les autres fonctions, comme “attr”, “style”, “class” ou “text”, modifient les attributs ou contenus du ou des éléments sélectionnés et retournent ces mêmes éléments. Ceci permet de chaîner ces appels.

Maintenant on dessine :

var <strong>monRectangle</strong> = d3.select("#graphe")<br></br>
.append("svg") // svg est ajouté à #graphe<br></br>
.append("rect") // un rect est ajouté au svg<br></br>
.class("monolith")<br></br>
.attr("x", 10)<br></br>
.attr("y", 25) // on modifie rect<br></br>
.attr("width", 50)<br></br>
.attr("height", 80); // Le dernier élément sélectionné est le rect donc la variable <strong>monRectangle</strong> pointe toujours dessus.

d3_rect

Oui, c’est un peu anticlimatique, mais Kubrick a fait un film de 2 heures 20 dessus.

– Maintenant je sais : nous insérons une balise svg dans un div. Dedans, nous allons ajouter deux axes. Et pour chaque donnée du tableau, nous générons un rectangle de taille correspondante. Il faut compter avec les marges, les marges entre rectangles, la taille maximale pour les rendre proportionnels. Et puis il faudrait des libellés en abscisse sur les données et en ordonnées sur les valeurs de 10 en 10, …

d3_calcul_coordonnees

Bon, il y a pas mal de choses que D3 va t’aider à faire facilement.

Tout d’abord la déclaration d’une échelle : fonction de transformation des valeurs en coordonnées. La plus courante est l’échelle linéaire : une homothétie qui servira à la plupart des cas.

d3_linear

Ce n’est qu’une fonction Javascript pour transformer une valeur en une autre. D3 facilite l’écriture de celle-ci. Les valeurs de départ sont le domain, les valeurs d’arrivées le range :

var xScale = d3.scale().linear().domain([0, 20]).range([150, 30]);

– Et si on ne veut pas calculer les valeurs du domaine soi-même ?

D3 fournit une fonction qui cherche le min et le max d’un tableau de données en prenant la fonction d’extraction de cette données. Par exemple, si la donnée est du format { “label” : “xxx”, “date” : “xxx”, “valeur” : “xxxx”} , cette fonction sera :

var myDomain = d3.extent(monTableauDeDonnees, function(donnee) {
  return donnee.valeur;

 }))<br></br>
var xScale = d3.scale().linear().domain(<strong>myDomain</strong>).range...<br></br>```

Il existe également des échelles logarithmiques, exponentielles et aussi des échelles de valeurs, ce qui est utile pour assigner des couleurs à des données.

Et comme dit précédemment, c’est une simple fonction de transformation qu’on peut utiliser directement :

var x = xScale(15); // x = 60


var x2 = xScale(30); // x2 = -30```

Ensuite, les axes :

var xAxis = d3.svg.axis().scale(xScale);

Ceci construit un axe avec les labels à sa droite, dont les libellés vont du minimum au maximum du domaine et la coordonnées qui varie allant du minimum au maximum du range. Vous reconnaissez ici la réutilisation de xScale. Cet axe est ensuite inséré avec les autres éléments en SVG, en le déplaçant correctement sur l’autre ordonnée :

<code class="html xml"><span class="javascript">svg.append(<span class="string">"g"</span>).attr(<span class="string">"transform"</span>, <span class="string">"translate(0,"</span> + height + <span class="string">")"</span>)<br></br>
.call(xAxis); </span>

Par défaut un axe est horizontal avec le trait en dessus des labels. Pour avoir un axe vertical, il lui faut spécifier son orientation : left, les labels sont à gauche, right : les labels sont à droite. De même, une orientation “bottom” mettra les labels au dessus du trait.

var yAxis = d3.svg.axis().scale(yScale).orient("left");

Enfin, le plus important : la liaison avec les données.

– Pour chaque donnée, on dessine un rectangle en SVG !

Presque… mais c’est l’inverse en grammaire D3. C’est plutôt “Chacun de ces éléments qui n’existe pas est lié avec telle donnée du tableau”.

– … Qui n’existe pas ?

Tu vas vite comprendre ; reprenons notre tableau de données pour faire des graphismes sur les abdos :

var donneesSport = [{'pompes' : 13, 'abdos' : 8}, {'pompes' : 18, 'abdos' : 17}, {'pompes' : 2, 'abdos' : 4}, {'pompes' : 5, 'abdos' : 18}, {'pompes' : 20, 'abdos' : 15}];

nous partons de div #graphe, auquel nous ajoutons un :

var g = d3.select("#graphe").append(<span class="string">"svg"</span>).attr(<span class="string">"width"</span>, 300).attr("height", 300);

Nous définissons notre échelle qui va aller de 0 au max d’abdos (d3.extent irait du min au max, ce qui dans un graphe n’est pas souhaitable car cela met l’abscisse au niveau du minimum, faisant croire qu’il est nul, alors que chaque abdo compte). Le graphe fera 200 px de hauteur.

var yScale = d3.scale.linear()  
 .domain([0, d3.max(donneesSport, function(d) { return d.pompes; })])  
 .range([0, 200]);

Maintenant, on lie nos rectangles à nos données.

– Mais… on n’a pas encore de rectangle, si ?

Non ! Mais comme on dit à D3 “on lie chaque rectangle à un élément de nos données”, D3 va le voir et créer ceux qui manquent. D’ailleurs, si un rectangle était présent, il aurait été lié avec le 1er objet de la liste. Cela aurait posé problème, car D3 n’aurait pas déclenché le “enter” qui suit la fonction data et donc ses attributs de taille n’auraient pas été modifiés comme les autres.

Bref, comme il n’y a pas de rectangle, pour chaque rectangle qui manque par rapport aux données D3 va faire quelque chose (“enter”). Ce quelque chose, c’est la création d’un rectangle append(“rect”) .

d3.select("#graphe")
    .append("svg")
    .attr("width", 200)
    .attr("height", 200)
    .selectAll("rect")    // On sélectionne tous les rectangles
        <strong>.data(donneesSport)     // Et on les lie un par un à donnéesSport.</strong>
       <strong> .enter()                // Comme il manque des rectangles par rapport à la longueur de données, pour chaque manque...</strong>
        .append("rect")         // ... on ajoute un rectangle.
            .attr("x", function(d, i) { return i * 22; })
            .attr("y", function(d, i) { return 200 - yScale(d.abdos);})
            .attr("width", 20)
            .attr("height", function(d, i) { return yScale(d.abdos);} );

– Pourquoi cette fonction en y ?

C’est nécessaire car le référentiel SVG commence avec 0 vers le haut. Pour ne pas avoir une barre de 16 de hauteur dans un graphe de 0 à 20 qui soit collée vers le haut, il faut faire une barre de 16 de hauteur qui démarre à (20-16) de hauteur.

– Et ces fonctions avec d et i ?

Ce sont des fonctions qui traitent chaque donnée du tableau passée en data : d est l’élément courant, i l’index. Vu que en x, on veut juste décaler les colonnes, on utilise l’index de la donnée en cours, et pour y, on a besoin de la donnée, on l’utilise, ou plutôt dans ce cas, la mise à l’échelle d’un champ de la donnée.

d3_une_donnee

Le code est disponible sous jsFiddle.

– Et si on voulait afficher les données sur les pompes et les données sur les abdos côte à côté, on ajoute un .append(“rect”) pour les pompes ?

Non, ceci ajouterait un rectangle au rectangle d’abdos et non au même niveau. Il faut donc mettre les 2 rectangles dans un conteneur SVG “g”, et faire deux fois le append :

var monElement = d3.select("#graphe")
    .append("svg")
    .selectAll("g")
    .data(donneesSport)
    .enter().append("g");

monElement .append("rect")
    .attr("x", function(d, i) { return i * 22; })
    .attr("y", function(d, i) { return 200 - yScale(d.abdos);} )
    .attr("width", 5)
    .attr("height", function(d, i) { return yScale(d.abdos);} )
    .attr("fill", "blue");

monElement .append("rect")
    .attr("x", function(d, i) { return i * 22 + 6; })
    .attr("y", function(d, i) { return 200 - yScale(d.pompes);} )
    .attr("width", 5)
    .attr("height", function(d, i) { return yScale(d.pompes);} )
    .attr("fill", "red");

d3_deux_donnees

Le code est disponible sous jsfiddle.

Allez, pour t’avancer un peu plus, je mets l’axe Y. Il faut l’ajouter au SVG, mais il y a un piège…

– Hum… Si on fait un append(g) avant le selectAll(g), D3 prendra l’axe pour un des éléments correspondant au tableau ?

Tout à fait, d’où un niveau supplémentaire de qui contient le l’axe pour le premier, et la liste des correspondant aux données pour le second (jsFiddle du code) :

d3_tableau_axes

– Ce n’est pas vraiment aussi beau que sur les exemples de Mike Bostok…

Ce n’est que le début ! La prochaine fois, on fait de l’animation et on regarde comment faire une directive angular !