D3JS permet de réaliser des transitions sur un ou plusieurs attributs d'un objet SVG. Une transition représente la modification de la valeur
d'un attribut ou plusieurs attributs sur un objet existant. Elle doit être mathématique pour permettre à la librairie de trouver les valeurs
intermédiaires permettant de représenter cette transition. Ainsi dans notre 1er exemple, D3JS calcule lui-même toutes les valeurs de cx
entre sa valeur d'origine (150) et la valeur finale (850). Les transitions peuvent être appliquées sur tout type d'attribut et sur le CSS,
elles peuvent être combinées, s'enchaîner, durer plus ou moins longtemps et paramétrées pour être (par exemple) rapides au début et lentes à
la fin. Enfin sachez qu'une transition doit partir d'une situation existante, le cercle de notre premier exemple doit exister même si son
rayon peut être égal à 0 comme son opacité.
Ce premier exemple montre la transition d'un cercle d'une position cx
égale à 150 vers une nouvelle position égale à 850. Nous
introduisons au passage la fonction duration
exprimée en millisecondes qui permet de préciser la durée de la transition.
var circleMove = svg.append("circle")
.attr("cx",150)
.attr("cy",50)
.attr("r",30);
circleMove.transition()
.duration(2000)
.attr("cx", 850);
Ici nous faisons varier la couleur via l'attribut fill
du rouge vers le bleu.
var circleColor = svg.append("circle")
.attr("cx",150)
.attr("cy",50)
.attr("r",30)
.attr("fill", "red");
circleColor.transition()
.duration(2000)
.attr("fill", "blue");
Un nouvel exemple pour jouer cette fois-ci sur le style CSS du cercle et le faire disparaitre.
var circleOpacity = svg.append("circle")
.attr("cx",150)
.attr("cy",50)
.attr("r",30);
circleOpacity.transition()
.duration(2000)
.style("opacity", "0");
Ce dernier exemple combine toutes les modifications vues précédemment en une seule. Il est bien sûr possible de faire varier plusieurs attributs en même temps.
var circleAllCombine = svg.append("circle")
.attr("cx",150)
.attr("cy",50)
.attr("r",30)
.attr("fill", "red");
circleAllCombine.transition()
.duration(2000)
.attr("cx", 850)
.attr("fill", "blue")
.style("opacity", "0.2");
Nous l'avons déjà vu une transition peut durer plus ou moins longtemps grâce à la fonction duration
. Une autre fonction utile
permet de définir un délai avant son démarrage, la fonction delay
.
circleDurationDelay1.transition()
.duration(2000)
.attr("x", 750);
circleDurationDelay2.transition()
.duration(4000)
.attr("x", 750);
circleDurationDelay3.transition()
.delay(2000)
.duration(2000)
.attr("x", 750);
Si vous souhaitez matérialiser le fait qu'une transition soit en cours, vous pouvez par exemple modifier son opacité. Dans l'exemple suivant nous déplaçons notre cercle après un délai de 750ms. Dès que la transition commence (start) nous modifions son opacité et dès qu'elle se termine (end) nous la remettons à sa valeur initiale.
circleMove.transition()
.duration(2000)
.delay(750)
.attr("cx", 850)
.each('start',function(){ d3.select(this).style("opacity", "0.2"); })
.each('end', function(){ d3.select(this).style("opacity", "1"); });
Il existe plusieurs façons d'enchainer les transitions sur un même objet. La plus simple est d'utiliser la fonction transition
en
cascade comme dans l'exemple suivant.
circleMove
.transition()
.duration(500)
.attr("cx", 850)
.transition()
.duration(500)
.attr("cx",150)
.transition()
.duration(500)
.attr("cx",650)
.transition()
.duration(500)
.attr("cx",350)
.transition()
.duration(500)
.attr("cx",500);
Si le nombre de transitions devient important, il peut être plus élégant et lisible d'utiliser une fonction récursive couplée avec l'évènement de fin de transition comme dans cet exemple.
var positions = [850, 200, 800, 250, 750, 300, 700, 350, 650, 400, 600, 450, 550, 500];
function animateMulti(node, positions, i) {
node.transition()
.duration(300)
.attr("cx", positions[i])
.on('end', function() {
if (i < (positions.length - 1)) {
animateMulti(d3.select(this), positions, ++i);
}
});
}
animateMulti(circleMultiTransition, positions, 0);
Nous utilisons ici le principe de récursion : la fonction animateMulti
s'appelle elle-même tant que la condition de la ligne
7 reste vrai, c'est-à-dire tant qu'il y a des index à traiter dans le tableau initial. Bien sûr rien ne vous empêche dans votre tableau
de stocker plusieurs informations à la fois et donc de faire varier plusieurs attributs.
var variations = [
{
"cx" : 850,
"fill" : "red"
}, {
"cx": 200,
"fill" : "black"
} // ...
];
Vous avez dû noter depuis le début de ces exemples que chaque transition est lente au début, d'une vitesse moyenne au milieu et à nouveau
lente à la fin. Cette façon de faire varier la vitesse de la transition est celle par défaut (nommé cubic-in-out
) et il est
possible de la paramétrer avec la fonction ease
. Vous pouvez cliquer sur les cercles dans l'exemple suivant pour voir les
différents paramètres que cette fonction peut prendre. La première transition (linear
) se définit ainsi :
d3.select(this)
.transition()
.duration(2000)
.ease(d3.easeLinear)
.attr("cx", 850);
Il faut reconnaître que les trois dernières sont plutôt amusantes. Il est possible d'aller un peu plus loin avec cette fonction
(par exemple en l'inversant grâce au suffixe -out
, ainsi pour inverser la fonction exponentielle il faut utiliser "exp-out").
Si besoin jetez un oeil à la documentation (en anglais) :
Spécifications de la fonction ease ou posez vos
questions dans la section commentaire.
Supposons un instant que vous soyez fan de la lettre Z, vous ne supportez pas qu'un texte ne contienne pas que des Z, nous allons vous montrer comment assouvir cette passion. Dans notre premier exemple, le cercle se déplace d'une position cx = 150 vers une position cy = 850. D3JS réalise lui-même l'interpolation, il calcule 151, 152... pour déplacer le cercle. Pour notre texte nous souhaitons que toutes les lettres se "déplacent" vers Z, c'est-à-dire que si ma lettre est E, alors nous souhaitons qu'elle prenne les valeurs F, G, H... jusqu'à Z. Pour commencer construisons le texte que vous voyez un peu plus bas.
var alphabet = svg.append("g");
for (var i = 0; i < 26; ++i) {
alphabet.append("text")
.attr("x", 200 + i * 25)
.attr("y", 50)
.attr("font-family", "Courier")
.attr("font-size", "40px")
.text(String.fromCharCode(65 + Math.random() * 26));
}
Ce code génère aléatoirement 26 objets SVG de type text
contenant chacun une lettre au hasard. Notez que notre lettre est obtenue
par la fonction String.fromCharCode()
qui convertit un code ASCII en une lettre. Ainsi 65 représente le A et 90 le Z. Si nous
effectuons une transition standard le résultat ne sera pas satisfaisant.
alphabetWrong.selectAll('text')
.transition()
.ease(d3.easeLinear)
.duration(10000)
.text('Z');
En effet, D3JS ne sait pas nativement interpoler des lettres. Il faut que nous lui indiquions que ces lettres peuvent être réprésentées par
des nombres qui eux-mêmes peuvent être interpolés. Pour cela nous devons introduire la fonction tween
qui définit la façon
dont elle se déroule. Nous commençons par récupérer la valeur numérique de notre lettre à la ligne 6, ainsi pour la lettre E cela donne 69.
A la ligne suivante nous construisons une fonction d'interpolation (arrondie) entre 69 et 90 (ce serait différent pour une autre lettre, ainsi
pour la lettre Y, l'interpolation se ferait entre 89 et 90, c'est pour cette raison que nous traitons lettre par lettre). C'est tout, maintenant
D3JS est capable de remplir le vide entre ces deux valeurs. La ligne 8 retourne la fonction qu'il va utiliser, cette fonction convertie les
valeurs 70, 71... jusque 90. On se sert du résultat pour définir la nouvelle lettre au fur et à mesure toujours grâce à la fonction String.fromCharCode()
.
Pour information t
est un nombre réel compris entre 0 et 1.
alphabet.selectAll('text')
.transition()
.ease(d3.easeLinear)
.duration(10000)
.tween('text', function() {
var currentValue = this.textContent.charCodeAt(0);
var i = d3.interpolateRound(currentValue, 90); // 90 = Z
return function(t) {
this.textContent = String.fromCharCode(i(t));
};
});
L'important dans cet exemple est que nous sommes parties d'une situation ou l'interpolation était impossible (entre des lettres) et que nous avons trouvé une représentation qui la rendait possible. Sachez qu'il existe des fonctions toutes prêtes pour réaliser divers types d'interpolations : Liste des interpolations.
Cette dernière partie présente comment les mécanismes de transitions peuvent être utilisés sur autre chose que des cercles. Avec tout ce que nous avons vu précédemment vous comprenez que vous n'êtes limités par rien, il vous suffit d'imaginer une transition complexe puis de la découper au fur et à mesure en transitions plus simples pour pouvoir la coder en piochant dans les exemples précédents.
Nous n'allons pas réétudier ici la façon de les construire mais juste présenter comment y intégrer des transitions. Avant d'obtenir
le graphique ci-dessous nous avons créé les domaines xBar
et yBar
. Ensuite nous générons des données aléatoires
et créons 4 rectangles. Voici le premier code permettant de réaliser une transition sur un Bar Chart.
var data = getRandomData(4, 150);
xBar.domain(data.map(d => d.name));
svg.selectAll("rect")
.data(data)
.transition()
.duration(2000)
.attr("y", d => yBar(d.value))
.attr("height", d => 200 - yBar(d.value));
Le code est assez simple et la sélection ressemble à ce que nous avons déjà vu pour les lettres. A la première ligne nous générons de nouvelles
données ce qui nous oblige à la suivante à recalculer notre domaine (les bornes min et max ont probablement évolué). Comme dans les autres
exemples nous faisons varier les attributs de notre choix dans une transition de 2 secondes, ici y
pour placer la barre en bas
et height
pour modifier sa hauteur. L'important ici est de voir que l'on associe notre nouveau jeu de données à nos rectangles
après avoir fait un selectAll
.
Ce second exemple réalise une transition intermédiaire qui vise à mettre toutes les colonnes à 0 avant de définir une nouvelle valeur.
var data = getRandomData(4, 150);
xBar.domain(data.map(d => d.name));
svg.selectAll("rect")
.data(data)
.transition()
.duration(1000)
.attr("y", 200)
.attr("height", 0)
.transition()
.duration(1000)
.attr("y", d => yBar(d.value))
.attr("height", d => 200 - yBar(d.value));
La question que vous pouvez vous poser sur ces deux exemples est la suivante : Quelle est l'animation la plus utile ? Dans la première les bars s'agrandissent ou se réduisent en fonction de leur nouvelle hauteur ce qui permet de voir une évolution d'années en années par exemple. En revanche dans le second exemple toutes les bars sont réduites à 0 puis leur nouvelle valeur est définie ce qui rend plus difficile la comparaison visuelle.
Le principe reste identique pour les Line Chart, nous avons préalablement construit xLine
et yLine
ainsi
que la fonction line
. Pour animer notre graphique, nous générons des données aléatoires, les domaines des axes X et
Y sont recalculés. La transition consiste à mettre à jour le path existant. On lui associe les nouvelles données et on appelle la
fonction line
.
var data = getRandomData(100, 150);
xLine.domain(data.map(d => d.name));
yLine.domain(d3.extent(data, d => d.value));
linePath.datum(data)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("d", line);
Nous continuons avec ce second exemple qui construit le path au fur et à mesure. Le code complet est cette fois-ci présentée, tout est
fait en une seule fois car nous partons d'une situation vide. Si vous exécutez le code jusqu'à la ligne 17 la graphique sera affichée sans
aucune animation. Le code qui suit est plus subtile que les précédents et pas évident à comprendre. La ligne 20 permet d'obtenir la longueur
de la ligne en pixel. Ensuite, nous modifions deux attributs sur cette ligne, stroke-dasharray
et stroke-dashoffset
.
Le premier détermine la longueur de nos pointillés et nous le définissons à la longueur de la ligne. Le second permet de déplacer le début
de la ligne que nous représentons. L'animation consiste à ramener ce dernier attribut à 0 ce qui permet de faire apparaître la ligne au fur
et à mesure.
var xLine = d3.scaleBand()
.range([0, 800])
.padding(0.1);
var yLine = d3.scaleLinear()
.range([100, 0]);
var line = d3.line()
.x(d => xLine(d.name))
.y(d => yLine(d.value));
var data = getRandomData(100, 150);
xLine.domain(data.map(d => d.name));
yLine.domain(d3.extent(data, d => d.value));
var linePath = svg.append("path")
.datum(data)
.attr("d", line)
.style("fill", "none")
.style("stroke", "#3498db")
.style("stroke-width", "1px")
.attr("transform", "translate(150, 0)");
var linePathLength = linePath.node().getTotalLength(); // LIGNE 20
linePath
.attr("stroke-dasharray", linePathLength)
.attr("stroke-dashoffset", linePathLength)
.transition()
.duration(4000)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
Pour réaliser des transitions sur des Area Chart, nous reprenons presque le même code que pour les Linear Chart : génération de nouvelles données, mise à jour des domaines puis transition.
var data = getRandomData(25, 100);
xArea.domain(data.map(d => d.name));
yArea.domain(d3.extent(data, d => d.value));
areaPath.datum(data)
.transition()
.duration(2000)
.attr("d", area);
Nous n'avons pas trouvé comme faire apparaître le graphique de la gauche vers la droite comme nous l'avons fait pour les lignes.
En revanche, nous pouvons le faire apparaître du bas vers le haut avec le code suivant. Il suffit de définir areaStart
et area
pour faire une transition ou toutes les valeurs sont au départ à 0 vers nos données aléatoires.
var data = getRandomData(25, 100);
var xArea = d3.scaleBand()
.range([0, 800])
.padding(0.1);
var yArea = d3.scaleLinear()
.range([100, 0]);
xArea.domain(data.map(d => d.name));
yArea.domain(d3.extent(data, d => d.value));
var areaStart = d3.area()
.x(d => xArea(d.name))
.y0(100)
.y1(100);
var area = d3.area()
.x(d => xArea(d.name))
.y0(100)
.y1(d => yArea(d.value));
var areaPath = svg.append("path")
.datum(data)
.attr("class", "area")
.attr("transform", "translate(150, 0)")
.attr("d", areaStart)
.transition()
.duration(2000)
.attr("d", area);
Souvenez-vous que pour toute transition il faut partir d'une situation existante, même si l'objet n'est pas visible à l'écran il doit exister. Il n'est pas possible de partir de rien et de faire une transition vers une situation. Si vous voulez faire apparaître un objet il faudra créer une situation de départ invisible (taille de cercle à 0, hauteur à 0...) avant de faire votre transition vers la situation finale.
Nous terminons ici ce tutoriel. Bien sûr nous n'avons pas étudié tous les objets sur lesquels il est possible de faire des transitions mais si vous rencontrez des difficultés sur les vôtres n'hésitez pas à les partager dans la section commentaire.
COMMENTAIRES
jean-manuel Meny
Bonjour
Une question sur la fonction animateMulti.
Une boucle semble fonctionner également (code ci-dessous).
Y a-t-il un avantage à ce choix récursif plutôt que le choix d'une boucle ?
merci d'avance
<html lang="fr">
<head>
<meta charset="utf-8">
<title> cercle </title>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<svg width="1000px" height="300px" id="figure">
</svg>
<script>
var fig = d3.select("#figure");
var cercle = fig.append("circle")
.attr("cx",150)
.attr("cy",50)
.attr("r",30)
.attr("fill", "red");
var positions = [850, 200, 800, 250, 750, 300, 700, 350, 650, 400, 600, 450, 550, 500];
for(var i = 0; i < positions.length; i += 1)
{
cercle = cercle.transition()
.duration(3000)
.attr("cx", positions[i]);
}
</script>
</body>
</html>
ericfrigot
Bonjour,
Je ne suis pas sûr de savoir pourquoi cela fonctionne mais il y a bien une différence. Dans votre exemple, l'ensemble des transitions est créé dès le début alors qu'avec la récursion il faut attendre la fin de la première transition avant de créé la seconde. Cela fonctionne peut-être car votre exemple est simple mais il est possible qu'une boucle for créé des situations non souhaitées dans un cas plus complexe. Si vous maitrisez l'anglais, la question mériterait d'être posée sur stackoverflow pour bien comprendre pourquoi cela fonctionne quand même. Eric