Готовые шаблоны Handlebars для Apache Superset
Готовые шаблоны Handlebars для Apache Superset

Apache Superset всё чаще становится выбором для визуализации данных благодаря открытому коду. Но, увы, столкнувшись с его ограничениями и тонкостями, даже самые опытные пользователи могут столкнуться с трудностями. Есть много ограничений, которые требуют обращения за доработками к разработчикам, но с помощью шаблона Handlebars в сочетании с шаблонизацией jinja некоторые трудности можно обойти.

С его помощью можно внедрить web-верстку прямо в ваши дашборды, обходя множество подводных камней. Готовых шаблонов для handlebars (superset) мало, так как это довольно трудоемкая задача, часто выходящая за рамки работы с готовыми BI-системами.

Работая аналитиком данных и столкнувшись с ограничениями в вёрстке дашборда, пришлось разработать несколько кастомизированных шаблонов, которые можно быстро применить на практике. Если пользователь не знаком с html и css - поначалу это будет довольно сложно, но постараюсь как можно подробнее расписать значение классов и элементов шаблона для неопытных пользователей, заодно можно немного погрузиться в мир web-разработки :).

Общие правила использования Handlebars для Superset:

  • сначала создаём необходимые показатели, которые будут отображаться в карточках,

Использование графиков Handlebars (названия окон для кода могут различаться, в зависимости от версии)
Использование графиков Handlebars (названия окон для кода могут различаться, в зависимости от версии)
  • размещаем код HTML + jinja в первом окне, CSS во втором. Элементы jinja заключаются в двойные фигурные скобки {{ col1 }}, внутри ваш показатель, он автоматически изменяется по мере обновления данных,

Пара {{#each data}} ... {{/each}}: блок шаблона Handlebars, который проходит по каждому элементу массива data и генерирует соответствующую разметку.

  • пользуясь подсказками к коду CSS, меняем цвета, размер полей и т.д. в соответствии со своими потребностями.

Первый шаблон

Набор карточек с общим показателем на 2 строки.

Набор карточек, с возможностью регулирования полей между всеми элементами, они автоматически растягиваются под размер контейнера
Набор карточек, с возможностью регулирования полей между всеми элементами, они автоматически растягиваются под размер контейнера

Иногда нужны довольно мелкие карточки для показателей и не всегда возможно много разместить на дашборде. Некоторым неудобством является, что для каждой карточки нужно прописать отдельный показатель если хочется разные сущности отобразить в одном контейнере, но это иногда и упрощает задачу по поиску нарушений в данных.

Готовый код HTML + jinja размещаемый в первом окне Handlebars Template:

<div class="f-section">
  {{#each data}}
  <div class="f-container">
    <div class="f-item left-f">
      <div class="glav">ВСЕГО</div>
      <div class="all">{{ ppt_all }}</div>
    </div>
    <div class="right-column">
      <div class="top-row">
        <div class="f-item">
          <h5>РЛС</h5>
          <div class="number">{{ rls }}</div>
        </div>
        <div class="f-item">
          <h5>ЛА</h5>
          <div class="number">{{ la }}</div>
        </div>
        <div class="f-item">
          <h5>ВО</h5>
          <div class="number">{{ vo }}</div>
        </div>
        <div class="f-item">
          <h5>ЛВО</h5>
          <div class="number">{{ lvo }}</div>
        </div>
        <div class="f-item">
          <h5>ТН</h5>
          <div class="number">{{ tn }}</div>
        </div>
        <div class="f-item">
          <h5>ОХО</h5>
          <div class="number">{{ oho }}</div>
        </div>
        <div class="f-item">
          <h5>КАК</h5>
          <div class="number">{{ kak }}</div>
        </div>
        <div class="f-item">
          <h5>ИСА</h5>
          <div class="number">{{ isa }}</div>
        </div>
        <div class="f-item">
          <h5>КХО</h5>
          <div class="number">{{ kho }}</div>
        </div>
        <div class="f-item">
          <h5>КП</h5>
          <div class="number">{{ kp }}</div>
        </div>
      </div>
      <div class="bottom-row">
        <div class="f-item">
          <div class="header1">СТИ ВСЕГО</div>
          <div class = "spp">11 344 тыс. м.</div>
        </div>
        <div class="f-item">
          <div class="header1">ГМС ВСЕГО</div>
          <div class = "spp">2 479 тыс. м.</div>
        </div>
        <div class="f-item">
          <div class="header1">ЦСМ ВСЕГО</div>
          <div class = "spp">2 650 тыс. м.</div>
        </div>
        <div class="f-item">
          <div class="header1">ГПК ВСЕГО</div>
          <div class = "spp">3 345 тыс. м.</div>
        </div>
      </div>
    </div>
  </div>
 {{/each}}
</div>

Готовый код CSS для второго окна:

.f-container {
    display: flex;
    gap: 14px;
  }
.left-f {
    flex: 0.4;
    width: 160px;
  }
.right-column {
    flex: 3;
    display: grid;
    flex-direction: column;
    gap: 10px;
  }
.top-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
    margin-bottom: 14px;
    gap: 10px;
  }
.bottom-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 10px; 
  }
.f-item {
    font-weight:bolder;
    border: solid 1px lightgray;
    background: #fff;
    padding: 10px 8px 6px 8px;
    border-radius: 12px;
    text-align: center;
    box-shadow: 0 4px 4px rgba(0,0,0,0.2);
    transition: transform 0.3s, box-shadow 0.3s;
  }
.f-item:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 10px rgba(0,0,0,0.2); 
  }
.f-item h5 {
    margin: 0 0 8px 0;
    font-weight:bolder;
  }
.glav {
    font-weight:bolder;
    padding-top: 30px;
    font-size: 22px;
    color: #c90808;
  }
.all {
    font-size: 26px; 
    color: #c90808;
  }
.number {
    font-weight:bolder;
    font-size: 20px;
    color: #2e75b6;
  }
.header1 {
    font-weight:bolder;
    color: #2e75b6; 
  }
.spp {
    font-style: italic;
  }

Подробное объяснение css для тех, кто web-вёрсткой обычно не занимается, но общее представление имеет

.f-container {   /*общий контейнер*/ 
  display: flex; /*позволяет элементы в строку или столбец с гибким расположением.*/
  gap: 14px;     /*промежуток между элементами внутри flex-контейнера*/
}

.left-f {        /*большая карточка*/
  /*коэффициент гибкости для элемента внутри flex-контейнера*/
  /*в данном случае элемент будет занимать 0.4 доли доступного пространства*/
  flex: 0.4; 
  width: 160px;          /*фиксированная ширина элемента в пикселях*/
}

.right-column {           /*колонка с двумя строками карточек*/
  /*коэффициент гибкости для элемента, позволяя ему занимать*/
  /*в 3 раза больше пространства по сравнению с элементами с меньшим значением*/
  flex: 3; 
  display: grid;          /*создание сеточной компоновки*/
  flex-direction: column; /*определяет направление расположения элементов внутри flex-контейнера по вертикали (в столбец)*/
  gap: 10px;
}

.top-row {               /*верхняя строка*/
  display: grid;
  /*определяет шаблон столбцов сетки, автоматически подгоняя количество столбцов*/
  /*под доступное пространство с минимальной шириной 70 пикселей*/
  /*и распределением оставшегося пространства поровну (1fr).*/
  grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
  margin-bottom: 14px;   /* внешний отступ под элементом*/
  gap: 10px;             /*промежуток между элементами сетки*/
}

.bottom-row {            /*нижняя строка*/
  display: grid;
  /*настраивает шаблон столбцов сетки с автоматическим подбором количества столбцов,*/
  /*минимальная ширина каждого столбца 120 пикселей, а оставшееся пространство*/
  /*распределяется равномерно (1fr)*/
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px; 
}

.f-item {                      /*внешний вид всех карточек*/
  font-weight:bolder;          /*более жирное начертание шрифта*/
  border: solid 1px lightgray; /*рамка карточек с шириной и серым цветом*/
  background: #fff;            /*белый фон для элемента*/
  /*внутренние отступы: сверху 10px, справа 8px, снизу 6px, слева 8px*/
  padding: 10px 8px 6px 8px;
  border-radius: 12px;         /*закругляет углы элемента радиусом 12 пикселей*/
  text-align: center;          /*выравнивает текст внутри элемента по центру*/
  /*добавляет тень к элементу с отступом по оси X: 0, по оси Y: 4px,*/
  /*размытием: 4px и цветом с прозрачностью 20%*/
  box-shadow: 0 4px 4px rgba(0,0,0,0.2); 
  /*определяет переходные эффекты для свойств transform*/
  /*и box-shadow с длительностью 0.3 секунды*/
  transition: transform 0.3s, box-shadow 0.3s;
}

.f-item:hover {                /*анимация карточек*/
  /*при наведении курсора элемент сдвигается вверх на 5 пикселей*/
  transform: translateY(-5px);
  /*усиливается тень элемента: вертикальный отступ 8px, размывание 10px*/
  /*с тем же цветом и прозрачностью.*/
  box-shadow: 0 8px 10px rgba(0,0,0,0.2); 
}

.f-item h5 {                  /*внешние отступы для заголовков верхних карточек*/
  margin: 0 0 8px 0;           
  font-weight:bolder;
}

.glav {                       /*текст на большой карточке*/
  font-weight:bolder;
  padding-top: 30px;          /*внутренний отступ*/
  font-size: 22px;            /*размер шрифта*/
  color: #c90808;             /*цвет текста*/
}

.all {                       /*показатель на большой карточке*/
  font-size: 26px; 
  color: #c90808;            /*цвет текста - оттенок красного*/
}

.number {                    /*стилизация цифр карточек в верхней строке*/
  font-weight:bolder;
  font-size: 20px;
  color: #2e75b6;            /*цвет текста - оттенок синего*/
}

.header1 {                   /*заголовки карточек нижней строки*/
  font-weight:bolder;
  color: #2e75b6; 
}

.spp {
  font-style: italic;        /*стиль текста - курсив*/
}

Второй шаблон

Имитация горизонтального бара
Имитация горизонтального бара

График с надписями на цветовых элементах и показателем с общей суммой со всплывающими подсказками. Цветовые шкалы меняются в процентном соотношении друг к другу.

Лучше всего выглядит для двух качественных значений, с уже накопленными показателями.

Готовый код HTML + jinja размещаемый в первом окне Handlebars Template

<div class="hb-ppt-oc">
  {{#each data}}
  <div class="bar-container">
    <div class="bars">
      <div class="bar gray-bar" 
           style="flex-grow: {{ ppt_1 }};" 
           title="Общие: {{ ppt_1 }}">
        <span class="bar-label">ОБЩИЕ: {{ ppt_1 }}</span>
      </div>
      <div class="bar red-bar" 
           style="flex-grow: {{ ppt_2 }};" 
           title="Сезонные: {{ ppt_2 }}">
        <span class="bar-label">СЕЗОННЫЕ: {{ ppt_2 }} </span>
      </div>
      <div class="bar-summ">БЮДЖЕТ: {{ inv }}</div>
    </div>
  </div>
  {{/each}}
</div>

Готовый код CSS для второго окна:

.hb-ppt-oc {
  width: 100%;
  margin: 0 auto;
}
.bar-container {
  width: 100%;
  height: 50px;
}
.bars {
  display: flex;
  align-items: center;
  height: 100%;
  width: calc(100% - 150px);
}
.bar-summ {
  margin-left: 20px;
  font-size: 18px;
  font-weight: bolder;
  flex: 0 0 150px;
  text-align: right;
}
.bar {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 95%;
  position: relative;
  color: white;
  font-weight: bold;
}
.gray-bar {
  margin-left: 20px;
  background-color: #9c9c9b;
  padding: 0 5px;
  border-radius: 6px 0px 0px 6px;
  margin-right: 0;
}
.red-bar {
  background-color: #e06969;
  border-radius: 0px 6px 6px 0px;
}
.bar-label {
  position: absolute;
  width: 100%;
  text-align: center;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

Подробное объяснение css для тех, кто вёрсткой обычно не занимается, но общее представление имеет

.hb-ppt-oc {         /*общий контейнер по центру и занимает всю доступную ширину*/
  width: 100%;       /*занимает всю доступную ширину родительского контейнера*/
  margin: 0 auto;    /*центрирует контейнер по горизонтали*/
}

/*Внутри .hb-ppt-oc, для каждого элемен��а данных ({{#each data}}),*/
/*создаётся .bar-container с фиксированной высотой.*/
.bar-container {  
  width: 100%; 
  height: 50px;      /*фиксированная высота для строки баров*/
}

.bars {                      /*Flex-контейнер внутри .bar-container*/
  display: flex;             /*Flexbox для горизонтального расположения*/
  align-items: center;       /*вертикальное выравнивание элементов по центру*/
  height: 100%;              /*высота равна высоте .bar-container*/
  width: calc(100% - 150px); /*занимает всю ширину минус 150px для .bar-summ*/
}

.bar-summ {              /*контейнер для подписи БЮДЖЕТ и числового показателя суммы*/
  margin-left: 20px;     /*отступ слева для отделения от бара*/
  font-size: 18px;       /*размер шрифта*/
  font-weight: bolder;   /*более жирный шрифт*/
  flex: 0 0 150px;       /*фиксированная ширина элемента с суммой*/
  text-align: right;     /*выравнивание текста по правому краю*/
}

.bar {  
  display: flex; 
  align-items: center; 
  justify-content: center;   /*центрирование по горизонтали*/
  height: 95%;               /*уменьшенние высоты для визуального отступа*/
  position: relative;        /*позиционирование дочерних элементов .bar-label*/
  color: white;              /*белый цвет текста на цветных барах*/
  font-weight: bold;
}

.gray-bar {                         /*серый бар*/
  margin-left: 20px;         /*отступ слева для отделения от предыдущего элемента*/
  background-color: #9c9c9b;        /*серый цвет фона бара*/
  padding: 0 5px;                   /*внутренние отступы слева и справа*/
  border-radius: 6px 0px 0px 6px;   /*скругление левых углов баров*/
  margin-right: 0;                  /*без отступа справа*/
}

.red-bar {                     /*красный бар*/
  background-color: #e06969;
  border-radius: 0px 6px 6px 0px;
}

.bar-label {                  /*текстовые описания и значение для баров*/
  position: absolute;         /*абсолютное позиционирование относительно .bar*/
  width: 100%; 
  text-align: center;
  font-size: 14px;
  white-space: nowrap;        /*запрет переноса текста на новую строку*/
  overflow: hidden;           /*скрывает содержимое, выходящее за пределы блока*/
  text-overflow: ellipsis;    /*добавляет троеточие при обрезке текста*/
}

В завершение, несколько подсказок:

  • нужно аккуратно относиться к форматированию кода в окнах, он может не срабатывать и выдавать ошибки если есть подсказки или пробелы в строках;

  • элементы классов и элементов css лучше для каждого графика называть индивидуально, т.к. они будут влиять на все графики на дашборде с одинаковыми именами классов, вы будете менять что-то в коде, а это не будет срабатывать или наоборот срабатывать не там :);

  • осторожнее с body, его включать в код не стоит, он будет влиять на весь дашборд;

  • можно было бы уменьшить код, и не прописывать каждый показатель отдельно в первом случае, а просто поместить колонку с названием показателей в код html, но карточки с нулевыми значениями показателя не появлялись, даже если в вычислении использовался SQL-запрос: COALESCE(COUNT(col1), 0).

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

Вторая часть здесь

Так как часто возникают проблемы с отображением HTML/CSS - элементов (они появляются в виде текста, а не являются исполняемым кодом). Во второй части размещу дополнение о возможных причинах этого и возможности решения.