Как BI-команда краснодарского ИТ-хаба мигрировала HR-отчетность и собрала GuideBook

Результатом работы BI-аналитика должен быть не просто отчет ради отчета — важно наладить партнерство с бизнесом: понимать его потребности и боли, а уже потом превращать это понимание в конкретный продукт.
BI-аналитик — это бизнес-партнер по данным и отчетности. Он прикреплен к определенному домену или продукту, понимает, как этот домен работает и какие бизнес-процессы существуют внутри него. Хорошо разбирается в данных своего направления — знает, откуда они берутся и как устроены. Именно поэтому BI-аналитик ведет свои продукты по полному циклу — от сбора требований до внедрения и поддержки, — а не просто визуализирует то, что попросили.
Результатом работы становится продукт, который закрывает понятную регулярную потребность бизнеса и имеет пользователей. Таким продуктом может быть дашборд, рассылка, алертинг, система мониторинга или бот. Если у продукта нет реальных пользователей и он не решает конкретную боль, это сигнал, что партнерство с бизнесом не сложилось.
Важно не забывать про качество данных: перед тем как строить любой продукт, BI-аналитик проверяет корректность данных, на которых он основан. Масштаб этой работы в компании впечатляет: BI-аналитиков у нас несколько сотен, а на BI-платформе размещено почти 12 тысяч отчетов, с метриками MAU — 31 тысяча, с DAU — 13 тысяч.
Кейсы из HR-аналитики
А теперь перейдем от общих цифр к реальным кейсам на примере моего направления. Моим первым большим вызовом в компании стала простая задача — посчитать сотрудников. Это может звучать странно, но, когда компания большая и каждый отдел имеет собственное представление о том, от какой даты считать, это интересный вызов.
Одни команды включали человека в численность с даты подписания договора, другие — только после того, как он проходил обязательное обучение. А дальше вопросов становилось только больше. Учитываем ли мы в численности стажеров? А людей в декретном отпуске? Они формально в штате, но фактически не работают. И подобных пограничных случаев набралось множество. Из этого разнообразия логик предстояло собрать единую методологию, согласовать ее со всеми стейкхолдерами и реализовать в источниках данных.
Мы запустили работу как pet-проект и получили первый результат уже через полгода: топ-менеджмент и руководители операционных подразделений увидели данные о численности компании, подсчитанные по единой согласованной логике. Еще несколько месяцев ушло на то, чтобы оптимизировать процессы сборки данных, и меньше чем за год у нас была витрина с MAU 500+.
Витрина оказалась не просто удачным отчетом, а точкой отсчета для более крупных и амбициозных проектов в HR-аналитике. Она подтолкнула сами системы-источники к тому, чтобы выстроить единые процессы найма и расторжения — с одной логикой для всех сотрудников. А чистые данные из источников открыли путь к промышленной enterprise-витрине: коллеги переиспользовали наши наработки и благодаря этому вывели ее в промышленную эксплуатацию заметно быстрее.
Со своей стороны, в BI-продуктах мы собрали мастер-отчет по ключевым HR-метрикам: численности, найму, оттоку и приросту. Путь к финальной версии отчета был непростым. Заказчики придирчиво проверяли каждое изменение, и это неудивительно: любая правка в таком отчете требует времени на принятие. А руководители вообще сверяли численность по количеству участников в командных чатах — с такой методологией сложно было спорить. Сейчас этот отчет входит в топ-3 среди всех отчетов в компании по количеству пользователей.
Как меняется стек: от Tableau к Proteus
Когда первые пользователи активно начали пользоваться отчетом и, что не менее важно, верить данным в нем, прилетел новый вызов. Мы отказались от Tableau. В 2024 году мы столкнулись с этим техническим вызовом, который обязывал за год перевести всю отчетность с Tableau на Proteus.
Proteus — внутренний продукт компании, построенный на базе Superset. Под него выделилась отдельная команда разработки, которая адаптировала продукт под запросы аналитиков в Т-Банке.
В январе 2024, на старте миграции, в компании было порядка 2 200 отчетов, которые предстояло перевезти на новый инструмент. В рамках нашей команды нужно было перевести 70 отчетов.
Первые проблемы не заставили себя ждать. В систему пришел сразу двойной наплыв пользователей: с одной стороны — новые аналитики, которые приступили к проекту миграции, с другой — бизнес-пользователи, которые уже начали использовать перенесенные отчеты в своей повседневной работе.
Каждый день чат с анонсами продукта пестрел алертами о недоступности системы. Команда продукта прилагала усилия к стабилизации ситуации, но проблема оставалась актуальной. Спустя месяц команда Proteus решила приостановить миграцию.
В своем анонсе команда продукта честно признала: сбоев стало больше, а пользовательский опыт заметно деградировал. Среди причин назвали сразу несколько факторов: сложную кодовую базу, которая копилась усилиями разных разработчиков на протяжении 10 лет и хорошо держала нагрузку лишь до сотни пользователей, неоптимальное использование ресурсов базовой платформы, дефицит на рынке нужных для продукта разработчиков и сильно обновившийся за последние три месяца состав команды, которому требовалось время на онбординг.
Миграцию поставили на паузу, чтобы не наращивать нагрузку на систему дальше, — уже перенесенные отчеты при этом остались в работе. Команда продукта занялась поиском причины роста потребления ресурсов, привлекая в помощь сильных разработчиков компании, и обещала нарастить вычислительные мощности. А от аналитиков попросили одного — временно воздержаться от создания новых отчетов в Proteus, чтобы дать системе стабилизироваться.
Но пауза с нагрузкой на систему не решала другую проблему. Изменения касались не только самого BI-инструмента, но и подхода к работе с источниками внутри BI-системы.
Раньше аналитик писал запрос в Greenplum, дальше делал экстракт в Tableau — и уже внутри самого инструмента мог как угодно крутить источник: делать оконные функции, сложные вычисления, донастраивать данные под задачу.
Переход на Proteus изменил эту логику. Сама система живет на базе ClickHouse, а привычного формата экстрактов там просто нет. А значит, каждый запрос теперь отправляется напрямую в базу. Каждый заход на дашборд, каждая настройка фильтра — это новый запрос в ClickHouse.
Проблема с нагрузкой на базу отчасти была связана и с тем, что каждый аналитик использовал собственные наработки и подходы к подготовке источников. Хорошо, если команды внутри хотя бы как-то обменивались практиками по оптимизации между собой. Но, если чудом удавалось загрузить неоптимизированный источник в Proteus, дальше чаще всего все ломалось уже на самом дашборде: запросы из него летели прямиком в ClickHouse и база уже не справлялась с тем объемом и способом, каким ее просили посчитать данные.
Вопрос оптимизации источников стоял достаточно остро, поэтому команда продукта и SRE ClickHouse подготовили конкретные рекомендации для аналитиков. В основном они сводились к двум подходам.
Оптимизация ORDER BY. Ключ ORDER BY в ClickHouse фактически работает как первичный индекс таблицы, который ускоряет чтение при запросах. Логика простая: если выставить ключ по тем колонкам, которые чаще всего используются в фильтрах запроса, ClickHouse сразу отсекает ненужные строки, вместо того чтобы сканировать всю таблицу. Здесь важно не переусердствовать. Пытаться заложить в ключ все возможные сценарии фильтрации не стоит: это только все усложняет. Эффективнее выбрать пару самых частых сценариев использования и оптимизировать именно под них.
Оптимизация партиционирования. Второй подход помогает с большими таблицами, где запросы чаще всего обращаются к конкретному временному промежутку — например, последним нескольким месяцам. Партиционирование по дате позволяет ClickHouse читать только нужный кусок данных, а не всю историю таблицы целиком, что ощутимо сокращает объем чтения по каждому запросу.
Еще один подход к оптимизации касался уже не самих данных, а того, как строятся запросы к ним внутри Proteus. На помощь пришла Jinja-шаблонизация. Вместо того чтобы писать отдельный статичный запрос под каждый срез отчета, можно один раз написать «живой» шаблон с местами-заглушками — например, для диапазона дат или выбранного подразделения, — которые автоматически подставляются в запрос в зависимости от того, что пользователь выбрал на дашборде.
Ценность здесь двойная. С одной стороны, вместо десятков почти одинаковых запросов под разные фильтры и отчеты пишется один параметризованный шаблон — это упрощает поддержку кода: правку нужно вносить в одном месте, а не искать все его копии. С другой стороны, для ClickHouse, который чувствителен к объему сканируемых данных, это особенно ценно: условия фильтрации можно вшить прямо в запрос через Jinja, чтобы база сразу отсекала ненужные строки, а не тянула весь массив данных с последующей фильтрацией уже при отрисовке конкретного визуала.
Например, на первых этапах оптимизации мы стали параметризовать через Jinja фильтр по дате — вместо того чтобы каждый раз тянуть из ClickHouse всю историю таблицы, запрос сразу ограничивался нужным диапазоном дат, который выбрал пользователь на дашборде. При больших объемах данных, где количество строк могло превышать 10 млн, такой подход позволил сократить время выполнения запроса минимум в два раза — с 20 до 10 секунд.
В ходе тестов и экспериментов мы пришли еще к одному подходу: источники стоит разделять под конкретный блок отчета, а не готовить один универсальный датасет на все визуалы сразу. Логика такая: если для одного визуала нужно показать метрику в разрезе большого количества измерений или за длинный период, источник под него действительно должен содержать весь этот объем данных. Но, если рядом стоит визуал, которому эта детализация не нужна, — например, для него достаточно агрегата по нескольким ключевым полям — тянуть туда тот же тяжелый источник нет смысла. Вместо этого для такого визуала лучше собрать и загрузить в ClickHouse отдельную облегченную версию датасета, урезанную ровно до нужных полей и уровней агрегации. Такое разделение снижает нагрузку на ClickHouse сразу с двух сторон: не по всем визуалам гоняются тяжелые запросы к широким таблицам, а сама база быстрее вычитывает компактные датасеты там, где детализация избыточна.
Опыт с Jinja-параметризацией, экспериментами вокруг объемов данных и разделением источников под конкретные визуалы прокачал наши хард-скиллы по оптимизации источников и научил чувствовать особенности новой базы. Вместе с этим мы выработали критерии, необходимые для заливки источников.
После того как у нас наконец получилось залить датасет, оставалось разработать визуал. Тут нас ждал неприятный сюрприз: стандартные коробочные графики не позволяли в полной мере закрыть потребность заказчика. А если совсем честно, со стандартными графиками у нас не было возможности сделать красивый визуал, к которому привыкли наши пользователи в Tableau.
И вот тут в нашу жизнь с ноги ворвались Handlebars.
Handlebars и кастомные визуалы
Handlebars — конструктор-шаблонизатор JavaScript, который позволяет создавать динамические HTML-шаблоны, объединяя работу с данными и HTML-разметку. В контексте Proteus Handlebars используется как прослойка, позволяющая связать данные из источника с визуальной составляющей графика, — и при этом график автоматически обновляется при изменении данных.
Внутри Proteus Handlebars доступен как один из стандартных графиков. Эти «усы» {{ }} с визуальным оформлением графика сложно не заметить. Интерфейс Handlebars состоит из двух основных вкладок: «Данные» и «Кастомизация».

На вкладке «Данные» через drag-in-drop можно добавить измерения из датасета, существующие меры или посчитать новые. На этой вкладке настраивается сортировка и фильтрация данных, если это требуется
Вкладка «Кастомизация» включает два основных блока: шаблон Handlebars и CSS-стили. Именно здесь и происходит вся магия.

Технически связка простая: результат запроса, который аналитик собрал на вкладке «Данные» (все измерения и меры), Proteus передает в шаблон единым массивом объектов — он доступен под именем data. Каждая строка этого массива — это одна строка из выборки, а ее поля называются точно так же, как колонки в датасете. Если в источнике есть измерение department и мера cnt_employee, обратиться к ним в шаблоне можно так:
{{#each data}} <div class="row"> <span>{{this.department}}</span> <span>{{formatCurrency this.cnt_employee locale="ru" pattern='#,##0.'}}</span> </div> {{/each}}
Из технической связки вытекает первое практическое правило для аналитика: то, что не выведено на вкладке «Данные» как измерение или мера, физически не попадет в шаблон. Поэтому, прежде чем писать сам Handlebars-код, нужно четко продумать, какие поля потребуются на визуале, включая служебные вроде полей для сортировки или дополнительного разреза, который не отображается напрямую, но используется в логике шаблона.
Кроме базового синтаксиса {{ }} Handlebars оперирует хелперами — по сути, это функции, в которые мы передаем аргумент, а они возвращают готовый к отображению результат. Они бывают блоковыми, то есть работающими как итератор с открывающим и закрывающим тегом ({{#each data}} ... {{/each}}), и обычными, которые просто вызываются с аргументами в одну строку ({{sum 'первый аргумент' 'второй аргумент'}}).
Полный список хелперов, которые в принципе существуют в экосистеме Handlebars, можно посмотреть по ссылке just-handlebars-helpers — там и математические функции, и сравнения, и работа со строками. Но здесь мы столкнулись с важным нюансом: не все хелперы из этого списка оказались доступны внутри Proteus. Какие-то работали из коробки, какие-то — нет, и это выяснялось только опытным путем, на конкретной задаче.
Поэтому, прежде чем закладывать в шаблон логику вокруг конкретного хелпера, стоит сначала проверить его доступность непосредственно в вашей BI-системе, а не ориентироваться на документацию пакета как на гарантированный список. Из того, что реально работало и использовалось командой, — each, if, unless для управления структурой вывода, eq/gt/lt и их производные для сравнений, sum/abs/ceil/floor для вычислений и formatCurrency для форматирования чисел под нужную локаль.
Если вкладка с шаблоном отвечает за структуру и логику визуала, то блок «CSS-стили» отвечает за его внешний вид. Здесь задаются параметры оформления, которые затем применяются к объектам, размеченным в HTML-шаблоне. На практике этот блок закрывает три типовые задачи. По сути, блок «CSS-стили» дает ту же гибкость, что и обычный CSS в вебе: стили создаются один раз, а затем применяются к нужным элементам через классы, размеченные в HTML-коде шаблона.
Если сложить все это вместе — подготовку всех основных и служебных полей в источнике, совместимость хелперов с Proteus и отдельный слой CSS-стилей поверх HTML-разметки, — становится понятно, почему мы не сразу прибегли к этой функциональности. Коллегам, которые никогда не сталкивались с синтаксисом HTML-разметки, было страшно и сложно принять мысль, что теперь для сборки визуала требуется куда больше усилий.
GuideBook: база знаний команды
Как бы ни хотелось написать, что есть какая-то волшебная таблетка, которая помогла с этим смириться, никакой магии не произошло. Пройдя все этапы от отрицания до принятия, мы приняли, что такова наша новая реальность, от которой никуда не деться.
Шаги, которые помогли усмирить HTML: всей командой мы прошли курс в HTML Академии и чаще стали проводить командные встречи, на которых делились наработками и полученным опытом.
Идея встреч возникла потому, что большая часть команды не успевала детально погружаться в новый инструмент, из-за чего создавался пробел в навыках сотрудников. От коллег с меньшим опытом в продукте пришел запрос: пусть более опытные коллеги делятся своими наработками. А более опытные коллеги, могли таким образом уберечь новичков от столкновения с уже знакомыми им ошибками.
Встречи проводились один-два раза в неделю — в зависимости от того, накопилась ли повестка и появились ли новости, которыми хотелось поделиться. Формат был онлайн: сначала спикер рассказывал свою часть, а дальше в режиме открытого микрофона коллеги задавали уточняющие вопросы.
Мне больше всего запомнилась одна из первых встреч, когда коллега демонстрировал первые попытки стилизации базовых графиков из коробки с помощью CSS. А я сидела и не могла отделаться от мысли, как в университете радовалась, что в будущей работе мне вряд ли когда-нибудь пригодятся эти навыки. Никогда не говори «никогда».
Несмотря на записи встреч и основных заметок после, не все коллеги успевали разбираться с новой функциональностью. В связи с этим возник вопрос: а в каком виде можно хранить накопленные знания, чтобы они не терялись и были доступны каждому в удобном формате? Так появился GuideBook.
GuideBook — это отчет в самом Proteus. Реализацию начали с добавления инструкций, как сделать барчарты и KPI-карточки, потому что это самые часто используемые решения в наших отчетах.
Первым к работе над продуктом приступил мой руководитель. Он был амбассадором Proteus от нашей команды, то есть первым узнавал новости от команды продукта. К нему присоединились ребята, которые уже активно использовали HTML/CSS в своих отчетах, а дополнительно на part time к команде присоединился коллега, который большую часть жизни занимался фронтенд-разработкой.
Ребята активно взялись за разработку новых инструкций. Чем больше членов команды входило в процесс миграции своих отчетов, тем больше вопросов возникало на командных встречах и в командных чатах. За несколько месяцев GuideBook разросся. Там появилось описание сводной таблицы, диаграммы Ганта, лайнчартов, спарклайнов, а также реализация всевозможных барчартов: вертикальных с двумя метриками, вертикальных bar-in-bar, горизонтальных и смешанных графиков.
Ценность GuideBook не в том, что он просто объясняет, как устроен график, а в том, что он дает готовую рабочую заготовку. Вместо того чтобы разбираться в Handlebars-коде и верстке с нуля, аналитик находит нужный тип графика, копирует шаблон и подключает к нему свой источник данных. Так теоретическое знание «как это работает» превращается в практический инструмент «как это сделать за пару кликов».
Чтобы показать, каков GuideBook на практике, разберем его устройство на примере вертикального барчарта.

В первую очередь в инструкции приводится исходный датасет. Handlebars считывает данные как массив, поэтому исходный датасет важно правильно подготовить и положить в источник. Далее в инструкции описываются особенности подготовки датасета — какие меры необходимо добавить и как настроить сортировку.
Добавление мер. Основная метрика — в настройках графика в меры добавляем показатель metric_value, например count(distinct mdm_employee_rk).
Добавление измерений. Календарная дата — в настройках графика в измерения добавляем разрез start_of_month_dt, например date_trunc('month', business_dt). ID графика — берем это значение из адресной строки браузера (slice_id=XXXXXX) и вставляем эти шесть цифр в измерения.
Добавление сортировки. В настройках графика в сортировку добавляем расчетное поле, по которому требуется отсортировать барчарт, например date_trunc('month', business_dt).
Перед тем как приступить к чтению особенностей настройки самого Handlebars, советуем открыть график с кодом и, проходясь по коду, изучать его особенности на практике: так описание ниже сразу ложится на реальный пример перед глазами.
Код для вертикального барчарта.
Блок HTML
<div class="chart-container">
<div class="chart-header">
<span class="chart-header-title">
Динамика штатной численности, чел.
</span>
<div class="header-legend">
<div class="legend-value">Сотрудники</div>
</div>
</div>
<div class="main-container">
{{#each data}}
<div class="chart-item {{#if (eqw (formatDate 'M' this.start_of_month_dt 'ru') 1)}}first-month{{/if}}">
<div class="barchart-item">
<div class="barchart-content" style="height: calc({{this.metric_value}}/{{max (pluck @root.data 'metric_value')}}*90%)">
<p class="barchart-values">{{formatCurrency this.metric_value locale="ru" pattern='#,##0.'}}</p>
<p class="tooltip" >
{{formatDate 'MMM YYYY' start_of_month_dt 'ru'}}<br>
<span class="item-mark" style="color:var(--barchart-color)">⬤ </span>
чел: {{formatCurrency this.metric_value locale="ru" pattern='#,##0.'}}</p>
</div>
</div>
{{#if (eqw (formatDate 'M' this.start_of_month_dt 'ru') 1)}}
<div class="barchart-x-axis">
<p class="lables year">
{{formatDate 'YYYY' start_of_month_dt 'ru'}}
</p>
</div>
{{else}}
<div class="barchart-x-axis">
<p class="lables month">
{{formatDate 'MMM' start_of_month_dt 'ru'}}
</p>
</div>
{{/if}}
</div>
{{/each}}
</div>
</div>
<style>
/* частота появления значений меток */
#chart-id-98114 .chart-item:not(:nth-child({{ ceil ( division (count data) 20 ) }}n)) .barchart-values {
visibility: hidden;
}
/* частота появления значений подписей оси X */
#chart-id-98114 .chart-item:not(:nth-child({{ ceil ( division (count data) 0 ) }}n + 1)) .barchart-x-axis .lables.month {
visibility: hidden;
}
</style>
Блок CSS
#chart-id-{{data.[0].ID}} {
--barchart-color: rgb(160, 222, 255);
--barchart-background-color: rgba(0, 0, 0, 0);
--barchart-year-separator: 1px dashed #dbdbdb;
--barchart-x-axis-separator: 1px solid rgb(155, 164, 181);
--barchart-height: 100%;
--barchart-max-height: 200px;
--barchart-width: min(10px, 15%);
--barchart-radius: 2px;
--barchart-header-title: visible; /*заголовок. hidden - скрыть*/
--barchart-header-legend: visible; /*легенда. hidden - скрыть*/
--barchart-header: flex; /*шапка (заголовок и легенда). none - убрать*/
}
.dashboard-chart-id-{{data.[0].ID}} .header-title {
display: none;
}
.dashboard-component:has(#chart-id-{{data.[0].ID}}) {
padding: 0;
}
#chart-id-{{data.[0].ID}} > div {
width: 100%;
padding-top: 13px;
}
#chart-id-{{data.[0].ID}} .chart-container {
height: 100%;
}
#chart-id-{{data.[0].ID}} .chart-header {
display: var(--barchart-header);
justify-content: space-between;
height: 24px;
padding-right: 10px;
}
#chart-id-{{data.[0].ID}} .chart-header-title {
align-content: center;
font-size: 14px;
font-weight: bold;
visibility: var(--barchart-header-title);
}
#chart-id-{{data.[0].ID}} .header-legend {
display: flex;
visibility: var(--barchart-header-legend);
gap: 10px;
}
#chart-id-{{data.[0].ID}} .legend-value {
font-size: 10px;
align-content: center;
text-align: center;
padding: 0 6px 0 6px;
border-radius: 4px;
opacity: 0.8;
}
#chart-id-{{data.[0].ID}} .legend-value:nth-child(1) {
background-color: var(--barchart-color);
}
#chart-id-{{data.[0].ID}} .main-container {
height: 100%;
max-height: var(--barchart-max-height);
width: 100%;
margin-top: 50px;
display: flex;
position: relative;
justify-content: space-between;
}
#chart-id-{{data.[0].ID}} .chart-item {
opacity: 0.8;
width: 100%;
min-width: 20px;
}
#chart-id-{{data.[0].ID}} .chart-item.first-month {
border-left: var(--barchart-year-separator);
}
#chart-id-{{data.[0].ID}} .chart-item:hover {
opacity: 1;
}
#chart-id-{{data.[0].ID}} .barchart-item {
position: relative;
height: var(--barchart-height);
border-radius: var(--barchart-radius) var(--barchart-radius) 0px 0px;
margin: 0px var(--barchart-width) 0px var(--barchart-width);
background-color: var(--barchart-background-color);
}
#chart-id-{{data.[0].ID}} .barchart-content {
position: absolute;
inset: auto 0 0 0;
border-radius: var(--barchart-radius) var(--barchart-radius) 0px 0px;
background-color: var(--barchart-color);
transition: 0.3s;
}
#chart-id-{{data.[0].ID}} .chart-item:hover .barchart-values {
color: rgb(0, 0, 0);
transition: 0.3s;
font-weight:bold;
}
#chart-id-{{data.[0].ID}} .barchart-values {
position: absolute;
top: -25px;
left: 50%;
font-size: 11px;
transform: translateX(-50%);
}
#chart-id-{{data.[0].ID}} .barchart-x-axis {
border-top: var(--barchart-x-axis-separator);
}
#chart-id-{{data.[0].ID}} .barchart-x-axis .lables {
text-align: center;
height: 28px;
padding: 4px;
margin: 0px;
color: rgba(0,0,0,0.8);
font-size: 11px;
}
#chart-id-{{data.[0].ID}} .barchart-x-axis .lables.year {
font-weight: bold;
}
#chart-id-{{data.[0].ID}} .barchart-item .tooltip {
margin: 0;
display: none;
padding: 8px 10px;
width: 170px;
background-color: white;
font-weight: 400;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
border-radius: 8px;
color:black;
left: 50%;
top:-90px;
transform: translateX(-50%);
opacity:1;
}
#chart-id-{{data.[0].ID}} .chart-item:nth-of-type(1) .tooltip,
#chart-id-{{data.[0].ID}} .chart-item:nth-of-type(2) .tooltip,
#chart-id-{{data.[0].ID}} .chart-item:nth-of-type(3) .tooltip {
left: 0;
transform: none;
}
#chart-id-{{data.[0].ID}} .chart-item:last-of-type .tooltip,
#chart-id-{{data.[0].ID}} .chart-item:nth-last-of-type(2) .tooltip,
#chart-id-{{data.[0].ID}} .chart-item:nth-last-of-type(3) .tooltip{
left:auto;
right:0;
transform:none;
}
#chart-id-{{data.[0].ID}} .barchart-item:hover .tooltip {
display: block;
z-index: 100;
}
#chart-id-{{data.[0].ID}} .tooltip p {
display: flex;
align-items: center;
margin-bottom: 4px;
}
#chart-id-{{data.[0].ID}} .tooltip p:last-child {
margin-bottom: 0;
}Переменные. Внешний вид горизонтального барчарта настраивается через набор CSS-переменных:
--barchart-color — цвет баров и легенды, по умолчанию rgb(160, 222, 255);
--barchart-background-color — цвет фона баров, по умолчанию rgba(0, 0, 0, 0) (чтобы добавить фон, значение можно поменять на rgba(0, 0, 0, 0.1));
--barchart-year-separator — стилизация вертикального разделителя при начале нового года, по умолчанию 1px dashed #dbdbdb;
--barchart-x-axis-separator — стилизация оси X, по умолчанию 1px solid rgb(155, 164, 181);
--barchart-height — высота баров относительно занимаемого контейнера, по умолчанию 100%;
--barchart-width — ширина баров относительно занимаемого контейнера, по умолчанию 6px;
--barchart-radius — округление верхней границы баров, по умолчанию 2px;
--barchart-header-title — показ или скрытие контейнера с заголовком, по умолчанию visible (для скрытия меняем на hidden);
--barchart-header-legend — показ или скрытие контейнера с легендой, по умолчанию visible (для скрытия меняем на hidden).
Основные блоки верстки. Структура графика построена на вложенных контейнерах:
chart-container — основной контейнер для всего графика, объединяет заголовок, легенду и область с данными;
chart-header — секция заголовка графика, включает заголовок и легенду;
header-title — элемент с заголовком графика;
header-legend — контейнер для легенды;
legend-value — элемент легенды, представляющий отдельную категорию;
main-container — основной контейнер для отображения данных графика;
chart-item — контейнер для каждого элемента данных, группирует данные и метку по оси X;
barchart-item — внутренний контейнер для одного бара, задает его общую высоту и ширину;
barchart-content — видимая часть бара, ее высота зависит от значения данных;
barchart-values — общий контейнер для графика;
barchart-x-axis — контейнер для меток на оси X, отображает текстовые метки месяца и года;
barchart-x-axis .labels — элемент для текстовых меток оси X, отвечает за центрирование и отображение текста.
Форматирование значений. В самом начале HTML-разметки, в теге style, задана стилизация двух классов: .barchart-values и .barchart-x-axis. Изменяя знаменатель в расчете {{ceil (division (count data) 20)}}, можно управлять частотой рендеринга отображаемых значений меток и подписей по оси X.
Формат отображения значений меток меняется через хелпер:
Переменные. Внешний вид горизонтального барчарта настраивается через набор CSS-переменных:
--barchart-color— цвет баров и легенды, по умолчаниюrgb(160, 222, 255);--barchart-background-color— цвет фона баров, по умолчаниюrgba(0, 0, 0, 0)(чтобы добавить фон, значение можно поменять наrgba(0, 0, 0, 0.1));--barchart-year-separator— стилизация вертикального разделителя при начале нового года, по умолчанию1px dashed #dbdbdb;--barchart-x-axis-separator— стилизация оси X, по умолчанию1px solid rgb(155, 164, 181);--barchart-height— высота баров относительно занимаемого контейнера, по умолчанию 100%;--barchart-width— ширина баров относительно занимаемого контейнера, по умолчанию6px;--barchart-radius— округление верхней границы баров, по умолчанию2px;--barchart-header-title— показ или скрытие контейнера с заголовком, по умолчаниюvisible(для скрытия меняем наhidden);--barchart-header-legend— показ или скрытие контейнера с легендой, по умолчаниюvisible(для скрытия меняем наhidden).
Основные блоки верстки. Структура графика построена на вложенных контейнерах:
chart-container— основной контейнер для всего графика, объединяет заголовок, легенду и область с данными;chart-header— секция заголовка графика, включает заголовок и легенду;header-title— элемент с заголовком графика;header-legend— контейнер для легенды;legend-value— элемент легенды, представляющий отдельную категорию;main-container— основной контейнер для отображения данных графика;chart-item— контейнер для каждого элемента данных, группирует данные и метку по оси X;barchart-item— внутренний контейнер для одного бара, задает его общую высоту и ширину;barchart-content— видимая часть бара, ее высота зависит от значения данных;barchart-values— общий контейнер для графика;barchart-x-axis— контейнер для меток на оси X, отображает текстовые метки месяца и года;barchart-x-axis .labels— элемент для текстовых меток оси X, отвечает за центрирование и отображение текста.
Форматирование значений. В самом начале HTML-разметки, в теге style, задана стилизация двух классов: .barchart-values и .barchart-x-axis. Изменяя знаменатель в расчете {{ceil (division (count data) 20)}}, можно управлять частотой рендеринга отображаемых значений меток и подписей по оси X.
Формат отображения значений меток меняется через хелпер:
text
{{formatCurrency metric_value locale="ru" pattern='#,##0.'}}
А формат подписей по оси X — через хелперы:
text
{{formatDate 'MMM' start_of_month_dt 'ru'}}
{{formatDate 'YYYY' start_of_month_dt 'ru'}}
Интерактивность. К некоторым классам применен псевдокласс :hover — это значит, что при наведении курсора на такой элемент к нему применяются стили, описанные после :hover в CSS-блоке шаблона.
После знакомства с особенностями подготовки датасета и разбора самого шаблона дальнейший путь до готового визуала занимает буквально пару шагов. Переходим в сам график в GuideBook, сохраняем его копию под своим названием, а дальше подменяем датасет в этой копии на свой — продовый.
Если названия мер и измерений в вашем источнике совпадают с теми, что заложены в шаблоне (metric_value, start_of_month_dt и так далее), за пару кликов вы получаете готовый визуал уже на реальных, продовых данных — без необходимости писать Handlebars-код и разбирать верстку с нуля. Именно в этом и заключается ценность GuideBook: инструкция не просто объясняет, как устроен график, а дает рабочую заготовку, которую достаточно скопировать и подключить к своему источнику.
Помимо реализации на Handlebars GuideBook постепенно начал обогащаться и другими практическими темами. Появилось описание того, как применять CSS для стилизации коробочных графиков Proteus, то есть тех визуалов, что идут в инструменте из коробки, без Handlebars. Добавилось практическое применение Jinja — то, о чем мы говорили ранее в контексте оптимизации запросов. И отдельным блоком легло описание того, как подключить ролевую модель в свой отчет.
Так GuideBook перестал быть просто справочником по кастомным Handlebars-визуалам и превратился в более широкую базу знаний. Туда можно было прийти за ответом почти на любой практический вопрос, который возникал в процессе миграции отчета на Proteus.
Оценить в цифрах, сколько именно отчетов удалось перевести благодаря GuideBook, сложно: слишком много факторов помимо самого инструмента влияло на скорость миграции каждого конкретного отчета. Но однозначно можно сказать, что GuideBook позволил сократить время входа в новый продукт в несколько раз. Он закрывал главный пробел — незнание языка разметки, — и, вместо того чтобы разбираться с HTML и Handlebars с нуля, аналитик получал готовые шаблоны, которые можно было сразу применять на проде.
Краснодарский хаб — площадка для экспериментов
Мне кажется, Краснодару повезло больше всех ИТ-хабов компании, потому что все MVP-решения внедрялись сначала у южных HR-партнеров. Коллеги активно вовлекались в тестирование, предоставление обратной связи для доработок и развития моих решений.
Очень удобно работать в одном офисе с HR: собирать требования от ключевых заказчиков можно за стаканчиком утреннего кофе или по пути на обед.
Формат был максимально живым: во время обеда или кофе-брейка можно было прямо на месте обсудить, чего не хватает в отчете или что работает не так, как ожидалось. Дальше такие требования фиксировались уже в Jira, и эти задачи учитывались при квартальном планировании — неформальный разговор за кофе имел вполне формальное продолжение в виде задачи в трекере.
Отдельных регулярных встреч с заказчиками при этом не было — синки происходили скорее по необходимости, когда накапливался фидбек или требовалось обсудить конкретную доработку. Здесь и раскрывается настоящее преимущество совместной локации: не нужно было дожидаться специально назначенного созвона, чтобы задать вопрос или показать прототип, — достаточно было дойти до соседнего ряда, где сидел HR-партнер.
Основным продуктом, на котором HR-партнеры проходили тестирование, стал переезд основного HR-отчета на Proteus. В тестировании участвовали три партнера из южных локаций: Краснодара, Ростова и Сочи. Коллеги активно заносили фидбек, когда им не хватало текстовых подсказок с описанием метрик, конкретных атрибутов для детализации метрик или их фильтрации.
Стоит отметить, что требования региональных площадок и Москвы различались — и разница была связана не столько с самим отчетом, сколько с ролями HR-партнеров на местах и в столице. Главное критическое различие ролей — фокус внимания самого HR-менеджера. Московские HR-менеджеры работают с одним продуктом напрямую, независимо от города, а региональный HR — с одной локацией, но с разными продуктами. И то, что критично для регионального партнера, может быть менее приоритетным для московского, и наоборот. Набор изучаемых метрик у HR-менеджеров может меняться, и нам необходимо было учитывать все это для расчета.
Отдельно расскажу про фидбек из Краснодара, который подтолкнул нас к разработке нового продукта. Я как-то заметила, что мой HR-партнер каждый месяц занимается тем, что заходит в наш отчет, делает скрины и переносит их на страничку в вики. Я сразу решила поинтересоваться, что она делает и для чего. Оказалось, что в таком формате коллеги ежемесячно собирали статистику по своей локации.
Она поделилась обратной связью, как это неудобно — заходить в отчет, делать скрины и потом куда-то их добавлять. На тот момент у нас уже был инструмент, с помощью которого мы ежемесячно отправляли руководителям основную статистику по их командам. И я предложила решение: а почему бы не сделать такое же, но в разрезе региональных популяций?
Так родился HR-дайджест для команды. Сначала мы запустили его на лидов HR-команды, чтобы коллеги накидали своих комментариев и помогли обкатать решение до того, как оно уйдет дальше. После доработок по их фидбеку решение раскатали на всю HR-команду ТЦР во всех наших локациях.
Про краснодарскую локацию можно говорить много. Мы одна из южных локаций компании, и мы интересны тем, что сам город развивается не по модели технологического города, где драйвером становятся государственные программы и технопарки, как, например, Казань или Новосибирск, а растет благодаря частным компаниям, аутсорсу, веб-студиям и так далее.
Инженеры концентрируются не вокруг одного технопарка, крупной корпорации, университета, а вокруг предпринимателей и сообщества, которые выбирают город для жизни, переезжают сюда, строят бизнес.
Краснодарские компании конкурируют не столько между собой, сколько с работодателями всей страны. Айтишники могут жить в Краснодаре, а работать чаще всего на Москву, Питер, зарубежную компанию. Поэтому работодатели вынуждены бороться с помощью не только зарплат, но и корпоративной культуры, интересных задач и гибкости — чем наш ИТ-хаб и отличается от местных компаний.
Мы открылись 12 апреля 2021 года, к нам сразу перевели 5 человек из виртуального центра разработки. Небольшой компашкой мы отметили открытие с пиццей и шариками. Всех новичков встречаем в нашем ИТ-хабе welcome-мерчем, экскурсией и вводной встречей про компанию, культуру и развитие. Раз в полгода мы проводим отдельно для них мероприятие, где они знакомятся между собой, а также могут задать свои вопросы старожилам офиса.

У нас есть традиции, которых мы придерживаемся:
Каждую пятницу играем в настолки.
Устраиваем шаурма-дэй через бота заказов.
Устраиваем кинопятницы.
Проводим турниры по настольному теннису и FIFA.
На Новый год всегда организуем Тайного Санту.
А на новогодних корпоративах каждый год произносим тост Доминика Торетто из «Форсажа» :)
Мы поддерживаем основные вузы города, где есть релевантные направления и студенты: КубГУ, КубГТУ. Сейчас у нас 5 стажеров из этих вузов. Всегда с радостью принимаем участие в днях карьеры и ярмарках вакансий.
Заключение
Миграция с Tableau на Proteus заняла у нашей команды около 8 месяцев. За это время мы:
Перенесли 70 отчетов, из которых в 80% используются шаблоны из GuideBook.
Сократили время загрузки некоторых дашбордов с 20 до 10 секунд благодаря Jinja-параметризации и оптимизации ClickHouse.
Обучили команду HTML/CSS — теперь каждый аналитик может создать кастомный визуал без помощи разработчиков. Отчасти мы сами стали разработчиками.
Запустили HR-дайджест для региональных команд — продукт, который родился из фидбека краснодарского хаба.
Главный урок, который мы вынесли: миграция BI-инструмента — это не только технический переезд, но и изменение роли аналитика. Раньше мы приходили к готовому инструменту и рисовали графики в рамках его возможностей. Теперь мы сами проектируем, как данные будут восприниматься — от SQL-запроса до CSS-стилей. Это требует больше усилий, но дает больше контроля над результатом.
Советы из нашего опыта:
1. Не начинать с визуала — сначала важно оптимизировать источники данных под новую базу. В нашем случае это дало двукратный прирост производительности.
2. Собирать шаблоны по ходу. Мы не ждали, пока у каждого накопятся свои наработки. GuideBook сэкономил нам десятки часов на старте.
3. Не бояться кастомных решений и погружаться в них. Разработка действительно может занять больше времени, зато сэкономит время пользователя дашборда и позволит сфокусироваться на том, что действительно важно.
4. Вовлекать бизнес как можно раньше: фидбек от HR-партнеров помог нам создать продукт, который они действительно используют, а не просто «еще один дашборд».
Делитесь своим опытом между BI-платформами в комментариях. Сталкивались ли с необходимостью писать код внутри визуализации? Мы прошли этот путь в 2024 году без ИИ, и будем благодарны, если поделитесь опытом, как в нынешних реалиях проходят подобные переезды 🙃
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.