Введение в D3
D3.js (или просто D3) это JavaScript-библиотека для обработки и визуализации данных. Она предоставляет удобные утилиты для обработки и загрузки массивов данных и создания DOM-элементов. Эта заметка описывает работу с основными методами библиотеки, она подойдёт для изучения основ библиотеки и погружения в её логику и возможности.
Для понимания статьи пригодятся знания JS, HTML и CSS.
Текучий интерфейс (fluent interface)
D3 реализует подход, называемый fluent interface. При чтении кода он выглядит как цепочка методов. Каждый метод вызывается на объекте, который вернул предыдущий метод. Чтобы код было удобно читать, каждый вызов располагается на отдельной строчке:
d3.select('body') // выбор в документе body .append('svg') // добавление в body svg-контейнера .append('text') // добавление в svg-контейнер элемента text .text('Click somewhere, please. ') // изменение текста в элементе text .attr('x', 50) // задание координат элемента text .attr('y', 50) .style("fill","firebrick") // заливка текста цветом
Выборка
В D3, как и в других JS-библиотеках, работающих с DOM-элементами, взаимодействие с документом начинается с поиска элементов в документе и создания выборки — обёртки набора элементов. Она даёт доступ к методам библиотеки для модификации выбранных элементов.
Выборка (selection) в D3 создается с помощью методов d3.select() и d3.selectAll(). Для создания выборки D3 использует querySelector/querySelectorAll или Sizzle, если он подключён к странице (например, с jQuery).
d3.select('span') // выбор первого span в документе d3.selectAll('span') // выбор всех span в документе
Полученную выборку используют для работы с элементами и для создания выборки из потомков (subselection).
будет зеленым будет красным эти будут жёлтыми
останется чёрным
d3.select('span') // выбор первого span в документе .style('color', 'darkgreen') // установка цвета d3.selectAll('p') // выбор всех параграфов .selectAll('span') // выбор всех span в этих параграфах. .style('color', 'goldenrod') // установка цвета d3.select('p') // выбор первого параграфа в документе .select('span') // выбор первого span в этом параграфе .style('color', 'firebrick') // установка цвета
Всегда помните, с какой выборкой вы сейчас работаете. Распространённые ошибки при работе с D3: вызов на элементе-потомке вместо родителя и попытка изменения свойств несуществующего (удаленного или ещё не созданного) элемента.
В примере уже используются операции над элементами (selection.style(name[, value])), дальше рассмотрим их более подробно.
Вычисление значений и функторы
- selection.classed(name, value) добавляет или удаляет класс name в зависимости от булевого значения value.
- selection.on(event, callback) используется для обработки событий, передавая название события event (например, «click») и функцию-обработчик callback. Функции-обработчики вызываются с текущим элементом в this, а также data и index в аргументах. Событие можно получить в переменной d3.event. Повторная установка обработчика заменит предыдущий.
var pressed = false var button = d3.select('button') // выбор кнопки .on('click', function (data, index) < // установка обработчика нажатия мыши button.classed('pressed', pressed = !pressed) // в обработчике меняем значение переменной и вычисляем класс >)
Обратите внимание, что мы пользуемся сохранённой в переменную button выборкой: вызовы on (а так же classed, attr, style, property, html, text) возвращают выборку, на которой они вызваны, что типично для «текучих» интерфейсов.
- Если вы подадите значение, являющееся функцией, оно будет вызвано с параметрами data, index (см. ниже), а контекстом (объектом this) будет элемент, DOM-узел.
- Если вы подадите значение, не являющееся функцией, оно будет обёрнуто в «функтор» (функцию, всегда возвращающую переданное значение)
- Если вы не подадите значение, функция сработает как getter и вернёт значение, о котором идёт речь (например, selection.style(‘color’) вернёт цвет текста, если он установлен для элемента).
О последнем нюансе важно помнить, если вы строите цепочку вызовов (такой getter обычно должен быть последним элементом цепочки).
Важно понимать, что значения или функции используются один раз для каждого элемента в выборке, после чего D3 о них «забывает». Иными словами, изменения в наборе данных или события в документе не заставят D3 «повторно вычислить» значение, поэтому это поведение нужно задавать самостоятельно, как мы выше сделали с classed.
Обратите внимание на аргументы функции (data и index). Они имеют специальное значение: index — номер элемента в выборке, а data — заданный для него элемент данных. Присутствие этих параметров в каждой функции, вызываемой на выборке является одним из важнейших контрактов в D3. Это позволяет писать лаконичный код, вычисляющий состояния свойств элементов в зависимости от данных.
Типичная работа с выборкой
Рассмотрим популярные методы на более сложном примере, демонстрирующем работу с DOM-узлами документа через выборку:
var svg = d3.select('body').append('svg') svg .append('text') .text('click somewhere') .attr('x', 50) .attr('y', 50) var events = [] svg.on('click', function () < events.push(d3.event) if (events.length >5) events.shift() var circles = svg.selectAll('circle') .data(events, function (e) < return e.timeStamp >) .attr('fill', 'gray') circles .enter() .append('circle') .attr('cx', function (d) < return d.x || d.pageX >) .attr('cy', function (d) < return d.y || d.pageY >) .attr('fill', 'red') .attr('r', 10) circles .exit() .remove() >)
- Методы selection.html() и selection.text() задают или возвращают содержимое элементов в виде HTML или текста.
- Методы selection.style(), selection.attr() и selection.property() задают или возвращают CSS-свойства элемента, его аттрибуты и свойства. Чаще всего мы будем пользоваться style и attr, особенно при описании свойств новых элементов.
- Метод selection.remove() удаляет элементы текущей выборки.
- selection.append() добавляет потомка к каждому элементу текущей выборки.
- Переданные в selection.data() данные сохраняются в поле __data__ DOM-элемента, при вызове методов на выборке происходит их извлечение из элемента.
- Получить или записать данные в один элемент можно, используя selection.datum().
Связанные множества
В примере особое внимание стоит обратить на метод data(). В отличие от других методов он возвращает модифицированную выборку, хранящую помимо списка элементов соответствие данных элементам. В переводе статьи Thinking with Joins мы подробно рассказываем о методах enter() и exit() которые есть у такой выборки и возможностях которые они дают.
Анимация и настройка
Анимировать изменение свойств элемента в D3 легко, нужно вызвать метод selection.transition(). Этот метод возвращает выборку, которая постепенно изменяет текущие значения на новые, создавая анимационный эффект. Длительность анимации задаётся методом transition.duration().
Добавим анимацию при добавлении и удалении элементов в предыдущий пример:
var svg = d3.select('body').append('svg') svg .append('text') .text('click here') .attr('x', 50) .attr('y', 50) var events = [] svg.on('click', function () < events.push(d3.event) if (events.length >5) events.shift() var circles = svg.selectAll('circle') .data(events, function (e) < return e.timeStamp >) .attr('fill', 'gray') circles .enter() .append('circle') .attr('cx', function (d) < return d.x || d.pageX >) .attr('cy', function (d) < return d.y || d.pageY >) .attr('fill', 'red') .attr('r', 0) // Начальное значение .transition() .duration(1000) // Длительность перехода от начального значения к конечному .attr('r', 10) // Конечное значение circles .exit() .transition() .attr('r', 0) .remove() >)
В этой статье я рассказал о возможностях D3 по работе с выборками. Следующие заметки планирую посвятить утилитам для обработки и загрузки данных, рисованию наборов SVG-элементов и созданию интерактивных элементов визуализации.
Я преподаю D3 на курсе «Визуализация данных». Если вы хотите освоить этот инструмент и начать применять его в своей работе, приходите к нам. Ближайший курс пройдёт в Москве в эти выходные, запись и отзывы участников январского курса: http://brainwashing.pro/dataviz.
- Блог компании Лаборатория данных
- JavaScript
- Визуализация данных
Просто о D3.js
70 тысяч звездочек на гитхабе и сотни интересных проектов. Кажется, что D3 это что-то большое и очень сложное, но это не так. Я расскажу об основах D3 и поделюсь опытом разработки инфографики Бюростат.
Что такое D3
D3 это не простая библиотека, где вызов функции с нужной конфигурацией строит график. D3 это набор инструментов для визуализации данных. Он состоит из нескольких десятков небольших модулей, каждый из которых решает свою задачу. Кроме модулей для построения различных фигур, внутри D3 есть модули для работы с элементами на странице (простой аналог jQuery), загрузкой данных (аналог fetch/$.ajax, заточенный под форматы csv, json, xml и другие), форматированием и масштабированием данных, математическими функциями и другим.
SVG
Визуализация в вебе, чаще всего, строится в векторном формате. Обычно в формате SVG. Он позволяет создавать простые фигуры и работать с ними: трансформировать, позиционировать и немного влиять через CSS. Простой пример:
Для построения простых фигур можно использовать теги rect, circle и еще несколько других.
Сложные фигуры строятся по координатам. Существует два варианта написания координат: абсолютный и относительный. В первом случае координаты считаются относительно всего графика, а во втором относительно последней точки. Весь путь записывается буквами и цифрами. Относительный вариант указывается буквой в нижнем регистре, абсолютный — в верхнем.
Начиная в точке 70 0, перемещаемся относительно этой точки на 0 пикселей по x и 30 по y. И еще раз. Начальная точка обозначается буквой M, следующая координата буквой l.
Вместо простых ломаных линий можно построить кривые. Например, кривую Безье можно построить так: C x1 y1, x2 y2, x y. Здесь x1,y и x,y начальная и конечная точки, а x2,y2 точка, через которую проходит кривая.
D3 поможет абстрагироваться от координат и строить полный путь, задумываясь только о данных.
Возможности d3
Данные
Самый простой пример, который можно написать на d3 это гистограмма. Поскольку все элементы в 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 = [ , ] // Масштабируем данные по 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 фильтры, не очень хорошо работает и не подходит, если обводку нужно сделать только сверху и снизу.
Ссылки
Рассказ дизайнера Миши Капанаги про Бюростат
При подготовке материала использовались источники:
https://habr.com/ru/companies/datalaboratory/articles/217905/
https://habr.com/ru/articles/342106/