Простые графики с помощью D3.js
D3.js (или просто D3) — это JavaScript-библиотека для обработки и визуализации данных с невероятно огромными возможностями. Я, когда впервые узнал про нее, наверное, потратил не менее двух часов, просто просматривая примеры визуализации данных, созданных на D3. И конечно, когда мне самому понадобилось строить графики для небольшого внутреннего сайта на нашем предприятии, первым делом вспомнил про D3 и с мыслью, что “сейчас я всех удивлю крутейшей визуализацией”, взялся изучать исходники примеров…
… и понял, что сам абсолютно ничего не понимаю! Странная логика работы библиотеки, в примерах целая куча строк кода, чтобы создать простейший график — это был конечно же удар, главным образом по самолюбию. Ладно, утер сопли — понял, что с наскоку D3 не взять и для понимания этой библиотеки надо начинать с самых ее основ. Потому решил пойти другим путем — взять за основу для своих графиков одну из библиотек — надстроек на D3. Как выяснилось, библиотек таких довольно много — значит не один я такой, непонимающий (говорило мое поднимающееся из пепла самолюбие).
Попробовав несколько библиотек, остановился на dimple как на более или менее подходящей для моих нужд, отстроил с ее помощью все свои графики, но неудовлетворенность осталась. Некоторые вещи работали не так, как хотелось бы, другой функционал без глубокого копания в dimple не удалось реализовать, и он был отложен. Да и вообще, если нужно глубоко копать, то лучше это делать напрямую с D3, а не с дополнительной настройкой, богатый функционал которой в моем случае используется не более, чем на пять-десять процентов, а вот нужных мне настроек наоборот не хватало. И поэтому случилось то, что случилось — D3.js.
Попытка номер дваПервым делом перечитал все, что есть по D3 на Хабре. И в комментарии под одной из статей увидел ссылку на книгу Interactive Data Visualization for the Web. Открыл, глянул, начал читать — и моментально втянулся! Книга написана на простом и понятном английском, плюс автор сам по себе прекрасный и интересный рассказчик, хорошо раскрывающий тему D3 с азов.
По результатам чтения этой книги (а так же параллельно кучи другой документации по теме) я и написал для себя небольшую (правильнее наверное сказать — микроскопическую) библиотеку по построению простых и минималистичных линейных графиков. И в свою очередь на примере этой библиотеки хочу показать, что строить графики с помощью D3.js совсем не сложно.
Итак (мое самое любимое слово), приступим.
Первым делом давайте решим, какие данные мы хотим отстроить в виде графика. Я решил не вымучивать из себя набор условных данных, а взял реальные, с которыми сталкиваюсь каждый день, упростив и обезличив их для лучшего понимания.
Представьте себе какой нибудь завод по добыче и переработке, допустим, железной руды на каком нибудь условном месторождении (“свечной заводик бери”, — напоминают мне крылатую фразу классиков литературы из-за плеча, но данные уже подготовлены, — потому свечной заводик отложен до следующего раза).
Итак, добываем руду и выпускаем железо. Есть план добычи, составленный технологами, учитывающий геологические особенности месторождения, производственные мощности, и тд. и тп. План (в нашем случае) разбит по месяцам, и видно, сколько в тот или иной месяц необходимо добыть руды и выплавить железа, чтобы выполнить этот план. Есть также факт — ежемесячные фактические данные по выполнению плана. Давайте все это и отобразим в виде графика.
Вышеназванные данные сервер нам будет предоставлять в виде следующего tsv файла:
Где в столбце Category находятся плановые или фактические значения, Date — это данные за каждый месяц (у нас датируются 25-м числом), Metal month — сколько металла за месяц, которое мы запланировали (или получили) и столбец Mined % — какой процент металла добыт на текущий момент.
С данными думаю все понятно, теперь приступаем к программе. Весь код, как то вызовы библиотек, css стили и тому подобное, я показывать не буду, чтобы не загромождать статью и сосредоточусь на главном, а описанный здесь пример вы сможете скачать гитхаба по ссылке в конце статьи.
Первым делом с помощью функции d3.tsv загрузим данные:
Загрузка данных в D3 очень проста. Если вам нужно загрузить данные в другом формате, например в csv, просто меняйте меняете вызов с d3.tsv на d3.сsv. Ваши данные в формате JSON? Меняете вызов на d3.json. Я пробовал все три формата и остановился на tsv, как на наиболее удобным для меня. Вы же можете использовать любой, какой понравится, или вообще генерировать данные непосредственно в программе.
На приведенном рисунке можно увидеть, как выглядят загруженные данные в нашей программе.
Если присмотреться внимательно к рисунку, то видно, что данные у нас загружены в виде строк, потому следующий этап работы программы — это приведение дат к типу данных date, а цифровых значений — к типу numeric. Без этих приведений D3 не сможет правильно обрабатывать даты, а к цифровым значениям будет применять избирательный подход, т.е. какие-то цифры будут браться, а другие — просто игнорироваться. Для этих приведений вызовем следующую функцию:
В параметрах этой функции мы передаем название столбца, в котором записаны даты, формат даты в соответствии с правилами записи дат в D3; затем идет массив с названиями столбцов, для которых нужно сделать преобразование цифровых значений. И последний параметр — это данные, которые мы загрузили ранее. Сама функция преобразования совсем небольшая и потому, чтобы снова к ней не возвращаться, приведу ее сразу:
Здесь мы инициализируем функцию прасинга дат и затем для каждой строки данных конвертируем даты, а для заданных столбцов переводим строки в цифры.
После выполнения данной функции наши данные представляются уже в таком виде:
Сразу отвечаю на возможный вопрос — Зачем в этой функции усложнение с указанием списка столбцов, в которых нужно форматировать цифровые данные? — и ответ этот прост: в реальной таблице может быть (и есть) гораздо большее количество столбцов и не все из них могут быть цифровые. Да и строить непременно по всем столбцам графики мы не будем, так зачем же лишние манипуляции по преобразованию данных?
Прежде чем перейти к следующему действию, вспомним наш файл данных — в нем последовательно записаны сначала фактические, а затем проектные данные. Если мы сейчас отстроим данные, как они есть, то получим полную кашу. Потому что и факт, и план отрисуются в виде одной диаграммы. Поэтому проводим еще одну манипуляцию с данными при помощи функции D3 с любопытным названием nest (гнездо):
В результате работы этой функции получаем следующий набор данных:
где мы видим что наш массив данных уже разбит на два подмассива: один факт, другой план.
Все, с подготовкой данных мы закончили — теперь переходим к заданию параметров для построения графика:
Здесь все просто:
Параметр Значение parentSelector id элемента нашей странички, в котором будет отстроен график width: 600 ширина height: 300 высота title: "Iron mine work" заголовок xColumn: "Date" название столбца, из которого будут браться координаты для оси Х xColumnDate: true если true, то ось x — это даты (к сожалению, данный функционал еще недоделан, т.е. по оси x мы можем строить только даты) yLeftAxisName: "Tonnes" название левой оси y yRightAxisName: "%" названия правой оси y categories: долго думал, как же назвать то. что вылетает из “гнезда” D3 и ничего лучше категорий не придумал. Для каждой категории задается наименование — как она прописана в наших данных и ширина построения series: обственно, сами диаграммы, задаем, из какого столбца берем значения для оси y, цвет, и к какой оси диаграмма будет относится, левой или правой
Все исходные данные мы задали, теперь наконец вызываем построение графика и наслаждаемся результатом:
Что же видим мы на этом графике? А видим мы, что планы были через чур оптимистичны и чтобы иметь достоверный прогноз нужно делать неизбежную корректировку. Также необходимо присмотреться и к производству, уж больно рваный фактический график… Ладно, ладно — это мы уже лезем туда, куда нас, программистов, никто не звал, поэтому возвращаемся к нашим баранам — как же этот график строится?
Снова повторю вызов функции построения графика:
Глядя на нее возникает резонный вопрос, который вы возможно хотите мне задать — Зачем в функцию передаются два массива данных, data и dataGroup? Отвечаю: исходный массив данных нужен для того, чтобы правильно задать диапазон данных для осей. Подозреваю, звучит это не очень понятно — но постараюсь вскоре этот момент объяснить.
Первое что мы выполняем в функции построения — это проверяем, есть ли вообще в наличии объект, в котором мы будем строить график. И если этого самого объекта нет — сильно ругаемся:
Следующие наши действия: инициализируем различные отступы, размеры и создаем шкалы.
Не забываем, что библиотека наша только-только вылупилась и настраивать кое — какие вещи (например отступы) извне еще не приучена, ввиду искуственно мной ускоренного инкубационного процесса. Поэтому еще раз прошу понять и простить.
Шутки-шутками, но вернемся обратно к коду выше — с отступами и размерами думаю все и так понятно, шкалы же нужны нам для пересчета исходных значений координат в координаты области построения. Видно, что шкала х инициализируется как шкала времени, а левые и правые шкалы по оси y инициализируются как линейные. Вообще в D3 много различных шкал, но рассмотрение их, а так же многого, многого другого уже сильно выходит за рамки этой статьи.
Продолжаем, шкалы мы создали, теперь нужно их настроить. И вот здесь то как раз и пригодится тот исходный набор данных. Если совсем по-простому — предыдущими действиями мы задали диапазон шкал в координатах графика, следующими же командами мы связываем этот диапазон с диапазонами данных:
Для шкалы X мы задаем минимальным значением минимальную дату в наших данных, максимальным — максимальную. Для осей Y за минимум берем 0, максимум же также узнаем из данных. Вот для этого и были нужны не разбитые данные — чтобы узнать минимальные и максимальные значения.
Следующее действие — настраиваем оси. Тут начинается небольшая путаница. В D3 есть шкалы (scales) и оси (axis). Шкалы отвечают за преобразование исходных координат в координаты области построения, оси же предназначены для отображения на графиках тех палочек и черточек, которые мы видим на графике и которые по русски называются “шкалы координат, отложенные по осям X и Y”. Поэтому, в дальнейшем, если я пишу шкала, имейте ввиду что речь идет об axis, т.е. об отрисовке шкалы на графике.
Итак, напоминаю — у нас две шкалы для оси Y и одна шкала для оси X, с которой пришлось изрядно повозиться. Дело в том, что меня совершенно не устраивало, как D3 по умолчанию выводит шкалу дат. Но все мои попытки настроить подписи дат так, как мне это нужно, разбивались, как волны, о скалы мощности и монументальности этой библиотеки. Потому пришлось пойти на подлог и обман: я создал две шкалы по оси X. На одной шкале у меня выводятся только годы, на другой месяцы. Для месяцев добавлена небольшая проверка, которая исключает первый месяц из вывода. И ведь всего пару предложений назад я обвинял эту библиотеку в монументальности, а тут такой замечательный пример гибкости.
Продолжаем рассматривать код. Все подготовительные работы мы провели и теперь приступаем непосредственно к формированию изображения. Следующие 4 строчки кода последовательно создают область svg, рисуют оконтуривающую рамку, создают с заданным смещением группу объектов svg, в которой будет строится наш график. И последнее действие — выводится заголовок.
Следующий большой кусок кода подписывает единицы измерения наших 3-х осей. Думаю здесь все понятно и подробно рассматривать не нужно:
Ну и, наконец, ядро функции построения графика — отрисовка самих диаграмм:
“Три вложенных друг в друга цикла!” — в ярости воскликните вы. И будете совершенно правы в своем негодовании — сам не люблю делать такие вложенные конструкции, но иногда приходится. В третьей вложенности цикла мы инициализируем наши линии диаграмм, где в зависимости от series указываем, к правой или левой шкале будет относится эта линия. После этого, во второй вложенности мы уже выводим линию на график, задавая ее толщину из свойств категорий. Т.е. фактически у нас на построении задействованы всего две строчки кода, все остальное лишь обвязка, необходимая для обработки различного количества диаграмм на графике.
Ну и последние действие с нашим графиком — это вывод легенды. С легендой я каюсь — тут уже торопился и сделал ее на тяп-ляп, код этот будет в скором времени переписан и показываю я его лишь для того чтобы еще раз продемонстрировать, что в D3 все довольно таки просто. А еще — вот хороший пример того, как делать не нужно:
Вот и все. Спасибо за внимание! Надеюсь, что не разочаровал вас своей статьей.
Код и исходный пример данных можно скачать с Гитхаба.
В заключение хочу лишь добавить, что именно подобную статью или туториал я искал, когда сам пытался разобраться с библиотекой D3. Искал статью, где бы на примерах, раздельно и последовательно, было бы показано: как загрузить и подготовить данные, как создать и настроить область построения, и как эти данные отобразить. К сожалению, ничего подобного я тогда не встретил, а в примерах по D3 от автора так все перемешано, что не понимая логики работы и не имея начальных знаний по этой библиотеке, очень трудно разобраться, где заканчиваются манипуляция с данными, а где начинается манипуляции с представлением этих данных, и наоборот.
23.06.2016 upd. Обновил программу на Гитхабе: доработал шкалу времени, сделал что по оси X можно вместо дат пускать числовой ряд, исправил некоторые ошибки и плюс теперь можно строить графики без категорий.
12.08.2016 upd. Переделал программу, чтобы работала на 4-й версии d3. Вылезло достаточно много несовместимостей. Пример описанный в статье работает только с 3-й версией библиотеки и лежит на Гитхабе в файлах с префиксом _v3 в имени файла.