Технические лайфхаки
2024-05-30 13:51 Визуализация

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

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

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

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

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

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

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

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

Шаг 1. Определяем структуру данных виджета, используя те#nbsp;самые 4 поля, и#nbsp;располагаем их#nbsp;в#nbsp;строках. В#nbsp;результате получаем что-то вроде «плоского дерева».
Шаг 2. Выбираем вид виджета HTML и#nbsp;заполняем следующие вкладки.
Шаг 3. В#nbsp;HTML прописываем подключение библиотеки и#nbsp;задание div-а с#nbsp;виджетом.
Пример 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. Вся магия происходит во#nbsp;вкладке#nbsp;JS.
  1. Формируем узлы графа;
  2. Формируем связи между узлами;
  3. Задаем разные опции:
  • Категории узлов (группировка) для расцветки;
  • Размеры узлов;
  • Расстояние между узлами и#nbsp;т.#nbsp;д.

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

// Инициализация виджета
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-ы браузера, в#nbsp;коде в#nbsp;нужном месте прописать debugger.

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