Skip to main content

Layouts

The @deck.gl-community/graph-layers package ships a handful of layouts, but every application eventually needs custom geometry or physics. This guide walks through the lifecycle of a layout and shows how to build, test, and ship your own implementation.

When to build a custom layout​

Use a bespoke layout when any of the following are true:

  • You have domain-specific rules (hierarchies, timelines, geographic projections) that the stock layouts cannot express.
  • You need to integrate an existing solver or physics engine and expose its parameters through GraphLayer props.
  • You want to preprocess server-side coordinates (e.g. a recommendation engine) and simply stream them into the client.

Before writing code, review the GraphLayout API reference to understand the contract between the layout and GraphLayer.

Tutorial: build a layout from scratch​

The snippets below sketch a SpiralLayout that spreads nodes along a spiral arm. Each section focuses on one part of the GraphLayout lifecycle.

1. Extend GraphLayout​

Create a class that extends GraphLayout (optionally passing a props interface) and seed internal state you will reuse across lifecycle calls.

import {GraphLayout, GraphLayoutProps} from '@deck.gl-community/graph-layers';

export type SpiralLayoutProps = GraphLayoutProps & {
radiusStep?: number;
angleStep?: number;
};

export class SpiralLayout extends GraphLayout<SpiralLayoutProps> {
get [Symbol.toStringTag]() {
return 'SpiralLayout';
}

static defaultProps: Required<SpiralLayoutProps> = {
...GraphLayout.defaultProps,
radiusStep: 20,
angleStep: Math.PI / 6
};

constructor(props: SpiralLayoutProps = {}) {
super(props, SpiralLayout.defaultProps);
this._nodePositions = new Map();
}
}

2. Respond to graph lifecycle hooks​

GraphLayer feeds data into your layout via initializeGraph (first load) and updateGraph (incremental updates). Cache any graph references you need and make sure updateGraph works even when the topology changes.

initializeGraph(graph) {
this._graph = graph;
this._nodePositions.clear();
}

updateGraph(graph) {
this._graph = graph;
for (const node of graph.getNodes()) {
if (!this._nodePositions.has(node.getId())) {
this._nodePositions.set(node.getId(), null);
}
}
}

The GraphLayout base class exposes _updateBounds() and _calculateBounds() helpers. Override _updateBounds when your layout derives bounds that differ from the raw node positions (for example, when padding edges or projecting coordinates).

3. Emit lifecycle events​

Call _onLayoutStart, _onLayoutChange, and _onLayoutDone during your computations. These hooks notify React components, power loading indicators, and keep GraphLayoutEventDetail data in sync.

start() {
if (!this._graph) {
return;
}
this.state = 'start';
this._onLayoutStart();

const positions = [];
let index = 0;
for (const node of this._graph.getNodes()) {
const radius = this.props.radiusStep * (index + 1);
const angle = this.props.angleStep * index++;
const nextPosition = [Math.cos(angle) * radius, Math.sin(angle) * radius];
this._nodePositions.set(node.getId(), nextPosition);
positions.push(nextPosition);
}

this._bounds = this._calculateBounds(positions);
this.state = 'done';
this._onLayoutChange();
this._onLayoutDone();
}

update() {
// For this deterministic layout, recomputing is identical to start().
this.start();
}

stop() {
this.state = 'done';
}

For long-running solvers (such as force-directed layouts), call _onLayoutChange inside your simulation loop so the view can animate.

GPUForceLayout emits onLayoutStart before submitting each worker calculation, including updates. Final positions and bounds are available before onLayoutChange and onLayoutDone. Completed workers are released, and results from stopped or replaced workers are ignored.

4. Publish node and edge geometry​

GraphLayer queries positions every render. Implement getNodePosition and getEdgePosition so they return finite coordinates when available.

getNodePosition(node) {
return this._nodePositions.get(node.getId()) ?? null;
}

getEdgePosition(edge) {
const source = this.getNodePosition(edge.getSourceNode());
const target = this.getNodePosition(edge.getTargetNode());
if (!source || !target) {
return null;
}
return {
type: 'line',
sourcePosition: source,
targetPosition: target,
controlPoints: []
};
}

When returning null, the layer hides unfinished primitives, allowing incremental solvers to display partial results without jitter.

5. Handle user interactions​

If your UI supports drag-and-drop, override the interaction hooks:

lockNodePosition(node, x, y) {
this._nodePositions.set(node.getId(), [x, y]);
}

unlockNodePosition(node) {
this._nodePositions.delete(node.getId());
}

resume() {
// Restart your solver here (e.g. re-enable a physics simulation).
this.start();
}

6. Wire the layout into GraphLayer​

Finally, instantiate your layout and pass it to GraphLayer. You can hot-swap layouts by comparing instances with layout.equals.

import {GraphLayer} from '@deck.gl-community/graph-layers';
import {SpiralLayout} from './spiral-layout';

const layout = new SpiralLayout({radiusStep: 30});

<GraphLayer
id="spiral-graph"
graph={graph}
layout={layout}
enableDragging
/>;

Robust layouts lean on automated tests so regressions do not slip in as the ecosystem evolves.

  1. Unit-test deterministic positioning. Instantiate your layout with a fixture ClassicGraph and assert that getNodePosition returns finite coordinates and that _bounds reflects the expected extent.
    import {ClassicGraph} from '@deck.gl-community/graph-layers';
    import {describe, expect, it} from 'vitest';
    import {SpiralLayout} from '../spiral-layout';

    describe('SpiralLayout', () => {
    it('computes stable positions', () => {
    const graph = new ClassicGraph({
    nodes: [{id: 'a'}, {id: 'b'}],
    edges: []
    });
    const layout = new SpiralLayout();
    layout.initializeGraph(graph);
    layout.start();
    expect(layout.getNodePosition(graph.getNode('a'))).toBeTruthy();
    expect(layout.getBounds()).not.toBeNull();
    });
    });
  2. Simulate lifecycle events. Stub onLayoutStart / onLayoutDone props and confirm they fire in the right order when start or update runs.
  3. Exercise drag helpers. When you expose dragging, verify lockNodePosition, unlockNodePosition, and resume interact correctly with your solver.
  4. Run the repo test suite (yarn test-node) before publishing so the shared CI harness validates your changes.

Further reading​