Сброс зума по кнопке

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

Сброс зума — это программное восстановление начального масштаба и позиции через вызов трансформации d3.zoomIdentity.

На практике такая возможность используется:

  • в интерактивных графиках;
  • на географических картах;
  • в SVG-редакторах;
  • в системах аналитики;
  • при визуализации больших наборов данных.

Объект zoomTransform

D3 хранит текущее состояние масштабирования в виде объекта трансформации:

{
    x: 0,
    y: 0,
    k: 1
}

Где:

  • x — смещение по оси X;
  • y — смещение по оси Y;
  • k — коэффициент масштаба.

Исходное состояние задаётся через:

d3.zoomIdentity

Эквивалентно:

{
    x: 0,
    y: 0,
    k: 1
}

Именно к этому состоянию обычно выполняется сброс.


Базовая настройка zoom

Простейшая схема масштабирования:

<svg width="800" height="400"></svg>
<button id="reset">Сбросить зум</button>
const svg = d3.select("svg");

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

container.append("circle")
    .attr("cx", 200)
    .attr("cy", 150)
    .attr("r", 80)
    .attr("fill", "steelblue");

Создание поведения zoom:

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

Подключение zoom к SVG:

svg.call(zoom);

Функция обновления трансформации:

function zoomed(event) {
    container.attr("transform", event.transform);
}

После этого SVG начинает поддерживать:

  • масштабирование колесом мыши;
  • перемещение drag;
  • touch-жесты.

Кнопка сброса масштаба

Сброс выполняется через метод:

zoom.transform

Полный пример:

d3.select("#reset").on("click", () => {

    svg.transition()
        .duration(750)
        .call(
            zoom.transform,
            d3.zoomIdentity
        );

});

Как работает zoom.transform

Метод:

zoom.transform

принудительно устанавливает новую трансформацию для элемента.

Аргументы:

selection.call(
    zoom.transform,
    transform
);

Где:

  • selection — SVG или контейнер;
  • transform — объект трансформации.

В роли трансформации может использоваться:

d3.zoomIdentity

или произвольное состояние:

d3.zoomIdentity.translate(100, 50).scale(2)

Плавная анимация сброса

Без transition:

svg.call(
    zoom.transform,
    d3.zoomIdentity
);

Сброс произойдёт мгновенно.

С transition:

svg.transition()
    .duration(500)
    .call(
        zoom.transform,
        d3.zoomIdentity
    );

D3 интерполирует:

  • позицию;
  • масштаб;
  • координаты.

Это создаёт плавный возврат камеры.


Полный пример

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">

<script src="https://d3js.org/d3.v7.min.js"></script>

<style>
body {
    font-family: Arial;
}

button {
    margin-bottom: 10px;
    padding: 8px 16px;
}

svg {
    border: 1px solid #999;
}
</style>
</head>

<body>

<button id="reset">
    Сбросить зум
</button>

<svg width="800" height="400"></svg>

<script>

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

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

for (let i = 0; i < 20; i++) {

    container.append("circle")
        .attr("cx", Math.random() * 700)
        .attr("cy", Math.random() * 300)
        .attr("r", 20)
        .attr("fill", "steelblue");
}

const zoom = d3.zoom()
    .scaleExtent([0.5, 10])
    .on("zoom", zoomed);

svg.call(zoom);

function zoomed(event) {

    container.attr(
        "transform",
        event.transform
    );
}

d3.select("#reset")
    .on("click", () => {

        svg.transition()
            .duration(700)
            .call(
                zoom.transform,
                d3.zoomIdentity
            );
    });

</script>

</body>
</html>

Сброс к произвольной позиции

Иногда необходимо возвращать пользователя не к исходному состоянию, а к заранее определённой области.

Пример:

const defaultTransform =
    d3.zoomIdentity
        .translate(200, 100)
        .scale(2);

Применение:

svg.transition()
    .duration(600)
    .call(
        zoom.transform,
        defaultTransform
    );

Такой подход используется:

  • в картах;
  • в редакторах схем;
  • в аналитических dashboard;
  • в графах и сетях.

Центрирование при сбросе

Частая задача — вернуть масштаб и одновременно центрировать объект.

Пример:

const transform =
    d3.zoomIdentity
        .translate(width / 2, height / 2)
        .scale(1.5);

Важно понимать:

translate()

выполняется до масштабирования.

Порядок операций влияет на итоговые координаты.


Получение текущего масштаба

D3 позволяет получить текущее состояние zoom:

const transform =
    d3.zoomTransform(svg.node());

Пример:

console.log(transform.k);

Получение:

console.log(transform.x);
console.log(transform.y);

Это полезно при:

  • сохранении состояния;
  • синхронизации нескольких графиков;
  • реализации undo/redo;
  • ограничении масштаба.

Частичный сброс

Иногда требуется сбросить только масштаб, но сохранить позицию.

Пример:

const current =
    d3.zoomTransform(svg.node());

const transform =
    d3.zoomIdentity
        .translate(current.x, current.y)
        .scale(1);

Применение:

svg.transition()
    .call(
        zoom.transform,
        transform
    );

Сброс только позиции

Обратная задача — вернуть положение, сохранив масштаб.

const current =
    d3.zoomTransform(svg.node());

const transform =
    d3.zoomIdentity
        .scale(current.k);

Работа с несколькими SVG

Если интерфейс содержит несколько визуализаций:

const charts = d3.selectAll("svg");

Сброс:

charts.transition()
    .duration(500)
    .call(
        zoom.transform,
        d3.zoomIdentity
    );

Синхронизация кнопки и zoom

Иногда после программного изменения трансформации возникают проблемы рассинхронизации.

Неправильный подход:

container.attr(
    "transform",
    "translate(0,0) scale(1)"
);

Так делать нельзя.

Причина:

  • D3 хранит внутреннее состояние zoom;
  • прямое изменение transform ломает синхронизацию;
  • следующий zoom может работать некорректно.

Правильный способ:

svg.call(
    zoom.transform,
    d3.zoomIdentity
);

Ограничение масштаба

Даже после сброса продолжают действовать ограничения:

.scaleExtent([1, 5])

Минимальный и максимальный zoom:

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

При попытке установить:

.scale(10)

D3 автоматически ограничит значение.


Сброс после двойного клика

Иногда сброс привязывают к двойному клику:

svg.on("dblclick", () => {

    svg.transition()
        .duration(400)
        .call(
            zoom.transform,
            d3.zoomIdentity
        );

});

Отключение стандартного double-click zoom

По умолчанию D3 масштабирует SVG по двойному клику.

Отключение:

svg.call(zoom)
   .on("dblclick.zoom", null);

После этого двойной клик можно использовать для собственного сброса.


Сброс при смене данных

При обновлении визуализации масштаб часто нужно обнулять:

function update(data) {

    render(data);

    svg.call(
        zoom.transform,
        d3.zoomIdentity
    );
}

Это особенно важно:

  • для временных рядов;
  • при фильтрации;
  • при загрузке новых наборов данных.

Типичные ошибки

Сброс transform напрямую

Ошибка:

container.attr("transform", null);

Проблема:

  • zoom-состояние остаётся старым;
  • возникает рассинхронизация.

Использование неправильного selection

Ошибка:

container.call(
    zoom.transform,
    d3.zoomIdentity
);

Корректно:

svg.call(
    zoom.transform,
    d3.zoomIdentity
);

Zoom должен вызываться на том же элементе, где был зарегистрирован.


Отсутствие transition

Без анимации пользователь может потерять ориентацию.

Предпочтительно:

.transition()
.duration(500)

Архитектурный подход

В крупных приложениях полезно хранить zoom отдельно:

const state = {
    zoom: d3.zoomIdentity
};

Обновление:

function zoomed(event) {

    state.zoom = event.transform;

    container.attr(
        "transform",
        event.transform
    );
}

Сброс:

state.zoom = d3.zoomIdentity;

svg.call(
    zoom.transform,
    state.zoom
);

Такой подход облегчает:

  • сериализацию состояния;
  • сохранение сессии;
  • синхронизацию компонентов;
  • интеграцию с React/Vue.

Интеграция с React

В React кнопка сброса обычно вызывает:

svg.transition()
    .duration(500)
    .call(
        zoom.transform,
        d3.zoomIdentity
    );

Важно избегать:

  • ручного обновления DOM вне D3;
  • конфликтов между virtual DOM и SVG transform.

Часто zoom хранится в useRef():

const zoomRef = useRef();

Производительность

Сброс zoom практически не нагружает браузер, поскольку изменяется только:

transform=""

Основная нагрузка возникает:

  • при большом количестве SVG-элементов;
  • при сложных transition;
  • при фильтрации и перерисовке.

Для Canvas-графики сброс выполняется ещё быстрее.


Сброс в Canvas

В Canvas D3 управляет только состоянием трансформации:

context.setTransform(
    1,
    0,
    0,
    1,
    0,
    0
);

Но zoom-состояние всё равно рекомендуется обновлять через:

zoom.transform

Комбинирование с fit-to-screen

Часто кнопка сброса заменяется более сложной логикой:

fitToScreen()

Алгоритм:

  1. вычислить bounds;
  2. определить нужный scale;
  3. вычислить translate;
  4. применить transform.

Пример:

const transform =
    d3.zoomIdentity
        .translate(tx, ty)
        .scale(scale);

Практический шаблон

Наиболее распространённый вариант:

const zoom = d3.zoom()
    .scaleExtent([0.5, 8])
    .on("zoom", ({transform}) => {

        container.attr(
            "transform",
            transform
        );
    });

svg.call(zoom);

function resetZoom() {

    svg.transition()
        .duration(600)
        .call(
            zoom.transform,
            d3.zoomIdentity
        );
}

Такой шаблон используется в большинстве интерактивных визуализаций на D3.js.