Будучи поклонником программных продуктов для визуализации активности в репозиториях таких как code_swarm и gource. В один прекрасный день я был посещен музой, которая вдохновила меня создать онлайн сервис для визуализации статистики репозиториев с GitHub.
И сегодня хочу предоставить на ваш суд мой проект GitHub Visualizer (проект на GitHub).
Вот скринкаст для предварительного знакомства.
И не большая Gif'ка
Что использовано
- SVG, Canvas — для интерактивной графики.
- D3.js — javascript библиотека позволяет очень удобно работать с данными и визуализировать их.
Гигантская коллекция примеров от автора библиотеки Mike Bostock. - API GitHub
Описание графиков и их реализации
В данном проекте есть три основных визуализации, демонстрирующие информацию о репозиториях, их истории и количественных показателях.
Визуализация списка репозиториев
- Круги (вершины) — это репозитории
- Размер вершины зависит от возраста репозитория, чем старше, тем меньше.
- Непрозрачность зависит от даты последнего изменения
- Цвет и группировка вершин зависит от основного языка репозитрория
- Гистограмма языков
- Показывает суммарную информацию по каждому языку
- Отображает цвет языка
- Позволяет фильтровать вершины при наведении
Для построения графа использовался D3.Layout.Force и метод кластеризации предложенный в данном примере.
Кусок кода из примераvar force = d3.layout.force() .nodes(nodes) .size([width, height]) .gravity(.02) .charge(0) .on("tick", tick) .start(); function tick(e) { circle .each(cluster(10 * e.alpha * e.alpha)) .each(collide(.5)) .attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }); } // Move d to be adjacent to the cluster node. function cluster(alpha) { var max = {}; // Find the largest node for each cluster. nodes.forEach(function(d) { if (!(d.color in max) || (d.radius > max[d.color].radius)) { max[d.color] = d; } }); return function(d) { var node = max[d.color], l, r, x, y, i = -1; if (node == d) return; x = d.x - node.x; y = d.y - node.y; l = Math.sqrt(x * x + y * y); r = d.radius + node.radius; if (l != r) { l = (l - r) / l * alpha; d.x -= x *= l; d.y -= y *= l; node.x += x; node.y += y; } }; } // Resolves collisions between d and all other circles. function collide(alpha) { var quadtree = d3.geom.quadtree(nodes); return function(d) { var r = d.radius + radius.domain()[1] + padding, nx1 = d.x - r, nx2 = d.x + r, ny1 = d.y - r, ny2 = d.y + r; quadtree.visit(function(quad, x1, y1, x2, y2) { if (quad.point && (quad.point !== d)) { var x = d.x - quad.point.x, y = d.y - quad.point.y, l = Math.sqrt(x * x + y * y), r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding; if (l < r) { l = (l - r) / l * alpha; d.x -= x *= l; d.y -= y *= l; quad.point.x += x; quad.point.y += y; } } return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; }); }; }
Собственно это и была та муза которая посетила меня.
Функции взяты практический без изменения за некоторыми исключениями и добавлениями.
Реализация функционала для визуализации списка репозиториев находиться в двух файлах repo.js и langHg.js
Визуализация истории репозитория
После того как вы загрузите информацию о списке репозиториев пользователя вы можете выбрать интересующий вас репозиторий или в графе, или в списке репозиториев в панели второго этапа (также здесь можно задать кол-во последних ревизий для анализа).
Затем выполнить его анализ нажатием кнопки «Analyze». Вовремя анализа построится график истории репозитория. На котором отображается информация по указанному вами количеству последних коммитов (по-умолчанию 100 коммитов. Может быть и меньше сколько есть в репозитории).
График истории
- Ось Х показывает даты фиксации.
- Каждая красная точка представляет собой фиксацию.
- Дуги вверх и вниз — это кол-ва добавленных и удаленных строк в коммите.
- Области на заднем фоне показывают кол-во изменяемых файлов.
- Добавленные файлы
- Модифицированные файлы
- Удаленные files
- Диаграмма участников — Показывает активность участника по различным параметрам.
Для того чтоб отрисовать диаграммы я использовал ряд средств и их комбинацию из библиотеки d3.js.
Вычисление областей выполняет компонент d3.svg.area() (пример Stacked Area). Стек я считаю сам, но все остальное тривиально для d3js.Кусок кода где считается стекvar layers = [ { color: colors.deletedFile, values: sorted.map(function (d) { return {t : 1, x: d.date, y0 : 0, y: (d.stats ? -d.stats.f.d : 0)} }) }, { color: colors.modifiedFile, values: sorted.map(function (d) { return {x: d.date, y0 : 0, y: (d.stats ? d.stats.f.m : 0)} }) }, { color: colors.addedFile, values: sorted.map(function (d) { return {x: d.date, y0: (d.stats ? d.stats.f.m : 0), y : (d.stats ? d.stats.f.a : 0)} }) } ] ; function interpolateSankey(points) { var x0 = points[0][0], y0 = points[0][1], x1, y1, x2, path = [x0, ",", y0], i = 0, n = points.length; while (++i < n) { x1 = points[i][0]; y1 = points[i][1]; x2 = (x0 + x1) / 2; path.push("C", x2, ",", y0, " ", x2, ",", y1, " ", x1, ",", y1); x0 = x1; y0 = y1; } return path.join(""); } var y1 = d3.scale.linear() .range([h6 * 4.5, h6 * 3, h6 * 1.5]) .domain([-data.stats.files, 0, data.stats.files]), area = d3.svg.area() .interpolate(interpolateSankey /*"linear" "basis"*/) .x(function(d) { return x(d.x); }) .y0(function(d) { return y1(d.y0); }) .y1(function(d) { return y1(d.y0 + d.y); }) ;
Для построения дуг использую d3.svg.arc() (есть множество примеров где используется данный компонент: Arc Tween, Pie Multiples).
Генерацию шкалы X делаю с использованием двух компонентов d3.time.scale() и d3.svg.axis. Реализация взята из этого примера Custom Time Format.
Диаграмму участников просчитывает d3.layout.pack() (пример Circle Packing). Для того чтоб сортировать и изменять размер кругов я меняю свойства sort и value.
Код для данной визуализации располагается в двух файлах stat.js и usercommit.js
Динамическая визуализация
Ради этого все и была вся затея. Мне нравится что получается при визуализации с использование code_swarm, но каждый раз клонировать репозиторий к себе на компьютер а затем его визуализировать доставляет не удобство.
В данной визуализации я постарался воплотить все идеи которые применяются в code_swarm и сделать изменение настроек на лету.
Визуализация song-of-github, Ссылка для запуска, Статья о Song-of-github на хабрахабре
- Каждая частица это файл. Они перемещаются от разработчика к разработчику.
- Размер частицы зависит от степени его изменения, чем чаще его изменяют тем он больше.
- Цвет частицы зависит от ее расширения.
- Со временем частица пропадает, как только пропадаю все частицы у пользователя пользователь тоже тает. (Это можно регулировать соответствующими настройками в панели 3 этап, User Life и File Life, значение 0 — бессмертные).
- Каждый участник собирает вокруг себя те файлы с которыми проводил манипуляции.
- Если файлы покидают орбиту пользователя и больше ни к кому не летит, значит он удален.
- Каждая секунда это день (планах добавить возможность изменения шага)
- Гистограмма показывает количество файлов участвующих в фиксации, разделенных по расширениям
- Легенда показывает кол-во существующих файлов на данный момент по каждому расширению.
Расчет физики выполняет пресловутый D3.Layout.Force, но с небольшим упущением их два. Один рассчитывает позиции пользователей, другой считает положение файлов в зависимости от положения пользователя. Как это сделано? У каждого файла есть свойство
author
, в него записывает текущий на данный момент (момент коммита) пользователь если этот файл есть в текущей фиксации. Выше указанный метод кластеризации получает его и считает положение данного файла в пространстве.Функция кластеризацииfunction tick() { if (_force.nodes()) { _force.nodes() .forEach(cluster(0.025)); _forceAuthor.nodes( _forceAuthor.nodes() .filter(function(d) { blink(d, !d.links && setting.userLife > 0); if (d.visible && d.links === 0 && setting.userLife > 0) { d.flash = 0; d.alive = d.alive / 10; } return d.visible; }) ); } _forceAuthor.resume(); _force.resume(); } // Move d to be adjacent to the cluster node. function cluster(alpha) { authorHash.forEach(function(k, d) { d.links = 0; }); return function(d) { blink(d, setting.fileLife > 0); if (!d.author || !d.visible) return; var node = d.author, l, r, x, y; if (node == d) return; node.links++; x = d.x - node.x; y = d.y - node.y; l = Math.sqrt(x * x + y * y); r = radius(nr(d)) / 2 + (nr(node) + setting.padding); if (l != r) { l = (l - r) / (l || 1) * (alpha || 1); x *= l; y *= l; d.x -= x; d.y -= y; } }; }
И место инициализации force layout'ов
_force = (_force || d3.layout.force() .stop() .size([w, h]) .friction(.75) .gravity(0) .charge(function(d) {return -1 * radius(nr(d)); } ) .on("tick", tick)) .nodes([]) ; ..... _forceAuthor = (_forceAuthor || d3.layout.force() .stop() .size([w, h]) .gravity(setting.padding * .001) .charge(function(d) { return -(setting.padding + d.size) * 8; })) .nodes([]) ;
Работают два потока (если так можно сказать) один это
setInterval
другойrequestAnimationFrame
. Первый отвечает за перемещение по времени, второй за отрисовку. Но на самом деле еще и force имеют свои таймеры и asyncForEach (нужен для того чтоб был хороший отклик системы и файлы из одного коммита вылетали не все сразу, а с небольшой задержкой) тоже запускает setTimeout'ы.
Код можно посмотреть в файле show.js.
Получение данных
Данные получаю с api.github.com.
Получение данных происходит по методике JSONP.
Согласно API GitHub необходимости в наличии Client_id
и Client_Secret
, но тогда лимит запросов будет в размере 60 для одного ip в час. По этому я создал приложение в настройках профиля на GitHub и в запрос добавляется не обходимая информация об авторизации.
Это я к чему все… А к тому что ограничение для такого способа авторизации 5000 запросов в час, некоторые репозитории типа mc имеют богатую историю. И если по ней пройтись хорошо, то лимит быстро исчерпывается, о чем вам скажет система. Если подобное произойдет вы можете указать в меню System settings с права client_id
и client_secret
вашего приложения (предварительно создав его если его еще нет).
У GitHub очень хорошие API, достаточно выполнить только один запрос допустим запросив информацию о пользователе https://api.github.com/users/{user}
все остальные ссылки будут в ответе. Причем если это много страничный запрос ( допустим получение перечня репозиториев, в ответе только информация по 10 репозиториям) то в объекте ответа в параметре meta
есть ссылка на следующую страницу с полным набором параметров авторизации.
В общем выражаю благодарность разработчикам API и тем кто писал документацию по нему, работать с ними одно удовольствие.
Также выражаю благодарность и разработчикам D3js за богатую коллекцию примеров (без которой возможно я бы и не вдохновился на подобное) и очень полную документацию со всеми объяснениями.
Заключение
В самом начале когда я стал делать проект это была игрушка для себя, собственно она такой и осталась. Если вы форкните мой репозитории и найдете кучу ошибок или прикрутите что-то новенькое, то прошу оставьте Pull Request или напишите в Issues.
Приложение при разработке проверялось только в Google Chrome dev-m (нет я конечно явные косяки, которые были в других браузерах исправил), если вы знаете, как сделать его корректно работающим в вашем любимом браузере буду бесконечно благодарен.
Жду здоровой критики.
Благодарю за внимание!
P.S.
Некоторые интересные репозитории:
- Ничего интересного репозитории проекта
- D3js (запуск визуализации)
- jQuery (запуск визуализации)
- MidnightCommander (запуск визуализации)
Автор: artzub