Invoque um retorno de chamada no final de uma transição

98

Eu preciso fazer um método FadeOut (semelhante ao jQuery) usando D3.js . O que preciso fazer é definir a opacidade para 0 usando transition().

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

O problema é que preciso de um retorno de chamada para saber quando a transição for concluída. Como posso implementar um retorno de chamada?

Tony
fonte

Respostas:

144

Você deseja ouvir o evento de "fim" da transição.

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

// old way
d3.select("#myid").transition().style("opacity","0").each("end", myCallback);

Da documentação para transition.each([type],listener):

Se o tipo for especificado, adiciona um ouvinte para eventos de transição, suportando eventos de "início" e "término". O ouvinte será chamado para cada elemento individual na transição, mesmo se a transição tiver um atraso e uma duração constantes. O evento de início pode ser usado para acionar uma mudança instantânea conforme cada elemento começa a fazer a transição. O evento final pode ser usado para iniciar transições de vários estágios selecionando o elemento atual this, e derivando uma nova transição. Todas as transições criadas durante o evento de término herdarão o ID de transição atual e, portanto, não substituirão uma transição mais recente que foi agendada anteriormente.

Veja este tópico do fórum sobre o tópico para mais detalhes.

Finalmente, observe que se você quiser apenas remover os elementos depois que eles desaparecerem (após o término da transição), você pode usar transition.remove().

Phrogz
fonte
7
Muito obrigado. Esta é uma GRANDE GRANDE biblioteca, mas não é tão fácil encontrar as informações importantes na documentação.
Tony
9
Então, meu problema com essa maneira de continuar desde o final da transição é que ela executa sua função N vezes (para N itens no conjunto de elementos em transição). Isso às vezes está longe de ser ideal.
Steven Lu,
2
Eu tenho o mesmo problema. Gostaria que executasse a função uma vez após a última remoção
canyon289
1
Como você executa um retorno de chamada somente depois que todas as transições terminadas para um d3.selectAll()(em vez de depois que cada elemento termina)? Em outras palavras, quero apenas retornar uma função de retorno quando todos os elementos terminarem a transição.
hobbes3 de
Olá, o primeiro link para empilhar / agrupar o gráfico de barras aponta para um bloco de notas Observável que não usa nenhum .eachouvinte de evento, nem o "end"evento. Não parece "encadear" as transições. O segundo link aponta para um github que não carrega para mim.
The Red Pea
65

Solução de Mike Bostock para v3 com uma pequena atualização:

  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
fonte
5
Se a seleção contiver zero elementos, o retorno de chamada nunca será disparado. Uma maneira de corrigir isso éif (transition.size() === 0) { callback(); }
hughes
1
if (! retorno de chamada) retorno de chamada = função () {}; por que não retornar instantaneamente ou lançar uma exceção? Um retorno de chamada inválido anula todo o propósito desta rotina, por que continuar com isso como um relojoeiro cego? :)
prizma
1
@kashesandr pode-se simplesmente não fazer nada, já que o usuário experimentará o mesmo efeito: (nenhuma chamada de retorno de chamada no final da transição) function endall(transition, callback){ if(!callback) return; // ... } ou, visto que é certamente um erro chamar esta função sem um retorno de chamada, lançando uma exceção parece seja a maneira apropriada de lidar com a situação Eu acho que este caso não precisa de muito complicado Exceção function endall(transition, callback){ if(!callback) throw "Missing callback argument!"; // .. }
prizma
1
Então, quando temos separações enter()e exit()transições e queremos esperar até que as três tenham terminado, precisamos colocar o código no retorno de chamada para ter certeza de que ele foi chamado três vezes, certo? D3 é tão bagunçado! Eu gostaria de ter escolhido outra biblioteca.
Michael Scheper,
1
Devo acrescentar que sei que sua resposta resolve alguns dos problemas que reclamei e posso escrever uma função de utilidade para aplicá-la. Mas não encontrei uma maneira elegante de aplicá-lo e ainda permitir personalização adicional para cada transição, especialmente quando as transições para dados novos e antigos são diferentes. Tenho certeza de que vou inventar algo, mas 'invocar este retorno de chamada quando todas essas transições terminarem' parece um caso de uso que deve ser suportado imediatamente, em uma biblioteca tão madura quanto D3. Portanto, parece que escolhi a biblioteca errada - realmente não é culpa da D3. Anyhoo, obrigado por sua ajuda.
Michael Scheper
44

Agora, no d3 v4.0, há um recurso para anexar explicitamente manipuladores de eventos às transições:

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

Para executar o código quando uma transição for concluída, tudo que você precisa é:

d3.select("#myid").transition().style("opacity", "0").on("end", myCallback);
Ericsoco
fonte
Lindo. Os manipuladores de eventos são grosseiros.
KFunk
Há também transition.remove()( link ), que lida com um caso de uso comum de transição de um elemento da visualização: `" Para cada elemento selecionado, remove o elemento quando a transição termina, desde que o elemento não tenha outras transições ativas ou pendentes. elemento tem outras transições ativas ou pendentes, não faz nada. "
brichins
9
Parece que isso é chamado de elemento PER ao qual a transição é aplicada, que não é o que a pergunta é em relação ao meu entendimento.
Taylor C. White
10

Uma abordagem um pouco diferente que funciona também quando há muitas transições com muitos elementos, cada uma funcionando simultaneamente:

var transitions = 0;

d3.select("#myid").transition().style("opacity","0").each( "start", function() {
        transitions++;
    }).each( "end", function() {
        if( --transitions === 0 ) {
            callbackWhenAllIsDone();
        }
    });
Jesper We
fonte
Obrigado, funcionou muito bem para mim. Eu estava tentando personalizar a orientação do rótulo do eixo x automaticamente depois de carregar um gráfico de barras discreto. A personalização não pode ter efeito antes do carregamento e isso forneceu um gancho de evento por meio do qual eu poderia fazer isso.
Whitestryder
6

A seguir está outra versão da solução de Mike Bostock e inspirada no comentário de @hughes à resposta de @kashesandr. Ele faz um único retorno de chamada ao transitionfinalizar.

Dada uma dropfunção ...

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 estender d3assim:

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 um JSFiddle .

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

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

... ou com um atraso opcional se transitionestiver vazio:

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

... ou com callbackargumentos opcionais :

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

d3.transition.endaplicará o passado callbackmesmo com um vazio transition se o número de milissegundos for especificado ou se o segundo argumento for verdadeiro. Isso também encaminhará quaisquer argumentos adicionais para o callback(e apenas esses argumentos). É importante notar que, por padrão , isso não se aplicará a callbackif transitionestá vazio, o que provavelmente é uma suposição mais segura nesse caso.

milos
fonte
Isso é bom, eu gosto.
kashesandr de
1
Obrigado @kashesandr. Isso foi realmente inspirado por sua resposta para começar!
milos de
realmente não acho que precisamos de uma função drop ou passagem de argumentos, uma vez que o mesmo efeito pode ser obtido por uma função wrapper ou utilizando bind. Caso contrário, acho que é uma ótima solução +1
Ahmed Masud
Funciona como um encanto !
Benoît Sauvère
Veja esta resposta, .end () agora foi adicionado oficialmente - stackoverflow.com/a/57796240/228369
chrismarx
5

A partir do D3 v5.8.0 +, agora existe uma maneira oficial de fazer isso usando transition.end. Os documentos estão aqui:

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

Um exemplo prático de Bostock está aqui:

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

E a ideia básica é que apenas acrescentando .end(), a transição retornará uma promessa que não será resolvida até que todos os elementos tenham concluído a transição:

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

Consulte as notas de lançamento da versão para ainda mais:

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

chrismarx
fonte
1
Esta é uma maneira muito boa de lidar com as coisas. Vou apenas dizer, para aqueles de vocês como eu que não sabem tudo sobre a v5 e gostariam de implementar apenas isso, você pode importar a nova biblioteca de transição usando <script src = " d3js.org/d3-transition.v1 .min.js "> </ script >
DGill
0

A solução de Mike Bostock melhorada por kashesandr + passando argumentos para a função de retorno de chamada:

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

Na verdade, há mais uma maneira de fazer isso usando temporizadores.

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

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

Resolvi um problema semelhante definindo uma duração nas transições usando uma variável. Então eu costumava setTimeout()chamar a próxima função. No meu caso, eu queria uma ligeira sobreposição entre a transição e a próxima chamada, como você verá no meu exemplo:

var transitionDuration = 400;

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

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