フローチャートや組織図など複雑なチャートを描くのは大変です。特に、インタラクティブな操作や編集が必要な場合は、専用のライブラリを使わないと実現が難しいでしょう。
今回はReactアプリの中で使えるReact Flowを試してみました。
Monacaアプリのベース
今回はFramework7 × Reactのテンプレートを使っています。
そして、追加でReact Flowを使うためのライブラリをインストールします。
コマンドラインで以下のように入力します。
npm install reactflow
次に、React Flowを使うためのセットアップです。ライブラリとCSSをインポートする必要があります。以下のようにインポートします。
import ReactFlow from "reactflow";
import "reactflow/dist/style.css";
あとは、ノードとエッジ(線)を定義して、React Flowコンポーネントに渡すだけです。
例えば、以下のようにノードとエッジを定義します。
これを React Flow コンポーネントに渡せば、あっという間にフローチャートが描画されます。
const initialNodes = [
{ id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
{ id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];
ここで、initialNodes
はフローチャートのノード(頂点)を表しています。各ノードには、一意のid
、画面上の位置を指定するposition
、そしてノードに表示するラベルを含むdata
オブジェクトを指定します。
一方、initialEdges
はノード間を結ぶエッジ(線)を表しています。各エッジには、一意のid
、エッジの始点となるノードのid
を指定するsource
、エッジの終点となるノードのid
を指定するtarget
を指定します。
以下のコードは、React FlowをReactアプリに組み込む例です。
import React from "react";
import { Page } from "framework7-react";
import ReactFlow from "reactflow";
import "reactflow/dist/style.css";
const initialNodes = [
{ id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
{ id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];
const HomePage = () => {
return (
<Page name="home">
<div style={{ width: "100vw", height: "100vh" }}>
<ReactFlow nodes={initialNodes} edges={initialEdges} />
</div>
</Page>
);
};
export default HomePage;
このコードでは、まず必要なモジュールをインポートしています。ReactはReactライブラリ自体、PageはFramework7のページコンポーネント、ReactFlowはReact Flowライブラリ、そしてreactflow/dist/style.cssはReact Flowのスタイルシートです。
次に、initialNodesとinitialEdgesを定義しています。これらは、前の例と同じようにノードとエッジの初期設定です。
そして、HomePageコンポーネントを定義しています。このコンポーネントは、Framework7のPageコンポーネントを使用し、その中にdiv要素を配置しています。このdiv要素のスタイルは、幅と高さを100vw(ビューポート幅)と100vh(ビューポート高さ)に設定し、フローチャートが画面全体に表示されるようにしています。
div要素の中には、ReactFlowコンポーネントが配置されており、nodesとedgesのプロパティに、先ほど定義したinitialNodesとinitialEdgesを渡しています。これにより、定義したノードとエッジに基づいてフローチャートが描画されます。
最後に、HomePageコンポーネントをエクスポートしています。これにより、他のファイルからこのコンポーネントを使用できるようになります。
線をつなげる、ノードを動かす
React Flowでは、ノードを動かしたり、ノード同士を線で結んだりするために、useNodesState
と useEdgesState
という特別な関数を使います。これらの関数は、ノードと線の状態を管理してくれます。
以下のコードを見てみましょう。
import React, { useCallback } from "react";
import { Page } from "framework7-react";
import ReactFlow, { useNodesState, useEdgesState, addEdge } from "reactflow";
import "reactflow/dist/style.css";
const HomePage = () => {
const initialNodes = [
{ id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
{ id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges],
);
return (
<Page name="home">
<div style={{ width: "100vw", height: "100vh" }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
/>
</div>
</Page>
);
};
export default HomePage;
まず、initialNodes
とinitialEdges
で、最初のノードと線を定義しています。これは前の例と同じですね。
次に、useNodesState
とuseEdgesState
を使って、ノードと線の状態を管理します。これらの関数は、現在のノードと線の状態(nodes
とedges
)、それらを更新するための関数(setNodes
とsetEdges
)、そしてノードや線が変更されたときに呼ばれるコールバック関数(onNodesChange
とonEdgesChange
)を返します。
onConnect関数は、新しい線が引かれたときに呼び出されます。この関数は、新しい線の情報をparamsとして受け取ります。
setEdges関数は、現在の線の状態を更新するための関数です。この関数は、現在の線の状態edsを受け取ります。
onConnect関数の中では、setEdges関数を使って、現在の線の状態edsに新しい線の情報paramsを追加しています。これは、addEdge関数を使って行われます。
最後に、ReactFlow
コンポーネントに、nodes
、edges
、onNodesChange
、onEdgesChange
、onConnect
を渡しています。これにより、React Flowは現在のノードと線の状態を知ることができ、ノードや線が変更されたときに適切に反応することができます。
このようにして、React Flowでノードを動かしたり、線で結んだりすることができるのです。少しコードが複雑に見えるかもしれませんが、基本的なアイデアは簡単です。ノードと線の状態を管理し、それらが変更されたときに適切に反応するだけなのです。
より複雑な例
ノードとエッジを追加すれば、より複雑なフローチャートを描くことができます。以下のコードを見てみましょう。
import React, { useCallback } from "react";
import { Page } from "framework7-react";
import ReactFlow, { useNodesState, useEdgesState, addEdge } from "reactflow";
import "reactflow/dist/style.css";
const initialNodes = [
{
id: "horizontal-1",
sourcePosition: "right",
type: "input",
data: { label: "Input" },
position: { x: 0, y: 80 },
},
{
id: "horizontal-2",
sourcePosition: "right",
targetPosition: "left",
data: { label: "A Node" },
position: { x: 250, y: 0 },
},
{
id: "horizontal-3",
sourcePosition: "right",
targetPosition: "left",
data: { label: "Node 3" },
position: { x: 250, y: 160 },
},
{
id: "horizontal-4",
sourcePosition: "right",
targetPosition: "left",
data: { label: "Node 4" },
position: { x: 500, y: 0 },
},
{
id: "horizontal-5",
sourcePosition: "top",
targetPosition: "bottom",
data: { label: "Node 5" },
position: { x: 500, y: 100 },
},
{
id: "horizontal-6",
sourcePosition: "bottom",
targetPosition: "top",
data: { label: "Node 6" },
position: { x: 500, y: 230 },
},
{
id: "horizontal-7",
sourcePosition: "right",
targetPosition: "left",
data: { label: "Node 7" },
position: { x: 750, y: 50 },
},
{
id: "horizontal-8",
sourcePosition: "right",
targetPosition: "left",
data: { label: "Node 8" },
position: { x: 750, y: 300 },
},
];
const initialEdges = [
{
id: "horizontal-e1-2",
source: "horizontal-1",
type: "smoothstep",
target: "horizontal-2",
animated: true,
},
{
id: "horizontal-e1-3",
source: "horizontal-1",
type: "smoothstep",
target: "horizontal-3",
animated: true,
},
{
id: "horizontal-e1-4",
source: "horizontal-2",
type: "smoothstep",
target: "horizontal-4",
label: "edge label",
},
{
id: "horizontal-e3-5",
source: "horizontal-3",
type: "smoothstep",
target: "horizontal-5",
animated: true,
},
{
id: "horizontal-e3-6",
source: "horizontal-3",
type: "smoothstep",
target: "horizontal-6",
animated: true,
},
{
id: "horizontal-e5-7",
source: "horizontal-5",
type: "smoothstep",
target: "horizontal-7",
animated: true,
},
{
id: "horizontal-e6-8",
source: "horizontal-6",
type: "smoothstep",
target: "horizontal-8",
animated: true,
},
];
const HomePage = () => {
const [nodes, _, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), []);
return (
<Page name="home">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
attributionPosition="bottom-left"
></ReactFlow>
</Page>
);
};
export default HomePage;
このコードでは、まずinitialNodes
とinitialEdges
で複数のノードとエッジを定義しています。各ノードはid
、sourcePosition
(エッジの出る位置)、targetPosition
(エッジの入る位置)、data
(ノードに表示するデータ)、position
(ノードの位置)などのプロパティを持っています。
エッジはid
、source
(始点のノードID)、target
(終点のノードID)、animated
(アニメーションの有無)などのプロパティを持っています。
HomePage
コンポーネントの中では、前の例と同様にuseNodesState
とuseEdgesState
を使ってノードとエッジの状態を管理しています。onConnect
関数も同様に定義されています。
ReactFlow
コンポーネントには、nodes
、edges
、onNodesChange
、onEdgesChange
、onConnect
に加えて、fitView
(全てのノードが画面内に収まるようにズームとパンを調整する)とattributionPosition
(帰属情報の位置)が指定されています。
このコードを実行すると、initialNodes
とinitialEdges
で定義されたノードとエッジを持つ、より複雑なフローチャートが表示されます。ユーザーはノードをドラッグしたり、ノード間に新しいエッジを引いたりすることができます。
このように、React Flowを使うと、複数のノードとエッジを定義することで、より複雑で対話的なフローチャートを簡単に作成することができるのです。
他の機能
React Flowには、他にもミニマップの表示や拡大・縮小などのコントロールを表示する機能があります。
fitViewプロパティを追加すると、フローチャートが画面内に収まるように自動的にサイズが調整されます。これは、フローチャートが画面からはみ出ることを防ぎ、常に全体が見えるようにしてくれます。
これらの機能を使うことで、フローチャートをより見やすく、操作しやすくすることができます。
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
} from "reactflow";
// 描画する際の指定
<ReactFlow
nodes={nodes}
edges={edgesWithUpdatedTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={onInit}
fitView
attributionPosition="top-right"
nodeTypes={nodeTypes}
>
<MiniMap style={minimapStyle} zoomable pannable />
<Controls />
<Background color="#aaa" gap={16} />
</ReactFlow>
まとめ
React Flowを使うと、複雑な表示のフローチャートをコードで簡単に描けます。さらにドラッグ&ドロップやノード同士の連結なども管理が容易です。編集結果をデータベースに保存すれば、ユーザーが自由にフローチャートを作成・共有できるアプリケーションも作れるでしょう。
ぜひReact Flowを使って、MonacaアプリのUIを充実させてみてください。