D3.js:
Analyse des correspondances multiples interactive.

Les sociologues, notamment français, ont avec l'analyse de correspondances multiples un rapport assez affectueux. Ils sont les derniers ou presque à l'utiliser et/ou à la comprendre, en tous cas en ont-ils l'impression. C'est à l'appui d'un exemple pourtant inintéressant sociologiquement que je vais procéder à l'explication de cette visualisation.

Les données utilisées ici ont été choisies parce que natives de R; elles n'ont aucun intérêt sociologique telles qu'elles sont représentées ici.

ACM avec d3

Tout a commencé ici avec la volonté de mettre en avant les qualités d'un package de R, développé par Joël Gombin. R possède néanmoins des inconvénients, notamment celui de ne pas se prêter très facilement à l'interactivité sur internet. Le package en question ne dérogeant pas à la règle, je me suis servi des résultats obtenus lors de cet exemple pour souligner l'intérêt de proposer une interface web, via d3.js.

Pour ceux qui ne connaissent pas ce langage, vous trouverez quelques rapides rudiments ici-même et sur de nombreux autres sites.

Ce qu'il faut savoir faire

Avant tout, il faut savoir faire une ACM/ACP/AFC avec R. Pour exporter les résultats de cette analyse, on va passer par un petit script à la fin de notre programme en R: on veut exporter les coordonnées des individus, celles des variables et celles des variables supplémentaires.

/*Attention, on est ici dans R */
indiv<-USAcrime$ind$coord
varia<-USAcrime$var$coord
vasup<-USAcrime$quali.sup$coord

write.csv(indiv,file="USAindiv.csv",row.names = TRUE)
write.csv(varia,file="USAvaria.csv",row.names = TRUE)
write.csv(vasup,file="USAvasup.csv",row.names = TRUE)

Pour plus de simplicité dans les explications de ce code, je suis ensuite allé modifier à la main le titre de la première colonne de chaque fichier ainsi que transformer le "Dim 1" et "Dim 2" créés par R en "Dim1" et "Dim2". Une fois cette toute petite opération terminée, je vous recommande de changer le nom de varia.csv et de vasup.csv en varia.tsv et vasup.tsv.

On commence donc par créer un cadre, à l'intérieur duquel placer la visualisation. On détermine aussi la hauteur, la largeur et les marges de celle-ci.

var margin = {right: 10, left: 10, top: 50, bottom: 50},
    width = 620 - margin.left - margin.right,
    height = 500+margin.top + margin.bottom;

On crée ensuite des échelles: l'une pour l'axe des abscisses et l'autre pour l'axe des ordonnées. Ici, j'ai réglé leur domain à la main. Le rangeRound permet d'arrondir les valeurs obtenues pour les coordonnées des points. On dit ici l'équivalent de "je veux que le coefficient x qui servira pour créer l'échelle des abscisses fasse en sorte que 1,5 soit la valeur minimale de mon axe, et qu'elle parte à 0 sur mon graphique, et que 3,2 soit la valeur maximale et qu'elle soit représentée au point extrême de la largeur de mon graphique". Idem pour la hauteur, si ce n'est l'astuce du "haut et bas inversés". On crée ensuite l'axe xAxis et yAxis.

var x = d3.scale.linear()
	.domain([-1.5,3.2])
	.rangeRound([0,width])
	
var y = d3.scale.linear()
	.domain([2,-1.5])
	.rangeRound([0,height]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")  
    .tickSize(1);

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .tickSize(1);

Il s'agit ensuite d'appeler sur le graphique ces deux axes ainsi que les deux véritables axes des abscisses et ordonnées: on crée en effet un axe en bas et un à gauche, mais les ACM utilisent des axes où x=0 et y=0. Enfin, on indique un titre.

var canvas = d3.select(".result-side1")
	.append("svg") 
	.attr("width", width+20) 
	.attr("height", height+20)
	.append('g')
    .attr('transform', 'translate(30,30)');
    
canvas.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + (height-30) + ")")
    .call(xAxis);
canvas.append("g")
    .attr("class", "y axis")
    .call(yAxis);
    
canvas.append("g")
	.attr("class", "axis zeroX")
	.append("line")
		.attr({
		x1: x(-1.5),y1: y(0),
		x2: x(2.5),y2: y(0),
		stroke: 'white', 
		'stroke-width': '0.5'});
canvas.append("g")
	.attr("class", "axis zeroY")
	.append("line")
		.attr({
		x1: x(0),y1: y(2),
		x2: x(0),y2: y(-1.3),
		stroke: 'white', 
		'stroke-width': '0.5'});
		
canvas.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("Les Etats classés selon leur profil");

On crée ensuite les "individus" (c'est-à-dire les éléments, ici, nos individus sont des Etats américains). On les ajoute à notre objet, qu'on a appelé canvas et on leur attribue une classe. Lorsque l'on cliquera sur le bouton "afficher le nuage de points", on déclenchera la fonction affichNuageInd, qui diminuera la visibilité des éléments des variables et des variables supplémentaires.

On va ensuite chercher au sein d'un fichier exporté depuis R les coordonnées des points.

var individus = canvas.append("g").attr("class","ind");  
/*Individus*/
function affichNuageInd(){
canvas.selectAll(".textvariable")
  	.style("opacity", 0.4);
canvas.selectAll(".textvasup")
  	.style("opacity", 0.4);

d3.csv("donnees/USAindiv.csv", function(error, data)
{
 //Add data to the graph and call enter.
individus.selectAll(".circleind")
	.data(data)
	.enter()
	.append("circle")
	.attr("class", "circleind")
	.attr("cx", function(d,i){return x(d.Dim1);})
    .attr("cy", function(d,i){return y(d.Dim2);})
    .attr("r", 2)
    .attr("fill","#3399FF")
    .style("opacity", 1);
}
);
}

Il s'agit ensuite de permettre l'affichage des noms des individus. Ici, on en propose deux versions: la première est la plus simple: il s'agit d'afficher le nom des individus juste à côté du point correspondant à ses coordonnées.

function affichIndSimple(){
d3.csv("donnees/USAindiv.csv", function(error, data)
{
individus.selectAll(".textind")
	.data(data)
	.enter()
	.append("text")
	.attr("class", "textind")
	.attr("x", function(d,i){return x(d.Dim1);})
    .attr("y", function(d,i){return y(d.Dim2);})
    .attr("fill","#3399FF")
    .style("opacity", 1.0)
    .text(function(d,i){return d.State ;})
    .style("font-size", "14px") 
    .attr("text-anchor", "middle");
 });
}

L'autre solution est beaucoup plus complexe, car elle calcule les bonnes coordonnées des textes pour faire en sorte qu'aucun ne soit au-dessus d'un autre. Ce qu'on augmente en lisibilité, on le perd en vitesse d'affichage. A l'utilisateur de choisir. Cela se joute dans une fonction, que l'on a appelé arrangeLabelsInd().

function affichIndJoli(){
canvas.selectAll(".textvariable")
  	.style("opacity", 0.6);
canvas.selectAll(".text.vasup")
  	.style("opacity", 0.6);

d3.csv("donnees/USAindiv.csv", function(error, data)
{
individus.selectAll(".textind")
	.data(data)
	.enter()
	.append("text")
	.attr("class", "textind")
	.attr("x", function(d,i){return x(d.Dim1);})
    .attr("y", function(d,i){return y(d.Dim2);})
    .attr("fill","#3399FF")
    .style("opacity", 1.0)
    .text(function(d,i){return d.State ;})
    .style("font-size", "14px") 
    .attr("text-anchor", "middle");
arrangeLabelsInd();
 });
 }

Ensuite, on répète la procédure pour les variables et les variables supplémentaires. Vous devriez normalement pouvoir comprendre ce qui se déroule, puisqu'on a fait le plus dur avec les individus. Petite subtilité cependant, le passage en tsv plutôt qu'en csv comme format de document.

/*Variables*/	
function affichVar(){
canvas.selectAll(".textind")
  	.style("opacity", 0.6);
canvas.selectAll(".textvasup")
  	.style("opacity", 0.6);

d3.tsv("donnees/USAvaria.tsv",
function(error1, datavaria)
{
 //Add data to the graph and call enter.
canvas.append("g").attr("class","var").selectAll(".circlevariable")
	.data(datavaria)
	.enter()
	.append("circle")
	.attr("class","circlevariable")
	.attr("cx", function(d,i){return x(d.Dim1);})
    .attr("cy", function(d,i){return y(d.Dim2);})
    .attr("r", 2)
    .attr("fill","#B9CC14");
canvas.append("g").selectAll(".textvariable")
	.data(datavaria)
	.enter()
	.append("text")
 	.attr("class","textvariable")
 	.attr("x", function(d,i){return x(d.Dim1);})
    .attr("y", function(d,i){return y(d.Dim2);})
    .attr("fill","#B9CC14")
    .attr("opacity", 1.0)
    .style("font-size", "14px") 
    .text(function(d,i){return d.Variables});
arrangeLabelsVar();
})
}

/*Variables supplémentaires*/
function affichVarSupp(){
canvas.selectAll(".textind")
  	.style("opacity", 0.6);

canvas.selectAll(".textvariable")
  	.style("opacity", 0.6);
var variablessupp = canvas.append("g").attr("class","varsup");
d3.tsv("donnees/USAvasup.tsv", function(error, datavasup)
{
 //Add data to the graph and call enter.
 variablessupp.selectAll(".circlevasup")
	.data(datavasup)
	.enter().
	append("circle")
	.attr("class","circlevasup")
	.attr("cx", function(d,i){return x(d.Dim1);})
    .attr("cy", function(d,i){return y(d.Dim2);})
    .attr("r", 2)
    .attr("fill","#FF584C");
variablessupp.selectAll(".textvasup")
	.data(datavasup)
	.enter()
	.append("text")
	.attr("class", "textvasup")
	.attr("x", function(d,i){return x(d.Dim1);})
    .attr("y", function(d,i){return y(d.Dim2);})
    .attr("fill","#FF584C")
    .attr("opacity", 0.7)
    .style("font-size", "14px") 
    .text(function(d,i){return d.Qualisup});

})
}

Enfin, on crée les fonctions qui permettent d'enlever les différents éléments de l'ACM. d3 sait manipuler les boutons également, mais j'ai préféré procéder ici en javascript.

function removeTextVar(){
canvas.selectAll(".textvariable").remove()
}
function removeTextVarSupp(){
canvas.selectAll(".textvasup").remove()
}
function removeInd(){
canvas.selectAll(".textind").remove()
}
function removeNuageInd(){
canvas.selectAll(".circleind").remove()
}

Ce qui nous donne...

VariablesVariables supplémentairesNuages de pointsIndividus

Evidemment, libre à chacun de s'amuser avec les opacités, les transitions , les couleurs, les échelles et les zooms. L'idée était ici de présenter une possibilité de sortir R de son cocon.

Parmi les éléments qui m'ont permis de réaliser cet exercice, je tiens à remercier:


Ce code n'est libre de droit qu'à condition de m'en avertir au préalable. Pour toute question, n'hésitez pas à me contacter sur Twitter, par exemple. @humeursdevictor

A bientôt, Victor