Масштабирование в D3 реализуется через модуль поведения zoom, который
основан на перехвате событий pointer и трансформации координатного
пространства. Основная идея заключается не в изменении самих данных, а в
изменении матрицы преобразования transform, которая
применяется к SVG-группе или canvas-контексту.
Ключевая сущность:
d3.zoom() — поведение масштабированияd3.zoomTransform(node) — получение текущего
трансформационного состоянияevent.transform — объект текущего зумаtransform.k, transform.x,
transform.y — масштаб и смещениеconst zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
g.attr("transform", event.transform);
}
Здесь вся логика замыкается на одном DOM-элементе. Однако при появлении нескольких визуальных областей возникает задача синхронизации состояния между ними.
При наличии нескольких графиков, каждый из которых поддерживает zoom, возникает расхождение состояний:
Необходим механизм, при котором одно zoom-событие становится источником истины для всех связанных элементов.
Основная сложность:
Базовый подход заключается в выделении общего состояния:
let sharedTransform = d3.zoomIdentity;
Все графики используют это состояние как источник истины.
Создаётся единое поведение zoom:
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", handleZoom);
Каждый график получает один и тот же обработчик:
svgA.call(zoom);
svgB.call(zoom);
function handleZoom(event) {
sharedTransform = event.transform;
applyTransform(svgA, sharedTransform);
applyTransform(svgB, sharedTransform);
}
Функция применения трансформации:
function applyTransform(svg, transform) {
svg.select("g")
.attr("transform", transform);
}
Такой подход уже синхронизирует визуальное поведение, но содержит скрытую проблему: повторный вызов zoom внутри update может вызвать цепочку событий.
D3 генерирует события zoom при программном вызове
call(zoom.transform, ...). Это приводит к циклическим
обновлениям, если не учитывать источник события.
Решение — фильтрация sourceEvent:
function handleZoom(event) {
if (event.sourceEvent) {
sharedTransform = event.transform;
svgA.call(zoom.transform, sharedTransform);
svgB.call(zoom.transform, sharedTransform);
}
}
Однако этот вариант всё ещё может вызвать повторные вызовы обработчика.
Более стабильный подход — разделение:
let isSyncing = false;
function handleZoom(event) {
if (isSyncing) return;
sharedTransform = event.transform;
isSyncing = true;
svgA.call(zoom.transform, sharedTransform);
svgB.call(zoom.transform, sharedTransform);
isSyncing = false;
}
Такой механизм гарантирует, что каскадные вызовы не запускают повторную синхронизацию.
Более масштабируемая архитектура предполагает, что каждый график имеет собственный zoom, но состояние централизовано.
const zoomA = d3.zoom().on("zoom", zoomedA);
const zoomB = d3.zoom().on("zoom", zoomedB);
Общий state:
let sharedTransform = d3.zoomIdentity;
Обработчики:
function zoomedA(event) {
sync(event.transform, svgB, zoomB);
}
function zoomedB(event) {
sync(event.transform, svgA, zoomA);
}
Функция синхронизации:
function sync(transform, targetSvg, targetZoom) {
sharedTransform = transform;
targetSvg.call(targetZoom.transform, transform);
}
При синхронизации важно учитывать, что zoom влияет не только на визуальное представление, но и на оси данных.
Для осей используется пересчёт шкал:
const newX = transform.rescaleX(xScale);
const newY = transform.rescaleY(yScale);
Применение к осям:
xAxis.call(d3.axisBottom(newX));
yAxis.call(d3.axisLeft(newY));
При синхронизации нескольких графиков необходимо применять одинаковый transform к каждой шкале.
Часто графики имеют разную структуру:
В таком случае zoom должен быть унифицирован на уровне координат, а не DOM.
Подход:
sharedTransformДля более сложных систем применяется event bus:
const dispatch = d3.dispatch("zoomchange");
Подписка:
dispatch.on("zoomchange", (transform) => {
sharedTransform = transform;
applyAll(transform);
});
График:
function zoomed(event) {
dispatch.call("zoomchange", null, event.transform);
}
Этот подход устраняет прямые зависимости между компонентами.
Внешнее управление часто требуется для:
svg.call(zoom.transform, d3.zoomIdentity);
или возврат к состоянию:
svg.call(zoom.transform, sharedTransform);
Анимация:
svg.transition()
.duration(750)
.call(zoom.transform, sharedTransform);
При создании нескольких графиков важно синхронизировать начальное состояние:
const initialTransform = d3.zoomIdentity.scale(2).translate(50, 30);
svgA.call(zoom.transform, initialTransform);
svgB.call(zoom.transform, initialTransform);
Без этого графики начинают с разных координатных систем, что нарушает синхронизацию при первом взаимодействии.
При частой синхронизации возможны микроскопические расхождения из-за:
Решение:
sharedTransform как единственный
источникСтабильная архитектура синхронизации zoom строится на принципе:
transformconst state = {
transform: d3.zoomIdentity
};
Все визуальные элементы реагируют только на изменение state, а не друг на друга.
При N графиках применяется единый контроллер:
const views = [svgA, svgB, svgC, svgD];
function applyAll(transform) {
views.forEach(v => {
v.call(zoom.transform, transform);
});
}
Это обеспечивает линейную масштабируемость архитектуры синхронизации.
При интенсивном взаимодействии (drag + zoom) возникает поток событий. Для стабильной синхронизации применяется throttle:
let last = 0;
function handleZoom(event) {
const now = Date.now();
if (now - last < 16) return;
last = now;
sharedTransform = event.transform;
applyAll(sharedTransform);
}
Синхронизация zoom между несколькими элементами сводится к трём слоям:
Такой подход устраняет зависимость между DOM-элементами и переводит взаимодействие в управляемую модель трансформаций координатного пространства.