Building a simple diagram by using Elkjs and React FlowIn 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.
Building a simple diagram by using Elkjs and React Flow
Oct 21 2022 | by Anton Shaleynikov

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 beusing 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:

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

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.

Each Node and Edge should have a unique identifier

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)
  • graph (obligatory) ELK JSON format
  • options - object configuration (if you need it)

Terms used in ELKjs:

  • Graph: a set of nodes and edges and everything that is related to them (labels, ports, etc.).
  • Node:
  1. Simple Node - a node that does not contain child nodes.
  2. Hierarchical Node - a node that contains child nodes.
  • Edge:
  1. 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
  2. Hierarchical Edge:
  3. 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.
  4. 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

Latest news
News
Dashbouquet Team Heads to Websummit
This premier tech conference brings together the brightest minds in the industry!
Dashbouquet Team Heads to Websummit
News
5-Star Review on Clutch: A Testament to Our Exceptional Service
We're thrilled to share a glowing 5-star review from our client at N-Drip, a precision irrigation company. They praised our team's exceptional work, highlighting our seamless integration into their team and our dedication to delivering high-quality solutions.
5-Star Review on Clutch: A Testament to Our Exceptional Service
Software Development
Dashbouquet Development Recognized as a Clutch Global Leader for Spring 2024
Dashbouquet Development named a top B2B company for Custom Web and Mobile development services
Dashbouquet Development Recognized as a Clutch Global Leader for Spring 2024