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

в 12:47, , рубрики: data visualization, dataviz, Git, github, metrics, Инфографика, Системы управления версиями, метки: , , , ,

Будучи поклонником программных продуктов для визуализации активности в репозиториях таких как 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.
Некоторые интересные репозитории:

Автор: artzub

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js