Instance
Import these from @grafloria/renderer.
On their own pages
CreateDiagramOptionsDiagramInstanceEdgeSpec: An edge, as a host hands it in. Node-to-node, like React Flow.NodeSpecPortSpec: A port on a node. Omitidto get the deterministic<nodeId>__<side>name.
Functions
applyEdges
Reconcile the diagram's links against specs. See {@link applyNodes}.
tsfunction applyEdges(diagram: DiagramModel, specs: Array<EdgeSpec | LinkModel>): boolean
applyEdgeSpec
Apply the mutable parts of an edge spec onto an existing link.
tsfunction 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.
tsfunction 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".
tsfunction 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).
tsfunction applyNodeSpec(node: NodeModel, spec: NodeSpec): void
buildEdge
Build a fresh LinkModel from a spec. Returns null when an endpoint is unresolvable.
tsfunction buildEdge(
diagram: DiagramModel,
spec: EdgeSpec,
index: number
): LinkModel | null
buildNode
Build a fresh NodeModel from a spec, with deterministic ports.
tsfunction buildNode(spec: NodeSpec, index: number): NodeModel
buildPort
Spec → PortModel, carrying the WHOLE port vocabulary through.
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 built
WITHOUT side, so explicitSide stays false and the group's side is
inherited, which is the entire reason that flag exists.
tsfunction buildPort(nodeId: string, spec: PortSpec, index: number): PortModel
contentBounds
World bounding box of what the canvas draws — every visible node, every routed link waypoint, every group frame with its caption — or null when there is nothing to fit.
tsfunction contentBounds(model: DiagramModel): Rectangle | null
createDiagram
tsfunction createDiagram(
container: HTMLElement,
options: CreateDiagramOptions = {}
): DiagramInstance
defaultPortId
The deterministic id of a node's default port on side.
tsfunction defaultPortId(nodeId: string, side: (typeof PORT_SIDES)[number]): string
edgeSpecId
Stable id for the nth edge of a spec list.
tsfunction edgeSpecId(spec: EdgeSpec, index: number): string
htmlLayerStyle
The HTML layer's style for a given camera transform (see ViewportController).
tsfunction htmlLayerStyle(transform: string): string
isGroupModel
True for a live GroupModel.
tsfunction isGroupModel(value: unknown): value is GroupModel
isLinkModel
True for a live LinkModel.
tsfunction isLinkModel(value: unknown): value is LinkModel
isNodeModel
True for a live NodeModel (vs a plain spec object).
tsfunction isNodeModel(value: unknown): value is NodeModel
nodeHostStyle
The style of one custom node's host element inside the HTML layer.
tsfunction nodeHostStyle(
x: number,
y: number,
width: number,
height: number
): string
nodeSpecId
Stable id for the nth node of a spec list.
tsfunction 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.
tsfunction resolvePortId(
diagram: DiagramModel,
nodeOrPortId: string,
handle: string | undefined,
fallbackSide: (typeof PORT_SIDES)[number]
): string | undefined
toEdgeSpec
Model → spec for a link. See {@link toNodeSpec} — no selected either, for the same reason.
tsfunction 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.
It carries no selected. Selection is viewer state that changes on every
click, and nodes:change — when hosts store this projection — fires for the
document (a node added, removed, dropped), not for a click. A projected
selected therefore went stale the moment the user clicked elsewhere, and the
host's next write (a rename) selected the node again. Absent, writing the
projection back never touches the selection. A host that wants to DRIVE the
selection still sets selected in its own specs (setNodes applies it) and
reads it from selection:change.
tsfunction toNodeSpec(node: NodeModel): NodeSpec
Classes
DomEventBinder
tsclass DomEventBinder
Methods
getDraggingNodeIds(): string[]— . The nodes currently being DRAGGED (past the movement threshold — an armed-but-uncommitted press is a click, not a drag).hasActiveGesture(): boolean— True while a resize / rotate / vertex gesture owns the pointer.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(): booleanonWheel(event: WheelEvent): voidonMouseDown(event: MouseEvent): voidonMouseMove(event: MouseEvent): voidonMouseUp(event: MouseEvent): voidonMouseLeave(): 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): voidonKeyUp(event: KeyboardEvent): voidbeginLabelEdit(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 onenableInPlaceTextEdit: an explicit call IS the host's opt-in. Returns false when the target does not exist / is not editable / readonly.
RenderScheduler
tsclass 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 ascoalesced, 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
tsconst HTML_LAYER_CLASS: "grafloria-html-layer"
INSTANCE_ATTR
data-grafloria-instance — the renderer's CSS scope, mirrored onto the root.
tsconst INSTANCE_ATTR: "data-grafloria-instance"
PORT_SIDES
tsconst 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 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.
tsconst ROOT_CLASS: "grafloria-diagram-root"
ROOT_STYLE
tsconst ROOT_STYLE: "position:relative;width:100%;height:100%;overflow:hidden"
SVG_LAYER_CLASS
tsconst SVG_LAYER_CLASS: "grafloria-svg-layer"
SVG_LAYER_STYLE
tsconst SVG_LAYER_STYLE: "position:absolute;top:0;left:0;width:100%;height:100%"
Interfaces
DiagramEventMap
tsinterface DiagramEventMap
Properties
| Name | Type | Default | Description |
|---|---|---|---|
'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 } | ||
ready | void | ||
'nodes:change' | { nodes: NodeModel[] } | ||
'edges:change' | { edges: LinkModel[] } | ||
'selection:change' | { nodes: NodeModel[]; edges: LinkModel[] } | ||
'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 } |
DomEventBinderHost
Everything the binder needs from its host. Keeps this class DI-free.
tsinterface DomEventBinderHost
Properties
| Name | Type | Default | Description |
|---|---|---|---|
viewport | ViewportController | ||
interaction | InteractionController |
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, …).beginSelectionBatch?(): void— Optional: bracket a USER GESTURE so the host can coalesce its selection events. The binder opens a batch around every DOM event it handles, and holds one from a press to its release (a marquee clears on the press and selects on the release). Batches nest; a host that implements these emits ONEselection:change, with the final selection, when the outermost one closes — instead of one per model mutation plus the binder's own (which fired stale, mid-gesture events:{n:1,e:1}then{n:1,e:0}for one click).endSelectionBatch?(): void
DomEventBinderOptions
tsinterface DomEventBinderOptions
Properties
| Name | Type | Default | Description |
|---|---|---|---|
enablePan? | boolean | Middle-drag / space-drag / wheel-scroll panning. Default true. | |
enableZoom? | boolean | Ctrl/⌘ + wheel zoom. Default true. | |
zoomSensitivity? | number | Relative zoom step per wheel notch. Default 0.1 (a notch is ×1.1). | |
dragThreshold? | number | CSS px the pointer must travel before a node drag commits. Default 4. | |
readonly? | boolean | Ignore 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).
tsinterface GroupFrameStyle
Properties
| Name | Type | Default | Description |
|---|---|---|---|
fill? | string | ||
stroke? | string | ||
strokeWidth? | number | ||
strokeDasharray? | string | ||
borderRadius? | number | ||
color? | string | Caption colour. | |
fontSize? | number | Caption size in px. Default 11. | |
fontWeight? | string | number | ||
fontFamily? | string | ||
letterSpacing? | number | Caption 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.
tsinterface GroupSpec
Properties
| Name | Type | Default | Description |
|---|---|---|---|
id | string | ||
label? | string | ||
children? | string[] | ||
bounds? | { x: number; y: number; width: number; height: number } | ||
padding? | number | Space between the children and the fitted frame. Default 20. | |
style? | GroupFrameStyle | ||
labelPlacement? | GroupLabelPlacement | Default '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. |
NodeSublabel
A node's second line, when it needs its own font or colour. See NodeSpec.sublabel.
tsinterface NodeSublabel
Properties
| Name | Type | Default | Description |
|---|---|---|---|
text | string | ||
fontFamily? | string | A CSS font stack, or 'mono' for a monospace one. | |
fontSize? | number | px. Default: 0.85 of the label's size. | |
color? | string | Default: 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.
- Idle-skip. A queued frame is DROPPED when
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.
tsinterface RenderSchedulerOptions
Properties
| Name | Type | Default | Description |
|---|---|---|---|
onFrame | () => void | The paint. Called at most once per frame. | |
shouldSkip? | () => boolean | 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) => void) => number | Injectable rAF (defaults to the platform one, with a setTimeout fallback). | |
cancelFrame? | (handle: number) => void | Injectable cancel, must pair with requestFrame. |
RenderSchedulerStats
Cheap counters — a steady-state idle canvas should paint 0 frames.
tsinterface RenderSchedulerStats
Properties
| Name | Type | Default | Description |
|---|---|---|---|
scheduled | number | schedule() calls. | |
painted | number | Frames actually painted (onFrame ran). | |
skipped | number | Queued frames dropped by shouldSkip(). | |
coalesced | number | schedule() calls that folded into an already-queued frame. | |
lastFrameMs | number | Duration (ms) of the most recent paint. |
Types
CreateDiagram
The factory's own signature, for hosts that store it.
tstype CreateDiagram = typeof import('./create-diagram').createDiagram;
DiagramEventHandler
tstype DiagramEventHandler<K extends DiagramEventName> = (
payload: DiagramEventMap[K]
) => void;
DiagramEventName
Also has every member of String, listed on its own entry.
tstype DiagramEventName = keyof DiagramEventMap;
EdgeInput
Also has every member of EdgeSpec, listed on its own entry.
tstype EdgeInput = EdgeSpec | LinkModel;
GroupLabelPlacement
Also has every member of String, listed on its own entry.
Where a zone's caption sits.
tstype GroupLabelPlacement = 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right';
NodeInput
Also has every member of NodeSpec, listed on its own entry.
Nodes/edges may be handed in as plain specs or as live engine models.
tstype NodeInput = NodeSpec | NodeModel;
Was this page helpful?