Главная / Статьи

D3.js внутри React: привязка данных, SVG и правило принадлежности useEffect

Узнайте, что на самом деле представляет собой D3, как работает привязка ввода/обновления/выхода, почему для неё подходит SVG, и как разделить управление DOM, чтобы D3 и React сотрудничали при создании столбчатой диаграммы.

Издатель: REACTAPP.TOP7 мин чтения1530 слов

Источники: Harnessing the Power of Data Visualization: Integrating D3.js with React

Три вещи, которые нужно знать сначала

  1. D3 — это набор инструментов, а не библиотека для графиков. Те, кто ожидает чего-то вроде Chart.js, сталкиваются с трудностями. D3 представляет собой низкоуровневый инструмент для связывания данных с элементами DOM, что объясняет как его мощность, так и более крутой угол обучения.
  2. React и D3 конкурируют за одни и те же узлы DOM. Решение простое: React отрисовывает контейнер SVG, а D3 управляет всем, что находится внутри него, с помощью useEffect. Если игнорировать это правило, придётся тратить часы на устранение странных конфликтов.
  3. Шаблоны enter, update и exit сначала кажутся странными. После нескольких примеров они становятся понятными, и тогда другие подходы к графикам, основанным на данных, кажутся неуклюжими.

Что на самом деле представляет собой D3

Это название означает «документы, основанные на данных». Важным словом здесь является «документы»: элементы DOM, внешний вид которых определяется данными. D3 связывает массивы с элементами и позволяет задавать их внешний вид в зависимости от значений в этих массивах. Диаграмма — это лишь один из возможных результатов, причем сама библиотека не имеет представления о типах диаграмм.

Именно здесь и кроются как сильные стороны, так и сложности. D3 не принимает решений за вас. Каждое визуальное свойство, каждая отметка на оси, каждый цвет и каждая интеракция — всё это вы указываете сами. У вас есть полная творческая свобода, но при этом приходится много печатать.

Связывание данных и DOM

Именно здесь большинство новичков сдаются, поэтому стоит тщательно разобраться с этим пошагово.

В обычном JavaScript вы создаёте элемент, заполняете его содержимым и вставляете на страницу. В D3 вы выбираете группу элементов, включая те, которых ещё нет, привязываете к этому выбору массив, а затем получаете обратно три группы:

  • элементы, которые уже существуют и нуждаются лишь в новых данных (выбор для обновления),
  • точки данных, у которых пока нет элементов (выбор для добавления),
  • элементы, у которых данные исчезли (выбор для удаления).

Странно то, что методы вызываются с выборкой, которая пуста. Вы выбираете все элементы rect внутри SVG, в котором их нет, вызываете .data(myArray), затем .enter(), после чего .append('rect'), и появляются прямоугольники. На самом деле .enter() создаёт заместитель для каждого данных, у которого нет элемента, а .append() превращает каждый такой заместитель в реальный узел. Это можно понимать как «для каждой точки данных, у которой нет элемента, создать один».

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

Установка визуальных свойств из данных

Как только данные привязаны, следующий шаг становится более понятным. Любой атрибут или стиль можно вычислить на основе данных:

selection.attr('height', d => yScale(d.value))

Здесь d — это данные, привязанные к конкретному элементу, а yScale — функция, преобразующая диапазон значений в пиксели. Высота столбца напрямую зависит от его данных. Измените данные, снова запустите код, и высота столбца автоматически изменится. Практически всё в визуализациях с использованием D3 сводится к вариациям этой идеи: задание визуального свойства в виде функции от границ диапазона значений.

Почему SVG

D3 рисует в формате SVG, и причины этого выходят за рамки исторических обстоятельств.

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

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

Проблемы возникают при большом количестве элементов. Тысячи узлов SVG замедляют загрузку страницы, поэтому отображение 50 000 отдельных точек в виде отдельных элементов негативно скажется на производительности. Для таких случаев более подходит рендеринг с использованием Canvas. Для панелей управления, бизнес-диаграмм и большинства аналитических инструментов SVG подходит отлично, а интеграция событий оправдана.

Интерактивность и гибкость

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

Большинство библиотек для графиков предоставляют готовые типы графиков для настройки: столбчатые, линейные, круговые, с возможностями выбора цветов, меток и легенд. D3 же предлагает базовые элементы: шкалы, оси, генераторы линий и алгоритмы разметки. Тип графика — это просто их комбинация, поэтому не существует списка поддерживаемых вариантов. Если какая-то визуализация нигде не существует, её можно создать, причём только с учётом ограничений SVG и математики.

Это требует дополнительных усилий. То, стоит ли это труда, зависит от проекта. Для стандартных бизнес-графиков подойдут Recharts или Chart.js, и на этом можно остановиться. Для кастомных интерактивных графиков, где критически важна презентабельность, лучше использовать D3.

Функции, которые вы будете использовать постоянно

Это не полный справочник, а только те функции, которые применяются в повседневной работе:

  • d3.select() выбирает первый элемент, соответствующий указанному селектору. В React обычно используется d3.select(svgRef.current) для получения доступа к контейнеру.
  • d3.selectAll() возвращает все соответствующие элементы. Обычно именно здесь начинается привязка данных, поскольку задействовано несколько элементов.
  • selection.data() связывает каждый элемент массива с соответствующим элементом интерфейса, чтобы переменная d была доступна при последующих вызовах. Все остальное зависит от этого шага.
  • selection.enter() создает временную выборку для данных, у которых ещё нет элементов, а затем .append() формирует реальные элементы.
  • selection.attr() используется для установки атрибутов, например высоты линии, показанной выше; такой код пишется сотни раз.
  • selection.style() выполняет аналогичные действия с CSS-свойствами, такими как цвет заполнения или степень прозрачности, которые зависят от того, активен ли элемент.
  • selection.transition() анимирует изменения. Используйте его перед .attr() или .style(), чтобы значения плавно изменялись, превращая резкие скачки в анимацию.
  • d3.scaleLinear() преобразует значения из одного числового диапазона в другой. При домене от 0 до 100 и диапазоне от 0 до 400 значение 50 соответствует пикселю 200. Масштабирование используется практически во всех визуализациях.
  • d3.axisBottom() и d3.axisLeft() создают оси с делениями и метками на основе масштаба, избавляя вас от необходимости вручную рисовать эти элементы.
  • d3.max() возвращает наибольшее значение, которое часто используется для задания диапазонов масштаба, например, в d3.max(data, d => d.value).
  • d3.transition() координирует анимации для нескольких элементов одновременно.
  • Использование D3 внутри React

    Проблема, о которой никто не упоминает, пока с ней не столкнёшься: React поддерживает виртуальный DOM и вносит свои изменения в реальный, тогда как D3 редактирует реальный DOM напрямую. Если обе библиотеки работают с одним элементом, перерисовка React перезаписывает результаты работы D3, или D3 вставляет узлы, о которых React не знает, из-за чего следующая перерисовка становится неопределённой.

    Работающий подход — разделение обязанностей. React отрисовывает контейнер SVG и не вмешивается в его содержимое. D3 получает доступ к этому контейнеру через ref и выполняет всю свою работу внутри useEffect. При изменении данных обновление состояния запускает этот эффект, и D3 очищает и перерисовывает элементы. Ни одна из библиотек не вмешивается в работу другой.

    1. Установите пакеты

    Добавьте D3 вместе с React и ReactDOM с помощью вашего менеджера пакетов.

    npm install d3 react react-dom
    

    Пользователи Yarn делают то же самое с помощью собственной команды add.

    yarn add d3 react react-dom
    

    2. Написание компонента

    У React есть одна задача: отрисовать элемент SVG и передать D3 ссылку на него. Ничто другое в визуализации не относится к React.

    3. Рисование в useEffect

    Внутри функции эффекта пишите код D3 так же, как это делали бы вне React, используя ссылку для поиска контейнера. Два масштаба выполняют вычисления координат: один соотносит метки категорий с горизонтальными позициями, а другой — значения данных с вертикальными. Высота каждой столбчатой штриховки равна нижнему краю графика за вычетом скалированного значения. Этот момент часто вызывает затруднения, поскольку ось Y в SVG направлена вниз, поэтому более высокие столбцы соответствуют меньшим значениям y.

    4. Отрисовка компонента

    Разместите компонент в своем приложении и передайте ему массив данных. При изменении массива диаграмма перерисовывается: React замечает изменение свойства, запускается функция effect, а D3 очищает и перерисовывает график. Связь между состоянием приложения и визуализацией осуществляется автоматически.

    5. Запустите приложение

    Запустите сервер разработки с помощью npm.

     npm start
    

    Или с Yarn.

     yarn start
    

    В браузере появится столбчатая диаграмма. Далее всё строится по тому же принципу: подсказки при наведении, шкалы цветов, анимированные обновления, возможность увеличения масштаба и другие типы диаграмм. Всё это реализуется с использованием вышеупомянутых функций.

    Итог

    D3 и React эффективно сотрудничают при наличии правильной структуры. React отвечает за контейнер и жизненный цикл компонентов, а D3 — за отрисовку внутри них. Соблюдайте чёткое разделение обязанностей, чтобы каждая библиотека выполняла свою функцию.

    Преимущество заключается в возможности создавать визуализации, которых нет в стандартных пакетах для диаграмм, поскольку вы работаете с примитивами, а не с параметрами настройки. Когда сам график является частью ценности продукта, это имеет большое значение. Если дашборду нужна лишь линейная диаграмма, достаточно использовать Recharts, избегая сложной настройки.