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 фильтры, не очень хорошо работает и не подходит, если обводку нужно сделать только сверху и снизу.
Ссылки
Рассказ дизайнера Миши Капанаги про Бюростат
Комментарии (21)
AndrewFoma
10.11.2017 15:01простите за прямоту, но по вашей ссылке — «бюростат», визуально — интересно, но, что с этим делать, использовать, я думаю — совсем бесполезно.
kirill3333
10.11.2017 18:05Вот тут наглядные хорошие примеры по такому типу диаграмм datavizproject.com/data-type/sankey-diagram и тут datavizproject.com/data-type/alluvial-diagram
akass
10.11.2017 18:07Сравнения ради, насколько трудозатратно на D3 сделать линейный график с полной закраской диапазонов по оси Х? Пример
anton_gcor Автор
10.11.2017 18:17+1Я специально вначале написал, что «D3 это не простая библиотека, где вызов функции с нужной конфигурацией строит график. D3 это набор инструментов для визуализации данных.»
Для совсем простых графиков D3 слишком исчерпывающее решение. Но чем сложнее должна быть визулизация, тем легче писать ее на D3. Посмотрите сколько всего интересного можно сделать: https://github.com/d3/d3/wiki/Gallery, https://datalaboratory.ru/.
Описал как сделать график с заливкой.
http://jsbin.com/wetiqiyare/1/edit?html,js,outputakass
10.11.2017 23:24Благодарю за ответ, я отлично понимаю что D3 для других задач, был осознаный интерес насколько много оверхеда на выполнение такой простой задачи. Ожидания получить в один клик и не было.
sshikov
10.11.2017 21:36Вы знаете, проще найти готовый, чем спрашивать. Вот примерно тут живут примеры https://bl.ocks.org/mbostock (это конкретно Майк, сам автор). Там диаграмм — десятки в самых разных видах.
Просто линейный график — это path в SVG, он прост (и выбор тут скорее между вариантами интерполяции, т.е. строить ли ломаную, или сглаживать ее сплайном, и т.п.).
А раскрашенные диапазоны — это просто прямоугольники. Все что вам нужно — это определить их число, и вычислить размеры (с учетом масштаба, через scale), и цвета.
Ну и понять, в первую очередь, какой у вас вообще масштаб, т.е. не логарифмический ли он, например. А дальше все прямолинейно. Причем заметьте, что scale умеет масштабировать и такие экзотические вещи, например, как цвета — т.е. вы можете создать функцию, которая отображает диапазон [0,1] на [красный, зеленый]. В любой цветовой модели, ну почти. Так что цвет раскраски тоже можно рассчитать, без усилий.
Samogonshik
10.11.2017 18:21Я бы добавил что у D3.js проблемы с совместимостью между версиями. Примеры и решения на разных версих (3й и 4й) приходится долго адаптировать.
anton_gcor Автор
10.11.2017 18:23Любую смену мажорных версий сложно адаптировать.
Я тоже столкнулся с этим, начиная писать инфографику на 3 версии и мучительно переводя впоследствии её на 4.
sshikov
10.11.2017 21:28Ну, тут все же вышел перебор. Посмотрите, из любопытства, на специализированные фреймворки построения диаграмм на базе d3. Многие из них, такие как c3, nvd3, и другие (я смотрел таких с десяток наверное) конкретно застряли на v3, и не могут уже годы перейти на v4. Ну т.е. понятно что это не просто — но об этом плохо подумали.
unsafePtr
10.11.2017 18:31-170 тысяч звездочек на гитхабе
И всего лишь 2 issue. Вот так действительно пример хорошо сделанной библиотеки.dom1n1k
10.11.2017 19:07Просто ее сейчас раздербанили на много модулей и каждый модуль в отдельном репозитории.
justboris
10.11.2017 21:02Число issue — совсем не показатель качества. Может, люди просто не знают, куда слать репорты. Или у автора есть бот, автоматически закрывающий все тикеты по кд.
Судя по таким комментариям-копипастам, я склоняюсь к версии про бота.
sshikov
10.11.2017 21:25Вы о чем? Откройте d3-scale, это отдельный модуль. 22 открытых, 69 закрытых. Нормальное такое немаленькое число задач. В других — аналогично, уж поверьте.
sshikov
10.11.2017 21:23Добавить отметки на осях позволяет модуль d3-axis. Буквально в две строчки.
Ну, это вы преувеличили :) На самом деле, это стандартный случай. Нестандартный, как обычно, может вылиться в любое число строк кода.
Например, представьте, что у вас по оси X текстовые метки. Месяцы там, или категории чего-либо. И они очень часто не влезают в ту ширину, которая им отводится между тиками. И если автор d3-axis это не предусмотрел, то… вам не повезло.
Хотя, ради объективности, надо сказать, что если элементы, из которых формируется ось, т.е. тики, текст к ним, и пр., спроектированы хорошо, то есть снабжены классами, то вы можете все же написать функцию, которая, например, повернет надписи вертикально, подвинет их по горизонтали или вертикали, лесенкой, и сделает много чего еще.
rawzes
10.11.2017 22:48Можете подсказать ещё каких-нибудь обучающих/вводных материалов для начала работы с d3?
batment
11.11.2017 03:56Мне очень помог сайт http://alignedleft.com/tutorials/d3, и вот его перевод, если нужен — http://serganbus.github.io/d3tutorials/. Получил там понимание как начать работать, а дальше уже искал конкретные методы для разных потребностей.
staticlab
Большое спасибо за статью. Отчего-то в FF текст в Бюростате получается чёрным по чёрному.
anton_gcor Автор
Исправил, спасибо