- PVSM.RU - https://www.pvsm.ru -

Просто о D3.js

Просто о D3.js - 1

70 тысяч звездочек на гитхабе и сотни интересных проектов. Кажется, что D3 [1] это что-то большое и очень сложное, но это не так. Я расскажу об основах D3 и поделюсь опытом разработки инфографики Бюростат [2].

Что такое D3

D3 это не простая библиотека, где вызов функции с нужной конфигурацией строит график. D3 это набор инструментов для визуализации данных. Он состоит из нескольких десятков небольших модулей [3], каждый из которых решает свою задачу. Кроме модулей для построения различных фигур, внутри D3 есть модули для работы с элементами на странице (простой аналог jQuery), загрузкой данных (аналог fetch/$.ajax, заточенный под форматы csv, json, xml и другие), форматированием и масштабированием данных, математическими функциями и другим.

SVG

Визуализация в вебе, чаще всего, строится в векторном формате. Обычно в формате SVG. Он позволяет создавать простые фигуры и работать с ними: трансформировать, позиционировать и немного влиять через CSS. Простой пример:

Просто о D3.js - 2

<rect width="30" height="30"></rect>
<circle cx="50" cy="15" r="15" ></circle>
<path d="M105,0L105,30L135,30"></path>
<path d="M70,0l0,30l30,0"></path>

Для построения простых фигур можно использовать теги rect, circle и еще несколько других [4].

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

<path d="M70,0l0,30l30,0"></path>

Начиная в точке 70 0, перемещаемся относительно этой точки на 0 пикселей по x и 30 по y. И еще раз. Начальная точка обозначается буквой M, следующая координата буквой l.

Просто о D3.js - 3

Вместо простых ломаных линий можно построить кривые. Например, кривую Безье можно построить так: C x1 y1, x2 y2, x y. Здесь x1,y и x,y начальная и конечная точки, а x2,y2 точка, через которую проходит кривая.

<path d="M0 20 C 0 0, 10 0, 50 20" stroke="black" fill="none"/>

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

Возможности d3

Данные

Самый простой пример, который можно написать на d3 это гистограмма. Поскольку все элементы в svg считаются от левого верхнего угла, столбики гистограммы рисуются сверху вниз

Просто о D3.js - 4

<svg>
    <rect width="20" height="20" x="0"></rect>
    <rect width="20" height="100" x="20"></rect>
    <rect width="20" height="60" x="40"></rect>
    <rect width="20" height="40" x="60"></rect>
    <rect width="20" height="70" x="80"></rect>
</svg>

// Данные для визуализации в пикселях
var data = [20, 100, 60, 40, 70]
// Ширина столбика гистограммы
var barWidth = 20

// Аналог document.querySelector('svg') или $('svg')
d3.select("svg")
  // Самая сложная для понимания часть.  
  // D3 связывает еще не созданные элементы с данными.
  .selectAll("rect")
  .data(data)
  .enter()   
  // Код ниже выполнится 5 раз. Ровно столько у нас данных.

  // Добавляем прямоугольник тегом rect с нужной шириной, 
  // высотой и координатами. Код похож на jQuery.
  .append("rect")
  .attr("width", barWidth)
  .attr("height", d => d)
  // Изначально все прямоугольники спозиционированы  
  // абсолютно и находятся в координате 0,0  
  // Сдвигаем прямоугольники по оси x, на [barWidth * i]
  .attr("x", (d, i) => barWidth * i)

Масштаб

Но, представим, что в качестве данных пришли даты. Их нужно трансформировать в координаты. Для этого понадобится модуль d3-scale [5].

var x = d3.scaleTime()
  // минимальное и максимальное значение х: 1 и 9 января 2017 года
  .domain([new Date(2017, 0, 1), new Date(2017, 0, 9)])
  // ширина графика 1000 пикселей
  .range([0, 1000])

// Точка 5 января будет в координате 500 пикселей
x(new Date(2017, 0, 5)) // 500

Координата y отображает цифры в пределе от 1 до 13 млн на ширине в 480 пикселей. Тогда точка 2 млн будет на координате 80

var y = d3.scaleLinear()
    .domain([1000000, 13000000])
    .range([0, 480]);

y(2000000); // 80

Модуль также позволяет высчитывать цвет относительно данных.

Подгрузка данных

d3.json, d3.json, d3.csv,… — аналог fetch или $.ajax с обработкой нужного формата данных.

d3.csv('data.csv', (err, res) => {

})

Оси

Добавить отметки на осях позволяет модуль d3-axis [6]. Буквально в две строчки.

g.append("g") 
 .call(d3.axisLeft(y))

События

Синтаксис D3 иногда похож на jQuery. Код ниже добавляет элемент li в список, который удаляется по клику на него.

d3.select("ul")
  .append("li")
  .on('click', function (d) {
    d3.select(this)
      .remove()
  })

Линия

D3 предоставляет некоторую абстракцию, которая помогает не думать над координатами.

var data = [
    {date: 1510299186768, value: 10},
    {date: 1510299195000, value: 40}
]

// Масштабируем данные по x
var x = d3.scaleTime()
    // d3.extent(data, d => d.date) возвратит массив 
    // из максимального и минимального элементов
    .domain(d3.extent(data, d => d.date))
    .range([0, width])

// Масштабируем данные по y
var y = d3.scaleLinear()
    .domain(d3.extent(data, d => +d.value))
    .range([height, 0])

// Объявляем функцию линию
var line = d3.line()
    .x(d => x(d.date))
    .y(d => y(d.value))

// Функция line сгенерирует последовательность координат
path.attr('d', line)

Другие графики

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

Бюростат

Просто о D3.js - 5

Инфографика [2] состоит из трех уровней, в каждом из которых есть список имен, график и номера позиций. Номера позиций изначально скрыты и появляются по ховеру. Сверху находится ось с датами.

Сложности

Данные

Исходные данные хранились в эксель-файлах в открытом доступе. Их нужно было просто преобразовать в большой json-файл, высчитав позиции студента в нужный день. К сожалению, в данных был беспорядок. Небольшой список того, что нужно проверить в наборе данных:

  • е и ё в разных местах
  • несколько данных на один срок
  • уменьшительно-ласкательные имена
  • девушка вышла замуж и сменила фамилию
  • разный формат заголовков
  • случайное повторение людей

Кастомная линия

Просто о D3.js - 6

Линия в инфографике нестандартная: 15 пикселей на переход между датами, 15 пикселей прямая. В D3 изначально есть несколько вариантов кривых [10], их можно выбирать функцией curve.

var line = d3.line()
  .x(d => x(d.date))
  .y(d => y(d.value))
  .curve(d3.curveMonotoneX)

Нужной кривой среди дефолтных не оказалось. Но, к счастью, D3 позволяет создавать свои кастомные кривые. За основу я взял простую кривую [11] и немного изменил.

function point(that, x, y) {
    // Если следующая точка выше текущей, 
    // то кривая будет выпуклой, иначе вогнутой
    let concaveCenter = that._x1 - (that._x1 - that._x0) / 2
    let convexCenter = that._x0 - (that._x0 - that._x1) / 2
    let currentCenter = that._y1 > that._y0 ? convexCenter : concaveCenter

    // Кривая Безье о которой я писал выше.
    that._context.bezierCurveTo(
        concaveCenter,
        that._y0,
        currentCenter,
        that._y1,
        that._x1,
        that._y1
    )
    // 15 пикселей прямая
    that._context.lineTo(that._x1 + 15, that._y1)
}

Выравнивание по центру

text-align:center в svg не работает, но существует аналог. Свойство text-anchor со значениями start, middle и end.

Прибитая к верху шапка

Даты должны быть прибиты к верху. Но обычный position:fixed не поможет, потому что блок с датами должен скроллиться по горизонтали. Решать задачу через js не стоит, потому что это будет тормозить. Есть способ решения через css. Достаточно запретить скролл страницы по вертикали и дать возможность скроллить вместо этого график.

z-index

В svg не работает свойство z-index. Z-index в svg рассчитывается из позиции элемента в коде. Чем позже элемент, тем выше он будет. В случае, если нужно вынести линию выше всех при ховере, придется пересортировать линии и вынести нужную наверх.

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

Просто о D3.js - 7

Чтобы этого хауса в линиях не было, при ховер я выношу наверх не саму линию, а ее копию. После того, как ховер сместился на другую линию, предыдущую копию я удаляю.

Если мне нужно обработать клик по линии, то это нужно уже делать не на линии, а на копии.

Обводка у линии

Просто о D3.js - 8

stroke задает цвет линии, fill цвет заливки. Нормального способа сделать у линии обводку нет. outline, box-shadow, border не работают внутри svg. Самый простой способ сделать обводку — дублировать код. То есть подложить линию с цветом обводки под основную линию. Другой способ, через svg фильтры, не очень хорошо работает и не подходит, если обводку нужно сделать только сверху и снизу.

Автор: anton_gcor

Источник [12]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/267988

Ссылки в тексте:

[1] D3: https://github.com/d3/d3/wiki/Gallery

[2] Бюростат: http://burostat.ru

[3] модулей: https://github.com/d3

[4] несколько других: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Basic_Shapes

[5] d3-scale: https://github.com/d3/d3-scale

[6] d3-axis: https://github.com/d3/d3-axis

[7] Дерево: https://bl.ocks.org/mbostock/4339184

[8] Граф: https://bl.ocks.org/mbostock/ad70335eeef6d167bc36fd3c04378048

[9] Круговая диаграмма: https://bl.ocks.org/mbostock/99f0a6533f7c949cf8b8

[10] кривых: https://github.com/d3/d3-shape#curves

[11] простую кривую: https://github.com/d3/d3-shape/blob/master/src/curve/basis.js

[12] Источник: https://habrahabr.ru/post/342106/