D3.js: les essentiels, partie 3

Si la première et la deuxième partie de ces pense-bête / tutoriels étaient issus des vidéos de Vienno, ceux qui suivent sont extraits de recherches diverses. Un personnage m'a néanmoins particulièrement aidé, avec une clarté remarquable: d3noob.

Dans cette troisième partie, on abordera le treemap et on verra quelques solutions pour présenter des séries temporelles, notamment les histogrammes et les courbes.

Treemap, donc

Le treemap, c'est cette méthode qui permet de créer des rectangles d'aires proportionnelles à l'importance des groupes à l'intérieur d'un ensemble. Histoire de ne pas trop se creuser, on repart avec les députés.

Autant ne pas se le cacher, ce point-ci me paraît encore particulièrement plus complexe que les autres. Pour tout dire, la version donnée par Vienno dans son tutoriel n'a pas fonctionné pour moi. Je suis donc allé cherché ailleurs. En l'occurence, j'ai fini par trouver ce fiddle qui me paraît compréhensible. Le reste est d'un niveau qui n'est pas le mien à l'heure où j'écris ces lignes.

J'espère être prochainement suffisamment plus doué pour éclaircir certains points de cette technique.

Pour commencer, il va falloir changer la façon de fonctionner avec d3.select(body). On faisait comme si l'animation était toute la page html, or elle ne concerne qu'une partie, qu'un DOM. Ici, nous créons un canvas1 et on va sélectionner le div correspondant à l'espace laissé dans notre page pour l'animation. Ici, pour des raisons diverses, je l'ai appelé result-side1a. On déclare une variable, que je vous laisse observer.

var canvas1=d3.select(".result-side1a")
var tree = {
    name: "assemblee",
    children: [
        { name: "PS", size: 285, "couleur": "pink" },
        { name: "Ecolo", size: 18,"couleur": "green" },
        { name: "RRDP", size: 17,"couleur": "yellow" },
        { name: "GDR", size: 15,"couleur": "red" },
        { name: "UMP", size: 199, "couleur": "blue" },
        { name: "UDI", size: 30, "couleur": "steelblue" },
        { name: "Non inscrits", size: 9, "couleur": "black" }
    ]
};

On définit ensuite une largeur et une hauteur ainsi qu'une variable div dont la position sera relative. On crée une variable treemap à laquelle on passe la méthode treemap à laquelle on donne une taille, une stabilité (c'est le sens de sticky: les blocs doivent-ils garder ou non leur place dans le bloc principal si on procède à une modification, comme une transition), et enfin une taille, qui est fonction de la size indiquée dans le tableau (ici, elle est calculée automatiquement par l'ajout de ses children).

var width = 560,
height = 560,
div = d3.select(".result-side1a").append("div")
    .style("position", "relative");

var treemap = d3.layout.treemap()
    .size([width, height])
    .sticky(true)
    .value(function(d) { return d.size; });

Jusqu'ici tout va bien. On crée ensuite une variable node, qui passe tous les éléments de classe "node" dans la variable tree. Comme on ne lie notre div qu'à un seul élément (en l'occurence tree), on peut utiliser div.datum, alors que si on sélectionnait l'ensemble des rectangles ou des cercles, il faudrait utiliser .data(lenomdujeudedonnees). On lui passe les noeuds de notre treemap auxquels ont passe la classe "node" et que l'on place selon la fonction position qui se trouve ci-dessous. Ensuite, parmi les noeuds, si le nom de l'élément est celui de l'élément principal, demander du blanc, sinon, afficher la couleur associée à chacun.Enfin, demander à ce que s'affiche le nom de chaque élément à une taille qui soit fonction de l'aire de chaque élément.

var node = div.datum(tree).selectAll(".node")
	.data(treemap.nodes)
    .enter()
    .append("div")
    .attr("class", "node")
    .call(position)
    .style("background-color", function(d) {
        return d.name == 'tree' ? '#fff' : d.couleur; })
    .append('div')
    .style("font-size", function(d) {
        // compute font size based on sqrt(area)
      return Math.max(20, 0.18*Math.sqrt(d.area))+'px'; })
    .text(function(d) { return d.children ? null : d.name; });
 
function position() {
  this.style("left", function(d) { return d.x + "px"; })
      .style("top", function(d) { return d.y  + "px"; })
      .style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
      .style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
};

Le résultat est assez chouette. J'espère être plus clair et pouvoir améliorer certains des aspects de ces graphiques prochainement.

Histogramme

Allez, c'est parti, l'histogramme. On a déjà réalisé un bar chart dans un exercice précédent. Ici, on va essayer d'améliorer leur look. On va progresser petit à petit, si vous le voulez bien.

On commence par du simple, par rapport à ce qu'on a fait dernièrement: on va représenter le nombre de blessés légers par an dans les trains de la SNCF, données qui sont en open data.

On commence par définir des marges, une largeur, une hauteur, ainsi que les données depuis 2008.

var margin = {right: 20, left: 30, top: 20, bottom: 20},
    width = 620 - margin.left - margin.right,
    height = 500-margin.top - margin.bottom;
var donnees = [1,19,17,5,11,16];

On crée ensuite deux échelles: une pour la taille des barres, une pour leur couleur. La première s'étale de la valeur minimum à la valeur maximum en entrée, pour sortir avec la hauteur maximale dans un cas et zéro dans le second. Quant à la couleur, on crée une variable identique, qui prendra vert pour le minimum et rouge pour le maximum.

var heightScale= d3.scale.linear()
	.domain([0, 20])
	.range([height,0]);

var colorScale=d3.scale.linear()
	.domain([0, 20])
	.range(["green", "red"]);

On crée une variable pour l'axe, qu'il y ait 5 indications (et non pas qu'elle gravisse de 5 en 5, même si ici, cela revient quasiment au même), que l'on place à gauche et qui est à l'échelle.

Au sein de notre sélection, on crée un svg, qui est de la largeur et de la heuteur déterminée avant. On fait en sorte qu'il contienne la marge droite et haute.

On crée ensuite une variable bars qui sélectionne l'ensemble des rectangles présents et à venir, à laquelle on passe les données présentes dans notre tableau. Pour chacune d'entre elle, on ajoute un rectangle, de 30px de large et écarté de 5px les uns des autres. Sa couleur sera fonction de la variable colorScale créée. On veut que sa taille soit proportionnelle au chiffre présent dans l'array, mais pour que la barre parte du haut, on a besoin de la petite subtilité entre height et y

var canvas=d3.select(".cadre")
		.append("svg")
		.attr("width",width).attr("height", 
		height+ margin.top + margin.bottom)
		.append("g") 
			.attr("transform","translate("
			+ margin.left + "," + margin.top + ")");
var bars=canvas.selectAll("rect")
	.data(donnees)
	.enter()
		.append("rect")
			.attr("transform", "translate(10,0)")
			.attr("height", function(d){
			return height-heightScale(d);
			})
			.attr("width", 30)
			.attr("x", function(d,i){return i*35;})
			.attr("y", function(d){ return heightScale(d) ;}  )
			.attr("fill", function (i) {return colorScale (i);})

Enfin, on y joint un axe, que l'on veut blanc. Pour cet axe, on joint un texte, légèrement décalé sur la droite.

canvas.append("g")
	  .attr("fill","white")
	  .call(axis)
			.append("text")
			.attr("transform", "translate(30,0)")
			.style("text-anchor", "start") 
			.attr("fill","white")
			.text("Blessés légers dans un train / an");

Tout ceci est très bien, m'enfin, ça n'est quand même pas très très présentable. Et surtout, pas très pratique. On voudrait récupérer des données csv, par exemple, et les traiter facilement...

On fait appel ici à l'excellent d3noob, qui semble s'appeler en réalité Malcolm Maclean. Je vous encourage à découvrir son d3 Tips & Tricks, qui est toujours bon à potasser (ce que je n'ai pas encore assez fait). Bien. La particularité ici va être qu'on va importer un csv.

var margin = {right: 20, left: 30, top: 60, bottom: 60},
    width = 620 - margin.left - margin.right,
    height = 500+margin.top + margin.bottom;


var	parseDate = d3.time.format("%Y").parse; 

var x = d3.scale.ordinal().rangeRoundBands([0, width], .8);
var y = d3.scale.linear().range([height, 0]);

var xAxis = d3.svg.axis() 
	.scale(x)
	.orient("bottom") 
	.tickFormat(d3.time.format("%Y"));
var yAxis = d3.svg.axis() 
	.scale(y)	
	.orient("left") 
	.ticks(10);

Le début est similaire. On crée une fonction parseDate qui passe l'année de notre tableau. La variable x permet de discrétiser la position des x, et y va permettre de placer les légendes de l'axe vertical. On crée ensuite notre deux axes, où l'on indique pour les abscisses que notre variables seront des années et pour les ordonnées, que l'on voudrait qu'il y ait 10 échelons.

var svg = d3.select(".result-side3")
	.append("svg") 
	.attr("width", width + margin.left + margin.right) 
	.attr("height", height + margin.top + margin.bottom)
		.append("g") 
		.attr("transform","translate("
		+ margin.left + "," + margin.top + ")");

On est ici exactement dans la même situation que plus haut, cela ne devrait plus vous poser de problème.

Attention, ensuite, on ouvre un tsv pour tabulated separated value, que l'on vérifie à l'aide de cette fonction: pour chaque élément, on transforme en année l'élément qui se trouve dans la colonne annee et pour chaque élement de la colonne nombre_accident, on le transforme en nombre.

d3.tsv("donnees/accidents-passagers.tsv", 
function(error, data) {
	data.forEach(function(d) { 
		d.annee = parseDate(d.annee); 
		d.nombre_accident =+ parseInt(d.nombre_accident);
	}); 

On crée ensuite un domain pour x et pour y, qui fasse en sorte de retourner les éléments qui se trouvent dans le nouvel élément data. On appelle ensuite sur le graphique l'axe des abscisses, que l'on place en bas du graph, et en dessous duquel, on indique les noms de chaque ligne, légèrement anglé. Idem pour l'axe des ordonnées. Et pour le fun, je vous montre comment rajouter un titre. Sympa.

x.domain(data.map(function(d) {
	return d.annee; 
}));
y.domain([0, d3.max(data, function(d) {
	return d.nombre_accident; 
})]);

svg.append("g") 
	.attr("class", "x axis") 
	.attr("transform", "translate(-5," + height +")") 
	.call(xAxis)
  .selectAll("text") 
  	.style("text-anchor", "end") 
  	.attr("dx", "-1.1em") 
  	.attr("dy", "-.05em") 
  	.attr("transform", "translate(-5,20)") 
  	.attr("transform", "rotate(-30)" );
  	
svg.append("g") 
	.attr("class", "y axis") 
	.call(yAxis)
  .append("text") 
  	.attr("transform", "rotate(-90)") 
  	.attr("y", 6)
  	.attr("dy", ".71em") 
  	.style("text-anchor", "end") 
  	.text("Nombre de blesses par an");
  	
svg.append("text") 
	.attr("x", (width / 2)) 
	.attr("y", 40 - (margin.top)) 
	.attr("text-anchor", "middle") 
	.style("font-size", "22px") 
	.style("text-decoration", "underline") 
	.attr("fill","white")
	.text("L'évolution du nombre de blessés légers dans les trains SNCF");

Et enfin, on sélectionne toutes les barres, on leur passe les valeurs contenues dans data et on leur attribue un rectangle, qui va s'étaler selon l'échelle définie pour x et qui va prendre pour hauteur la valeur du nombre d'accidents, avec toujours la même petite astuce, lorsque l'on veut partir du bas pour monter...

svg.selectAll("bar") 
	.data(data)
  .enter().append("rect") 
  	.style("fill", "#3399FF") 
  	.attr("x", function(d) { return x(d.annee); }) 
  	.attr("width", 30) 
  	.attr("y", function(d) { return y(d.nombre_accident); }) 
  	.attr("height", function(d) {
  	return height - y(d.nombre_accident);
  	});
});

Voila qui commence à ressembler franchement à quelque chose, non? Bon, mais il faut également voir que l'on serait mieux à représenter ces données sous forme de courbe. Courage, c'est bientôt fini.

Ici, on ne change que peu de choses.

var margin = {top: 30, right: 20, bottom: 30, left: 50},
    width = 600 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.linear()
	.domain(["2008","2013"])
	.range([0, width]);
var y = d3.scale.linear()
	.domain([0,20])
	.range([height,0]);

var xAxis = d3.svg.axis().scale(x)
    .orient("bottom").ticks(5);

var yAxis = d3.svg.axis().scale(y)
    .orient("left").ticks(5);

var valueline = d3.svg.line()
    .x(function(d) { return x(d.annee); })
    .y(function(d) { return y(d.nombre_accident); });
    
var canvas = d3.select("encadre")
    .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
    .append("g")
        .attr("transform", "translate(" + margin.left
        + "," + margin.top + ")");

// On importe les données
d3.tsv("donnees/accidents-passagers.tsv", function(error, data) {
    data.forEach(function(d) {
		d.annee =+ parseInt(d.annee); 
		d.nombre_accident =+ parseInt(d.nombre_accident);
    });

    // On crée l'échelle
    x.domain(d3.extent(data, function(d) { return d.annee; }));
    y.domain([0, d3.max(data, function(d) { return d.nombre_accident; })]);

    canvas.append("path")      // On appelle ici la fonction valueline,
    qui donne des coordonnées à relier à la ligne.
        .data(data)
        .attr("d", valueline(data));

    canvas.append("g")         // Add the X Axis
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    canvas.append("g")         // Add the Y Axis
        .attr("class", "y axis")
        .call(yAxis);
});

Ce qui donne un résultat comme celui-ci...

Mais bon, comme on est vraiment DINGUE, ce dont on a envie, c'est que la courbe soit plus élégante, quoi. Plus lisse, plus smooth, carrément Philippe Starck, quoi. Et bien une ligne suffit: .interpolate("basis"), qu'il faut rajouter dans la variable qu'on a appelé valueline

On peut trouver d'autres "interpolations" ici. Le nombre de blessés n'est pas tant une tendance qu'un chiffre absolu. On peut donc tester d'autres styles à appliquer sur un path, comme celui-ci: .interpolate("step-after").

Et bien nous voici tout de même pas mal avancés, par rapport à notre premier pense-bête. On essaiera dans les prochains tutoriels de continuer notre liste de jolis petits graphiques toujours bons à avoir sous la main...

Merci et à bientôt.

Victor