Ограничение зума: scaleExtent и translateExtent

Механизм масштабирования и панорамирования в D3.js основан на поведении d3.zoom, которое управляет трансформацией transform для SVG- и DOM-элементов через параметры масштаба и смещения. Управление границами этих трансформаций реализуется двумя ключевыми ограничителями: scaleExtent и translateExtent, которые определяют допустимый диапазон зума и перемещения сцены.

Параметр scaleExtent задаёт диапазон допустимых значений масштаба, то есть коэффициента увеличения k в трансформации transform = translate(x, y) scale(k).

const zoom = d3.zoom()
  .scaleExtent([1, 8]);

В данном случае минимальный масштаб равен 1 (отсутствие увеличения), максимальный — 8 (восемькратное увеличение). Любая попытка выйти за эти пределы будет автоматически ограничена поведением zoom-поведения.

Механика ограничения масштаба

Во внутренней модели D3 значение k всегда нормализуется относительно диапазона:

  • если пользователь уменьшает масштаб ниже минимального значения, применяется k_min
  • если увеличивает выше максимального — применяется k_max

Это ограничение работает на уровне событий wheel, touchzoom, а также при программном вызове zoom.scaleTo и zoom.scaleBy.

svg.call(
  d3.zoom()
    .scaleExtent([0.5, 5])
    .on("zoom", (event) => {
      g.attr("transform", event.transform);
    })
);

Любое значение event.transform.k гарантированно находится в пределах [0.5, 5].

Практическое значение scaleExtent

Ограничение масштаба используется для:

  • предотвращения чрезмерного увеличения, при котором графика теряет структуру
  • предотвращения слишком сильного уменьшения, при котором сцена становится неразличимой
  • контроля производительности при рендеринге больших наборов данных
  • ограничения UX-поведения, чтобы интерфейс оставался предсказуемым

translateExtent

Если scaleExtent ограничивает масштаб, то translateExtent ограничивает область перемещения (панорамирования). Это границы, в пределах которых может перемещаться содержимое сцены.

const zoom = d3.zoom()
  .translateExtent([[0, 0], [width, height]]);

Данный параметр задаёт прямоугольник в координатах пространства сцены:

  • [x0, y0] — верхний левый угол допустимой области
  • [x1, y1] — нижний правый угол допустимой области

Принцип работы translateExtent

Когда применяется трансформация:

transform = d3.zoomIdentity
  .translate(x, y)
  .scale(k);

D3 вычисляет допустимые границы смещения так, чтобы видимая область не выходила за пределы translateExtent.

Если пользователь пытается перетащить сцену за границу, значения x и y корректируются автоматически.


Связь translateExtent с размером viewport

Особенность translateExtent заключается в том, что он работает в координатах внутренней системы, а не экрана. Поэтому при масштабировании границы воспринимаются с учётом текущего scale.

Фактически ограничение вычисляется как:

  • область трансформации делится на текущий масштаб
  • затем применяется clamp к координатам перевода

Это приводит к тому, что при сильном увеличении сцена «сжимается» внутри допустимой области, а при уменьшении — расширяется.


Поведение при сочетании scaleExtent и translateExtent

Одновременное использование двух ограничителей формирует замкнутую систему управления камерой:

const zoom = d3.zoom()
  .scaleExtent([1, 10])
  .translateExtent([[0, 0], [2000, 2000]]);

В этой конфигурации:

  • масштаб ограничен десятикратным увеличением
  • панорамирование ограничено квадратом 2000×2000

Важный эффект взаимодействия

При изменении масштаба меняется и «эффективная» область панорамирования. Это связано с тем, что viewport отображает часть сцены, и при увеличении масштаба пользователь видит меньшую часть пространства, но с большей детализацией.

Таким образом:

  • при k → min доступная область перемещения уменьшается
  • при k → max доступная область перемещения увеличивается

Центрирование и ограничения трансформации

При инициализации zoom важно учитывать начальную трансформацию:

const zoom = d3.zoom()
  .scaleExtent([1, 6])
  .translateExtent([[0, 0], [width, height]]);

svg.call(zoom);

svg.call(
  zoom.transform,
  d3.zoomIdentity.translate(0, 0).scale(1)
);

Если начальная трансформация не соответствует допустимым границам, D3 автоматически корректирует её в сторону ближайшего валидного значения.


Программное управление и clamp-логика

D3 предоставляет методы, которые также подчиняются ограничениям:

zoom.scaleTo(svg, 3);
zoom.translateTo(svg, 100, 100);

Если значения выходят за пределы:

  • scaleTo будет ограничен scaleExtent
  • translateTo будет скорректирован в пределах translateExtent

Эта логика делает программные изменения идентичными пользовательским жестам.


Особенности расчёта границ при SVG

В SVG-рендеринге координаты translateExtent обычно привязываются к:

  • размеру viewBox
  • габаритам группы <g>
  • логическим координатам сцены

Пример:

const svg = d3.select("svg");

const g = svg.append("g");

const zoom = d3.zoom()
  .scaleExtent([1, 4])
  .translateExtent([[0, 0], [1000, 600]])
  .on("zoom", (event) => {
    g.attr("transform", event.transform);
  });

svg.call(zoom);

Если сцена выходит за границы 1000×600, D3 автоматически ограничивает перемещение.


Edge cases и нюансы поведения

1. translateExtent меньше viewport

Если область ограничения меньше видимой области, поведение становится «зажатым»:

  • панорамирование почти отсутствует
  • сцена фиксируется в пределах центра

2. scaleExtent слишком узкий

Если диапазон масштабирования минимален:

.scaleExtent([1, 1.1])
  • зум становится почти статическим
  • wheel-ивенты могут казаться «неработающими»

3. несоответствие пропорций

Если translateExtent не соответствует соотношению сторон viewport:

  • появляются ограничения только по одной оси
  • сцена может «упираться» в границы по X или Y асимметрично

Внутренняя модель трансформации

Каждое zoom-событие формирует объект:

event.transform = {
  x: number,
  y: number,
  k: number
};

При этом:

  • k всегда ограничен scaleExtent
  • (x, y) всегда скорректированы по translateExtent

Именно этот объект является единственным источником истины для рендера сцены.


Инверсия координат и влияние ограничений

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

const point = d3.pointer(event);
const inverted = transform.invert(point);

Если активированы ограничения:

  • invert работает в рамках уже ограниченной трансформации
  • координаты не выходят за пределы сцены, даже при крайних значениях зума

Поведение при инерции и плавной прокрутке

В современных версиях D3 zoom поддерживает плавные переходы. Ограничения применяются и во время анимации:

  • инерция не может «перелететь» за translateExtent
  • easing-функции обрезаются на границах допустимого пространства
  • конечное состояние всегда нормализуется

Комбинация с кастомной логикой ограничений

Иногда встроенных ограничений недостаточно, и поверх scaleExtent и translateExtent добавляется дополнительная логика:

.on("zoom", (event) => {
  const t = event.transform;

  const clampedX = Math.max(minX, Math.min(maxX, t.x));
  const clampedY = Math.max(minY, Math.min(maxY, t.y));

  g.attr("transform", `translate(${clampedX},${clampedY}) scale(${t.k})`);
});

Однако при использовании translateExtent такая дополнительная обработка обычно избыточна, так как D3 уже выполняет аналогичный clamp.


Поведение в canvas-рендеринге

При использовании canvas вместо SVG:

  • scaleExtent и translateExtent остаются логическими ограничителями
  • отрисовка полностью зависит от текущего transform
  • координаты сцены пересчитываются вручную на основе k, x, y

Это требует явного применения трансформации к контексту:

context.setTransform(k, 0, 0, k, x, y);

Именно здесь ограничения становятся критически важными для предотвращения выхода за пределы данных.


Итоговая модель взаимодействия ограничений

Система zoom в D3 формирует трёхуровневую модель:

  • ограничение масштаба через scaleExtent
  • ограничение перемещения через translateExtent
  • нормализация трансформации через event.transform

Эти элементы работают совместно, формируя предсказуемое управление сценой, где любое действие пользователя или кода остаётся в рамках заданного пространства координат.