
Привет, меня зовут Евгений ��лаев, я разработчик интерфейсов в команде Yandex DataLens. Это облачный BI‑инструмент для анализа данных и построения дашбордов, и графики в нём — не «одна из фич», а сердце продукта. Пользователь открывает дашборд и первое, что видит, — визуализации. Именно они отвечают на вопрос: «Что происходит с моими данными?»
DataLens работает в двух инсталляциях — для самого Яндекса и для внешних пользователей. Суммарно на сегодня создано больше 18,3 млн графиков. Каждый из этих графиков — результат работы той самой библиотеки визуализации, о которой пойдёт речь.
Долгое время графики в DataLens строились на Highcharts. Поначалу это был разумный выбор: быстрый старт, богатый набор типов, большое сообщество. Но BI‑инструмент со временем становится сложнее — появляются нестандартные требования к поведению, дизайн‑система, которую нужно выдерживать в едином стиле. И в какой‑то момент Highcharts начал мешать больше, чем помогать.
В этой статье расскажу, как и почему мы приняли решение написать собственную опенсорс‑библиотеку для визуализации — @gravity‑ui/charts. Мы с коллегой — core‑контрибьютеры этой библиотеки, так что я в подробностях расскажу, что нас не устраивало в Highcharts, какие альтернативы рассматривали, как устроена архитектура и с какими конкретными техническими вызовами столкнулись в процессе.
От удобства к ограничениям
Лицензия и vendor lock
Highcharts — коммерческая библиотека. Для некоммерческого использования она бесплатна, но как только продукт монетизируется, нужна лицензия. Это само по себе не проблема — многие команды работают с платными инструментами. Проблема начинается там, где модель использования становится сложнее.
У DataLens три формата распространения: облако, опенсорс и on‑premises. Несвободная лицензия в зависимостях — даже опциональная — усложняет для нас поставку каждого из них.
Помимо лицензии — жёсткая зависимость от чужого roadmap. Хочешь новый тип графика? Жди, пока его сделает вендор. Нашёл баг в поведении тултипа? Открывай тикет и надейся, что он попадёт в приоритет. Нужно нестандартное поведение при клике на легенду? Ищи workaround в документации или на Stack Overflow. Когда продукт активно развивается и требования к визуализации нетривиальные, такая зависимость начинает тормозить.
Отдельная история — мажорные обновления. Мы использовали последнюю версию Highcharts в рамках восьмого мажора. Переход на следующий мажор выглядел дорого сам по себе, но особенно сложным его делала одна особенность DataLens: у нас есть Editor — режим, в котором пользователи пишут JavaScript‑код для настройки графика. Фактически они работают с Highcharts напрямую, формируя конфигурацию через код. Такой пользовательский код невозможно мигрировать автоматически: нельзя написать кодмод, который безопасно переведёт произвольный JS с одним интерфейсом на другой.
Контроль над поведением
В BI‑инструменте пользователи активно взаимодействуют с графиками: кликают на легенду, чтобы скрыть серию, зависают на точках данных, фиксируют тултипы. Каждый из этих сценариев в Highcharts требовал тонкой настройки через колбэки и события — причём не всегда задокументированные и не всегда стабильные между версиями.
Вот несколько характерных примеров:
Фиксация тултипа. Стандартный тултип Highcharts исчезает при уходе мыши. Нативной поддержки «прилипающего» тултипа в 8-м мажоре не было — она появилась только в 12-й версии. Мы реализовали это через переопределение внутренних методов: перехватывали события мыши, подменяли логику скрытия. Код работал, но был хрупким.
Кастомный рендер тултипа. Formatter возвращает HTML‑строку, что означ��ет ручную сборку разметки конкатенацией. Никакого React, никаких компонентов. В итоге рендер тултипа превратился в отдельный слой с собственной логикой шаблонизации.
Клик по легенде. Стандартное поведение переключает видимость серии. Переопределить его полностью, не сломав остальное, нетривиально: обработчик приходилось вешать через внутренние события и аккуратно отменять дефолтное поведение.
Темизация. Цвета, шрифты и отступы в Highcharts задаются через JavaScript‑конфигурацию и применяются как inline‑стили. Поверх этого мы держали отдельный SCSS‑файл для переопределения базовых стилей, который нужно было синхронизировать вручную при каждом обновлении палитры в Gravity UI.
Каждый такой обходной путь — это не изолированная проблема, а вклад в общий техдолг. Один хак живёт сам по себе, два можно удержать в голове, но к десятому они начинают взаимодействовать: правишь одно — ломается другое. Сложность поддержки растёт нелинейно, а каждое обновление Highcharts превращается в аудит: что из наших обходных путей на этот раз тихо сломалось.
Оценка альтернатив
Прежде чем писать своё, мы честно прошлись по существующим решениям. Основными критериями были:
Открытая лицензия — MIT или совместимая, никаких платных тиров и OEM‑соглашений.
Полная кастомизация рендера и стилей — возможность контролировать внешний вид любого элемента без обходных путей.
Встраивание HTML — тултип, подписи на графике, подписи осей должны рендериться как полноценная HTML‑разметка, а не как SVG‑текст или строки‑шаблоны.
Смотрели на ECharts, Recharts, Plotly, Chart.js и D3.
Chart.js и Plotly отпали быстро: Canvas‑рендер по умолчанию закрывает возможности кастомизации стилей через CSS, а встраивание произвольного HTML в элементы графика там не предусмотрено.
Recharts — React‑first, SVG, MIT. Хорошо подходит для простых дашбордов, но глубокая кастомизация форм и поведения быстро упирается в ограничения внутреннего API.
ECharts был наиболее близким вариантом: богатый набор типов, гибкая конфигурация, поддержка кастомных рендереров. Но при ближайшем рассмотрении он не закрывал один из ключевых критериев: полноценное встраивание HTML в произвольные части графика. Для наших кейсов это было принципиально. Плюс к этому — vendor lock никуда не исчезал: MIT‑лицензия снимала юридический вопрос, но зависимость от чужого roadmap и цикла обновлений оставалась.
D3 — не библиотека графиков, а набор примитивов: шкалы, генераторы форм, трансформации данных. Сам по себе не решает задачу, но даёт идеальный фундамент для построения своего решения поверх него.
Вывод оказался логичным: взять D3 как основу и построить поверх него собственную библиотеку — это давало и свободу, и контроль, и снимало зависимость от любого вендора целиком.
Почему D3, а не что‑то другое
D3 не рисует графики. Это набор инструментов для работы с данными и DOM: шкалы, генераторы геометрических форм, трансформации. Именно это нам и нужно — примитивы, из которых можно собрать любую визуализацию, не ограничиваясь тем, что предусмотрел автор библиотеки.
D3 даёт scaleLinear, scaleBand, scaleUtc для осей, d3.line(), d3.arc(), d3.area() для фигур, d3.extent() и d3.group() для трансформаций данных. Всё остальное — наше.
Архитектура @gravity‑ui/charts
Входная точка — объект‑конфиг
Пользователь передаёт в компонент один объект с данными и настройками: серии, оси, заголовок, легенду, тултип. Это осознанный выбор в пользу декларативного подхода — большая часть конфига легко сериализуется, логируется и передаётся между системами.
Там, где декларативного описания недостаточно, конфиг принимает функции: кастомный рендерер тултипа, обработчики кликов, форматтеры меток осей. Это не противоречие, а намеренная граница — структура данных остаётся предсказуемой, а точки расширения явными.
Поток данных
Внутри библиотеки конфиг проходит несколько стадий обработки:
Нормализация. Входные данные приводятся к единому внутреннему формату. На этом этапе проставляются дефолты, разрешаются неоднозначности, данные каждой серии типизируются.
Подготовка серий. Каждый тип графика (линия, bar, pie и так далее) обрабатывается своим модулем. Назначаются цвета, формируются метаданные для легенды.

Построение шкал и осей. На основе данных D3 строит шкалы: линейные, логарифмические, временны́е, категорийные. Шкалы определяют, как значения из данных переводятся в пиксельные координаты.

Рендер форм. Каждый тип серии рисует свои SVG‑элементы: линии, прямоугольники, дуги, точки. D3 предоставляет генераторы форм, React управляет жизненным циклом элементов.
SVG HTML: гибридный рендер
Основной рендер — SVG. Это даёт чёткие границы, масштабирование без потери качества и полный контроль над позиционированием.
Но SVG не умеет переносить текст, не поддерживает богатую HTML‑разметку внутри элементов. Для data labels с переносами, кастомных тултипов и интерактивных элементов поверх графика используется отдельный HTML‑слой — абсолютно спозиционированный div, который накладывается на SVG и синхронизируется с его координатами.
Система событий
Взаимодействия пользователя с графиком — ховер, click, движение мыши — обрабатываются через централизованную шину событий на основе d3.dispatch. Это позволяет разным частям интерфейса (тултип, crosshair, легенда) реагировать на одно событие независимо и согласованно, без прямых зависимостей между компонентами.
Но у этого подхода есть ещё одно важное следствие — производительность. Движение мыши по графику генерирует события с высокой частотой. Если на каждое такое событие запускать React‑рендер, это приведёт к дорогим пересчётам всего дерева компонентов. Вместо этого часть обновлений — например, ховер форм, поиск ближайшей точки к поинтеру — применяется напрямую через D3, минуя React. Компонент не перерисовывается: D3 просто обновляет нужные DOM‑атрибуты. React включается только там, где это действительно необходимо — при изменении структуры или состояния, которое влияет на весь граф.
Миграция без остановки продукта
Путь данных от источника до графика
Прежде чем говорить о смене библиотеки, важно понять, как вообще устроен рендер графиков в DataLens — потому что именно эта архитектура определила, как мы могли мигрировать.
Когда пользователь открывает чарт (или этот чарт рендерится на дашборде), на наш Node.js‑бэкенд уходит запрос за данными. Node.js‑бэкенд, в свою очередь, ходит в наш Python‑сервис за сырыми данными. Но данные сами по себе — это ещё не график. На Node происходит подготовка: определяется тип визуализации, проверяется, не превышены ли лимиты данных для этого типа, и если всё в порядке — выбирается функция подготовки данных, специфичная для конкретного типа графика.
Ключевой момент: у каждого типа визуализации — своя функция подготовки данных. Для линейного графика — одна, для столбчатого — другая, для area — третья. Результат этой функции и есть конфиг, который летит на клиент. Там он попадает не напрямую в @gravity‑ui/charts, а в @gravity‑ui/chartkit — отдельный пакет, который мы используем для работы с несколькими библиотеками визуализации одновременно. Он динамически подгружает только ту библиотеку, которая нужна для конкретного типа чарта, и предоставляет для всех них единый интерфейс — клиентский код не знает и не думает о том, какая именно библиотека рендерит конкретный тип.
Помимо маршрутизации, chartkit содержит обвязки поверх самих библиотек: логику, которая не укладывается в рамки конкретной библиотеки, но нужна в DataLens. Например, показ тултипа по тапу на мобильном устройстве — это поведение одинаково нужно и для Highcharts, и для @gravity‑ui/charts, поэтому оно реализовано на уровне chartkit.
Такие вещи потенциально полезны не только в DataLens. Так что подробный рассказ про @gravity‑ui/chartkit заслуживает отдельной статьи.
Стратегия постепенного перехода
Эта архитектура дала нам возможность мигрировать поэтапно, не переписывая всё сразу.
Мы ввели фича‑флаги на уровне типа визуализации. Логика простая: если флаг для конкретной визуализации установлен в true — Node готовит данные в формате @gravity‑ui/charts и клиент использует новую библиотеку. Если флаг не установлен — данные готовятся в формате Highcharts и рендерится старый код.
Это означало, что в какой‑то момент в DataLens одновременно работали обе библиотеки — каждая для своего набора типов графиков. Такой подход дал несколько преимуществ:
Изолированный риск. Баг в новой реализации линейчатой диаграммы не затрагивает столбчатые диаграммы, которые ещё на Highcharts
Постепенная проверка. Каждый тип после перехода проходит период наблюдения в продакшне, прежде чем мигрирует следующий
Возможность отката. Если что‑то пошло не так, достаточно выключить флаг
Миграция шла в три волны, и порядок был выбран осознанно — от простого к сложному.
Волна 1: pie и treemap. Самый логичный старт — визуализации без осей. Нет шкал, нет crosshair, нет сложной системы координат. Это позволило отработать базовую интеграцию, настроить пайплайн подготовки данных и убедиться, что инфраструктура перехода работает корректно, не рискуя самыми нагруженными типами.

Волна 2: bar‑y, bar‑y normalized, scatter. Следующий шаг — графики с осями, но с важным ограничением: эти типы не используются в режиме split (когда несколько графиков выстраиваются один под другим с общей осью X) и не участвуют в комбинированных диаграммах. Это резко сужало количество крайних случаев и делало переход предсказуемым.

Волна 3: area, area normalized, bar‑x, bar‑x normalized, line и их комбинации. Самый сложный этап. Именно эти типы — самые популярные в DataLens: их используют на большинстве дашбордов.

К тому же здесь появляются комбинированные диаграммы (например, line bar‑x на одном графике), split‑режим и все нетривиальные сценарии взаимодействия. Каждая из этих визуализаций требовала особого внимания при тестировании, а фича‑флаги давали возможность откатиться, если что‑то шло не так в продакшене.
Технические решения
Объект‑конфиг: знакомо, но лучше. Мы сознательно выбрали объект‑конфиг как способ описания графика. DataLens уже работал с таким подходом, и резкая смена парадигмы создала бы лишний барьер. Структура конфига во многом перекликается с тем, к чему привыкли разработчики: серии, оси, заголовок, тултип — всё на своих местах. Но там, где интерфейс Highcharts казался нам неудачным, мы приняли собственные решения. Не ради оригинальности, а потому что видели конкретные проблемы или неудобства в реальном использовании.
Минимальный пример — линейный график с временной осью:
import {Chart} from '@gravity-ui/charts'; <Chart data={{ series: { data: [ { type: 'line', name: 'Выручка', data: [ {x: new Date('2024-01-01').getTime(), y: 120}, {x: new Date('2024-02-01').getTime(), y: 145}, {x: new Date('2024-03-01').getTime(), y: 132}, {x: new Date('2024-04-01').getTime(), y: 178}, ], }, ], }, xAxis: {type: 'datetime'}, yAxis: [{title: {text: 'тыс. руб.'}}], }} />
Получается так:

Но можно сделать линейный график и посложнее:

Разумные дефолты и кастомизация там, где нужно. За годы разработки BI‑инструмента мы сформировали понимание того, как должна вести себя библиотека графиков в таком продукте. Типичные сценарии должны работать из коробки — без настройки и без сюрпризов. Там, где дефолтов недостаточно, есть явные точки кастомизации: передать свой React‑компонент как рендерер тултипа, переопределить форматтер метки оси, задать ширину линий. Всё это — через тот же объект‑конфиг, без залезания во внутренности библиотеки.
Нативная интеграция с Gravity UI. DataLens построен на Gravity UI — дизайн‑системе с компонентами, иконками и CSS‑темизацией. Темизация работает через CSS‑переменные @gravity‑ui/uikit: подключил тему — и все цвета осей, сеток, меток автоматически подстраиваются под светлый или тёмный режим. Тултип и кнопки в интерфейсе графика — стандартные компонен��ы uikit, которые наследуют доступность, клавиатурную навигацию и обработку событий из дизайн‑системы.

Масштаб без хаоса. Когда типов графиков много, архитектура начинает решать исход. Добавление нового типа не должно требовать правок в ядре или ломать уже работающие. Мы решили это через единую структуру для каждого типа: своя подготовка данных, свой компонент рендера, свои типы — всё в изолированном модуле. Ядро библиотеки знает о существовании типов только через общий контракт, не через конкретные реализации.
Визуальное тестирование. Визуализация — это прежде всего то, что видит пользователь, поэтому unit‑тестов здесь недостаточно. Каждый тип графика покрыт скриншотными тестами на Playwright, которые запускаются в Docker для воспроизводимости. Изменение в одном модуле не может тихо сломать рендер другого: это сразу видно по упавшему снапшоту.
Итоги и выводы
Переход занял время и потребовал серьёзных инвестиций. Но результат — это не просто «заменили одну библиотеку на другую».Вот, что мы получили:
Полный контроль над кодом. Когда что‑то работает не так — мы можем зайти в любое место, понять причину и починить. Нет чёрных ящиков, нет workaround поверх чужого кода, нет ожидания фикса от вендора.
Опенсорс. Биб��иотека доступна всем — не только команде DataLens. Это значит внешние contributions, публичные issues, прозрачная история изменений. Проблема, о которой сообщит внешний пользователь, помогает улучшить продукт для всех.
Единый визуальный язык. Графики стали органичной частью интерфейса DataLens, а не вставным элементом от стороннего вендора. Темизация, типографика, интерактивные компоненты — всё из одной дизайн‑системы.
Писать свою библиотеку графиков — это не то решение, которое стоит принимать легко. Это большой объём работы, необходимость поддерживать, документировать и развивать ещё один продукт.
Но если ваш инструмент строится вокруг визуализации данных, если у вас нестандартные требования к поведению, если вы хотите полного контроля над внешним видом и глубокой интеграции с дизайн‑системой — vendor lock на чужой библиотеке в какой‑то момент станет потолком. Мы упёрлись в него и решили его убрать.
Что дальше
Библиотека активно развивается, и у нас есть несколько направлений, над которыми мы работаем прямо сейчас.
Framework‑agnostic core. Сегодня @gravity‑ui/charts — это React‑библиотека. Мы планируем выделить ядро — логику подготовки данных, построения шкал, расчёта координат — в отдельный пакет без зависимости от какого‑либо фреймворка. Это откроет два пути: использование на чистом JavaScript без React, и возможность написать тонкие обвязки для Vue, Angular или других фреймворков, не дублируя при этом основную логику.
Range slider для категорийных осей. Компонент range slider сейчас работает с числовыми и временны́ми осями — он позволяет выбрать диапазон и «приблизить» нужный участок данных. Но категорийные оси (например, список стран или имён) не менее важны для BI‑аналитики. Мы дорабатываем слайдер так, чтобы он умел работать и с категориями: выбирать подмножество значений и передавать его в фильтрацию данных.
Документация для контрибьюторов. Сейчас добавить новый тип графики — понятный процесс для тех, кто знает архитектуру библиотеки изнутри. Но для внешнего контрибьютора это не так очевидно. Мы хотим создать подробные гайды: что такое модуль типа, какой контракт он должен выполнять, как написать тесты, как подключить новую визуализацию к общей системе. Цель — чтобы человек, никогда не работавший с кодовой базой, мог самостоятельно добавить новый тип графика по документации.
Библиотека @gravity‑ui/charts открыта под MIT‑лицензией — можете попробовать поработать с ней в вашем проекте.
Документация → gravity‑ui.github.io/charts
Storybook → preview.gravity‑ui.com/charts
GitHub → github.com/gravity‑ui/charts
Будем рады звёздочке на GitHub ⭐
