Анализ процессов, Графы

Vis.js + Chart.js или строим интерактивные графы с возможностью просмотра дополнительных статистик.

Время прочтения: 6 мин.

В последнее время огромную популярность набирает область, специализирующаяся на анализе процессов посредством построения различного рода графов (process mining, knowledge graph и т.д). Но как сделать результат вычислений и визуализацию графа не только понятной, но также интерактивной и переносимой (для беспрепятственной передачи для анализа профильному сотруднику)? В данной статье рассмотрим механизм построения интерактивных графов с возможностью просмотра статистики по вершинам и ребрам с помощью библиотек Vis.js и Chart.js.

В первую очередь, напишем шаблон нашей страницы:

<!doctype html>
<html lang="ru">
<head>
    <meta charset="utf-8"/>
    <title>Interractive graph</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.bundle.min.js"></script>
</head>
<body>
<div class="parent">
    <div id="mynetwork"></div>
    <div id="all_information"></div>
</div>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
</body>
</html>

В данном шаблоне импортируем необходимые для работы библиотеки из хранилища cdn. Стоит обратить внимание, что в случае отсутствия подключения к сети интернет, библиотеки работать не будут. Чтобы избежать данной проблемы, необходимо скачать данные библиотеки и разместить в одной директории с запускаемым html-файлом.

Далее наша страница будет разделена на две части: первая — для отображения графа, вторая – для вывода информации по вершинам и ребрам. Для этого создаем два div-элемента – mynetwork и all_information. Теперь приступим к рисованию графа. Разместим тег <script> в конце нашего шаблона (дальнейший код будем писать внутри него). Для работы с графом в библиотеке vis.js необходимо задать информацию о его ребрах и вершинах. Для этого в классе vis реализована собственная структура – DataSet. Пример хранения списка вершин и ребер представлен ниже:

var nodes = new vis.DataSet([{ id: 1, label: "Log Start", "node_size": 30, count_by_timeperiod: {'от 1 до 3 лет': 1977, 'от 30 до 90 дней': 1988, 'от 90 дней до 1 года': 3763, 'свыше 3 лет': 980}},{id: 2, label: "Event_1", "node_size": 30, count_by_timeperiod: {'от 1 до 3 лет': 1977, 'от 30 до 90 дней': 1988, 'от 90 дней до 1 года': 3763, 'свыше 3 лет': 980}}]);
var edges = new vis.DataSet([{from: 1, to: 2, arrows: "to", edge_size: 1, count_by_timeperiod: {'от 1 до 3 лет': 2, 'от 30 до 90 дней': 2, 'от 90 дней до 1 года': 4, 'свыше 3 лет': 6}},{from: 1, to: 3, arrows: "to", edge_size: 10, count_by_timeperiod: {'от 1 до 3 лет': 1975, 'от 30 до 90 дней': 1986, 'от 90 дней до 1 года': 3759, 'свыше 3 лет': 974}}]);

В данном случае, переменная count_by_timeperiod будет отвечать за длительность исполнения обязанностей по тем или иным видам документов (пользователь может создавать и хранить свои собственные структуры). Переменная arrows указывает на тип стрелки, в нашем случае она идет только в одну сторону, поэтому указано значение «to».

            Далее укажем область, в которую будет производиться отрисовка нашего графа:

var container = document.getElementById('mynetwork');

Далее укажем опции для нашего графика, список вершин и ребер.

var data = {nodes: nodes,edges: edges};
var options = {"physics":{"barnesHut":{"gravitationalConstant": -4000, "springConstant": 0.006, "damping":0.02}, "repulsion":{"nodeDistance":300}}};

Создаём объект нашего графа:

var network = new vis.Network(container, data, options);

После этого кода мы уже можем посмотреть на предварительный результат:

Не хватает заявленных интерактивности и информативности, не так-ли? Ну так приступим к их реализации! Создадим слушатель события нажатия на ребро или вершину:

network.on( 'click', function(properties) {
    var clickedNodes = nodes.get(properties.nodes);
    var clickedEdges = edges.get(properties.edges);    
    var clickedObject = clickedNodes.length==0 ? clickedEdges[0] : clickedNodes[0];
    var count_by_timeperiod = clickedObject['count_by_timeperiod'];
    var cbp_labels = Object.keys(count_by_timeperiod); // total sum by timeperiod
    var cbp_values = Object.values(count_by_timeperiod); 
    showChart('myChart', cbp_labels, cbp_values, "Длительность исполнения"); 
});

При нажатии мыши на нашем графе будут определяться его параметры (хранятся в переменной properties). Далее, с помощью тернарного оператора мы определим тип объекта, на котором сработало наше событие (это необходимо, в случае если для вершин и ребер хранятся различные метрики и, как следствие, данные). Так как данные хранятся в виде словарей (пар «ключ-значение»), мы можем выбрать необходимую для отображения метрику и передать её в метод для рисования графиков. Данный метод представлен ниже:

function showChart(canvas_id, labels_list, values_list, legend){
  //удаляем элемент, чтобы очистить контекст
  var elem = document.getElementById(canvas_id);
  if (elem!=null){elem.remove(elem);}
  //создаём элемент заново, чтобы забрать контекст заново
  let canvas = document.createElement('canvas');
  canvas.setAttribute('id',canvas_id);
  canvas.style.width = "500px";
  canvas.style.height = "300px";
  res_container.append(canvas);
  var element = document.getElementById(canvas_id);
  var ctx = element.getContext('2d');
  var myChart = new Chart(ctx, {
    type: 'bar', // тип графика, в нашем случае это столбчатая диаграмма
data: {
labels: labels_list,// список имен параметров, по которым будет выводиться информация
datasets: [{
            label: legend,//легенда нашей гистограммы
            data: values_list,// значения переменных для построения столбцов
            borderWidth: 1 //ширина рамки
        }]},
    options: {
      responsive:false,
        legend: {display: false},
        title: {display: true,text: legend,position: 'top',fontSize: 16, padding: 0 },
scales: {yAxes: [{ticks: {min: 0}}]}}});}

Повторно запустим нашу страницу и попробуем нажать на вершину или ребро графа:

Как видим, при нажатии на вершину графа, ребра, соединяющие смежные с ней вершины выделяются цветом. При этом, в отдельном окне выводится график с информацией по данной вершине (реализована возможность вывода всплывающих окон при наведении на интересующий столбец).

Также информацию о графе можно представить в виде таблицы. Для этого реализуем функцию createTable.

function createTable(labels, values){
  var table = document.getElementById('text_information');
  if (table != null){table.remove(table);
    table = document.createElement('table');
    table.setAttribute('id', 'text_information');
    res_container.appendChild(table);}
  else{table = document.createElement('table');
    table.setAttribute('id', 'text_information');
    res_container.appendChild(table);}
  var tr_header = document.createElement('tr');
  var th_col_label = document.createElement('th');
  th_col_label.innerHTML = "Длительность";
  var th_col_value = document.createElement('th');
  th_col_value.innerHTML = "Количество";
  tr_header.appendChild(th_col_label);
  tr_header.appendChild(th_col_value);
  table.appendChild(tr_header);
  for (var i=0; i < labels.length; i++){
    var tr = document.createElement('tr');
    let td1 = document.createElement('td');
    td1.innerHTML = labels[i];
    let td2 = document.createElement('td');
    td2.innerHTML = values[i];
    tr.appendChild(td1);
    tr.appendChild(td2);
    table.appendChild(tr);}}

В итоге получим:

В результате разработки мы получаем интерактивный граф, где для каждой из сущностей (вершин, ребер) можем задать информацию, которую в дальнейшем сможем показать в виде диаграмм или таблиц, так горячо любимых профильными сотрудниками)) Полный код приложения можно посмотреть в репозитории.

Советуем почитать