Использование Canvas 2D API в экосистеме D3.js применяется в сценариях, где количество визуализируемых элементов превышает разумные пределы для SVG-дерева. В отличие от SVG, Canvas не создает отдельные DOM-узлы для каждого графического примитива. Вместо этого вся сцена отрисовывается в одном растровом контексте, что радикально снижает нагрузку на браузер при работе с десятками тысяч элементов.
Ключевое отличие подхода заключается в переходе от декларативного DOM-ориентированного рендеринга к императивному рисованию на пиксельном буфере. D3 при этом сохраняет свою роль как слой вычислений: масштабирование, подготовка данных, генерация геометрии, но не участвует напрямую в создании визуальных узлов.
При интеграции D3 с Canvas логика делится на два уровня:
D3 выполняет:
scaleLinear, scaleTime,
scaleOrdinal)Canvas выполняет:
CanvasRenderingContext2DТакое разделение устраняет необходимость поддерживать тысячи DOM-узлов, что особенно критично для scatter plot, heatmap и сетевых графов.
Canvas-рендеринг в D3 почти всегда строится вокруг полного перерисовывания сцены.
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
function render(data, xScale, yScale) {
context.clearRect(0, 0, canvas.width, canvas.height);
data.forEach(d => {
context.beginPath();
context.arc(xScale(d.x), yScale(d.y), 3, 0, 2 * Math.PI);
context.fillStyle = "steelblue";
context.fill();
});
}
Каждый кадр представляет собой полное восстановление сцены. В отличие от SVG, где можно изменять отдельные элементы, Canvas не хранит структуру изображения.
Шкалы D3 остаются центральным элементом вычислительного слоя. Они трансформируют данные в пиксельные координаты, не завися от способа рендеринга.
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, canvas.width]);
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([canvas.height, 0]);
Дальнейшая отрисовка полностью полагается на эти преобразования. Canvas не имеет встроенного понятия масштабов, поэтому вся логика координат централизуется в D3.
Механизм enter/update/exit в привычном виде теряет
смысл, поскольку отсутствуют DOM-элементы. Вместо этого применяется
модель полного пересчета сцены.
Типичный паттерн:
function upd ate(data) {
render(data, xScale, yScale);
}
При этом возможно сохранение внутренней структуры состояния, но не DOM-связи.
Поскольку Canvas не хранит элементы, состояние визуальных объектов переносится в отдельные структуры данных.
const state = {
hoveredId: null,
selectedIds: new Se t()
};
Каждый кадр использует это состояние для изменения внешнего вида элементов:
data.forEach(d => {
context.beginPath();
context.arc(xScale(d.x), yScale(d.y), 3, 0, 2 * Math.PI);
if (d.id === state.hoveredId) {
context.fillStyle = "orange";
} else {
context.fillStyle = "steelblue";
}
context.fill();
});
Отсутствие DOM требует самостоятельной реализации определения попадания курсора в объект.
Простейший подход — линейный перебор:
function findNearest(x, y, data) {
let minDist = Infinity;
let target = null;
data.forEach(d => {
const dx = xScale(d.x) - x;
const dy = yScale(d.y) - y;
const dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
target = d;
}
});
return target;
}
Для больших наборов данных применяется пространственная индексация:
d3-quadtree)const tree = d3.quadtree()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.addAll(data);
Поиск ближайшего элемента:
const found = tree.find(mouseX, mouseY);
Это позволяет заменить линейный поиск логарифмическим по сложности в среднем случае.
Модуль d3-zoom работает независимо от типа рендеринга.
Он модифицирует трансформацию, которая затем применяется к шкалам.
const zoom = d3.zoom()
.scaleExtent([0.5, 20])
.on("zoom", (event) => {
const transform = event.transform;
const zx = transform.rescaleX(xScale);
const zy = transform.rescaleY(yScale);
render(data, zx, zy);
});
d3.select(canvas).call(zoom);
Canvas при этом просто перерисовывается с новыми масштабами, не зная о внутренней логике трансформаций.
Полный redraw кадра может стать узким местом. Основные стратегии оптимизации:
1. Dirty rectangles (частичная перерисовка) Используется редко, так как усложняет синхронизацию состояния.
2. OffscreenCanvas Позволяет выполнять рендеринг в отдельном потоке.
3. Кэширование слоев
const buffer = document.createElement("canvas");
const bufferCtx = buffer.getContext("2d");
Статические элементы рисуются один раз, динамические — поверх.
4. Пропуск кадров
let scheduled = false;
function requestRender() {
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
render(data, xScale, yScale);
});
}
}
Canvas становится оправданным при плотных визуализациях:
SVG в таких сценариях деградирует из-за стоимости DOM-узлов, тогда как Canvas сохраняет постоянную стоимость рендера, зависящую только от количества операций отрисовки.
В архитектурно чистом варианте D3 полностью отделяется от визуализации:
Пример для force-directed графа:
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", render);
Каждый тик симуляции вызывает перерисовку Canvas.
Линии и узлы требуют строгого порядка отрисовки:
function render(data) {
context.clearRect(0, 0, width, height);
// связи
context.strokeStyle = "#aaa";
links.forEach(l => {
context.beginPath();
context.moveTo(l.source.x, l.source.y);
context.lineTo(l.target.x, l.target.y);
context.stroke();
});
// узлы
nodes.forEach(n => {
context.beginPath();
context.arc(n.x, n.y, 4, 0, 2 * Math.PI);
context.fillStyle = "#4682b4";
context.fill();
});
}
Порядок важен, поскольку Canvas не имеет z-index слоя как DOM.
Для дисплеев с высокой плотностью пикселей требуется масштабирование:
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
context.scale(dpr, dpr);
Без этого визуализация становится размытым растровым изображением.
Несмотря на производительность, подход имеет ограничения:
D3 при этом не компенсирует эти ограничения, а лишь предоставляет вычислительный инструментарий.
Часто применяется комбинированная модель:
Например:
Это разделение позволяет сохранить производительность и интерактивность одновременно.
Типичный pipeline:
function update(rawData) {
const processed = preprocess(rawData);
xScale.domain(d3.extent(processed, d => d.x));
yScale.domain(d3.extent(processed, d => d.y));
render(processed, xScale, yScale);
}
Canvas в связке с D3 формирует модель, где сцена описывается не как дерево объектов, а как функция состояния:
Scene = f(data, state, scales)
Каждый кадр является чистой функцией от входных параметров, что упрощает предсказуемость поведения при сложных интерактивных сценариях.