Медиазапросы и D3

При разработке визуализаций на базе D3.js важна не только корректная отрисовка графиков, но и их способность адаптироваться к различным размерам экранов. Современные интерфейсы должны одинаково хорошо работать на широкоформатных мониторах, планшетах и мобильных устройствах. Для решения этой задачи используются медиазапросы CSS, механизмы адаптивной верстки и динамические возможности D3.js.

D3.js напрямую не содержит встроенной системы медиазапросов, поскольку библиотека отвечает за работу с данными и DOM-структурой. Однако D3 прекрасно взаимодействует с CSS, JavaScript API браузера и SVG, что позволяет строить полностью адаптивные визуализации.


Роль медиазапросов в визуализациях

Медиазапросы (media queries) позволяют применять разные CSS-правила в зависимости от характеристик устройства:

  • ширины экрана;
  • высоты окна;
  • ориентации устройства;
  • плотности пикселей;
  • типа устройства.

Пример классического медиазапроса:

@media screen and (max-width: 768px) {
    .chart-title {
        font-size: 14px;
    }
}

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

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

Адаптивный SVG

SVG обладает важным преимуществом — векторной природой. Это означает, что графика масштабируется без потери качества.

Базовый адаптивный SVG:

const svg = d3.select("#chart")
    .append("svg")
    .attr("viewBox", "0 0 800 400")
    .attr("preserveAspectRatio", "xMidYMid meet")
    .style("width", "100%")
    .style("height", "auto");

Ключевые параметры

viewBox

Определяет внутреннюю систему координат SVG:

viewBox="0 0 800 400"

Структура:

min-x min-y width height

preserveAspectRatio

Управляет поведением масштабирования:

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

Наиболее популярные варианты:

Значение Описание
meet Сохраняет пропорции
slice Заполняет область с обрезкой
none Игнорирует пропорции

Реакция на изменение размера окна

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

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

window.addEventListener("resize", resizeChart);

function resizeChart() {
    const width = window.innerWidth;

    svg.attr("width", width);
}

Однако простого изменения ширины обычно недостаточно. Необходимо:

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

Полное перестроение графика

Наиболее надежный подход — полная перерисовка визуализации.

function renderChart() {

    d3.select("#chart")
        .selectAll("*")
        .remove();

    const width = window.innerWidth * 0.8;
    const height = 400;

    const svg = d3.select("#chart")
        .append("svg")
        .attr("width", width)
        .attr("height", height);

    // построение графика
}

renderChart();

window.addEventListener("resize", renderChart);

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

  • корректное обновление всех элементов;
  • простота поддержки;
  • отсутствие артефактов масштабирования.

Недостатки

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

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

API window.matchMedia() позволяет работать с медиазапросами непосредственно из JavaScript.

Пример:

const mobileQuery = window.matchMedia("(max-width: 768px)");

if (mobileQuery.matches) {
    console.log("Мобильное устройство");
}

Интеграция matchMedia с D3.js

function render() {

    const isMobile = window
        .matchMedia("(max-width: 768px)")
        .matches;

    const width = isMobile ? 320 : 900;
    const height = isMobile ? 250 : 500;

    d3.select("#chart")
        .selectAll("*")
        .remove();

    const svg = d3.select("#chart")
        .append("svg")
        .attr("width", width)
        .attr("height", height);

    svg.append("text")
        .attr("x", 20)
        .attr("y", 40)
        .text(isMobile ? "Mobile" : "Desktop");
}

render();

window.addEventListener("resize", render);

Адаптивные размеры графиков

Размеры графиков обычно рассчитываются динамически.

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

const container = d3.select("#chart").node();

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

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

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

const observer = new ResizeObserver(entries => {

    for (let entry of entries) {

        const width = entry.contentRect.width;

        console.log(width);

        renderChart();
    }
});

observer.observe(document.getElementById("chart"));

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

  • реагирует именно на контейнер;
  • работает точнее window.resize;
  • подходит для сложных интерфейсов;
  • корректно работает в flex/grid-layout.

Адаптация осей

На мобильных устройствах длинные подписи осей создают серьезные проблемы.

Уменьшение количества тиков

const xAxis = d3.axisBottom(xScale)
    .ticks(5);

Для десктопа:

.ticks(12)

Для мобильных:

.ticks(4)

Поворот подписей

svg.selectAll(".x-axis text")
    .attr("transform", "rotate(-45)")
    .style("text-anchor", "end");

Скрытие части подписей

svg.selectAll(".x-axis text")
    .style("display", (d, i) => {
        return i % 2 === 0 ? "block" : "none";
    });

Адаптивные шрифты

Размеры текста должны изменяться вместе с экраном.

Через CSS

.chart-label {
    font-size: 16px;
}

@media screen and (max-width: 768px) {

    .chart-label {
        font-size: 11px;
    }
}

Через D3.js

const fontSize = window.innerWidth < 768
    ? "10px"
    : "16px";

svg.selectAll("text")
    .style("font-size", fontSize);

Адаптация столбчатых диаграмм

На мобильных устройствах столбцы часто становятся слишком узкими.

Горизонтальная диаграмма

Вместо вертикальной:

svg.selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("y", d => yScale(d.name))
    .attr("width", d => xScale(d.value))
    .attr("height", yScale.bandwidth());

Такой подход обеспечивает:

  • лучшую читаемость;
  • больше пространства для подписей;
  • удобство взаимодействия.

Адаптивные круговые диаграммы

Изменение радиуса

const radius = Math.min(width, height) / 2;

Перемещение легенды

На десктопе:

[Диаграмма] [Легенда]

На мобильных:

[Диаграмма]
[Легенда]

Управление плотностью данных

При большом количестве точек визуализация становится нечитаемой.

Downsampling

Сокращение числа отображаемых элементов:

const reducedData = data.filter((d, i) => i % 5 === 0);

Адаптивная линия графика

Толщина линий может изменяться:

const strokeWidth = window.innerWidth < 768
    ? 1
    : 3;

path.attr("stroke-width", strokeWidth);

Учет touch-интерфейсов

Мобильные устройства используют сенсорное управление.

Увеличение области взаимодействия

Плохой вариант:

.attr("r", 3)

Лучший вариант:

.attr("r", 10)
.attr("opacity", 0)

Невидимая область облегчает касание элементов.


Hover и touch

Событие mouseover не работает на большинстве сенсорных устройств.

Вместо него используются:

.on("click", handler)

или:

.on("touchstart", handler)

Responsive-паттерн для D3.js

Один из самых популярных шаблонов:

function responsivefy(svg) {

    const container = d3.select(svg.node().parentNode);

    const width = parseInt(svg.style("width"));
    const height = parseInt(svg.style("height"));

    const aspect = width / height;

    svg.attr("viewBox", `0 0 ${width} ${height}`)
        .attr("preserveAspectRatio", "xMinYMid")
        .call(resize);

    d3.select(window)
        .on("resize." + container.attr("id"), resize);

    function resize() {

        const targetWidth =
            parseInt(container.style("width"));

        svg.attr("width", targetWidth);

        svg.attr("height",
            Math.round(targetWidth / aspect));
    }
}

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

const svg = d3.select("#chart")
    .append("svg")
    .attr("width", 800)
    .attr("height", 400);

responsivefy(svg);

Flexbox и D3.js

D3 отлично работает внутри flex-контейнеров.

.dashboard {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
}

Каждый график:

.chart {
    flex: 1 1 400px;
}

CSS Grid и визуализации

.dashboard {
    display: grid;
    grid-template-columns:
        repeat(auto-fit, minmax(300px, 1fr));
}

Такой подход позволяет автоматически перестраивать сетку.


Адаптация tooltip

Tooltip должен корректно отображаться на маленьких экранах.

Ограничение ширины

.tooltip {
    max-width: 200px;
}

Позиционирование tooltip

tooltip
    .style("left", `${event.pageX + 10}px`)
    .style("top", `${event.pageY + 10}px`);

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


Производительность адаптивных графиков

Частая перерисовка может вызывать проблемы.

Debounce

function debounce(fn, delay) {

    let timeout;

    return function() {

        clearTimeout(timeout);

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

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

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

Retina и HiDPI-дисплеи

SVG автоматически выглядит четко на Retina-дисплеях. Однако при использовании Canvas требуется учитывать devicePixelRatio.


Комбинирование CSS и D3

Наиболее эффективная стратегия:

  • D3 отвечает за данные;
  • CSS отвечает за адаптивность;
  • медиазапросы управляют оформлением;
  • JavaScript перестраивает структуру при необходимости.

Практический пример адаптивного графика

function drawChart() {

    d3.select("#chart")
        .selectAll("*")
        .remove();

    const container =
        document.getElementById("chart");

    const width = container.clientWidth;

    const height = width < 600
        ? 300
        : 500;

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

    const data = [10, 20, 30, 40];

    const xScale = d3.scaleBand()
        .domain(data.map((d, i) => i))
        .range([0, width])
        .padding(0.1);

    const yScale = d3.scaleLinear()
        .domain([0, 40])
        .range([height, 0]);

    svg.selectAll("rect")
        .data(data)
        .enter()
        .append("rect")
        .attr("x", (d, i) => xScale(i))
        .attr("y", d => yScale(d))
        .attr("width", xScale.bandwidth())
        .attr("height",
            d => height - yScale(d));
}

drawChart();

window.addEventListener(
    "resize",
    debounce(drawChart, 150)
);

Частые ошибки

Фиксированные размеры SVG

Плохо:

.attr("width", 800)
.attr("height", 600)

Лучше:

.style("width", "100%")
.attr("viewBox", "0 0 800 600")

Игнорирование мобильных устройств

График может быть идеальным на десктопе и полностью нечитаемым на смартфоне.


Слишком много элементов

Большое количество SVG-элементов резко снижает производительность мобильных браузеров.


Отсутствие перерасчета шкал

При изменении размеров необходимо обновлять:

  • диапазоны шкал;
  • оси;
  • позиции элементов;
  • размеры графических объектов.

Архитектура responsive-визуализации

Типичная структура:

Container
 └── SVG
      ├── Scales
      ├── Axes
      ├── Shapes
      ├── Labels
      └── Interactions

При изменении размера:

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

Подход Mobile First

Эффективная стратегия адаптивной визуализации — проектирование сначала под мобильные устройства.

Пример:

.chart-title {
    font-size: 12px;
}

@media screen and (min-width: 768px) {

    .chart-title {
        font-size: 20px;
    }
}

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

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

Адаптивность и доступность

Responsive-дизайн тесно связан с accessibility.

Важно учитывать:

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

Современные подходы

В современных приложениях D3.js обычно используется совместно с:

  • React;
  • Vue;
  • Svelte;
  • CSS Grid;
  • Flexbox;
  • ResizeObserver;
  • CSS Container Queries.

Container Queries особенно полезны, поскольку позволяют адаптировать график не под окно браузера, а под размер конкретного контейнера.

Пример:

@container (max-width: 500px) {

    .chart-label {
        display: none;
    }
}

Это особенно эффективно для dashboard-интерфейсов и модульных систем визуализации данных.