ELK
@joint/layout-elk lays out a graph with the Eclipse Layout Kernel (ELK). Beyond positioning elements, it sizes containers around their content, routes links to the exact port they connect to, and places link labels along the route.
The package works on the JointJS graph, so in @joint/react you call it exactly as in plain JointJS - the ELK layout guide covers its options and callbacks in full. This page shows the React side.
A complete example
The same diagram as the first example in the ELK layout guide, with React cell rendering: two containers, services connected through ports, and labeled links - port to port, port to element, and container to container. Nothing has a position to begin with, and the layout runs with the package defaults and a few ELK options - no callbacks. ELK places every element, sizes each container to fit its children, and routes the links to the exact ports they connect to.
Every element has an explicit size and renders with <HTMLHost useModelGeometry>, so the cards fill whatever size the layout gives them - containers included.
Running the layout
Elements with explicit sizes - rendered with useModelGeometry, as in the demo - need no measuring, so the layout can run as soon as the paper exists. layout() is asynchronous: freeze the paper before anything is drawn, and unfreeze it once the result is applied, so the elements render straight at their final positions:
import { useLayoutEffect } from 'react';
import { useGraph, usePaper } from '@joint/react';
import { layout } from '@joint/layout-elk';
function Layout() {
const { graph } = useGraph();
const { paper } = usePaper();
useLayoutEffect(() => {
if (!paper) return;
const controller = new AbortController();
paper.freeze();
layout(graph, {
signal: controller.signal,
elkLayoutOptions: { 'elk.direction': 'RIGHT' }
}).then(() => {
paper.unfreeze();
paper.transformToFitContent({ padding: 24, useModelGeometry: true });
}).catch((error) => {
if (error.name !== 'AbortError') throw error;
});
// Drop a layout still running when the component unmounts.
return () => controller.abort();
}, [graph, paper]);
return null;
}
When nodes measure themselves instead (HTMLHost without useModelGeometry), their sizes only exist once they render - run the layout from useOnElementsMeasured, as described in How a layout runs.
The result is written onto the same models @joint/react renders - element positions and sizes, port positions, link vertices and label positions - so the diagram updates on its own.
Two things are specific to @joint/react:
-
Link labels from
labelMaphave no size, and ELK keeps clear only the area a label'ssizegives it. Set one in the link record'sdefaultLabel, as the demo does:{
id: 'webapp-api',
type: 'link',
source: { id: 'webapp', port: 'out' },
target: { id: 'api', port: 'in1' },
labelMap: { name: { text: 'request' } },
defaultLabel: { size: { width: 70, height: 20 } },
} -
Ports from
portMapstay where theircx/cyput them - the package default. To let ELK move them, see Ports in the guide. AllportMapports belong to one port group, so inexportPortpick each port's side by its id or your data, not by its group.
Running ELK in a Web Worker
By default, ELK runs on the main thread and blocks the page while it computes - unnoticeable for a small graph, but up to seconds for a large one. To keep the page responsive, run it in a Web Worker: create one with createWorkerElk() and pass it to layout() as the elk option.
In React, the only thing to take care of is the worker's lifetime. Create it once per component rather than on every render, share it between layouts, and terminate it on unmount:
import { useEffect, useLayoutEffect, useMemo } from 'react';
import { useGraph, usePaper } from '@joint/react';
import { layout, createWorkerElk } from '@joint/layout-elk';
function Layout() {
const { graph } = useGraph();
const { paper } = usePaper();
// The worker starts on the first layout, not here.
const elk = useMemo(() => createWorkerElk(() => new Worker(
new URL('@joint/layout-elk/worker', import.meta.url),
{ type: 'module' }
)), []);
// A terminated instance starts a new worker on its next layout, so this is
// safe under React Strict Mode too.
useEffect(() => () => elk.terminate(), [elk]);
useLayoutEffect(() => {
if (!paper) return;
const controller = new AbortController();
paper.freeze();
layout(graph, {
elk,
signal: controller.signal,
elkLayoutOptions: { 'elk.direction': 'RIGHT' }
}).then(() => {
paper.unfreeze();
paper.transformToFitContent({ padding: 24, useModelGeometry: true });
}).catch((error) => {
if (error.name !== 'AbortError') throw error;
});
return () => controller.abort();
}, [graph, paper, elk]);
return null;
}
Aborting a layout the worker is busy with terminates the worker, and a new one takes over the next layout. How to start the worker script depends on your bundler - see Running ELK in a Web Worker in the guide. The demo above runs ELK on the main thread, because a worker can't start inside the sandbox.
Installation
- npm
- pnpm
- yarn
npm add @joint/layout-elk
pnpm add @joint/layout-elk
yarn add @joint/layout-elk
Everything else - algorithms, options, callbacks, limitations - is in the ELK layout guide and the API reference.