Skip to content
D
Documentation

Layout — Incremental

reference
3 min readUpdated

Import these from @grafloria/engine.

Functions

affectedRegion

The nodes allowed to move: the changed set grown by radius hops.

This is what "re-run layout only in the affected region" means concretely — and it is only possible because anchors are now real: the layout works AROUND the frozen nodes instead of laying out over them and being corrected afterwards.

ts
function affectedRegion(
  diagram: DiagramModel,
  changed: string[],
  radius: number
): Set<string>

alignToPrevious

The translation that minimises total squared displacement between two layouts.

For a pure translation the optimum is exactly the difference of the centroids of the shared nodes — no search, no iteration. This is the single highest-value function in the file: a layered layout is defined only up to translation, so a new node widening rank 0 can shift the ENTIRE drawing sideways. Every node then "moves" although the picture is unchanged, the movement budget blows, and a naive implementation starts fighting its own layout with constraints. Align first; constrain only what is left.

ts
function alignToPrevious(next: Positions, previous: Positions): { positions: Positions; shift: { x: number; y: number } }

constraintsForStrategy

Note what this does NOT do: it does not emit positions to be clamped afterwards. It emits ANCHORS, which the layered engine honours during coordinate assignment — the whole reason the four scaffolded strategies never worked.

ts
function constraintsForStrategy(
  diagram: DiagramModel,
  before: Positions,
  options: IncrementalOptions
): SemanticConstraints

measureMovement

Measure how much the mental map was disturbed.

Only nodes that existed BEFORE count: a new node cannot "move", and including it would flatter the numbers exactly when the layout is at its most disruptive.

ts
function measureMovement(
  before: Positions,
  after: Positions,
  budget: MovementBudget | undefined,
  savedByAlignment = 0
): MovementReport

planTween

ts
function planTween(before: Positions, after: Positions): TweenPlan

Interfaces

IncrementalOptions

ts
interface IncrementalOptions

Properties

NameTypeDefaultDescription
strategy?IncrementalStrategy
changed?string[]Node ids the user just added/edited. Everything else is "existing".
radius?numberHow far the disturbance is allowed to spread, in graph hops. Default 1.
budget?MovementBudget

MovementBudget

ts
interface MovementBudget

Properties

NameTypeDefaultDescription
maxPerNode?numberNo single node may move further than this.
averagePerNode?numberThe mean movement across all nodes that existed before.

MovementReport

ts
interface MovementReport

Properties

NameTypeDefaultDescription
totalnumberSum of the distances every pre-existing node travelled.
averagenumber
maxnumber
movednumberHow many pre-existing nodes moved at all (beyond a 0.5px epsilon).
unmovednumberOf the pre-existing nodes, how many stayed exactly put.
withinBudgetbooleanDid we stay inside the caller's budget?
savedByAlignmentnumberHow much of the movement the centroid re-alignment removed.

TweenPlan

A tween plan: pure data, so the engine stays free of rAF/DOM/time.

The card asks for "an animated tweened transition from old to new positions instead of snapping". The engine's job is to say WHERE things go at time t; the host's job is to drive t. Keeping it that way is what lets the same code run in a worker, in SSR, and in a test.

ts
interface TweenPlan

Properties

NameTypeDefaultDescription
movingIdsstring[]Nodes that actually move — a host can skip the rest entirely.

Members

  • at(t: number): Positions — Positions at normalised time t ∈ [0, 1]. Eased.

Types

IncrementalStrategy

Also has every member of String, listed on its own entry.

ts
type IncrementalStrategy =

  | 'region'

  | 'pin-existing'

  | 'minimal-shift';

Positions

ts
type Positions = Map<string, { x: number; y: number }>;

Properties

NameTypeDefaultDescription
sizenumber

Members

  • clear(): void
  • delete(key: K): boolean
  • forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void — Executes a provided function once per each key/value pair in the Map, in insertion order.
  • get(key: K): V | undefined — Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
  • has(key: K): boolean
  • set(key: K, value: V): this — Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.
  • entries(): MapIterator<[K, V]> — Returns an iterable of key, value pairs for every entry in the map.
  • keys(): MapIterator<K> — Returns an iterable of keys in the map
  • values(): MapIterator<V> — Returns an iterable of values in the map
  • [Symbol.iterator](): MapIterator<[K, V]> — Returns an iterable of entries in the map.
  • readonly [Symbol.toStringTag]: string

Was this page helpful?

Layout — Incremental — Grafloria