Обсудить
бизнес-задачи

Разработка кастомной визуализации на AW BI

блог о bi, №1 в рунете
Analytic Workspace (AW) – мощная отечественная BI-платформа, которую мы уже рассматривали в статье “Разработка дашборда на BI-платформе Analytic Workspace”. В коробочном функционале AW присутствуют основные элементы визуализаций, необходимые для разработки дашбордов. Требования современных потребителей BI – продуктов нередко выходят за границы базовых визуализаций, которые могут быть невыполнимы даже в самых популярных инструментах таких как PowerBI. Для решения этой проблемы AW предоставило достаточно гибкую возможность создания собственного виджета, используя три кита web-технологии: CSS, HTML и JS. В данной статье мы покажем пример создания такой визуализации.
Создадим виджет, отображающий суммарное значение выручки в выбранном месяце, показатели MoM, YoY в процентах и график с подневной динамикой.
На первом этапе нам необходимо создать новый виждет, для этого на панели управления переходим в «Виджеты» (кнопка 1 на рисунке) и нажимаем «Добавить» (кнопка 2 на рисунке)
Далее выберем модель данных, нажав на кнопку «Выбрать модель» по центру. Будем использовать заранее подготовленную модель со всеми необходимыми показателями. Выберем для построения виджета данные по продажам в текущем периоде «fact_sale», за прошлый месяц «fact_sale_prev_m» и за прошлый год «fact_sale_prev_y» с агрегатом суммирования и данные для группировки значений «dt» для построения графика.
Настройка структуры модели завершена, переходим на вкладку Вид и выбираем тип виджета HTML. Откроются три вкладки для разработки — HTML, CSS и JS.
Создадим первоначальную верстку с заголовком «Выручка» и полем для вывода итогового значения выручки. Для этого во вкладке HTML напишем следующий код и нажмем кнопку «Выполнить»:
<div class="widget"> <!--объявление контейнера-->
    <header class="values"> <!--блок заголовка-->
        <h1 id="title">Выручка</h1> <!--текст заголовка-->
        <h2 id="title_value"></h2> <!--заголовок для вывода значения-->
    </header>
</div>
Переходим на вкладку CSS для настройки стилизации виджета, добавив следующий код:
/*Импорт шрифта*/
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

/*Поддержка изменения темы AW*/
:root {
  --main-color: rgb(85, 85, 85);
  --text-color: rgb(233, 233, 233);
}
 :root.dark {
  --main-color: rgb(197, 197, 197);
  --text-color: #080808;
}

/*Стилизация вложенных объектов*/
.widget {
    font-family: 'Montserrat', sans-serif;
    border: 1px solid #757784;
    border-radius: 24px;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding-inline: 37px;
    padding-block: 28px;
    color: var(--text-color);
    background-color: var(--main-color);
}
.values {
    display: grid;
    grid-template-columns: 2fr 1fr;
    font-size: 16px;
    font-weight: 500;
    margin-bottom: 5px;
    color: var(--text-color);
}
Далее во вкладке JS добавляем функцию render (в ней будут доступны свойства объекта window, а именно data и widget, из которых необходимо будет забирать данные) и открываем инструменты разработчика нажав CTRL-SHIFT-I, чтобы добавить данные на наш виджет.
function render() {
    console.log(window); //выводим в консоль объект window
}
Если мы раскроем DATA, то увидим необходимый нам массив данных для суммирования и вывода в виджет. Требуется вывести сумму всех значений из второй строки массивов data.
Доработаем js код:
//Находим контейнер заголовка для вывода данных по id
const totleDom = document.getElementById('title_value');

//Функция получения данных
function transformData(data) {
    const currentData = [];
    data.forEach(row => {
        currentData.push([
            row.dt.value,
            row.fact_sale.agg_value
            ]);
        });
    return currentData;
}
function render() {
    console.log(window);
    const data = transformData(window.DATA.data);
    var sum_m = 0

//Суммирование элементов массива
    data.forEach(row => {
        sum_m += row[1];
        });
    console.log(sum_m);

//Форматирование значения для вывода в заголовок
    if(sum_m >= 1000000) {
        totleDom.innerHTML = String((sum_m / 1000000).toFixed(2)) + ' млн';
    } else if (sum_m >= 1000) {
        totleDom.innerHTML = String((sum_m / 1000).toFixed(2)) + ' тыс';
    } else {
        totleDom.innerHTML = sum_m.toFixed(2);
    }
}
Аналогичным образом получим показатели для расчета значений MoM иYoY, рассчитаем их и выведем на виджет. Создадим необходимые объекты во вкладках HTML и CSS.
HTML:
<div class="change">
    <div id="yoy" class="metric_change"></div>
    <div id="mom" class="metric_change"></div>
</div>
CSS:
.change {
    display: flex;
    flex-direction: column;
    gap: 15px;
}
.metric_change {
    display: inline-block;
    border-radius: 20px;
    font-size: 16px;
    font-weight: 500;
    background-color: #d3f9d8;
    color: #2b8a3e;
    padding: 4px 10px;
    margin-left: auto;
    text-align: right;
}
JS:
const totleDom = document.getElementById('title_value');
const yoyDom = document.getElementById('yoy');
const momDom = document.getElementById('mom');

function transformData(data) {
    const currentData = [];
    data.forEach(row => {
        currentData.push([
            row.dt.value,
            row.fact_sale.agg_value,
            row.fact_sale_prev_m.agg_value,
            row.fact_sale_prev_y.agg_value
            ]);
        });
    return currentData;
}
function render() {
    const data = transformData(window.DATA.data);
    var sum_m = 0;
    var sum_m_prev = 0;
    var sum_y_prev = 0;
    var yoy;
    var mom;

    data.forEach(row => {
        sum_m += row[1];
        sum_m_prev += row[2];
        sum_y_prev += row[3];

        });
    yoy = (sum_m - sum_y_prev) / sum_y_prev * 100;
    mom = (sum_m - sum_m_prev) / sum_m_prev * 100;

if(sum_m >= 1000000) {
        totleDom.innerHTML = String((sum_m / 1000000).toFixed(2)) + ' млн';
    } else if (sum_m >= 1000) {
        totleDom.innerHTML = String((sum_m / 1000).toFixed(2)) + ' тыс';
    } else {
        totleDom.innerHTML = sum_m.toFixed(2);
    }
    yoyDom.innerHTML = yoy ? yoy.toFixed(2) + "% YoY" : "null";

//Стилизация в зависимости от значения YoY
    if (yoy >= 0 && yoy != "") {
        yoyDom.style.backgroundColor = "#d3f9d8";
        yoyDom.style.color = "#2b8a3e";
    } else if(yoy < 0 && yoy != "") {
        yoyDom.style.backgroundColor = "#e9ecef";
        yoyDom.style.color = "#e63d09";
    } else {
        yoyDom.style.backgroundColor = "#d3d3d3";
        yoyDom.style.color = "#4d4d4d";
    };

    momDom.innerHTML = mom ? mom.toFixed(2) + "% MoM" : "null";

//Стилизация в зависимости от значения MoM
    if (mom >= 0 && mom != "") {
        momDom.style.backgroundColor = "#d3f9d8";
        momDom.style.color = "#2b8a3e";
    } else if(mom < 0 && mom != "") {
        momDom.style.backgroundColor = "#e9ecef";
        momDom.style.color = "#e63d09";
    } else {
        momDom.style.backgroundColor = "#d3d3d3";
        momDom.style.color = "#4d4d4d";
    };
}
Далее построим график, для этого будем использовать готовую визуализацию из открытой библиотеки Apache eChart. В ней есть большое кол-во визуализаций, она бесплатная и открыта для использования. Будем использовать Smoothed Line Chart.
В первую очередь нам необходимо подключить библиотеку Apache Echarts к AW. Это можно сделать на вкладке HTML с помощью тега script.
<script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script>
Для загрузки кода библиотеки из интернета через CDN необходимо перейти по ссылке https://cdnjs.com/libraries/echarts, найти необходимый чарт и нажать кнопку «Скопировать».
Также можно загрузить js файл с библиотекой напрямую в AW и в src указать название файла.
<script src="test.js"></script>
Для этого необходимо скопировать ссылку из CDN и открыть ее в отдельном окне получим полный код библиотеки. Его сохранить в файл c расширением js.
Переходим в AW и загружаем скрипт нажав на скрепку в левой нижней части экрана, далее «Загрузить файл», наша библиотека появится в списке файлов и ее можно будет подключить к исходному коду виджета:
Доработаем код для отрисовки графика на виджете. Полный код HTML/CSS:
HTML:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script>
<div class="widget">
    <div class="values">
        <header class="tot">
            <h1 id="title">Выручка</h1>
            <h1 id="title_value"></h1>
        </header>
        <div class="change">
            <div id="yoy" class="metric_change"></div>
            <div id="mom" class="metric_change"></div>
        </div>
    </div>
    <div class="chart-container">
        <div id="chart"></div>
    </div>
</div>
CSS:
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

:root {
  --main-color: rgb(136, 136, 136);
  --text-color: rgb(243, 243, 243);
}
 
:root.dark {
  --main-color: rgb(197, 197, 197);
  --text-color: #3d3d3d;
}
.widget {
    font-family: 'Montserrat', sans-serif;
    border: 1px solid #757784;
    border-radius: 24px;
    width: 100%;
    height: 100%;

    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding-inline: 37px;
    padding-block: 28px;
 
    color: var(--text-color);
    background-color: var(--main-color);
}
.values {
    display: grid;
    grid-template-columns: 2fr 1fr;
    font-size: 16px;
    font-weight: 500;
    margin-bottom: 5px;
    color: var(--text-color);
}
.change {
    display: flex;
    flex-direction: column;
    gap: 15px;
}

.metric_change {
    display: inline-block;
    border-radius: 20px;
    font-size: 16px;
    font-weight: 500;
    background-color: #d3f9d8;
    color: #2b8a3e;
    padding: 4px 10px;
    margin-left: auto;
    text-align: right;
}
.chart-container {
    width: 100%;
    height: 100%;
}

#chart {
    width: 100%;
    height: 100%;
}
В JS подключили инициализировали объект echart и метод для масштабировния графика:
const chart_month = echarts.init(document.getElementById('chart'));
window.addEventListener('resize', () => {  chart_month.resize();})
Далее необходимо добавить свойств графика, они описываются в переменной options их можно скопировать с сайта eChart.
Теперь мы можем подключить к графику данные и стилизовать график. Полный код JS для нового виджета:
const chart_month = echarts.init(document.getElementById('chart'));

window.addEventListener('resize', () => {
  chart_month.resize();
})
const totleDom = document.getElementById('title_value');
const yoyDom = document.getElementById('yoy');
const momDom = document.getElementById('mom');

var option = {
    tooltip: {
        trigger: 'axis',
        formatter: function(params) {
            let result = params[0].name + '<br/>';
            params.forEach(function(param) {
                const value = param.value ?? 0;
                const seriesName = param.seriesIndex === 0 
                    ? 'Текущий год' 
                    : 'Предыдущий год';
                result += `${param.marker} ${seriesName}: ${numberWithSpaces(value)}<br/>`;
            });
            return result;
        }
    },
    grid: {
        top: 17,
        bottom: 10,
        left: 50,
        right: 0
    },
xAxis: {
        data: null,
        axisLine: { show: false },
        axisTick: { show: false },
        axisLabel: { show: false }
    },
    yAxis: {
        axisLabel: {
            color: 'white',
            formatter: val => `${(val / 1000000).toFixed(0)} млн`
        },
        min: function(value) {
            return Math.floor(value.min / 1000000) * 1000000; 
        }
    },
    series: [
        {
            data: null,
            type: 'line',
            smooth: true,
            lineStyle: {color: '#1ac9e8', width: 3},
            showSymbol: false,
            itemStyle: {
                normal: {
                    color: '#1ac9e8'
                }
            }
        }
    ]
};

function transformData(data) {
    const currentData = [];
    data.forEach(row => {
        currentData.push([
            row.dt.value,
            row.fact_sale.agg_value,
            row.fact_sale_prev_m.agg_value,
            row.fact_sale_prev_y.agg_value
            ]);
        });
    return currentData;
}
function render() {
    const data = transformData(window.DATA.data);
    var sum_m = 0;
    var sum_m_prev = 0;
    var sum_y_prev = 0;
    var yoy;
    var mom;
    var chart_x = [];
    var chart_m_cur = [];
    data.forEach(row => {
        
        sum_m += row[1];
        sum_m_prev += row[2];
        sum_y_prev += row[3];
        chart_m_cur.push(Number(row[1].toFixed(0)))
        chart_x.push(String(row[0]).substring(0, 4) + '-' + String(row[0]).substring(4, 6) + '-' + String(row[0]).substring(6, 8));
        });

    yoy = (sum_m - sum_y_prev) / sum_y_prev * 100;
    mom = (sum_m - sum_m_prev) / sum_m_prev * 100;

    if(sum_m >= 1000000) {
        totleDom.innerHTML = String((sum_m / 1000000).toFixed(2)) + ' млн';
    } else if (sum_m >= 1000) {
        totleDom.innerHTML = String((sum_m / 1000).toFixed(2)) + ' тыс';
    } else {
        totleDom.innerHTML = sum_m.toFixed(2);
    }
    yoyDom.innerHTML = yoy ? yoy.toFixed(2) + "% YoY" : "null";

    if (yoy >= 0 && yoy != "") {
        yoyDom.style.backgroundColor = "#d3f9d8";
        yoyDom.style.color = "#2b8a3e";
    } else if(yoy < 0 && yoy != "") {
        yoyDom.style.backgroundColor = "#e9ecef";
        yoyDom.style.color = "#e63d09";
    } else {
        yoyDom.style.backgroundColor = "#d3d3d3";
        yoyDom.style.color = "#4d4d4d";
    };

    momDom.innerHTML = mom ? mom.toFixed(2) + "% MoM" : "null";

    if (mom >= 0 && mom != "") {
        momDom.style.backgroundColor = "#d3f9d8";
        momDom.style.color = "#2b8a3e";
    } else if(mom < 0 && mom != "") {
        momDom.style.backgroundColor = "#e9ecef";
        momDom.style.color = "#e63d09";
    } else {
        momDom.style.backgroundColor = "#d3d3d3";
        momDom.style.color = "#4d4d4d";
    };
    console.log(chart_m_cur);
    option.xAxis.data = chart_x;
    option.series[0].data = chart_m_cur;
    chart_month.setOption(option);
}
Нажимаем кнопку «Опубликовать». Теперь мы можем вывести наш виджет на любую информационную панель (Дашборд).