Многие аналитические платформы сталкивается с одним и тем же вопросом: как связать компоненты, которые изначально не знают друг о друге? График живёт по одним правилам, карта — по вторым, таблица — по третьим. Каждый движок говорит на собственном языке событий и данных, и заставить их работать слаженно — нетривиальная архитектурная задача. В этой статье рассказываем, как мы её решаем в Modus.

Рисунок 1. Пример отчёта с несколькими типами визуализаций
Рисунок 1. Пример отчёта с несколькими типами визуализаций

Что такое аналитический отчет?

Рассмотрим типичную задачу, где нужно проанализировать продажи мебели по регионам России. Открываем BI-систему и собираем отчёт как конструктор:

  • Выбираем набор данных — аналитики уже подготовили и загрузили его в систему.

  • Добавляем визуализации — например, столбчатую диаграмму и TreeMap, чтобы видеть динамику продаж по датам и структуру ассортимента.

  • Загружаем карту — она покажет объём продаж по регионам.

  • Добавляем таблицу — для структурного сопоставления данных.

  • Настраиваем фильтры — по датам, категориям товаров, менеджерам.

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

Например, выбираем столбец на одной диаграмме — таблица и карта мгновенно перестраиваются под выбранную дату. Кликаем по региону на карте — график показывает только его данные. Меняем фильтр — обновляются все компоненты сразу. Все взаимосвязано, все влияет друг на друга. 

Все вместе - это и есть аналитический отчет.

Для пользователя — это удобный способ визуализировать большие объемы данных и находить закономерности.

Но внутри платформы — это сложная система компонентов на разных движках. Один возвращает категорию товара, другой — геометрию, третий — состояние. Зачастую они работают по совершенно разным принципам: диаграмма оперирует осями и сериями, карта — слоями и GeoJSON, таблица — строками и столбцами. 

Главный вопрос: как связать всё это так, чтобы компоненты оставались изолированными и не взаимодействовали напрямую? Например, чтобы заменить библиотеку графиков, не трогая карту, или добавить новый тип визуализации, не переписывая существующие. Именно эту задачу мы решали в рамках нашего проекта в Modus BI.

Почему не работают очевидные подходы

Разберем почему связать разнородные визуализации и фильтры «по-простому» не получится, а также точки отказа, которые встречаются на этом пути. 

Связать компоненты напрямую

Самый очевидный путь — сделать так, чтобы компоненты реагировали на события друг друга: график обновляет карту, карта передаёт состояние таблице, таблица влияет на график. 

При таком подходе каждый компонент знает о существовании остальных. В этом случае добавление новой визуализации требует изменений в существующих компонентах, а удаление одного из них ломает зависимости в других. Это нарушает все основные архитектурные принципы и подходы к разработке, и не только в аналитических системах.

Выбрать одну библиотеку для всего

Второй вариант элегантнее: берем одну библиотеку, получаем единый API и забываем о зоопарке зависимостей. Но классы задач слишком разные. Движок диаграмм заточен под оси и легенды, картографический — под тайлы и слои, табличный — под колонки и sticky-области. Универсальной библиотеки, которая одинаково хорошо решает все эти задачи, не существует. На практике специализированные сценарии приходится достраивать вокруг ограничений универсального инструмента. 

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

Написать все на D3

Третий путь — взять низкоуровневый инструмент вроде D3 и «нарисовать» все самостоятельно. Это максимальный контроль над каждым элементом визуализации, гибкость и тонкость настроек всех частей системы, а также унификация.

Но «можно построить» не означает «выгодно поддерживать». За привычными функциями готовых движков стоят годы доработок: сочетания настроек, поддержка браузеров, управление с помощью клавиатуры и сенсорного экрана, пограничные случаи, о которых узнаешь только в продакшене. Все это придется воспроизводить самостоятельно, а затем сопровождать.

Свобода D3 оправдана, когда продукт имеет особую уникальность или сложность. Для типовых задач готовый движок обычно дешевле. Поэтому мы пошли другим путем: унифицировали не движок  отрисовки, а связанные с ним контракты.                                                                      

Архитектурный силуэт: четыре контракта

На верхнем уровне каждую визуализацию окружают четыре зоны ответственности:

  • Размещение. Отчёт знает тип компонента, положение, размер и настройки, но не знает внутреннюю реализацию и способ построения визуализации.

  • Данные. Платформа берёт на себя весь цикл: формирует запрос с учётом активных фильтров, получает ответ, нормализует и адаптирует его. Визуализация получает уже готовую конфигурацию, поэтому кэширование, обработка ошибок и трансформации живут в одном месте и не дублируются в каждом компоненте.

  • Взаимодействие. События библиотеки переводятся в предметные действия: выбрать категорию, перейти на уровень детализации, показать связанную сущность. 

  • Рендеринг. Адаптер готовит конфигурацию в формате выбранного движка и изолирует его жизненный цикл и особенности — инициализацию, обновление, реакцию на resize и освобождение ресурсов. Платформе не нужно знать про эти детали.

Эти контракты намеренно унифицированы. Отчёту достаточно знать, что нужно для совместного поведения компонентов, но не нужно описывать каждую разновидность легенды, слоя или табличной группировки. Общее остаётся общим, специфичное живёт рядом с визуализацией.

Рисунок 2. Архитектурный силуэт: отчёт, контекст, данные, адаптеры и специализированные движки
Рисунок 2. Архитектурный силуэт: отчёт, контекст, данные, адаптеры и специализированные движки

Это концептуальная иллюстрация, а не интерфейс нашего продукта. Главное на ней — граница: наружу возвращается не объект события конкретной библиотеки, а смысл действия. Поэтому клик по столбцу и клик по области карты могут одинаково означать выбор поля «Регион».

Адаптер работает в обе стороны. На входе он переводит нормализованный ответ и настройки в конфигурацию движка — серии, слои, колонки. На выходе превращает клики, наведение и переходы по уровням в события, понятные отчёту. Именно поэтому при замене движка не нужно переучивать всю платформу под новый API — достаточно переписать один адаптер.

Один клик — три независимых обновления

Вернёмся к отчёту о продажах. Пользователь выбирает дату отгрузки на столбчатой диаграмме — и дальше запускается следующая цепочка:

  1. Движок графика сообщает адаптеру о выбранном столбце.

  2. Адаптер преобразует это в семантическое событие: выбрано значение даты поля «Дата отгрузки».

  3. Контекст отчёта получает это событие и обновляет общий фильтр.

  4. Платформа определяет затронутые компоненты по смыслу поля, а не по устройству графика.

  5. График, карта и таблица независимо друг от друга запрашивают актуальные данные.

  6. Каждый адаптер самостоятельно готовит нужный формат для своего движка: серии, географические объекты или строки таблицы.

Рисунок 3. Последовательность общей фильтрации нескольких визуализаций
Рисунок 3. Последовательность общей фильтрации нескольких визуализаций

График не вызывает карту напрямую и ничего не передаёт таблице — компоненты общаются только через контекст отчёта. Это значит, что любой из них можно заменить или временно отключить, не трогая остальные.

Самый важный переход происходит между вторым и третьим шагом. Пока событие выражено как «нажата точка серии номер два», оно принадлежит библиотеке. После преобразования в «выбрана дата отгрузки» это уже часть пользовательского сценария, и контекст может применить его к любому компоненту, который понимает соответствующее бизнес-поле.

Отдельный момент — сопоставление данных. Регион в продажах, логистике и справочной географии может храниться по-разному: разные колонки, разные типы идентификаторов. Но для общего сценария фильтрации важна не физическая колонка, а бизнес-значение. Правила сопоставления остаются в модели данных, а renderer о них не знает.

Путь данных до renderer

На верхнем уровне цепочка выглядит так:

Поля и настройки

→ описание потребности в данных;

→ получение ответа;

→ нормализация;

→ адаптер библиотеки;

→ отрисовка.

Платформа отвечает за данные и контекст отчёта — она решает, какие данные актуальны с учётом фильтров и когда компоненту нужно обновиться. При этом ей не нужно знать, что конкретному движку требуются серии особой формы или служебные поля географического объекта. Адаптер берёт на себя визуальную механику: оси, слои карты, раскладку графа или поведение колонок.

Адаптер, в отличие от платформы, знает обе стороны границы. Именно здесь удобно сосредоточить подготовку конфигурации, обработку resize, создание и освобождение экземпляра библиотеки. Продуктовые правила при этом не должны опускаться в renderer — его задача сводится к тому, чтобы качественно отрисовать уже подготовленное представление.

Почему несколько готовых библиотек лучше одной

Каждую библиотеку в нашем стеке мы рассматриваем как специалиста в своём классе задач — не универсальный инструмент, а точечное решение под конкретную визуализацию.

Задача

Готовый движок

За что он отвечает

Классические графики

amCharts

Оси, легенды, масштабирование и интерактивность

Карты

Leaflet

Тайлы, GeoJSON, слои, маркеры и кластеризация

Таблицы

Reactabular

Sticky-области, resize и табличные взаимодействия

Графы связей

GoJS

Узлы, рёбра и автоматические раскладки

Временные планы

Gantt-библиотека

Шкала времени, задачи и режимы отображения

Уникальная геометрия

D3

Формы, шкалы, переходы и полный контроль

Но у такого подхода есть своя цена: у каждой библиотеки собственный жизненный цикл, своя модель событий и формат входных данных. Именно здесь адаптер выполняет свою главную функцию — не устраняет зависимость, а локализует её.

При этом библиотека выбирается не навсегда. Библиотеки развиваются, а часть задач могут перерасти инструмент. Поэтому стабильное окружение не делает миграцию дешевой, но позволяет рассматривать её в качестве замены механизма — элемента системы, а не как глобальную переделку, которая может затронуть соседние компоненты и принцип работы с данными.

Здесь важна ещё одна граница. Общий контракт между платформой и визуализациями не должен описывать каждую функцию движков — иначе мы просто создадим ещё одну универсальную библиотеку, только внутреннюю. Наверх выносится только то, что необходимо для совместной работы компонентов, а всё специфичное остаётся рядом с визуализацией.

Где остаётся место для D3

D3 — это не конкурент готовым движкам, а набор низкоуровневых инструментов. Он живёт на другом уровне абстракции: не предлагает готовых типов диаграмм, зато даёт полный контроль над нестандартной геометрией, шкалами, раскладками и переходами. Это делает его незаменимым именно для обхода ограничений готовых библиотек.

На практике мы смотрим на несколько признаков:

  • укладывается ли задача в предметную модель готового движка;

  • несёт ли специфичная реализация реальную ценность для пользователя;

  • сколько типовой механики — подсказки, масштабирование, интерактивность — придётся писать и потом поддерживать самостоятельно;

  • можно ли задействовать отдельные модули D3, например d3-scale или d3-hierarchy, для вычислений, не поднимая весь renderer с нуля.

Из этого вырастает простое правило: готовый движок — для типовой предметной механики, D3 — когда уникальность визуализации сама по себе является частью продукта, а не просто техническим требованием. При этом оба варианта подключаются к одним и тем же данным, фильтрам и семантическим событиям платформы — граница проходит только внутри адаптера.

Как визуализации развиваются независимо

Независимость здесь — это не про физическую изоляцию файлов, а про стабильность точек взаимодействия. Пока контракт держится, внутреннее устройство компонента не касается остальной системы. На практике добавление нового типа визуализации разбивается на пять шагов:

  • сформулировать потребность в данных;

  • подготовить преобразование ответа;

  • подключить renderer;

  • перевести его события в семантические действия, понятные платформе;

  • добавить специфичные пользовательские настройки.

Если для этого не нужно трогать существующие графики, карты и таблицы — граница работает. Новый тип подключается к общей фильтрации не потому, что соседние компоненты знают о нём или его API, а потому что он «говорит» с отчётом на том же языке бизнес-событий.

Схожая логика работает и при замене библиотеки. Это не бесплатно, но изменения в основном остаются внутри визуализации и её адаптера, а контекст отчёта продолжает работать как есть.

То же справедливо для эволюции существующих типов. Карта может обрасти новым слоем, таблица — получить другой режим группировки, график — расширить набор подсказок. Пока правки не затрагивают смысл данных и событий, они остаются локальными. Но если расширять общий контракт всё-таки придется, это оформляется как осознанное изменение платформы, а не как побочный эффект возможностей библиотеки. В Modus BI этот подход позволяет добавлять новые типы визуализаций или создавать собственные плагины, не меняя ядро системы.

Вместо вывода

Единый BI-отчёт не требует единого графического движка. Целостность для пользователя создаётся не на уровне технологий, а на уровне согласованных данных, фильтров и реакций на действия.

Из нашего опыта следуют пять принципов:

  • визуализации не должны вызывать друг друга напрямую;

  • бизнес-события устойчивее событий конкретной библиотеки;

  • адаптер локализует стоимость зависимости от визуализации;

  • одной библиотекой не стоит закрывать все задачи;

  • D3 — инструмент точечного применения, не фундамент платформы.

График, карта и таблица — это разные технологические миры, каждый со своей моделью данных, жизненным циклом и событиями. Но в рамках одного отчёта они живут по общим правилам: вместе — для пользователя, достаточно независимо — для разработчика и аналитика, которые будут развивать продукт дальше.

P. S. Присоединяйтесь к нашему BI‑сообществу в Telegram и будьте в курсе последних новостей Modus!