В визуализациях на D3.js оси не являются статическими элементами SVG.
Они представляют собой производные конструкции, полностью зависящие от
состояния шкал (scales). Любое изменение данных, влияющее на диапазон
или структуру значений, требует пересчёта шкалы и повторного рендеринга
оси через генератор d3.axis.
Ключевая идея: ось — это функция отображения шкалы, а не независимый графический объект.
В D3.js оси строятся поверх шкал (d3-scale). Именно
шкала определяет:
Пример базовой шкалы:
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, width]);
Ось создаётся на основе этой шкалы:
const xAxis = d3.axisBottom(xScale);
Любое изменение данных начинается не с оси, а со шкалы.
При изменении данных меняется статистическая основа визуализации:
Если не обновить шкалу, график становится некорректным: элементы выходят за пределы области или сжимаются в одну точку.
Первый шаг динамического обновления — пересчёт домена:
xScale.domain(d3.extent(data, d => d.x));
Для временных рядов:
xScale.domain(d3.extent(data, d => new Date(d.date)));
Для категориальных данных:
xScale.domain(data.map(d => d.category));
Важно: domain всегда должен соответствовать текущему набору данных.
Ось в D3 обновляется вызовом генератора через call:
svg.select(".x-axis")
.call(xAxis);
Если шкала изменилась, но ось не была вызвана повторно, визуализация останется устаревшей.
Типичный цикл обновления включает три шага:
function update(data) {
xScale.domain(d3.extent(data, d => d.x));
svg.select(".x-axis")
.call(xAxis);
}
Этот шаблон является базовым для всех интерактивных визуализаций.
Без анимации обновление выглядит резким. D3 позволяет плавно
интерполировать изменения через transition().
svg.select(".x-axis")
.transition()
.duration(750)
.call(xAxis);
Анимация затрагивает:
Важно: переход работает только если ось заново вызывается через
call.
При добавлении новых данных диапазон часто расширяется.
Пример расширяющегося графика:
data.push({ x: 120, y: 80 });
xScale.domain(d3.extent(data, d => d.x));
svg.select(".x-axis")
.transition()
.call(xAxis);
Если новые данные выходят за пределы старого domain, без пересчёта шкалы они просто не отобразятся.
Ось должна обновляться синхронно с графическими элементами:
xScale.domain(d3.extent(data, d => d.x));
svg.selectAll(".bar")
.data(data)
.join("rect")
.transition()
.attr("x", d => xScale(d.x))
.attr("width", d => width - xScale(d.x));
svg.select(".x-axis")
.transition()
.call(xAxis);
Если обновить только ось или только график, визуализация потеряет согласованность.
Хотя оси не используют enter-update-exit напрямую, они зависят от этого паттерна при работе с данными.
const bars = svg.selectAll(".bar")
.data(data, d => d.id);
bars.enter()
.append("rect")
.attr("class", "bar")
.merge(bars)
.transition()
.attr("x", d => xScale(d.x))
.attr("height", d => height - yScale(d.y));
bars.exit()
.remove();
После любого изменения набора данных необходимо пересчитать domain и обновить ось, иначе соответствие координат нарушается.
Для временных шкал используется d3.scaleTime.
const xScale = d3.scaleTime()
.range([0, width]);
Обновление:
xScale.domain(d3.extent(data, d => new Date(d.timestamp)));
svg.select(".x-axis")
.transition()
.call(d3.axisBottom(xScale));
Особенность временных осей — автоматическая адаптация tick formatting:
d3.axisBottom(xScale)
.ticks(d3.timeMinute.every(10));
При динамических данных часто требуется адаптация форматирования:
const xAxis = d3.axisBottom(xScale)
.tickFormat(d3.format(".2f"));
Для времени:
d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%H:%M"));
При изменении масштаба можно пересоздавать ось:
xAxis = d3.axisBottom(xScale);
и затем вызывать call.
При неправильном обновлении могут возникать артефакты:
Причина — повторное добавление оси без очистки старого состояния или без использования одного и того же selection.
Правильный подход:
svg.select(".x-axis")
.call(xAxis);
не:
svg.append("g")
.attr("class", "x-axis")
.call(xAxis);
внутри каждого обновления.
При zoom и pan оси обновляются в реальном времени.
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", (event) => {
const newX = event.transform.rescaleX(xScale);
svg.select(".x-axis")
.call(d3.axisBottom(newX));
svg.selectAll(".bar")
.attr("x", d => newX(d.x));
});
svg.call(zoom);
Здесь ключевой момент — rescaleX, создающий временную
шкалу для отображения.
В потоковых графиках данные добавляются постоянно:
setInterval(() => {
data.push({ x: Date.now(), y: Math.random() * 100 });
xScale.domain(d3.extent(data, d => d.x));
svg.select(".x-axis")
.call(xAxis);
}, 1000);
Часто используется ограничение длины массива:
if (data.length > 50) data.shift();
Это предотвращает перегрузку шкалы.
При высокочастотных обновлениях ось может стать узким местом производительности.
Практики оптимизации:
Пример простого throttling:
let lastUpdate = 0;
function update(data) {
const now = Date.now();
if (now - lastUpdate < 100) return;
lastUpdate = now;
xScale.domain(d3.extent(data, d => d.x));
svg.select(".x-axis")
.call(xAxis);
}
При сложных графиках используется несколько осей:
Каждая обновляется независимо:
yScale.domain([0, d3.max(data, d => d.y)]);
svg.select(".y-axis")
.transition()
.call(d3.axisLeft(yScale));
Несогласованное обновление приводит к визуальной рассинхронизации.
D3 автоматически подбирает количество делений:
Но это поведение можно контролировать:
d3.axisBottom(xScale)
.ticks(5);
или динамически:
const tickCount = Math.max(2, Math.floor(width / 80));
d3.axisBottom(xScale)
.ticks(tickCount);
В архитектуре визуализаций ось становится реактивным слоем:
Любое изменение данных запускает цепочку преобразований, где ось — финальный шаг отображения координатной системы.