Время прочтения: 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);}}
В итоге получим:
В результате разработки мы получаем интерактивный граф, где для каждой из сущностей (вершин, ребер) можем задать информацию, которую в дальнейшем сможем показать в виде диаграмм или таблиц, так горячо любимых профильными сотрудниками)) Полный код приложения можно посмотреть в репозитории.