Appeler un rappel à la fin d'une transition

98

Je dois faire une méthode FadeOut (similaire à jQuery) en utilisant d3.js . Ce que je dois faire est de définir l'opacité sur 0 à l'aide de transition().

d3.select("#myid").transition().style("opacity", "0");

Le problème est que j'ai besoin d'un rappel pour me rendre compte de la fin de la transition. Comment puis-je implémenter un rappel?

Tony
la source

Réponses:

144

Vous voulez écouter l'événement "fin" de la transition.

// d3 v5
d3.select("#myid").transition().style("opacity","0").on("end", myCallback);

// old way
d3.select("#myid").transition().style("opacity","0").each("end", myCallback);
  • Cette démo utilise l'événement "end" pour enchaîner plusieurs transitions dans l'ordre.
  • L' exemple de beignet livré avec D3 l'utilise également pour enchaîner plusieurs transitions.
  • Voici ma propre démo qui change le style des éléments au début et à la fin de la transition.

De la documentation pour transition.each([type],listener):

Si le type est spécifié, ajoute un écouteur pour les événements de transition, prenant en charge les événements de «début» et de «fin». L'auditeur sera appelé pour chaque élément individuel de la transition, même si la transition a un délai et une durée constants. L'événement de démarrage peut être utilisé pour déclencher un changement instantané lorsque chaque élément commence la transition. L'événement de fin peut être utilisé pour lancer des transitions à plusieurs étapes en sélectionnant l'élément actuel this, et en dérivant une nouvelle transition. Toutes les transitions créées lors de l'événement de fin hériteront de l'ID de transition actuel et ne remplaceront donc pas une transition plus récente qui était précédemment planifiée.

Voir ce fil de discussion sur le sujet pour plus de détails.

Enfin, notez que si vous souhaitez simplement supprimer les éléments après leur disparition (une fois la transition terminée), vous pouvez utiliser transition.remove().

Phrogz
la source
7
Merci beaucoup. C'est une GRANDE bibliothèque, mais il n'est pas si facile de trouver les informations importantes dans la documentation.
Tony
9
Donc, mon problème avec cette façon de continuer à partir de la fin de la transition est qu'elle exécute votre fonction N fois (pour N éléments dans l'ensemble des éléments de transition). C'est parfois loin d'être idéal.
Steven Lu
2
J'ai le même problème. Je souhaite qu'il exécute la fonction une fois après la dernière suppression
canyon289
1
Comment effectuer un rappel uniquement après la fin de toutes les transitions pour un d3.selectAll()(au lieu de cela après la fin de chaque élément)? En d'autres termes, je veux juste rappeler une fonction une fois que tous les éléments ont terminé la transition.
hobbes3
Salut, le premier lien vers le graphique à barres de pile / groupe pointe vers un cahier observable qui n'utilise aucun .eachécouteur d'événement, ni l' "end"événement. Il ne semble pas «enchaîner» les transitions. Le deuxième lien pointe vers un github qui ne se charge pas pour moi.
The Red Pea
65

La solution de Mike Bostock pour la v3 avec une petite mise à jour:

  function endall(transition, callback) { 
    if (typeof callback !== "function") throw new Error("Wrong callback in endall");
    if (transition.size() === 0) { callback() }
    var n = 0; 
    transition 
        .each(function() { ++n; }) 
        .each("end", function() { if (!--n) callback.apply(this, arguments); }); 
  } 

  d3.selectAll("g").transition().call(endall, function() { console.log("all done") });
kashesandr
la source
5
Si la sélection ne contient aucun élément, le rappel ne se déclenchera jamais. Une façon de résoudre ce problème estif (transition.size() === 0) { callback(); }
hughes
1
if (! callback) callback = function () {}; pourquoi ne pas revenir instantanément ou lever une exception? Un rappel invalide va à l'encontre de tout le but de cette rutine, pourquoi le faire comme un horloger aveugle? :)
prizma
1
@kashesandr on ne peut simplement rien faire, puisque l'utilisateur ressentira le même effet: (pas d'appel de rappel à la fin de la transition) function endall(transition, callback){ if(!callback) return; // ... } ou, puisque c'est très certainement une erreur d'appeler cette fonction sans callback, lancer une exception être la manière appropriée de gérer la situation Je pense que cette affaire n'a pas besoin d'exception trop compliquée function endall(transition, callback){ if(!callback) throw "Missing callback argument!"; // .. }
prizma
1
Donc, lorsque nous avons des transitions séparées enter()et que nous exit()voulons attendre la fin des trois, nous devons mettre du code dans le rappel pour nous assurer qu'il a été invoqué trois fois, non? D3 est tellement désordonné! J'aimerais avoir choisi une autre bibliothèque.
Michael Scheper
1
Je devrais ajouter que je me rends compte que votre réponse résout certains des problèmes sur lesquels je me plaignais et que je peux écrire une fonction utilitaire pour l'appliquer. Mais je n'ai pas trouvé de moyen élégant de l'appliquer et de toujours permettre une personnalisation supplémentaire pour chaque transition, en particulier lorsque les transitions pour les nouvelles et anciennes données sont différentes. Je suis sûr que je vais trouver quelque chose, mais «invoquer ce rappel lorsque toutes ces transitions sont terminées» semble être un cas d'utilisation qui devrait être pris en charge dès le départ, dans une bibliothèque aussi mature que D3. Il semble donc que j'ai choisi la mauvaise bibliothèque - pas vraiment la faute de D3. Anyhoo, merci pour votre aide.
Michael Scheper
44

Désormais, dans d3 v4.0, il existe une fonction permettant d'attacher explicitement des gestionnaires d'événements aux transitions:

https://github.com/d3/d3-transition#transition_on

Pour exécuter du code lorsqu'une transition est terminée, tout ce dont vous avez besoin est:

d3.select("#myid").transition().style("opacity", "0").on("end", myCallback);
ericsoco
la source
Belle. Les gestionnaires d'événements sont dégoûtants.
KFunk
Il existe également transition.remove()( link ), qui gère un cas d'utilisation courant de transition d'un élément de la vue: `` Pour chaque élément sélectionné, supprime l'élément à la fin de la transition, tant que l'élément n'a pas d'autres transitions actives ou en attente. Si le l'élément a d'autres transitions actives ou en attente, ne fait rien. "
brichins
9
Il semble que cela s'appelle l'élément PER auquel la transition est appliquée, ce qui n'est pas ce à quoi la question est posée d'après ma compréhension.
Taylor
10

Une approche légèrement différente qui fonctionne également lorsqu'il y a de nombreuses transitions avec de nombreux éléments exécutés chacun simultanément:

var transitions = 0;

d3.select("#myid").transition().style("opacity","0").each( "start", function() {
        transitions++;
    }).each( "end", function() {
        if( --transitions === 0 ) {
            callbackWhenAllIsDone();
        }
    });
Jesper nous
la source
Merci, cela a bien fonctionné pour moi. J'essayais de personnaliser automatiquement l'orientation de l'étiquette sur l'axe des x après avoir chargé un histogramme discret. La personnalisation ne peut pas prendre effet avant le chargement, et cela a fourni un hook d'événement à travers lequel je pouvais le faire.
whitestryder
6

Ce qui suit est une autre version de la solution de Mike Bostock et inspirée du commentaire de @hughes à la réponse de @ kashesandr. Il effectue un seul rappel à transitionla fin.

Étant donné une dropfonction ...

function drop(n, args, callback) {
    for (var i = 0; i < args.length - n; ++i) args[i] = args[i + n];
    args.length = args.length - n;
    callback.apply(this, args);
}

... nous pouvons étendre d3comme ça:

d3.transition.prototype.end = function(callback, delayIfEmpty) {
    var f = callback, 
        delay = delayIfEmpty,
        transition = this;

    drop(2, arguments, function() {
        var args = arguments;
        if (!transition.size() && (delay || delay === 0)) { // if empty
            d3.timer(function() {
                f.apply(transition, args);
                return true;
            }, typeof(delay) === "number" ? delay : 0);
        } else {                                            // else Mike Bostock's routine
            var n = 0; 
            transition.each(function() { ++n; }) 
                .each("end", function() { 
                    if (!--n) f.apply(transition, args); 
                });
        }
    });

    return transition;
}

En tant que JSFiddle .

Utilisez transition.end(callback[, delayIfEmpty[, arguments...]]):

transition.end(function() {
    console.log("all done");
});

... ou avec un délai optionnel si transitionest vide:

transition.end(function() {
    console.log("all done");
}, 1000);

... ou avec des callbackarguments optionnels :

transition.end(function(x) {
    console.log("all done " + x);
}, 1000, "with callback arguments");

d3.transition.endappliquera le passé callbackmême avec un vide transition si le nombre de millisecondes est spécifié ou si le deuxième argument est vrai. Cela transmettra également tous les arguments supplémentaires au callback(et uniquement ces arguments). Surtout, cela n'appliquera pas par défaut le callbackif transitionest vide, ce qui est probablement une hypothèse plus sûre dans un tel cas.

milos
la source
C'est sympa, j'aime ça.
kashesandr
1
Merci @kashesandr. Cela a en effet été inspiré par votre réponse au départ!
milos
ne pensez pas vraiment que nous avons besoin d'une fonction de suppression ou de passage d'arguments, car le même effet peut être obtenu par une fonction wrapper ou en utilisant bind. Sinon je pense que c'est une excellente solution +1
Ahmed Masud
Fonctionne comme un charme!
Benoît Sauvère
Voir cette réponse, .end () a maintenant été officiellement ajouté - stackoverflow.com/a/57796240/228369
chrismarx
5

Depuis D3 v5.8.0 +, il existe désormais un moyen officiel de le faire en utilisant transition.end . Les documents sont ici:

https://github.com/d3/d3-transition#transition_end

Un exemple de travail de Bostock est ici:

https://observablehq.com/@d3/transition-end

Et l'idée de base est que rien qu'en ajoutant .end(), la transition renverra une promesse qui ne se résoudra pas tant que tous les éléments n'auront pas terminé la transition:

 await d3.selectAll("circle").transition()
      .duration(1000)
      .ease(d3.easeBounce)
      .attr("fill", "yellow")
      .attr("cx", r)
    .end();

Pour en savoir plus, consultez les notes de version de la version:

https://github.com/d3/d3/releases/tag/v5.8.0

chrismarx
la source
1
C'est une très belle façon de gérer les choses. Je dirai simplement que pour ceux d'entre vous comme moi qui ne connaissent pas tout la v5 et qui aimeraient implémenter cela, vous pouvez importer la nouvelle bibliothèque de transition en utilisant <script src = " d3js.org/d3-transition.v1 .min.js "> </ script >
DGill
0

La solution de Mike Bostock améliorée par kashesandr + passant des arguments à la fonction de rappel:

function d3_transition_endall(transition, callback, arguments) {
    if (!callback) callback = function(){};
    if (transition.size() === 0) {
        callback(arguments);
    }

    var n = 0;
    transition
        .each(function() {
            ++n;
        })
        .each("end", function() {
            if (!--n) callback.apply(this, arguments);
    });
}

function callback_function(arguments) {
        console.log("all done");
        console.log(arguments);
}

d3.selectAll("g").transition()
    .call(d3_transition_endall, callback_function, "some arguments");
int_ua
la source
-2

En fait, il existe une autre façon de faire cela en utilisant des minuteries.

var timer = null,
    timerFunc = function () {
      doSomethingAfterTransitionEnds();
    };

transition
  .each("end", function() {
    clearTimeout(timer);
    timer = setTimeout(timerFunc, 100);
  });
ifadey
la source
-2

J'ai résolu un problème similaire en définissant une durée sur les transitions à l'aide d'une variable. Ensuite, setTimeout()j'appelais la fonction suivante. Dans mon cas, je voulais un léger chevauchement entre la transition et le prochain appel, comme vous le verrez dans mon exemple:

var transitionDuration = 400;

selectedItems.transition().duration(transitionDuration).style("opacity", .5);

setTimeout(function () {
  sortControl.forceSort();
}, (transitionDuration * 0.75)); 
Brett
la source