Механизм перетаскивания в D3.js строится вокруг поведения
d3.drag(), однако сенсорные экраны существенно отличаются
от работы с мышью:
hover;На мобильных устройствах drag-интерфейсы требуют дополнительной настройки, иначе возникают:
Базовая настройка drag на SVG-элементе:
<svg width="600" height="400"></svg>
const svg = d3.select("svg");
const circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 200)
.attr("r", 40)
.attr("fill", "steelblue");
circle.call(
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 dragged(event) {
d3.select(this)
.attr("cx", event.x)
.attr("cy", event.y);
}
function dragEnded(event) {
d3.select(this)
.attr("stroke", null);
}
D3 автоматически поддерживает:
Современные версии D3 используют унифицированную модель указателей,
поэтому отдельная реализация для touchstart чаще всего не
требуется.
При касании экрана D3 преобразует touch-взаимодействие в drag-поток:
| Touch-событие | Drag-событие D3 |
|---|---|
| touchstart | start |
| touchmove | drag |
| touchend | end |
Объект event содержит:
event.x
event.y
event.dx
event.dy
event.identifier
На сенсорных устройствах особенно важен identifier.
Пример:
function dragged(event) {
console.log(event.identifier);
}
identifier позволяет различать несколько одновременных
касаний.
Главная сложность touch drag — конфликт между:
Без настройки браузер может интерпретировать движение пальца как scroll.
touch-actionНаиболее важное CSS-свойство:
circle {
touch-action: none;
}
Либо:
svg {
touch-action: none;
}
Это отключает стандартные touch-жесты браузера:
Без touch-action: none drag на мобильных устройствах
часто работает нестабильно.
Полное отключение touch-жестов не всегда желательно.
Например:
touch-action: pan-y;
Разрешает вертикальный scroll, но блокирует горизонтальные жесты.
Варианты:
| Значение | Поведение |
|---|---|
| none | Полное отключение жестов |
| pan-x | Только горизонтальный scroll |
| pan-y | Только вертикальный scroll |
| manipulation | Разрешение базовых жестов |
Современные браузеры объединяют:
D3 использует Pointer Events, поэтому drag работает единообразно.
Пример pointer-информации:
function dragged(event) {
console.log(event.pointerType);
}
Возможные значения:
mouse
touch
pen
Это позволяет создавать разные сценарии поведения.
Иногда требуется отдельная обработка сенсорного ввода.
Пример:
function dragged(event) {
if (event.pointerType === "touch") {
d3.select(this)
.attr("fill", "orange");
}
if (event.pointerType === "mouse") {
d3.select(this)
.attr("fill", "steelblue");
}
d3.select(this)
.attr("cx", event.x)
.attr("cy", event.y);
}
На touch-экранах задержки особенно заметны.
Плохая практика:
.on("drag", function(event) {
heavyCalculation();
d3.select(this)
.attr("cx", event.x)
.attr("cy", event.y);
});
Тяжёлые вычисления внутри drag-цикла вызывают:
Следует:
На мобильных устройствах transform обычно работает
плавнее.
Менее эффективно:
.attr("cx", event.x)
.attr("cy", event.y)
Более эффективно:
.attr("transform", `translate(${event.x}, ${event.y})`)
Пример:
function dragged(event) {
d3.select(this)
.attr(
"transform",
`translate(${event.x}, ${event.y})`
);
}
На touch-экранах пользователь может легко вывести объект за границы сцены.
Ограничение координат:
function dragged(event) {
const x = Math.max(0, Math.min(600, event.x));
const y = Math.max(0, Math.min(400, event.y));
d3.select(this)
.attr("cx", x)
.attr("cy", y);
}
Часто перетаскивается не отдельный элемент, а группа.
Пример:
const group = svg.append("g");
group.append("rect")
.attr("width", 120)
.attr("height", 60);
group.append("text")
.text("Node")
.attr("x", 20)
.attr("y", 35);
group.call(
d3.drag()
.on("drag", function(event) {
d3.select(this)
.attr(
"transform",
`translate(${event.x}, ${event.y})`
);
})
);
Такой подход особенно полезен для:
На сенсорных устройствах мелкие элементы неудобны.
Плохой пример:
.attr("r", 5)
Палец перекрывает маленький объект.
group.append("circle")
.attr("r", 30)
.attr("fill", "transparent")
.style("pointer-events", "all");
Визуальный элемент может оставаться маленьким, но touch-область становится удобной.
Drag часто используется вместе с d3-force.
Пример:
const drag = d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded);
node.call(drag);
function dragStarted(event, d) {
if (!event.active) {
simulation.alphaTarget(0.3).restart();
}
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) {
simulation.alphaTarget(0);
}
d.fx = null;
d.fy = null;
}
На touch-экранах это особенно важно, поскольку узлы графа иначе начинают «убегать» от пальца.
После drag мобильные браузеры могут генерировать click.
Это вызывает проблемы:
D3 предоставляет:
event.sourceEvent
Пример:
function dragEnded(event) {
event.sourceEvent.stopPropagation();
}
Метод filter() позволяет определить, какие события
активируют drag.
Пример:
const drag = d3.drag()
.filter(event => {
return event.pointerType !== "mouse"
|| event.button === 0;
});
Здесь:
D3 поддерживает несколько одновременных касаний.
Пример отслеживания:
function dragged(event) {
console.log(
event.identifier,
event.x,
event.y
);
}
Однако стандартный drag чаще всего предполагает один активный объект.
Для мультитач-интерфейсов обычно требуется собственная логика:
Типичный мобильный интерфейс:
Пример:
const zoom = d3.zoom()
.scaleExtent([1, 8]);
svg.call(zoom);
Конфликт drag и zoom необходимо контролировать.
drag.filter(event => {
return event.pointerType !== "touch"
|| event.touches?.length < 2;
});
D3 может использоваться не только с SVG.
Для Canvas drag выглядит иначе:
const canvas = d3.select("canvas");
canvas.call(
d3.drag()
.on("drag", dragged)
);
Но обновление выполняется вручную:
function dragged(event) {
object.x = event.x;
object.y = event.y;
render();
}
На мобильных устройствах Canvas часто производительнее SVG при большом числе объектов.
Touch-события могут генерироваться очень часто.
Полезно применять throttling.
Пример:
let lastTime = 0;
function dragged(event) {
const now = performance.now();
if (now - lastTime < 16) {
return;
}
lastTime = now;
update(event);
}
16 мс соответствует примерно 60 FPS.
Ещё более эффективный подход — синхронизация с рендерингом браузера.
let pending = false;
function dragged(event) {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
d3.select("#node")
.attr("cx", event.x)
.attr("cy", event.y);
pending = false;
});
}
Такой подход уменьшает нагрузку на мобильный GPU и CPU.
Браузеры оптимизируют touch-события через passive listeners.
Однако drag-интерфейсы иногда требуют блокировки стандартного поведения.
D3 автоматически управляет обработчиками, но при ручной работе важно помнить:
event.preventDefault();
может не работать внутри passive listener.
Мобильные интерфейсы часто используют инерционное движение.
D3 не реализует inertia автоматически, но её можно добавить.
Пример хранения скорости:
let vx = 0;
let vy = 0;
Во время drag:
vx = event.dx;
vy = event.dy;
После завершения:
function animate() {
x += vx;
y += vy;
vx *= 0.95;
vy *= 0.95;
render();
if (Math.abs(vx) > 0.1) {
requestAnimationFrame(animate);
}
}
На мобильных устройствах размеры SVG часто меняются.
Проблема:
event.x
event.y
зависят от системы координат контейнера.
При использовании viewBox необходимо учитывать
масштабирование.
Пример:
const point = d3.pointer(event, svg.node());
d3.pointer() корректно преобразует координаты
относительно контейнера.
Сенсорные интерфейсы требуют поддержки доступности.
Рекомендуется:
Минимальный рекомендуемый размер touch-области:
44x44 px
Причины:
touch-action: none;Решение:
touch-action: none;
overflow: hidden;
Причины:
Причины:
Решение:
d3.pointer(event);
Стабильный drag-интерфейс обычно включает:
touch-action;Такой подход обеспечивает: