グラフ可視化ライブラリ:Gravity UIでCanvas vs HTMLのジレンマをどう解決したか

こんにちは!私はアンドレイ、Yandexのインフラストラクチャサービスユーザーエクスペリエンスチームのインターフェース開発者です。私たちはGravity UIを開発しています — 社内外の数十の製品で使用されているオープンソースのデザインシステムとReactコンポーネントライブラリです。今日は、複雑なグラフの可視化タスクに直面した経緯、既存のソリューションがなぜ満足できなかったのか、そして最終的に@gravity‑ui/graph — コミュニティにオープンにすることにしたライブラリ — がどのように生まれたかをお話しします。

この物語は実際の問題から始まりました:インタラクティブなコンポーネントを持つ10,000以上の要素のグラフをレンダリングする必要がありました。Yandexには、ユーザーが複雑なデータ処理パイプラインを作成するプロジェクトが多数あります — シンプルなETLプロセスから機械学習まで。そのようなパイプラインがプログラムで作成される場合、ブロックの数は数万に達することがあります。

既存のソリューションには満足できませんでした:

  • HTML/SVGライブラリは見た目が良く、開発が便利ですが、数百の要素でも遅くなり始めます。
  • Canvasソリューションはパフォーマンスに対応できますが、複雑なUI要素を作成するには膨大なコードが必要です。

Canvasで角丸とグラデーションのあるボタンを描くのはそれほど難しくありません。しかし、独自の複雑なコントロールやレイアウトを作成する必要があるときに問題が発生します — 数十行の低レベルの描画コマンドを書く必要があります。各インターフェース要素をゼロからプログラミングする必要があります — クリック処理からアニメーションまで。私たちが必要としていたのは、本格的なUIコンポーネント:ボタン、セレクト、入力フィールド、ドラッグアンドドロップでした。

私たちはCanvasとHTMLのどちらかを選ぶのではなく、両方の技術の良いところを使うことにしました。アイデアはシンプルでした:ユーザーがグラフをどれだけ近くで見ているかに応じて、モード間を自動的に切り替えます。

Full screen image

自分で試してみてください

タスクの由来

Nirvanaとそのグラフ

Yandexには、データ処理グラフを作成・実行するためのNirvanaというサービスがあります(2018年に記事を書きました)。サービスは大規模で人気があり、長い間存在しています。

一部のユーザーは手動でグラフを作成します — マウスを動かし、ブロックを追加し、接続します。そのようなグラフには問題ありません:ブロックが少なく、すべてがうまく動作します。しかし、プログラムでグラフを作成するプロジェクトがあります。そしてここで複雑さが始まります:1つのグラフに最大10,000の操作を入れることができます。そして、このようになります:

Full screen image
そしてこんな感じにも:

image
image
image
image
image

そのようなグラフを通常のHTML + SVGの組み合わせは処理できません。ブラウザが遅くなり始め、メモリがリークし、ユーザーが苦しみます。問題を直接解決しようとしました:HTMLレンダリングを最適化しましたが、遅かれ早かれ物理的な制限にぶつかりました — DOMは同時に見える数千のフローティングインタラクティブ要素向けに設計されていません。

別のソリューションが必要で、ブラウザにはCanvasしか残っていません。必要なパフォーマンスを確保できるのはCanvasだけです。

最初の考え — 既製のソリューションを見つける。2017-2018年頃で、Canvasやグラフレンダリング用の人気のあるライブラリを調べましたが、すべてのソリューションが同じ問題にぶつかりました:Canvasとプリミティブな要素を使うか、HTML/SVGを使ってパフォーマンスを犠牲にするかです。

では、選ばないとしたら?

Level of Details:GameDevからのインスピレーション

GameDevと地図作成には素晴らしいコンセプトがあります — Level of Details (LOD)。この技術は必要性から生まれました — パフォーマンスを殺さずに巨大な世界をどう表示するか?

本質はシンプルです:1つのオブジェクトは、どれだけ近くで見られているかに応じて、複数のレベルの詳細を持つことができます。ゲームでは特に顕著です:

  • 遠くに山が見えます — シンプルなポリゴンと基本的なテクスチャです。
  • 近づくと — 詳細が現れます:草、石、影。
  • さらに近づくと — 木の個々の葉が見えます。

プレイヤーが山頂に立って遠くを見ているとき、誰も何百万もの草のポリゴンをレンダリングしません。

地図でも原理は同じです — 各ズームレベルには独自のデータセットと詳細レベルがあります:

  • 大陸スケール — 国だけが見えます。
  • 都市にズームイン — 通りと地区が現れます。
  • さらに近づくと — 建物番号、カフェ、バス停。

私たちは気づきました:ユーザーは10,000ブロックのグラフの大きなスケールでインタラクティブなボタンを必要としません — とにかく見えないし、操作もできません。

さらに、10,000のHTML要素を同時にレンダリングしようとすると、ブラウザがフリーズします。しかし、特定のエリアにズームインすると、見えるブロックの数は急激に減少します — 10,000から、例えば50に。ここでリソースが解放され、豊かなインタラクティビティを持つHTMLコンポーネントが使えます。

Level of Detailsスキームの3つのレベル

Minimalistic(スケール0.1-0.3)— シンプルなプリミティブ付きCanvas

このモードでは、ユーザーはシステムの全体的なアーキテクチャを見ます:主要なブロックグループがどこにあるか、それらがどのように接続されているか。各ブロックは基本的な色分けを持つシンプルな長方形です。テキスト、ボタン、詳細なアイコンはありません。しかし、何千もの要素を快適にレンダリングできます。このレベルで、ユーザーは詳細な調査のためのエリアを選択します。

Full screen image

Schematic(スケール0.3-0.7)— 詳細付きCanvas

ブロック名、状態アイコン、接続用のアンカーが表示されます。テキストはCanvas APIで描画されます — 高速ですが、スタイリングの可能性は限られています。ブロック間の接続がより情報豊富になります:データフローの方向、接続状態を表示できます。これはCanvasのパフォーマンスと基本的な情報性を組み合わせた移行モードです。

Full screen image

Detailed(スケール0.7+)— 完全なインタラクティビティを持つHTML

ここでブロックは本格的なインターフェースコンポーネントに変わります:コントロールボタン、パラメータフィールド、プログレスバー、セレクト。HTML/CSSのあらゆる機能を使用し、UIライブラリを接続できます。このモードでは、通常20-50個以下のブロックがビューポートに収まり、詳細な作業に快適です。

Full screen image

詳細レベルを選択するためにFPSをカウントするのはどうですか?

FPSに基づいて詳細を選択するアプローチがありました。しかし、そのようなアプローチは不安定さを生み出すことがわかりました — パフォーマンスが向上するとシステムがより詳細なモードに切り替わり、FPSが低下して元に戻る切り替えを引き起こす可能性があります — そしてその繰り返しです。

ソリューションに至った経緯

よし、LODは素晴らしい。しかし、実装にはパフォーマンスのためにCanvasが必要で、これは新たな頭痛の種です。Canvasに描画するのはそれほど難しくありません — 問題はインタラクティビティを作る必要があるときに現れます。

問題:ユーザーがどこをクリックしたかをどう理解するか?

HTMLではすべてがシンプルです:ボタンをクリックすると、要素で直接イベントを取得します。Canvasではより複雑です:キャンバスをクリックしました — それで?ユーザーがどの要素をクリックしたかを自分で把握する必要があります。

基本的に3つのアプローチがあります:

  • Pixel Testing(カラーピッキング)
  • Geometric approach(すべての要素の単純な反復)
  • Spatial Indexing(空間インデックス)

Pixel Testing(カラーピッキング)

アイデアはシンプルです:2番目の非表示のキャンバスを作成し、そこにシーンをコピーしますが、各要素をオブジェクトのIDと見なされるユニークな色で塗りつぶします。クリック時に、getImageDataを通じてマウスポインターの下のピクセルの色を読み取り、要素のIDを取得します。

長所

短所

  • 数十行で実装

  • 追加のデータ構造が不要

  • Canvasのアンチエイリアスが色を混ぜる — 図形の境界でクリックすると「無効な」IDが得られる可能性がある

  • 2D-Canvasではアンチエイリアスをオフにできない

  • 2番目のキャンバスがメモリを複製し、レンダーパスを2倍にする

小さなシーンには適していますが、10,000以上の要素ではエラー率が許容できなくなります — Pixel Testingは延期します。

Geometric approach(すべての要素の単純な反復)

アイデアはシンプルです:すべての要素を反復し、クリックポイントが要素の内側にあるかどうかをチェックします。

長所

短所

  • 数十行で実装

  • 追加のデータ構造が不要

  • 要素が多いと非常に遅い

  • 大きなシーンには適さない

Spatial Indexing

幾何学的アプローチの発展。幾何学的アプローチでは要素の数に限界がありました。空間インデックスアルゴリズムは、隣接する要素を何らかの方法でグループ化しようとし、主にツリーを使用することで、複雑さをlog nに減らすことができます。

空間インデックスアルゴリズムはかなり多くあり、rbushライブラリの形でR-Treeデータ構造を選択しました。

R-Treeは、名前からわかるように、各オブジェクトが最小サイズの長方形(MBR)に配置され、それらの長方形がより大きな長方形にグループ化されるツリーです。こうして、各長方形が他の長方形を含むツリーが得られます。

Full screen image

WikipediaのR‑treeからの画像

RTreeで検索するには、特定の要素に到達するまでツリーを下降(長方形の奥深く)する必要があります。パスは、検索長方形とMBRの交差をチェックすることで選択されます。バウンディングボックスが検索長方形に触れてさえいないすべてのブランチはすぐに破棄されます — これが、トラバースの深さが通常3-5レベルに制限され、検索自体が数万の要素でもマイクロ秒で完了する理由です。

このバリアントは、ピクセルテスト(最良の場合O (log n)、最悪の場合O (n))よりも遅く動作しますが、より正確でメモリ要件が低いです。

イベントモデル

RTreeに基づいて、イベントモデルを構築できるようになりました。ユーザーがクリックすると、ヒットテストプロシージャが起動します:カーソル座標に1×1ピクセルの長方形を形成し、R-Treeでその交差を検索します。この長方形が収まる要素を取得したら、イベントをその要素に委任します。要素がイベントを停止しなかった場合、イベントはその親に渡され、ルートまで続きます。このモデルの動作は、ブラウザでおなじみのイベントモデルの動作に似ています。イベントはインターセプト、プリベント、または伝播を停止できます。

すでに述べたように、ヒットテスト中に1×1ピクセルの長方形を形成します。これは、任意のサイズの長方形を形成できることを意味します。そしてこれが、もう1つの非常に重要な最適化 — Spatial Culling — を行うのに役立ちます。

Spatial Culling

Spatial Cullingは、見えないものを描画しないことを目的としたレンダリング最適化技術です。例えば、カメラの空間外にあるオブジェクトや、シーンの他の要素に隠されているオブジェクトを描画しません。グラフは2D空間で描画されるため、カメラの可視領域(ビューポート)外にあるオブジェクトのみを描画しなければ十分です。

動作原理:

  • カメラを移動またはズームするたびに、現在のビューポートに等しい長方形を形成します
  • R-Treeでその交差を検索します
  • 結果は実際に見える要素のリストになります
  • それらだけをレンダリングし、他のすべてはスキップされます

このテクニックにより、パフォーマンスは要素の総数にほぼ依存しなくなります:フレームに40ブロックが収まる場合 — ライブラリは正確に40を描画し、画面外に隠れている数万は描画しません。遠いスケールでは多くの要素がビューポートに収まるため、軽量なCanvasプリミティブを描画し、カメラが近づくと要素数が減少し、解放されたリソースで完全な詳細を持つHTMLモードに切り替えることができます。

すべてをまとめると、シンプルなスキームになります:

  • Canvasは速度を担当
  • HTMLはインタラクティビティを担当
  • R-TreeとSpatial Cullingは、どの要素をHTMLレイヤーに描画できるかをすばやく理解できるようにして、それらを単一のシステムに見えない形で統合します

カメラが動いている間、小さなビューポートはR-Treeに実際にフレーム内にあるオブジェクトのみを要求します。このアプローチにより、本当に大きなグラフを描画できます。少なくとも、ユーザーがビューポートを制限するまでパフォーマンスの余裕があります。

まとめると、ライブラリのコアには以下が含まれています:

  • シンプルなプリミティブを持つCanvasモード
  • 完全な詳細を持つHTMLモード
  • パフォーマンス最適化のためのR-TreeとSpatial Culling
  • おなじみのイベントモデル

しかし、本番環境にはこれだけでは不十分で、ライブラリを拡張し、ニーズに合わせてカスタマイズする機能が必要です。

カスタマイズ

ライブラリは、動作の拡張と変更のための2つの補完的な方法を提供します:

  • 基本コンポーネントのオーバーライド。標準のBlock、Anchor、Connectionのロジックを変更します。
  • レイヤー(Layers)による拡張。既存のシーンの上/下に根本的に新しい機能を追加します。

コンポーネントのオーバーライド

既存の要素の外観や動作を変更する必要がある場合、基本クラスを継承し、キーメソッドをオーバーライドします。次に、独自の名前でコンポーネントを登録します。

ブロックのカスタマイズ

例えば、ブロックにプログレスバーのあるグラフを作成する必要がある場合 — 例えば、パイプラインのタスク実行状態を表示するために — 標準ブロックを簡単にカスタマイズできます:

import { CanvasBlock } from "@gravity‑ui/graph";

class ProgressBlock extends CanvasBlock {
  // 角丸の基本ブロック形状
  public override renderBody(ctx: CanvasRenderingContext2D): void {
    ctx.fillStyle = "#ddd";
    ctx.beginPath();
    ctx.roundRect(this.state.x, this.state.y, this.state.width, this.state.height, 12);
    ctx.fill();
    ctx.closePath();
  }

  public renderSchematicView(ctx: CanvasRenderingContext2D): void {
    const progress = this.state.meta?.progress || 0;

    // ブロックの基本を描画
    this.renderBody(ctx);

    // 色インジケーション付きプログレスバー
    const progressWidth = (this.state.width - 20) * (progress / 100);
    ctx.fillStyle = progress < 50 ? "#ff6b6b" : progress < 80 ? "#feca57" : "#48cae4";
    ctx.fillRect(this.state.x + 10, this.state.y + this.state.height - 15, progressWidth, 8);

    // プログレスバーのフレーム
    ctx.strokeStyle = "#ddd";
    ctx.lineWidth = 1;
    ctx.strokeRect(this.state.x + 10, this.state.y + this.state.height - 15, this.state.width - 20, 8);

    // パーセントと名前のテキスト
    ctx.fillStyle = "#2d3436";
    ctx.font = "12px Arial";
    ctx.textAlign = "center";
    ctx.fillText(`${Math.round(progress)}%`, this.state.x + this.state.width / 2, this.state.y + 20);
    ctx.fillText(this.state.name, this.state.x + this.state.width / 2, this.state.y + 40);
  }
}

接続のカスタマイズ

同様に、接続の動作と外観を変更する必要がある場合 — 例えば、ブロック間のデータフローの強度を表示するために — カスタム接続を作成できます:

import { BlockConnection } from "@gravity-ui/graph";

class DataFlowConnection extends BlockConnection {
  public override style(ctx: CanvasRenderingContext2D) {
    // 接続されたブロックからフローデータを取得
    const sourceBlock = this.sourceBlock;
    const targetBlock = this.targetBlock;

    const sourceProgress = sourceBlock?.state.meta?.progress || 0;
    const targetProgress = targetBlock?.state.meta?.progress || 0;

    // ブロックの進捗に基づいてフロー強度を計算
    const flowRate = Math.min(sourceProgress, targetProgress);
    const isActive = flowRate > 10; // 進捗 > 10%でフローがアクティブ

    if (isActive) {
      // アクティブなフロー -- 太い緑の線
      ctx.strokeStyle = "#00b894";
      ctx.lineWidth = Math.max(2, Math.min(6, flowRate / 20));
    } else {
      // 非アクティブなフロー -- 点線のグレーの線
      ctx.strokeStyle = "#ddd";
      ctx.lineWidth = this.context.camera.getCameraScale();
      ctx.setLineDash([5, 5]);
    }

    return { type: "stroke" };
  }
}

カスタムコンポーネントの使用

作成したコンポーネントをグラフ設定に登録します:

const customGraph = new Graph({
  blocks: [
    {
      id: "task1",
      is: "progress",
      x: 100,
      y: 100,
      width: 200,
      height: 80,
      name: "データ処理",
      meta: { progress: 75 },
    },
    {
      id: "task2",
      is: "progress",
      x: 400,
      y: 100,
      width: 200,
      height: 80,
      name: "分析",
      meta: { progress: 30 },
    },
    {
      id: "task3",
      is: "progress",
      x: 700,
      y: 100,
      width: 200,
      height: 80,
      name: "出力",
      meta: { progress: 5 },
    },
  ],
  connections: [
    { sourceBlockId: "task1", targetBlockId: "task2" },
    { sourceBlockId: "task2", targetBlockId: "task3" },
  ],
  settings: {
    // カスタムブロックを登録
    blockComponents: {
      'progress': ProgressBlock,
    },
    // すべての接続にカスタム接続を登録
    connection: DataFlowConnection,
    useBezierConnections: true,
  },
});

customGraph.setEntities({
  blocks: [
    {
    is: 'progress',
    id: '1',
    name: "プログレスブロック',
    x: 10, 
    y: 10, 
    width: 10, 
    height: 10,
    anchors: [],
    selected: false,
    }
  ]
})

customGraph.start();

結果

結果として得られるグラフでは:

  • ブロックが色インジケーション付きで現在の進捗を表示
  • 接続がデータフローを可視化:アクティブなフローは緑で太く、非アクティブなフローはグレーで点線
  • ズーム時にブロックが自動的に完全なインタラクティビティを持つHTMLモードに切り替わる

レイヤーによる拡張

レイヤーは、グラフの「空間」に挿入される追加のCanvasまたはHTML要素です。本質的に、各レイヤーは独自のキャンバス(高速グラフィック用)またはHTMLコンテナ(複雑なインタラクティブ要素用)を含むことができる別個のレンダリングチャネルです。

ちなみに、ライブラリのReact統合はレイヤーを通じて動作します:ReactコンポーネントはReact Portalを通じてHTMLレイヤーにレンダリングされます。

レイヤーのアーキテクチャ

レイヤーは、Canvas vs HTMLのジレンマに対するもう1つの重要なソリューションです。レイヤーはCanvasとHTML要素の位置を同期し、それらが互いに正しく重なることを保証します。これにより、単一の空間に留まりながら、CanvasとHTMLをシームレスに切り替えることができます。グラフは、互いに重なる独立したレイヤーで構成されています:

Full screen image

レイヤーは2つの座標システムで動作できます:

  • グラフにバインド(transformByCameraPosition: true):

    • 要素がカメラと一緒に移動する
    • ブロック、接続、グラフ要素
  • 画面に固定(transformByCameraPosition: false):

    • パンしても所定の位置に留まる
    • ツールバー、凡例、UIコントロール

React統合の仕組み

React統合を持つレイヤーは、レイヤーとは何かを示すのに十分です。まず、カメラの可視領域内にあるブロックのリストを強調するコンポーネントを見てみましょう。このために、カメラの変更を購読し、各変更後にカメラのビューポートと要素のヒットボックスの交差をチェックする必要があります。

import { Graph } from "@gravity-ui/graph";

const BlocksList = ({ graph, renderBlock }: { graph: Graph, renderBlock: (graph: Graph, block: TBlock) => React.JSX.Element }) => {
  const [blocks, setBlocks] = useState([]);

  const updateVisibleList = useCallback(() => {
    const cameraState = graph.cameraService.getCameraState();
    const CAMERA_VIEWPORT_TRESHOLD = 0.5;
    const x = -cameraState.relativeX - cameraState.relativeWidth * CAMERA_VIEWPORT_TRESHOLD;
    const y = -cameraState.relativeY - cameraState.relativeHeight * CAMERA_VIEWPORT_TRESHOLD;
    const width = -cameraState.relativeX + cameraState.relativeWidth * (1 + CAMERA_VIEWPORT_TRESHOLD) - x;
    const height = -cameraState.relativeY + cameraState.relativeHeight * (1 + CAMERA_VIEWPORT_TRESHOLD) - y;
    
    const blocks = graph
      .getElementsOverRect(
        {
          x,
          y,
          width,
          height,
        }, // ブロックリストを検索するエリアを定義
        [CanvasBlock] // カメラの可視領域で検索される要素タイプを定義
      ).map((component) => component.connectedState); // ブロックモデルのリストを取得

      setBlocks(blocks);
  });

    useGraphEvent(graph, "camera-change", ({ scale }) => {
      if (scale >= 0.7) {
        // スケールが0.7以上の場合、ブロックリストを更新
        updateVisibleList()
        return;
      }
      setBlocks([]);
    });

    return blocks.map(block => <React.Fragment key={block.id}>{renderBlock(graphObject, block)}</React.Fragment>)
}

次に、このコンポーネントを使用するレイヤー自体の説明を見てみましょう。

import { Layer } from '@gravity-ui/graph';

class ReactLayer extends Layer {
  constructor(props: TReactLayerProps) {
    super({
      html: {
        zIndex: 3, // レイヤーを他のレイヤーの上に上げる
        classNames: ["no-user-select"], // テキスト選択を無効にするクラスを追加
        transformByCameraPosition: true, // レイヤーがカメラにバインド - レイヤーがカメラと一緒に移動
      },
      ...props,
    });
  }

  public renderPortal(renderBlock: <T extends TBlock>(block: T) => React.JSX.Element) {
    if (!this.getHTML()) {
      return null;
    }

    const htmlLayer = this.getHTML() as HTMLDivElement;

    return createPortal(
      React.createElement(BlocksList, {
        graph: this.context.graph,
        renderBlock: renderBlock,
      }),
      htmlLayer,
    );
  }
}

これでアプリケーションでこのレイヤーを使用できます。

import { Flex } from "@gravity-ui/uikit";

const graph = useMemo(() => new Graph());
const containerRef = useRef<HTMLDivElement>();

useEffect(() => {
    if (containerRef.current) {
      graph.attach(containerRef.current);
    }

    return () => {
      graph.detach();
    };
  }, [graph, containerRef]);


const reactLayer = useLayer(graph, ReactLayer, {});

const renderBlock = useCallback((graph, block) => <Block graph={graph} block={block}>{block.name}</Block>)

  return (
    <div>
      <div style={{ position: "absolute", overflow: "hidden", width: "100%", height: "100%" }} ref={containerRef}>
        {graph && reactLayer && reactLayer.renderPortal(renderBlock)}
      </div>
    </div>
  );

全体的にかなりシンプルです。上記で説明したことは自分で書く必要はありません — すべて書かれており、使用準備が整っています。

私たちのグラフライブラリ:利点と使い方

ライブラリの作業を開始したとき、主な質問は:開発者がパフォーマンスと開発の便利さのどちらかを選ぶ必要がないようにするにはどうすればよいかでした。答えはこの選択を自動化することでした。

利点

パフォーマンス + 便利さ

@gravity‑ui/graphは、スケールに応じてCanvasとHTMLを自動的に切り替えます。これは以下を得ることを意味します:

  • 数千の要素を持つグラフで安定した60 FPS
  • 詳細表示時に豊かなインタラクティビティを持つ本格的なHTMLコンポーネントを使用する機能
  • レンダリング方法に関係なく統一されたイベントモデル — click、mouseenterはCanvasとHTMLで同じように動作

UIライブラリとの互換性

主な利点の1つは、任意のUIライブラリとの互換性です。チームが使用している場合:

  • Gravity UI
  • Material‑UI
  • Ant Design
  • カスタムコンポーネント

…それらを放棄する必要はありません!ズームインすると、グラフは自動的にHTMLモードに切り替わり、必要な色テーマでおなじみのButtonSelectDatePickerが通常のReactアプリケーションと同じように動作します。

Framework agnostic

Reactを使用して基本的なHTMLレンダラーを実装しましたが、ライブラリがframework-agnosticであるように開発しました。これは、必要に応じてお気に入りのフレームワークの統合を持つレイヤーを非常に簡単に実装できることを意味します。

代替品はあるか?

現在、市場にはグラフ描画のためのソリューションがかなり多くあります。yFilesJointJSなどの有料ソリューションから、Foblex FlowbaklavajsjsPlumbなどのオープンソースソリューションまで。しかし、比較のために最も人気のあるツールとして@antv/g6React Flowを検討しています。それぞれに特徴があります。

React Flow — ノードベースのインターフェース構築に適した良いライブラリです。多くの機能がありますが、svgとhtmlを使用しているため、パフォーマンスはかなり控えめです。グラフが100-200ブロックを超えないことが確実な場合には良いライブラリです。

一方、@antv/g6には多くの機能があり、Canvas、特にWebGLをサポートしています。@antv/g6と@gravity‑ui/graphを直接比較することはおそらくできません:彼らはより多くのグラフやダイアグラムの構築に焦点を当てています — しかし、ノードベースのUIもサポートされています。したがって、ノードベースのインターフェースだけでなく、チャートを描くことも重要な場合は、antv/g6が適しています。

@antv/g6ライブラリはcanvas/webglとhtml/svgの両方ができますが、切り替えルールの管理は手動で行う必要があり、正しく行う必要があります。パフォーマンスはReact Flowよりもはるかに速いですが、ライブラリにはまだ疑問があります。WebGLサポートがあると主張されていますが、ストレステストを見ると、60kノードでライブラリがダイナミクスを提供できないことがわかります — MacBook M3で1フレームのレンダリングに4秒かかりました。比較のために、同じMacbook M3での111kノードと109k接続を持つ私たちのストレステスト:グラフ全体のシーンのレンダリングに約60msかかり、約15-20FPSを提供します。これはそれほど多くありませんが、Spatial Cullingを考慮すると、ビューポートを制限してレスポンシブ性を向上させることができます。メンテナーは100kノードを30 FPSでレンダリングすることを主張していましたが、どうやらそれはまだ達成されていないようです。

@gravity‑ui/graphが勝っているもう1つのポイントはバンドルサイズです。

バンドルサイズ Minified

バンドルサイズ Minified + Gzipped

@antv/g6 bundlephobia

1.1 MB

324.5 kB

react flow bundlephobia

181.2 kB

56.4 kB

@gravity-ui/graph bundlephobia

2.2 kB

672 B

両方のライブラリはパフォーマンスまたは統合の便利さにおいてかなり強力ですが、@gravity‑ui/graphにはいくつかの利点があります — ライブラリは本当に大きなグラフでパフォーマンスを確保し、ユーザーのためにUI/UXを維持し、開発を簡素化できます。

今後の計画

現在、ライブラリにはほとんどのタスクに十分なパフォーマンスの余裕があるため、近い将来、ライブラリ周りのエコシステムの開発にもっと注意を払います — レイヤー(プラグイン)の開発、他のライブラリやフレームワーク(Angular/Vue/Svelte、…など)の統合の追加、タッチデバイスのサポート、モバイルブラウザへの適応、全体的なUX/DXの改善を行います。

試して参加してください

リポジトリには完全に動作するライブラリがあります:

  • Canvas + R‑Treeのコア(約30K行のコード)
  • React統合
  • サンプル付きStorybook

ライブラリは1行でインストールできます:

npm install @gravity-ui/graph


現在@gravity‑ui/graphと呼ばれているライブラリは、かなり長い間Nirvana内の内部ツールでしたが、選択されたアプローチは十分に実証されました。今、私たちは開発を共有し、外部の開発者がより簡単に、より速く、より効率的にグラフを描くのを助けたいと思っています。

オープンソースコミュニティで複雑なグラフを表示するアプローチを標準化したいと思っています — あまりにも多くのチームが車輪の再発明をしているか、不適切なツールに苦しんでいます。

そのため、フィードバックを収集することは非常に重要です — 異なるプロジェクトは異なるエッジケースをもたらし、ライブラリを発展させることができます。これにより、ライブラリを改善し、Gravity UIエコシステムをより早く成長させることができます。

グラフ可視化ライブラリ:Gravity UIでCanvas vs HTMLのジレンマをどう解決したか

Sign in to save this post