D3.js: les essentiels, partie 2

Un pense-bête autant qu'un tutoriel

Quelques uns des basiques de D3.

Petit rappel : cette page a été réalisée avec les notes prises lors du visionnage des excellents tutoriels sur D3 par l'excellent Vienno, consultables (et je vous y encourage) sur Youtube.

Arc de cercle.

Si l'on sait réaliser un cercle depuis longtemps, on ne sait pas comment le diviser en plusieurs parties. Pour cela, il faut comprendre comment créer un arc de cercle. Il existe là aussi un path generator spécialement conçu, qui prend 4 paramètres: le rayon intérieur (innerRadius), le rayon extérieur (outerRadius), l'angle de départ et l'angle d'arrivée. Attention, les angles sont mesurés ici en radion et non en degré.

var canvas=d3.select("body")
		.append("svg")
		.attr("width",500).attr("height",500);

var r= 100;
var p=Math.PI*2;
var group= canvas.append("g")
    .attr("transform", "translate(200,200)");

var arc = d3.svg.arc()
    .innerRadius(r-60)
    .outerRadius(r)
    .startAngle(1)
    .endAngle(p-1);

group.append("path")
    .attr("d",arc);

Le rayon d'un cercle équivaut à 2 fois pi, si l'on mesure en radion. Il y a donc environ 6,28 radions dans un cercle. Jouer avec ces quatre données permet de bien comprendre comment fonctionne ce principe d'arc de cercle en d3.

Du coup, on comprend qu'on n'est plus très loin de savoir réaliser un graphique pour des résultats électoraux par exemple...

Donut et pie chart (c'est plus appétissant qu'anneau et camembert)

On va tenter de travailler avec les données de la quatorzième législature de l'Assemblée Nationale, autrement dit, les couleurs politiques des députés actuels.

var canvas=d3.select("body")
		.append("svg")
		.attr("width",500).attr("height",500);

var r= 100;
var p=Math.PI*2;
var group= canvas.append("g")
    .attr("transform", "translate(200,200)");

var arc = d3.svg.arc()
    .innerRadius(r-60)
    .outerRadius(r)
    .startAngle(1)
    .endAngle(p-1);

group.append("path")
    .attr("d",arc);

Nous créons un tableau contenant les données, les noms des partis et leur couleur respective. On décale l'ensemble de 200px à droite et en bas. Avec la variable arc, on indique le rayon et le rayon intérieur. La nouveauté est ici l'appel à la méthode d3.layout.pie( ), qui va chercher les valeurs contenues dans les données fournies. On crée ensuite une variable pour les arcs qui prendront la valeur des données, passées au filtre de "pie". Pour chaque path, on donne la couleur qui se trouve dans le tableau et pour chaque étiquette, on donne un positionnement au centre de l'arc.

On constate qu'on a encore pas mal de boulot pour rendre un tel graphique hyper élégant, m'enfin, on peut déjà trouver à juste titre qu'on progresse.

Pour ne rien vous cacher, on progresse même tellement vite qu'on va directement transformer l'anneau en camembert en ne changeant qu'un élément du graphique:

.innerRadius(0)
Pour le fun, on ajoute également à la dernière ligne une fonctionnalité qui permet d'afficher le nombre de députés de chaque famille:
.text(function(d,i){return donnees[i].name  + donnees[i].chiffre});
Et bim !

Arbre généalogique, arbre de causalité et tous les autres

Imaginons que vous vouliez réaliser un petit arbre généalogique. Pour ne pas trop compliquer les choses, on va ici reprendre l'exemple de la législature parlementaire de 2012.

Mais d'abord apprenons à réaliser une jolie diagonale.

var canvas=d3.select("body")
    .append("svg")
	.attr("width",500).attr("height",500);

var diagonal = d3.svg.diagonal()
    .source({x:10, y:10})
    .target({x:400,y:450});
canvas4.append("path")
    .attr("fill", "none").attr("stroke", "white")
    .attr("d", diagonal)

Avec le générateur de diagonale, on donne une origine et un objectif et zou, ou presque.

Pour réaliser notre arbre de l'Assemblée nationale, on va diviser les 577 députés en majorité ou opposition puis en partis. On a donc un fichier json (pour ceux qui ne connaissent pas ce format, lisez ici), nommé assemblee.json comme cela:

{
    "name": "Assemblee",
    "children": [
        {
            "name": "Majorite","chiffre":335,"couleur": "pink",
            "children": [
                {
                    "name": "PS",
                    "chiffre": 285,
                    "couleur": "pink"
                },
                {
                    "name": "Ecolo",
                    "chiffre": 18,
                    "couleur": "green"
                },
                {
                    "name": "RRDP",
                    "chiffre": 17,
                    "couleur": "yellow"
                },
                {
                    "name": "GDR",
                    "chiffre": 15,
                    "couleur": "red"
                }
            ]
        },
        {
            "name": "Opposition","chiffre":242,"couleur": "blue",
            "children": [
                {
                    "name": "UMP",
                    "chiffre": 199,
                    "couleur": "blue"
                },
                {
                    "name": "UDI",
                    "chiffre": 30,
                    "couleur": "steelblue"
                },
                {
                    "name": "NI",
                    "chiffre": 9,
                    "couleur": "black"
                }
            ]
        }
    ]
}

Attention, on ne se laisse pas impressionner, on explique tout ça en dessous.

var canvas=d3.select(".result-side5")
		.append("svg")
		.attr("width",650).attr("height",650)
		.append("g")
		    .attr("transform","translate(20,20)");

var tree = d3.layout.tree()
    .size([600,520]);

d3.json("donnees/assemblee.json", function (data){
    var nodes=tree.nodes(data);
    var links= tree.links(nodes);
    
    var node =  canvas.selectAll(".node")
        .data(nodes)
        .enter()
        .append("g")
        .attr("class","node")
        .attr("transform", function (d){
        return "translate("+ d.x + "," + d.y + ")";
        });

    node.append("circle")
    .attr("r", function(d){return d.chiffre/5})
    .attr("fill", function(d,i) {return d.couleur;});
    
    node.append("text")
    .attr("transform","translate(4,20)")
    .text(function(d){return d.name;});

    var diagonal=d3.svg.diagonal()
    .projection(function(d) {return [d.x, d.y];});

    canvas.selectAll(".link")
    .data(links)
    .enter()
    .append("path")
    .attr("class", "link")
    .attr("fill","none")
    .attr("stroke", "white")
    .attr("d",diagonal);
})

Cela mérite bien sûr explication. Essayez de suivre ligne par ligne:

var canvas=d3.select("body")
    .append("svg")
	.attr("width",650).attr("height",650)
	.append("g")
	.attr("transform","translate(20,20)");
		
var tree = d3.layout.tree()
    .size([600,520]);

d3.json("donnees/assemblee.json", function (data){
    var nodes=tree.nodes(data);
    var links= tree.links(nodes);
    
    var node =  canvas.selectAll(".node")
        .data(nodes)
        .enter()
        .append("g")
        .attr("class","node")
        .attr("transform", function (d){
        return "translate("+ d.x + "," + d.y + ")";
        });
  • A chaque noeud, on crée un cercle dont on veut que le rayon soit proportionnel à la présence du groupe à l'Assemblée. On veut aussi que chaque noeud soit de la couleur traditionnellement associée à ce parti.
    node.append("circle")
        .attr("r", function(d){return d.chiffre/5})
        .attr("fill", function(d,i) {return d.couleur;});
  • On demande à ce que chaque noeud soit accompagné de texte, récupéré dans le name du fichier json, décalé d'un peu sur la droite et de 20px en bas.
    node.append("text")
        .attr("transform","translate(4,20)")
        .text(function(d){return d.name;});
  • On veut que le lien soit une belle diagonale, on lui passe donc une méthode de projection pour que les coordonnées soient respectées.
  • Enfin, on sélectionne tous les éléments qui sont des liens, on leur passe les éléments qu'ils doivent relier, on crée un path pour chacun d'entre eux, de classe link, on lui donne la couleur blanche et on lui demande de tracer ce chemin en suivant les paramètres de la variable "diagonale".
    var diagonal=d3.svg.diagonal()
        .projection(function(d) {return [d.x, d.y];});
    
        canvas.selectAll(".link")
        .data(links)
        .enter()
        .append("path")
        .attr("class", "link")
        .attr("fill","none")
        .attr("stroke", "white")
        .attr("d",diagonal);
    })

    Bah les amis, quelle aventure. Prenez un petit biscuit, vous l'avez mérité.

    Et pourtant, objectivement, cette manière de présenter l'Assemblée n'est pas particulièrement efficace; l'oeil met en tous cas un peu de temps à comprendre et à tenter de comparer les différents cercles. Il lui est, de plus, impossible de comprendre les jeux de pouvoir entre les deux gros groupes et les autres, qui sont pourtant essentiels dans le jeu démocratique français.

    Bonne nouvelle, si vous avez un jeu de données que vous voulez "clusteriser", c'est-à-dire que vous voulez donner la même profondeur (ou positionnement sur l'axe des abscisses) à tous les éléments qui n'ont d'enfants (imaginons qu'on ne veuille considérer les non-inscrits ni dans la majorité ni dans l'opposition), il suffit de changer le mot tree par cluster dans d3.layout.tree( ). Attention, ici, on a également changé le fichier assemblee.json, pour faire en sorte que les non-inscrits ne figurent plus dans l'opposition.

  • Pack et bulle

    L'idée est à présent d'entourer les différents groupes au sein de groupes plus importants. On va reprendre l'exemple de l'Assemblée, ça marche pas mal. L'idée de pack est de placer les éléments d'un document de façon graphique sous forme de bulle.

    var width = 600;
    var height = 600;
    var canvas = d3.select("body")
        .append("svg")
        .attr("width", width).attr("height", height)
        .append("g")
        .attr("transform", "translate(0,0)");
    
    var pack = d3.layout.pack()
        .size([width, height])
        .padding(5);
    
    d3.json("donnees/assemblee3.json", function (data) {
        var nodes = pack.nodes(data);
        
        var node = canvas.selectAll(".node")
            .data(nodes)
            .enter()
            .append("g")
    	        .attr("class","node")
        	    .attr("transform", function (d){
        	    return "translate("+d.x+","+d.y+")";
        	    });
    
        node.append("circle")
            .attr("r", function (d) {return d.r;})
            .attr("fill", function(d){return d.couleur;})
            .attr("opacity", function(d){return  0.5;})
            .attr("stroke", "white")
            .attr("stroke-width", "2");
    
        node.append("text")
            .text(function (d) {
            return d.children ? "" : d.name;})
        	.attr("transform", "translate(-10,20)");
    });
    

    Concrètement, on passe nos données dans la méthode pack, on va chercher nos données, que l'on transforme en noeuds. Pour chaque noeud, on donne des coordonnées et pour chaque noeud, on crée un cercle auquel on donne le rayon prévu par la méthode pack. Sa couleur sera celle indiquée dans sa variable "couleur". Quant au texte, il sera placé à peu près au centre, à 20px en bas du centre du cercle.

    Pour créer un bubble chart, il faut utiliser le même type de code, mais faire en sorte que les éléments aient tous la même profondeur dans le fichier json (qu'aucun n'ait d'enfants).

    Ici, j'ai joué avec l'opacité et les couleurs des contours pour obtenir le résultat ci-dessus.

    .attr("opacity", function(d){return d.children ? 0.1 : 0.5;})
    .attr("stroke", function(d){return d.children ? "#3399FF" : "black";})

    Le tutoriel de Vienno continue ensuite sur un histogramme. De mon côté, je préfère présenter ensuite la méthode Treemap, qui est relativement proche des deux précédentes, par son code et son objectif.

    Vers la suite... Treemap, bar chart et courbes diverses