D3JS - Ligne avancée (Linear Chart)

Tooltip directement en SVG, dégradé sous la courbe, point de suivi des valeurs...

Dernière mise à jour le 13/06/2018

Introduction

Ce tutoriel est le prolongement du précédent consacré au Line Chart. Notre objectif est simple, faire aussi bien que Highcharts, une excellente librairie pour obtenir des graphiques élégants en peu de temps, voir en ce qui nous concerne cet exemple.

Initialisation

Le code suivant est identique au précédent tutoriel : définition de la taille de notre SVG, des marges, des domaines X et Y, des axes et création de la fonction pour la ligne. Nous avons juste ajouté la fonction area pour dessiner le dégradé sous la courbe.

const margin = {top: 50, right: 30, bottom: 30, left: 60},
	width = document.getElementById("container").offsetWidth * 0.95 - margin.left - margin.right,
	height = 400 - margin.top - margin.bottom;

const parseTime = d3.timeParse("%d/%m/%Y");
const dateFormat = d3.timeFormat("%d/%m/%Y");

const x = d3.scaleTime()
	.range([0, width]);

const y = d3.scaleLinear()
	.range([height, 0]);
	
const line = d3.line()
	.x(function(d) { return x(d.date); })
	.y(function(d) { return y(d.close); });

const area = d3.area()
	.x(function(d) { return x(d.date); })
	.y0(height)
	.y1(function(d) { return y(d.close); });

// Attention ici il faut que le body possède déjà un DIV dont l'ID est chart
const svg = d3.select("#chart").append("svg")
	.attr("id", "svg")
	.attr("width", width + margin.left + margin.right)
	.attr("height", height + margin.top + margin.bottom)
	.append("g")
	.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

Ajout d'un titre

L'ajout d'un titre est plutôt simple à réaliser, on lui demande juste d'être centré. Pour cela il faut le positionner au milieu de l'axe X et presque en haut du graphique (25px plus bas en ce qui nous concerne). Par ailleurs, il faut bien définir l'attribut text-anchor à middle.

svg.append("text")
	.attr("x", (width / 2))             
	.attr("y", 0 - (margin.top / 2))
	.attr("text-anchor", "middle")
	.style("fill", "#5a5a5a")
	.style("font-family", "Raleway")
	.style("font-weight", "300")
	.style("font-size", "24px")
	.text("Cours journalier de l'or depuis 2001");

Ajout du tooltip

Dans le précédent tutoriel il est impossible que la souris passe sur le tooltip sinon l'évènement d'écoute mousemove est perdu puisqu'il est posé sur le SVG et que le DIV de notre tooltip a pour parent le body de la page. Cette contrainte nous a obligé a toujours décalé le tooltip par rapport à la souris. Ce n'est pas le cas dans ce tutoriel ou la souris peut passer sur le tooltip sans que cela ne gêne le mécanisme d'écoute. Par contre nous avons dû créer ce tooltip entièrement en SVG pour l'ajouter à notre SVG. Notez que nous avons été particulièrement vigilants au style utilisé en choisissant police, couleur et transparence pour obtenir quelque chose de propre.

function addTooltip() {
	// Création d'un groupe qui contiendra tout le tooltip plus le cercle de suivi
	var tooltip = svg.append("g")
		.attr("id", "tooltip")
		.style("display", "none");
	
	// Le cercle extérieur bleu clair
	tooltip.append("circle")
		.attr("fill", "#CCE5F6")
		.attr("r", 10);

	// Le cercle intérieur bleu foncé
	tooltip.append("circle")
		.attr("fill", "#3498db")
		.attr("stroke", "#fff")
		.attr("stroke-width", "1.5px")
		.attr("r", 4);
	
	// Le tooltip en lui-même avec sa pointe vers le bas
	// Il faut le dimensionner en fonction du contenu
	tooltip.append("polyline")
		.attr("points","0,0 0,40 55,40 60,45 65,40 120,40 120,0 0,0")
		.style("fill", "#fafafa")
		.style("stroke","#3498db")
		.style("opacity","0.9")
		.style("stroke-width","1")
		.attr("transform", "translate(-60, -55)");
	
	// Cet élément contiendra tout notre texte
	var text = tooltip.append("text")
		.style("font-size", "13px")
		.style("font-family", "Segoe UI")
		.style("color", "#333333")
		.style("fill", "#333333")
		.attr("transform", "translate(-50, -40)");
	
	// Element pour la date avec positionnement spécifique
	text.append("tspan")
		.attr("dx", "-5")
		.attr("id", "tooltip-date");
	
	// Positionnement spécifique pour le petit rond	bleu
	text.append("tspan")
		.style("fill", "#3498db")
		.attr("dx", "-60")
		.attr("dy", "15")
		.text("●");
	
	// Le texte "Cours : "
	text.append("tspan")
		.attr("dx", "5")
		.text("Cours : ");
	
	// Le texte pour la valeur de l'or à la date sélectionnée
	text.append("tspan")
		.attr("id", "tooltip-close")
		.style("font-weight", "bold");
	
	return tooltip;
}

L'avantage du polyline pour le rectangle c'est que la bordure associée est propre au niveau du triangle. Nous aurions pu dessiner un rectangle plus un triangle mais il y aurait eu un morceau de bordure au-dessus du triangle. Ce tooltip est placé dans un groupe ce qui rendra facile son positionnement par la suite. Deux IDs ont été défini, tooltip-date et tooltip-close pour mettre à jour la date et le cours de l'or en fonction de la position de la souris.

Création des axes et de la ligne

Ce code est identique au précédent tutoriel, après avoir chargé le fichier de données, on le parcourt pour lire la date et la formatter, on n'oublie pas de passer en numérique la colonne du cours, on trie les données, on met à jour les domaines puis on ajoute les axes au graphique. Enfin on ajoute les lignes horizontales et la ligne correspondant au cours de l'or.

d3.tsv("d3js/linearchart-improve/data.tsv", function(error, data) {
	data.forEach(function(d) {
		d.date = parseTime(d.date);
		d.close = +d.close;
	});
	
	data.sort(function(a, b) {
		return a.date - b.date;
	});
	
	x.domain(d3.extent(data, function(d) { return d.date; }));
	y.domain(d3.extent(data, function(d) { return d.close; }));
	
	svg.append("g")
		.attr("transform", "translate(0," + height + ")")
		.call(d3.axisBottom(x));

	svg.append("g")
		.call(d3.axisLeft(y))
		.append("text")
		.attr("fill", "#000")
		.attr("transform", "rotate(-90)")
		.attr("y", 6)
		.attr("dy", "0.71em")
		.style("text-anchor", "end")
		.text("$");
  
	svg.selectAll("y axis").data(y.ticks(10)).enter()
		.append("line")
		.attr("class", "horizontalGrid")
		.attr("x1", 0)
		.attr("x2", width)
		.attr("y1", function(d){ return y(d);})
		.attr("y2", function(d){ return y(d);});
		
	var linePath = svg.append("path")
		.datum(data)
		.style("fill", "none")
		.style("stroke", "#3498db")
		.style("stroke-width", "1px")
		.style("opacity", "0.6")
		.attr("d", line);
	
	// Le reste du code ira ici
}

Création du dégradé sous la courbe (Area Chart)

Nous abordons une partie subtile de ce tutoriel et à notre connaissance il n'existe pas d'autre façon de procéder pour obtenir un dégradé. Nous définissons un gradient linéaire au niveau de notre SVG et lui attribuons l'ID areachart-gradient, ce gradient sera ensuite utilisé par notre Area Chart. Celui-ci s'applique uniquement à la verticale (c'est pour cette raison que x1 et x2 sont définis à 0) entre la plus faible et la plus haute valeur du cours de l'or (d.close). La couleur du dégradé varie entre #F7FBFE (bleu très clair) à 0% et #3498DB (bleu plus foncé) à 100%. Il est bien sûr possible d'ajouter des valeurs intermédiaire entre 0% et 100%. Une autre façon d'écrire la même chose est visible ici. Pour pouvoir choisir facilement les couleurs de votre gradient vous pouvez regarder ce lien.

svg.append("linearGradient")
	.attr("id", "areachart-gradient")
	.attr("gradientUnits", "userSpaceOnUse")
	.attr("x1", 0)
	.attr("x2", 0)
	.attr("y1", y(d3.min(data, function(d) { return d.close; })))
	.attr("y2", y(d3.max(data, function(d) { return d.close; })))
	.selectAll("stop")
		.data([
			{offset: "0%", color: "#F7FBFE"},
			{offset: "100%", color: "#3498DB"}
		])
	.enter().append("stop")
		.attr("offset", function(d) { return d.offset; })
		.attr("stop-color", function(d) { return d.color; });

Il ne nous reste plus qu'à dessiner notre Area Chart en faisant référence pour le remplissage à notre gradient. L'opacité est diminuée pour permettre la visualisation des lignes horizontales. L'ensemble du code présenté dans cette partie provient de ce tutoriel qui utilise 3 couleurs distinctes.

var areaPath = svg.append("path")
	.datum(data)
	.style("fill", "url(#areachart-gradient)")
	.style("opacity", "0.6")
	.attr("d", area);

Gestion des évènements de la souris

Dans cette partie la solution pour gérer les évènements de la souris et positionner notre tooltip est entièrement différente du précédent tutoriel, elle est à la fois plus concise et plus élégante. Dans un premier temps on ajoute notre tooltip à notre SVG via la fonction définit précédemment.

var tooltip = addTooltip();

Nous définissons ensuite la fonction bisector pour retrouver la date la plus proche de la position de la souris (celle sur la gauche en l'occurrence, nous aurions aussi pu choisir right).

var bisectDate = d3.bisector(function(d) { return d.date; }).left;

Plutôt de calculer de manière plus ou moins compliquée si la souris est en dehors de notre linechart, nous construisons un rectangle qui fait exactement ses dimensions. Il suffit ainsi d'ajouter les fonctions mouseover et mouseout pour savoir si l'on doit afficher le tooltip ou non. La dernière fonction gère le positionnement du tooltip et la définition de son contenu.

svg.append("rect")
	.attr("class", "overlay")
	.attr("width", width)
	.attr("height", height)
	.on("mouseover", function() { 
		tooltip.style("display", null);
	})
	.on("mouseout", function() {
		tooltip.style("display", "none");
	})
	.on("mousemove", mousemove);

La fonction mousemove commence par récupérer la position X de la souris puis récupère l'index dans nos données correspondant à cette position grâce à la fonction bisectDate définit précédemment. La donnée complète est donc récupérée avec data[i]. Il ne nous reste plus qu'à déplacer notre tooltip au point de la courbe correspond à la date récupérée. Ensuite nous mettons à jour le contenu du tooltip en utilisant les ids définit lors de sa création.

function mousemove() {
	var x0 = x.invert(d3.mouse(this)[0]),
		i = bisectDate(data, x0),
		d = data[i];
	
	tooltip.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
	
	d3.select('#tooltip-date')
		.text(dateFormat(d.date));
	d3.select('#tooltip-close')
		.text(d.close + "$");
}

Conclusion

Le résultat obtenu n'est peut-être pas aussi élégant que celui de HighCharts (il manque notamment la transition gauche-droite) mais il nous permet de voir le chemin parcouru depuis le premier tutoriel sur les LineChart. Le code est plus précis et nos différentes recherches nous ont permis de réunir plusieurs techniques pour améliorer la qualité de cette visualisation. Enfin n'oubliez pas de récupérer le CSS associé en regardant le code source de la page.

Map - Projection orthographique Voir le tutoriel

comments powered by Disqus