Технические лайфхаки
Визуализация

Создание кастомных виджетов. Построение графа зависимостей объектов в Analytic Workspace

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

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

С версии 1.16 в Analytic Workspace есть возможность самостоятельно создавать собственный виджет с использованием HTML, CSS и JS, благодаря встроенной интеграции с Echarts, где доступно 400 видов визуализации.

Расскажем, как визуализировать связи между источниками/моделями/виджетами и дашбордами в виде одного понятного графа, то есть построим граф связей аналитических объектов, который в дальнейшем также можно использовать при разработке новых объектов, а также понимать влияние изменений существующих.

Создание модели данных

Шаг 1. Для начала необходимо подключиться к своей же внутренней БД PostgreSQL, где хранятся метаданные, которые и будут использоваться для создания модели. Тонкости подключения к БД опустим, сейчас не об этом, перейдем сразу к делу.
Итоговая модель должна выглядеть так, как на картинке выше.
Также для удобства прикрепляем картинку с таблицами и их связями. Отметим, везде используется left-join.
Шаг 2. После построения итоговой модели необходимо найти, переименовать и сделать справочными следующие 4 поля:
Шаг 3. Далее загружаем данные и переходим к созданию виджета.

Создание виджета

Шаг 1. Определяем структуру данных виджета, используя те самые 4 поля, и располагаем их в строках. В результате получаем что-то вроде «плоского дерева».
Шаг 2. Выбираем вид виджета HTML и заполняем следующие вкладки.
Шаг 3. В HTML прописываем подключение библиотеки и задание div-а с виджетом.
Пример HTML:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
<div id="force-graph"></div>
В дальнейшем нам еще понадобится: id="force-graph"
Именно по этому идентификатору привязываем CSS стили, в рассматриваемом нами случае они достаточно простые.
Пример CSS:
#force-graph
{
    width: 100%;
    height: 100%;
}
Шаг 4. Вся магия происходит во вкладке JS.
  1. Формируем узлы графа;
  2. Формируем связи между узлами;
  3. Задаем разные опции:
  • Категории узлов (группировка) для расцветки;
  • Размеры узлов;
  • Расстояние между узлами и т. д.

Разработка и отладка

// Инициализация виджета
var forceGraph = echarts.init(document.getElementById('force-graph'));

// при обновлении данных перерисовываем виджет
function render() {

    const categories = [
        {
            "name": "Панель"
        },
        {
            "name": "Виджет"
        },
        {
            "name": "Модель"
        },
        {
            "name": "Источник"
        }
    ]
    var catIndex = categories.map(function (a) {
        return a.name;
    })
    /*
     * получаем названия колонок
    [0] name - Название дашборда
    [1] widget_name - Название виджета 
    [2] model_name - Название модели 
    [3] data_source_name - Название источника 
    */
    var columns = window.WIDGET.columns
        .map(column => column.field);

    var data = [];
    var links = []

    window.DATA.data.forEach(item => {
        // Нам нужно получить список всех узлов графа, для этого
        // бежим по всем строкам в табличном представлении
        // раскидываем данные строки в объект data с проверкой на уникальность 
        // прописываем префикс, определяющий к какой категории относится объект
        // это также даст уникальность объектам
        // Панели
        if (data.indexOf("Панель|" + item[columns[0]]?.value) === -1) {
            data.push("Панель|" + item[columns[0]]?.value)
        }
        // Виджеты
        if (data.indexOf("Виджет|" + item[columns[1]]?.value) === -1) {
            data.push("Виджет|" + item[columns[1]]?.value)

        }
        // Модели
        if (data.indexOf("Модель|" + item[columns[2]]?.value) === -1) {
            data.push("Модель|" + item[columns[2]]?.value)
        }
        // Источники
        if (data.indexOf("Источник|" + item[columns[3]]?.value) === -1) {
            data.push("Источник|" + item[columns[3]]?.value)
        }

        // Нам нужно получить связи между узлами графа
        // Попутно обозначив из какого и в какой узел эта связь
        // Так как есть модели и источники с одним наименованием, это приводит к ошибке
        // Связь панели [0] и виджета [1]
        links.push({
            source: "Панель|" + item[columns[0]]?.value || '',
            target: "Виджет|" + item[columns[1]]?.value || '',
        })

        // Связь виджета [1] и модели [2]
        links.push({
            source: "Виджет|" + item[columns[1]]?.value || '',
            target: "Модель|" + item[columns[2]]?.value || '',
        })

        // Связь модели [2] и источника [3]
        // Так как есть модели и источники с одним наименованием, это приводит к ошибке
        links.push({
            source: "Модель|" + item[columns[2]]?.value || '',
            target: "Источник|" + item[columns[3]]?.value || '',
        })
    })

    var nodesVolume = {}
    // проставляем "значение" это количество ссылок на объект
    // от этого будет зависить величина (диаметр) узла
    // за каждую ссылку на этот узел добавляем значение
    links.forEach(link => {
        if (isNaN(nodesVolume[link.source])) {
            nodesVolume[link.source] = 20 // начинаем с 20
        }
        else {
            nodesVolume[link.source] += 1
        }
    })
    debugger
   // Формируем узлы для графа
    var nodes = []
    data.forEach(item => {
        var category = item.split('|')[0] // отделяем категорию
        var itemName = item.split('|')[1] // отделяем наименование 
        var categoryIndex = catIndex.indexOf(category) // порядковый номер категории требуется в настройках виджета
        var ssize = nodesVolume[item] / 2 // размер текста
        if (category == "Источник") { 
            ssize = 10
        }

        nodes.push({
            name: itemName,
            value: nodesVolume[item] - 20,
            category: categoryIndex,
            id: item,
            label: { show: categoryIndex == 2 }, // сразу показываем текст узлов только для моделей, для остальных при наведении
            symbolSize: ssize
        })
    })
    
    // определяем настройки для графа
    // что значит каждая настройка можно почитать в документации https://echarts.apache.org/en/option.html#series-graph
    var option = {
        tooltip: {},
        legend: [
            {
                data: catIndex
            }
        ],
        series: [
            {
                type: 'graph',
                layout: 'force',
                animation: false,
                roam: true,
                symbol: 'roundRect',
                stateAnimation: {
                    duration: 0,
                    easing: 'cubicIn'
                },
                label: {
                    position: 'right',
                    formatter: '{b} ({c})',
                    show: false
                },
                draggable: true,
                data: nodes,
                categories: categories,
                force: {
                    edgeLength: 50,
                    repulsion: 50,
                    gravity: 0.1,
                    initLayout: null,
                    friction: 0.1,
                    layoutAnimation: true
                },
                edges: links
            }
        ]
    }
    // для удобства логируем все, что передали
    console.log(option)

    // Подставляем новые данные и обновляем виджет
    forceGraph.setOption(option)
}
Поскольку разработка клиентская, для отладки кода необходимо открыть DevTools-ы браузера, в коде в нужном месте прописать debugger.

Отладка для JS-кода привычная.
Лог в консоли выглядит следующим образом. (см. картинку ниже)
Для запуска тестового скрипта жмем «Выполнить», а когда все готово «Опубликовать».
Таким образом, в результате проделанной работы мы визуализировали связи между источниками/моделями/виджетами и дашбордами в виде одного понятного графа.