
Substituir ou Manter: Como o DataLens Migrou do Highcharts
Substituir ou Manter: Como o DataLens Migrou do Highcharts

Olá, meu nome é Evgeny Alaev, sou desenvolvedor de interfaces na equipe do Yandex DataLens. É uma ferramenta de BI em nuvem para análise de dados e criação de dashboards, e os gráficos nela não são “apenas mais um recurso”, mas o coração do produto. O usuário abre um dashboard e a primeira coisa que vê são as visualizações. São elas que respondem à pergunta: “O que está acontecendo com meus dados?”
O DataLens opera em duas instalações — para o próprio Yandex e para usuários externos. No total, foram criados mais de 18,3 milhões de gráficos até hoje. Cada um desses gráficos é resultado do trabalho daquela mesma biblioteca de visualização sobre a qual falaremos.
Por muito tempo, os gráficos no DataLens foram construídos com o Highcharts. No início, era uma escolha razoável: início rápido, rico conjunto de tipos, grande comunidade. Mas uma ferramenta de BI vai se tornando mais complexa com o tempo — surgem requisitos não convencionais de comportamento, um sistema de design que precisa ser mantido em estilo unificado. E em certo momento o Highcharts começou a atrapalhar mais do que ajudar.
Neste artigo vou contar como e por que tomamos a decisão de escrever nossa própria biblioteca open source para visualização — @gravity‑ui/charts. Eu e meu colega somos core contributors desta biblioteca, então vou detalhar o que não nos agradava no Highcharts, quais alternativas consideramos, como é a arquitetura e quais desafios técnicos concretos enfrentamos durante o processo.
Da conveniência às limitações
Licença e vendor lock
O Highcharts é uma biblioteca comercial. Para uso não comercial é gratuita, mas assim que o produto é monetizado, é necessária uma licença. Isso por si só não é um problema — muitas equipes trabalham com ferramentas pagas. O problema começa quando o modelo de uso se torna mais complexo.
O DataLens tem três formatos de distribuição: nuvem, open source e on‑premises. Uma licença não livre nas dependências — mesmo que opcional — complica a entrega de cada um deles.
Além da licença, há uma dependência rígida do roadmap alheio. Quer um novo tipo de gráfico? Espere o fornecedor fazê-lo. Encontrou um bug no comportamento do tooltip? Abra um ticket e torça para que entre nas prioridades. Precisa de um comportamento personalizado ao clicar na legenda? Procure um workaround na documentação ou no Stack Overflow. Quando um produto está em desenvolvimento ativo e os requisitos de visualização são não triviais, essa dependência começa a travar.
Há ainda o capítulo à parte das atualizações de versão principal. Estávamos usando a última versão do Highcharts dentro da oitava versão principal. A migração para a próxima versão principal já seria cara por si mesma, mas uma particularidade do DataLens a tornava especialmente difícil: temos o Editor — um modo em que os usuários escrevem código JavaScript para configurar o gráfico. Na prática, eles trabalham diretamente com o Highcharts, formando a configuração via código. Esse código de usuário não pode ser migrado automaticamente: não é possível escrever um codemod que converta com segurança JavaScript arbitrário de uma interface para outra.
Controle sobre o comportamento
Em uma ferramenta de BI, os usuários interagem ativamente com os gráficos: clicam na legenda para ocultar uma série, pairam sobre os pontos de dados, fixam tooltips. Cada um desses cenários no Highcharts exigia uma configuração fina via callbacks e eventos — nem sempre documentados e nem sempre estáveis entre versões.
Alguns exemplos característicos:
-
Fixação do tooltip. O tooltip padrão do Highcharts desaparece quando o mouse sai. O suporte nativo ao tooltip “fixo” não existia na oitava versão principal — ele só apareceu na versão 12. Implementamos isso por meio da substituição de métodos internos: interceptávamos eventos do mouse e substituíamos a lógica de ocultação. O código funcionava, mas era frágil.
-
Renderização personalizada do tooltip. O Formatter retorna uma string HTML, o que significa montagem manual de markup por concatenação. Sem React, sem componentes. No final, a renderização do tooltip se transformou em uma camada separada com sua própria lógica de template.
-
Clique na legenda. O comportamento padrão alterna a visibilidade da série. Substituí-lo completamente sem quebrar o restante não é trivial: o handler precisava ser associado via eventos internos e cancelar cuidadosamente o comportamento padrão.
-
Tematização. Cores, fontes e espaçamentos no Highcharts são definidos via configuração JavaScript e aplicados como inline‑styles. Por cima disso, mantínhamos um arquivo SCSS separado para substituir estilos base, que precisava ser sincronizado manualmente a cada atualização de paleta no Gravity UI.
Cada um desses contornos não é um problema isolado, mas uma contribuição para a dívida técnica geral. Um hack vive sozinho, dois ainda se controlam, mas no décimo eles começam a interagir: corrija um — quebre outro. A complexidade de manutenção cresce de forma não linear, e cada atualização do Highcharts se torna uma auditoria: o que dos nossos contornos quebrou silenciosamente desta vez.
Avaliação das alternativas
Antes de escrever a nossa própria, passamos honestamente pelas soluções existentes. Os critérios principais eram:
-
Licença aberta — MIT ou compatível, sem tiers pagos e acordos OEM.
-
Personalização total de renderização e estilos — possibilidade de controlar a aparência de qualquer elemento sem contornos.
-
Incorporação de HTML — tooltip, rótulos no gráfico, rótulos de eixos devem renderizar como markup HTML completo, não como texto SVG ou strings de template.
Analisamos ECharts, Recharts, Plotly, Chart.js e D3.
Chart.js e Plotly caíram rapidamente: a renderização em Canvas por padrão fecha as possibilidades de personalização de estilos via CSS, e a incorporação de HTML arbitrário em elementos do gráfico não está prevista.
Recharts — React‑first, SVG, MIT. Adequado para dashboards simples, mas a personalização profunda de formas e comportamento rapidamente esbarra nas limitações da API interna.
ECharts era a opção mais próxima: rico conjunto de tipos, configuração flexível, suporte a renderizadores personalizados. Mas examinado de perto, não cobria um dos critérios principais: incorporação total de HTML em partes arbitrárias do gráfico. Para nossos casos de uso, isso era fundamental. Além disso — o vendor lock não desaparecia: a licença MIT resolvia a questão jurídica, mas a dependência do roadmap alheio e do ciclo de atualizações permanecia.
D3 — não é uma biblioteca de gráficos, mas um conjunto de primitivos: escalas, geradores de formas, transformações de dados. Por si só não resolve a tarefa, mas fornece uma base ideal para construir sua própria solução sobre ela.
A conclusão foi lógica: tomar o D3 como base e construir sobre ele uma biblioteca própria — isso nos dava liberdade, controle e eliminava completamente a dependência de qualquer fornecedor.
Por que D3 e não outra coisa
D3 não desenha gráficos. É um conjunto de ferramentas para trabalhar com dados e DOM: escalas, geradores de formas geométricas, transformações. É exatamente isso que precisamos — primitivos com os quais se pode montar qualquer visualização, sem se limitar ao que o autor da biblioteca previu.
D3 fornece scaleLinear, scaleBand, scaleUtc para eixos, d3.line (), d3.arc (), d3.area () para formas, d3.extent () e d3.group () para transformações de dados. Todo o resto — é nosso.
Arquitetura do @gravity‑ui/charts
Ponto de entrada — objeto de configuração
O usuário passa ao componente um único objeto com dados e configurações: séries, eixos, título, legenda, tooltip. Esta é uma escolha consciente em favor da abordagem declarativa — a maior parte da configuração é facilmente serializável, registrável em log e transmissível entre sistemas.
Onde a descrição declarativa é insuficiente, a configuração aceita funções: um renderizador personalizado de tooltip, handlers de clique, formatadores de rótulos de eixos. Isso não é uma contradição, mas uma fronteira intencional — a estrutura de dados permanece previsível, e os pontos de extensão são explícitos.
Fluxo de dados
Dentro da biblioteca, a configuração passa por várias etapas de processamento:
-
Normalização. Os dados de entrada são convertidos para um formato interno unificado. Nesta etapa, os padrões são definidos, as ambiguidades são resolvidas, os dados de cada série são tipados.
-
Preparação das séries. Cada tipo de gráfico (linha, bar, pie etc.) é processado pelo seu módulo. As cores são atribuídas, os metadados para a legenda são formados.

- Construção de escalas e eixos. Com base nos dados, o D3 constrói as escalas: lineares, logarítmicas, temporais, categóricas. As escalas determinam como os valores dos dados são convertidos em coordenadas de pixel.

- Renderização de formas. Cada tipo de série desenha seus elementos SVG: linhas, retângulos, arcos, pontos. O D3 fornece os geradores de formas, o React gerencia o ciclo de vida dos elementos.
SVG + HTML: renderização híbrida
A renderização principal é SVG. Isso garante bordas nítidas, escalonamento sem perda de qualidade e controle total sobre o posicionamento.
Mas o SVG não sabe fazer quebra de texto, não suporta markup HTML rico dentro dos elementos. Para data labels com quebras de linha, tooltips personalizados e elementos interativos sobre o gráfico, é usada uma camada HTML separada — um div com posicionamento absoluto que se sobrepõe ao SVG e é sincronizado com suas coordenadas.
Sistema de eventos
As interações do usuário com o gráfico — hover, click, movimento do mouse — são processadas por um barramento de eventos centralizado baseado em d3.dispatch. Isso permite que diferentes partes da interface (tooltip, crosshair, legenda) reajam ao mesmo evento de forma independente e coordenada, sem dependências diretas entre os componentes.
Mas essa abordagem tem ainda outra consequência importante — performance. O movimento do mouse sobre o gráfico gera eventos com alta frequência. Se a cada evento desse fosse disparado um render do React, isso levaria a recálculos custosos de toda a árvore de componentes. Em vez disso, parte das atualizações — por exemplo, hover das formas, busca do ponto mais próximo ao ponteiro — é aplicada diretamente via D3, contornando o React. O componente não é redesenhado: o D3 simplesmente atualiza os atributos DOM necessários. O React entra em ação apenas onde é realmente necessário — ao mudar a estrutura ou o estado que afeta todo o grafo.
Migração sem parar o produto
O caminho dos dados da fonte ao gráfico
Antes de falar sobre a troca de biblioteca, é importante entender como funciona a renderização de gráficos no DataLens — porque é exatamente essa arquitetura que determinou como poderíamos migrar.
Quando um usuário abre um chart (ou esse chart é renderizado em um dashboard), uma requisição de dados é enviada ao nosso backend Node.js. O backend Node.js, por sua vez, consulta nosso serviço Python para obter dados brutos. Mas os dados por si só ainda não são um gráfico. No Node ocorre a preparação: determina-se o tipo de visualização, verifica-se se os limites de dados para esse tipo foram excedidos e, se tudo estiver em ordem, seleciona-se a função de preparação de dados específica para o tipo de gráfico concreto.
Ponto-chave: cada tipo de visualização tem sua própria função de preparação de dados. Para um gráfico de linhas — uma, para um gráfico de barras — outra, para area — uma terceira. O resultado dessa função é a configuração que vai para o cliente. Lá ela não vai diretamente para o @gravity‑ui/charts, mas para o @gravity‑ui/chartkit — um pacote separado que usamos para trabalhar com várias bibliotecas de visualização simultaneamente. Ele carrega dinamicamente apenas a biblioteca necessária para o tipo específico de chart e fornece uma interface unificada para todas elas — o código cliente não sabe e não precisa saber qual biblioteca exatamente renderiza cada tipo.
Além do roteamento, o chartkit contém wrappers sobre as próprias bibliotecas: lógica que não cabe nos limites de uma biblioteca específica, mas é necessária no DataLens. Por exemplo, exibir um tooltip ao toque em dispositivos móveis — esse comportamento é igualmente necessário tanto para o Highcharts quanto para o @gravity‑ui/charts, por isso está implementado no nível do chartkit.
Essas coisas são potencialmente úteis não apenas no DataLens. Então um relato detalhado sobre o @gravity‑ui/chartkit merece um artigo separado.
Estratégia de transição gradual
Essa arquitetura nos deu a possibilidade de migrar de forma gradual, sem reescrever tudo de uma vez.
Introduzimos feature flags no nível do tipo de visualização. A lógica é simples: se o flag para uma visualização específica está definido como true — o Node prepara os dados no formato @gravity‑ui/charts e o cliente usa a nova biblioteca. Se o flag não está definido — os dados são preparados no formato Highcharts e o código antigo é renderizado.
Isso significava que em algum momento no DataLens as duas bibliotecas funcionavam simultaneamente — cada uma para seu conjunto de tipos de gráficos. Essa abordagem trouxe várias vantagens:
-
Risco isolado. Um bug na nova implementação de gráfico de barras horizontais não afeta os gráficos de barras verticais que ainda estão no Highcharts
-
Verificação gradual. Cada tipo após a transição passa por um período de observação em produção antes que o próximo migre
-
Possibilidade de rollback. Se algo deu errado, basta desligar o flag
A migração ocorreu em três ondas, e a ordem foi escolhida conscientemente — do simples ao complexo.
Onda 1: pie e treemap. O início mais lógico — visualizações sem eixos. Sem escalas, sem crosshair, sem sistema de coordenadas complexo. Isso permitiu trabalhar a integração básica, configurar o pipeline de preparação de dados e garantir que a infraestrutura de transição funciona corretamente, sem arriscar os tipos mais utilizados.

Onda 2: bar‑y, bar‑y normalized, scatter. Próximo passo — gráficos com eixos, mas com uma restrição importante: esses tipos não são usados no modo split (quando vários gráficos são dispostos um abaixo do outro com eixo X compartilhado) e não participam de gráficos combinados. Isso reduzia drasticamente a quantidade de casos extremos e tornava a transição previsível.

Onda 3: area, area normalized, bar‑x, bar‑x normalized, line e suas combinações. A etapa mais complexa. São exatamente esses tipos os mais populares no DataLens: são usados na maioria dos dashboards.

Além disso, aqui aparecem gráficos combinados (por exemplo, line bar‑x em um único gráfico), modo split e todos os cenários de interação não triviais. Cada uma dessas visualizações exigia atenção especial durante os testes, e os feature flags davam a possibilidade de reverter caso algo desse errado em produção.
Soluções técnicas
Objeto de configuração: familiar, mas melhor. Escolhemos conscientemente o objeto de configuração como forma de descrever o gráfico. O DataLens já trabalhava com essa abordagem, e uma mudança brusca de paradigma criaria uma barreira desnecessária. A estrutura da configuração em muito se assemelha ao que os desenvolvedores estão acostumados: séries, eixos, título, tooltip — tudo no lugar certo. Mas onde a interface do Highcharts nos parecia inadequada, tomamos nossas próprias decisões. Não por originalidade, mas porque víamos problemas concretos ou inconveniências no uso real.
Um exemplo mínimo — gráfico de linhas com eixo temporal:
import {Chart} from '@gravity-ui/charts';
<Chart
data={{
series: {
data: [
{
type: 'line',
name: 'Receita',
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: 'mil. R$'}}],
}}
/>
O resultado é assim:

Mas é possível criar um gráfico de linhas ainda mais elaborado:

Padrões razoáveis e personalização onde necessário. Ao longo dos anos de desenvolvimento de uma ferramenta de BI, formamos uma compreensão de como uma biblioteca de gráficos deve se comportar em tal produto. Os cenários típicos devem funcionar por padrão — sem configuração e sem surpresas. Onde os padrões são insuficientes, há pontos de personalização explícitos: passar seu próprio componente React como renderizador de tooltip, substituir o formatador de rótulos de eixo, definir a largura das linhas. Tudo isso — pelo mesmo objeto de configuração, sem mexer nos internos da biblioteca.
Integração nativa com o Gravity UI. O DataLens é construído sobre o Gravity UI — um sistema de design com componentes, ícones e tematização CSS. A tematização funciona via variáveis CSS do @gravity‑ui/uikit: conectou o tema — e todas as cores de eixos, grades, rótulos se ajustam automaticamente para o modo claro ou escuro. O tooltip e os botões na interface do gráfico são componentes padrão do uikit, que herdam acessibilidade, navegação por teclado e tratamento de eventos do sistema de design.

Escala sem caos. Quando há muitos tipos de gráficos, a arquitetura começa a determinar o resultado. Adicionar um novo tipo não deve exigir alterações no núcleo ou quebrar os que já funcionam. Resolvemos isso por meio de uma estrutura unificada para cada tipo: sua própria preparação de dados, seu próprio componente de renderização, seus próprios tipos — tudo em um módulo isolado. O núcleo da biblioteca conhece a existência dos tipos apenas por meio de um contrato comum, não por implementações concretas.
Testes visuais. Visualização é antes de tudo o que o usuário vê, portanto testes unitários não são suficientes aqui. Cada tipo de gráfico é coberto por testes de screenshot no Playwright, que são executados no Docker para reprodutibilidade. Uma mudança em um módulo não pode quebrar silenciosamente a renderização de outro: isso fica imediatamente visível pelo snapshot com falha.
Resultados e conclusões
A transição levou tempo e exigiu investimentos sérios. Mas o resultado não é simplesmente “trocamos uma biblioteca por outra”. Veja o que obtivemos:
-
Controle total sobre o código. Quando algo não funciona como esperado — podemos entrar em qualquer lugar, entender a causa e corrigir. Sem caixas pretas, sem workarounds sobre código alheio, sem esperar por um fix do fornecedor.
-
Open source. A biblioteca está disponível a todos — não apenas à equipe do DataLens. Isso significa contribuições externas, issues públicas, histórico de mudanças transparente. Um problema reportado por um usuário externo ajuda a melhorar o produto para todos.
-
Linguagem visual unificada. Os gráficos se tornaram parte orgânica da interface do DataLens, e não um elemento inserido de um fornecedor terceiro. Tematização, tipografia, componentes interativos — tudo de um único sistema de design.
Escrever sua própria biblioteca de gráficos não é uma decisão a ser tomada levianamente. É um grande volume de trabalho, a necessidade de manter, documentar e desenvolver mais um produto.
Mas se sua ferramenta é construída em torno da visualização de dados, se você tem requisitos não convencionais de comportamento, se quer controle total sobre a aparência e integração profunda com o sistema de design — o vendor lock em uma biblioteca alheia em algum momento se tornará um teto. Chegamos a esse teto e decidimos removê-lo.
O que vem a seguir
A biblioteca está em desenvolvimento ativo, e temos várias direções em que estamos trabalhando agora.
Núcleo framework‑agnostic. Hoje, @gravity‑ui/charts é uma biblioteca React. Planejamos extrair o núcleo — a lógica de preparação de dados, construção de escalas, cálculo de coordenadas — em um pacote separado sem dependência de nenhum framework. Isso abrirá dois caminhos: uso em JavaScript puro sem React, e a possibilidade de escrever wrappers finos para Vue, Angular ou outros frameworks, sem duplicar a lógica principal.
Range slider para eixos categóricos. O componente range slider atualmente funciona com eixos numéricos e temporais — ele permite selecionar um intervalo e “dar zoom” no trecho de dados desejado. Mas os eixos categóricos (por exemplo, lista de países ou nomes) são igualmente importantes para a análise de BI. Estamos aprimorando o slider para que ele saiba trabalhar também com categorias: selecionar um subconjunto de valores e passá-lo para a filtragem de dados.
Documentação para contribuidores. Atualmente, adicionar um novo tipo de gráfico é um processo compreensível para quem conhece a arquitetura da biblioteca por dentro. Mas para um contribuidor externo, isso não é tão óbvio. Queremos criar guias detalhados: o que é um módulo de tipo, qual contrato ele deve cumprir, como escrever testes, como conectar uma nova visualização ao sistema geral. O objetivo é que uma pessoa que nunca trabalhou com a base de código consiga adicionar um novo tipo de gráfico de forma independente seguindo a documentação.
A biblioteca @gravity‑ui/charts está disponível sob a licença MIT — você pode experimentá-la no seu projeto.
-
Documentação → gravity‑ui.github.io/charts
-
Storybook → preview.gravity‑ui.com/charts
-
GitHub → github.com/gravity‑ui/charts
Ficaremos felizes com uma estrela no GitHub ⭐

Evgeny Alaev
Usuário