D3JS - Frequency Trails

Anomalies de températures à la surface du globe

Dernière mise à jour le 28/10/2021
d3js5.x

Introduction

Cette visualisation présente les anomalies de températures de 1900 jusqu'à 2019. On part de la température moyenne du globe pendant tout le 20ème siècle. Ensuite pour chaque mois et chaque année on affiche la différence entre cette température et la température de l'année. Ainsi dans les premières décennies du 20ème les courbes sont principalement bleues car les températures constatées sont en-dessous de la moyenne. En approchant de la fin du 20ème siècle et au début du suivant les courbes deviennent uniquement rouges.

Les données

Les données utilisées proviennent du NOAA et peuvent être extraites directement sous la forme d'un CSV.

date,value
190001,-0.35
190002,-0.07
190003,-0.04
190004,-0.09

Initialisation

L'initialisation contient quelques particularités :

Pour le reste on insère toujours notre SVG sur un élément DIV existant dans la page, les marges dépendent des légendes qui sont associées et les valeurs d.date, d.valueP et d.valueN seront définies juste en-dessous.

const margin = {top: 20, right: 40, bottom: 100, left: 40},
	areaHeight = 90;
	width = document.getElementById("container").offsetWidth - margin.left - margin.right,
	height = 600 - margin.top - margin.bottom;

let data = [], // Contiendra les données du CSV restructurées
	verticalLine = null;
	
const svg = d3.select("#chart").append("svg")
	.attr("id", "svg")
	.attr("width", width + margin.left + margin.right)
	.attr("height", height + margin.top + margin.bottom);

let group = svg.append("g")
	.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
	
const x = d3.scaleBand()
   	.range([0, width]);

const y = d3.scaleLinear()
   	.range([areaHeight, 0]);

var areaP = d3.area()
	.x(d => x(d.date))
	.y0(areaHeight)
	.y1(d => y(d.valueP));

var areaN = d3.area()
	.x(d => x(d.date))
	.y0(areaHeight)
	.y1(d => y(d.valueN));

Chargement du CSV

Lorsque le fichier CSV est chargé nous le transformons pour construire le tableau data.

[{
	"year":"2010",
	"values": [{
		"date":"1001",
		"valueP":0.74,
		"valueN":null
	}, {
		"date":"1002",
		"valueP":0.8,
		"valueN":null
	}, //...
	]},
	"year":"2000",
	"values": //...
}]

Ensuite nous ajoutons les deux Areas, les textes et la gestion de la souris.

d3.csv('d3js/frequency-trails/world-temperature-1900-2020.csv').then(function(csv) {
	csv.forEach(d => {
		let year = d.date.substring(0, 3) + "0"; // On regroupe par décennie
		let yearData = null;
		for (let i = 0; i < data.length; ++i) {
			if (data[i].year === year) {
				yearData = data[i];
			}
		}
		if (yearData === null) {
			yearData = {"year": year, "values": []};
			data.push(yearData);
		}
		yearData.values.push({
			"date": d.date.substring(2, 6), 
			"valueP": +d.value >= 0 ? +d.value : null, 
			"valueN": +d.value < 0 ? +d.value : null
		});
	});
	data = data.reverse(); // On veut les années les plus anciennes en bas.

	addAreas();
	addTexts();
	handleMouse();
});

Ajoute des graphiques

Le code a été découpé en fonctions pour qu'il soit plus simple à présenter. Ce n'est pas optimal en termes de performances car on parcourt deux fois la liste des données mais au vu du volume ça n'est pas très grave. La construction des deux areas (une pour les valeurs positives et une pour les valeurs négatives) est assez simple. Il faut bien penser à décaler chaque area au fur et à mesure du parcours des données. Nous réalisons cette opération avec la variable transY et la transformation appliquée à chaque area.

function addAreas() {
	for (let i = 0; i < data.length; ++i) {
		x.domain(data[i].values.map(d => d.date));
		y.domain([0, 1.4]); // Depuis le site source

		let transY = (height / data.length) * i;
			
		group.append("path")
			.datum(data[i].values)
			.attr("id", "areaP_" + i)
			.attr("fill", "#fc9272")
			.attr("stroke", "#de2d26")
			.attr("transform", "translate(0," + transY + ")")
			.attr("d", areaP);

		group.append("path")
			.datum(data[i].values)
			.attr("id", "areaN_" + i)
			.attr("fill", "#9ecae1")
			.attr("stroke", "#3182bd")
			.attr("transform", "translate(0," + transY + ")")
			.attr("d", areaN);
	}
}

Ajoute des textes

L'ajout des textes reprend la même logique, on parcourt les données de chaque décennie et on ajoute trois textes à chaque fois. L'affichage des décennies étant fixe, la classe CSS associée est différente.

function addTexts() {
	for (let i = 0; i < data.length; ++i) {
		let transY = (height / data.length) * i;

		group.append("text") // Affichage de la décennie à gauche
			.attr("class", "text-legend-fixed")
			.attr("x", 0)
			.attr("dx", "-5px")
			.attr("y", transY + areaHeight)
			.attr("text-anchor", "end")
			.text(data[i].year);
			
		group.append("text") // Affichage dynamique de la date en fonction de la position de la souris
			.attr("id", "text_date_" + i)
			.attr("class", "text-legend")
			.attr("x", width)
			.attr("dx", "-5px")
			.attr("y", transY + areaHeight)
			.attr("text-anchor", "start");

		group.append("text") // Affichage dynamique de la température en fonction de la position de la souris
			.attr("id", "text_temp_" + i)
			.attr("class", "text-legend")
			.attr("x", width)
			.attr("dx", "-5px")
			.attr("y", transY + areaHeight + 15)
			.attr("text-anchor", "start");
	}
}

Gestion de la souris

Pour la gestion de la souris nous ajoutons au graphique une barre verticale que l'on va déplacer quand la souris se déplace. Lorsque la souris quitte le graphique nous voulons masquer la barre verticale ainsi que la date et la température affichées sur la droite, c'est pour cela que nous ajoutons un rectangle transparent qui couvre le graphique. Lorsqu'on entre dans le graphique (mouseover) on rend visible la ligne verticale et les deux textes et lorsqu'on en sort (mouseout) on les masque.

function handleMouse() {
	verticalLine = group.append("line")
	    .attr("class", "vertical-line")
   		.attr("x1",0)
   		.attr("y1",0)
   		.attr("x2",0)
   		.attr("y2", height + margin.top + margin.bottom)
   		.style("opacity", 0);

	group.append("rect")
		.attr("class", "overlay")
		.attr("width", width)
		.attr("height", height + margin.top + margin.bottom)
		.on("mouseover", function() {
			svg.selectAll(".text-legend")
				.style("display", null);
			svg.selectAll(".vertical-line")
				.style("display", null);
		})
		.on("mouseout", function() {
			svg.selectAll(".text-legend")
				.style("display", "none");
			svg.selectAll(".vertical-line")
				.style("display", "none");
		})
		.on("mousemove", mousemove);
}

Gestion du mousemove

Nous nous intéressons uniquement à la position horizontale de la souris, c'est pour cette raison que nous récupérons mouse[0] (mouse[1] contient la position verticale). Cette position est exprimée en pixels et nous la divisons par la largeur de chaque step. Pour chaque area il y a 120 steps représentant les 10 années multipliées par 12 mois. Cela correspond à notre tableau contenu dans la variable data qui contient pour chaque décennie 120 entrées. Si la valeur de j est au-dessous ou en-dessous nous ne faisons rien. Sinon nous positionnons la barre à la position de la souris et nous définissons les textes de chaque décennie.

function mousemove() {
	let mouse = d3.mouse(this),
    	j = Math.floor((mouse[0] / x.step()));
		
	if (j < 0 || j >= 120) { 
		return; 
	}

	// Positionnement de la barre verticale toujours en tenant compte de la marge
	verticalLine.attr("x1", mouse[0]);
	verticalLine.attr("x2", mouse[0]);
   	verticalLine.style("opacity", 1);
			
	for (let i = 0; i < data.length; ++i) {
		d3.select("#text_date_" + i)
			.text(data[i].values[j].date.substring(2, 4) + "/" + data[i].year.substring(0, 2) + data[i].values[j].date.substring(0, 2));
		d3.select("#text_temp_" + i)
			.text(data[i].values[j].valueP !== null ? "+" + data[i].values[j].valueP : data[i].values[j].valueN)
			.style("fill", data[i].values[j].valueP !== null ? "#de2d26" : "#3182bd")
			.style("font-weight", "bold");
	}
}

Conclusion

Ce tutoriel n'introduit aucune nouveauté par rapport aux précédents mais permet de construire une nouvelle représentation d'une évolution temporelle. Comme toujours la section commentaire est présente pour toutes vos questions.

VOUS POURRIEZ AIMER


D3JS - Sunburst Chart

Sunburst Chart

Construction d'un graphique hiérarchique de type Sunburst, variation d'une couleur en HSL et utilisation de patterns SVG

D3JS - Histogramme Empilé (Stacked Bar Chart)

Histogramme Empilé (Stacked Bar Chart)

Construire un diagramme empilé avec tooltip en SVG, moyenne mobile et gestion de la souris avancée

D3JS - Graphique en essaims (Swarm Chart)

Graphique en essaims (Swarm Chart)

Construire un graphique en essaims avec d3.forceSimulation et un paramétrage avancé des axes

comments powered by Disqus