В экосистеме веб-визуализации D3.js интерактивность исторически
строилась вокруг событий мыши: click,
mousemove, mousedown, mouseup.
Однако на мобильных устройствах этот подход требует переосмысления,
поскольку сенсорный ввод не является прямой заменой мыши и имеет
собственную модель событий, поведение браузеров и ограничения
производительности.
Мобильная поддержка в D3.js опирается на три ключевых слоя:
Классическая модель мыши в браузере предполагает единственный указатель. Сенсорные устройства вводят многоточечное взаимодействие: один или несколько пальцев, жесты масштабирования, прокрутка.
Существуют три уровня API:
Mouse Events
clickmousemovemousedownmouseupОни сохраняются для обратной совместимости, но не отражают реальную картину сенсорного ввода.
Touch Events
touchstarttouchmovetouchendtouchcancelОни предоставляют доступ к массиву касаний (touches,
changedTouches), но требуют ручной обработки координат и
жестов.
Pointer Events
pointerdownpointermovepointerupЭто унифицированный слой, который объединяет мышь, перо и сенсор. Именно он считается наиболее стабильной основой для современных интерактивных визуализаций.
D3 часто работает с SVG-элементами и масштабируемыми сценами. На мобильных устройствах возникают специфические проблемы:
touchmove блокируется без
preventDefault;Поэтому взаимодействие необходимо строить с учётом:
В D3.js ключевую роль играет преобразование координат из экранных в систему координат SVG.
Типичный способ:
const svg = d3.select("svg");
svg.on("touchstart", function(event) {
const touch = event.touches[0];
const point = d3.pointer(touch, this);
console.log(point[0], point[1]);
});
Функция d3.pointer учитывает трансформации контейнера,
включая viewBox, масштабирование и смещения.
Для pointer events подход упрощается:
svg.on("pointerdown", function(event) {
const point = d3.pointer(event, this);
});
Современные реализации D3-поведения (drag, zoom) внутри используют pointer events, что снижает необходимость ручной обработки touch.
Пример базового drag:
const drag = d3.drag()
.on("start", (event) => {
d3.select(event.sourceEvent.target).raise();
})
.on("drag", (event) => {
d3.select(event.sourceEvent.target)
.attr("cx", event.x)
.attr("cy", event.y);
});
d3.selectAll("circle").call(drag);
На мобильных устройствах event.x и event.y
уже нормализованы относительно контейнера, что избавляет от ручного
вычисления touch координат.
Современные браузеры оптимизируют прокрутку страницы через passive
event listeners. Это приводит к важному ограничению: нельзя вызывать
preventDefault() в пассивных обработчиках.
На мобильных визуализациях это критично, так как:
Решение — явно отключать passive режим:
svg.on("touchmove", function(event) {
event.preventDefault();
}, { passive: false });
В D3 это возможно через доступ к нативному
addEventListener:
svg.node().addEventListener("touchmove", function(event) {
event.preventDefault();
}, { passive: false });
Поведение drag в D3 адаптировано под pointer events, но при необходимости можно расширить поддержку touch.
Основная проблема — многоточечный ввод.
На мобильных устройствах:
Чтобы избежать конфликтов, часто применяют фильтрацию:
const drag = d3.drag()
.filter((event) => event.touches ? event.touches.length === 1 : true);
Это ограничивает drag одним касанием.
Модуль zoom в D3 поддерживает масштабирование и панорамирование.
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
На мобильных устройствах zoom автоматически обрабатывает pinch-жесты через pointer events. Однако есть нюансы:
Дополнительно:
svg.call(zoom)
.on("touchstart", function(event) {
event.preventDefault();
}, { passive: false });
Touch-события генерируются с высокой частотой. При каждом движении пальца может происходить десятки вызовов в секунду.
Типичная проблема — перегрузка DOM:
.attr() обновления;Решение — использование requestAnimationFrame:
let latestPoint = null;
svg.on("pointermove", (event) => {
latestPoint = d3.pointer(event, svg.node());
});
function render() {
if (latestPoint) {
circle.attr("cx", latestPoint[0])
.attr("cy", latestPoint[1]);
}
requestAnimationFrame(render);
}
render();
Такой подход синхронизирует обновления с кадровой частотой экрана.
SVG удобен для интерактивных графиков, но на мобильных устройствах может становиться узким местом при больших объёмах данных.
Особенности:
SVG
Canvas
D3 часто используют как слой вычислений (scales, layouts), а рендеринг выносят в canvas для мобильной оптимизации.
В SVG каждый элемент является DOM-узлом, что упрощает определение попадания:
d3.selectAll("circle")
.on("pointerdown", function(event, d) {
console.log(d);
});
В Canvas необходимо вычислять попадание вручную:
function hitTest(x, y, data) {
return data.find(d => {
const dx = d.x - x;
const dy = d.y - y;
return Math.sqrt(dx*dx + dy*dy) < d.r;
});
}
На мобильных устройствах это особенно важно из-за точности касаний.
Мобильная поддержка в D3 требует адаптивной геометрии:
viewBox вместо фиксированных
размеров;d3.scaleLinear;const width = window.innerWidth;
const height = window.innerHeight;
const svg = d3.select("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
Для плотности пикселей:
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
На мобильных устройствах важно предотвращать конфликт:
Типичное решение:
svg.on("contextmenu", (event) => event.preventDefault());
И комбинированная фильтрация pointer events:
const zoom = d3.zoom()
.filter((event) => {
return !event.touches || event.touches.length === 2;
});
Устойчивые визуализации строятся на нескольких принципах:
Особенно важно:
transform вместо изменения координат
элементов.D3 активно использует трансформации для мобильной адаптации:
g.attr("transform", event.transform);
Это позволяет:
Длительное удержание на мобильных устройствах часто вызывает системное меню. Это мешает взаимодействию с графиками.
Отключение:
svg.style("touch-action", "none");
Свойство touch-action является ключевым для управления
поведением браузера:
none — полное отключение жестов;pan-x, pan-y — частичное разрешение
прокрутки;manipulation — стандартное поведение.При сложных визуализациях важно хранить состояние жестов отдельно:
const state = {
dragging: false,
lastPoint: null
};
svg.on("pointerdown", (event) => {
state.dragging = true;
state.lastPoint = d3.pointer(event);
});
Это позволяет:
Мобильная среда накладывает ограничения, которые напрямую влияют на архитектуру D3-визуализаций:
Эти ограничения формируют устойчивую практику: переход от событийной модели «каждое касание = обновление DOM» к модели «события = обновление состояния = отрисовка через один контролируемый цикл».