Гибридный подход: SVG для интерактивности, Canvas для производительности

SVG и Canvas решают разные задачи визуализации, и их поведение при работе с большими объёмами данных принципиально различается.

SVG представляет каждый графический элемент как DOM-узел. Это обеспечивает:

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

Однако при увеличении количества элементов SVG резко теряет производительность. Несколько тысяч DOM-узлов уже создают нагрузку на layout, repaint и memory usage.

Canvas работает иначе: вся сцена отрисовывается в одном bitmap-контексте. Это даёт:

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

Но Canvas не хранит структуру объектов. Отсутствуют встроенные события на элементы, и любая интерактивность требует дополнительного слоя логики.


Причины применения гибридного подхода

Гибридная архитектура SVG + Canvas возникает там, где одновременно требуются:

  • высокая плотность данных (Canvas);
  • интерактивность на уровне отдельных элементов (SVG);
  • визуальная гибкость (D3 scales, axes, transitions).

Типичные сценарии:

  • scatter plot с десятками тысяч точек;
  • временные ряды с плотной агрегацией;
  • географические проекции с большим количеством объектов;
  • real-time визуализация потоковых данных.

Ключевая идея: Canvas отвечает за массовый рендеринг, SVG — за интерактивные сущности и UI-слой.


Архитектура гибридного слоя

Базовая структура обычно состоит из двух перекрывающихся слоёв:

  • нижний слой — <canvas> (рендеринг данных);
  • верхний слой — <svg> (оси, тултипы, интерактивные элементы).

Слои синхронизируются через общие шкалы координат D3.

<div class="chart">
  <canvas id="canvas-layer"></canvas>
  <svg id="svg-layer"></svg>
</div>

Ключевой принцип: Canvas не знает про DOM, SVG не хранит данные — оба зависят от общей модели данных и шкал.


Масштабирование данных через D3 scales

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

const xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, width]);

const yScale = d3.scaleLinear()
  .domain([0, 100])
  .range([height, 0]);

Canvas использует те же шкалы для отрисовки:

function renderCanvas(ctx, data) {
  ctx.clearRect(0, 0, width, height);

  data.forEach(d => {
    ctx.beginPath();
    ctx.arc(xScale(d.x), yScale(d.y), 2, 0, Math.PI * 2);
    ctx.fill();
  });
}

SVG слой при этом использует идентичные координаты для интерактивных элементов:

svg.selectAll("circle")
  .data(focusPoints)
  .join("circle")
  .attr("cx", d => xScale(d.x))
  .attr("cy", d => yScale(d.y))
  .attr("r", 5);

Разделение ответственности слоёв

Canvas слой:

  • отрисовка всех точек, линий, полей;
  • обновление при zoom/pan;
  • минимизация перерисовок;
  • batch rendering.

SVG слой:

  • tooltip и hover-индикаторы;
  • выделенные элементы;
  • оси и подписи;
  • интерактивные контролы.

Синхронизация взаимодействия и координат

При масштабировании и панорамировании важно синхронизировать оба слоя.

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

    xScale.range([0, width].map(d => transform.applyX(d)));
    yScale.range([height, 0].map(d => transform.applyY(d)));

    renderCanvas(ctx, data);
    renderSvgOverlay();
  });

Canvas полностью перерисовывается, SVG обновляет только интерактивные элементы.


Проблема hit-testing и стратегии её решения

Canvas не поддерживает native hit-testing, поэтому требуется отдельный механизм определения попадания курсора в объект.

Подход 1: линейный перебор

Подходит только для небольших наборов данных.

function findNearestPoint(mouseX, mouseY) {
  return data.find(d => {
    const dx = xScale(d.x) - mouseX;
    const dy = yScale(d.y) - mouseY;
    return Math.sqrt(dx*dx + dy*dy) < 5;
  });
}

Подход 2: spatial indexing через d3-quadtree

const tree = d3.quadtree()
  .x(d => xScale(d.x))
  .y(d => yScale(d.y))
  .addAll(data);

function getPoint(mx, my) {
  return tree.find(mx, my, 5);
}

Quadtree значительно ускоряет поиск ближайшего элемента при больших объёмах данных.


Передача событий с Canvas на SVG слой

Canvas не генерирует событий по элементам, поэтому события обрабатываются на уровне холста:

canvas.addEventListener("mousemove", (event) => {
  const rect = canvas.getBoundingClientRect();
  const mx = event.clientX - rect.left;
  const my = event.clientY - rect.top;

  const point = tree.find(mx, my, 6);

  if (point) {
    showTooltip(point);
    highlightSvg(point);
  } else {
    hideTooltip();
  }
});

SVG слой используется только для визуального отображения состояния hover/selection.


Оптимизация производительности Canvas

Эффективность гибридного подхода зависит от качества рендеринга Canvas слоя.

1. Минимизация перерисовок

Перерисовка выполняется только при изменении данных или трансформации.

2. Batch rendering

Все операции рисования выполняются в одном цикле:

ctx.beginPath();
for (const d of data) {
  ctx.moveTo(xScale(d.x), yScale(d.y));
  ctx.arc(xScale(d.x), yScale(d.y), 2, 0, Math.PI * 2);
}
ctx.fill();

3. Offscreen buffering

Использование промежуточного canvas для статичных слоёв (например, фоновых сеток).


Типовой паттерн: scatter plot с большим числом точек

Архитектура:

  • Canvas: 100 000+ точек;
  • SVG: выделенные точки и tooltip;
  • D3: шкалы, zoom, quadtree.

Поток обновления:

  1. изменение масштаба → обновление scales;
  2. Canvas перерисовывает все точки;
  3. quadtree пересобирается;
  4. SVG обновляет только активные элементы.

Типовой паттерн: line chart с интерактивной навигацией

Canvas используется для отрисовки линии:

ctx.beginPath();
ctx.moveTo(xScale(data[0].x), yScale(data[0].y));

for (let i = 1; i < data.length; i++) {
  ctx.lineTo(xScale(data[i].x), yScale(data[i].y));
}

ctx.stroke();

SVG слой добавляет:

  • вертикальный курсор;
  • маркеры значений;
  • tooltip с ближайшей точкой.

Сложности синхронизации слоёв

Основные проблемы возникают при:

  • несовпадении координатных систем;
  • дрейфе трансформаций zoom;
  • разной частоте обновления Canvas и SVG;
  • несогласованности hit-testing и визуального слоя.

Для устранения используется единый источник состояния:

  • scales как единственный truth source;
  • централизованная модель данных;
  • одинаковая трансформация для всех слоёв.

Задержки и оптимизация событий

При высокой частоте событий mousemove применяется throttling через requestAnimationFrame:

let latestEvent;

canvas.addEventListener("mousemove", (e) => {
  latestEvent = e;
});

function tick() {
  if (latestEvent) {
    handleMouse(latestEvent);
    latestEvent = null;
  }
  requestAnimationFrame(tick);
}

tick();

Это предотвращает избыточные вычисления при движении курсора.


Расширенные подходы гибридной визуализации

В сложных системах добавляются дополнительные слои:

  • WebGL как третий уровень для экстремально больших данных;
  • SVG только для UI-компонентов;
  • Canvas разделяется на несколько слоёв (data layer, interaction layer, background layer).

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