Отзывчивость визуализации — способность корректно адаптироваться под размеры контейнера, окна браузера, плотность пикселей и ориентацию устройства. В D3.js основная проблема отзывчивости связана с тем, что SVG по умолчанию имеет фиксированные размеры. Если не реализовать адаптацию, график может:
В экосистеме D3.js используются две ключевые стратегии:
viewBoxОбе стратегии решают разные задачи и имеют принципиально разные подходы к масштабированию.
viewBoxSVG поддерживает встроенный механизм масштабирования через атрибут
viewBox.
Пример:
<svg viewBox="0 0 800 400"></svg>
Структура:
viewBox="minX minY width height"
Значения:
| Параметр | Назначение |
|---|---|
| minX | начальная координата X |
| minY | начальная координата Y |
| width | виртуальная ширина |
| height | виртуальная высота |
SVG начинает работать как независимая координатная система.
<div class="chart-container">
<svg id="chart"></svg>
</div>
.chart-container {
width: 100%;
}
svg {
width: 100%;
height: auto;
display: block;
}
const width = 800;
const height = 400;
const svg = d3.select("#chart")
.attr("viewBox", `0 0 ${width} ${height}`);
Теперь SVG масштабируется автоматически.
viewBoxSVG создаёт виртуальную систему координат:
0 -------------------- 800
|
|
|
400
При изменении размеров контейнера браузер автоматически пересчитывает отображение.
Например:
| Размер контейнера | Что происходит |
|---|---|
| 800×400 | отображение 1:1 |
| 400×200 | уменьшение в 2 раза |
| 1600×800 | увеличение в 2 раза |
При этом:
viewBoxconst width = 800;
const height = 400;
const data = [10, 20, 30, 40, 50];
const svg = d3.select("#chart")
.attr("viewBox", `0 0 ${width} ${height}`);
const x = d3.scaleBand()
.domain(d3.range(data.length))
.range([50, width - 50])
.padding(0.1);
const y = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([height - 50, 50]);
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d))
.attr("width", x.bandwidth())
.attr("height", d => height - 50 - y(d))
.attr("fill", "steelblue");
Такой график будет автоматически адаптироваться к размеру контейнера.
preserveAspectRatioviewBox почти всегда используется вместе с
preserveAspectRatio.
Пример:
svg.attr("preserveAspectRatio", "xMidYMid meet");
meetСохраняет пропорции полностью.
Весь SVG помещается в контейнер
Пример:
.attr("preserveAspectRatio", "xMidYMid meet")
sliceSVG заполняет весь контейнер, но часть изображения может быть обрезана.
.attr("preserveAspectRatio", "xMidYMid slice")
noneПропорции не сохраняются.
.attr("preserveAspectRatio", "none")
Это может привести к деформации графика.
viewBoxНе требуется:
Браузер выполняет масштабирование на уровне рендеринга SVG.
D3 не участвует в адаптации.
svg.attr("viewBox", "0 0 800 400");
Этого уже достаточно для базовой адаптивности.
viewBoxУвеличиваются и уменьшаются:
На маленьких экранах текст становится нечитаемым.
Невозможно:
Особенно заметны на:
Вместо масштабирования SVG выполняется:
Это полноценный responsive rendering.
const container = document.querySelector(".chart-container");
const width = container.clientWidth;
const height = 400;
const svg = d3.select("#chart")
.attr("width", width)
.attr("height", height);
function render() {
const container = document.querySelector(".chart-container");
const width = container.clientWidth;
const height = 400;
d3.select("#chart").selectAll("*").remove();
const svg = d3.select("#chart")
.attr("width", width)
.attr("height", height);
const data = [10, 20, 30, 40, 50];
const x = d3.scaleBand()
.domain(d3.range(data.length))
.range([50, width - 50])
.padding(0.1);
const y = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([height - 50, 50]);
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d))
.attr("width", x.bandwidth())
.attr("height", d => height - 50 - y(d))
.attr("fill", "tomato");
}
render();
window.addEventListener("resize", render);
.range([0, width])
Ширина зависит от контейнера.
Можно менять:
Пример:
const ticks = width < 500 ? 5 : 10;
axis.ticks(ticks);
const fontSize = width < 600 ? 10 : 14;
svg.selectAll("text")
.style("font-size", `${fontSize}px`);
const margin = {
top: 20,
right: width < 600 ? 10 : 40,
bottom: 40,
left: width < 600 ? 40 : 60
};
window.resize реагирует только на изменение окна
браузера.
Но контейнер может изменяться:
Современный подход — ResizeObserver.
const container = document.querySelector(".chart-container");
const observer = new ResizeObserver(() => {
render();
});
observer.observe(container);
Отслеживается именно контейнер.
Корректно работает с:
Особенно важна для:
В примерах часто встречается:
svg.selectAll("*").remove();
Это простой, но неэффективный подход.
В D3 следует обновлять существующие элементы.
const bars = svg.selectAll("rect")
.data(data);
bars.enter()
.append("rect")
.merge(bars)
.attr("x", (d, i) => x(i))
.attr("width", x.bandwidth());
bars.exit().remove();
viewBoxviewBoxПодход хорошо подходит для:
Если график:
Для MVP и демонстраций viewBox часто оказывается
оптимальным решением.
Например:
Если плотность данных должна зависеть от размеров экрана.
Когда требуется:
На практике часто используется комбинация:
viewBoxconst svg = d3.select("#chart")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio", "xMidYMid meet");
При этом:
Например:
if (width < 600) {
axis.ticks(5);
} else {
axis.ticks(10);
}
const margin = width < 768
? { top: 10, right: 10, bottom: 30, left: 30 }
: { top: 20, right: 40, bottom: 50, left: 60 };
SVG хорошо масштабируется на Retina-дисплеях, поскольку является векторным форматом.
Однако при пересчёте размеров возникают проблемы с:
Для чётких линий используют:
.attr("shape-rendering", "crispEdges")
Иногда линия становится чётче при смещении:
.attr("x1", 0.5)
При resize браузер генерирует множество событий.
Без оптимизации возможны:
function debounce(fn, delay) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn();
}, delay);
};
}
window.addEventListener(
"resize",
debounce(render, 200)
);
При адаптивной архитектуре полезно разделять:
| Тип | Назначение |
|---|---|
| virtual size | внутренняя система координат |
| real size | физический размер контейнера |
Часто используется структура:
Chart
├── data
├── scales
├── axes
├── layout
├── resize system
└── render pipeline
Resize становится частью общей render-системы.
Типичный жизненный цикл:
resize
↓
measure container
↓
recalculate layout
↓
update scales
↓
update axes
↓
update marks
↓
render
svg.attr("width", "100%");
Без viewBox это не создаёт настоящую адаптивность.
SVG растягивается, но внутренняя координатная система остаётся прежней.
Современный responsive D3 строится вокруг контейнера, а не окна браузера.
Правильный подход:
container.clientWidth
Неправильный:
window.innerWidth
Во время resize анимации могут конфликтовать с обновлением layout.
rect.transition()
.duration(1000)
При resize возникают:
Во время resize часто:
svg.selectAll("*").interrupt();
В React обычно используют:
useRefResizeObserveruseEffectuseEffect(() => {
const observer = new ResizeObserver(() => {
render();
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
Ключевой вопрос:
Нужно ли адаптировать логику визуализации
или достаточно масштабировать SVG?
Если нужен только scaling — подходит viewBox.
Если требуется полноценный responsive behavior — необходим пересчёт размеров.