D3JS - Projection orthographique

Projection orthographique et rotation avec la souris ou le doigt
d3js7.x
Sources :

Présentation

Ce tutoriel introduit la projection orthographique qui permet de représenter la terre sous forme de sphère. Cette projection permet à la fois de conserver la taille des pays et de ne pas les déformer. Pour vous donner un ordre d'idée, vous pouvez comparer la différence de taille entre l'Amérique du Sud et le Groeland dans ce tutoriel et dans celui sur Les prix Nobels. Le Groeland est 8 fois plus gros dans la projection de Mercator qu'il ne l'est en réalité alors que cette projection est la plus utilisée. Sont présents également sur cette carte les méridiens et les parallèles. Cette visualisation est interactive et permet de faire tourner la terre selon ses 3 axes en cliquant dessus et en se déplaçant ensuite. La coloration des pays indique leur température moyenne au cours de l'année (l'échelle associée va de -15° à 30° et nous l'avons nous même créée).

Préparation

Notre page HTML contient un DIV dont l'ID est map. Comme pour les précédents tutoriels géographiques, on commence par définir notre projection. Celle-ci est de type orthographique, le scale définit le zoom réalisé et est dépendant de la taille que l'on veut associer à notre SVG. La fonction translate permet de centrer la projection (au lieu de l'avoir en haut à gauche). Le clipAngle est important car sans lui les pays de l'autre côté de la terre seraient visibles. La fonction rotate permet de définir sur quel endroit de la terre sera située la projection dans son état initial. Notez enfin que nous avons défini l'ID world sur notre SVG.

const width = 800, height = 800;

const projection = d3.geoOrthographic()
	.scale(350)
	.translate([width / 2, height / 2])
	.clipAngle(90) // sans cette option les pays de l'autre côté du globle sont visibles
	.precision(.1)
	.rotate([0,0,0]);

const path = d3.geoPath()
	.projection(projection);

const svg = d3.select("#map").append("svg")
	.attr("id", "world")
	.attr("width", width)
	.attr("height", height);

Méridiens et parallèles

L'ajout des méridiens et des parallèles est grandement facilité par D3JS. En effet, la fonction d3.geo.graticule() nous retourne une liste de chaînes de caractères les représentants. Il nous suffit ensuite d'ajouter un path avec ces données pour les dessiner. Ces méridiens et parallèles seront situés sous les pays, si ce n'est pas ce que vous voulez il faut les ajouter après avec créé les pays.

const graticule = d3.geoGraticule();
svg.append("path")
	.datum(graticule)
	.attr("class", "graticule")
	.attr("d", path);

Il faut ajouter un peu de CSS pour matérialiser ces informations.

.graticule {
	fill: none;
	stroke: #777;
	stroke-width: .5px;
	stroke-opacity: .5;
}

Chargement des pays

Le chargement des pays n'a rien d'original par rapport aux précédents tutoriaux. N'oubliez pas de définir un ID sur chaque pays. Il servira juste en dessous pour attribuer une couleur à ce pays. Le fichier utilisé est world-countries.json.

d3.json("/tutorials/d3js/map-world-temperature/world-countries.json").then(function(collection) {
	var countries = svg.selectAll("path")
		.data(collection.features)
		.enter().append("path")
		.attr("d", path)
		.attr("class", "country")
		.attr("id", d => d.id);

	// Ici vient se placer tout le code de définition de la légende
});

Ajout de la légende

La liste des températures moyenne par pays peut être récupérée dans le fichier world-temperature.csv. Il vous faudra également le fichier CSS associé temperature.css. Celui-ci contient la définition d'un dégradé du bleu vers le rouge en 60 nuances. Ce nombre est important car nous l'utilisons pour construire notre fonction quantile qui permet d'associer une température à un nombre compris entre 0 et 60. De la même façon nous construisons à la ligne 11 60 rectangles représentant ce dégradé. Pour plus de détails sur la création de légendes vous pouvez vous référer au tutoriel Carte choroplèthe. Notez simplement qu'à la ligne 35 nous utilisons les ID définit pour chacun de nos pays afin de leur associer une couleur en fonction de leur température moyenne.

d3.csv("/tutorials/d3js/map-world-temperature/world-temperature.csv").then(function(data) {
	var quantile = d3.scale.quantile().domain([
			d3.min(data, e => +e.temperature),
			d3.max(data, e => +e.temperature)
		]).range(d3.range(60));
		
	var legend = svg.append('g')
		.attr('transform', 'translate(35, 10)')
		.attr('id', 'legend');
		
	legend.selectAll('.colorbar') // LIGNE 11
		.data(d3.range(60))
		.enter().append('rect')
		.attr('y', d => d * 5 + 'px')
		.attr('height', '5px')
		.attr('width', '20px')
		.attr('x', '0px')
		.attr("class", d => "temperature-" + d);
	
	legendScale = d3.scaleLinear()
		.domain([d3.min(data, e => +e.temperature), d3.max(data, e => +e.temperature)])
		.range([0, 60 * 5]);
		
	svg.append("g")
		.attr('transform', 'translate(25, 10)')
		.call(d3.axisLeft(legendScale).ticks(10));
	
	data.forEach(function(e,i) {
		d3.select("#" + e.country) // LIGNE 29
			.attr("class", e => "country temperature-" + quantile(+e.temperature));
	});
});

Gestion de la rotation

Nous commençons par définir les deux domaines de rotation que nous allons utiliser (phi : φ et lambda : λ). Le domaine de départ est notre globe (sur la largeur puis sur la hauteur). Les ranges correspondent au schéma ci-dessous. Vous pouvez voir à quoi correspond la rotation de ces axes sur le site suivant : Voir la rotation des axes. Notez que nos fichiers HTML et Javascript sont encodés en UTF8, sans quoi nous ne pourrions pas utiliser directement ces symboles en tant que caractères valides.

Coordonnées sphériques
const λ = d3.scale.linear()
	.domain([0, width])
	.range([-180, 180]);

const φ = d3.scale.linear()
	.domain([0, height])
	.range([90, -90]);

Le mécanisme de "drag" se définit en deux temps, d'abord nous récupérons la position d'origine ou l'utilisateur clique pour débuter son action, ensuite nous définissons une fonction qui réalisera la rotation tant que l'utilisateur n'a pas relâché le bouton de la souris ou son doigt sur un écran tactile. Les méridiens et parallèles sont redessinés en premier, puis on redessine les pays. Enfin, on associe la fonction de "drag" à notre SVG.

var drag = d3.drag().subject(function() {
	var r = projection.rotate();
	return {
		x: λ.invert(r[0]),
		y: φ.invert(r[1])
	};
}).on("drag", function(event) {
	projection.rotate([λ(event.x), φ(event.y)]);

	svg.selectAll(".graticule")
		.datum(graticule)
		.attr("d", path);
	
	svg.selectAll(".country")
		.attr("d", path);
});

svg.call(drag);

COMMENTAIRES

contact GDweb


Bonjour,
Je me suis largement inspiré de votre tuo pour réaliser cette page: https://www.gdeleage.fr/pro...
J'aurai aimé ajouter les noms des pays sur le gobe, mais je n'y arrive pas... Pouvez-vous m'aider?
GilberD

ericfrigot


Bonjour,
J'ai tenté de faire quelque chose pour les libellés : https://www.datavis.fr/d3js... (juste après l'ajout des pays et aussi dans la fonction on drag pour tenter de déplacer les libellés)
Mais ça fonctionne très mal. Il y a des soucis de densité lorsqu'il y a trop de pays sur une faible zone, il y a des soucis liés à la projection utilisée et à la rotation. D'ailleurs je m'interroge, le tooltip que vous avez mis en place sur votre site me semble très bien.
Ca marche bien si vous acceptez de grosses contraintes comme ici : https://www.d3-graph-galler... (tous les états ont la même taille et un libellé sur 2 caractères).
Cordialement, Eric

PierreV


Bonjour, merci pour ce tuto qui permet de progresser un peu plus par rapport aux précédentes cartes.

Par contre j'ai quelques petits problèmes avec. Tout d'abord y a-t-il un soucis avec l'Afghanistan ? on a un vide à son emplacement, pourtant je ne vois pas de problème dans la géométrie du json ou dans son identifiant. Que ce pays soit le premier dans la liste a peut être un rapport ?

Sinon j'ai repris du précédent tuto sur la carte chroroplète la méthode pour ajouter un menu et choisir le champ du tsv à visualiser : jusque là ça va mais j'ai voulu mettre aussi une transition pour que les couleurs changent progressivement. Là c'est assez étrange au moment où commence la transition tous les pays dont leur couleur doit changer deviennent noir et quand la transition se termine ils prennent instantanément leur nouvelle couleur. De plus il y a un deuxième soucis c'est qu'à la fin de la transition certains pays restent noir et ne prennent pas leur nouvelle couleur : cela se produit pour l'Antarctique, l'Angola, l'Arménie et les Emirats arabes unis; qui se trouvent d'ailleurs être eux aussi les premiers pays dans le json après l'Afghanistan. Je trouve ça assez étrange je ne vois pas trop comment corriger ça.

Je vous mets l'extrait du code utilisé pour la transition :
data.forEach(function(e,i) {
d3.select("#" + e.country)
.transition()
.duration(1000)
.attr("class", function(d) { return "country temperature-" + quantile(+e[selectedData]); });
});

ericfrigot


Bonjour, désolé d'avoir tardé à vous répondre mais c'est que je ne trouve toujours pas de solutions aux problèmes que vous avez mentionnés.

Pour l'Afghanistan, je ne comprends vraiment pas, j'ai également vérifié le JSON qui est correct mais je vais continuer de chercher, j'ai déjà l'impression que la projection utilisée est responsable de la disparation du pays (voir exemple ci-dessous ou le pays est toujours visible).

Pour la transition, j'ai fait les deux exemples suivants : http://www.datavis.fr/d3js/... et http://www.datavis.fr/d3js/.... Aucun des deux n'utilisent des données en provenance d'un CSV, néanmoins je lis le CSV et utilise l'ID du pays pour modifier la couleur. Dans le premier exemple je vois les mêmes problèmes que vous, ma transition est aléatoire sur une échelle de couleurs et certains pays bleu au départ se retrouvent noir à la fin, certains noirs au départ se retrouvent rouge à la fin. En rafraichissant la page, on constate que ce n'est jamais les mêmes pays. Le second exemple qui va du bleu vers le rouge pour tous les pays fonctionnent en revanche très bien à chaque fois.

Cet exemple démontre pourtant que ça fonctionne bien pour l'Afghanistan : https://bl.ocks.org/mbostoc.... Il faut peut-être utiliser ce geoJSON et revoir du coup l'association avec les données.

Je vais continuer d'investiguer et si je ne trouve pas je poserais une question sur stackoverflow.

Eric

PierreV


Bonjour, merci pour les efforts que vous fournissez pour tenter de résoudre ces problèmes.
J'ai vu les exemples que vous avez faits et déjà ils montrent que la transition de couleurs fonctionne (sauf pour quelques pays). De mon côté la transition ne passe pas du tout, quand je sélectionne par un menu la nouvelle colonne à afficher les pays deviennent instantanément noir et lorsque la transition se termine (selon la valeur donnée à "duration") les pays passent instantanément du noir à leur nouvelle couleur.

Alors je ne sais pas mais les deux seules différences que je vois par rapport à vos tests c'est d'une part que je lance la transition lorsque je sélectionne une nouvelle valeur dans un menu et d'autre part que les couleurs sont définies par un fichier css externe (là dessus je ne vois quel problème cela pourrait poser).

Pierre

ericfrigot


Bonjour Pierre,
Je reviens vers au sujet des transitions. Sur un autre projet j'ai finalement compris pourquoi cela ne fonctionnait pas. D3JS ne sait pas faire une transition entre deux classes, quand bien même cette classe ne fait que définir une couleur. Pour que cela fonctionne il faut faire la transition entre deux couleurs directement, pour ma part j'ai mis toutes les couleurs dans un tableau en javascript et cela fonctionne très bien. C'est 5 mois plus tard, mais c'est quand même une réponse :)

PierreV


Bonsoir,
Merci pour ce retour, ça fait un mystère de résolu :)