javascript

Просто о D3.js

  • суббота, 11 ноября 2017 г. в 03:14:09
https://habrahabr.ru/post/342106/
  • Работа с векторной графикой
  • Визуализация данных
  • JavaScript



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


Что такое D3


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


SVG


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



<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 и еще несколько других.


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


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

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



Вместо простых ломаных линий можно построить кривые. Например, кривую Безье можно построить так: 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 считаются от левого верхнего угла, столбики гистограммы рисуются сверху вниз



<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.


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. Буквально в две строчки.


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 есть достаточно инструментов для построения сложных графиков:



Бюростат



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


Сложности


Данные


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


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

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



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


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

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


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. А эта область строится между конечной и начальной точками. В итоге, если постоянно выносить линии наверх, то в графике получится бардак. Какая-нибудь линия обязательно перекроит другую.



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


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


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



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


Ссылки


Рассказ дизайнера Миши Капанаги про Бюростат