Usage guidesLayout

Custom layouts

Register position calculations, handle options and cancellation, and distinguish Graph execution from standalone calculation

Use a LayoutExtension to calculate node positions from graph data. This example arranges nodes in a row with equal spacing. See the Layout overview for built-in layouts.

Implement and execute

Read current data and runtime.options, retain node IDs/business fields, and return finite x/y. Reading options from runtime also supports standalone execution without init.

Loading example…
Start Slow layout then immediately Cancel + grid. Old results cannot replace the grid. Layout again restores the column
export function createColumnLayout(): LayoutExtension<ColumnOptions> {
  let context: ExtensionContext | undefined;
  return {
    init(next) { context = next; },
    async execute(data, { width, height, signal, options }) {
      const gap = Number(options.gap ?? 100);
      const nodes = [];
      for (const [index, node] of data.nodes.entries()) {
        await pause(signal, Number(options.delay ?? 0));
        if (signal?.aborted || context?.isDestroyed()) throw new DOMException('Canceled', 'AbortError');
        nodes.push({ ...node, x: width / 2 + (index - (data.nodes.length - 1) / 2) * gap, y: height / 2 });
      }
      return { nodes };
    },
    destroy() { context = undefined; },
  };
}
register('layout', 'docs-column', () => createColumnLayout());

Six nodes start in a row. Layout again recalculates. Start Slow layout and immediately choose Cancel + grid to invalidate the old run and restore a grid. Run the column layout again to verify recovery.

Cancellation and resources

The wait helper clears its timer on abort and removes its listener on completion. Each step checks signal / isDestroyed. Chunk expensive work and yield so cancellation can run.

Graph calls destroy after success, failure, or cancellation. Cleanup must tolerate partial initialization. Layout fallback may absorb failures; fulfilled await alone does not prove application. The buttons use their own generation check before status updates and fitting.

Graph versus standalone execution

Graph.render / layout / setData use init → execute → destroy. Standalone runLayout skips init but still calls execute / destroy. This example reads options from runtime so both paths work.

import { runLayout, type GraphData } from '@antv/g6-lite';
export async function calculate(data: GraphData) {
  // Register docs-column above first. This calculates positions, without updating Graph.
  return runLayout(data, { type: 'docs-column', gap: 100 }, { width: 640, height: 320 });
}

See the runLayout example for displaying calculated results. Do not request rendering from execute; Graph applies results. context.data can be the old graph, not this execution's input.

Common questions

Does worker: true move a custom layout to a Worker?

No. Registered extensions currently take the main-thread path. Own messages, cancellation, and cleanup if implementing your own Worker; see Worker guidance.

Why can cancellation still fulfill the Promise?

Cancellation invalidates old results rather than promising rejection. Keep an application generation check to prevent stale UI updates.

See LayoutExtension for runtime fields and fallback, and the two-row example.

Related runnable examples