Создание простой диаграммы с использованием Elkjs и React Flow
25 октября 2022 г.В этой статье мы рассмотрим процесс построения диаграммы с помощью библиотек Elkjs и React Flow. Мы надеемся, что наше руководство окажется для вас полезным и прольет свет на использование React Flow и Elksj.
Мы будем использовать React Flow (библиотека для создания приложений на базе узлов) для визуализации (рендеринга). И мы будем использовать Elkjs a> для вычисления позиций элементов. Прежде чем приступить к фактическому процессу построения диаграммы, давайте сначала рассмотрим основные понятия и терминологию, которые мы будем использовать в этой статье.
Зависимости, необходимые для проекта:
- отреагировать
- реагировать
- рендерер реактивного потока
- ЭлкДжс
Во-первых, нам нужно создать пустой проект React и установить вышеупомянутые зависимости.
Основные термины, используемые в React-flow:
- Узел — перетаскиваемый элемент, который можно соединить с другими узлами.
- Edge — соединение между двумя узлами.
- Handles – своего рода порт узла, который используется для соединения узлов. Вы начинаете соединение с одного дескриптора, а заканчиваете другим.
Возвращаясь к исходной теме, скажем, нам нужно построить следующую диаграмму:
Диаграмма состоит из восьми узлов трех типов:
* круг - элементы, которые используются для обозначения начала и конца (круговой узел) * прямоугольник - элементы, которые используются для описания определенного процесса (rectangleNode) * ромб - элемент выбора (rhombusNode)
n Теперь давайте создадим файл Flow.js, в котором мы опишем компонент Flow. Он будет отвечать за отрисовку элементов диаграммы.
Мы импортируем компонент
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 — макет (график, параметры)
Термины, используемые в 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.
:::информация Также опубликовано здесь. р>
:::
Оригинал