Блог

D3.js для построения диаграмм

  • Цветков Максим
  • 09.03.2017

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

Сейчас модно рассказывать про Data-Driven Design, так вот, библиотека D3 как раз расшифровывается как Data-Driven Documents, идея которого в связке данных с DOM. Сразу поговорим об ограничениях. D3 не поддерживает старые браузеры, работает на клиенте, так что спрятать оригинальные данные не выйдет. Если данные нужно прятать, то рисуйте картинку или используйте Flash (что не так уж плохо, как звучит). В остальном, D3 позволяет создавать потрясающую интерактивную инфографику. Доступные методы Bundle, Chord, Cluster, Force, Histogram, Pack, Partition, Pie, Stack, Tree, Treemap. Это не все существующие типы графиков, которые могут понадобиться, но этого достаточно для решения большинства задач.

Для начала работы нужно скачать D3 и перекинуть библиотеку в подпапку проекта. Либо использовать легкую версию библиотеки. Теперь самое время читать документацию. Без utf-8 браузер не сможет парсить данные для D3, поэтому убедитесь, что с кодировкой везде все хорошо и вы не потеряли <meta charset="utf-8">.

Структура иерархии папок должна получиться такой:

project-folder/
   d3/
      d3.v3.js
      d3.v3.min.js (optional)
   index.html

Так как мы визуализируем реальные данные, то первый вопрос: «где эти данные брать?». Должен быть JSON или GeoJSON с данными. Но сгодится и CSV файл. Для визуализации привязываем входящие данные к элементам DOM (D3 работает с JS DOM), используя d3.select("body"). Можно использовать  d3.selectall("span"), отличается он тем, что возвращает все совпадения для указанного элемента. Так, первый код вернет лишь один body, второй код вернет все элементы span, даже если их 1000. Если на странице не найдется ни одного элемента <span>, будет возвращено пустое значение. Для загрузки CSV используется код:

d3.csv("data.csv", function(data) {
    console.log(data);
});

d3.csv() это глобальный объект. Пустая функция является функцией обратного вызова, служит для передачи кода в качестве одного из параметров другого кода и срабатывает после загрузки CSV файла в память. Это позволяет быть уверенным, что d3.csv() существует к моменту обратного вызова.

Абсолютно аналогично подгружается и обрабатывается JSON файл.

d3.json("waterfallVelocities.json", function(json) { console.log(json); //Log output to console });

Итак, данные подгружены, осталось их использовать. В D3.js используется подход «fluent interface». Это кода пишется как цепочка методов, где каждый метод вызывается на объекте, который вернул предыдущий метод. В коде ниже видно, что каждый вызов располагается на отдельной строчке. Нужно выбрать определенный набор данных, это можно сделать следующим способом:

var dataset = [ 5, 10, 15, 20, 25 ];
 
			d3.select("body").selectAll("p")
				.data(dataset)
				.enter()
				.append("p")
				.text(function(d) { return d; });

Теперь мы имеем переменные данные, для которых создаем элементы DOM, удаляя и добавляя элементы в зависимости от количества полученных данных, если данные меняются. Самое интересное здесь это .enter(), служит для создания нового элемента, связанного с данными. Если у нас 20 человечков в данных, то будет создано 20 элементов DOM. Следующим этапом .append(«p») берет свежесозданных человечков и добавляет их в P. Функции select(), append(), classes() являются методами объекта selectAll. Результатом выполнения возвращается объект типа select.

Дополнительно важно знать про update(), exit(). Exit() нужен для идентификации и сопоставления и элементами, для которых нет данных, и эти элементы должны быть удалены. Update() нужен для идентификации элементов DOM, для которых уже есть данные.

Получается достаточно стандартный подход: применяем к div класс и меняем в классе свойства CSS. Для этого используется метод selection.attr(). При этом важно понимать разницу между attr() и style(). Последний применяет изменения в CSS напрямую к элементу, а attr() применяет изменения к атрибутам DOM. Это важно понимать, так как CSS штука понятная и учится за 15 минут, но на поиск удобного способа организации CSS уходят годы. На помощь приходит Styled components, или CSS in JS.

Работать с Canvas интересно, но куда интереснее работать с SVG. Мы можем вместо создания div создавать rect, сформировав SVG. Для выбора всех rect в SVG будет использоваться selectall. Добавляем rect в DOM, учитывая что он обязан содержать атрибуты xywidth, и height.  Вместо «left» и «top» для HTML-элементов задаются координаты «x» и «y» для SVG.

Так, код позволяет указать следующие размеры: прямоугольник 150px по высоте, 60px в ширину, отступ слева 60 и 10 отступ сверху. Цвет по умолчанию черный. Разумеется, в дальнейшем понадобится менять эти параметры в зависимости от данных, которые мы будем подгружать.

.attr("height","150")
.attr("width","60")
.attr("x", "60"})
.attr("y","10");

svg.append("rect").attr({“x”:”60px”, “y”:”10px”, "width":"60px", "height":"150px"});

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

var circles = svgContainer.selectAll("circle") // выбираем все SVG-шные круги
                   .data(circleRadii)  //применяем данные к выбранным кружкам
                   .enter()  //выбираем виртуальные недостающие элементы
                   .append("circle");  //добавляем элементы

See the Pen D3 Max Tsvetkov by Maxim (@Yourscorpion) on CodePen.


Мы нарисовали одинокий векторный квадратик, что довольно скучно. SVG может содержать не только квадратики, но и круги (circle), линии (path) и текст (text). Перейдем к более интересным возможностям, украсив наш график и визуализировав данные. В данном случае мы возьмем не готовые данные, а генерируем их каждый раз с нуля.

See the Pen D3 Max Tsvetkov by Maxim (@Yourscorpion) on CodePen.

Уже не плохо, вполне сгодится в качестве инфографики в веб-издание средней руки. 

Можно возразить, сказав, что такой график можно нарисовать в любой графической программе. В том же Illustrator есть Graph Tool, которым можно нарисовать нечто похожее за 1 минуту. Но кодом можно нарисовать диаграммы, которые в графическом редакторе попросту невозможно изобразить. Например, chord diagram. Основывается на квадратной матрице, показывает отношение между двумя наборами данных. Такая диаграмма может наглядно показать, что 14% пользователей устройств Samsung раньше использовали телефоны от Apple, и 21% владельцев телефонов Apple раньше использовали телефоны Samsung.

See the Pen Сhord diagram test by Maxim (@Yourscorpion) on CodePen.

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

See the Pen Chord Max Tsvetkov by Maxim (@Yourscorpion) on CodePen.

8 комментариев

  1. Alexander Koretskiy

    04.12.2017

    Здравствуйте. По какой формуле можно добавить к второму примеру круговые подписи?

    • Цветков Максим

      04.12.2017

      Вы про нахождение радианы углов? По правилам тригонометрии, косинусы, тангенсы, синусы.

      Например, любой треугольник делится на прямоугольные треугольники, и с ними проще работать. Освежим в памяти основы, которые преподавались еще в школе. Если длина гипотенузы неизвестна (B), но треугольники геометрически подобны, отсюда формула на картинке ниже.

      В левом треугольнике можно применить Теорему Пифагора, по которой сумма квадратов катетов равна квадрату гипотенузы. Sin²α + Cos²α = 1.


      Sinα = Cosβ = Cos (90° — α)
      Cosα = Sinβ = Sin (90° — α)

      И ответ на ваш вопрос, как описанное выше ложится на вычисление позиции заголовков вокруг диаграммы. По осям X,Y косинус и синус следят за положением нужной точки на границе круга.

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

      14² = 196. 9² = 81. 196+81=277. Корень из 277 это 16.6.
      tgα = ctgβ = 9/14. ctgα = tgβ = 14/9.
      cosα = sinβ = 14/16,6. sinα = cosβ = 9/16,6.

      Расчет достаточно простой. Угол 30 = радиане 0.523598 = cos 0.866. Проверяем:

      var A=30; // угол 0-360 (radians)
      with(Math){
      n=cos(A*PI/180); //Pi равно 3,14, A переменная. 180 для формульной записи углов.
      } 
      console.log(n);
      • Марк

        25.04.2023

        Если нужно добавить некие атмосферные эффекты, или освещение — куда копать?

        • Цветков Максим

          25.04.2023

          Это копать в сторону линейной алгебры и интегральных исчислений, именно они применяются для освещения, атмосферных эффектов. Хотя бы производные интегралы и сведение неопределенных интегралов. Тригонометрия для 3D.

  2. Герман

    18.02.2021

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

    • Цветков Максим

      18.02.2021

      Flash был быстрый и векторный, с очень хорошей математикой, что позволяло управлять степенью сглаженности вектора. Алгоритм такой: когда линия проходила по пикселям, flash определял по 16-и точкам, какие части фигуры попадают в пиксель и таким образом высчитывал средний цвет. А Canvas 2D считает геометическую площадь, что очень хорошо для одной фигуры, но если две фигуры пересекаются, то идут баги.

      Важно смириться с простой идеей: когда начали продвигать html5 (Canvas 2D и WebGL на основе мобильного ES), они сильно уступали и уступают Flash по кол-ву фичей. Надежда была, что крупные разработчики сами допилят все нужное. Но всем пришлось быстро переводить проекты с Flash на HTML5, в результате чего на рынке зоопарк из Phaser, CreateJS, PIXI, но без крутой математики. Например, Blur на CPU и GPU делается по разному, посмотрите на квадратный blur в Figma.

  3. Василий GDS

    21.02.2021

    Привет! выбираем библиотеку для построения огромной карты сети. На что следовало бы обратить внимание, какие-нибудь советы?

    • Цветков Максим

      21.02.2021

      Библиотек много, я бы выбирал из:
      D3 + webcola и на canvas
      gojs
      jointjs
      cytoscape
      G6 Ant
      ArcGis
      Yworks
      visjs

      А по существу смотрите на производительность. В webGL есть такое понятие дальние земли. Обычно все числа в JS с двойной точностью, но те, что уходят на шейдер могут иметь координаты во float одинарной точности (single precision). На GPU внутри шейдера точность чисел небольшая, и числа с плавающей точкой должны быть маленькие. Шейдер это всего лишь конечное звено программирования графики, поэтому он зачастую не особо гибкий. Разные видеокарты по разному считают, это большая боль webGL, особенно в мобайле. Для дебага хороша команда:

      TASKKILL /IM chrome.exe /F
      cd "C:Program Files (x86)GoogleChromeApplication"
      chrome.exe --disable-gpu-vsync

      , полезно докинуть --disable-frame-rate-limit.
      Либо зайти в NVIDIA Control Panel / Catalyst Control Center и отключить VSYNC. Либо по старинке, setInterval.

Оставить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.