Стратегии отзывчивости: viewBox vs пересчёт размеров

Отзывчивость визуализации — способность корректно адаптироваться под размеры контейнера, окна браузера, плотность пикселей и ориентацию устройства. В D3.js основная проблема отзывчивости связана с тем, что SVG по умолчанию имеет фиксированные размеры. Если не реализовать адаптацию, график может:

  • выходить за пределы контейнера;
  • обрезаться;
  • деформироваться;
  • терять читаемость;
  • неправильно масштабировать подписи и оси.

В экосистеме D3.js используются две ключевые стратегии:

  1. Использование viewBox
  2. Полный пересчёт размеров визуализации

Обе стратегии решают разные задачи и имеют принципиально разные подходы к масштабированию.


Использование viewBox

Принцип работы

SVG поддерживает встроенный механизм масштабирования через атрибут viewBox.

Пример:

<svg viewBox="0 0 800 400"></svg>

Структура:

viewBox="minX minY width height"

Значения:

Параметр Назначение
minX начальная координата X
minY начальная координата Y
width виртуальная ширина
height виртуальная высота

SVG начинает работать как независимая координатная система.


Базовая адаптивная схема

HTML

<div class="chart-container">
    <svg id="chart"></svg>
</div>

CSS

.chart-container {
    width: 100%;
}

svg {
    width: 100%;
    height: auto;
    display: block;
}

D3.js

const width = 800;
const height = 400;

const svg = d3.select("#chart")
    .attr("viewBox", `0 0 ${width} ${height}`);

Теперь SVG масштабируется автоматически.


Как работает масштабирование через viewBox

SVG создаёт виртуальную систему координат:

0 -------------------- 800
|
|
|
400

При изменении размеров контейнера браузер автоматически пересчитывает отображение.

Например:

Размер контейнера Что происходит
800×400 отображение 1:1
400×200 уменьшение в 2 раза
1600×800 увеличение в 2 раза

При этом:

  • координаты остаются прежними;
  • D3 ничего не пересчитывает;
  • SVG масштабируется целиком.

Пример графика с viewBox

const 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");

Такой график будет автоматически адаптироваться к размеру контейнера.


Атрибут preserveAspectRatio

viewBox почти всегда используется вместе с preserveAspectRatio.

Пример:

svg.attr("preserveAspectRatio", "xMidYMid meet");

Режимы масштабирования

meet

Сохраняет пропорции полностью.

Весь SVG помещается в контейнер

Пример:

.attr("preserveAspectRatio", "xMidYMid meet")

slice

SVG заполняет весь контейнер, но часть изображения может быть обрезана.

.attr("preserveAspectRatio", "xMidYMid slice")

none

Пропорции не сохраняются.

.attr("preserveAspectRatio", "none")

Это может привести к деформации графика.


Преимущества viewBox

Простота

Не требуется:

  • обработка resize;
  • пересчёт шкал;
  • повторный рендер;
  • обновление осей.

Высокая производительность

Браузер выполняет масштабирование на уровне рендеринга SVG.

D3 не участвует в адаптации.


Минимум кода

svg.attr("viewBox", "0 0 800 400");

Этого уже достаточно для базовой адаптивности.


Недостатки viewBox

Масштабируется всё

Увеличиваются и уменьшаются:

  • линии;
  • шрифты;
  • оси;
  • отступы;
  • толщины stroke.

На маленьких экранах текст становится нечитаемым.


Потеря контроля

Невозможно:

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

Проблемы со сложными графиками

Особенно заметны на:

  • heatmap;
  • network graph;
  • treemap;
  • dashboard;
  • scatter plot с большим количеством точек.

Стратегия полного пересчёта размеров

Основная идея

Вместо масштабирования SVG выполняется:

  1. получение текущего размера контейнера;
  2. пересчёт размеров графика;
  3. пересчёт шкал;
  4. обновление осей;
  5. повторная отрисовка элементов.

Это полноценный responsive rendering.


Получение размеров контейнера

const container = document.querySelector(".chart-container");

const width = container.clientWidth;
const height = 400;

Динамический SVG

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
};

ResizeObserver вместо resize

window.resize реагирует только на изменение окна браузера.

Но контейнер может изменяться:

  • внутри flex;
  • внутри grid;
  • после открытия sidebar;
  • после изменения layout.

Современный подход — ResizeObserver.


Пример ResizeObserver

const container = document.querySelector(".chart-container");

const observer = new ResizeObserver(() => {
    render();
});

observer.observe(container);

Преимущества ResizeObserver

Точная реакция

Отслеживается именно контейнер.


Поддержка сложных layout

Корректно работает с:

  • CSS Grid;
  • Flexbox;
  • динамическими панелями;
  • SPA-интерфейсами.

Более современная архитектура

Особенно важна для:

  • React;
  • Vue;
  • Svelte;
  • Angular.

Проблема полного удаления SVG

В примерах часто встречается:

svg.selectAll("*").remove();

Это простой, но неэффективный подход.


Более правильный подход — update pattern

В 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();

Сравнение подходов

viewBox

Плюсы

  • минимум кода;
  • высокая производительность;
  • простота;
  • мгновенная адаптация;
  • идеально для простых SVG.

Минусы

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

Пересчёт размеров

Плюсы

  • полный контроль;
  • адаптивная типографика;
  • responsive layout;
  • гибкость;
  • профессиональный подход.

Минусы

  • больше кода;
  • сложнее архитектура;
  • выше нагрузка;
  • нужен resize handling.

Когда использовать viewBox

Простые диаграммы

Подход хорошо подходит для:

  • простых bar chart;
  • line chart;
  • иконок;
  • схем;
  • небольших SVG-иллюстраций.

Статические визуализации

Если график:

  • редко обновляется;
  • не имеет сложной интерактивности;
  • не требует мобильной адаптации.

Быстрые прототипы

Для MVP и демонстраций viewBox часто оказывается оптимальным решением.


Когда нужен пересчёт размеров

Сложные аналитические панели

Например:

  • BI dashboard;
  • trading UI;
  • monitoring system;
  • realtime analytics.

Большие объёмы данных

Если плотность данных должна зависеть от размеров экрана.


Мобильная адаптация

Когда требуется:

  • скрывать элементы;
  • менять layout;
  • уменьшать количество подписей;
  • перестраивать оси.

Гибридный подход

На практике часто используется комбинация:

  • viewBox
    • частичный пересчёт.

Пример гибридной архитектуры

const svg = d3.select("#chart")
    .attr("viewBox", `0 0 ${width} ${height}`)
    .attr("preserveAspectRatio", "xMidYMid meet");

При этом:

  • размеры SVG адаптивны;
  • отдельные элементы пересчитываются вручную.

Частичный adaptive rendering

Например:

if (width < 600) {
    axis.ticks(5);
} else {
    axis.ticks(10);
}

Responsive margins

const margin = width < 768
    ? { top: 10, right: 10, bottom: 30, left: 30 }
    : { top: 20, right: 40, bottom: 50, left: 60 };

Проблема Retina и HiDPI

SVG хорошо масштабируется на Retina-дисплеях, поскольку является векторным форматом.

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

  • тонкими линиями;
  • pixel snapping;
  • blurry rendering.

Pixel snapping

Для чётких линий используют:

.attr("shape-rendering", "crispEdges")

Полупиксельное выравнивание

Иногда линия становится чётче при смещении:

.attr("x1", 0.5)

Debounce resize

При resize браузер генерирует множество событий.

Без оптимизации возможны:

  • лаги;
  • повторные рендеры;
  • перегрузка CPU.

Debounce-функция

function debounce(fn, delay) {

    let timeout;

    return function() {

        clearTimeout(timeout);

        timeout = setTimeout(() => {
            fn();
        }, delay);
    };
}

Использование debounce

window.addEventListener(
    "resize",
    debounce(render, 200)
);

Virtual size и real size

При адаптивной архитектуре полезно разделять:

Тип Назначение
virtual size внутренняя система координат
real size физический размер контейнера

Архитектура профессиональных D3-приложений

Часто используется структура:

Chart
 ├── data
 ├── scales
 ├── axes
 ├── layout
 ├── resize system
 └── render pipeline

Resize становится частью общей render-системы.


Responsive chart lifecycle

Типичный жизненный цикл:

resize
   ↓
measure container
   ↓
recalculate layout
   ↓
update scales
   ↓
update axes
   ↓
update marks
   ↓
render

Типичная ошибка начинающих

svg.attr("width", "100%");

Без viewBox это не создаёт настоящую адаптивность.

SVG растягивается, но внутренняя координатная система остаётся прежней.


Важность container-driven architecture

Современный responsive D3 строится вокруг контейнера, а не окна браузера.

Правильный подход:

container.clientWidth

Неправильный:

window.innerWidth

Resize и transitions

Во время resize анимации могут конфликтовать с обновлением layout.


Проблемный пример

rect.transition()
    .duration(1000)

При resize возникают:

  • рывки;
  • накопление transition;
  • артефакты.

Решение

Во время resize часто:

  • отключают transition;
  • уменьшают duration;
  • используют interrupt.

Пример

svg.selectAll("*").interrupt();

Responsive SVG в React

В React обычно используют:

  • useRef
  • ResizeObserver
  • useEffect

Базовый шаблон

useEffect(() => {

    const observer = new ResizeObserver(() => {
        render();
    });

    observer.observe(containerRef.current);

    return () => observer.disconnect();

}, []);

Главный критерий выбора стратегии

Ключевой вопрос:

Нужно ли адаптировать логику визуализации
или достаточно масштабировать SVG?

Если нужен только scaling — подходит viewBox.

Если требуется полноценный responsive behavior — необходим пересчёт размеров.