Поведение перетаскивания в D3.js реализуется через модуль
d3-drag. Во время перемещения элемента библиотека
непрерывно изменяет его координаты, используя события:
startdragendБазовая схема выглядит следующим образом:
const drag = d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded);
d3.select("circle")
.call(drag);
Во время обработки события drag обычно обновляются
координаты SVG-элемента:
function dragged(event, d) {
d3.select(this)
.attr("cx", event.x)
.attr("cy", event.y);
}
Однако без дополнительной логики после повторного рендера, обновления данных или перерисовки графа элемент может вернуться в исходное положение. Для предотвращения такого поведения требуется сохранять координаты внутри данных.
Частая ошибка — обновление только DOM-атрибутов без изменения объекта данных.
Пример неправильного подхода:
function dragged(event) {
d3.select(this)
.attr("cx", event.x)
.attr("cy", event.y);
}
SVG визуально изменяется, но источник данных остаётся прежним. При следующем вызове:
selection.attr("cx", d => d.x);
элемент снова получит старые координаты.
Причина заключается в архитектуре D3.js:
Правильный подход — обновлять данные одновременно с DOM.
const nodes = [
{ id: 1, x: 100, y: 120 },
{ id: 2, x: 300, y: 200 }
];
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 20);
const drag = d3.drag()
.on("drag", function(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this)
.attr("cx", d.x)
.attr("cy", d.y);
});
Теперь координаты сохраняются непосредственно в объекте:
{
id: 1,
x: 240,
y: 180
}
При любом повторном рендере элемент останется на новом месте.
После изменения данных интерфейс можно безопасно перерисовывать.
function render() {
svg.selectAll("circle")
.data(nodes)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 20);
}
Если координаты были сохранены:
d.x = event.x;
d.y = event.y;
то повторный вызов:
render();
не приведёт к сбросу позиции.
Во многих визуализациях перемещение реализуется через
transform.
.attr("transform", d => `translate(${d.x}, ${d.y})`)
Во время drag:
function dragged(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this)
.attr("transform", `translate(${d.x}, ${d.y})`);
}
Такой подход особенно полезен для:
<g>;Обычно интерактивный узел состоит не из одного SVG-элемента, а из нескольких:
<g>
<circle></circle>
<text></text>
</g>
В этом случае drag применяется к контейнеру
<g>.
const node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => `translate(${d.x}, ${d.y})`)
.call(
d3.drag()
.on("drag", dragged)
);
Функция перемещения:
function dragged(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this)
.attr("transform", `translate(${d.x}, ${d.y})`);
}
Положение всей группы сохраняется в объекте данных.
D3 предоставляет несколько вариантов вычисления позиции.
event.x
event.y
Это текущая позиция курсора.
event.dx
event.dy
Это изменение относительно предыдущего события.
Иногда удобнее накапливать координаты вручную.
function dragged(event, d) {
d.x += event.dx;
d.y += event.dy;
d3.select(this)
.attr("transform", `translate(${d.x}, ${d.y})`);
}
Подход полезен при:
В d3-force координаты рассчитываются физическим движком.
Если drag не фиксирует позицию, узел может вернуться обратно.
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(400, 300));
Для force simulation используются специальные свойства:
d.fx
d.fy
const drag = d3.drag()
.on("start", function(event, d) {
d.fx = d.x;
d.fy = d.y;
})
.on("drag", function(event, d) {
d.fx = event.x;
d.fy = event.y;
})
.on("end", function(event, d) {
d.fx = event.x;
d.fy = event.y;
});
Теперь узел сохраняет новую позицию даже после обновления simulation.
Если требуется временное перетаскивание без постоянной фиксации:
.on("end", function(event, d) {
d.fx = null;
d.fy = null;
});
После отпускания мыши физическая симуляция снова начинает двигать элемент.
Для долговременного хранения координат используются:
localStorage;localStorage.setItem(
"graph-nodes",
JSON.stringify(nodes)
);
const saved = localStorage.getItem("graph-nodes");
if (saved) {
nodes = JSON.parse(saved);
}
function dragEnded(event, d) {
localStorage.setItem(
"nodes",
JSON.stringify(nodes)
);
}
Теперь после обновления страницы элементы сохранят свои координаты.
Без ограничений элемент можно увести за пределы SVG.
function dragged(event, d) {
d.x = Math.max(0, Math.min(width, event.x));
d.y = Math.max(0, Math.min(height, event.y));
d3.select(this)
.attr("cx", d.x)
.attr("cy", d.y);
}
Для кругов важно учитывать размер.
const radius = 20;
d.x = Math.max(radius,
Math.min(width - radius, event.x));
d.y = Math.max(radius,
Math.min(height - radius, event.y));
При использовании d3.zoom() координаты курсора могут
отличаться от координат SVG.
После масштабирования элемент прыгает в неправильное место.
Причина — drag работает в экранных координатах, а SVG находится под трансформацией.
function dragged(event, d) {
const transform = d3.zoomTransform(svg.node());
const point = transform.invert([
event.x,
event.y
]);
d.x = point[0];
d.y = point[1];
d3.select(this)
.attr("transform",
`translate(${d.x}, ${d.y})`);
}
При обновлении данных важно правильно задавать ключи.
.data(nodes)
D3 может пересоздать элементы.
.data(nodes, d => d.id)
Теперь библиотека понимает, какой объект соответствует какому DOM-элементу.
const width = 800;
const height = 500;
const nodes = [
{ id: 1, x: 100, y: 100 },
{ id: 2, x: 300, y: 200 }
];
const svg = d3.select("svg");
const drag = d3.drag()
.on("drag", dragged)
.on("end", savePositions);
function render() {
svg.selectAll("circle")
.data(nodes, d => d.id)
.join("circle")
.attr("r", 25)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.call(drag);
}
function dragged(event, d) {
d.x = Math.max(25,
Math.min(width - 25, event.x));
d.y = Math.max(25,
Math.min(height - 25, event.y));
d3.select(this)
.attr("cx", d.x)
.attr("cy", d.y);
}
function savePositions() {
localStorage.setItem(
"saved-nodes",
JSON.stringify(nodes)
);
}
render();
.attr("cx", event.x)
Без:
d.x = event.x
позиция теряется.
nodes = newArray;
Старые координаты исчезают.
.data(nodes)
может приводить к уничтожению DOM-элементов.
Без transform.invert() координаты становятся
некорректными.
Стрелочные функции меняют контекст.
Неправильно:
.on("drag", (event, d) => {
d3.select(this);
})
Правильно:
.on("drag", function(event, d) {
d3.select(this);
})
Наиболее устойчивый подход строится по следующей схеме:
Такая модель обеспечивает: