28 марта 2013 в 16:47

Github Visualizer — Сервис визуализации истории репозиториев с GitHub

Будучи поклонником программных продуктов для визуализации активности в репозиториях таких как code_swarm и gource. В один прекрасный день я был посещен музой, которая вдохновила меня создать онлайн сервис для визуализации статистики репозиториев с GitHub.
И сегодня хочу предоставить на ваш суд мой проект GitHub Visualizer (проект на GitHub).
Вот скринкаст для предварительного знакомства.

И не большая Gif'ка
image

Что использовано



Описание графиков и их реализации


В данном проекте есть три основных визуализации, демонстрирующие информацию о репозиториях, их истории и количественных показателях.

Визуализация списка репозиториев

Граф репозиториев
список репозиториев

  • Круги (вершины) — это репозитории
  • Размер вершины зависит от возраста репозитория, чем старше, тем меньше.
  • Непрозрачность зависит от даты последнего изменения
  • Цвет и группировка вершин зависит от основного языка репозитрория
    main language
  • Гистограмма языков
    • Показывает суммарную информацию по каждому языку
    • Отображает цвет языка
    • Позволяет фильтровать вершины при наведении


Для построения графа использовался 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 коммитов. Может быть и меньше сколько есть в репозитории).
График истории
image

  • Ось Х показывает даты фиксации.
  • Каждая красная точка представляет собой фиксацию.
  • Дуги вверх и вниз — это кол-ва добавленных и удаленных строк в коммите.
  • Области на заднем фоне показывают кол-во изменяемых файлов.
    • Добавленные файлы
    • Модифицированные файлы
    • Удаленные 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 на хабрахабре
image

  • Каждая частица это файл. Они перемещаются от разработчика к разработчику.
  • Размер частицы зависит от степени его изменения, чем чаще его изменяют тем он больше.
  • Цвет частицы зависит от ее расширения.
  • Со временем частица пропадает, как только пропадаю все частицы у пользователя пользователь тоже тает. (Это можно регулировать соответствующими настройками в панели 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.
Некоторые интересные репозитории:

Какой ваш основной браузер

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Проголосовало 533 человека. Воздержалось 30 человек.

Работает все корректно

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Проголосовало 327 человек. Воздержалось 63 человека.

+44
8444
85
artzub 53,8 G+

Комментарии (30)

+1
silvansky, #
Здорово! Гипнотизирует.

Правда, анализ более 1000 коммитов занимает прилично времени (а ФФ вообще подвесил, в Хроме получше).
+1
artzub, #
Я особо не гонял в FF причины подвисанния сказать не могу. А предполагать и тыкать пальцем пока не разобрался не буду. Сегодня завтра погляжу что да как в FF.
–1
MorrisDecker, #
Даже среди пользователей хабра есть те, кто постоянно использует ИЕ 0_о куда катиться мир?
+3
Doomsday_nxt, #
IE10 — очень неплохой браузер. Использую дома :-)
+4
nick4fake, #
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
Radagast, #
какая музыка играет в ролике на Youtube?
0
artzub, #
Странно, обычно youtube указывает в инфо какая музыка.
Behind Closed Eyelids — Shpongle (Dance & Electronic).
0
vittore, #
Послушайте версию от The Who.
Ох как я перепутал Behind Closed Eyelids и Behind Blue Eyes.
0
artzub, #
Ага кстати на lingualeo.com на днях в версии The Who ее и слушал… она более инструментальная =)
+1
mIK_LH, #
Красота то какая!
0
artzub, #
Спасибо!
+1
daydiff, #
Firefox 19.0.2 — на всех примерах из статьи отработал без проблем.
0
artzub, #
Замечательно! Это радует!
+1
phoenixweiss, #
Сначала грешным делом подумал что речь идет о каком-то движке частиц на canvas или о создании какой-либо космической аркады.
А тут оказалось все просто на порядок интереснее.
0
artzub, #
На самом деле саму динамическую визуализацию планирую вывести в отдельную библиотеку, так как у концепции есть потенциал, я ее уже применил для другой визуализации World Bank Global Development Sprint. version 28
+1
phoenixweiss, #
Шикарно!
+1
Magiq, #
Source battle, кто больше кода за месяц написал
0
ka8725, #
Просто оставлю эту ссылку avtobox.by/
0
artzub, #
ВАУ! Классно! Я мечтал о таком сервисе! но еще бы можно было бы на страивать параметры для Gource еще круче было бы =)
0
m03r, #
Глюк: когда наводишь на верхнюю часть сектора, изображающего добавление/удаление кода, он расширяется вниз и уходит из под мыши — и после этого обратно вверх. Мерцает очень неприятно
0
PaulIonkin, #
Отличный проект!
Не нашел как отобразить коммиты, которые я отсылаю в репозиторий организации, в которой состою.
0
artzub, #
Поподробней можно что вы имеете виду. Есть нюанс в том что приложение смотрит в ветвь по-умолчанию, а ваши коммиты могут находиться в другой!
0
Prototik, #
Прикольно смотреть на визуализацию linux kernel: сотни разработчиков пишут код, делают какие-то изменения, потом появляется хитрый Торвальдс, тырит все коммиты и методично сваливает в неизвестном направлении…
0
artzub, #
Ага! Только он не коммиты тырит а файлы. Видимо ревизию делает какую то.
0
Prototik, #
Т.е. одна частица обозначает то, что этот человек последний изменил конкретный файл?
0
artzub, #
Да я вроде так и написал, хотя может немного размыто. Одна частица это файл. Файл летит к тому кто его изменяет.
+1
sigod, #
Здорово!

А когда планируете добавить поддержку Gource?
0
artzub, #
Сейчас немного поработаю над другим проектом, и доделаю еще некоторый функционал.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.