
Remplacer ou Conserver: Comment DataLens a Migré depuis Highcharts
Remplacer ou Conserver: Comment DataLens a Migré depuis Highcharts

Bonjour, je m’appelle Evgueni Alayev, je suis développeur d’interfaces dans l'équipe Yandex DataLens. C’est un outil BI cloud pour l’analyse de données et la création de tableaux de bord, et les graphiques ne sont pas « l’une des fonctionnalités » — ils sont le cœur du produit. L’utilisateur ouvre un tableau de bord et la première chose qu’il voit, ce sont les visualisations. Ce sont elles qui répondent à la question: « Que se passe-t-il avec mes données? ”
DataLens fonctionne dans deux installations — pour Yandex lui-même et pour les utilisateurs externes. Au total, plus de 18,3 millions de graphiques ont été créés à ce jour. Chacun de ces graphiques est le résultat du travail de la bibliothèque de visualisation dont il sera question ici.
Pendant longtemps, les graphiques de DataLens ont été construits avec Highcharts. Au début, c'était un choix raisonnable: démarrage rapide, riche ensemble de types, grande communauté. Mais un outil BI devient plus complexe avec le temps — des exigences non standard apparaissent quant au comportement, au système de design qu’il faut maintenir dans un style cohérent. Et à un moment donné, Highcharts a commencé à gêner plus qu'à aider.
Dans cet article, je vais expliquer comment et pourquoi nous avons décidé d'écrire notre propre bibliothèque open source de visualisation — @gravity‑ui/charts. Mon collègue et moi sommes les principaux contributeurs de cette bibliothèque, donc je vais expliquer en détail ce qui ne nous convenait pas dans Highcharts, quelles alternatives nous avons envisagées, comment l’architecture est organisée et quels défis techniques concrets nous avons rencontrés en cours de route.
Des avantages aux limites
Licence et vendor lock
Highcharts est une bibliothèque commerciale. Pour un usage non commercial, elle est gratuite, mais dès que le produit est monétisé, une licence est nécessaire. Ce n’est pas un problème en soi — de nombreuses équipes travaillent avec des outils payants. Le problème commence là où le modèle d’utilisation devient plus complexe.
DataLens existe en trois formats de distribution: cloud, open source et on-premises. Une licence non libre dans les dépendances — même optionnelle — complique pour nous la livraison de chacun d’eux.
Outre la licence, il y a une dépendance forte à la roadmap d’un tiers. Vous voulez un nouveau type de graphique? Attendez que le vendeur le réalise. Vous avez trouvé un bug dans le comportement d’une infobulle? Ouvrez un ticket et espérez qu’il soit priorisé. Vous avez besoin d’un comportement personnalisé au clic sur la légende? Cherchez un contournement dans la documentation ou sur Stack Overflow. Quand un produit évolue activement et que les exigences de visualisation sont complexes, cette dépendance commence à freiner.
Autre point délicat — les mises à jour majeures. Nous utilisions la dernière version de Highcharts dans le cadre de la version majeure 8. La migration vers la version majeure suivante semblait coûteuse en soi, mais elle était rendue particulièrement difficile par une spécificité de DataLens: nous disposons d’un mode Éditeur dans lequel les utilisateurs écrivent du code JavaScript pour configurer les graphiques. Concrètement, ils travaillent directement avec Highcharts en formant la configuration via du code. Ce type de code utilisateur ne peut pas être migré automatiquement: il est impossible d'écrire un codemod qui traduise de manière sûre du JS arbitraire d’une interface vers une autre.
Contrôle du comportement
Dans un outil BI, les utilisateurs interagissent activement avec les graphiques: ils cliquent sur la légende pour masquer une série, survolent les points de données, fixent les infobulles. Chacun de ces scénarios dans Highcharts nécessitait une configuration fine via des callbacks et des événements — souvent non documentés et pas toujours stables d’une version à l’autre.
Voici quelques exemples caractéristiques:
-
Infobulle fixée. L’infobulle standard de Highcharts disparaît quand la souris s'éloigne. La prise en charge native d’une infobulle « collante » n’existait pas dans la version majeure 8 — elle n’est apparue qu’en version 12. Nous l’avons implémentée en redéfinissant les méthodes internes: nous interceptons les événements souris et remplaçons la logique de masquage. Le code fonctionnait, mais était fragile.
-
Rendu personnalisé de l’infobulle. Le formatter retourne une chaîne HTML, ce qui implique une construction manuelle du balisage par concaténation. Pas de React, pas de composants. Au final, le rendu de l’infobulle est devenu une couche distincte avec sa propre logique de templating.
-
Clic sur la légende. Le comportement standard bascule la visibilité d’une série. Le redéfinir complètement sans casser le reste n’est pas trivial: il fallait attacher le gestionnaire via des événements internes et annuler soigneusement le comportement par défaut.
-
Thématisation. Les couleurs, polices et espacements dans Highcharts sont définis via une configuration JavaScript et appliqués comme styles inline. Par-dessus cela, nous maintenions un fichier SCSS séparé pour remplacer les styles de base, qu’il fallait synchroniser manuellement à chaque mise à jour de la palette dans Gravity UI.
Chaque contournement n’est pas un problème isolé, mais une contribution à la dette technique globale. Un hack vit seul, deux peuvent être gardés en tête, mais au dixième ils commencent à interagir: on corrige une chose et une autre se casse. La complexité de maintenance croît de façon non linéaire, et chaque mise à jour de Highcharts devient un audit: qu’est-ce qui, parmi nos contournements, a silencieusement cessé de fonctionner cette fois-ci.
Évaluation des alternatives
Avant d'écrire notre propre solution, nous avons honnêtement passé en revue les solutions existantes. Les critères principaux étaient:
-
Licence ouverte — MIT ou compatible, pas de niveaux payants ni d’accords OEM.
-
Personnalisation complète du rendu et des styles — possibilité de contrôler l’apparence de chaque élément sans contournements.
-
Intégration HTML — les infobulles, les étiquettes sur le graphique et les étiquettes des axes doivent être rendues comme du balisage HTML complet, et non comme du texte SVG ou des chaînes de templates.
Nous avons examiné ECharts, Recharts, Plotly, Chart.js et D3.
Chart.js et Plotly ont été rapidement éliminés: le rendu Canvas par défaut ferme les possibilités de personnalisation des styles via CSS, et l’intégration de HTML arbitraire dans les éléments du graphique n’y est pas prévue.
Recharts — React-first, SVG, MIT. Convient bien aux tableaux de bord simples, mais la personnalisation profonde des formes et du comportement se heurte rapidement aux limites de l’API interne.
ECharts était l’option la plus proche: riche ensemble de types, configuration flexible, support des renderers personnalisés. Mais en y regardant de plus près, il ne couvrait pas l’un des critères clés: l’intégration HTML complète dans des parties arbitraires du graphique. Pour nos cas d’usage, c'était fondamental. De plus, le vendor lock ne disparaissait pas: la licence MIT réglait la question juridique, mais la dépendance à la roadmap et au cycle de mise à jour d’un tiers demeurait.
D3 n’est pas une bibliothèque de graphiques, mais un ensemble de primitives: échelles, générateurs de formes, transformations de données. Ne résout pas le problème à lui seul, mais fournit une base idéale pour construire sa propre solution par-dessus.
La conclusion s’est imposée d’elle-même: prendre D3 comme base et construire par-dessus notre propre bibliothèque — cela donnait à la fois liberté et contrôle, et supprimait totalement la dépendance envers tout fournisseur.
Pourquoi D3 et pas autre chose
D3 ne dessine pas de graphiques. C’est un ensemble d’outils pour travailler avec les données et le DOM: échelles, générateurs de formes géométriques, transformations. C’est exactement ce dont nous avions besoin — des primitives à partir desquelles on peut assembler n’importe quelle visualisation, sans être limité par ce que l’auteur de la bibliothèque a prévu.
D3 fournit scaleLinear, scaleBand, scaleUtc pour les axes, d3.line (), d3.arc (), d3.area () pour les formes, d3.extent () et d3.group () pour les transformations de données. Tout le reste, c’est nous.
Architecture de @gravity‑ui/charts
Point d’entrée — l’objet de configuration
L’utilisateur passe au composant un seul objet avec les données et les paramètres: séries, axes, titre, légende, infobulle. C’est un choix délibéré en faveur d’une approche déclarative — la majeure partie de la configuration est facilement sérialisable, journalisable et transmissible entre systèmes.
Là où la description déclarative est insuffisante, la configuration accepte des fonctions: un renderer d’infobulle personnalisé, des gestionnaires de clics, des formateurs d'étiquettes d’axes. Ce n’est pas une contradiction, mais une frontière intentionnelle — la structure des données reste prévisible, et les points d’extension sont explicites.
Flux de données
À l’intérieur de la bibliothèque, la configuration passe par plusieurs étapes de traitement:
-
Normalisation. Les données d’entrée sont ramenées à un format interne uniforme. À cette étape, les valeurs par défaut sont appliquées, les ambiguïtés résolues, et les données de chaque série sont typées.
-
Préparation des séries. Chaque type de graphique (ligne, bar, pie, etc.) est traité par son propre module. Les couleurs sont attribuées, les métadonnées pour la légende sont formées.

- Construction des échelles et des axes. Sur la base des données, D3 construit les échelles: linéaires, logarithmiques, temporelles, catégorielles. Les échelles définissent comment les valeurs des données sont traduites en coordonnées en pixels.

- Rendu des formes. Chaque type de série dessine ses éléments SVG: lignes, rectangles, arcs, points. D3 fournit les générateurs de formes, React gère le cycle de vie des éléments.
SVG + HTML: rendu hybride
Le rendu principal est en SVG. Cela donne des contours nets, une mise à l'échelle sans perte de qualité et un contrôle total sur le positionnement.
Mais SVG ne sait pas faire de retour à la ligne pour le texte et ne prend pas en charge un balisage HTML riche dans les éléments. Pour les data labels avec retours à la ligne, les infobulles personnalisées et les éléments interactifs superposés au graphique, on utilise une couche HTML séparée — une div positionnée en absolu qui se superpose au SVG et se synchronise avec ses coordonnées.
Système d'événements
Les interactions de l’utilisateur avec le graphique — survol, clic, mouvement de la souris — sont gérées via un bus d'événements centralisé basé sur d3.dispatch. Cela permet aux différentes parties de l’interface (infobulle, crosshair, légende) de réagir à un même événement de manière indépendante et cohérente, sans dépendances directes entre les composants.
Mais cette approche a une autre conséquence importante — les performances. Le mouvement de la souris sur le graphique génère des événements à haute fréquence. Si chaque événement déclenchait un re-rendu React, cela entraînerait des recalculs coûteux de tout l’arbre de composants. À la place, une partie des mises à jour — par exemple, le survol des formes, la recherche du point le plus proche du pointeur — est appliquée directement via D3, en contournant React. Le composant n’est pas re-rendu: D3 met simplement à jour les attributs DOM nécessaires. React n’intervient que là où c’est réellement nécessaire — lors d’un changement de structure ou d'état qui affecte l’ensemble du graphe.
Migration sans arrêter le produit
Le chemin des données de la source au graphique
Avant de parler du changement de bibliothèque, il est important de comprendre comment le rendu des graphiques est organisé dans DataLens — car c’est précisément cette architecture qui a déterminé comment nous pouvions migrer.
Quand un utilisateur ouvre un graphique (ou que ce graphique est rendu sur un tableau de bord), une requête de données est envoyée à notre backend Node.js. Le backend Node.js, à son tour, interroge notre service Python pour obtenir les données brutes. Mais les données seules ne font pas encore un graphique. Sur Node, une préparation a lieu: le type de visualisation est déterminé, on vérifie que les limites de données pour ce type ne sont pas dépassées, et si tout va bien — une fonction de préparation des données spécifique au type de graphique particulier est sélectionnée.
Point clé: chaque type de visualisation a sa propre fonction de préparation des données. Pour un graphique linéaire — une fonction, pour un graphique en barres — une autre, pour une area — une troisième. Le résultat de cette fonction est la configuration qui part vers le client. Là, elle ne passe pas directement dans @gravity‑ui/charts, mais dans @gravity‑ui/chartkit — un package séparé que nous utilisons pour travailler simultanément avec plusieurs bibliothèques de visualisation. Il charge dynamiquement uniquement la bibliothèque nécessaire pour le type de graphique particulier et fournit une interface unifiée pour toutes — le code client ne sait pas et ne se préoccupe pas de quelle bibliothèque précisément rend tel type.
En plus du routage, chartkit contient des surcouches sur les bibliothèques elles-mêmes: une logique qui ne rentre pas dans le cadre d’une bibliothèque spécifique mais qui est nécessaire dans DataLens. Par exemple, l’affichage d’une infobulle au tap sur mobile est un comportement également nécessaire pour Highcharts et pour @gravity‑ui/charts, c’est pourquoi il est implémenté au niveau de chartkit.
Ce genre de fonctionnalités est potentiellement utile au-delà de DataLens. Aussi, un article détaillé sur @gravity‑ui/chartkit mériterait son propre espace.
Stratégie de transition progressive
Cette architecture nous a permis de migrer par étapes, sans tout réécrire d’un coup.
Nous avons introduit des feature flags au niveau du type de visualisation. La logique est simple: si le flag pour une visualisation spécifique est à true — Node prépare les données au format @gravity‑ui/charts et le client utilise la nouvelle bibliothèque. Si le flag n’est pas activé — les données sont préparées au format Highcharts et l’ancien code est rendu.
Cela signifiait qu'à un certain moment, les deux bibliothèques fonctionnaient simultanément dans DataLens — chacune pour son ensemble de types de graphiques. Cette approche a offert plusieurs avantages:
-
Risque isolé. Un bug dans la nouvelle implémentation du graphique linéaire n’affecte pas les graphiques en barres qui sont encore sur Highcharts.
-
Vérification progressive. Chaque type, après la transition, passe une période d’observation en production avant que le suivant ne migre.
-
Possibilité de retour arrière. Si quelque chose ne va pas, il suffit de désactiver le flag.
La migration s’est déroulée en trois vagues, et l’ordre a été choisi délibérément — du plus simple au plus complexe.
Vague 1: pie et treemap. Le démarrage le plus logique — des visualisations sans axes. Pas d'échelles, pas de crosshair, pas de système de coordonnées complexe. Cela a permis de tester l’intégration de base, de configurer le pipeline de préparation des données et de s’assurer que l’infrastructure de transition fonctionnait correctement, sans risquer les types les plus chargés.

Vague 2: bar‑y, bar‑y normalized, scatter. L'étape suivante — des graphiques avec axes, mais avec une contrainte importante: ces types ne sont pas utilisés en mode split (lorsque plusieurs graphiques s’alignent l’un sous l’autre avec un axe X commun) et ne participent pas aux graphiques combinés. Cela réduisait considérablement le nombre de cas limites et rendait la transition prévisible.

Vague 3: area, area normalized, bar‑x, bar‑x normalized, line et leurs combinaisons. L'étape la plus complexe. Ce sont précisément ces types qui sont les plus populaires dans DataLens: ils sont utilisés sur la majorité des tableaux de bord.

De plus, c’est là qu’apparaissent les graphiques combinés (par exemple, line + bar‑x sur un même graphique), le mode split et tous les scénarios d’interaction non triviaux. Chacune de ces visualisations nécessitait une attention particulière lors des tests, et les feature flags permettaient de revenir en arrière si quelque chose ne se passait pas bien en production.
Solutions techniques
Objet de configuration: familier, mais meilleur. Nous avons délibérément choisi l’objet de configuration comme moyen de décrire un graphique. DataLens fonctionnait déjà avec cette approche, et un changement brusque de paradigme aurait créé une friction inutile. La structure de la configuration recoupe largement ce à quoi les développeurs sont habitués: séries, axes, titre, infobulle — tout à sa place. Mais là où l’interface de Highcharts nous semblait malheureuse, nous avons pris nos propres décisions. Non par souci d’originalité, mais parce que nous constations des problèmes ou des inconvénients concrets dans l’utilisation réelle.
Exemple minimal — graphique linéaire avec axe temporel:
import {Chart} from '@gravity-ui/charts';
<Chart
data={{
series: {
data: [
{
type: 'line',
name: 'Chiffre d\'affaires',
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: 'k €'}}],
}}
/>
Ce que ça donne:

Mais on peut aussi créer un graphique linéaire plus élaboré:

Valeurs par défaut raisonnables et personnalisation là où c’est nécessaire. Au fil des années de développement d’un outil BI, nous avons forgé une compréhension claire de la façon dont une bibliothèque de graphiques doit se comporter dans un tel produit. Les scénarios typiques doivent fonctionner sans configuration et sans surprises. Là où les valeurs par défaut sont insuffisantes, il existe des points de personnalisation explicites: passer son propre composant React comme renderer d’infobulle, redéfinir le formateur d'étiquette d’axe, définir la largeur des lignes. Tout cela — via le même objet de configuration, sans avoir à toucher aux entrailles de la bibliothèque.
Intégration native avec Gravity UI. DataLens est construit sur Gravity UI — un système de design avec des composants, des icônes et une thématisation CSS. La thématisation fonctionne via les variables CSS de @gravity‑ui/uikit: connectez un thème et toutes les couleurs des axes, des grilles et des étiquettes s’adaptent automatiquement au mode clair ou sombre. L’infobulle et les boutons dans l’interface du graphique sont des composants uikit standard, qui héritent de l’accessibilité, de la navigation au clavier et de la gestion des événements du système de design.

Mise à l'échelle sans chaos. Quand il y a beaucoup de types de graphiques, l’architecture commence à dicter l’issue. L’ajout d’un nouveau type ne doit pas nécessiter de modifications dans le noyau ni casser ce qui fonctionne déjà. Nous avons résolu cela via une structure uniforme pour chaque type: sa propre préparation des données, son propre composant de rendu, ses propres types — tout dans un module isolé. Le noyau de la bibliothèque ne connaît l’existence des types qu'à travers un contrat commun, pas à travers des implémentations concrètes.
Tests visuels. La visualisation, c’est avant tout ce que voit l’utilisateur, donc les tests unitaires ne suffisent pas ici. Chaque type de graphique est couvert par des tests de captures d'écran sur Playwright, qui s’exécutent dans Docker pour la reproductibilité. Un changement dans un module ne peut pas silencieusement casser le rendu d’un autre: c’est immédiatement visible par le snapshot échoué.
Bilan et conclusions
La transition a pris du temps et a nécessité un investissement sérieux. Mais le résultat, ce n’est pas simplement « remplacer une bibliothèque par une autre “. Voici ce que nous avons obtenu:
-
Contrôle total sur le code. Quand quelque chose ne fonctionne pas — nous pouvons aller n’importe où, comprendre la cause et corriger. Pas de boîtes noires, pas de contournements sur du code tiers, pas d’attente d’un correctif du fournisseur.
-
Open source. La bibliothèque est accessible à tous — pas seulement à l'équipe DataLens. Cela signifie des contributions externes, des issues publiques, un historique des changements transparent. Un problème signalé par un utilisateur externe aide à améliorer le produit pour tous.
-
Langage visuel unifié. Les graphiques sont devenus une partie organique de l’interface de DataLens, et non un élément rapporté d’un fournisseur tiers. Thématisation, typographie, composants interactifs — tout issu d’un même système de design.
Écrire sa propre bibliothèque de graphiques n’est pas une décision à prendre à la légère. C’est un volume de travail considérable, la nécessité de maintenir, documenter et faire évoluer encore un produit.
Mais si votre outil est centré sur la visualisation des données, si vous avez des exigences non standard en matière de comportement, si vous voulez un contrôle total sur l’apparence et une intégration profonde avec votre système de design — le vendor lock sur une bibliothèque tierce deviendra à un moment donné un plafond. Nous avons heurté ce plafond et avons décidé de le supprimer.
Et la suite?
La bibliothèque est en développement actif, et nous avons plusieurs axes sur lesquels nous travaillons en ce moment.
Noyau framework-agnostic. Aujourd’hui, @gravity‑ui/charts est une bibliothèque React. Nous prévoyons d’extraire le noyau — la logique de préparation des données, de construction des échelles, de calcul des coordonnées — dans un package séparé sans dépendance envers un quelconque framework. Cela ouvre deux possibilités: une utilisation en JavaScript pur sans React, et la possibilité d'écrire de fines surcouches pour Vue, Angular ou d’autres frameworks, sans dupliquer la logique principale.
Range slider pour les axes catégoriels. Le composant range slider fonctionne actuellement avec les axes numériques et temporels — il permet de sélectionner une plage et de « zoomer » sur la partie souhaitée des données. Mais les axes catégoriels (par exemple, une liste de pays ou de noms) sont tout aussi importants pour l’analytique BI. Nous améliorons le slider pour qu’il puisse également travailler avec des catégories: sélectionner un sous-ensemble de valeurs et le passer dans le filtrage des données.
Documentation pour les contributeurs. Actuellement, ajouter un nouveau type de graphique est un processus clair pour ceux qui connaissent l’architecture de la bibliothèque de l’intérieur. Mais pour un contributeur externe, ce n’est pas aussi évident. Nous souhaitons créer des guides détaillés: qu’est-ce qu’un module de type, quel contrat doit-il remplir, comment écrire des tests, comment connecter une nouvelle visualisation au système global. L’objectif — qu’une personne n’ayant jamais travaillé avec la base de code puisse, de manière autonome, ajouter un nouveau type de graphique en suivant la documentation.
La bibliothèque @gravity‑ui/charts est publiée sous licence MIT — vous pouvez l’essayer dans votre projet.
-
Documentation → gravity‑ui.github.io/charts
-
Storybook → preview.gravity‑ui.com/charts
-
GitHub → github.com/gravity‑ui/charts
Une étoile sur GitHub nous ferait très plaisir ⭐

Evgueni Alayev
Utilisateur