- PVSM.RU - https://www.pvsm.ru -
70 тысяч звездочек на гитхабе и сотни интересных проектов. Кажется, что D3 [1] это что-то большое и очень сложное, но это не так. Я расскажу об основах D3 и поделюсь опытом разработки инфографики Бюростат [2].
D3 это не простая библиотека, где вызов функции с нужной конфигурацией строит график. D3 это набор инструментов для визуализации данных. Он состоит из нескольких десятков небольших модулей [3], каждый из которых решает свою задачу. Кроме модулей для построения различных фигур, внутри D3 есть модули для работы с элементами на странице (простой аналог jQuery), загрузкой данных (аналог fetch/$.ajax, заточенный под форматы csv, json, xml и другие), форматированием и масштабированием данных, математическими функциями и другим.
Визуализация в вебе, чаще всего, строится в векторном формате. Обычно в формате 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 и еще несколько других [4].
Сложные фигуры строятся по координатам. Существует два варианта написания координат: абсолютный и относительный. В первом случае координаты считаются относительно всего графика, а во втором относительно последней точки. Весь путь записывается буквами и цифрами. Относительный вариант указывается буквой в нижнем регистре, абсолютный — в верхнем.
<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 это гистограмма. Поскольку все элементы в 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 [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 есть достаточно инструментов для построения сложных графиков:
Инфографика [2] состоит из трех уровней, в каждом из которых есть список имен, график и номера позиций. Номера позиций изначально скрыты и появляются по ховеру. Сверху находится ось с датами.
Исходные данные хранились в эксель-файлах в открытом доступе. Их нужно было просто преобразовать в большой json-файл, высчитав позиции студента в нужный день. К сожалению, в данных был беспорядок. Небольшой список того, что нужно проверить в наборе данных:
Линия в инфографике нестандартная: 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. Достаточно запретить скролл страницы по вертикали и дать возможность скроллить вместо этого график.
В svg не работает свойство z-index. Z-index в svg рассчитывается из позиции элемента в коде. Чем позже элемент, тем выше он будет. В случае, если нужно вынести линию выше всех при ховере, придется пересортировать линии и вынести нужную наверх.
Но хуже всего, что этот метод в случае с линиями не поможет. Дело в том, что линия ховера определяется областью fill. А эта область строится между конечной и начальной точками. В итоге, если постоянно выносить линии наверх, то в графике получится бардак. Какая-нибудь линия обязательно перекроит другую.
Чтобы этого хауса в линиях не было, при ховер я выношу наверх не саму линию, а ее копию. После того, как ховер сместился на другую линию, предыдущую копию я удаляю.
Если мне нужно обработать клик по линии, то это нужно уже делать не на линии, а на копии.
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/
Нажмите здесь для печати.