Тач-события и мобильная поддержка

В экосистеме веб-визуализации D3.js интерактивность исторически строилась вокруг событий мыши: click, mousemove, mousedown, mouseup. Однако на мобильных устройствах этот подход требует переосмысления, поскольку сенсорный ввод не является прямой заменой мыши и имеет собственную модель событий, поведение браузеров и ограничения производительности.

Мобильная поддержка в D3.js опирается на три ключевых слоя:

  • события указателей (Pointer Events),
  • нативные touch-события,
  • абстракции D3 для координат и жестов (drag, zoom, pointer utilities).

Модель событий: мышь, touch и pointer

Классическая модель мыши в браузере предполагает единственный указатель. Сенсорные устройства вводят многоточечное взаимодействие: один или несколько пальцев, жесты масштабирования, прокрутка.

Существуют три уровня API:

Mouse Events

  • click
  • mousemove
  • mousedown
  • mouseup

Они сохраняются для обратной совместимости, но не отражают реальную картину сенсорного ввода.

Touch Events

  • touchstart
  • touchmove
  • touchend
  • touchcancel

Они предоставляют доступ к массиву касаний (touches, changedTouches), но требуют ручной обработки координат и жестов.

Pointer Events

  • pointerdown
  • pointermove
  • pointerup

Это унифицированный слой, который объединяет мышь, перо и сенсор. Именно он считается наиболее стабильной основой для современных интерактивных визуализаций.


Почему D3 требует особого подхода к touch

D3 часто работает с SVG-элементами и масштабируемыми сценами. На мобильных устройствах возникают специфические проблемы:

  • браузер перехватывает жесты для скролла страницы;
  • touchmove блокируется без preventDefault;
  • координаты касаний требуют трансформации относительно SVG;
  • мультитач конфликтует с drag-логикой;
  • производительность деградирует при частых перерасчётах DOM.

Поэтому взаимодействие необходимо строить с учётом:

  • оптимизации событий,
  • правильной привязки координат,
  • выбора модели pointer events вместо touch.

Получение координат касания в SVG

В 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);
});

Унификация событий через pointer events

Современные реализации 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 координат.


Проблемы scroll и passive listeners

Современные браузеры оптимизируют прокрутку страницы через passive event listeners. Это приводит к важному ограничению: нельзя вызывать preventDefault() в пассивных обработчиках.

На мобильных визуализациях это критично, так как:

  • жесты перетаскивания могут вызвать скролл страницы;
  • zoom может конфликтовать с pinch-to-zoom браузера.

Решение — явно отключать passive режим:

svg.on("touchmove", function(event) {
  event.preventDefault();
}, { passive: false });

В D3 это возможно через доступ к нативному addEventListener:

svg.node().addEventListener("touchmove", function(event) {
  event.preventDefault();
}, { passive: false });

Drag и мобильные жесты

Поведение drag в D3 адаптировано под pointer events, но при необходимости можно расширить поддержку touch.

Основная проблема — многоточечный ввод.

На мобильных устройствах:

  • один палец — drag;
  • два пальца — zoom/scroll.

Чтобы избежать конфликтов, часто применяют фильтрацию:

const drag = d3.drag()
  .filter((event) => event.touches ? event.touches.length === 1 : true);

Это ограничивает drag одним касанием.


Zoom и pinch-жесты

Модуль zoom в D3 поддерживает масштабирование и панорамирование.

const zoom = d3.zoom()
  .scaleExtent([1, 10])
  .on("zoom", (event) => {
    g.attr("transform", event.transform);
  });

svg.call(zoom);

На мобильных устройствах zoom автоматически обрабатывает pinch-жесты через pointer events. Однако есть нюансы:

  • браузерный pinch-zoom может конфликтовать с D3 zoom;
  • требуется блокировка default поведения;
  • важно ограничивать scaleExtent.

Дополнительно:

svg.call(zoom)
  .on("touchstart", function(event) {
    event.preventDefault();
  }, { passive: false });

Оптимизация частоты обновлений

Touch-события генерируются с высокой частотой. При каждом движении пальца может происходить десятки вызовов в секунду.

Типичная проблема — перегрузка DOM:

  • частые .attr() обновления;
  • перерасчёт layout;
  • лишние reflow/repaint.

Решение — использование 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 vs Canvas на мобильных устройствах

SVG удобен для интерактивных графиков, но на мобильных устройствах может становиться узким местом при больших объёмах данных.

Особенности:

SVG

  • высокая точность взаимодействия;
  • удобная событийная модель;
  • ухудшение производительности при тысячах элементов.

Canvas

  • лучше подходит для больших наборов данных;
  • требует ручной реализации hit-testing;
  • сложнее интеграция с D3, но возможна через scales и render loop.

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;

Управление конфликтами жестов

На мобильных устройствах важно предотвращать конфликт:

  • scroll страницы vs drag графика;
  • pinch zoom браузера vs zoom D3;
  • long press vs context menu.

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

svg.on("contextmenu", (event) => event.preventDefault());

И комбинированная фильтрация pointer events:

const zoom = d3.zoom()
  .filter((event) => {
    return !event.touches || event.touches.length === 2;
  });

Практика устойчивых мобильных визуализаций

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

  • единая модель pointer events вместо раздельных touch/mouse;
  • минимизация DOM-операций;
  • контроль частоты обновлений;
  • отключение системных жестов при необходимости;
  • использование трансформаций вместо пересчёта геометрии.

Особенно важно:

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

Интерактивные сцены и трансформации

D3 активно использует трансформации для мобильной адаптации:

g.attr("transform", event.transform);

Это позволяет:

  • масштабировать всю сцену без перерасчёта элементов;
  • сохранять плавность на слабых устройствах;
  • уменьшать количество операций над DOM.

Поведение длительного удержания и жестов

Длительное удержание на мобильных устройствах часто вызывает системное меню. Это мешает взаимодействию с графиками.

Отключение:

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);
});

Это позволяет:

  • контролировать сложные сценарии взаимодействий;
  • объединять zoom и drag;
  • предотвращать неконсистентные состояния.

Итоговые технические ограничения мобильных браузеров

Мобильная среда накладывает ограничения, которые напрямую влияют на архитектуру D3-визуализаций:

  • высокая стоимость DOM-операций;
  • особенности event loop под нагрузкой;
  • системные жесты, конкурирующие с интерактивностью;
  • различия между браузерами в обработке touch/pointer событий;
  • ограничения passive listeners.

Эти ограничения формируют устойчивую практику: переход от событийной модели «каждое касание = обновление DOM» к модели «события = обновление состояния = отрисовка через один контролируемый цикл».