🕸️

ReactでNetwork図(Network Graqh)を作成するなら、vis-networkがおすすめ!

2024/01/09に公開

こんにちは、AIQ株式会社のフロントエンドエンジニアのまさぴょんです!
今回は、ReactでNetwork図(Network Graqh)を作成する際にvis-networkというライブラリを使って、Network図を作成したので、実装方法などについて、解説します。

ReactでNetwork図(Network Graqh)を作成する

vis-networkというライブラリを使って、次の画像のようなNetwork図(Network Graqh)Componentを作っていきます。

vis-networkの環境構築

まずは、vis-networkvis-dataをinstallします。

yarn add vis-network vis-data

# または、、、

npm install vis-network vis-data

TypeScriptを使用している場合は、型定義ファイルもあるので、installします。

yarn add @types/vis

# または、、、

npm i @types/vis

https://www.npmjs.com/package/@types/vis

Network図(Network Graqh)のSampleCode

Network図(Network Graqh)のSampleCodeは、次のとおりです。

このSample Componentを使用すると、冒頭の画像のようなNetwork図(Network Graqh)が表示できます。

NetworkGraph.tsx
import { useEffect, useRef } from "react";
import { Network } from "vis-network";
import { DataSet } from "vis-data";

interface NodeType {
  id: string | number;
  label: string;
  shape: string;
  image: string;
  url?: string;
}

interface EdgeType {
  from: string | number;
  to: string | number;
}

interface NodeEdgeDataSet {
  nodes: NodeType[];
  edges: EdgeType[];
}

/**
 * NOTE: generateDataFromNodes
 * => Nodes と Edges を定義して、返却する Func
 * => 引数: nodes, parentNodeId
 * => nodes はノードの情報を含む配列
 * => parentNodeId は親ノードのID
 */
const generateDataFromNodes: any = (
  nodes: NodeType[],
  parentNodeId: string | number
) => ({
  nodes: nodes,
  /** from 親・Node => to 子・Node に向かって、Edges(枝)を伸ばす */
  edges: nodes.map((node: NodeType) => ({ from: parentNodeId, to: node.id })),
});

// ----------------------------------- Node Data 領域 -----------------------------------

/**
 * 第一階層の Node & それに紐づいた 5つの第二階層の Node の設定値
 */
const controlNodes = [
  // 1つ目の Node設定
  {
    id: "robo_tamachan12", // ノードの一意の識別子です。この ID はネットワーク内でこのノードを一意に特定するために使用
    label: "ロボ玉 Ver.2", // ノードのラベル => この場合、"Extracted Files (3)" というテキストがノードに表示されます。
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://dzdih2euft5nz.cloudfront.net/users/avatars/909381?1664074598",
    url: "https://masanyon.com/", // ノードをクリックしたときに開くURL
  },
  // 2つ目以降の Node設定
  {
    id: "girl_1",
    label: "ガール_1",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://pbs.twimg.com/profile_images/1620230831892754432/2SQFDJSA_400x400.jpg",
    url: "https://masanyon.com/", // ノードをクリックしたときに開くURL
  },
  {
    id: "girl_2",
    label: "ガール_2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://m.media-amazon.com/images/I/61kr0JvtxML._AC_UF894,1000_QL80_.jpg",
    url: "https://masanyon.com/", // ノードをクリックしたときに開くURL
  },
  {
    id: "girl_3",
    label: "ガール_3",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://eiga.k-img.com/images/person/88674/9169af40192118d8/640.jpg?1627093516",
    url: "https://masanyon.com/", // ノードをクリックしたときに開くURL
  },
  {
    id: "space_broccoli",
    label: "スペース・ブロッコリー",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://d1q9av5b648rmv.cloudfront.net/v3/500x500/cushion/free/white/front/12256617/1664106143-512x512.png.7.3047+0.0+0.0.jpg?h=1af54b2d021d1ca0ed9c8f1210d7d7122231c91f&printed=true",
    url: "https://masanyon.com/", // ノードをクリックしたときに開くURL
  },
];

const Node_1 = [
  {
    id: "copy_robo_1_1",
    label: "ロボ玉 Ver.2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://dzdih2euft5nz.cloudfront.net/users/avatars/909381?1664074598",
    url: "https://zenn.dev/p/aiq_dev", // ノードをクリックしたときに開くURL
  },
  {
    id: "copy_robo_1_2",
    label: "ロボ玉 Ver.2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://dzdih2euft5nz.cloudfront.net/users/avatars/909381?1664074598",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robo_1_3",
    label: "ロボ玉 Ver.2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://dzdih2euft5nz.cloudfront.net/users/avatars/909381?1664074598",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robo_1_4",
    label: "ロボ玉 Ver.2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://dzdih2euft5nz.cloudfront.net/users/avatars/909381?1664074598",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robo_1_5",
    label: "ロボ玉 Ver.2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://dzdih2euft5nz.cloudfront.net/users/avatars/909381?1664074598",
    url: "https://zenn.dev/p/aiq_dev",
  },
];

const Node_2 = [
  {
    id: "copy_girl_2_1",
    label: "ガール_2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://m.media-amazon.com/images/I/61kr0JvtxML._AC_UF894,1000_QL80_.jpg",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_girl_2_2",
    label: "ガール_2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://m.media-amazon.com/images/I/61kr0JvtxML._AC_UF894,1000_QL80_.jpg",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_girl_2_3",
    label: "ガール_2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://m.media-amazon.com/images/I/61kr0JvtxML._AC_UF894,1000_QL80_.jpg",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_girl_2_4",
    label: "ガール_2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://m.media-amazon.com/images/I/61kr0JvtxML._AC_UF894,1000_QL80_.jpg",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_girl_2_5",
    label: "ガール_2",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://m.media-amazon.com/images/I/61kr0JvtxML._AC_UF894,1000_QL80_.jpg",
    url: "https://zenn.dev/p/aiq_dev",
  },
];

const Nodes_3 = [
  {
    id: "nw_1",
    label: "ガール_3",
    shape: "circularImage",
    image:
      "https://eiga.k-img.com/images/person/88674/9169af40192118d8/640.jpg?1627093516",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "nw_2",
    label: "ガール_3",
    shape: "circularImage",
    image:
      "https://eiga.k-img.com/images/person/88674/9169af40192118d8/640.jpg?1627093516",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "nw_3",
    label: "ガール_3",
    shape: "circularImage",
    image:
      "https://eiga.k-img.com/images/person/88674/9169af40192118d8/640.jpg?1627093516",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "nw_4",
    label: "ガール_3",
    shape: "circularImage",
    image:
      "https://eiga.k-img.com/images/person/88674/9169af40192118d8/640.jpg?1627093516",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "nw_5",
    label: "ガール_3",
    shape: "circularImage",
    image:
      "https://eiga.k-img.com/images/person/88674/9169af40192118d8/640.jpg?1627093516",
    url: "https://zenn.dev/p/aiq_dev",
  },
];

const Nodes_4 = [
  {
    id: "copy_robotama_1",
    label: "ロボ玉",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robotama_2",
    label: "ロボ玉",
    shape: "circularImage",
    image:
      "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robotama_3",
    label: "ロボ玉",
    shape: "circularImage",
    image:
      "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robotama_4",
    label: "ロボ玉",
    shape: "circularImage",
    image:
      "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_robotama_5",
    label: "ロボ玉",
    shape: "circularImage",
    image:
      "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c",
    url: "https://zenn.dev/p/aiq_dev",
  },
];

const Nodes_5 = [
  {
    id: "copy_space_broccoli_1",
    label: "スペース・ブロッコリー",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://d1q9av5b648rmv.cloudfront.net/v3/500x500/cushion/free/white/front/12256617/1664106143-512x512.png.7.3047+0.0+0.0.jpg?h=1af54b2d021d1ca0ed9c8f1210d7d7122231c91f&printed=true",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_space_broccoli_2",
    label: "スペース・ブロッコリー",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://d1q9av5b648rmv.cloudfront.net/v3/500x500/cushion/free/white/front/12256617/1664106143-512x512.png.7.3047+0.0+0.0.jpg?h=1af54b2d021d1ca0ed9c8f1210d7d7122231c91f&printed=true",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_space_broccoli_3",
    label: "スペース・ブロッコリー",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://d1q9av5b648rmv.cloudfront.net/v3/500x500/cushion/free/white/front/12256617/1664106143-512x512.png.7.3047+0.0+0.0.jpg?h=1af54b2d021d1ca0ed9c8f1210d7d7122231c91f&printed=true",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_space_broccoli_4",
    label: "スペース・ブロッコリー",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://d1q9av5b648rmv.cloudfront.net/v3/500x500/cushion/free/white/front/12256617/1664106143-512x512.png.7.3047+0.0+0.0.jpg?h=1af54b2d021d1ca0ed9c8f1210d7d7122231c91f&printed=true",
    url: "https://zenn.dev/p/aiq_dev",
  },
  {
    id: "copy_space_broccoli_5",
    label: "スペース・ブロッコリー",
    shape: "circularImage", // これだけで、丸い写真になる!!
    image:
      "https://d1q9av5b648rmv.cloudfront.net/v3/500x500/cushion/free/white/front/12256617/1664106143-512x512.png.7.3047+0.0+0.0.jpg?h=1af54b2d021d1ca0ed9c8f1210d7d7122231c91f&printed=true",
    url: "https://zenn.dev/p/aiq_dev",
  },
];

// ----------------------------------- Node Data 領域 Fin -----------------------------------

/** NOTE: generateDataFromNodes() で Nodes から Data を生成する */

/** 中央に表示する 第一階層の Nodes & Edges */
const initialData = generateDataFromNodes(controlNodes, "rootNode");

/** 第二階層以降の Nodes & Edges */
const NodeData_1 = generateDataFromNodes(Node_1, "robo_tamachan12");
const NodeData_2 = generateDataFromNodes(Node_2, "girl_1");
const NodeData_3 = generateDataFromNodes(Nodes_3, "girl_2");
const NodeData_4 = generateDataFromNodes(Nodes_4, "girl_3");
const NodeData_5 = generateDataFromNodes(Nodes_5, "space_broccoli");

/** Network図 */
let network: any;

/** Network図 の Component */
const NetworkGraph = () => {
  /** DOMを参照できるように useRef を使用して Element を取得する */
  const ref = useRef<HTMLDivElement>(null);

  /** Network図を Create する処理 (初期表示) */
  useEffect(() => {
    /**
     * 1. Node の配列を作成します
     * => DataSet オブジェクトはグラフデータを保持して、管理するために使用します。
     */
    const nodes = new DataSet([
      // CenterNode
      {
        id: "rootNode",
        label: "ロボ玉",
        type: "diamond",
        shape: "circularImage", // これだけで、丸い写真になる!!
        image:
          "https://lh3.googleusercontent.com/a-/AOh14GhYX8r5eB8cdfa1yTA6hD1axnAibrQQzBwMDmxHuQQ=s96-c",
      },
      ...initialData.nodes,
      ...NodeData_1.nodes,
      ...NodeData_2.nodes,
      ...NodeData_3.nodes,
      ...NodeData_4.nodes,
      ...NodeData_5.nodes,
    ]);

    /** 2. Edges を作成する */
    const edges = new DataSet([
      ...initialData.edges,
      ...NodeData_1.edges,
      ...NodeData_2.edges,
      ...NodeData_3.edges,
      ...NodeData_4.edges,
      ...NodeData_5.edges,
    ]);

    /** 3. Option を追加する */
    const options = {
      /** physics オプションは、グラフの物理的シミュレーションに関する設定を指定します。*/
      physics: {
        // barnesHut は、物理エンジンの一部で、ノード間の相互作用を効率的に計算するためのアルゴリズムです。
        barnesHut: {
          /**
           * gravitationalConstant は、ノード間の引力定数を設定します。
           * この値はノード間の引力の強さを調整します。-4000という値は、引力が非常に強いことを示しており、ノードが強く引かれることを意味します。
           * これにより、ノードが密に配置され、よりクラスタリングされる可能性が高くなります。
           */
          gravitationalConstant: -4000,
        },
      },
      /** interaction オプションは、ユーザーとのインタラクションに関する設定を指定します。 */
      interaction: {
        // multiselect がtrueに設定されている場合、ユーザーは複数のノードを選択できるようになります。
        multiselect: true,
      },
    };

    // Network が存在しない場合の処理
    if (!network && ref.current) {
      // Network Instance を作成して、DataをSetする => new Network(Dom領域, Data(Nodes & Edges), Options)
      network = new Network(
        ref.current,
        {
          nodes: nodes,
          edges: edges,
        },
        options
      );
    }

    // Click イベントハンドラ を追加する
    network.on("click", (params: { nodes: number[] }) => {
      if (params.nodes.length > 0) {
        // クリックした Node のIDを取得する
        const nodeId = params.nodes[0];
        /** クリックした Node のIDから、該当の Nodeを取得する */
        const node = nodes.get(nodeId);
        // URLが存在する場合の処理
        if (node?.url) {
          /** クリックしたノードのURLを取得して、Openする */
          window.open(node.url, "_blank");
        }
      }
    });
  }, []);

  return (
    <div>
      {/* Network図 を表示する領域 */}
      <div style={{ height: 800, width: "100%" }} ref={ref} />
    </div>
  );
};

export default NetworkGraph;

vis-networkでのNetwork図の作成ポイントは、次のとおりです。

Cannot add item: item with id xxxxx already exists エラー

idが重複すると、Cannot add item: item with id xxxxx already exists エラーが発生するので、注意してください。

先述のvis-networkでのNetwork図の作成ポイントにも書きましたが、
idは、重複しないようにuuidを組み合わせるなどすることをおすすめします。

Webブラウザの標準機能で、uuidは作成できます。

// UUID を作成する
const uuid = crypto.randomUUID();
console.log(uuid);

https://zenn.dev/manase/scraps/3b9c1c307d5a98

vis-networkに関する役立つDoc

1. 公式が提供するNetwork図のExmaples

vis-networkを使って、どんなNetwork図が作れるかは、公式が提供するExmaplesが参考になります。

https://visjs.github.io/vis-network/examples/

2. vis-networkをReactで実装した人たちのSampleCode集

vis-networkをReactで実装した人たちのSampleCodeなどが、CodeSandbox上にあります。

https://codesandbox.io/examples/package/vis-network-react

3. 公式GitHubのSampleDirectory

公式GitHubのSampleDirectoryにもCodeの事例などがまとまっています。

https://github.com/visjs/vis-network/tree/master/examples/network

4. vis-networkの公式Doc

https://visjs.github.io/vis-network/docs/network/

まとめ

vis-networkは、簡単にNetwork図を作成できるので、おすすめです。

個人で、Blogもやっています、よかったら見てみてください。

https://masanyon.com/

注意事項

この記事は、AIQ 株式会社の社員による個人の見解であり、所属する組織の公式見解ではありません。

求む、冒険者!

AIQ株式会社では、一緒に働いてくれるエンジニアを絶賛、募集しております🐱🐹✨

詳しくは、Wantedly (https://www.wantedly.com/companies/aiqlab)を見てみてください。

参考・引用

https://zenn.dev/manase/scraps/9bec750d2be485

https://www.npmjs.com/package/vis-network

https://www.npmjs.com/package/vis-data

https://www.npmjs.com/package/@types/vis

https://visjs.github.io/vis-network/examples/

https://codesandbox.io/examples/package/vis-network-react

https://visjs.github.io/vis-network/docs/network/

https://github.com/visjs/vis-network

https://github.com/visjs/vis-network/tree/master/examples/network

https://zenn.dev/manase/scraps/3b9c1c307d5a98

https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID

AIQ Tech Blog (有志)

Discussion