
置き換えるか残すか:DataLensがHighchartsから移行した方法
置き換えるか残すか:DataLensがHighchartsから移行した方法

こんにちは、私はEvgeny Alayevといいます。Yandex DataLensチームのインターフェース開発者です。DataLensはデータ分析とダッシュボード構築のためのクラウドBIツールであり、グラフは「機能のひとつ」ではなく、プロダクトの心臓部です。ユーザーがダッシュボードを開いて最初に目にするのはビジュアライゼーションです。それが「自分のデータに何が起きているか?」という問いに答えるものです。
DataLensはYandex社内向けと外部ユーザー向けの2つのインストール形態で動作しています。現在までに合計1,830万以上のグラフが作成されています。それらのグラフはすべて、本記事で取り上げるビジュアライゼーションライブラリによって生成されています。
長い間、DataLensのグラフはHighchartsで構築されていました。当初はこれが合理的な選択でした。素早い立ち上げ、豊富なグラフの種類、大きなコミュニティがあります。しかしBIツールは時とともに複雑になっていきます。動作に関する特殊な要件が生まれ、統一されたスタイルで維持すべきデザインシステムが必要になります。そしてある時点から、Highchartsは助けになるよりも邪魔になり始めました。
この記事では、独自のオープンソースビジュアライゼーションライブラリ@gravity‑ui/chartsを開発するという決断に至った経緯を説明します。私と同僚はこのライブラリのコアコントリビューターです。Highchartsの何が問題だったか、どのような代替案を検討したか、アーキテクチャはどうなっているか、そして実際にどのような技術的課題に直面したかを詳しく説明します。
利便性から制約へ
ライセンスとベンダーロック
Highchartsは商用ライブラリです。非商用利用であれば無料ですが、製品を収益化した時点でライセンスが必要になります。それ自体は問題ではありません。多くのチームが有償ツールを使っています。問題は、利用形態が複雑になるところから始まります。
DataLensには3つの配布形態があります。クラウド、オープンソース、そしてオンプレミスです。依存関係に非フリーなライセンスが含まれていると、たとえオプションであっても、それぞれの配布が複雑になります。
ライセンスの問題に加え、外部のロードマップへの強い依存もあります。新しいグラフタイプが欲しければ、ベンダーが実装するのを待つしかありません。ツールチップの動作にバグを見つけたら、チケットを起票してそれが優先されることを祈るしかありません。凡例クリック時に独自の動作が必要なら、ドキュメントやStack Overflowでワークアラウンドを探すことになります。プロダクトが活発に開発されており、ビジュアライゼーションへの要件が複雑な場合、このような依存関係が開発スピードを落とします。
メジャーバージョンアップも別の話です。私たちはHighchartsの8系の最新バージョンを使用していました。次のメジャーバージョンへの移行はそれ自体コストが高いですが、DataLensの特性がそれをさらに困難にしていました。DataLensにはEditorという機能があり、ユーザーがJavaScriptコードを書いてグラフをカスタマイズできます。実質的に彼らはHighchartsを直接操作し、コードで設定を構成しています。このようなユーザーコードを自動的に移行することはできません。任意のJSを一方のインターフェースから別のインターフェースへ安全に変換するコードモドを書くことは不可能です。
動作の制御
BIツールではユーザーがグラフと積極的にインタラクションします。凡例をクリックして系列を非表示にし、データポイントにカーソルを合わせ、ツールチップを固定します。Highchartsでこれらのシナリオを実現するにはコールバックとイベントによる細かい調整が必要でした。そのドキュメントは必ずしも完全ではなく、バージョン間での安定性も保証されていませんでした。
いくつかの典型的な例を示します:
-
ツールチップの固定。標準的なHighchartsのツールチップはマウスが離れると消えます。8系では「スティッキー」なツールチップのネイティブサポートがなく、それは12系になって初めて実装されました。私たちは内部メソッドをオーバーライドすることで実装しました。マウスイベントを横取りし、非表示ロジックを置き換えました。コードは動作しましたが、脆弱でした。
-
ツールチップのカスタムレンダリング。FormatterはHTML文字列を返すため、文字列連結によるマークアップの手動組み立てが必要です。Reactもコンポーネントも使えません。結果としてツールチップのレンダリングは独自のテンプレートロジックを持つ別のレイヤーになってしまいました。
-
凡例のクリック。標準的な動作は系列の表示切り替えです。他を壊さずにそれを完全にオーバーライドするのは容易ではありません。ハンドラーは内部イベント経由でアタッチし、デフォルトの動作を丁寧にキャンセルする必要がありました。
-
テーマ設定。HighchartsにおけるカラーやフォントやスペーシングはJavaScript設定で指定され、インラインスタイルとして適用されます。その上に、Gravity UIのパレットが更新されるたびに手動で同期が必要な、基本スタイルをオーバーライドするためのSCSSファイルを別途管理していました。
このような回避策はそれぞれ孤立した問題ではなく、技術的負債の積み重ねです。1つのハックは単独で機能し、2つであれば頭の中で管理できますが、10個になると相互作用し始めます。一方を直せば別の何かが壊れます。保守の複雑さは非線形に増大し、Highchartsのアップデートのたびに監査が必要になります。今回のアップデートで私たちのどのワークアラウンドが静かに壊れたかを確認するために。
代替案の評価
独自実装の前に、既存のソリューションを公正に調査しました。主な評価基準は次の通りでした:
-
オープンライセンス — MITまたは互換性のあるもの。有償ティアやOEM契約なし。
-
レンダリングとスタイルの完全なカスタマイズ — ワークアラウンドなしに任意の要素の見た目を制御できること。
-
HTML埋め込み — ツールチップ、グラフのラベル、軸ラベルをSVGテキストや文字列テンプレートではなく、完全なHTMLマークアップとしてレンダリングできること。
ECharts、Recharts、Plotly、Chart.js、D3を検討しました。
Chart.jsとPlotlyはすぐに除外されました。デフォルトのCanvasレンダリングはCSSによるスタイルカスタマイズの可能性を閉ざしており、グラフ要素への任意のHTML埋め込みも想定されていません。
RechartsはReactファースト、SVG、MITライセンスです。シンプルなダッシュボードには適しています。しかしシェイプや動作の深いカスタマイズはすぐに内部APIの限界にぶつかります。
EChartsが最も近い選択肢でした。豊富なグラフの種類、柔軟な設定、カスタムレンダラーのサポートがあります。しかし詳しく調べると、主要な評価基準の1つを満たしていませんでした。グラフの任意の部分への完全なHTML埋め込みです。私たちのユースケースではこれが不可欠でした。加えて、ベンダーロックはなくなりません。MITライセンスは法的な問題を解消しますが、外部のロードマップと更新サイクルへの依存は残ります。
D3はグラフライブラリではなく、スケール、シェイプジェネレーター、データ変換などのプリミティブのセットです。単体では問題を解決しませんが、その上に独自のソリューションを構築するための理想的な基盤を提供します。
結論は論理的なものでした。D3を基盤として、その上に独自のライブラリを構築する。これにより自由と制御の両方が得られ、あらゆるベンダーへの依存を完全に排除できます。
なぜD3であって他のものではないのか
D3はグラフを描画しません。スケール、幾何学的シェイプのジェネレーター、変換など、データとDOMを扱うためのツールのセットです。まさにそれが必要なものでした。ライブラリの作者が想定した範囲に縛られることなく、任意のビジュアライゼーションを組み立てることができるプリミティブです。
D3はscaleLinear、scaleBand、scaleUtcを軸のために、d3.line ()、d3.arc ()、d3.area ()をシェイプのために、d3.extent ()とd3.group ()をデータ変換のために提供します。それ以外はすべて私たちが実装します。
@gravity‑ui/chartsのアーキテクチャ
エントリーポイント — 設定オブジェクト
ユーザーはデータと設定を含む1つのオブジェクトをコンポーネントに渡します。系列、軸、タイトル、凡例、ツールチップが含まれます。これは宣言的なアプローチを採用するための意識的な選択です。設定の大部分はシリアライズ、ログ記録、システム間の受け渡しが容易です。
宣言的な記述で不十分な場合、設定は関数を受け付けます。カスタムツールチップのレンダラー、クリックハンドラー、軸ラベルのフォーマッターです。これは矛盾ではなく、意図的な境界線です。データ構造は予測可能であり続け、拡張ポイントは明示的です。
データフロー
ライブラリ内部では、設定はいくつかの処理段階を経ます:
-
正規化。入力データは統一された内部フォーマットに変換されます。このステップでデフォルト値が設定され、曖昧さが解消され、各系列のデータが型付けされます。
-
系列の準備。グラフの種類(line、bar、pieなど)ごとにそれぞれのモジュールが処理します。色が割り当てられ、凡例のメタデータが生成されます。

- スケールと軸の構築。データに基づいてD3がスケールを構築します。線形、対数、時間、カテゴリです。スケールはデータの値をピクセル座標に変換する方法を定義します。

- シェイプのレンダリング。各系列タイプは独自のSVG要素を描画します。線、矩形、円弧、点です。D3がシェイプジェネレーターを提供し、Reactが要素のライフサイクルを管理します。
SVGとHTML:ハイブリッドレンダリング
メインレンダリングはSVGです。これにより明確な境界、クオリティを損なわないスケーリング、そして位置決めの完全な制御が得られます。
しかしSVGはテキストの折り返しができず、要素内のリッチなHTMLマークアップをサポートしません。折り返しのあるデータラベル、カスタムツールチップ、グラフ上のインタラクティブな要素には、SVGに重ねて配置される別のHTMLレイヤーを使用しています。これはSVGの座標と同期する絶対位置指定のdivです。
イベントシステム
グラフとのユーザーインタラクション(ホバー、クリック、マウス移動)はd3.dispatchに基づく集中型イベントバスで処理されます。これによりインターフェースの異なる部分(ツールチップ、クロスヘア、凡例)が、コンポーネント間の直接依存なしに、1つのイベントに対して独立かつ一貫して反応できます。
しかしこのアプローチにはさらに重要な側面があります。パフォーマンスです。グラフ上でのマウス移動は高頻度でイベントを生成します。それぞれのイベントでReactのレンダリングが走れば、コンポーネントツリー全体の高コストな再計算につながります。代わりに、シェイプのホバーや最近傍点の検索などの一部の更新はD3を通じて直接適用され、Reactをバイパスします。コンポーネントは再レンダリングされません。D3が必要なDOM属性を更新するだけです。Reactが必要なのは構造や状態の変化がグラフ全体に影響する場合だけです。
プロダクトを止めずに移行する
データソースからグラフまでのデータの流れ
ライブラリの切り替えについて話す前に、DataLensにおけるグラフのレンダリングがどのように構成されているかを理解することが重要です。なぜなら、まさにそのアーキテクチャが移行方法を決定したからです。
ユーザーがチャートを開く(またはダッシュボードでレンダリングされる)と、Node.jsバックエンドにデータリクエストが送信されます。Node.jsバックエンドは次にPythonサービスに生データを取得しに行きます。しかしデータそのものはまだグラフではありません。Node側では準備処理が行われます。ビジュアライゼーションのタイプが決定され、そのタイプのデータ制限を超えていないかが確認され、問題がなければそのグラフタイプ固有のデータ準備関数が選択されます。
重要なポイントは、各ビジュアライゼーションタイプにはそれぞれ専用のデータ準備関数があるということです。折れ線グラフ用に1つ、棒グラフ用に1つ、エリアグラフ用に1つあります。この関数の出力がクライアントに送られる設定オブジェクトです。クライアント側では@gravity‑ui/chartsに直接渡されるのではなく、@gravity‑ui/chartkitという別のパッケージを介して渡されます。これは複数のビジュアライゼーションライブラリを同時に扱うために使用するパッケージで、特定のチャートタイプに必要なライブラリのみを動的に読み込み、すべてに対して統一されたインターフェースを提供します。クライアントコードは特定のタイプをどのライブラリがレンダリングしているかを知らず、気にしません。
ルーティングに加えて、chartkitはライブラリ自体の上にラッパーを含んでいます。特定のライブラリの範囲には収まらないがDataLensで必要なロジックです。たとえばモバイルデバイスでのタップによるツールチップ表示は、HighchartsにもJSON@gravity‑ui/chartsにも同様に必要なため、chartkitのレベルで実装されています。
このようなものはDataLensだけでなく潜在的に役立つ可能性があります。そのため@gravity‑ui/chartkitの詳細な説明は別の記事に値します。
段階的移行の戦略
このアーキテクチャが、すべてを一度に書き直すことなく段階的に移行する可能性を与えてくれました。
ビジュアライゼーションタイプのレベルでフィーチャーフラグを導入しました。ロジックはシンプルです。特定のビジュアライゼーションのフラグがtrueに設定されている場合、Nodeは@gravity‑ui/charts形式でデータを準備し、クライアントは新しいライブラリを使用します。フラグが設定されていない場合、データはHighcharts形式で準備され、古いコードがレンダリングします。
これは、DataLensがある時点で両方のライブラリを同時に動作させていたことを意味します。それぞれのグラフタイプのセットに対して。このアプローチにはいくつかの利点がありました:
-
リスクの分離。新しい実装の棒グラフのバグは、まだHighchartsを使っている縦棒グラフには影響しません。
-
段階的な検証。各タイプは次のタイプが移行する前に、本番環境での観察期間を経ます。
-
ロールバックの可能性。何か問題が起きた場合、フラグを無効にするだけです。
移行は3つのウェーブで行われ、順序は意識的に選択されました。単純なものから複雑なものへ。
ウェーブ1:pieとtreemap。最も論理的なスタートは軸のないビジュアライゼーションです。スケールなし、クロスヘアなし、複雑な座標系なし。これにより基本的な統合を確認し、データ準備パイプラインを整え、最も負荷の高いタイプにリスクをかけることなく移行インフラが正しく機能することを確認できました。

ウェーブ2:bar‑y、bar‑y normalized、scatter。次のステップは軸のあるグラフですが、重要な制限があります。これらのタイプはsplitモード(複数のグラフが共通のX軸で縦に並ぶモード)でも、複合グラフとしても使用されません。これによりエッジケースの数が大幅に減り、移行が予測可能になりました。

ウェーブ3:area、area normalized、bar‑x、bar‑x normalized、lineおよびそれらの組み合わせ。最も複雑な段階です。これらのタイプはDataLensで最も人気があり、ほとんどのダッシュボードで使用されています。

さらに複合グラフ(たとえばline bar‑xを1つのグラフ上に)、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テーマを持つデザインシステムです。テーマはは@gravity‑ui/uikitのCSS変数で機能します。テーマを接続すれば、軸、グリッド、ラベルのすべての色がライトモードまたはダークモードに自動的に適応します。グラフインターフェースのツールチップとボタンはuikitの標準コンポーネントであり、デザインシステムからのアクセシビリティ、キーボードナビゲーション、イベント処理を継承しています。

混乱なきスケール。グラフタイプが多くなると、アーキテクチャが結果を左右し始めます。新しいタイプの追加はコアへの修正や既存の動作の破壊を要求すべきではありません。各タイプに統一された構造を採用することでこれを解決しました。独自のデータ準備、独自のレンダリングコンポーネント、独自の型を持つ、すべて独立したモジュールに収められています。ライブラリのコアはタイプの存在を具体的な実装ではなく共通のコントラクトを通じてのみ知ります。
ビジュアルテスト。ビジュアライゼーションは何よりもユーザーが見るものです。そのためユニットテストだけでは不十分です。各グラフタイプはPlaywrightによるスクリーンショットテストでカバーされており、再現性のためにDockerで実行されます。1つのモジュールへの変更が別のモジュールのレンダリングを静かに壊すことはできません。スナップショットの失敗として即座に検出されます。
まとめと結論
この移行は時間と多大な投資を要しました。しかし結果は単なる「ライブラリの交換」ではありません。私たちが得たものを示します:
-
コードの完全な制御。何かが期待通りに動作しない場合、どこにでも入って原因を理解し修正できます。ブラックボックスなし、他人のコードへのワークアラウンドなし、ベンダーからの修正待ちなし。
-
オープンソース。ライブラリはすべての人に公開されています。DataLensチームだけでなく。これは外部コントリビューション、公開イシュー、透明な変更履歴を意味します。外部ユーザーが報告した問題がすべての人のためにプロダクトを改善します。
-
統一されたビジュアル言語。グラフはサードパーティベンダーからの挿入要素ではなく、DataLensインターフェースの有機的な一部になりました。テーマ設定、タイポグラフィ、インタラクティブなコンポーネント、すべてが1つのデザインシステムから。
独自のグラフライブラリを書くことは、軽々しく下すべき決断ではありません。大量の作業量が伴い、さらにもう1つのプロダクトを維持し、ドキュメント化し、発展させる必要があります。
しかし、もしあなたのツールがデータビジュアライゼーションを中心に構築されているなら、動作への非標準的な要件があるなら、見た目の完全な制御とデザインシステムとの深い統合を求めているなら、他人のライブラリへのベンダーロックはいつか天井になります。私たちはその天井にぶつかり、取り除くことを決めました。
今後の展望
ライブラリは積極的に開発が続いており、現在いくつかの方向で作業しています。
フレームワーク非依存のコア。現在@gravity‑ui/chartsはReactライブラリです。データ準備、スケール構築、座標計算のロジックをコアとして、フレームワーク依存のない別パッケージに切り出す計画があります。これにより2つの道が開かれます。ReactなしのプレーンJavaScriptでの使用と、主要ロジックを複製せずにVue、Angular、その他のフレームワーク向けの薄いラッパーを書く可能性です。
カテゴリ軸のレンジスライダー。レンジスライダーコンポーネントは現在、数値軸と時間軸で動作します。範囲を選択してデータの必要な部分を「ズームイン」できます。しかしカテゴリ軸(国名やユーザー名のリストなど)もBI分析では同様に重要です。値のサブセットを選択してデータのフィルタリングに渡せるよう、スライダーをカテゴリでも機能するように拡張しています。
コントリビューター向けドキュメント。現在、新しいグラフタイプを追加するのはライブラリのアーキテクチャを内部から知っている人には理解可能なプロセスです。しかし外部コントリビューターには明確ではありません。詳細なガイドを作成したいと考えています。タイプモジュールとは何か、どのようなコントラクトを満たすべきか、テストの書き方、新しいビジュアライゼーションを共通システムに接続する方法。目標は、コードベースを一度も触ったことのない人でもドキュメントを参照して独自に新しいグラフタイプを追加できるようにすることです。
@gravity‑ui/chartsライブラリはMITライセンスのもと公開されています。ぜひあなたのプロジェクトで試してみてください。
-
ドキュメント → gravity‑ui.github.io/charts
-
Storybook → preview.gravity‑ui.com/charts
-
GitHub → github.com/gravity‑ui/charts
GitHubでのスターをお待ちしています ⭐

Евгений Алаев
ユーザー