# Extend layout execution

Use a shipped layout first. Extend the registry when your domain needs an arrangement the shipped algorithms do not express, and attach a worker when layout computation must leave the main thread. The samples below render a connected graph and an editorial workflow.

Run layouts through the mounted [`DiagramEngine`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-engine) and use its registry only to add an algorithm; [How Grafloria works](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/how-grafloria-works) explains obtaining it from [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance) through `getEngine()`.

## 1. Choose a shipped layout

The engine registers its built-ins for you. You do not need to construct adapters or call [`createBuiltInLayoutAdapters`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-functions-a-t).

| Graph | Layout name | Arrangement |
| --- | --- | --- |
| Pipelines and DAGs | `elk`, `dagre`, `layered` | Layered ranking |
| Systems with zones | `architecture` | Regions composed on a grid |
| Hierarchies | `tree` | Parent-centered branches |
| Networks | `force`, `community`, `spectral` | Physical spread or clusters |
| Catalogs | `grid`, `circular`, `radial` | Uniform placement |
| No explicit choice | `auto` | Graph classification and dispatch |

Calling `engine.layout()` selects `auto`. An unknown name throws an error listing the registered names. For ordinary declarative layout and on-demand reruns, see [Lay out a diagram](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/lay-out-a-diagram).

Install the shared packages and the binding you use in your browser application:

```bash
npm install @grafloria/engine @grafloria/renderer @grafloria/element
# Angular
npm install @grafloria/angular
# Qwik
npm install @grafloria/qwik
# Vue
npm install @grafloria/vue
```

## 2. Serve layout in a module worker

Your application creates the worker; the engine does not choose a bundler or worker URL for you. [`LayoutPort`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-i-l) defines the host-side message surface, and [`serveLayout`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-functions-a-t) supplies the worker's message loop.

> **Known issue:** The documented `engine.setLayoutPort(worker)` and `serveLayout(self)` calls can fail strict TypeScript checks because the port types accept a plain `{ data }` event while browser handlers require a full `MessageEvent`. Until it is fixed, forward browser events through the typed port objects below.

Create these shared files beside your application component or entry point. Use a toolchain that bundles module workers created with `new Worker(new URL(..., import.meta.url))`.

```ts title="layout.worker.ts"
import { serveLayout, type LayoutServePort, type LayoutRequest } from '@grafloria/engine';

const port: LayoutServePort = {
  onmessage: null,
  postMessage: (message) => self.postMessage(message),
};
self.addEventListener('message', (event: MessageEvent<LayoutRequest>) => {
  port.onmessage?.({ data: event.data });
});
serveLayout(port);
```

[`LayoutServePort`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-l-u) and [`LayoutRequest`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-types) type the worker side. [`LayoutResponse`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-types) types the messages returned to the host.

The worker resolves the algorithm by name in its own bundle. Registering a function in the main thread does not transfer that function to the worker.

The data uses the library's [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec). Chain edges keep all 45 nodes connected so force layout can use its interruptible path.

```ts title="graph.ts"
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';

export const nodes: NodeSpec[] = Array.from({ length: 45 }, (_, i) => ({
  id: `n${i}`,
  label: String(i),
  position: { x: (i % 9) * 90, y: Math.floor(i / 9) * 90 },
  size: { width: 40, height: 40 },
}));

export const edges: EdgeSpec[] = Array.from({ length: 44 }, (_, i) => ({
  id: `e${i}`,
  source: `n${i}`,
  target: `n${i + 1}`,
  type: 'direct',
}));
```

This shared function first applies the shipped `grid` layout, then attaches the worker and requests a long `force` run. Its progress callback requests cancellation at 10%, and its completion handler prints the returned status. The returned function aborts and waits for settlement before terminating the worker; call it during unmount.

```ts title="execution.ts"
import type { DiagramEngine, LayoutPort, LayoutResponse } from '@grafloria/engine';

export function startLayout(engine: DiagramEngine, repaint: () => void): () => void {
  const controller = new AbortController();
  const worker = new Worker(new URL('./layout.worker.ts', import.meta.url), {
    type: 'module',
  });
  const port: LayoutPort = {
    onmessage: null,
    postMessage: (message) => worker.postMessage(message),
  };
  worker.addEventListener('message', (event: MessageEvent<LayoutResponse>) => {
    port.onmessage?.({ data: event.data });
  });
  let disposed = false;

  const running = (async () => {
    await engine.layout('grid', { columns: 9 });
    if (disposed) return;
    repaint();
    engine.setLayoutPort(port);
    const result = await engine.layout('force', {
      seed: 0x5eed,
      iterations: 4000,
      threshold: 0,
      sliceMs: 0,
      signal: controller.signal,
      onProgress: (progress) => {
        console.log('Layout progress', progress.progress, progress.phase);
        if (progress.progress >= 0.1) controller.abort();
      },
    });
    console.log('Layout result', result.partial, result.reason, result.iteration);
    if (!disposed) repaint();
  })().catch((error: Error) => {
    if (!disposed) console.error(error);
  });

  return () => {
    disposed = true;
    controller.abort();
    void running.finally(() => {
      engine.setLayoutPort(undefined);
      worker.terminate();
    });
  };
}
```

Cancellation is not an exception: `layout()` resolves with a [`UnifiedLayoutResult`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-l-u), including `nodePositions`, `bounds`, `partial`, `reason`, and iteration counts. The engine commits the returned positions even when `partial` is true. Keep that picture; do not reset the nodes after an abort.

## 3. Run against the mounted diagram

The JavaScript, Angular and Vue tabs run the shipped `grid` layout on their mounted engine and show numbered nodes in five rows. The Qwik tab replaces the browser-side rule setup from [Validate port connections](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/validate-port-connections#2-mount-the-same-editor-in-your-framework) with `startLayout()` to attach the worker, report progress and cancel the run.

Add the layout call to the mounting patterns for [`render`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core), [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent), Qwik's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik) and Vue's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue) in [Edit nodes](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/edit-nodes#2-mount-the-canvas-in-your-framework).

:::code-group
```ts title="JavaScript"
import { render } from '@grafloria/element';
import { nodes, edges } from './graph';

export function mountLayout(container: HTMLElement): () => void {
  container.style.height = '400px';
  const instance = render({ nodes, edges }, container);
  void instance.getEngine().layout('grid', { columns: 9 }).then(() => {
    instance.fitView(30);
  });
  return () => {
    instance.dispose();
  };
}

const container = document.createElement('div');
document.body.append(container);
export const unmount = mountLayout(container);
// Call unmount() when your application removes this view.
```
```ts title="Angular"
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { nodes, edges } from './graph';

@Component({
  selector: 'app-layout',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block;height:400px" />
  `,
})
export class LayoutComponent implements AfterViewInit {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  nodes = nodes;
  edges = edges;

  ngAfterViewInit(): void {
    const canvas = this.canvas();
    const engine = canvas.activeEngine();
    if (engine) {
      void engine.layout('grid', { columns: 9 }).then(() => canvas.scheduleRender());
    }
  }
}
```
```tsx title="Qwik"
import { component$, noSerialize, useSignal, useVisibleTask$, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { nodes, edges } from './graph';
import { startLayout } from './execution';

export default component$(() => {
  const instance = useSignal<NoSerialize<DiagramInstance>>();

  useVisibleTask$(({ track, cleanup }) => {
    const api = track(() => instance.value);
    if (!api) return;
    cleanup(startLayout(api.getEngine(), () => {
      api.renderNow();
      api.fitView(30);
    }));
  });

  return (
    <div style={{ height: '400px' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
        onInit$={(api: DiagramInstance) => { instance.value = noSerialize(api); }} />
    </div>
  );
});
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';
import { nodes, edges } from './graph';

function onInit(instance: DiagramInstance): void {
  void instance.getEngine().layout('grid', { columns: 9 }).then(() => {
    instance.fitView(30);
  });
}
</script>

<template>
  <div style="height:400px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" />
  </div>
</template>
```
:::

The JavaScript sample initially shows the numbered nodes in five rows with connecting arrows.

The Angular sample shows the same grid at the left edge of its canvas.

The Qwik sample shows the nodes spread into a compact network.

Connecting arrows link the numbered nodes in the Qwik canvas.

The Vue sample initially shows the five-row grid framed in the canvas.

## 4. Register a domain-specific layout only when needed

Suppose your editorial workflow requires a fixed reading order: Draft, Review, Published. [`createLayout`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-functions-a-t) wraps a [`GraphLayoutFn`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-types) as a [`RegisteredLayout`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-l-u). It provides canonical input order and disconnected-component packing. Return a [`LayoutResult`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-i-l) rather than mutating node positions yourself.

Register it in the mounted engine's [`LayoutRegistry`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-classes). `register()` returns a disposer that restores the previous layout under that name, if one existed.

> **Known issue:** A layout created with `createLayout()` exposes an adapter, so an attached worker receives its name even though `serveLayout(self)` cannot resolve your main-thread registration. Until it is fixed, finish any worker run and call `engine.setLayoutPort(undefined)` before running this custom layout inline.

The intended invocation is `await engine.layout('editorial')` after registration, including when a worker is attached. The sample below includes the inline workaround and renders the three stages from left to right.

```ts title="editorial.ts"
import { createLayout, type GraphLayoutFn } from '@grafloria/engine';
import { render } from '@grafloria/element';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';

const stageOrder: Record<string, number> = { draft: 0, review: 1, published: 2 };
const arrangeEditorial: GraphLayoutFn = (nodes) => {
  const nodePositions = new Map<string, { x: number; y: number }>();
  let width = 0;
  let height = 0;
  for (const node of nodes) {
    const x = (stageOrder[node.id] ?? 0) * 220;
    nodePositions.set(node.id, { x, y: 0 });
    width = Math.max(width, x + (node.size?.width ?? 140));
    height = Math.max(height, node.size?.height ?? 60);
  }
  return { nodePositions, bounds: { x: 0, y: 0, width, height } };
};

export async function mountEditorial(container: HTMLElement): Promise<() => void> {
  const nodes: NodeSpec[] = [
    { id: 'draft', label: 'Draft', size: { width: 140, height: 60 } },
    { id: 'review', label: 'Review', size: { width: 140, height: 60 } },
    { id: 'published', label: 'Published', size: { width: 140, height: 60 } },
  ];
  const edges: EdgeSpec[] = [
    { source: 'draft', target: 'review' },
    { source: 'review', target: 'published' },
  ];
  container.style.height = '400px';
  const instance = render({ nodes, edges }, container);
  const engine = instance.getEngine();
  const unregister = engine.getLayoutRegistry().register(
    createLayout('editorial', arrangeEditorial),
  );
  engine.setLayoutPort(undefined);
  await engine.layout('editorial');
  instance.fitView(30);
  return () => {
    unregister();
    instance.dispose();
  };
}

const container = document.createElement('div');
document.body.append(container);
export const unmountEditorial = mountEditorial(container);
// On unmount, use unmountEditorial.then((unmount) => unmount()).
```

![Draft, Review and Published arranged left to right with connecting arrows.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/f836353434d5ed3931de709cd4f95b65.png)

The algorithm and registry call are framework-independent: use the same registration on the engine obtained in step 3. A [`LayoutAdapter`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-i-l) additionally defines `applyIncremental()` and `validateOptions()`. The adapter produced by `createLayout()` throws for `applyIncremental()`; do not treat this wrapper as an incremental-layout implementation.

## Options that control execution

These fields belong to [`UnifiedLayoutOptions`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-unifiedlayoutoptions). Run controls stay on the host side rather than crossing the worker boundary as callbacks or signals.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `signal` | `AbortSignal` | Not supplied | Requests cooperative cancellation |
| `onProgress` | `(progress: LayoutProgress) => void` | Not supplied | Reports progress on the caller's thread |
| `sliceMs` | `number` | `12` | Sets computation time between event-loop yields |
| `timeBudgetMs` | `number` | No budget | Stops an interruptible run with a partial result and `reason: 'timeout'` |
| `stopAfterIteration` | `number` | No cap | Stops at an iteration count with `reason: 'iteration-cap'` |
| `seed` | `number` | Fixed constant | Makes randomized layouts reproducible |
| `iterations` | `number` | `300` for force | Sets the force simulation's iteration limit |

[`LayoutProgress`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-layout-interfaces-i-l) contains `progress` from 0 to 1, `phase`, `iteration`, and `totalIterations`. A completed force run reaches 1; a cancelled run reports its actual stopping point. A time budget depends on wall-clock timing; use `stopAfterIteration` when you need a reproducible partial result.

## Execution limits

- Mid-run cancellation and iteration progress require the steppable path. The shipped force adapter uses it for connected graphs. Disconnected force graphs take the packed, one-shot path instead; they retain readable component placement but lose mid-run cancellation.
- One-shot adapters, including dagre, spectral and community, cannot stop inside their algorithm call. They report start and completion rather than simulation iterations.
- Grouped diagrams use the nested-container path by default. That path runs inline and returns a complete single-pass result; attaching a worker does not move it off-thread.
- A `RegisteredLayout` without an `adapter` also runs inline. Worker-side algorithms must exist in the worker bundle; a main-thread closure cannot cross `postMessage()`.

> **Known issue:** Requesting `engine.layout('elk')` through the module worker can fail while constructing ELK's nested worker. Until it is fixed, settle the current run, call `engine.setLayoutPort(undefined)`, then call `await engine.layout('elk')` inline.

## Live demo and related guides

See [Off-thread layout](https://grafloria.com/demos/layout/off-thread-layout.html) for a real worker, streamed progress, cancellation and a main-thread responsiveness check. Its [source](https://github.com/grafloria/grafloria/blob/cccb505e76c7af26c8bafecaba8146db7d8df928/demos/layout/off-thread-layout.html) shows the same worker wiring.

- [Lay out a diagram](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/lay-out-a-diagram): declarative layouts and explicit reruns.
- [Commands and history](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history): user-facing edits and undo.
- [Theme a canvas](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/theme-a-canvas): sizing and appearance.
