D3JS - Bulles (Bubble Chart)

Comment réaliser une visualisation sous forme de bulle
d3js7.x
Sources :

Introduction

Ce tutoriel présente une visualisation hiérarchique de la population française par département mais aussi par région. En passant la souris sur un département sa population apparait sous forme de tooltip. Il est également possible de voir la population d'une région en passant la souris sur le bleu foncé du cercle la représentant ou même du pays tout entier en la passant sur le plus gros cercle bleu clair. Les données proviennent d'un fichier JSON que nous avons nous même réalisé à partir des données de population par département : population.json.

Les données

Cette visualisation étant hiérarchique, les données le sont également :

{
	"name": "France",
	"children": [
			{
			"name": "Auvergne-Rhône-Alpes",
			"children": [
				{"name": "Ain", "population": 614331},
				{"name": "Allier", "population": 353124},
				{"name": "Ardèche", "population": 324885},
				{"name": "Cantal", "population": 154135},
				{"name": "Drôme", "population": 499313},
				{"name": "Isère", "population": 1233759},
				{"name": "Loire", "population": 766729},
				{"name": "Haute-Loire", "population": 231877},
				{"name": "Puy-de-Dôme", "population": 649643},
				{"name": "Rhône", "population": 1756069},
				{"name": "Savoie", "population": 428751},
				{"name": "Haute-Savoie", "population": 760979}
			]
		}, ...
	]
}

Nous partons d'un nœud racine, ici la France, puis nous en décrivons les fils à commencer par la région Auvergne-Rhône-Alpes. Pour chaque région nous listons à nouveau ses fils et cette fois-ci nous donnons une valeur numérique à ces nœuds fils, en l'occurence la population du département.

Initialisation

Le SVG construit dans ce tutoriel est un cercle, nous définissons donc uniquement un diamètre pour le construire.

const radius = 700;

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

Nous commençons par ajouter un groupe légèrement décalé pour éviter les problèmes de bordure aux extrémités du plus grand cercle.

const g = svg.append("g").attr("transform", "translate(2,2)");

Construction de la hiérarchie

Nous construisons ensuite notre Layout de type pack (il en existe bien d'autres pour représenter de manière hiérarchique des données : d3-hierarchy). La taille définie est légèrement inférieure à celle déclarée précédemment (toujours pour les mêmes raisons) et un padding (l'espacement souhaité entre les cercles) est ajouté.

const packLayout = d3.pack()
	.size([radius - 4, radius - 4]) // lié à la translation précédente
	.padding(1.5);

Le reste du code réalise le traitement du fichier JSON et la construction des cercles, textes et tooltip.

d3.json("/tutorials/d3js/bubblechart/population.json").then(function(data) {
	// Le reste du code va ici
});

Notre fichier est bien formé pour constituer une hiérarchie au sens ou D3JS l'entend, c'est-à-dire qu'il possède un nœud racine (et un seul), que ce nœud racine possède un tableau de nœuds fils dénommé children et que les feuilles se terminent par un libellé et une valeur numérique. Il est bien sûr possible de définir ce type de données à partir de n'importe qu'elle autre en écrivant une fonction de conversion. La construction de la hiérarchie consiste simplement à définir la fonction sum (pour calculer la taille de chaque cercle) et donc à mentionner l'attribut population. La fonction sort permet l'organisation des cercles à l'intérieur d'un cercle plus grand.

let root = d3.hierarchy(data)
	.sum(d => d.population)
	.sort((a, b) => b.value - a.value);

Nous pouvons maintenant appliquer notre layout à ce noeud racine.

root = packLayout(root).descendants();

Après l'exécution de ce code les nœuds sont enrichis de plusieurs attributs. En fait cette fonction va réaliser la répartition des nœuds (attributs x,y pour la position, r pour le diamètre en pixels) de manière à ce qu'il n'y est aucun chevauchement et en tenant compte du diamètre global que nous avons fourni à l'initialisation de notre objet pack. Il y aussi un attribut indiquant la profondeur à laquelle se trouve le nœud (depth) et une référence vers le nœud parent. Ce qui est génial, c'est que maintenant il ne nous reste plus qu'à prendre ces informations pour construire les cercles et ajouter textes et tooltips. Voici un exemple pour le département Paris.

{
	data: Object { name="Paris",  population=2268265},
	depth: 2,
	height: 0,
	parent: // Object, lien vers le parent
	r: 43.19412010935609,
	value: 2268265,
	x: 323.7945204077783,
	y: 285.23659422163723
}

Il ne nous reste plus qu'à utiliser ces données pour construire la visualisation. Nous coommençons par construire un groupe associé à chaque nœud. Il contiendra un cercle et un texte. On lui associe une classe CSS différente si c'est une feuille.

var node = g.selectAll(".node")
	.data(root)
	.enter().append("g")
		.attr("class", d => d.children ? "node" : "leaf node")
		.attr("transform", d => "translate(" + d.x + "," + d.y + ")");

Ainsi, pour les cercles et les tooltips, ces lignes suffisent. Nous récupérons simplement le rayon de notre objet enrichi. Noter que nous ne définissons pas de coordonnées pour le cercle, celui-ci, comme les objets qui vont suivre, est contenu dans un nœud g que nous avons déjà placé au bon endroit. Le tooltip renvoi le nom du département ou de la région et sa population.

node.append("circle")
	.attr("r", d => d.r);
	
node.append("title")
	.text(d => d.data.name + "\n" + format(d.value));

Il y a une petite subtilité pour le texte contenu dans les cercles car nous réalisons un substring pour éviter qu'il ne déborde du cercle. Celui-ci est lié au diamètre et doit être testé en fonction de la police de caractère (le texte n'est défini que pour les nœuds feuilles et qui ne possèdent donc pas d'enfant).

node.filter(d => !d.children)
	.append("text")
	.attr("dy", "0.3em")
	.text(d => d.data.name.substring(0, d.r / 3));

Voici le CSS utilisé :

circle {
	fill: rgb(31, 119, 180);
	fill-opacity: .25;
	stroke: rgb(31, 119, 180);
	stroke-width: 1px;
}

.leaf circle {
	fill: #ff7f0e;
	fill-opacity: 1;
}

text {
	font: 10px sans-serif;
	text-anchor: middle;
	cursor: pointer; cursor: hand
}

Conclusion

Nous avons donné un maximum d'explications pour ce tutoriel mais le code javascript ne fait que 37 lignes (en comptant les espaces). N'oubliez pas de regarder le code source pour être sûr qu'il ne manque rien (principalement pour le CSS). Il est aussi important de debugger le code javascript pour comprendre ce qui se passe et enfin de regarder la structure HTML qui est générée.

COMMENTAIRES

Yoh


Simple précision : ceci n'est pas un Bubble Chart, c'est un Circle Packing ;)
Un Bubble Chart propose un repère x,y orthonormé, et pour chaque donnée une valeur z représentée par le rayon ou diamètre d'une bulle et/ou un gradient de couleur.
Bubble Chart : https://datavizcatalogue.co...
Circle Packing : https://datavizcatalogue.co...