Граф зависимостей аналитических объектов

После выхода долгожданной версии 1.16, где появилась возможность использовать кастомные виджеты, сразу как появилось свободное время фича была опробована)

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

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

Для начала необходимо подключиться к своей же внутренней БД PostgreSQL, в которой хранятся метаданные, которые будут служить нам данными. Тонкости подключения к БД описывать тут не будем, перейдем сразу к модели.

Итоговая модель получается такая, для удобства снизу таблица таблиц и их связей

Таблица слева Поле слева Поле справа Таблица справа
DASHBOARD_OBJECT dashboard_id id DASHBOARD
DASHBOARD_OBJECT widget_id widget__id WIDGET
WIDGET model_id model__id MODEL
MODEL model__id model_data_source__model_id MODEL_DATA_SOURCE
MODEL_DATA_SOURCE data_source_id data_source__id DATA_SOURCE

Везде используется left-join

Далее нам необходимо для удобства найти-переименовать-сделать справочными следующие 4 поля


Загружаем данные и переходим к созданию виджета

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

Определяем структуру данных виджета используя те самые 4 поля, которые располагаем в строках. У нас получается что-то вроде “плоского дерева”

Выбираем вид виджета html и заполняем следующие вкладки

В 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%;
}

Вся магия происходит во вкладке JS. Краткая суть

  1. Формируем узлы графа
  2. Формируем связи между узлами
  3. Задаем разные опции
    3.1. Категории узлов (группировка) для расцветки
    3.2. Размеры узлов
    3.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 кода привычная

Лог в консоли выглядит следующим образом

Для запуска тестового скрипта жмем “Выполнить”, а когда все готово “Опубликовать”

Результат

4 лайка