ELK
@joint/layout-elk lays out JointJS graphs with the Eclipse Layout Kernel (ELK), through its JavaScript port elkjs. The package exposes the layout() function, the createWorkerElk() helper for running ELK in a Web Worker, and toNativeElkNode().
See the learn section for installation steps and examples. The ELK layout options themselves (elk.algorithm, elk.direction, ...) are documented in the ELK option reference.
Functions
createWorkerElk()
createWorkerElk(createWorker: () => Worker): WorkerElk;
Returns an ELK instance that runs ELK in a Web Worker, to pass to layout() as the elk option. createWorker starts the worker, running the package's worker script, @joint/layout-elk/worker:
const elk = createWorkerElk(() => new Worker(
new URL('@joint/layout-elk/worker', import.meta.url),
{ type: 'module' }
));
The worker is started on the first layout and then lays out every graph, one at a time. If a layout is aborted while the worker is busy with it, or the worker crashes, the worker is terminated and a new one takes over the layouts still waiting. If the worker fails to load or start, every waiting layout is rejected and the next layout tries again with a new worker. ELK never falls back to the main thread.
layout()
layout(graphOrCells: dia.Graph | dia.Cell[], options?: LayoutOptions): Promise<LayoutResult>;
Lays out the provided JointJS graph, or only the given cells, and applies the result: element positions and sizes, port and port label positions, link vertices, anchors and label positions. The result is applied in a single 'layout' batch on the graph. The returned promise resolves with a LayoutResult.
When an array of cells is given, only those elements are laid out, and a link only if both of the elements it connects are in the array too. An element whose parent is not in the array is laid out as a top-level element. Top-level elements, each container's children, and the links are sent to ELK in the order of the array. A cell listed more than once is laid out once.
The promise rejects if the layout is aborted with the signal option, if ELK fails, or if two cells, or two ports of one element, have ids that differ only in type (5 and '5'). ELK ids are strings, and the ELK graph uses the ids converted to strings, so such ids are not unique to ELK.
Options
elk
elk?: WorkerElk | ELK;
The ELK instance to lay out with - an instance returned by createWorkerElk(), an elkjs instance, or any object with layout() and terminate() methods (see WorkerElk). It is handed the ELK graph with every layout option written out as a string. The package never terminates it.
Defaults to a shared instance running on the main thread, loaded on the first layout that needs it.
elkLayoutOptions
elkLayoutOptions?: ElkLayoutOptions;
The ELK layout options of the root of the ELK graph. ELK does not pass them down to containers - set the options of a container in exportElement.
The package is optimized mostly for the layered algorithm, which is the default. Other algorithms may ignore ports, port sides or label placement.
Values can be given in ELK's string spelling or in the JavaScript spelling, which is converted for you: numbers and booleans, a dia.Sides for paddings and margins, { x, y } for points, and an array of values for sets of enum values. See Option values.
The package fills in the following defaults. The options you pass take precedence.
| Option | Default |
|---|---|
elk.algorithm | 'layered' |
elk.hierarchyHandling | 'INCLUDE_CHILDREN' |
elk.layered.considerModelOrder.portModelOrder | 'true' |
elk.json.edgeCoords | 'ROOT' |
elk.json.shapeCoords | 'PARENT' |
The default import applies link routes as graph-absolute and element positions as relative to their container, as these last two options make ELK return them. Override either, and the routes and positions are misplaced unless you apply them yourself with setLinkAttributes and setElementAttributes.
exportElement
exportElement?: ExportElementCallback;
Called for every element, with the ElkNodeDraft built for it. Change the draft in place - e.g. to set layoutOptions of that node, or to give ELK a size other than the element's own. Return false to leave the element out of the layout, together with everything embedded in it and every link connected to any of them.
type ExportElementCallback = (params: {
element: dia.Element;
elkNode: ElkNodeDraft;
}) => void | false;
exportLink
exportLink?: ExportLinkCallback;
Called for every link that is laid out, with the ElkEdgeDraft built for it. Return false to leave the link out of the layout - it keeps the route it already has.
type ExportLinkCallback = (params: {
link: dia.Link;
elkEdge: ElkEdgeDraft;
}) => void | false;
exportLinkLabel
exportLinkLabel?: ExportLinkLabelCallback;
Called for every label of a link that is laid out, with the ElkLabelDraft built for it. The draft starts with the label's size and { 'elk.edgeLabels.inline': true }. Return false to leave the label out of the layout.
type ExportLinkLabelCallback = (params: {
link: dia.Link;
labelIndex: number;
elkEdgeLabel: ElkLabelDraft;
}) => void | false;
exportPort
exportPort?: ExportPortCallback;
Called for every port of an element that is laid out, with the ElkPortDraft built for it - e.g. to set elk.port.side. Return false to leave the port out of the layout, together with every link connected to it.
type ExportPortCallback = (params: {
portId: dia.Cell.ID;
element: dia.Element;
elkPort: ElkPortDraft;
}) => void | false;
exportPortLabel
exportPortLabel?: ExportPortLabelCallback;
Called for every port that is laid out, with an ElkLabelDraft for its label. The draft starts at 0 by 0, which leaves the port without a label in the ELK graph - set its width and height to have ELK place the label. Return false to leave the label out.
type ExportPortLabelCallback = (params: {
portId: dia.Cell.ID;
element: dia.Element;
elkPortLabel: ElkLabelDraft;
}) => void | false;
setElementAttributes
setElementAttributes?: SetElementAttributesCallback;
Applies the computed attributes of an element, in place of the default element.set(attributes). position is graph-absolute. size is present only when ELK laid the element out at a size other than its own - a container, or an element resized by the algorithm.
type SetElementAttributesCallback = (params: {
element: dia.Element;
attributes: {
position: dia.Point;
size?: dia.Size;
};
elkNode: ElkNode;
}) => void;
setLinkAttributes
setLinkAttributes?: SetLinkAttributesCallback;
Applies the computed attributes of a link, in place of the default link.set(attributes). source and target are present only for an end connected to the element itself rather than to a port - the end's existing properties with a new anchor. labels is present only when the link has labels - the whole labels array, with the position of each laid out label replaced.
type SetLinkAttributesCallback = (params: {
link: dia.Link;
attributes: {
vertices: dia.Point[];
source?: dia.Link.EndJSON;
target?: dia.Link.EndJSON;
labels?: dia.Link.Label[];
};
elkEdge: ElkExtendedEdge;
}) => void;
setPortAttributes
setPortAttributes?: SetPortAttributesCallback;
Applies the computed attributes of a port, in place of the default element.portProp(portId, attributes). position.args is the port's center relative to the element, read by the absolute port layout. label.position.args is present only when ELK placed the port's label - an offset from the port's center, read by the manual port label layout. hasMoved tells whether ELK moved the port from where it was exported.
type SetPortAttributesCallback = (params: {
element: dia.Element;
portId: dia.Cell.ID;
attributes: {
position: { args: dia.Point };
label?: { position: { args: dia.Point } };
};
elkPort: ElkPort;
hasMoved: boolean;
}) => void;
signal
signal?: AbortSignal;
Aborts the layout. layout() then rejects with the signal's reason, and nothing is applied to the graph. A layout a createWorkerElk() instance is busy with is stopped by terminating the worker. A layout on the main thread, or in any other elk instance without a terminate() method, keeps running - only its result is ignored.
toNativeElkNode()
toNativeElkNode(node: ElkNode): NativeElkNode;
Returns a copy of an ELK graph with every layout option written out as the string ELK takes - the node's own options, and those of its labels, ports, edges and children, at every level. The graph passed in is left as it is. layout() does this for you - use it for an ELK graph you build yourself.
Types
ElkEdgeDraft
interface ElkEdgeDraft {
readonly id: string;
layoutOptions: ElkEdgeLayoutOptions;
}
The ELK edge handed to exportLink. id is the link's id, with any \ and : escaped with a \.
ElkLabelDraft
interface ElkLabelDraft {
x?: number;
y?: number;
width: number;
height: number;
layoutOptions: ElkLabelLayoutOptions;
}
The ELK label handed to exportLinkLabel and exportPortLabel. Only its size matters to ELK - the package gives every label a placeholder text.
ElkNodeDraft
interface ElkNodeDraft {
readonly id: string;
x?: number;
y?: number;
width: number;
height: number;
layoutOptions: ElkNodeLayoutOptions;
labels?: ElkLabelDraft[];
}
The ELK node handed to exportElement.
id- the element's id, with any\and:escaped with a\.width,height- the element's size, or0for a container, whose size ELK computes from its content.layoutOptions-{ 'elk.portConstraints': 'FIXED_POS' }for an element with ports, so ELK keeps the ports where JointJS places them, and empty otherwise.labels- none. JointJS element labels are not sent to ELK. Add a sized label, together with theelk.nodeLabels.placement,elk.nodeSize.constraintsandelk.nodeSize.minimumoptions, to have ELK reserve room for it - see Element labels.
ElkPortDraft
interface ElkPortDraft {
readonly id: string;
x: number;
y: number;
width: number;
height: number;
layoutOptions: ElkPortLayoutOptions;
}
The ELK port handed to exportPort. id is <element id>:<port id>, with any \ and : in either escaped with a \. x, y, width and height are the port's rectangle relative to the element, as JointJS computes it.
LayoutOptions
interface LayoutOptions {
elk?: WorkerElk | ELK;
elkLayoutOptions?: ElkLayoutOptions;
signal?: AbortSignal;
exportElement?: ExportElementCallback;
exportPort?: ExportPortCallback;
exportPortLabel?: ExportPortLabelCallback;
exportLink?: ExportLinkCallback;
exportLinkLabel?: ExportLinkLabelCallback;
setElementAttributes?: SetElementAttributesCallback;
setPortAttributes?: SetPortAttributesCallback;
setLinkAttributes?: SetLinkAttributesCallback;
}
See Options.
LayoutResult
interface LayoutResult {
bbox: g.Rect;
elkGraph: ElkNode;
}
bbox is the tight bounding box of the laid out cells. elkGraph is the raw ELK layout result, for anything the package does not apply itself, such as junction points.
Option types
ElkLayoutOptions types the options of the root, and ElkNodeLayoutOptions, ElkPortLayoutOptions, ElkEdgeLayoutOptions and ElkLabelLayoutOptions those of each kind of ELK object. They accept both spellings of an option value. Each has two counterparts:
Strict*(e.g.StrictElkLayoutOptions) - the JavaScript spelling alone.Native*(e.g.NativeElkLayoutOptions) - ELK's string spelling alone, astoNativeElkNode()produces it.
WorkerElk
interface WorkerElk {
layout(graph: ElkNode, options?: { signal?: AbortSignal }): Promise<ElkNode>;
terminate(): void;
}
Returned by createWorkerElk(). layout() lays out an ELK graph in the worker - aborting signal rejects it, and terminates the worker if it is busy with that layout. terminate() terminates the worker and rejects every layout not settled yet. A later layout starts a new worker.
Any object of this shape can be passed as the elk option. layout() hands it the abort signal, rather than only ignoring a late result.