Skip to content
D
Documentation

Instance

reference
10 min readUpdated

Import these from @grafloria/renderer.

On their own pages

Functions

applyEdges

Reconcile the diagram's links against specs. See {@link applyNodes}.

ts
function applyEdges(diagram: DiagramModel, specs: Array<EdgeSpec | LinkModel>): boolean

applyEdgeSpec

Apply the mutable parts of an edge spec onto an existing link.

ts
function applyEdgeSpec(link: LinkModel, spec: EdgeSpec): void

applyGroups

Reconcile the diagram's groups against specs — add, update, remove — the way {@link applyNodes} does for nodes. A live GroupModel passes through (a Mermaid subgraph arrives that way). Removing a group never removes its boxes.

ts
function applyGroups(diagram: DiagramModel, specs: Array<GroupSpec | GroupModel>): boolean

Returns whether anything changed.

applyNodes

Reconcile the diagram's nodes against specs: add what is new, update what moved, remove what disappeared. Live NodeModels pass through untouched, so a host can mix "give me the data" with "here is my own model".

ts
function applyNodes(diagram: DiagramModel, specs: Array<NodeSpec | NodeModel>): boolean

Returns whether anything changed (i.e. whether a repaint is warranted).

applyNodeSpec

Apply the mutable parts of a spec onto an existing node (the update path).

ts
function applyNodeSpec(node: NodeModel, spec: NodeSpec): void

buildEdge

Build a fresh LinkModel from a spec. Returns null when an endpoint is unresolvable.

ts
function buildEdge(
  diagram: DiagramModel,
  spec: EdgeSpec,
  index: number
): LinkModel | null

buildNode

Build a fresh NodeModel from a spec, with deterministic ports.

ts
function buildNode(spec: NodeSpec, index: number): NodeModel

buildPort

The gating spec is flattened onto the model's individual fields because that is the shape resolvePortConfig() reads; PortSpec.gating is only the ergonomic grouping of them.

A port with no explicit side and no group still lands on right (the PortModel default) — but a port that names a group and no side is now built WITHOUT side, so explicitSide stays false and the group's side is inherited, which is the entire reason that flag exists.

ts
function buildPort(nodeId: string, spec: PortSpec, index: number): PortModel

contentBounds

World bounding box of every visible node, or null when there is nothing to fit.

ts
function contentBounds(model: DiagramModel): Rectangle | null

createDiagram

ts
function createDiagram(
  container: HTMLElement,
  options: CreateDiagramOptions = {}
): DiagramInstance

defaultPortId

The deterministic id of a node's default port on side.

ts
function defaultPortId(nodeId: string, side: (typeof PORT_SIDES)[number]): string

edgeSpecId

Stable id for the nth edge of a spec list.

ts
function edgeSpecId(spec: EdgeSpec, index: number): string

htmlLayerStyle

The HTML layer's style for a given camera transform (see ViewportController).

ts
function htmlLayerStyle(transform: string): string

isGroupModel

True for a live GroupModel.

ts
function isGroupModel(value: unknown): value is GroupModel

isLinkModel

True for a live LinkModel.

ts
function isLinkModel(value: unknown): value is LinkModel

isNodeModel

True for a live NodeModel (vs a plain spec object).

ts
function isNodeModel(value: unknown): value is NodeModel

nodeHostStyle

The style of one custom node's host element inside the HTML layer.

ts
function nodeHostStyle(
  x: number,
  y: number,
  width: number,
  height: number
): string

nodeSpecId

Stable id for the nth node of a spec list.

ts
function nodeSpecId(spec: NodeSpec, index: number): string

resolvePortId

Resolve an edge endpoint to a PORT id. Accepts (in order): an explicit port id, a side name, or the node's default port for fallbackSide. Returns undefined when the node does not exist.

ts
function resolvePortId(
  diagram: DiagramModel,
  nodeOrPortId: string,
  handle: string | undefined,
  fallbackSide: (typeof PORT_SIDES)[number]
): string | undefined

toEdgeSpec

Model → spec for a link. See {@link toNodeSpec}.

ts
function toEdgeSpec(link: LinkModel): EdgeSpec

toNodeSpec

Model → spec: the projection a host needs to write model changes BACK into its own state (a React useState, a Vue ref, a web-component property). Without it a wrapper would have to reach into engine models, which is exactly the coupling these specs exist to avoid.

ts
function toNodeSpec(node: NodeModel): NodeSpec

Classes

DomEventBinder

ts
class DomEventBinder

Methods

  • getDraggingNodeIds(): string[] — . The nodes currently being DRAGGED (past the movement threshold — an armed-but-uncommitted press is a click, not a drag).

Node-drag state lives here, not on the InteractionController, so a custom node component had no way to know it was being dragged — and dragging is one of the props the component contract promises. This is the read-only window onto it.

  • hasActiveGesture(): boolean — True while a resize / rotate / vertex gesture owns the pointer.

The companion to {@link getDraggingNodeIds} for the gestures that are NOT node drags. Custom-node culling needs it: unmounting a host element mid-resize would take the pointer capture and the handles with it, and SelectionToolsController keeps the gesture's own node private, so "is a gesture live" plus the current selection is the answer available from out here.

  • constructor( private readonly container: HTMLElement, private readonly host: DomEventBinderHost, options: DomEventBinderOptions = {} )
  • attach(): void — Bind DOM listeners. No-op on the server and no-op if already attached.
  • detach(): void — Remove EXACTLY the listeners we added, and drop all gesture state.
  • get isAttached(): boolean
  • onWheel(event: WheelEvent): void
  • onMouseDown(event: MouseEvent): void
  • onMouseMove(event: MouseEvent): void
  • onMouseUp(event: MouseEvent): void
  • onMouseLeave(): void — Pointer left the canvas — abort every in-flight gesture so nothing sticks.
  • onDoubleClick(event: MouseEvent): void — Double-click: node → in-place rename; link label → rename; link body → waypoint.
  • onKeyDown(event: KeyboardEvent): void
  • onKeyUp(event: KeyboardEvent): void
  • beginLabelEdit(target: TextEditTarget, options?: { seed?: string }): boolean — Open the in-place label editor programmatically — the seam behind F2 and a host's context-menu Rename. Unlike the double-click path this is NOT gated on enableInPlaceTextEdit: an explicit call IS the host's opt-in. Returns false when the target does not exist / is not editable / readonly.

RenderScheduler

ts
class RenderScheduler

Methods

  • constructor(options: RenderSchedulerOptions)
  • get stats(): Readonly<RenderSchedulerStats>
  • get pending(): boolean — True while a frame is queued but has not run yet.
  • schedule(): void — Mark dirty and queue a frame. Idempotent within a tick — the second and later calls before the frame runs are counted as coalesced, not queued.
  • flush(): void — Paint NOW, bypassing rAF and the idle-skip check, and cancel any queued frame. This is the mount paint (and the "give me a correct DOM before I measure it" escape hatch).
  • cancel(): void — Drop a queued frame without painting.
  • dispose(): void

Constants

HTML_LAYER_CLASS

ts
const HTML_LAYER_CLASS: "grafloria-html-layer"

INSTANCE_ATTR

data-grafloria-instance — the renderer's CSS scope, mirrored onto the root.

ts
const INSTANCE_ATTR: "data-grafloria-instance"

PORT_SIDES

ts
const PORT_SIDES: readonly ["top", "right", "bottom", "left"]

ROOT_CLASS

The DOM skeleton of a mounted diagram — ONE definition, used by both halves.

The server (renderToStaticSVG) emits this markup as a string; the client (createDiagram) builds the identical structure with createElement, or ADOPTS the server's when hydrating. Any divergence between the two — a class name, a style declaration, an attribute — is a hydration mismatch, so both paths read the constants from here rather than each spelling them out.

← the deterministic part
…custom nodes…
← client-only

The HTML layer holds nodes that render as framework components (React portals, slotted templates). It carries the camera as a CSS transform so it stays registered with the SVG layer, and is pointer-events: none so it does not eat clicks meant for the SVG underneath — each mounted node host turns pointer events back on for itself.

ts
const ROOT_CLASS: "grafloria-diagram-root"

ROOT_STYLE

ts
const ROOT_STYLE: "position:relative;width:100%;height:100%;overflow:hidden"

SVG_LAYER_CLASS

ts
const SVG_LAYER_CLASS: "grafloria-svg-layer"

SVG_LAYER_STYLE

ts
const SVG_LAYER_STYLE: "position:absolute;top:0;left:0;width:100%;height:100%"

Interfaces

DiagramEventMap

ts
interface DiagramEventMap

Properties

NameTypeDefaultDescription
'nodes:change'{ nodes: NodeModel[] }
'edges:change'{ edges: LinkModel[] }
'selection:change'{ nodes: NodeModel[]; edges: LinkModel[] }
connect{ link: LinkModel }
reconnect{ link: LinkModel; endpoint: 'source' | 'target' }
'node:click'{ node: NodeModel; world: { x: number; y: number } }
'node:doubleclick'{ node: NodeModel; world: { x: number; y: number } }
'edge:click'{ edge: LinkModel; world: { x: number; y: number } }
'viewport:change'{ viewport: Rectangle; zoom: number }
readyvoid

DomEventBinderHost

Everything the binder needs from its host. Keeps this class DI-free.

ts
interface DomEventBinderHost

Properties

NameTypeDefaultDescription
viewportViewportController
interactionInteractionController

Members

  • getEngine(): DiagramEngine | null — The engine, or null before a diagram is attached.
  • getRect(): CanvasRect — The canvas' client rect (for screen→world).
  • requestRender(): void — "Something visible changed" — the host coalesces this into a frame.
  • emit(event: string, payload: unknown): void — Emit a public diagram event (node:click, connect, …).

DomEventBinderOptions

ts
interface DomEventBinderOptions

Properties

NameTypeDefaultDescription
enablePan?booleanMiddle-drag / space-drag / wheel-scroll panning. Default true.
enableZoom?booleanCtrl/⌘ + wheel zoom. Default true.
zoomSensitivity?numberRelative zoom step per wheel notch. Default 0.1 (a notch is ×1.1).
dragThreshold?numberCSS px the pointer must travel before a node drag commits. Default 4.
readonly?booleanIgnore every mutation-causing gesture (still pans/zooms). Default false.

GroupFrameStyle

A zone's own frame — what makes a group look like the tinted, captioned regions of the diagrams AI tools draw instead of the theme's titled box. Declaring any of it replaces the theme frame (no title band).

ts
interface GroupFrameStyle

Properties

NameTypeDefaultDescription
fill?string
stroke?string
strokeWidth?number
strokeDasharray?string
borderRadius?number
color?stringCaption colour.
fontSize?numberCaption size in px. Default 11.
fontWeight?string | number
fontFamily?string
letterSpacing?numberCaption letter spacing in px.
textTransform?'none' | 'uppercase' | 'lowercase' | 'capitalize'

GroupSpec

A GROUP in the spec — a zone around some boxes. bounds pins its frame; without it the frame is fitted around children with padding. The children become the group's members (they travel with it). Stored as a GroupModel whose metadata.frameStyle carries style + labelPlacement, so it serializes.

ts
interface GroupSpec

Properties

NameTypeDefaultDescription
idstring
label?string
children?string[]
bounds?{ x: number; y: number; width: number; height: number }
padding?numberSpace between the children and the fitted frame. Default 20.
style?GroupFrameStyle
labelPlacement?GroupLabelPlacementDefault 'top-left'.
direction?'LR' | 'RL' | 'TB' | 'TD' | 'BT'How the zone lays its boxes out under a composing layout: 'LR' a row, 'TB' a column.

NodeSpec

ts
interface NodeSpec

Properties

NameTypeDefaultDescription
id?string
type?stringEngine node type. Default 'rect'.
position?{ x: number; y: number }Where the box sits. Optional: absent, a new box starts at the origin and an existing one stays put — and a composing layout (layout: 'architecture') places it anyway.
size?{ width: number; height: number }
data?Record<string, any>Free-form user payload — passed straight to custom node components.
label?stringConvenience for metadata.label.
sublabel?string | NodeSublabelA second, smaller, muted line under the label — the name-and-description box of the diagrams AI tools draw ("Our API" / "sherkety-erp-api"). With a sublabel the label is drawn semi-bold (a style.fontWeight still wins). An object sets the line's own font ('mono' = a monospace stack), size and colour. Stored on metadata.sublabel, so it serializes with the node.
near?{ target: string; side?: 'right' | 'left' | 'above' | 'below'; gap?: number } | nullA note placed BESIDE what it is about — a relation, not a coordinate: the architecture layout puts this box to the side of target (a node or zone), gap px away. Stored on metadata.near.
shape?Record<string, any>Convenience for metadata.shape (fill / stroke / cornerRadius / …).
style?Partial<NodeStyle>
selected?boolean
draggable?boolean
selectable?boolean
custom?booleanRender this node through the host's custom-node callback (React component, slotted template, …) instead of as SVG. Sets metadata.useHTMLLayer.
ports?PortSpec[]Ports. Omit to keep the four deterministic defaults.
metadata?Record<string, any>Anything else you want on node.metadata.

NodeSublabel

A node's second line, when it needs its own font or colour. See NodeSpec.sublabel.

ts
interface NodeSublabel

Properties

NameTypeDefaultDescription
textstring
fontFamily?stringA CSS font stack, or 'mono' for a monospace one.
fontSize?numberpx. Default: 0.85 of the label's size.
color?stringDefault: the theme's secondary text colour.
fontWeight?string | number

RenderSchedulerOptions

RenderScheduler — framework-agnostic rAF coalescing + idle-skip.

Blocker #3 of the headless-instance contract (see ./diagram-instance.ts): the only render loop in the codebase was DiagramCanvasComponent.scheduleRender(), a private Angular method. This is that logic, lifted verbatim in behaviour and with no framework or DOM imports, so React / the web component / a plain <script> host all inherit the same frame discipline:

  • Coalescing. Any number of schedule() calls in one tick collapse into exactly ONE painted frame. A burst of engine events (node:changed ×N, a drag's mousemoves, several prop changes in one React commit) paints once.
    • Idle-skip. A queued frame is DROPPED when shouldSkip() says nothing visible can have changed — cheaper than a no-op render of a big diagram.
    • Synchronous escape. flush() paints right now and cancels the queued frame; used for the mount paint (so the first frame is not one rAF late) and by tests.

requestFrame/cancelFrame are injectable: pass fakes in tests, and note the default falls back to setTimeout where rAF is missing (Node), so a scheduler constructed during SSR never throws — it simply never gets a chance to fire because nothing calls schedule() on the server.

ts
interface RenderSchedulerOptions

Properties

NameTypeDefaultDescription
onFrame()The paint. Called at most once per frame.
shouldSkip?()Idle-skip predicate, evaluated INSIDE the frame (not at schedule time, so it sees the final state of the tick). Return true to drop the frame.
requestFrame?(cb: (time: number)Injectable rAF (defaults to the platform one, with a setTimeout fallback).
cancelFrame?(handle: number)Injectable cancel, must pair with requestFrame.

RenderSchedulerStats

Cheap counters — a steady-state idle canvas should paint 0 frames.

ts
interface RenderSchedulerStats

Properties

NameTypeDefaultDescription
schedulednumberschedule() calls.
paintednumberFrames actually painted (onFrame ran).
skippednumberQueued frames dropped by shouldSkip().
coalescednumberschedule() calls that folded into an already-queued frame.
lastFrameMsnumberDuration (ms) of the most recent paint.

Types

CreateDiagram

The factory's own signature, for hosts that store it.

ts
type CreateDiagram = typeof import('./create-diagram').createDiagram;

DiagramEventHandler

ts
type DiagramEventHandler<K extends DiagramEventName> = (
  payload: DiagramEventMap[K]
) => void;

DiagramEventName

ts
type DiagramEventName = keyof DiagramEventMap;

EdgeInput

ts
type EdgeInput = EdgeSpec | LinkModel;

GroupLabelPlacement

Where a zone's caption sits.

ts
type GroupLabelPlacement = 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right';

NodeInput

Nodes/edges may be handed in as plain specs or as live engine models.

ts
type NodeInput = NodeSpec | NodeModel;

Was this page helpful?