Поведение d3.drag() используется для реализации
перетаскивания элементов мышью или касанием. Механизм входит в модуль
d3-drag и позволяет:
d3.drag() не занимается автоматическим перемещением
элементов. Поведение только отслеживает жесты и предоставляет события,
внутри которых изменяются координаты объектов.
<script src="https://d3js.org/d3.v7.min.js"></script>
После подключения становится доступен глобальный объект
d3.
const drag = d3.drag();
Полученный объект поведения затем подключается к элементам:
d3.selectAll("circle")
.call(drag);
Метод .call() применяет drag-поведение ко всем выбранным
элементам.
<svg width="600" height="400"></svg>
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)
В результате окружность перемещается вслед за курсором.
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/dyevent.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 и
dyfunction 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-элементов используются разные атрибуты координат.
cx
cy
x
y
x1
y1
x2
y2
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})`
);
})
);
Перемещение группы позволяет:
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)
);
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() автоматически поддерживает:
Дополнительная настройка обычно не требуется.
Во время drag браузер может выделять текст.
svg {
user-select: none;
}
circle {
cursor: grab;
}
circle:active {
cursor: grabbing;
}
d3.drag() часто используется вместе с
d3.zoom().
Пример типичной схемы:
Если drag и zoom работают на одном элементе, события могут конфликтовать.
Типичное решение:
event.sourceEvent.stopPropagation();
function dragStarted(event) {
event.sourceEvent.stopPropagation();
}
d3.drag() активно используется вместе с
d3.forceSimulation().
Перетаскивание узлов обычно реализуется так:
node.call(
d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
);
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 может вызывать нагрузку.
Основные причины:
Плохо:
function dragged(event) {
d3.select(this)
.attr("cx", event.x);
updateChart();
}
Лучше:
function dragged(event) {
d3.select(this)
.attr("cx", event.x);
}
Для сложных объектов быстрее:
.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);
})
function dragged(event) {
console.log(event.x, event.y);
}
function dragStarted(event) {
console.log("start");
}
function dragEnded(event) {
console.log("end");
}
Внутри D3 drag-поведение:
d3.drag()d3.drag() особенно полезен для: