d3.drag(): подключение и основы

Поведение d3.drag() используется для реализации перетаскивания элементов мышью или касанием. Механизм входит в модуль d3-drag и позволяет:

  • перемещать SVG-элементы;
  • изменять координаты объектов;
  • строить интерактивные диаграммы;
  • реализовывать drag-and-drop интерфейсы;
  • создавать узлы force-графов с ручным управлением;
  • комбинировать перетаскивание с масштабированием и анимацией.

d3.drag() не занимается автоматическим перемещением элементов. Поведение только отслеживает жесты и предоставляет события, внутри которых изменяются координаты объектов.


Подключение библиотеки

Подключение через CDN

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

После подключения становится доступен глобальный объект d3.


Базовая структура drag-поведения

const drag = d3.drag();

Полученный объект поведения затем подключается к элементам:

d3.selectAll("circle")
    .call(drag);

Метод .call() применяет drag-поведение ко всем выбранным элементам.


Первый пример перетаскивания

HTML

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

JavaScript

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

const circle = svg.append("circle")
    .attr("cx", 200)
    .attr("cy", 150)
    .attr("r", 40)
    .attr("fill", "steelblue");

const drag = d3.drag()
    .on("drag", dragged);

circle.call(drag);

function dragged(event) {
    d3.select(this)
        .attr("cx", event.x)
        .attr("cy", event.y);
}

Как работает перетаскивание

Во время движения указателя D3 генерирует событие drag.

Внутри обработчика доступны координаты:

event.x
event.y

Они содержат текущую позицию курсора относительно контейнера.

Элемент обновляет собственные координаты:

.attr("cx", event.x)
.attr("cy", event.y)

В результате окружность перемещается вслед за курсором.


События drag-поведения

d3.drag() поддерживает три основных события:

Событие Описание
start начало перетаскивания
drag процесс движения
end завершение перетаскивания

Обработка начала перетаскивания

const drag = d3.drag()
    .on("start", dragStarted)
    .on("drag", dragged)
    .on("end", dragEnded);

Пример

function dragStarted(event) {
    d3.select(this)
        .attr("stroke", "black")
        .attr("stroke-width", 3);
}

При начале движения элемент получает обводку.


Обработка завершения

function dragEnded(event) {
    d3.select(this)
        .attr("stroke", null);
}

После завершения drag-состояния оформление удаляется.


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

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

svg.append("circle")
    .attr("cx", 250)
    .attr("cy", 200)
    .attr("r", 50)
    .attr("fill", "orange")
    .call(
        d3.drag()
            .on("start", dragStarted)
            .on("drag", dragged)
            .on("end", dragEnded)
    );

function dragStarted(event) {
    d3.select(this)
        .attr("opacity", 0.7);
}

function dragged(event) {
    d3.select(this)
        .attr("cx", event.x)
        .attr("cy", event.y);
}

function dragEnded(event) {
    d3.select(this)
        .attr("opacity", 1);
}

Объект события event

В обработчиках drag-событий передаётся объект event.

Основные свойства

Свойство Назначение
event.x текущая координата X
event.y текущая координата Y
event.dx смещение по X
event.dy смещение по Y
event.subject объект перетаскивания
event.active число активных жестов
event.sourceEvent исходное DOM-событие

Разница между x/y и dx/dy

event.x и event.y

Абсолютные координаты курсора:

.attr("cx", event.x)
.attr("cy", event.y)

event.dx и event.dy

Смещение относительно предыдущей позиции:

.attr("cx", currentX + event.dx)
.attr("cy", currentY + event.dy)

Использование dx и dy

function dragged(event) {
    const circle = d3.select(this);

    const x = +circle.attr("cx");
    const y = +circle.attr("cy");

    circle
        .attr("cx", x + event.dx)
        .attr("cy", y + event.dy);
}

Контекст this

Внутри обработчиков this указывает на DOM-элемент, к которому применён drag.

function dragged(event) {
    console.log(this);
}

Обычно используется:

d3.select(this)

Перетаскивание прямоугольников

svg.append("rect")
    .attr("x", 100)
    .attr("y", 100)
    .attr("width", 120)
    .attr("height", 80)
    .attr("fill", "tomato")
    .call(
        d3.drag()
            .on("drag", function(event) {
                d3.select(this)
                    .attr("x", event.x)
                    .attr("y", event.y);
            })
    );

Особенности координат SVG

Для разных SVG-элементов используются разные атрибуты координат.

Circle

cx
cy

Rect

x
y

Line

x1
y1
x2
y2

Group

transform

Перетаскивание групп <g>

Часто удобнее перемещать целую группу.

Пример

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

group.append("circle")
    .attr("r", 40)
    .attr("fill", "purple");

group.call(
    d3.drag()
        .on("drag", function(event) {
            d3.select(this)
                .attr(
                    "transform",
                    `translate(${event.x}, ${event.y})`
                );
        })
);

Почему группы удобнее

Перемещение группы позволяет:

  • двигать несколько элементов одновременно;
  • не изменять координаты дочерних объектов;
  • строить сложные интерактивные структуры;
  • создавать draggable-компоненты.

Ограничение области перемещения

Ограничение по X

function dragged(event) {
    const x = Math.max(50, Math.min(550, event.x));

    d3.select(this)
        .attr("cx", x);
}

Ограничение по двум координатам

function dragged(event) {
    const x = Math.max(40, Math.min(560, event.x));
    const y = Math.max(40, Math.min(360, event.y));

    d3.select(this)
        .attr("cx", x)
        .attr("cy", y);
}

Перетаскивание данных

D3 часто связывает элементы с данными.

Пример

const data = [
    { x: 100, y: 100 },
    { x: 300, y: 200 }
];

Связь данных и элементов

svg.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .attr("cx", d => d.x)
    .attr("cy", d => d.y)
    .attr("r", 30)
    .call(
        d3.drag()
            .on("drag", dragged)
    );

Изменение данных во время drag

function dragged(event, d) {
    d.x = event.x;
    d.y = event.y;

    d3.select(this)
        .attr("cx", d.x)
        .attr("cy", d.y);
}

Теперь изменяется не только SVG, но и сама модель данных.


Зачем обновлять данные

Такой подход позволяет:

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

Настройка области захвата

Метод .subject()

Позволяет определить объект перетаскивания.

d3.drag()
    .subject(function(event, d) {
        return d;
    });

Значение event.subject

Во время drag:

event.subject

содержит объект, возвращённый из .subject().


Метод .filter()

Позволяет фильтровать события.

Пример

d3.drag()
    .filter(event => !event.ctrlKey);

Перетаскивание не начнётся при нажатом Ctrl.


Игнорирование правой кнопки мыши

d3.drag()
    .filter(event => event.button === 0);

Метод .clickDistance()

Определяет минимальное расстояние движения.

d3.drag()
    .clickDistance(5);

Если курсор сместился менее чем на 5 пикселей, drag не запускается.


Работа на сенсорных устройствах

d3.drag() автоматически поддерживает:

  • touch-события;
  • pointer events;
  • мышь;
  • стилусы.

Дополнительная настройка обычно не требуется.


Предотвращение выделения текста

Во время drag браузер может выделять текст.

CSS

svg {
    user-select: none;
}

Изменение курсора

circle {
    cursor: grab;
}

circle:active {
    cursor: grabbing;
}

Совмещение drag и zoom

d3.drag() часто используется вместе с d3.zoom().

Пример типичной схемы:

  • zoom применяется к контейнеру;
  • drag применяется к отдельным элементам.

Конфликт drag и zoom

Если drag и zoom работают на одном элементе, события могут конфликтовать.

Типичное решение:

event.sourceEvent.stopPropagation();

Пример остановки всплытия

function dragStarted(event) {
    event.sourceEvent.stopPropagation();
}

Drag в force simulation

d3.drag() активно используется вместе с d3.forceSimulation().

Перетаскивание узлов обычно реализуется так:

node.call(
    d3.drag()
        .on("start", dragStarted)
        .on("drag", dragged)
        .on("end", dragEnded)
);

Пример для force-графа

function dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
}

Освобождение узла

function dragEnded(event, d) {
    d.fx = null;
    d.fy = null;
}

Использование анимации

Во время drag можно изменять:

  • прозрачность;
  • размер;
  • цвет;
  • тени;
  • фильтры.

Пример

function dragStarted(event) {
    d3.select(this)
        .transition()
        .duration(200)
        .attr("r", 60);
}

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

При большом количестве объектов drag может вызывать нагрузку.

Основные причины:

  • постоянное обновление DOM;
  • сложные SVG;
  • тяжёлые вычисления в обработчике.

Оптимизация drag

Минимизация DOM-операций

Плохо:

function dragged(event) {
    d3.select(this)
        .attr("cx", event.x);

    updateChart();
}

Лучше:

function dragged(event) {
    d3.select(this)
        .attr("cx", event.x);
}

Использование transform вместо координат

Для сложных объектов быстрее:

.attr("transform", `translate(${x}, ${y})`)

чем изменение множества отдельных атрибутов.


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

Отсутствует .call()

Неправильно:

const drag = d3.drag();

Правильно:

element.call(drag);

Неверные атрибуты

Ошибка:

.attr("x", event.x)

для circle.

Правильно:

.attr("cx", event.x)

Использование стрелочных функций

Стрелочные функции меняют контекст this.

Проблемный вариант:

.on("drag", (event) => {
    d3.select(this);
})

Корректный вариант:

.on("drag", function(event) {
    d3.select(this);
})

Отладка drag-событий

Логирование координат

function dragged(event) {
    console.log(event.x, event.y);
}

Логирование состояния

function dragStarted(event) {
    console.log("start");
}

function dragEnded(event) {
    console.log("end");
}

Архитектура drag-поведения

Внутри D3 drag-поведение:

  1. регистрирует pointer/mouse/touch события;
  2. отслеживает начало жеста;
  3. вычисляет координаты;
  4. создаёт drag-события;
  5. вызывает пользовательские обработчики.

Когда использовать d3.drag()

d3.drag() особенно полезен для:

  • интерактивных графиков;
  • редакторов диаграмм;
  • карт;
  • конструкторов интерфейсов;
  • force-directed graph;
  • визуальных редакторов;
  • интерактивных SVG-сцен;
  • data visualization приложений.