Invocar una devolución de llamada al final de una transición

98

Necesito hacer un método FadeOut (similar a jQuery) usando D3.js . Lo que tengo que hacer es establecer la opacidad en 0 usando transition().

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

El problema es que necesito una devolución de llamada para darme cuenta de cuándo ha finalizado la transición. ¿Cómo puedo implementar una devolución de llamada?

Tony
fuente

Respuestas:

144

Quieres escuchar el evento "final" de la transición.

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

// old way
d3.select("#myid").transition().style("opacity","0").each("end", myCallback);
  • Esta demostración utiliza el evento "end" para encadenar muchas transiciones en orden.
  • El ejemplo de donut que se envía con D3 también usa esto para encadenar múltiples transiciones.
  • Aquí está mi propia demostración que cambia el estilo de los elementos al principio y al final de la transición.

De la documentación para transition.each([type],listener):

Si se especifica el tipo , agrega un detector para eventos de transición, que admite eventos de "inicio" y "finalización". Se invocará al oyente para cada elemento individual de la transición, incluso si la transición tiene un retraso y una duración constantes. El evento de inicio se puede utilizar para activar un cambio instantáneo a medida que cada elemento comienza a realizar la transición. El evento final se puede utilizar para iniciar transiciones de varias etapas seleccionando el elemento actual thisy derivando una nueva transición. Cualquier transición creada durante el evento final heredará el ID de transición actual y, por lo tanto, no anulará una transición más nueva que se programó previamente.

Ver este hilo del foro sobre el tema para obtener más detalles.

Finalmente, tenga en cuenta que si solo desea eliminar los elementos después de que se hayan desvanecido (una vez finalizada la transición), puede usar transition.remove().

Phrogz
fuente
7
Muchas gracias. Esta es una GRAN GRAN biblioteca, pero no es tan fácil encontrar la información importante en la documentación.
Tony
9
Entonces, mi problema con esta forma de continuar desde el final de la transición es que ejecuta su función N veces (para N elementos en el conjunto de elementos de transición). A veces, esto está lejos de ser ideal.
Steven Lu
2
Tengo el mismo problema. Ojalá ejecutara la función una vez después de la última eliminación
canyon289
1
¿Cómo se realiza una devolución de llamada solo después de que todas las transiciones hayan terminado para d3.selectAll()(en lugar de eso, después de que finalice cada elemento)? En otras palabras, solo quiero volver a llamar a una función una vez que todos los elementos terminen de hacer la transición.
hobbes3
Hola, el primer enlace para apilar / agrupar el gráfico de barras apunta a un cuaderno observable que no usa ningún .eachdetector de eventos, ni el "end"evento. No parece "encadenar" las transiciones. El segundo enlace apunta a un github que no se carga para mí.
The Red Pea
65

Solución de Mike Bostock para v3 con una pequeña actualización:

  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
fuente
5
Si la selección contiene cero elementos, la devolución de llamada nunca se activará. Una forma de solucionar esto esif (transition.size() === 0) { callback(); }
Hughes
1
if (! callback) callback = function () {}; ¿Por qué no regresar instantáneamente o lanzar una excepción? Una devolución de llamada inválida anula todo el propósito de esta rutina, ¿por qué seguir adelante como un relojero ciego? :)
prizma
1
@kashesandr uno simplemente no puede hacer nada, ya que el usuario experimentará el mismo efecto: (no hay llamada de devolución de llamada al final de la transición) function endall(transition, callback){ if(!callback) return; // ... } o, dado que es más seguro un error llamar a esta función sin una devolución de llamada, arrojar una excepción a ser la forma adecuada de manejar la situación Creo que este caso no necesita una excepción demasiado complicada function endall(transition, callback){ if(!callback) throw "Missing callback argument!"; // .. }
prizma
1
Entonces, cuando tenemos transiciones enter()y separadas exit(), y queremos esperar hasta que las tres hayan terminado, debemos poner código en la devolución de llamada para asegurarnos de que se haya invocado tres veces, ¿verdad? ¡D3 es tan desordenado! Ojalá hubiera elegido otra biblioteca.
Michael Scheper
1
Debo agregar, me doy cuenta de que su respuesta resuelve algunos de los problemas por los que me quejé, y puedo escribir una función de utilidad para aplicarla. Pero no he encontrado una forma elegante de aplicarlo y aún así permitir una personalización adicional para cada transición, especialmente cuando las transiciones para datos nuevos y antiguos son diferentes. Estoy seguro de que se me ocurrirá algo, pero 'invocar esta devolución de llamada cuando todas estas transiciones hayan terminado' parece un caso de uso que debería ser compatible desde el primer momento, en una biblioteca tan madura como D3. Así que parece que he elegido la biblioteca incorrecta, no es realmente culpa de D3. De todos modos, gracias por tu ayuda.
Michael Scheper
44

Ahora, en d3 v4.0, hay una función para adjuntar explícitamente controladores de eventos a las transiciones:

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

Para ejecutar código cuando se completa una transición, todo lo que necesita es:

d3.select("#myid").transition().style("opacity", "0").on("end", myCallback);
ericsoco
fuente
Hermoso. Los controladores de eventos son asquerosos.
KFunk
También hay transition.remove()( enlace ), que maneja un caso de uso común de transición de un elemento de la vista: `" Para cada elemento seleccionado, elimina el elemento cuando finaliza la transición, siempre que el elemento no tenga otras transiciones activas o pendientes. Si el elemento tiene otras transiciones activas o pendientes, no hace nada. "
brichins
9
Parece que esto se llama elemento PER al que se aplica la transición, que no es a lo que se refiere la pregunta, según tengo entendido.
Taylor C. White
10

Un enfoque ligeramente diferente que funciona también cuando hay muchas transiciones con muchos elementos, cada uno ejecutándose simultáneamente:

var transitions = 0;

d3.select("#myid").transition().style("opacity","0").each( "start", function() {
        transitions++;
    }).each( "end", function() {
        if( --transitions === 0 ) {
            callbackWhenAllIsDone();
        }
    });
Jesper Nosotros
fuente
Gracias, eso funcionó muy bien para mí. Estaba intentando personalizar la orientación de la etiqueta del eje x automáticamente después de cargar un gráfico de barras discretas. La personalización no puede tener efecto antes de la carga, y esto proporcionó un enlace de eventos a través del cual pude hacer esto.
whitestryder
6

La siguiente es otra versión de la solución de Mike Bostock e inspirada en el comentario de @hughes a la respuesta de @ kashesandr. Hace una única llamada atransition final.

Dada una dropfunción ...

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);
}

... podemos extendernos d3así:

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;
}

Como JSFiddle .

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

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

... o con un retraso opcional si transitionestá vacío:

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

... o con callbackargumentos opcionales :

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

d3.transition.endaplicará el pasado callbackincluso con un vacío transition si se especifica el número de milisegundos o si el segundo argumento es verdadero. Esto también enviará cualquier argumento adicional al callback(y solo esos argumentos). Es importante destacar que esto no aplicará de forma predeterminadacallback si transitionestá vacío, lo que probablemente sea una suposición más segura en tal caso.

milos
fuente
Eso es lindo, me gusta.
kashesandr
1
Gracias @kashesandr. ¡Esto fue realmente inspirado por tu respuesta para empezar!
milos
realmente no creo que necesitemos una función de eliminación o pasar argumentos, ya que el mismo efecto se puede lograr mediante una función contenedora o utilizando bind. De lo contrario, creo que es una gran solución +1
Ahmed Masud
Funciona de maravilla !
Benoît Sauvère
Ver esta respuesta, .end () se ha añadido ahora oficialmente - stackoverflow.com/a/57796240/228369
chrismarx
5

A partir de D3 v5.8.0 +, ahora hay una forma oficial de hacer esto usando transition.end . Los documentos están aquí:

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

Un ejemplo práctico de Bostock está aquí:

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

Y la idea básica es que con solo agregar .end(), la transición devolverá una promesa que no se resolverá hasta que todos los elementos hayan terminado de hacer la transición:

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

Consulte las notas de la versión para obtener más información:

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

Chrismarx
fuente
1
Esta es una forma muy agradable de manejar las cosas. Solo diré, para aquellos de ustedes como yo que no conocen todo sobre v5 y les gustaría implementar solo esto, pueden importar la nueva biblioteca de transición usando <script src = " d3js.org/d3-transition.v1 .min.js "> </ script >
DGill
0

La solución de Mike Bostock fue mejorada por kashesandr + pasando argumentos a la función de devolución de llamada:

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
fuente
-2

En realidad, hay una forma más de hacer esto usando temporizadores.

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

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

Resolví un problema similar estableciendo una duración en las transiciones usando una variable. Entonces solía setTimeout()llamar a la siguiente función. En mi caso, quería una ligera superposición entre la transición y la siguiente llamada, como verá en mi ejemplo:

var transitionDuration = 400;

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

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