15 - Échelles


Lien vers le tutoriel original : http://alignedleft.com/tutorials/d3/scales


Échelles

Dernière mise à jour le 30 décembre 2012

“Les échelles sont des fonctions qui font correspondre un domaine en entrée à une plage de sortie.”

C’est la définition de Mike Bostock des échelles de D3.

Les valeurs de n’importe quel ensemble de données ont peu de chances de correspondre exactement aux mesures en pixels utilisées dans votre visualisation. Les échelles proposent un moyen pratique de faire correspondre les valeurs de vos données à de nouvelles valeurs utilisables dans des visualisations.

Les échelles de D3 sont des fonctions que vous parametrez. Une fois créée, vous appelez la fonction d’échelle, vous lui passez une valeur, et elle vous retourne gentiment une valeur mise à l’échelle. Vous pouvez définir et utiliser autant d’échelles que vous voulez.

Il pourrait être tentant de penser à une échelle comme à quelque chose apparaissant visuellement sur l’image finale — comme un ensemble de marques indiquant une progression de valeurs. Ne vous faites pas avoir ! Ces marques font partie d’un axe, qui est fondamentalement une représentation visuelle d’une échelle. Une échelle est une relation mathématique, sans représentation visuelle directe. Je vous encourage à penser les échelles et les axes comme deux choses différentes, mais quand même liées.

Cet article s’intéresse seulement aux échelles linéaires, car elles sont les plus courantes et les plus facilement compréhensible. Une fois que vous aurez compris les échelles linéaires, comprendre les autres sera un jeu d’enfants.

Pommes et pixels

Imaginez que l’ensemble de données qui suit représente le nombre de pommes vendues sur un stand de marché chaque mois :

var dataset = [ 100, 200, 300, 400, 500 ];

Bonne nouvelle, le stand vend 100 pommes de plus chaque mois ! Le business est en plein essor. Pour mettre en valeur ce succès, vous voulez faire un graphique à barres illustrant la montée rapide des ventes de pommes, chaque valeur correspondant à la hauteur d’une barre.

Jusqu’à maintenant, on a utilisé les valeurs des données directement dans les valeurs d’affichage, sans s’intéresser aux différences d’unités. Donc si 500 pommes sont vendues, le barre correspondante ferait 500 pixels de haut.

Ça pourrait fonctionner, mais qu’en est-il du prochain mois, lorsque 600 pommes seront vendues ? Et l’année qui suit, lorsque 1 800 pommes seront vendues ? Votre public devra s’acheter des écrans de plus en plus grands, juste pour pouvoir voir la hauteur totale de vos barres de pommes ! (Miam, des barres de pommes !)

C’est là que les échelles entrent en jeu. Comme les pommes ne sont pas des pixels (qui ne sont pas eux-mêmes des oranges), on a besoin d’échelles pour passer de l’un à l’autre.

Domaines et plages

Le domaine d’entrée d’une échelle est l’ensemble des valeurs possibles en entrée. Pour les données de pommes plus haut, un domaine d’entrée approprié serait soit entre 100 et 500 (les valeurs minimum et maximum de notre ensemble de données) soit entre 0 et 500. (la traduction est difficile, mais l’ensemble est à comprendre au sens mathématique, les deux valeurs correspondant à ses bornes : {100 … 500} ou {0 … 500})

La plage de sortie d’une échelle est l’ensemble des valeurs possibles en sortie, le plus souvent utilisées comme valeurs d’affichage en pixels. La plage de sortie dépend entièrement de vous, en tant que designer d’information. Si vous décidez que la plus petite barre doit faire 10 pixels de haut, et que la plus grande doit faire 350 pixels de haut, alors vous pourriez définir une plage de sortie entre 10 et 350.

Par exemple, créez une échelle avec un domaine d’entrée de 100,500 et une plage de sortie de 10,350. Si vous donnez à cette échelle la valeur 100, elle vous retournera 10. Si vous lui donnez 500, elle vous sortira 350. Si vous lui donnez 300, elle vous présentera 180 sur un plateau d’argent. (300 est le centre du domaine d’entrée, et 180 le centre de la plage de sortie.)

On pourrait visualiser le domaine d’entrée et la plage de sortie comme deux axes correspondants, côte-à-côte :

Domaine d'entrée 100 300 500 10 180 350 Plage de sortie

Une dernière chose : Comme il est vraiment facile de mélanger la terminologie domaine d’entrée et plage de sortie, j’aimerais vous proposer un petit exercice. Quand je dis “entrée”, vous dites “domaine”. Puis je dis “sortie” et vous répondez “plage”. Prêt ? Okay :

  • Entrée! Domaine!
  • Sortie! Plage!
  • Entrée! Domaine!
  • Sortie! Plage!

Vous l’avez ? Cool.

Normalisation

Si vous êtes familiés avec le concept de normalisation, il pourrait vous être utile de savoir qu’avec les échelles, c’est tout ce qui ce passe.

La normalisation est le processus qui fait correspondre une valeur numérique à une nouvelle valeur comprise entre 0 et 1, en se basant sur les valeurs minimum et maximum possibles. Par exemple, avec 365 jours dans l’année, le jour 310 correspond à à peu près 0.85, soit 85% de la progression dans l’année.

Avec les échelles linéaires, on laisse juste D3 s’occuper des maths pour le processus de normalisation. La valeur d’entrée est normalisée en accord avec le domaine d’entrée, puis la valeur normalisée est mise à l’échelle par rapport à la plage de sortie.

Créer une échelle

Les générateurs d’échelles de D3 sont accessibles avec d3.scale suivi du type d’échelle que vous souhaitez.

var scale = d3.scale.linear();

Bravo ! Maintenant scale est une fonction à laquelle vous pouvez passer des valeurs en entrée. (Ne soyez pas trompé par le var au-dessus ; rappelez-vous qu’en JavaScript les variables peuvent stocker des fonctions.)

scale(2.5);  // Retourne 2.5

Comme on a pas défini de domaine d’entrée et de plage de sortie, cette fonction fait correspondre les valeurs sur une échelle 1:1. Ce qui veut dire que, quelque soit la valeur en entrée, elle est la même en sortie.

On peut définir le domaine d’entrée à 100,500 en passant ces valeurs dans un tableau à la méthode domain() :

scale.domain([100, 500]);

On définit la plage de sortie d’une manière similaire, avec range() :

scale.range([10, 350]);

Ces étapes peuvent être faites séparément, comme ci-dessus, ou enchaînées en une ligne de code :

var scale = d3.scale.linear()
                    .domain([100, 500])
                    .range([10, 350]);

Quoiqu’il arrive, notre échelle est prête à l’emploi !

scale(100);  // Retourne 10
scale(300);  // Retourne 180
scale(500);  // Retourne 350

Typiquement, vous appellerez les fonctions d’échelle dans une méthode attr() ou similaire, mais jamais toutes seules. Modifions notre nuage de points pour utiliser des échelles dynamiques.

Mettre le nuage de points à l’échelle

Retournons à notre ensemble de données :

var dataset = [
    [5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
    [410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];

Vous vous rappelez que dataset est un tableau de tableaux. On fait correspondre la première valeur de chaque tableau sur l’axe x, et la seconde valeur sur l’axe y. Commençons avec l’axe x.

En survolant les valeurs de x, il semble que les valeurs vont de 5 à 480, donc un domaine d’entrée de 0,500 serait convenable, pas vrai ?

Pourquoi vous me regardez comme ça ? Ah, parceque vous voulez garder un code souple et évolutif (scalable), pour qu’il continue de fonctionner même si nos données changent plus tard. Très intelligent !

Plutôt que de spécifier des valeurs fixes pour le domaine, on utilise des fonctions pour les tableaux min() et max() pour analyser nos données à la volée. Par exemple, ce code itère chaque valeur de x dans nos tableaux et retourne la plus grande valeur :

d3.max(dataset, function(d) {    // Retourne 480
    return d[0];  // Référence la première valeur de chaque sous-tableau
});

En mettant tout ensemble, créons la fonction d’échelle pour notre axe x :

var xScale = d3.scale.linear()
                     .domain([0, d3.max(dataset, function(d) { return d[0]; })])
                     .range([0, w]);

D’abord, notez que je l’ai nommé xScale. Bien sûr, vous pouvez nommer vos variables comme vous le souhaitez, mais un nom comme xScale m’aide à me rappeler ce que cette fonction fait.

Ensuite, notez que j’ai défini le début du domaine d’entrée à zéro. (Vous auriez pu également utiliser min() pour calculer une valeur dynamique.) La fin de notre domaine est définie comme la valeur maximum dans dataset (qui est 480).

Enfin, vous pouvez observer que la plage de sortie est définie de 0 à w, la largeur de notre SVG.

On utilisera un code similaire pour créer la fonction d’échelle de notre axe y :

var yScale = d3.scale.linear()
                     .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                     .range([0, h]);

Notez que la fonction max() référence d[1], la valeur y de chaque sous-tableau. Également, la fin de la plage de sortie est définie comme h (la hauteur du SVG) plutôt que w.

Les fonctions d’échelle sont en place ! Maintenant il nous faut les utiliser. Modifiez juste le code où l’on crée un cercle circle pour chaque valeur de donnée

.attr("cx", function(d) {
    return d[0];
})

pour retourner une valeur mise à l’échelle (à la place de la valeur originale) :

.attr("cx", function(d) {
    return xScale(d[0]);
})

De la même manière, pour l’axe y, ceci

.attr("cy", function(d) {
    return d[1];
})

donne cela :

.attr("cy", function(d) {
    return yScale(d[1]);
})

Changeons également le placement des étiquettes ; ces lignes

.attr("x", function(d) {
    return d[0];
})
.attr("y", function(d) {
    return d[1];
})

deviennent ce qui suit :

.attr("x", function(d) {
    return xScale(d[0]);
})
.attr("y", function(d) {
    return yScale(d[1]);
})

Nous y voilà !

Nuage de points utilisant des échelles en x et y

Voilà le code en fonctionnement. Visuellement, ça ressemble étrangement à notre nuage original ! Et pourtant on a fait plus de progrès que ce qui est apparait.

Affiner le nuage

Vous avez peut-être remarqué que les valeurs de y les plus petites sont en haut du nuage, et les plus grandes en bas. Maintenant que l’on utilise des échelles, il est super facile d’inverser cela, de sorte que les plus grandes valeurs soient en haut, comme vous pourriez vous y attendre. Il suffit juste de changer la plage de sortie de

.range([0, h]);

à

.range([h, 0]);

Nuage de points avec une échelle en y inversée

Voilà le code. Oui, maintenant une valeur d’entrée plus petite qui passe par la fonction d’échelle yScale produira une valeur de sortie plus grande, poussant ces éléments circle et text vers le bas, plus près de la ligne de base de l’image. Je sais, c’est presque trop facile !

Et pourtant certains éléments sont coupés. Définissons une variable de marge padding :

var padding = 20;

Ajoutons-là aux deux échelles. La plage de sortie de xScale était range([0, w]), maintenant elle devient

.range([padding, w - padding]);

Pour yScale on passe de range([h, 0]), à

.range([h - padding, padding]);

Ça devrait nous donner 20 pixels en haut, en bas, à gauche et à droite du SVG. Et ça le fait !

Nuage de points avec des marges

Mais les étiquettes sur le bord droit sont toujours coupés, je double donc la marge droite de xScale :

.range([padding, w - padding * 2]);

Nuage de points avec des marges plus grandes

C’est mieux ! Voilà le code. Mais il y a encore un changement que j’aimerais apporter. Plutôt que de définir le rayon de chaque circle comme la racine carrée de sa valeur y (ce qui était un peu du bricolage, et pas vraiment utile quoi qu’il arrive), pourquoi ne pas définir une nouvelle échelle personnalisée ?

var rScale = d3.scale.linear()
                     .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                     .range([2, 5]);

Puis définir le rayon comme ceci :

.attr("r", function(d) {
    return rScale(d[1]);
});

C’est excitant, car on garantit que notre rayon tombera toujours dans une plage de sortie 2,5. (Ou presque toujours : Jetez un oeil à la référence à clamp() plus bas.) Donc une valeur de 0 (l’entrée minimum) donnera des cercles de rayon 2 (ou d’un diamètre de 4 pixels). La plus grande valeur donnera un cercle de rayon 5 (d’un diamètre de 10 pixels).

Nuage de points avec des rayons mis à l'échelle

Voilà : Notre première échelle utilisée pour une propriété visuelle autre qu’une valeur d’axe.

Enfin, juste au cas où la puissance des échelles ne vous aurait pas encore sauté aux yeux, je vais ajouter un point dans notre ensemble de données : [600, 150]

Nuage de points avec des valeurs plus grandes ajoutées

Boom ! Voilà le code. Notez comment tous les anciens points maintiennent leurs positions relatives, mais ont migré ensemble plus près, plus bas et plus à gauche, pour laisser de la place au nouveau venu.

Et maintenant, une dernière révélation : On peut désormais changer facilement les dimensions de notre SVG, et tout se mettra à l’échelle en conséquence. Ici, j’ai augmenté la valeur de h de 100 à 300 et je n’ai fait aucun autre changement :

Grand nuage de points

Boom, encore ! Voilà le code mis à jour. J’espère qu’en regardant cela vous réalisez : Plus de nuit blanche à cause de votre client qui vous dit que le graphique devrait faire 800 pixels de large plutôt que 600. Oui, vous dormirez plus grâce à moi (et aux méthodes intégrées brillantes de D3). Être bien reposé est un avantage (pour être plus efficace au travail). Vous pourrez me remercier plus tard.

Autres méthodes

d3.scale.linear() a quelques autres méthodes utiles que je vais mentionner rapidement :

  • nice() — Indique à l’échelle de d’arrondir les valeurs en entrée. Je cite le wiki de D3 : “Par exemple, pour un domaine de [0.20147987687960267, 0.996679553296417], le domaine arrondi est [0.2, 1].” C’est utile pour les gens normaux, pour qui lire des nombres comme 0.20147987687960267 est difficile.
  • rangeRound() — Utilisez rangeRound() à la place de range() et toutes les valeurs de sortie seront arrondies au nombre entier le plus proche. C’est utile lorsque vous voulez que vos formes aient des valeurs de pixels exactes, pour éviter d’avoir des bords flous qui pourraient apparaître avec l’anticrénelage (antialiasing).
  • clamp() — Par défaut, une échelle linéaire peut retourner des valeurs en dehors de la plage spécifiée. Par exemple, si vous donnez en entrée une valeur qui dépasse le domaine, l’échelle retournera aussi une valeur qui dépasse la plage de sortie. En appelant .clamp(true) sur une échelle, en revanche, on force les valeurs de sorties à être dans la plage spécifiée. Ce qui veut dire que les valeurs qui dépassent seront arrondies aux valeurs haute et basse de la plage de sortie (celle qui est la plus proche).

Autres échelles

En plus des échelles linéaires linear (dont on a parlé plus haut), D3 possède quelques autres méthodes d’échelle intégrées :

  • identity — Une échelle 1:1, utile principalement pour des valeurs de pixels
  • sqrt — Une échelle racine carrée
  • pow — Une échelle de puissance (exponentielle)
  • log — Une échelle logarithmique
  • quantize — Une échelle avec des valeurs discrètes en sortie, lorsque vous voulez rangez des données dans des “bacs”
  • quantile — Similaire à celle du dessus, mais avec des valeurs discrètes en entrée (lorsque vous avez déjà des “bacs”)
  • ordinal — Les échelles ordinales utilisent des valeurs non quantitatives (par exemple des noms de catégories) en sortie ; parfait pour comparer des pommes et des oranges