Cette visualisation présente les anomalies de températures de 1900 à 2023. 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 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
L'initialisation contient quelques particularités :
areaHeight
représente la hauteur en pixel du graphique associé à chaque décennie, il est possible de la faire varier pour que le graphique empiète plus ou moins sur les suivantsd3.area
car pour chaque décennie nous dessinons deux graphiques, un pour les valeurs négatives et un autre pour les valeurs positivesd.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));
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('/tutorials/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 : 0,
"valueN": +d.value < 0 ? +d.value : 0
});
});
data = data.reverse(); // On veut les années les plus anciennes en bas.
addAreas();
addTexts();
handleMouse();
});
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);
}
}
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");
}
}
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 les dates et les températures 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);
}
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(event) {
let mouse = d3.pointer(event),
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 !== 0 ? "+" + data[i].values[j].valueP : data[i].values[j].valueN)
.style("fill", data[i].values[j].valueP !== 0 ? "#de2d26" : "#3182bd")
.style("font-weight", "bold");
}
}
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
COMMENTAIRES