Import these from @grafloria/renderer.
On their own pages
CreateDiagramOptionsDiagramInstanceEdgeSpec: An edge, as a host hands it in. Node-to-node, like React Flow.PortSpec: 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
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.
tsfunction buildPort(nodeId: string, spec: PortSpec, index: number): PortModel
contentBounds
World bounding box of every visible node, 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}.
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.
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).
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(): 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 |
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, …).
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. |
NodeSpec
tsinterface NodeSpec
Properties
| Name | Type | Default | Description |
|---|---|---|---|
id? | string | ||
type? | string | Engine 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? | string | Convenience for metadata.label. | |
sublabel? | string | NodeSublabel | A 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 } | null | A 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? | boolean | Render 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.
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 | () | 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.
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
tstype DiagramEventName = keyof DiagramEventMap;
EdgeInput
tstype EdgeInput = EdgeSpec | LinkModel;
GroupLabelPlacement
Where a zone's caption sits.
tstype 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.
tstype NodeInput = NodeSpec | NodeModel;
Was this page helpful?