Autopsie d'une dataviz [8.3] : coder une carte choroplèthe avec D3js

Troisième et ultime volet d'un tutoriel qui décrit la marche à suivre pour coder une carte avec la librairie D3js. Passage en revue du code.

Il est temps, après avoir préparé un geojson complet et léger et s'être posé quelques questions technique sur la future carte, de passer au Javascript.

Pour ceux qui auraient eu des soucis lors de la première partie et souhaitent quand même faire ce volet, ils pourront trouver le geojson correspond sur ce répertoire GitHub.

Comment fonctionne D3 ?

D3, pour Data-Driven Documents, est une librairie Javascript initiée par Mike Bostock et qui connaît un important succès public et critique, notamment dans le petit milieu du journalisme de données.

Elle a l'énorme avantage d'être très exhaustive, permettant de coder à la fois des histogrammes ou des cartes.

D'un autre côté, elle demande un certain bagage en code, et surtout est parfois aux fraises sur support mobile, ce qui peut constituer un handicap alors que des lib plus optimales comme Highcharts ou Leaflet sont dans les parages.

Techniquement, D3 permet de générer des formes vectorielles au format SVG, qui peut facilement être stylisé avec du CSS. On peut donc directement coder une ou plusieurs classes et ensuite les affecter aux éléments SVGqui nous intéressent.

Côté script, on trouve en général :

  • l'ossature de notre visualisation en variables : axes des ordonnées et des abscisses pour les graphes, projections pour les cartes, sélection des div qui vont accueillir les futurs formes vectorielles
  • une fonction qui va animer tout ça à partir d'un ou de plusieurs fichiers de données

Les premiers bouts de code

On va commencer petit à petit en créant un fichier HTML à côté du fichier geojson et du petit plugin d3.tip.js (présent dans le répertoire GitHub précédemment cité) qui nous permettra de construire de belles infobulles interactives.

La structure de base

On va dans cette première partie :

  • appeler la librairie
  • créer des balises style, body et script qui contiendront respectivement notre CSS, nos éléments HTML de base et le Javascript qui va permettre d'afficher la carte

    <!DOCTYPE html>

    C'est dans le nord de la région que le FN a fait ses meilleurs scores en 1995

    Source : Data.gouv.fr

Le CSS

Le code CSS suivant va nous permettre, une fois inséré dans la balise style, de bien agencer les éléments statiques présents juste avant la balise script, mais pas que.

On va notamment créer une classe "communes", dont les membres s'afficheront d'une couleur donnée au survol de la souris. On se laissera quelques affectations pour la fonction Javascript, par exemple les différentes teintes que l'on appliquera aux zones.

.communes {
  fill:#000;
}

.communes:hover {  
  fill:#331515;
}
.t_commune{
  font-size: 13px;
  font-weight: bold;
}
.d3-tip {
  line-height: 1.5;
  font-weight: normal;
  font-family: Arial;
  padding: 5px;
  background: rgba(125, 125, 125, 0.9);
  color: white;
  border-radius: 4px;
  font-size: 11px;
  margin-left: 25px;
  margin-top: 205px;
}
#container{
  width:500px;
  height:1200px;
  position: absolute;
}
#carte{
  width:500px;
  height: 950px;
  margin-top:-30px;
  position: absolute;
}
#legende{
    margin-top:900px;
    position:absolute;
    width: 500px;
}
#source{
  position: absolute;
  margin-top: 1020px;
  width: 500px;
margin-left: 30px;
font-family: Arial;
font-size: 0.8em;
}
h1{
  font-family: Arial;
  font-size: 1.2em;
  width: 500px;
  margin-left: auto;
  text-align:center;
}

On a également soigné la future infobulle (d3-tip) qui s'affichera au survol de chaque commune. On s'attardera notamment sur les attributs margin-top et margin-left, qui permettront d'afficher cette infobulle en-dessous à droite du curseur.

Par défaut, elle risquerait de déborder hors du cadre pour les communes périphériques. Evidemment, cela est facilement adaptable.

Si vous affichez le fichier .html à ce stade, vous aurez simplement la titre et la source. Il est temps de remplir le vide avec notre script !

Le script

Les variables

Côté variables, nous avons besoin de :

  • de deux duos largeur/hauteur, correspondant à la carte et à la légende
  • d'une projection cartographique. Attention à cette étape : il faut impérativement que la projection corresponde précisément à celle dans laquelle est paramétrée le geojson. Si vous avez bien suivi le premier tuto, il s'agissait de la WSG 84, qui correspond à la projection Mercator. La liste des projections disponibles avec D3 est disponible ici. Il faut également définir l'échelle (=niveau de zoom, se fait au doigt mouillé) et le centre de la carte (par défaut, c'est en plein milieu de l'Atlantique). Pour viser ce centre, j'utilise cet exemple de Mapbox (attention, la déclaration des coordonnées est inversée dans D3). Enfin, il faut effectuer une translation de cette projection pour bien caler le centre au pixel près
  • d'un tableau rempli de couleurs hexadécimales. Je vous le donne en mille : il correspond aux futures teintes de notre prochaine choroplèthe !
  • d'un "générateur de traits" qui affichera convenablement les coordonnées contenues dans le geojson. Ce générateur prend en considération la projection définie avant, il faut donc bien veiller à utiliser la même proj' que le fichier qui contient les données. Sinon, le résultat ressemblera à du yaourt
  • de deux éléments svg, qui s'appliqueront sur les div "carte" et "legende"

Côté code, ça se traduit comme ça :

var width = 500,
    height = 950;
var width3=500,
    height3=150;

var proj = d3.geo.mercator()
  .center([7.56, 48.27])
    .scale(20000)
    .translate([width / 2, height / 2]);

var couleurs = ['#000000','#252525','#525252','#737373','#969696','#bdbdbd','#d9d9d9','#f0f0f0','#ffffff'];
var path = d3.geo.path()
    .projection(proj);

var svg = d3.select("#carte").append("svg")
    .attr("width", width)
    .attr("height", height);

var svg3 = d3.select("#legende").append("svg")
    .attr("width", width3)
    .attr("height", height3);

La fonction

Côté variables, on est bon, reste plus qu'à définir notre fonction. Dans celle-là, on utilise qu'un fichier json, on peut donc se rabattre sur l'option d3.json qui prend en argument le fameux fichier et la fonction qui affiche tout ça.

On va entamer les festivités en déclarant comme variable notre infobulle interactive. On lui appliquera la classe d3-tip et on ajoutera une fonction qui définit le contenu de ladite infobulle.

Rien de bien sorcier, on déclare en variables locales les données qu'on veut voir s'afficher (à l'aide du nom de colonnes) et on retourne ce contenu en HTML.

En code, le début de notre fonction, située juste après les variables, se traduit ainsi :

d3.json ("fn_1995_communes.json",function (error,fn) {

  var tip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(function(d,i) {
    nom_commune = fn.features[i].properties.COMMUNE
    voix_fn = fn.features[i].properties.voix_fn
    fn_exp=fn.features[i].properties.fn_p_exp
    inscrits = fn.features[i].properties.inscrits
    abs_ins=fn.features[i].properties.abs_ins
    return "<span class='t_commune'>"+nom_commune+"</span><br/>En 1995<hr/><u>Inscrits</u> : "+inscrits+"</br></br><u>Abstention</u> : <b>"+abs_ins+"%</b></br></br> <u>FN</u> : <b>"+fn_exp+"%</b>,</br> soit <b> "+voix_fn+" électeurs</b> "
      })
      svg.call(tip);


});

Le "fn" en attribut de la fonction nous permet d'appeler les propriétés associées. du geojson.

Par exemple, fn.features[0].properties.COMMUNE désigne le contenu de la propriété "COMMUNE" de la première ligne de notre fichier (autrement dit : "Still").

Rien de révolutionnaire côté fichier final, il est peut-être temps d'afficher nos différentes communes. On va sélectionner notre variable svg, lui appliquer la classe "communes", et quelques autres propriétés CSS à la volée.

Pour cela, on écrit juste en-dessous du svg.call(tip) précédent :

svg.append("g").attr("class","villes") // append("g") désigne un groupe et le nom de la classe n'a aucune importance
    .selectAll("path")
    .data(fn.features)
    .enter()
      .append("path")
      .attr("class", "communes") // là en revanche, le nom de classe compte
      .attr("d", path)
      .attr("stroke","#333333")
      .attr('stroke-width', '0.5');

Si le plan s'est déroulé sans accroc, les communes doivent s'afficher en noir et changer de couleur au survol :

Tout cela est bel et bon, mais on veut une choroplèthe, donc des communes de différentes teintes.

On va commencer par virer du CSS précédent ce qui concerne la classe "communes", et ajouter une fonction d'attribution de couleurs.

Celle-ci fonctionnera avec des boucles if/else if qui affecteront les bonnes teintes du tableau couleurs aux bons seuils. Concrètement, ça se traduit ainsi :

svg.append("g").attr("class","villes")
    .selectAll("path")
    .data(fn.features)
    .enter()
      .append("path")
      .attr("class", "communes")
      .attr("d", path)
      .attr("stroke","#333333")
      .attr('stroke-width', '0.5')
      .attr("fill", function(d,i){ // on applique les teintes aux zones avec une fonctione if/else qui distribue les valeurs du tableau précédent en fonction de seuils
          if (fn.features[i].properties.fn_p_exp>32.17){
        return couleurs[0]
      }
      else if (fn.features[i].properties.fn_p_exp>29.85) {return couleurs[1]
      }
      else if (fn.features[i].properties.fn_p_exp>28.14) {return couleurs[2]
      }
      else if (fn.features[i].properties.fn_p_exp>26.36) {return couleurs[3]
      }
      else if (fn.features[i].properties.fn_p_exp>24.92) {return couleurs[4]
      }
      else if (fn.features[i].properties.fn_p_exp>23.49) {return couleurs[5]
      }
      else if (fn.features[i].properties.fn_p_exp>21.92) {return couleurs[6]
      }
      else if (fn.features[i].properties.fn_p_exp>19.45) {return couleurs[7]
      }
      else {return couleurs[8]}
    });

Si vous avez bien viré la classe "communes" (juste la classe, pas le :hover), vous devriez voir s'afficher la carte suivante :

Il ne nous reste plus qu'à afficher/cacher l'infobulle quand la souris est sur une zone et quand elle n'est plus dessus.

Ce qui nous donne, en reprenant tout depuis le début, ceci :

svg.append("g").attr("class","villes") // le nom de cette classe n'a pas d'importance
    .selectAll("path")
    .data(fn.features)
    .enter()
      .append("path")
      .attr("class", "communes")
      .attr("d", path)
      .attr("stroke","#333333")
      .attr('stroke-width', '0.5')
      .attr("fill", function(d,i){ // on applique les teintes aux zones avec une fonctione if/else qui distribue les valeurs du tableau précédent en fonction de seuils
          if (fn.features[i].properties.fn_p_exp>32.17){
        return couleurs[0]
      }
      else if (fn.features[i].properties.fn_p_exp>29.85) {return couleurs[1]
      }
      else if (fn.features[i].properties.fn_p_exp>28.14) {return couleurs[2]
      }
      else if (fn.features[i].properties.fn_p_exp>26.36) {return couleurs[3]
      }
      else if (fn.features[i].properties.fn_p_exp>24.92) {return couleurs[4]
      }
      else if (fn.features[i].properties.fn_p_exp>23.49) {return couleurs[5]
      }
      else if (fn.features[i].properties.fn_p_exp>21.92) {return couleurs[6]
      }
      else if (fn.features[i].properties.fn_p_exp>19.45) {return couleurs[7]
      }
      else {return couleurs[8]}
    })
    .on('mouseover', function(d, i) {
      tip.show(d,i);
    })
    .on('mouseout',  function(d, i) {
      tip.hide(d,i);
    });

Pour la légende, j'ai bricolé en m'inspirant d'un exemple des Décodeurs du Monde : en gros, on définit à chaque fois une ligne de carrés qui se remplissent de teintes, puis on ajoute en-dessous le texte associé, comme ceci :

svg3.append("g")
      .append("text")
      .attr("x", 30)
      .attr("y", 22)
      .attr("font-size", 11)
      .attr("font-family","Arial")
      .text("En avril 1995, le FN enregistrait dans cette ville :")
    svg3.append("g").attr("class", "legende")
      .selectAll("rect")
      .data(["Plus de 32%", "30% à 32%", "28% à 30%","26% à 28%","25% à 26%"])
      .enter()
      .append("rect")
      .attr("stroke","#000")
      .attr('stroke-width', '1')
      .attr("x", function(d,i){
      return 30+i*83
      })
      .attr("y", 40)
      .attr("fill", function(d){
    if (d == "Plus de 32%"){
        return couleurs[0]
      }
      else if (d == "30% à 32%"){
       return couleurs[1]
      }
      else if (d == "28% à 30%"){
       return couleurs[2]
     } else if (d == "26% à 28%"){
       return couleurs[3]
     }
      else if (d == "25% à 26%"){
       return couleurs[4]
     }
      })
      .attr("width", 50)
      .attr("height", 12)
    svg3.append("g").attr("class", "legende")
      .selectAll("text")
      .data(["Plus de 32%", "30% à 32%", "28% à 30%","26% à 28%","25% à 26%"])
      .enter()
      .append("text")
      .attr("x", function(d,i){
      return 27+i*85
      })
      .attr("y", 69)
      .attr("font-family","Arial")
      .attr("font-size", 10.1)
      .text(function(d){return d})
      svg3.append("g").attr("class", "legende")
      .selectAll("rect")
      .data(["23% à 25%", "22% à 23%", "19% à 22%","Moins de 19%"])
      .enter()
      .append("rect")
      .attr("stroke","#000")
      .attr('stroke-width', '1')
      .attr("x", function(d,i){
      return 30+i*83
      })
      .attr("y", 80)
      .attr("fill", function(d){
    if (d == "23% à 25%"){
        return couleurs[5]
      }
      else if (d == "22% à 23%"){
       return couleurs[6]
      }
      else if (d == "19% à 22%"){
       return couleurs[7]
     } else if (d == "Moins de 19%"){
       return couleurs[8]
     }
      })
      .attr("width", 50)
      .attr("height", 12)
    svg3.append("g").attr("class", "legende")
      .selectAll("text")
      .data(["23% à 25%", "22% à 23%", "19% à 22%","Moins de 19%"])
      .enter()
      .append("text")
      .attr("x", function(d,i){
      return 27+i*84
      })
      .attr("y", 109)
      .attr("font-family","Arial")
      .attr("font-size", 10.1)
      .text(function(d){return d})

Ca ne paraît pas comme ça, mais c'est assez facilement réutilisable !

Plus qu'à mettre en ligne notre fichier html pour bricoler une iframe et le tour est joué :-) !

Par Raphaël da Silva dans la catégorie
Tags : #d3, #carte, #javascript,