Создание простой диаграммы с использованием Elkjs и React Flow

Создание простой диаграммы с использованием Elkjs и React Flow

25 октября 2022 г.

В этой статье мы рассмотрим процесс построения диаграммы с помощью библиотек Elkjs и React Flow. Мы надеемся, что наше руководство окажется для вас полезным и прольет свет на использование React Flow и Elksj.

Мы будем использовать React Flow (библиотека для создания приложений на базе узлов) для визуализации (рендеринга). И мы будем использовать Elkjs для вычисления позиций элементов. Прежде чем приступить к фактическому процессу построения диаграммы, давайте сначала рассмотрим основные понятия и терминологию, которые мы будем использовать в этой статье.

Зависимости, необходимые для проекта:

Во-первых, нам нужно создать пустой проект React и установить вышеупомянутые зависимости.

Основные термины, используемые в React-flow:

Возвращаясь к исходной теме, скажем, нам нужно построить следующую диаграмму:

Диаграмма состоит из восьми узлов трех типов:

* круг - элементы, которые используются для обозначения начала и конца (круговой узел) * прямоугольник - элементы, которые используются для описания определенного процесса (rectangleNode) * ромб - элемент выбора (rhombusNode)

n Теперь давайте создадим файл Flow.js, в котором мы опишем компонент Flow. Он будет отвечать за отрисовку элементов диаграммы.

Мы импортируем компонент из 'react-flow-renderer'

.
import ReactFlow from 'react-flow-renderer';

function Flow() {
 return (
   <ReactFlow
     nodes={nodes}
     edges={edges}
  />

 );
}

Чтобы все работало правильно, вам нужно будет передать узлы и ребра в .

Ниже вы можете увидеть исходные данные, которые мы будем использовать в нашем приложении (файл initialData.js)

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"
 }
];

Каждый узел и край должны иметь уникальный идентификатор. Узлу также потребуются позиция и данные.

Для Edge обязательными параметрами также будут источник и цель.

Дополнительную информацию о параметрах можно найти здесь: Узел , Параметры Edge.

Так как на схеме используются элементы разной формы и размера, расположение узлов по умолчанию не подойдет. Чтобы создать узел с пользовательскими настройками, мы воспользуемся функцией React Flow — Custom Node. Поскольку мы уже определили три типа элементов, мы создадим три компонента Custom Node в отдельных файлах:

* 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;

Мы будем использовать компонент Handle для соединения пользовательского узла с другими узлами.

Затем мы добавим новые типы узлов в свойства nodeTypes.

const nodeTypes = {
 circleNode: CircleNode,
 rectangleNode: RectangleNode,
 rhombusNode: RhombusNode
};

Важно, чтобы типы узлов запоминались useMemo или определялись вне компонента. В нашем случае мы определили их вне компонента. В противном случае React будет создавать новый объект при каждом рендеринге, что приведет к проблемам с производительностью и ошибкам.

На данном этапе компонент Flow.jsx выглядит примерно так:

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;

Файл со стилями .css:

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;
}

Как выглядит диаграмма:

Перейдем к интеграции с Elkjs. Для этого добавим автоматический расчет позиций элементов диаграммы и будем передавать полученные значения в React-поток.

Библиотека Elkjs представляет собой отдельный объект ELK. В ELK есть конструктор, который можно использовать для создания:

  • новый ELK (опции) - опции - не обязательны

Один из методов ELK — макет (график, параметры)

  • график (обязательно) ELK JSON формат
  • options — конфигурация объекта (если вам это нужно)

Термины, используемые в ELKjs:

  • График: набор узлов и ребер и все, что с ними связано (метки, порты и т. д.).
  • Узел:
  • Простой узел — узел, не содержащий дочерних узлов.
  • Иерархический узел — узел, содержащий дочерние узлы.
  • Край:
  • Простой край: соединяет два узла в один простой граф. Подразумевается, что исходный и целевой узлы ребра имеют один и тот же родительский узел..
  • Иерархический край:
  • Короткое иерархическое ребро: Иерархическое ребро, которое выходит (или входит) только в один Иерархический узел, чтобы достичь своей цели. Таким образом, короткий Иерархический край соединяет узлы в соседних слоях иерархии.
  • Длинный иерархический край: иерархический край, который не является коротким иерархическим краем.
  • Порт: очевидная точка соединения на узле, к которому подключаются ребра.
  • Корневой узел графа: минимальный общедоступный элемент-предок всех узлов графа.

Подробнее о структуре здесь: Структура данных графика

Графический формат JSON состоит из пяти основных элементов: узлов, портов, меток, ребер и секций ребер.

Все элементы, кроме меток, должны иметь уникальный идентификатор (строку или целое число).

Узлы, порты и метки имеют координаты (x, y) и размеры (ширина, высота)

Графом можно назвать простой узел, дочерними элементами которого являются узлы верхнего уровня графа. Вот почему в графе дочерние элементы (потомки) отвечают за передачу пакета объектов (основанных на начальных узлах) с особенностями

идентификатор, ширина и высота. Поскольку размеры пользовательских узлов различаются, мы добавили проверку node.type.

Файл graph.js

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) — возвращает Promise. В случае его успешного выполнения мы получим Graph.

Для нашей диаграммы:

Теперь нам нужно передать полученные координаты в React Flow. Новые узлы — это набор объектов с набором признаков исходных узлов и дополнительной «позицией» (которая получается из графа дочерних узлов по идентификатору). Процесс построения пакета описывается nodesForFlow(). Новые ребра — набор объектов graph.edges — описываются функцией edgeForFlow(). При успешном выполнении возвращенного промиса elkLayout() мы передаем полученный граф в nodesForFlow() и edgeForFlow(). Затем мы записываем результаты выполнения в состояние и используем их в качестве новых узлов и ребер для построения диаграммы.

Flow.jsx выглядит так:

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;

Результат:

Ссылка на codeandbox.

:::информация Также опубликовано здесь. р>

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE