D3, сокращенно от Data-Driven Documents, —  это библиотека JavaScript, которая позволяет отображать данные самыми разными способами и содействует проверке и управлению элементами DOM.

D3 не относится к разряду типичных библиотек для построения графиков и диаграмм. Она обладает намного большей гибкостью и усилена возможностями Recharts, C3js и Raw graphs. Ее преимущество очевидно: там, где обычная библиотека терпит фиаско, когда вы слишком увлекаетесь индивидуальными настройками, D3 продолжает уверенно работать 

В статье мы познакомимся с D3 на основе конкретных примеров. Общий процесс сводится к следующим этапам: загрузка данных  —  выборка элементов  —  привязка данных  —  создание/изменение/ удаление элементов.

Но прежде немного порисуем на веб-странице и попрактикуемся в применении селекторов и фигур разных цветов. 

Рисование базовых фигур 

Как же конкретно рисовать с D3? Прежде всего создаем HTML-страницу, содержащую пустой элемент SVG (аббревиатура расшифровывается как язык разметки масштабируемой векторной графики). Для тех кто не знает, SVG  —  это изображение, состоящее из основных геометрических фигур, благодаря чему оно становится хорошо масштабируемым. Как раз для рисования таких фигур и понадобится D3. 

Сначала для импорта D3 создаем HTML-документ, с который мы будем работать на протяжении всей статьи. Редактироваться будет только код между выделенными тегами скрипта. 

В строке 4 импортируем D3. Если у вас frontend-фреймворк с Node.js, то вы можете использовать NPM. В строке 7 определяем элемент SVG, в котором будем рисовать фигуры.

Напишем первые строки кода D3:

В строке 2 задействуем метод d3.select(). Он будет искать первый элемент с CSS-селектором, который вы передали в функцию. В рассматриваемом примере на странице только один тег <svg>, поэтому он вернет единственно имеющийся элемент SVG.

Позже обратимся к другому методу, а именно d3.selectAll(), который вернет не один, а все элементы, соответствующие CSS-селектору. 

В строке 4 к элементу SVG добавляем “circle” (круг). Помимо него в нашем распоряжении есть еще множество разных фигур. В строке 5 применяем цепочку методов D3, что позволяет поочередно вызывать несколько функций и избавляет от необходимости писать svg.function(...) в каждой отдельной строке. 

В строках 5, 6, 7 и 8 устанавливаем атрибуты с помощью функции d3.attr, которые потребуются для визуализации фигуры. cx и cy —  координаты x и y центра круга; r —  радиус; fill —  цвет заливки. Это фиксированные атрибуты. С примерами их использования для основных геометрических форм вы можете ознакомиться на freeCopeCamp.

Итак, в браузере увидим следующий результат: 

Кто-то может подумать, что здесь нет ничего особенного. Однако позволю себе не согласиться. Даже самые сложные графические изображения складываются из нескольких простых фигур. 

Теперь дополним изображение квадратом: 

Добавляем фигуру rect и устанавливаем нужные атрибуты для ее отображения. Затем выполняем код и получаем следующий результат: 

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

Загрузка данных 

Для получения разных типов данных в D3 существует множество функций. Начнем с самых интересных: d3.json('...'), d3.csv('...') и d3.xml('...').

Для вышеуказанной цели все из них предусматривают применение Fetch. CSV, в отличие от JSON и XML, не допускает вложенных объектов.

В последующих разделах мы будем работать с файлом данных (socialnetwork.json) из вымышленной социальной сети и стилями, представленными ниже:

Сначала построим гистограмму для библиотеки. По горизонтали отобразим годы (years), а по вертикали  —  количество комментариев (comments). Для этой цели воспользуемся соответственно d3.axisBottom() и d3.axisLeft(). В коде загрузка данных и рисование осей происходят следующим образом: 

Как много кода! Без паники, рассмотрим каждую строку по отдельности. 

В строке 3 получаем JSON. Поскольку d3.json() возвращает промис, нам не обойтись без  .then() или await, чтобы дождаться окончания процедуры. Как только это происходит, в строках 5 и 10 приступаем к созданию функций масштабирования. 

Функции масштабирования 

Функции масштабирования играют важную роль в отрисовке данных. Допустим, мы рассматриваем временной промежуток с 2015 по 2020 год при разрешении 500 х 500 пикселей. При отрисовке номера года на оси x необходимо сопоставить 2015–2020 в 0–500.

Самое время для функций масштабирования. Одна из них, .scaleLinear(), линейно преобразует один диапазон чисел (domain) в другой (range). В строке 7 мы передаем этой функции диапазон (range)[100,800], поскольку нам нужно начать отрисовку с 100-го пикселя по оси x, а закончить на 800-м.

После этого происходит передача диапазона входных значений (domain), которые надлежит преобразовать. .domain() ожидает массив с минимальным и максимальным значениями. К счастью, у D3 есть функции d3.extent(), d3.max() и d3.min(). В строке 8 устанавливаем минимальное значение minimum of years — 1 и максимальное maximum of years + 1. Это позволит в дальнейшем отцентрировать столбцы гистограммы. 

В строке 13 делаем немного по-другому, поскольку ось должна начинаться с 0, а заканчиваться максимальным числом комментариев. Наша задача  —  отобразить диапазон от 0 до 150, а не от 50 до 150. 

d3.extent() вернет массив из максимального и минимального значений [max, min], избавляя от необходимости вызывать их по отдельности.  

Приступаем к рисованию оси: в строке 15 добавляем ось x. В целях стилизации присваиваем ей класс и затем перемещаем элемент g, поскольку ось x всегда рисуется из начала координат, расположенного в верхнем левом углу. Поэтому сдвигаем g на горизонталь, после чего выполняем .call(d3.axisBottom(x).tickFormat(d3.format("d")). Функция  .axisBottom рисует ось. (Обратите внимание, что мы передали ей функцию range, поэтому она знает, в каком диапазоне range/domain нужно выполняться). Затем дополнительная функция .tickFormat() сообщает D3, как отображать деления на оси. В данном случае используем "d" для обозначения простых чисел в интервале от 2014 до 2021.

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

Полученный результат: 

Выборка элементов и привязка данных 

На этом этапе нужно нарисовать прямоугольники для каждого года в зависимости от числа комментариев. С этой целью проведем выборку SVG и привязку данных. Пишем следующий код: 

Сначала воспользуемся функцией d3.select() для получения SVG. (Обратите внимание, что в предыдущих фрагментах кода мы ее сохранили в переменной svg). Затем с помощью селектора CSS selector .barchartrectangle проводим поиск и отбираем все существующие прямоугольники гистограммы. Это связано с процессами обновления: если массив изменится, мы будем выбирать имеющиеся прямоугольники для их корректировки либо удаления.

Далее вызываем функцию .data(), которая содержит один обязательный аргумент, представляющий data, которые нужно отрисовать. При этом второй аргумент является необязательным и определяет ключ. Для этого мы передаем ему функцию, принимающую элемент (item) и индекс (index). В данном примере возвращается индекс. 

Однако, как мы увидим в следующем разделе, возможно применение поля элемента для выявления тех элементов, которые подлежат изменению/удалению/вставке. При наличии свойства id в датасете можно написать что-то подобное (item, index)=> item.id

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

Создание, изменение и удаление элементов 

Когда происходит изменение данных в массиве (например, путем фильтрации), D3 сравнивает массивы и решает, что делать. 

Здесь мы рассмотрим только 2 наиболее важные функции: добавление .enter() и удаление .exit().

Понять логику поможет следующий код: 

Рассмотрим использование этого массива в качестве данных [1,2,3] при первом выполнении кода. 

Селектор .barchartrectangle вернет 0 элементов, поэтому D3 в курсе, что элементы в массиве являются тремя новыми фрагментами данных. Следовательно, она задействует функции, измененные на инструкцию .enter(). В ней мы присоединяем новые rect каждому новому фрагменту данных и присваиваем им класс barchartrectangle. Поскольку “удалений” нет, инструкция .exit() будет проигнорирована. 

Теперь при изменении массива на [1,2] и повторном выполнении кода селектор .barchartrectangle вернет три существующих элемента. Однако D3 отметит наличие в массиве только двух. Тогда она воспользуется функциями, связанными с инструкцией .exit(), и в этом случае удалит .remove() третий из них. 

Как D3 понимает, какой элемент следует удалять? Это зависит от ключа данных, определенного ранее. В нашем примере им по-прежнему является индекс, поэтому он удалит третий добавленный элемент. Но если элемент оказался фактическим id (item, index)=> item, и мы изменили массив на [1,3], то вместо третьего будет удален второй barchartrectangle.

Работа инструкций .enter() и .exit() теперь понятна, переходим к отображению гистограммы. 

В строке 4 применяем .enter() и тем самым инструктируем D3, что делать с данными. В строке 5 добавляем новый rect и устанавливаем необходимые атрибуты. Как видно в строках 6 и 7, функция .attr() принимает не только фиксированные значения, но и функцию, которая использует элемент и его индекс, что позволяет отрисовывать элементы в зависимости от данных. 

Сначала в строке 6 получаем масштабированное положение x путем вызова функции масштабирования x, определенной ранее с номером года. В ответ вернется значение пикселя, на котором нужно разместить гистограмму. Поскольку далее ширина будет установлена как 100, то мы подстраиваем столбец к центральной точке этого значения, выполняя -50. Для координаты y просто вызываем функцию масштабирования. В строке 9 для определения высоты выполняем 800- y(item.comments) из-за обратной функции масштабирования. Напомню, что диапазон был определен как [800, 100], поэтому малые значения комментариев будут отображаться в большие значения пикселей. В связи с этим мы обращаем эту функцию, выполняя 800 — y(item.comments).

Наконец, для привлекательности задаем синий цвет заливки. Итоговый результат: 

Отлично! На основе данных мы отрисовали на экране гистограмму. В этом и состоит вся суть D3. Но у нее намного больше возможностей. На D3.js — Data-Driven Documents (d3js.org) вы можете ознакомиться с интересными графиками и фигурами. 

Заключение

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

А впереди еще много интересного: геокартирование, управление HTML, визуализация сетевых процессов и т.д. D3 ценна прежде всего тем, что предоставляет почти безграничные возможности, которыми вы обязательно должны воспользоваться.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Diederik Mathijs: A Quick Look at D3.js

Предыдущая статьяКак добавить множественные примеры запросов и ответов в FastAPI
Следующая статья9 навыков, которые нужно освоить в самом начале карьеры программиста