Les cartes de chaleur peuvent prendre des formes variées. Ici la chaleur se traduit par le nombre d'articles publiés pour un jour et à une heure précise. Contrairement aux tutoriaux précédent nous n'utilisons pas une représentation provenant de D3JS pour nos données (comme l'aurait été une carte choroplèthe ou un nuage de mots). En fait la représentation est simplement constituée de carrés que nous construisons à partir de nos données. Ici D3JS nous aide surtout par tout ce qui gravite autour de la représentation en elle-même, à savoir la sélection, les échelles ou la manipulation de données. Dans ce tutoriel nous verrons également comment aborder l'aspect responsive d'une visualisation car la taille des rectangles s'adaptent à la largeur disponible.
Les données que nous présentons ci-dessus proviennent du site Rue89 qui n'existe plus aujourd'hui. Wikipedia pourra vous renseigner plus en détails sur ce site crée en 2007 et racheté ensuite par le Nouvel Obs. A sa création ce journal purement numérique se voulait ouvert et était plutôt orienté à gauche, il offrait un espace important à ses lecteurs dans la section commentaire avec une modération équilibrée rendant les échanges enrichissants. Malheureusement pour lui il n'a jamais atteint l'équilibre financier et il subsiste aujourd'hui comme une rubrique du journal qui l'a racheté.
Le tutoriel est un peu long car la visualisation est assez soignée et comme nous l'avons dit elle est partiellement responsive. Si vous ne la connaissez pas, vous pouvez jeter un oeil au compte Twitter de Nadieh Bremer qui présente des visualisations à couper le souffle !
Nous commençons par définir les jours et les heures. La fonction d3.range
retourne un tableau contenant une progression arithmétique. Par défaut la progression est de 1 et notre appel permet
de construire un tableau de 0 à 23. Avec le code d3.range(5, 10, 0.5)
nous aurions obtenu le tableau : [5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5], de 5 inclue à 10 exclue par palier de 0.5. Les
marges sont définies de manière empirique pour laisser la place au titre, sous-titre et éléments de légende.
Ici la largeur et la hauteur correspondent uniquement aux dimensions de notre grille. Nous voulons une largeur qui soit au maximum de 1000px et au minimum de 500px. Pourquoi 500px au minimum ? Parce que en
dessous de cette taille les libellés des jours et des heures se chevaucheraient et il faudrait diminuer leur font-size
. Une fois cette largeur définie nous pouvons
déterminer la taille de chaque élément de la grille ce qui nous permet ensuite de calculer la hauteur de cette grille. Pour construire une visualisation responsive il faut rendre les paramètres de taille
dépendant de l'espace dont nous disposons à l'écran, c'est bien ce que nous faisons ici.
let days = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"],
times = d3.range(24),
margin = {
top: 90,
right: 50,
bottom: 140,
left: 50
};
let width = Math.max(Math.min(window.innerWidth, 1000), 500) - margin.left - margin.right,
gridSize = Math.floor(width / times.length),
height = gridSize * days.length;
Rien de bien sorcier ici, nous sélectionnons un DIV existant dans la page (chart
) et lui ajoutons un SVG avec les paramètres définis ci-dessus. Un groupe est ajouté à ce SVG, il contiendra
tous les éléments de la visualisation.
var maingroup = d3.select('#chart')
.append("svg")
.attr("class", "svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
Ajouter les jours et les heures a l'air assez simple, pourtant si l'on veut bien faire il faut être précis. La construction suit la même logique pour les deux axes. Nous précisons le jeu de données
associé à chaque construction (days
et times
) pour ajouter autant d'éléments text que chaque jeu contient d'entrées. Pour les jours c'est y
qui varie alors
que pour les heures c'est x
. Lorsqu'ils varient, ils le font en fonction de la gridSize
et de la position dans chaque tableau. Ainsi lorsque la largeur disponible est plus
petite les libellés seront plus rapprochés. Le texte est positionné avec une marge de 6px par rapport à la grille et centré sur chaque cellule, le tout grâce à la fonction transform
et
au style text-anchor
. La classe CSS associée n'est pas la même pour différencier les périodes de travail des temps de repos.
var dayLabels = maingroup.selectAll(".dayLabel")
.data(days)
.enter().append("text")
.text(function (d) { return d; })
.attr("x", 0)
.attr("y", function (d, i) { return i * gridSize; })
.attr("transform", "translate(-6," + gridSize / 2 + ")")
.attr("class", function (d, i) { return ((i >= 0 && i <= 4) ? "dayLabel axis-workweek" : "dayLabel"); })
.style("text-anchor", "end");
var timeLabels = maingroup.selectAll(".timeLabel")
.data(times)
.enter().append("text")
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * gridSize; })
.attr("y", 0)
.attr("transform", "translate(" + gridSize / 2 + ", -6)")
.attr("class", function(d, i) { return ((i >= 9 && i <= 19) ? "timeLabel axis-worktime" : "timeLabel"); })
.style("text-anchor", "middle");
Le code est habituel pour ces éléments, ils sont centrés et associés à un CSS particulier.
maingroup.append("text")
.attr("class", "title")
.attr("x", width / 2)
.attr("y", -70)
.style("text-anchor", "middle")
.text("Nombre d'Articles publiés sur Rue89");
maingroup.append("text")
.attr("class", "credit")
.attr("x", width/2)
.attr("y", gridSize * (days.length+1))
.style("text-anchor", "middle")
.text("Basé sur le travail de Nadieh Bremer & Miles McCrocklin");
Tout le reste du code que nous allons étudier est contenu dans la fonction de traitements de nos données. Celles-ci se matérialisent par un fichier TSV dont les premières lignes sont :
day hour count
2 22 3
2 21 11
2 20 33
2 23 11
4 20 47
4 23 3
4 22 7
4 21 13
Dans ce fichier on retrouve les trois colonnes utiles à notre visualisation : le jour de 0 à 6, l'heure de 0 à 23 et enfin une valeur représentant le nombre d'articles publiés en moyenne à cette
heure ce jour-là. Il n'est pas nécessaire que ces données soient triées. Nous commençons simplement en chargeant ce tableau dans une variable data
et en s'assurant que son contenu
soit toujours reconnu comme des valeurs numériques. La deuxième partie ajoute un sous-titre à la visualisation en tenant compte de la somme des valeurs contenues dans la colonne count
.
d3.tsv("/tutorials/d3js/day-hour-heatmap/data2010.tsv").then(function(data) {
data.forEach(function(d) {
d.day = +d.day;
d.hour = +d.hour;
d.count = +d.count;
});
maingroup.append("text")
.attr("class", "subtitle")
.attr("x", width / 2)
.attr("y", -40)
.style("text-anchor", "middle")
.text("En 2010 - " + d3.sum(data, function(d) {return d.count; }) + " articles");
// Le reste du code va ici
});
C'est toujours une étape délicate, il faut tenir compte de la répartition des valeurs (ici le nombre d'articles publiés chaque heure de chaque jour), de leur min et max. Par exemple dans un précédent tutoriel (Les prix Nobel) nous avons utilisé une échelle non linéaire. Ici ce n'est pas le cas et nous décidons de construire une échelle linéaire entre un domaine composé de 0, de la moitié de la valeur max et de la valeur max vers un range composé également de trois couleurs (d'un bleu presque blanc à un bleu foncé).
var colorScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {return d.count; }) / 2, d3.max(data, function(d) {return d.count; })])
.range(["#f7fbff", "#6baed6", "#08306b"]);
On y arrive enfin, le code pour construire tous les blocs de couleurs est assez simple, nous avons bien préparé le terrain. Pour chaque entrée de notre tableau data
on ajoute un rectangle
dont le x
dépend de l'heure de la journée et le y
du jour de la semaine. Ce rectangle est rempli grâce à notre échelle dépendant du nombre d'articles. C'est l'utilisation du
CSS stroke
et stroke-opacity
qui fournit le quadrillage blanc que l'on peut observer sur le résultat final.
var heatMap = maingroup.selectAll(".hour")
.data(data)
.enter().append("rect")
.attr("x", function(d) { return d.hour * gridSize; })
.attr("y", function(d) { return d.day * gridSize; })
.attr("width", gridSize)
.attr("height", gridSize)
.style("stroke", "white")
.style("stroke-opacity", 0.6)
.style("fill", function(d) { return colorScale(d.count); });
Contrairement au tutoriel que nous avons cité précédemùent, la légende n'est pas composée de petits rectangles de couleurs entre notre couleur la plus claire et la plus foncée. Au contraire vous pouvez constater que cette légende est parfaitement continue. Ce n'est pas facile à faire et cela nécessite de passer par du CSS. Nous n'entrons pas ici dans les détails mais voici en résumé ce que le code suivant fait :
id
legend-trafficvar countScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {return d.count; })])
.range([0, width])
numStops = 3;
countPoint = [0, d3.max(data, function(d) {return d.count; }) / 2, d3.max(data, function(d) {return d.count; })];
maingroup.append("defs")
.append("linearGradient")
.attr("id", "legend-traffic")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%")
.selectAll("stop")
.data(d3.range(numStops))
.enter().append("stop")
.attr("offset", function(d,i) {
return countScale(countPoint[i]) / width;
})
.attr("stop-color", function(d,i) {
return colorScale(countPoint[i]);
});
var legendWidth = Math.min(width * 0.8, 400);
var legendsvg = maingroup.append("g") // groupe principal
.attr("class", "legendWrapper")
.attr("transform", "translate(" + (width/2) + "," + (gridSize * days.length + 40) + ")");
legendsvg.append("rect") // rectangle avec gradient
.attr("class", "legendRect")
.attr("x", -legendWidth/2)
.attr("y", 0)
.attr("width", legendWidth)
.attr("height", 10)
.style("fill", "url(#legend-traffic)");
legendsvg.append("text") // légende
.attr("class", "legendTitle")
.attr("x", 0)
.attr("y", -10)
.style("text-anchor", "middle")
.text("Nombre d'Articles");
var xScale = d3.scaleLinear() // scale pour x-axis
.range([-legendWidth / 2, legendWidth / 2])
.domain([ 0, d3.max(data, function(d) { return d.count; })] );
legendsvg.append("g") // x axis
.attr("class", "axis")
.attr("transform", "translate(0," + (10) + ")")
.call(d3.axisBottom(xScale).ticks(5));
Comme indiqué en début de tutoriel celui-ci est un peu long, mais il reste accessible et permet d'obtenir une visualisation propre et plutôt responsive. N'hésitez pas à nous poser des questions, sur la partie gradient ou sur tout autre aspect si ça n'est pas assez clair. Pour vous aider dans le choix de vos couleurs vous pouvez toujours jeter un oeil au site Color Brewer qui est tout indiqué pour ce genre de visualisation. Enfin voici un autre exemple de heatmap cette fois-ci sur une année complète, la construction est identique à ce que nous venons de voir, il suffit d'adapter les axes X et Y (regardez le code source pour plus de détails).
VOUS POURRIEZ AIMER
COMMENTAIRES
Kim
If you want to fuck, contact me on whatsapp : +44 4866 1285
Jean-Pierre
Hello, bien le site !