# Preserve the layout mental map

Add a node to an arranged graph with an incremental pass while minimizing movement to existing diagram content.

Use incremental layout after inserting or editing nodes in a graph people already know. A full layout can move the entire graph; the incremental pass confines disturbance around the changed nodes and reports movement.

## Add a node with incremental layout

1. Mount a connected graph and lay it out with `layered` so its starting positions come from the same layout engine. In JavaScript, [`render`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core) mounts the spec and returns the live instance; framework apps use their canvas component.
2. Add the new node and its edges to the live [`DiagramInstance`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-diagraminstance). Mark the new node in `changed`, then call `layoutIncremental()` on the engine returned by `getEngine()`.
3. Repaint and frame the result. The new node appears in the chain, while nodes outside the affected neighborhood remain anchors. The result includes a movement report and a tween plan; the engine returns the plan rather than animating it, so a host can drive the animation if needed.

The task-specific difference is inserting a node into an arranged chain and measuring movement among the existing nodes; see [Lay out diagrams](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/lay-out-diagrams) for the shared mounting, framework-binding, and typed-spec patterns.

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';

async function main() {
const nodes = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5'].map((id) => ({
  id,
  position: { x: 0, y: 0 },
  size: { width: 110, height: 46 },
  label: id,
}));
const edges = [
  { id: 'e0', source: 'n0', target: 'n1' },
  { id: 'e1', source: 'n1', target: 'n2' },
  { id: 'e2', source: 'n2', target: 'n3' },
  { id: 'e3', source: 'n3', target: 'n4' },
  { id: 'e4', source: 'n4', target: 'n5' },
];
const host = document.getElementById('app');
if (!host) throw new Error('Missing #app element');
host.style.height = '400px';
const instance = render({ nodes, edges }, host);
const engine = instance.getEngine();

await engine.layout('layered');
instance.renderNow();
instance.fitView(40);

instance.setNodes([
  ...nodes,
  { id: 'inserted', position: { x: 0, y: 0 }, size: { width: 110, height: 46 }, label: 'inserted' },
]);
instance.setEdges([
  ...edges,
  { id: 'x0', source: 'n2', target: 'inserted' },
  { id: 'x1', source: 'inserted', target: 'n4' },
]);
const result = await engine.layoutIncremental({ name: 'layered', changed: ['inserted'], radius: 1 });
instance.renderNow();
instance.fitView(40);

console.log(result.movement.total, result.tween.movingIds);
}

void main();
```
```ts title="Angular"
import { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { LinkModel, NodeModel } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <button type="button" (click)="insertNode()">Insert node</button>
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      [layout]="'layered'" style="display:block; height:400px" />
  `,
})
export class AppComponent {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  nodes: readonly (NodeSpec | NodeModel)[] | undefined = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5'].map((id) => ({
    id,
    position: { x: 0, y: 0 },
    size: { width: 110, height: 46 },
    label: id,
  }));
  edges: readonly (EdgeSpec | LinkModel)[] | undefined = [
    { id: 'e0', source: 'n0', target: 'n1' },
    { id: 'e1', source: 'n1', target: 'n2' },
    { id: 'e2', source: 'n2', target: 'n3' },
    { id: 'e3', source: 'n3', target: 'n4' },
    { id: 'e4', source: 'n4', target: 'n5' },
  ];

  insertNode(): void {
    const engine = this.canvas().activeEngine();
    if (!engine) return;
    this.nodes = [
      ...(this.nodes ?? []),
      { id: 'inserted', position: { x: 0, y: 0 }, size: { width: 110, height: 46 }, label: 'inserted' },
    ];
    this.edges = [
      ...(this.edges ?? []),
      { id: 'x0', source: 'n2', target: 'inserted' },
      { id: 'x1', source: 'inserted', target: 'n4' },
    ];

    setTimeout(() => {
      const liveEngine = this.canvas().activeEngine();
      if (liveEngine) void liveEngine.layoutIncremental({ name: 'layered', changed: ['inserted'], radius: 1 });
    }, 0);
  }
}
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5'].map((id) => ({
  id,
  position: { x: 0, y: 0 },
  size: { width: 110, height: 46 },
  label: id,
}));
const edges: EdgeSpec[] = [
  { id: 'e0', source: 'n0', target: 'n1' },
  { id: 'e1', source: 'n1', target: 'n2' },
  { id: 'e2', source: 'n2', target: 'n3' },
  { id: 'e3', source: 'n3', target: 'n4' },
  { id: 'e4', source: 'n4', target: 'n5' },
];

async function onInit(instance: DiagramInstance): Promise<void> {
  const engine = instance.getEngine();
  await engine.layout('layered');
  instance.renderNow();
  instance.fitView(40);

  instance.setNodes([
    ...nodes,
    { id: 'inserted', position: { x: 0, y: 0 }, size: { width: 110, height: 46 }, label: 'inserted' },
  ]);
  instance.setEdges([
    ...edges,
    { id: 'x0', source: 'n2', target: 'inserted' },
    { id: 'x1', source: 'inserted', target: 'n4' },
  ]);
  const result = await engine.layoutIncremental({ name: 'layered', changed: ['inserted'], radius: 1 });
  instance.renderNow();
  instance.fitView(40);
  console.log(result.movement.total, result.tween.movingIds);
}
</script>

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

In Qwik, mount `GrafloriaFlow` with `defaultNodes={baseNodes}` and `defaultEdges={baseEdges}`, then wrap the handler below with `$()` and pass it as `onInit$`. It lays out the mounted chain, inserts a node between `n2` and `n4`, and incrementally lays out the changed neighborhood.

```ts title="Qwik"
import type { DiagramInstance } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

export const baseNodes: NodeSpec[] = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5'].map((id) => ({
  id,
  position: { x: 0, y: 0 },
  size: { width: 110, height: 46 },
  label: id,
}));
export const baseEdges: EdgeSpec[] = [
  { id: 'e0', source: 'n0', target: 'n1' },
  { id: 'e1', source: 'n1', target: 'n2' },
  { id: 'e2', source: 'n2', target: 'n3' },
  { id: 'e3', source: 'n3', target: 'n4' },
  { id: 'e4', source: 'n4', target: 'n5' },
];

export async function onInit(instance: DiagramInstance): Promise<void> {
  const engine = instance.getEngine();
  await engine.layout('layered');
  instance.renderNow();
  instance.fitView(40);
  instance.setNodes([
    ...baseNodes,
    { id: 'inserted', position: { x: 0, y: 0 }, size: { width: 110, height: 46 }, label: 'inserted' },
  ]);
  instance.setEdges([
    ...baseEdges,
    { id: 'x0', source: 'n2', target: 'inserted' },
    { id: 'x1', source: 'inserted', target: 'n4' },
  ]);
  await engine.layoutIncremental({ name: 'layered', changed: ['inserted'], radius: 1 });
  instance.renderNow();
  instance.fitView(40);
}
```

The repository's [Qwik dynamic-layouting component](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/apps/demos-qwik/gallery/demos/dynamic-layouting.tsx) uses this `onInit$` flow in a Qwik-optimized application.

The visible result is a laid-out chain with `inserted` connected after `n2` and before `n4`. `movement.total` reports the total distance traveled by pre-existing nodes; `tween.movingIds` identifies which existing nodes move. The tween plan is data only: call `result.tween.at(t)` over normalized time values if your host wants to animate positions.

See the [live incremental-layout demo](https://grafloria.com/demos/layout/dynamic-layouting.html) and its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/layout/dynamic-layouting.html).

## Options that shape the incremental pass

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `changed` | `string[]` | `[]` | Identifies newly added or edited node IDs. All other nodes count as existing for the movement report. |
| `radius` | `number` | `1` | Sets how many graph hops the affected region may spread from changed nodes. |
| `budget` | `{ maxPerNode?: number; averagePerNode?: number }` | — | Sets a maximum permitted movement per existing node or average movement. The returned report includes `withinBudget`. |

The incremental call's `name` option selects a registered layout. When prior positions came from another engine, an incremental pass can redraw the graph because the engines do not share a common layout geometry.

## Pitfalls

- Include the new node ID in `changed`; otherwise its default position can distort the baseline used to align the result.
- A declarative `layout` prop reruns when the prop value changes, not when node data changes. Call the engine method for an on-demand pass; Angular also exposes `applyLayout()` for its bound layout.
- Give the canvas wrapper a resolved height. The canvas fills its parent, so a zero-height parent renders a blank area.
- In plain JavaScript and React, custom renderers are not used unless the node spec opts into the HTML layer with `custom: true`; see [Lay out diagrams](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/lay-out-diagrams).

## Related

- [Lay out diagrams](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/lay-out-diagrams) for layout choices and declarative layout.
- [Add nodes from palettes](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/add-nodes-from-palettes) for reconciliation behavior when node IDs remain in the data.
- [Command history](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history) for undoing user edits.
