Les nuages de mots tout le monde connait, on les a vu fleurir un peu partout sur Internet il y a quelques années maintenant. Ils sont souvent utilisés pour leur esthétisme uniquement et dans ces cas-là parfaitement inutiles. Néanmoins cette représentation de données peut être très intéressante pour rendre compte de "quoi on parle" dans un ensemble de mots ou dans un texte. Associé à une chronologie elle permet de visualiser directement l'évolution ou les tendances d'un site Internet par exemple. La représentation que vous voyez ci-dessus n'est composée que d'éléments HTML, ce qui signifie qu'un moteur de recherche va indexer son contenu. Ce tutoriel utilise la librairie développée par Jason Davis que vous pouvez récupérer sur Github. Nous verrons par l'exemple à quoi correspond les différents paramétrages qu'elle peut offrir et les questions qu'il faut se poser pour construire un nuage de mot.
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é.
La réalisation d'un nuage de mot se décompose en trois étapes qu'on retrouve assez souvent lorsqu'on projette des données sur une représentation particulière. On commence par initialiser différentes variables. Ensuite on fait appel à la représentation (layout) pour qu'elle calcule les meilleurs choix (positionnement des mots afin d'éviter le chevauchement) puis on construit le SVG avec les informations fournies.
Nous commençons donc par initialiser un certain nombre de paramètres. Si la largeur et la hauteur sont assez habituelles, il faudra attacher une importance toute particulière à la police de caractères et à sa taille. Le premier peut changer la nature de votre nuage de mot, lui donner une apparence moderne avec une police bien choisie ou au contraire le rendre un peu vieillot. La taille par contre est l'élément essentiel qui assure la bonne construction de la représentation, nous verrons d'ailleurs que la librairie possède un défaut quelque peu gênant dans un contexte de réutilisation.
const width = document.getElementById("container").offsetWidth * 0.95,
height = 500,
fontFamily = "Open Sans",
fontScale = d3.scaleLinear().range([20, 120]), // Construction d'une échelle linéaire continue qui va d'une font de 20px à 120px
fillScale = d3.scaleOrdinal(d3.schemeCategory10); // Construction d'une échelle discrète composée de 10 couleurs différentes
C'est uniquement ici que nous faisons appel à la librarie d3-cloud. Nous lui fournissons notre tableau de mots (qui doit contenir pour chaque entrée une variable text
et une variable
size
) ainsi que les différents paramètres que nous avons initialisés. Ce ne sera peut-être pas le cas pour vous mais dans notre contexte les données proviennent d'un fichier CSV que
nous devons parser.
d3.csv("/tutorials/d3js/word-cloud/wordsCount2016.csv").then(function(csv) {
var words = [];
csv.forEach(function(e,i) {
words.push({"text": e.LABEL, "size": +e.COUNT});
});
words.length = 100; // Nous ne voulons que les 100 premiers mots
// Calcul du domain d'entrée de notre fontScale
// L'objectif est que la plus petite occurence d'un mot soit associée à une font de 20px
// La plus grande occurence d'un mot est associée à une font de 120px
let minSize = d3.min(words, d => d.size);
let maxSize = d3.max(words, d => d.size);
// Nous projettons le domaine [plus_petite_occurence, plus_grande_occurence] vers le range [20, 120]
// Ainsi les mots les moins fréquents seront plus petits et les plus fréquents plus grands
fontScale.domain([minSize, maxSize]);
d3.layout.cloud()
.size([width, height])
.words(words)
.padding(1)
.rotate(function() {
return ~~(Math.random() * 2) * 45;
})
.spiral("rectangular")
.font(fontFamily)
.fontSize(d => fontScale(d.size))
.on("end", draw)
.start();
// La méthode draw sera définie ici
});
Nous étudierons les différents paramètres un peu plus bas. Notez juste que le code ne fait qu'initialiser des paramètres et c'est l'appel de la méthode start
qui déclenche le traitement.
Avant de définir la méthode draw
qui est appelée à la fin du traitement regardons le travail réalisé par la librairie. Grâce à la console de Chrome par exemple nous pouvons observer le
contenu d'un élément du tableau avant et après le traitement.
Contenu du tableau | Explication |
---|---|
Initialisation réalisée par nos soins du texte et de son nombre d'occurrences | |
Après traitement la librairie a enrichie notre tableau de nombreuses variables. On retrouve d'abord notre font , le padding spécifié à la construction du
layout et une size de 120px ce qui correspond bien à la valeur la plus grande de notre fontScale (Facebook étant le mot avec le plus d'occurrences). Parmi les autres variables ajoutées considérons x et y qui correspondent à la position du mot dans le SVG et rotate qui défini une rotation de ce texte.
Les valeurs de x et y sont négatives car la position est calculée depuis le centre du SVG (nous devrons donc centrer le contenu de notre SVG). Les autres variables
ne sont pas utiles dans notre contexte mais servent en interne à la librairie pour réaliser le positionnement de tous les mots.
|
Tout le travail est déjà fait, il nous suffit de prendre notre tableau pour ajouter autant d'éléments text
qu'il contient en utilisant les paramètres de chaque entrée. Le code doit être
défini dans une méthode draw
et dans notre cas positionner à l'intérieur du traitement du CSV.
function draw() {
d3.select("#word-cloud").append("svg") // Ajout d'un élément SVG sur un DIV existant de la page
.attr("class", "svg")
.attr("width", width)
.attr("height", height)
.append("g") // Ajout du groupe qui contiendra tout les mots
.attr("transform", "translate(" + width / 2 + ", " + height / 2 + ")") // Centrage du groupe
.selectAll("text")
.data(words)
.enter().append("text") // Ajout de chaque mot avec ses propriétés
.style("font-size", d => d.size + "px")
.style("font-family", fontFamily)
.style("fill", d => fillScale(d.size))
.attr("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text);
}
Le résultat obtenu par ce code est visible dans l'image ci-dessous. Notez que chaque exécution du layout peut engendrer un positionnement différent des mots.
Dans cette partie nous allons passer en revue les différents paramètres de la librairie pour obtenir le nuage de mots que vous souhaitez.
En CSS, le padding
correspond aux marges internes d'un élément HTML, il est exprimé en pixels. Dans le contexte d'un nuage de mots chaque mot est représenté par un rectangle. On fournit
à la librarie la font-size
ainsi que la font-family
car elle doit calculer la taille de chaque rectangle. Afin d'espacer les mots entre eux on ajoute donc du padding
.
Il est logique que la librarie prenne connaissance de ce paramètre puisqu'il changera la façon d'arranger les mots comme il agrandit la taille des rectangles. On peut observer dans les images ci-dessous
le résultat de différentes valeurs de ce paramètre.
padding = 0 | padding = 2 | padding = 5 |
---|---|---|
Le padding s'exerce des quatre côtés du mot, pour cette raison de petites valeurs éloignent assez rapidement les mots.
Ce paramètre n'est pas défini par une valeur statique mais par une fonction qui retourne un angle. Dans l'exemple de départ cette fonction retournait ~~(Math.random() * 2) * 45
.
Le code mérite d'être analysé. La fonction Math.random
renvoie un nombre flottant pseudo-aléatoire compris dans l'intervalle [0, 1[ (ce qui signifie que 0 est compris dans
l'intervalle mais que 1 en est exclu) . En le multipliant par 2 on obtient donc un nombre aléatoire dans l'intervalle [0, 2[. L'opérateur ~~
est un raccourci de la fonction
Math.floor
qui permet d'obtenir un arrondi. Les deux valeurs possibles sont donc 0 ou 1. En la multipliant par 45 on obtient donc un angle de 0° ou de 45°. Voici différentes
alternatives pour cette fonction.
Tous les mots sur une ligne return 0; |
Avec un angle de 0°, 45° ou 90° return ~~(Math.random() * 3) * 45; |
Avec un angle compris entre 0° et 90° return ~~(Math.random() * 91); |
---|---|---|
Ce paramètre est également défini par une fonction. Elle représente le type de spirale qui sera utilisée pour positionner les mots. Deux fonctions sont prédéfinis : "rectangular" et "archimedean". Il est possible de définir manuellement cette fonction sous la forme d'une formule mathématique mais c'est très vite complexe et pas forcément pertinent.
"rectangular" | "archimedean" |
---|---|
Lorsqu'on lit des tutoriaux sur Internet, les détails problématiques sont souvent masqués pour ne présenter que la partie positive. Dans le cas d'un nuage de mots et de l'utilisation de cette librairie il y a pourtant deux points très importants à considérer.
Sauf si on vous donne une liste de mots avec leur nombre d'occurrence directement vous allez devoir la constituer vous-même. Nous concernant nous avons récupéré tous les titres des articles du site
Rue89 mais avant d'obtenir notre liste de mots nous avons dû réaliser un certains nombres d'opérations. La première consiste à supprimer tout ce qui n'est pas un mot, les virgules, les guillemets...
Pour cela nous avons utilisé la fonction replaceAll
en Java et le code suivant : wordsText.replaceAll("[^a-zA-Z]", " ");
qui remplace tout ce qui n'est pas constitué des
caractères de l'alphabet par un espace. Ensuite vous vous rendrez assez vite compte qu'il y a des mots qui ne sont pas intéressants comme "le" ou "tous". A ce niveau se pose la question d'éliminer
tous les mots qui ne font pas sens dans le contexte qui est le vôtre. Pour vous donner une idée voici la liste de tous les mots que nous avons supprimés.
public static String[] STOP_LIST = {"au", "aux", "avec", "bien", "ce", "celui", "ces", "ci", "cm", "comme", "dans", "de", "des", "du", "elle", "en", "et", "eux", "il", "je",
"la", "le", "leur", "lui", "ma", "mais", "me", "meme", "mes", "moi", "mm", "mon", "ne", "nos", "notre", "nous", "on", "ou", "plus", "par", "pas",
"pour", "qu", "que", "qui", "sa", "se", "ses", "son", "sur", "ta", "te", "tes", "toi", "ton", "tout", "tu", "un", "une", "vos", "votre", "voir",
"vous", "ceci", "cela", "cet", "cette", "ici", "ils", "les", "leurs", "quel", "quels", "quelle", "quelles", "sans", "soi", "est", "tres", "lot", "ref",
"fait", "ans", "ai", "rue", "si", "pourquoi", "sont", "faire", "etre", "ont", "comment", "ca", "apres", "mois", "veut", "euros", "suis", "deux", "peut",
"va", "quand", "entre", "chez", "tous", "quoi", "ou", "ete", "non", "aussi", "faut", "trop", "sous", "etait", "contre", "dit", "autres", "rien", "trois",
"encore", "avoir", "toujours", "moins", "vraiment", "tres", "avant", "font", "jour", "histoire", "bon", "dix", "cinq", "grand", "passe", "face", "petit",
"retour", "peu", "jamais", "questions", "mieux", "tour", "the", "temps", "avez", "autre", "selon", "dire", "bonne", "heures", "enfin", "fautil", "tete",
"ni", "quatre", "meme", "voici", "france", "met", "fin", "mis", "vu", "deja", "veulent", "eu", "an", "existe", "ex", "super", "jeu", "prix", "donne",
"tres", "fin", "mere", "nuit", "petite", "doit", "avait", "aime", "envie", "devenu", "village", "livre", "allez", "payer", "re", "ur", "soir", "sort",
"porte", "pied", "lire", "nom", "sait", "etes", "puis", "fois", "sera", "petits", "sort", "toute", "autour", "appris", "demande", "mettre", "aller",
"six", "amis", "reste", "fois", "merci", "depuis", "derriere", "parle", "vue", "jour", "gens", "idees", "ville", "jours", "of", "hui", "prend", "marche",
"devient", "tre", "decouvrez", "jeux", "devenir", "annee", "joue"};
En regardant le nuage généré au début de ce tutoriel on peut vite se rendre compte que certains mots sont passés dans les mailles du filet comme "dix", "mieux" ou "autres". Il n'est pas évident de déterminer cette liste de mots et si votre ensemble de mots est dynamique ça vous prendra du temps pour arriver à la produire.
Il y a un paramètre que nous n'avons pas trop détaillé qui est la font-size
choisie. Dans la section paramétrage nous utilisons une font-size entre 15px et 60px. Comment avons-nous déterminé
ces valeurs ? La réponse est simple : en tâtonnant. Nous avons choisi la limite inférieure à 15px pour que les mots ne soient pas trop petits mais pour la limite supérieure il nous a fallu tester
plusieurs fois. Pour vous donner une idée du problème voici le nuage de mots dans un SVG qui fait 300px en largeur et 300px en hauteur.
Le mot qui possède le plus d'occurrences (FACEBOOK) est absent ainsi que de nombreux autres mots. Si la librairie ne peut pas faire rentrer votre mot elle va simplement l'ignorer. Le pire est qu'à
chaque exécution le positionnement de vos mots sera différent (vous pouvez le vérifier en rafraichissant plusieurs fois cette page et en regardant le nuage de mot tout en haut). Il est donc possible
qu'à un moment tous les mots soient présents mais qu'il en manque un dans l'exécution suivante ! Sur Github un ticket a été
ouvert pour discuter de ce problème et plusieurs solutions sont proposées. Aucune n'est vraiment complète. Pourtant vous pouvez faire le test avec notre nuage de mot en haut de cette page il possèdera
toujours les 100 mots de notre tableau d'entré. C'est simplement parce que nous avons mis en place un code qui relance le layout
autant de fois que nécessaire et qu'à chaque fois on diminue
la font-size
supérieure jusqu'à ce que tous les mots soient présents. C'est aussi ce qui explique que le nuage de mots met un certain temps à apparaitre. Voici le code qui permet de réaliser
ce traitement.
d3.csv("word-cloud/wordsCount2016.csv").then(function(csv) {
var words = [];
csv.forEach(function(e,i) {
words.push({"text": e.LABEL, "size": +e.COUNT});
});
words.length = 100;
// Nous essayons de déterminer la font-size maximale que nous pouvons utiliser.
// Pour cela nous plaçons dans la page le mot avec le plus d'occurence et récupérons la taille qu'il occupe en pixels avec une font de "500px"
// Voir le code source et le DIV avec un id = test-width associé à un CSS portant le même nom
// Grâce à une règle de trois et l'utilisation de la largeur de notre SVG nous obtenons une maxFontSize qui garantie que le mot passera dans le SVG.
var testDiv = document.getElementById("test-width");
testDiv.innerHTML = words[0].text;
var testWidth = testDiv.clientWidth;
var maxFontSize = width * 500 / testWidth;
let minSize = d3.min(words, d => d.size);
let maxSize = d3.max(words, d => d.size);
computeAndDraw(words, maxFontSize);
function computeAndDraw(tmp_words, max_font_size) { // Nous allons apeller cette fonction tant que tous les mots ne sont pas présents en sortie
let fontScale = d3.scaleLinear()
.domain([minSize, maxSize])
.range([10, max_font_size]);
d3.layout.cloud()
.size([width, height])
.words(tmp_words)
.padding(1)
.rotate(function() {
return ~~(Math.random() * 2) * 45;
})
.spiral("rectangular")
.font(fontFamily)
.fontSize(function(d) { return fontScale(d.size); })
.on("end", function(output) {
// Le code intéressant se situe ici. Nous vérifions si l'output possède bien tous les mots.
// Si c'est le cas nous apellons la fonction draw sinon nous rapellons computeAndDraw en diminuant max_font_size de 5px
// A noter qu'il est nécessaire de reconstruire le tableau d'entré sinon ça ne fonctionne pas
if (output.length !== words.length) {
var tmp_words = [];
csv.forEach(function(e,i) {
tmp_words.push({"text": e.LABEL, "size": +e.COUNT});
});
tmp_words.length = 100;
computeAndDraw(tmp_words, max_font_size - 5);
} else {
draw(output);
}
})
.start();
function draw(output) {
d3.select("#word-cloud").append("svg")
.attr("class", "svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + ", " + height / 2 + ")")
.selectAll("text")
.data(output)
.enter().append("text")
.style("font-size", d => d.size + "px")
.style("font-family", fontFamily)
.style("fill", d => fillScale(d.size))
.attr("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text);
}
}
});
Ce code est particulièrement utile si votre tableau d'entré varie et que vous voulez utiliser le même code pour l'afficher. Impossible pour vous de connaître le nombre maximal d'occurrence et donc de procéder à tâton. Néanmoins si vous voulez mettre en place un nuage de mots fixe nous vous conseillons plutôt de procéder par ajustements successifs et de trouver une valeur suffisamment basse pour la font-size maximale afin que tous les mots soient toujours présents après plusieurs itérations. Cela garantira que vous nuage s'affiche rapidement.
La mise en oeuvre d'un nuage de mots est facile et ne présente aucune difficulté en soi. Néanmoins nous avons vu qu'il faut préparer correctement ses données et bien connaître les limites de la librairie de Jason Davis. Comme toujours la section commentaire vous permet d'aborder des sujets que nous aurions pu oublier.
VOUS POURRIEZ AIMER
COMMENTAIRES