Cette cartographie présente les données de l'institut Gallup pour l'année 2018, un classement des pays selon le sentiment de sécurité de leurs habitants. Ce classement se matérialise par un score compris entre 0 et 100 pour chaque pays. Plus de 148 000 adultes à travers 142 pays ont été interrogés pour ce classement, voici les questions qu'on leur a posé :
La couleur du Vénézuela est la même que celle du Groëland alors qu'il n'y a aucune donnée dans le rapport pour le Groëland, il n'est ainsi pas possible de différencier les pays dont le score est le plus faible avec les pays pour lesquels l'enquête n'a pas été réalisée. Nous avons résolu ce problème avec une couleur spécifique (le gris) pour ces pays et nous le mentionnons dans la légende.
Le rapport mentionné plus haut contient les données que nous avons utilisées, on s'est contenté de les copier dans un fichier CSV. Les problèmes arrivent quand on veut associer le score d'un pays pour colorier le pays en question sur la carte. Le fichier geoJSON utilisé contient en plus des polygones le nom du pays et le code ISO à deux lettres de ce pays. Mais la dénomination d'un pays varie en fonction des usages, ainsi le rapport mentionne le pays Congo (Brazaville) alors que le nom officiel est Republic of the Congo et que notre fichier geoJSON utilise bien cette dénomination. Nous avons donc dû corriger notre fichier CSV à chaque fois qu'un pays n'était pas retrouvé. Autre exemple, le Kosovo n'a pas de code ISO officiel et son statut est contesté, il n'est pas présent dans le fichier geoJSON. Le dernier exemple concerne la Côte d'Ivoire dont le nom anglais est le même qu'en français, le rapport fait l'erreur de le traduire en Ivory Coast. Vous l'aurez compris, lorsqu'on assemble des données de différentes sources, il y a une phase de nettoyage qui peut prendre un peu de temps. Enfin n'oubliez pas que si vous rassemblez des fichiers de différentes sources il faut s'assurer que l'encodage soit le même et de préférence UTF-8 sinon vous aurez quelques surprises.
On ne va pas vous mentir, nous sommes assez fier du travail réalisé sur cette visualisation concernant les couleurs. L'ensemble est cohérent que ce soit pour les dégradés, le texte et le background.
En fait on a rien inventé, on a simplement regardé ce que font les autres et en particulier
ce lien. Toute ressemblance étant parfaitement volontaire.
Il nous a fallut comprendre comment ils avaient procédé pour que le résultat soit agréable à l'œil et en fait c'est plus simple qu'il n'y parait. Tout commence par une couleur de départ et nous
avons choisi le vert de la première page du rapport Gallup : #77BE4E
en hexadécimal. Ensuite on utilise le site
www.w3schools.com pour obtenir un dégradé de couleurs. On supprime les trois premières et les trois dernières
pour ne pas aller dans les extrêmes :
Cet autre site nous donne la couleur complémentaire de la nôtre : #944EBE. C'est celle que nous utilisons lorsqu'on passe la souris au-dessus d'un pays. Poursuivons avec le background du svg et avec le titre ainsi que d'autres éléments de textes dans la cartographie. Pour trouver une couleur en rapport avec la nôtre il suffit d'utiliser google et de recherche la couleur :
Le rond blanc représente la couleur recherchée dans Google. En rouge on trouve la zone très claire ou on a sélectionné la couleur utilisée pour le texte, une couleur pas très loin du blanc mais qui conserve un peu de vert. Même chose pour le background qui se rapproche du noir et se trouve dans la zone entourée en bleu. Tous ces détails permettent d'obtenir une certaine cohésion de l'ensemble, en somme ces couleurs se marient très bien entre elles. Et comme nous venons de le voir il n'est pas si difficile d'arriver à un tel résultat.
Il n'y a rien de vraiment nouveau dans le code javascript par rapport aux précédents tutoriels (principalement Carte choroplète et Lignes avancées (Linear Chart)). Néanmoins nous allons étudier quelques points intéressant à commencer par la légende. Le code de départ est assez simple, on définit quelques constantes et on crée le SVG.
const width = document.getElementById("container").offsetWidth * 0.95,
height = 550,
legendCellSize = 20,
colors = ['#d4eac7', '#c6e3b5', '#b7dda2', '#a9d68f', '#9bcf7d', '#8cc86a', '#7ec157', '#77be4e', '#70ba45', '#65a83e', '#599537', '#4e8230', '#437029', '#385d22', '#2d4a1c', '#223815'];
const svg = d3.select('#chart').append("svg")
.attr("id", "svg")
.attr("width", width)
.attr("height", height)
.attr("class", "svg";
La largeur de notre SVG est déterminée dynamiquement à partir d'un DIV contenant tout le tutoriel, le reste est fixe. On retrouve d'ailleurs notre tableau contenant la palette de couleurs dont
nous parlons dans la section précédente. Le SVG est ajouté au DIV map
déjà présent dans la page.
Comme toujours lorsqu'on s'apprête à ajouter plusieurs éléments à notre SVG qui sont liés entre eux il est préférable de créer un groupe les contenant tous (cela permet par exemple de les
déplacer facilement). Dans le code source tout ce qui suit se trouve dans la fonction addLegend
. Dès le départ nous positionnons (translate
) notre groupe légèrement
vers le bas et légèrement vers la droite par rapport au point de départ en haut à gauche.
var legend = svg.append('g')
.attr('transform', 'translate(40, 50)');
Le point rouge représente ce point de départ. Tous nos éléments seront positionnés vis-à-vis de ce point.
Premier élément de notre légende : la palette de couleurs. Elle prend comme données les indexes de notre tableau de couleurs. Chaque élément fait 20px de large et de haut (legendCellSize
).
On décale de 5px cette palette par rapport au point de départ (le point rouge) et la position verticale (y) est augmentée pour chaque entrée du tableau de couleurs. Après l'appel à la fonction
enter()
le code est exécuté autant de fois qu'il y a d'éléments dans le tableau. Le d
dont il est question correspond à l'index de parcours du tableau. Ainsi pour définir
la couleur d'une cellule il suffit de récupérer colors[d]
.
legend.selectAll()
.data(d3.range(colors.length))
.enter().append('svg:rect')
.attr('height', legendCellSize + 'px')
.attr('width', legendCellSize + 'px')
.attr('x', 5)
.attr('y', d => d * legendCellSize)
.style("fill", d => colors[d]);
Si vous voulez construire une légende horizontale c'est x
qui va varier en fonction de l'index de votre tableau de données et y
qui sera fixe.
Nous ne détaillons pas la construction de l'élément de légende pour les données absentes, il est assez simple à réaliser. Passons directement à l'axe de valeur situé à gauche de la palette. Nous avons choisi de borner notre axe par le min et le max des scores de nos données. Depuis la version 4 de D3JS le code est vraiment court.
var legendScale = d3.scaleLinear().domain([min, max])
.range([0, colors.length * legendCellSize]);
legendAxis = legend.append("g")
.attr("class", "axis")
.call(d3.axisLeft(legendScale));
Ce code construit une échelle linéaire entre notre domaine (de min à max) vers le range entre 0 et 340. Ce range correspond à la hauteur de toute la palette de couleurs. Ensuite on
ajoute un groupe à notre légende contenant l'axe correspondant, on choisit simplement de le positionner à gauche. Nous n'avons pas déplacé cet élément (translate
), il
se retrouve du coup juste au niveau de notre point rouge.
Nous verrons plus loin comment ajouter un mouseover
sur la palette permettant de sélectionner tous les pays associés.
Par rapport au tutoriel sur les lignes avancées il n'y a rien de nouveau concernant le tooltip. Il est construit de manière statique (ses dimensions sont fixes). Nous avons simplement
veillé à lui associer des couleurs en accord avec le thème. Comme indiqué dans les commentaires nous ajoutons un id
pour les éléments dynamiques (pays et score). Tous les
éléments du tooltip sont placés dans un groupe qui est par défaut masqué (display
défini à none
).
function addTooltip() {
var tooltip = svg.append("g") // Group for the whole tooltip
.attr("id", "tooltip")
.style("display", "none");
tooltip.append("polyline") // The rectangle containing the text, it is 210px width and 60 height
.attr("points","0,0 210,0 210,60 0,60 0,0")
.style("fill", "#222b1d")
.style("stroke","black")
.style("opacity","0.9")
.style("stroke-width","1")
.style("padding", "1em");
tooltip.append("line") // A line inserted between country name and score
.attr("x1", 40)
.attr("y1", 25)
.attr("x2", 160)
.attr("y2", 25)
.style("stroke","#929292")
.style("stroke-width","0.5")
.attr("transform", "translate(0, 5)");
var text = tooltip.append("text") // Text that will contain all tspan (used for multilines)
.style("font-size", "13px")
.style("fill", "#c1d3b8")
.attr("transform", "translate(0, 20)");
text.append("tspan") // Country name udpated by its id
.attr("x", 105) // ie, tooltip width / 2
.attr("y", 0)
.attr("id", "tooltip-country")
.attr("text-anchor", "middle")
.style("font-weight", "600")
.style("font-size", "16px");
text.append("tspan") // Fixed text
.attr("x", 105) // ie, tooltip width / 2
.attr("y", 30)
.attr("text-anchor", "middle")
.style("fill", "929292")
.text("Score : ");
text.append("tspan") // Score udpated by its id
.attr("id", "tooltip-score")
.style("fill","#c1d3b8")
.style("font-weight", "bold");
return tooltip;
}
Même chose ici, on s'assure de choisir une police élégante et des couleurs en accord avec notre thème. C'est la valeur de x
associée à l'utilisation du text-anchor
qui permet de centrer ces titres dans la visualisation. Rappelons par ailleurs qu'il est important de fournir la source des données que l'on représente lorsqu'on les a récupérés d'une étude
ou d'un site.
svg.append("text")
.attr("x", (width / 2))
.attr("y", 25)
.attr("text-anchor", "middle")
.style("fill", "#c1d3b8")
.style("font-weight", "300")
.style("font-size", "16px")
.text("Sentiment de sécurité des habitants de chaque pays en 2018");
svg.append("text")
.attr("x", (width / 2))
.attr("y", 50)
.attr("text-anchor", "middle")
.style("fill", "#929292")
.style("font-weight", "200")
.style("font-size", "12px")
.text("(source : Gallup Report 2018 - Global Law and Order)");
Pour charger nos données nous utilisons la méthode des promesses introduite dans la V5 de D3JS. La construction de la carte est assez habituelle. Nous utilisons une nouvelle projection
et son dimensionnement est calculé en fonction de la largeur et de la hauteur que nous avons définie (le .80
permet d'étaler la carte sur 80% de notre SVG). Nous associons
à chaque pays un id
qui correspond au code ISO de ce pays.
const projection = d3.geoNaturalEarth1()
.scale(1)
.translate([0, 0]);
const path = d3.geoPath()
.pointRadius(2)
.projection(projection);
const cGroup = svg.append("g");
var promises = [];
promises.push(d3.json("d3js/map-improve/world-countries-no-antartica.json"));
promises.push(d3.csv("d3js/map-improve/data.csv"));
Promise.all(promises).then(function(values) {
const geojson = values[0];
const scores = values[1];
var b = path.bounds(geojson),
s = .80 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection
.scale(s)
.translate(t);
cGroup.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path)
.attr("id", d => "code" + d.id)
.attr("class", "country");
// Le traitement du CSV est réalisé ici
});
Avant de charger notre fichier de données, il nous faut déclarer deux fonctions utilitaires. La première permet de réduire la longueur du nom de certains pays et la deuxième permet
de retrouver l'index d'une couleur dans notre tableau colors
.
function shortCountryName(country) {
return country.replace("Démocratique", "Dem.").replace("République", "Rep.");
}
function getColorIndex(color) {
for (var i = 0; i < colors.length; i++) {
if (colors[i] === color) {
return i;
}
}
return -1;
}
Notre fichier CSV que vous pouvez télécharger ici contient 4 colonnes : country
, code
,
frenchCountry
et score
. La colonne country
n'est pas utilisée. La colonne code
permet de faire le lien avec le fichier geoJSON,
la colonne frenchCountry
est affichée dans le tooltip et le score permet de colorier chaque pays. Le code se décompose ainsi :
scorecolor
qui sera utilisé dans la partie suivante pour sélectionner tous les pays d'une même couleurfill
en fonction du quantile associé au score de ce paysmouseover
qui passe en violet le pays, affiche le tooltip, le renseigne avec son nom et son score et enfin on positionne le curseur
au niveau de la légende.mouseout
qui remet la bonne couleur au pays, masque le tooltip et masque le curseurmousemove
qui déplace le tooltip en fonction de la position de la souris, l'objectif est d'éviter que la souris se retrouve sur
le tooltip, d'ou le léger décalage réalisé par la fonction translate
.const min = d3.min(scores, d => +d.score),
max = d3.max(scores, d => +d.score);
var quantile = d3.scaleQuantile().domain([min, max])
.range(colors);
var legend = addLegend(min, max);
var tooltip = addTooltip();
scores.forEach(function(e,i) {
var countryPath = d3.select("#code" + e.code);
countryPath
.attr("scorecolor", quantile(+e.score))
.style("fill", quantile(+e.score))
.on("mouseover", function(d) {
countryPath.style("fill", "#9966cc");
tooltip.style("display", null);
tooltip.select('#tooltip-country')
.text(shortCountryName(e.frenchCountry));
tooltip.select('#tooltip-score')
.text(e.score);
legend.select("#cursor")
.attr('transform', 'translate(' + (legendCellSize + 5) + ', ' + (getColorIndex(quantile(+e.score)) * legendCellSize) + ')')
.style("display", null);
})
.on("mouseout", function(event, d) {
countryPath.style("fill", quantile(+e.score));
tooltip.style("display", "none");
legend.select("#cursor").style("display", "none");
})
.on("mousemove", function(event, d) {
var mouse = d3.pointer(event);
tooltip.attr("transform", "translate(" + mouse[0] + "," + (mouse[1] - 75) + ")");
});
});
Les 8 premières lignes du code ci-dessous sont identiques à celle du paragraphe : Construction de la légende. On se contente d'ajouter les deux évènements mouseover
et mouseout
. Le premier évènement positionne correctement le curseur, à la bonne hauteur et légèrement sur la droite. Ensuite nous sélectionnons tous les pays
qui possèdent la même couleur que le carré sur lequel se trouve la souris (grâce à l'attribut scorecolor
définit juste au-dessus) et nous changeons leur couleur en violet.
Le deuxième évènement permet de remettre la bonne couleur sur ces pays et de masquer le curseur.
legend.selectAll()
.data(d3.range(colors.length))
.enter().append('svg:rect')
.attr('height', legendCellSize + 'px')
.attr('width', legendCellSize + 'px')
.attr('x', 5)
.attr('y', d => d * legendCellSize)
.style("fill", d => colors[d]);
.on("mouseover", function(event, d) {
legend.select("#cursor")
.attr('transform', 'translate(' + (legendCellSize + 5) + ', ' + (d * legendCellSize) + ')')
.style("display", null);
d3.selectAll("path[scorecolor='" + colors[d] + "']")
.style('fill', "#9966cc");
})
.on("mouseout", function(event, d) {
legend.select("#cursor")
.style("display", "none");
d3.selectAll("path[scorecolor='" + colors[d] + "']")
.style('fill', colors[d]);
});
Ce tutoriel est maintenant terminé. Il permet de réaliser qu'il est assez facile de construire une belle cartographie pour peu qu'on prenne le temps de bien décomposer les différents éléments.
VOUS POURRIEZ AIMER
COMMENTAIRES
Jihane
code source complet svp
ericfrigot
Bonjour, vous pouvez accéder au code source directement depuis le navigateur, il est en clair. Il vous suffit de faire bouton droit avec la souris puis "Afficher le code source de la page" sous Chrome par exemple. Le code commence à la ligne 71 par l'import des librairies et la partie CSS. A la ligne 113 vous avez la déclaration du DIV qui contient la map et à partir de la ligne 489 tout le javascript. Eric