paint-brush
Creating a Simple Diagram by Using Elkjs and React Flowby@dashmagazine
6,244 reads
6,244 reads

Creating a Simple Diagram by Using Elkjs and React Flow

by Dashbouquet DevelopmentOctober 25th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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 shed some light on using React Flow and Elksj. The main terms used in React-flow: Node — a draggable unit that can be connected to other nodes. Edge — a connection between two nodes. Handles — a sort of a node port that is used for connecting nodes. You start the connection with one handle and end it with another.

Company Mentioned

Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Creating a Simple Diagram by Using Elkjs and React Flow
Dashbouquet Development HackerNoon profile picture

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 React Flow (a library for creating apps based on nodes) for visualization (rendering). And we will use Elkjs for calculating the elements’ positions. Before starting the actual process of building a diagram, let’s first review the main concepts and terminology that we will be using in this article.


Dependencies required for the project:

  • react
  • react-dom
  • react-flow-renderer
  • elkjs


First, we need to create an empty React project and install the abovementioned dependencies.


The main terms used in React-flow:

  • Node — a draggable unit that can be connected to other nodes.
  • Edge — a connection between two nodes.
  • Handles —  a sort of a node port that is used for connecting nodes. You start the connection with one handle and end it with another.


Getting back to the initial topic, say, we need to build the following diagram:

The diagram consists of eight nodes of three types:


  • a circle - elements that are used to indicate start and end (circleNode)
  • a rectangle - elements that are used to describe a certain process (rectangleNode)
  • a rhombus - an element of choice (rhombusNode)


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:  Node, Edge Options.


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:


  • CircleNode.jsx
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;
  • RectangleNode.jsx
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;
  • RhombusNode.jsx
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:

  • new ELK (options) - options - not obligatory

One of ELK methods is -  layout(graph, options)


Terms used in ELKjs:

  • Graph: a set of nodes and edges and everything that is related to them (labels, ports, etc.).
  • Node:
  • Simple Node - a node that does not contain child nodes.
  • Hierarchical Node - a node that contains child nodes.
  • Edge:
  • Simple Edge: connects two nodes in one simple graph. It is implied that the edge’s source and target nodes both have the same parent node..
  • Hierarchical Edge:
    • Short Hierarchical Edge:  a Hierarchical Edge that exits (or enters) only one Hierarchical Node in order to get to its goal. In this way, a short Hierarchical Edge connects nodes in nearby hierarchy layers.
    • Long Hierarchical Edge: a hierarchical edge that is not a short hierarchical edge.
  • Port: an obvious point of connection on a node to which edges connect.
  • Root Node of a Graph: a minimal public ancestor element of all graph nodes.


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 codesandbox.


Also published here.