In this article, we will look at the process of building a diagram with the help of Elkjs and React Flow libraries. We hope our tutorial will be useful for you and will shed some light on using React Flow and Elksj.
We will be using
Dependencies required for the project:
First, we need to create an empty React project and install the abovementioned dependencies.
The main terms used in React-flow:
Getting back to the initial topic, say, we need to build the following diagram:
The diagram consists of eight nodes of three types:
Now let’s create a Flow.js file where we will describe the Flow component. It will be responsible for rendering the elements of the diagram.
We will import the <ReactFlow /> component from 'react-flow-renderer'
import ReactFlow from 'react-flow-renderer';
function Flow() {
return (
<ReactFlow
nodes={nodes}
edges={edges}
/>
);
}
In order for everything to function properly, you will need to pass over nodes and edges to <ReactFlow>.
Below you can see the initial data that we will use in our app ( the initialData.js file)
export const initialNodes = [
{
id: "1",
type: "circleNode",
data: { label: "Request PTO" },
position: { x: 250, y: 25 }
},
{
id: "2",
type: "rectangleNode",
data: { label: "manager reviews data" },
position: { x: 240, y: 125 }
},
{
id: "3",
type: "rhombusNode",
data: { label: "Pending manager approval" },
position: { x: 250, y: 250 }
},
{
id: "4",
type: "rectangleNode",
data: { label: "PTO request approved" },
position: { x: 150, y: 350 }
},
{
id: "5",
type: "rectangleNode",
data: { label: "PTO request denied" },
position: { x: 400, y: 350 }
},
{
id: "6",
type: "rectangleNode",
data: { label: "Notify teammate1" },
position: { x: 150, y: 450 }
},
{
id: "7",
type: "rectangleNode",
data: { label: "Notify teammate2" },
position: { x: 400, y: 450 }
},
{
id: "8",
type: "circleNode",
data: { label: "End" },
position: { x: 250, y: 550 }
}
];
export const initialEdges = [
{
id: "e1-2",
source: "1",
target: "2"
},
{
id: "e2-3",
source: "2",
target: "3"
},
{
id: "e3-4",
source: "3",
target: "4"
},
{
id: "e3-5",
source: "3",
target: "5"
},
{
id: "e4-6",
source: "4",
target: "6"
},
{
id: "e5-7",
source: "5",
target: "7"
},
{
id: "e6-8",
source: "6",
target: "8"
},
{
id: "e7-8",
source: "7",
target: "8"
}
];
Each Node and Edge should have a unique identifier. The node will also need the position and data.
For Edge, obligatory parameters will also include source and target.
More information on the options can be found here:
Since the diagram uses elements of different forms and sizes, the layout of nodes by default will not be suitable. In order to create a node with custom settings, we will use a React Flow feature - Custom Node. As we already defined three types of elements, we will create three Custom Node components in separate files:
import { Handle, Position } from "react-flow-renderer";
const CircleNode = ({ data, id }) => {
return (
<div className="circleNode">
{data.handles[0] ? (
<Handle type="target" position={Position.Top} id={`${id}.top`} />
) : null}
{data.handles[1] ? (
<Handle type="source" position={Position.Bottom} id={`${id}.bottom`} />
) : null}
</div>
);
};
export default CircleNode;
import { Handle, Position } from "react-flow-renderer";
const RectangleNode = ({ data, id }) => {
return (
<div className="rectangleNode">
{data.handles[0] ? (
<Handle type="target" position={Position.Top} id={`${id}.top`} />
) : null}
{data.handles[1] ? (
<Handle type="source" position={Position.Bottom} id={`${id}.bottom`} />
) : null}
</div>
);
};
export default RectangleNode;
import { Handle, Position } from "react-flow-renderer";
const RhombusNode = ({ data, id }) => {
return (
<div className="handles-container">
<div className="rhombusNode"></div>
<Handle type="target" position={Position.Top} id={`${id}.top`} />
<Handle type="source" position={Position.Bottom} id={`${id}.bottom`} />
</div>
);
};
export default RhombusNode;
We will use the Handle component to connect the custom node with other nodes.
Then we will add new node types to the nodeTypes props.
const nodeTypes = {
circleNode: CircleNode,
rectangleNode: RectangleNode,
rhombusNode: RhombusNode
};
It is important that node types are remembered by useMemo or are defined outside of the component. In our case, we defined them outside of the component. Otherwise, React will be creating a new object during every rendering and that will lead to performance issues and bugs.
At this stage, the Flow.jsx component looks something like this:
import { useState } from "react";
import ReactFlow from "react-flow-renderer";
import { initialNodes, initialEdges } from "./initialData";
import CircleNode from "./CircleNode";
import RectangleNode from "./RectangleNode";
import RhombusNode from "./RhombusNode";
const nodeTypes = {
circleNode: CircleNode,
rectangleNode: RectangleNode,
rhombusNode: RhombusNode
};
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
return (
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView />
);
}
export default Flow;
The file with .css styles:
html,
body,
#root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: sans-serif;
}
.circleNode {
background-color: #02a9ea;
background-image: url("https://img.icons8.com/doodle/48/000000/sun--v1.png");
height: 50px;
width: 50px;
border-radius: 50%;
border: 1px solid black;
}
.rhombusNode {
display: block;
position: absolute;
transform: rotate(45deg);
background-color: #a600ff;
top: 10px;
right: auto;
width: 50px;
height: 50px;
border: 1px solid;
z-index: -1;
}
.handles-container {
position: relative;
background-image: url("https://img.icons8.com/color/48/000000/decision.png");
background-repeat: no-repeat;
background-position: center;
background-size: contain;
height: 70px;
width: 50px;
}
.rectangleNode {
background-color: #ffd000;
background-image: url("https://img.icons8.com/external-flaticons-lineal-color-flat-icons/45/000000/external-process-productivity-flaticons-lineal-color-flat-icons-2.png");
background-position: center;
height: 50px;
width: 70px;
background-repeat: no-repeat;
border: 1px solid black;
}
How the diagram looks:
Let’s move on to the Elkjs integration. For that, we will add the automatic calculation of positions of the diagram’s elements and will transfer obtained values to React flow.
The Elkjs library is a single ELK object. ELK has a constructor that can be used for the creation of:
One of ELK methods is - layout(graph, options)
Terms used in ELKjs:
More on the structure here: Graph Data Structure
The JSON graph format has five main elements: nodes, ports, labels, edges, and edge sections.
All elements, except for labels, should have a unique identifier (string or integer).
Nodes, ports and labels have coordinates (x, y) and sizes (width, height)
A graph can be called a simple node, the child elements of which are nodes of the upper graph level. This is why in a graph, child elements (children) are responsible for transferring a batch of objects (that are based on initialNodes) with features of
id, width and height. Since the sizes of custom Nodes differ, we added the node.type check.
The graph.js file
import ELK from "elkjs";
import { initialNodes, initialEdges } from "./initialData";
const elk = new ELK();
const elkLayout = () => {
const nodesForElk = initialNodes.map((node) => {
return {
id: node.id,
width: node.type === "rectangleNode" ? 70 : 50,
height: node.type === "rhombusNode" ? 70 : 50
};
});
const graph = {
id: "root",
layoutOptions: {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"nodePlacement.strategy": "SIMPLE"
},
children: nodesForElk,
edges: initialEdges
};
return elk.layout(graph);
};
export default elkLayout;
elk.layout(graph) - returns Promise. In case of its successful execution, we’ll get Graph.
For our diagram:
Now we need to transfer obtained coordinates to React Flow. New nodes is a batch of objects with a set of features of initialNodes and an additional “position”( that is obtained from child nodes graph by id). The process of constructing the batch is described by nodesForFlow(). New edges - a batch of graph.edges objects - is described by edgesForFlow(). Upon successful execution of returned elkLayout() Promise, we transfer the obtained graph to nodesForFlow() and edgesForFlow(). We then write down the execution results in state and use them as new nodes and edges for building a diagram.
Flow.jsx looks like this:
import { useState } from "react";
import ReactFlow from "react-flow-renderer";
import { initialNodes } from "./initialData";
import CircleNode from "./CircleNode";
import RectangleNode from "./RectangleNode";
import RhombusNode from "./RhombusNode";
import elkLayout from "./graph";
const nodeTypes = {
circleNode: CircleNode,
rectangleNode: RectangleNode,
rhombusNode: RhombusNode
};
function Flow() {
const [nodes, setNodes] = useState(null);
const [edges, setEdges] = useState(null);
const nodesForFlow = (graph) => {
return [
...graph.children.map((node) => {
return {
...initialNodes.find((n) => n.id === node.id),
position: { x: node.x, y: node.y }
};
})
];
};
const edgesForFlow = (graph) => {
return graph.edges;
};
elkLayout().then((graph) => {
setNodes(nodesForFlow(graph));
setEdges(edgesForFlow(graph));
});
if (nodes === null) {
return <></>;
}
return (
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView />
);
}
export default Flow;
The result:
A link to the
Also published here.