Событие end у перехода

Механизм переходов в D3.js построен вокруг управляемого изменения атрибутов и стилей элементов во времени. Каждый переход представляет собой отдельный объект состояния, который отслеживает прогресс анимации, интерполяцию значений и набор событий жизненного цикла. Одним из ключевых событий этого цикла является событие завершения перехода end, фиксирующее момент, когда анимация конкретного элемента достигла финального состояния.

Переход в D3.js проходит несколько стадий:

  • инициализация перехода (создание объекта transition)
  • вычисление начальных и конечных значений
  • интерполяция значений на каждом тикe таймера
  • завершение анимации
  • вызов события end

Событие end связано не с группой элементов целиком, а с каждым отдельным DOM-элементом, участвующим в переходе. Это означает, что завершение фиксируется индивидуально, даже если переход применяется к набору элементов через выборку.

Базовая регистрация обработчика end

Обработчик события завершения перехода привязывается через метод .on("end", callback):

d3.selectAll("circle")
  .transition()
  .duration(1000)
  .attr("r", 20)
  .on("end", function(event, d) {
    console.log("Переход завершён для элемента:", this);
  });

В момент вызова:

  • this указывает на DOM-элемент
  • event содержит объект события D3
  • d — связанные данные элемента (если использовался data join)

Ключевая особенность заключается в том, что callback вызывается после завершения конкретного перехода у конкретного элемента, а не после завершения всей выборки.

Индивидуальность завершения для элементов выборки

При применении перехода к набору элементов каждый элемент может завершить анимацию в разные моменты времени (особенно при использовании задержек delay или различной длительности через функции).

d3.selectAll("rect")
  .transition()
  .delay((d, i) => i * 100)
  .duration(500)
  .attr("height", 100)
  .on("end", function() {
    console.log("Готово:", this);
  });

Здесь событие end срабатывает последовательно, по мере завершения анимации каждого прямоугольника.

Отличие end от interrupt

Событие завершения перехода не срабатывает при принудительном прерывании анимации. Если переход был остановлен новым переходом или методом interrupt, вместо end может быть вызвано событие interrupt.

d3.select("circle")
  .transition()
  .duration(2000)
  .attr("r", 50)
  .on("interrupt", function() {
    console.log("Переход прерван");
  });

Это различие критично при построении логики, зависящей от гарантированного завершения анимации.

Последовательные переходы и цепочки завершения

При построении цепочек переходов событие end относится только к текущему переходу в цепи. Новый переход создаёт новый жизненный цикл.

d3.select("circle")
  .transition()
  .duration(500)
  .attr("r", 30)
  .on("end", function() {
    d3.select(this)
      .transition()
      .duration(500)
      .attr("fill", "red");
  });

В этом случае первый end запускает второй переход, но не означает завершение всей цепочки визуальных изменений.

Координация нескольких элементов через end

Событие end часто используется для синхронизации логики, когда необходимо определить момент завершения всей группы переходов. Поскольку событие вызывается для каждого элемента отдельно, требуется механизм подсчёта завершённых элементов.

let total = 0;
let completed = 0;

const circles = d3.selectAll("circle");
total = circles.size();

circles
  .transition()
  .duration(800)
  .attr("cx", 300)
  .on("end", function() {
    completed += 1;

    if (completed === total) {
      console.log("Все элементы завершили переход");
    }
  });

Такой подход компенсирует отсутствие встроенного события «end для всей группы».

Поведение при повторных переходах

Если на элемент назначается новый переход до завершения предыдущего, предыдущий переход прерывается, и его end не вызывается. Это создаёт важный эффект:

  • активен всегда только последний переход
  • промежуточные переходы теряют управление жизненным циклом
  • логика end должна учитывать возможную отмену
const sel = d3.select("rect");

sel.transition()
  .duration(1000)
  .attr("width", 200);

sel.transition()
  .duration(500)
  .attr("width", 50);

В данном случае второй переход перезаписывает первый, и событие end первого не будет вызвано.

Использование event в контексте end

Объект события, передаваемый в обработчик, содержит информацию о текущем переходе и позволяет различать источники вызова при сложной анимационной логике:

d3.selectAll("circle")
  .transition()
  .attr("r", 10)
  .on("end", function(event, d) {
    console.log(event.type);
    console.log(d);
  });

Хотя объект события в D3 менее насыщен, чем в стандартных DOM-событиях, он сохраняет минимально необходимый контекст для отладки и управления потоком анимации.

Последовательность end при вложенных transition

При вложенных переходах каждый уровень имеет собственный end, и они не пересекаются:

d3.select("circle")
  .transition()
  .attr("r", 20)
  .on("end", function() {
    d3.select(this)
      .transition()
      .attr("r", 40)
      .on("end", function() {
        console.log("второй переход завершён");
      });
  });

Здесь фиксируются два независимых события завершения, каждое из которых относится к своему переходу.

Особенности тайминга и точности завершения

Событие end вызывается после завершения интерполяции всех атрибутов и стилей, участвующих в переходе. Однако фактический момент вызова зависит от таймера D3 и может не совпадать с системным временем с абсолютной точностью.

При большом количестве элементов или сложных интерполяциях возможна небольшая задержка между фактическим окончанием анимации и вызовом обработчика.

Поведение end при нулевой длительности

Если переход имеет нулевую или неопределённую длительность, событие end вызывается практически мгновенно после применения начальных изменений:

d3.select("circle")
  .transition()
  .duration(0)
  .attr("r", 100)
  .on("end", function() {
    console.log("мгновенное завершение");
  });

В этом случае переход теряет анимационную фазу, но сохраняет семантику завершения.

Практика управления состоянием через end

Событие завершения перехода часто используется как точка фиксации состояния DOM после анимации. Это позволяет:

  • запускать последующие визуальные эффекты
  • обновлять данные интерфейса
  • синхронизировать несколько независимых переходов
  • фиксировать финальное состояние визуализации после динамических изменений

При этом логика должна учитывать, что end относится к отдельному элементу и отдельному переходу, а не к всей сцене в целом.