Grafloria keeps one diagram model beneath every framework binding: nodes, links, ports, groups, and viewport data are the persistent meaning of the diagram; rendering and layout turn that meaning into geometry on screen.
How the parts fit together
mermaidflowchart LR S["Node and edge specs"] --> I["DiagramInstance"] I --> M["DiagramModel"] M --> N["nodes"] M --> L["links and ports"] M --> G["groups"] M --> D["serialized document"] D --> F["fromDocument()"] F --> I
The framework component supplies initial or controlled specs. The mounted
DiagramInstance
owns the live diagram and gives you the model through getModel(). Use that
model for queries and persistence; use getEngine() for behavior such as
layout, validation, and undo.
Nodes identify the things in the diagram
A NodeModel has an ID, a
type, geometry, ports, style, and data. The ID is the stable identity used by
links and groups. Keep it when you update a node: reconciliation can then
update the existing live model instead of treating the item as a new one.
At the spec boundary, a node can carry id, position, size, label,
data, and ports. The four default ports are deterministic; add explicit
ports when a connection must use a particular direction or port identity.
The model exposes those ports for lookup, including getPortBySide() and
getPort().
Links express relationships, not pixels
An EdgeSpec names its
endpoints with source and target. Those values are node IDs. Set
sourceHandle or targetHandle to pin an endpoint to a port; a bare side such
as 'right' is also accepted. Without a handle, the renderer chooses the
appropriate port-facing attachment as the nodes move.
type, router, connector, labels, styles, and waypoints describe link
intent. The model stores that intent, while routing and rendering calculate the
visible path. Do not persist a screen-space path as a substitute for the link's
endpoints.
Groups add membership and containment
A GroupSpec is a zone with an
ID, an optional label, and child IDs. Its bounds can fix the frame; otherwise
the frame fits its children using padding. Children become group members and
move with the group. Groups can be nested and collapsed, so they are model
entities rather than a background rectangle.
The live GroupModel
stores membership, collapse state, geometry, and layout configuration. Removing
a group through setGroups() keeps its boxes; it removes the zone, not the
nodes inside it.
One document is the persistence boundary
The DiagramModel
contains maps of nodes, links, and groups. Serialize that live model rather
than only the framework's input arrays: gestures and model-level details such
as ports, groups, metadata, and waypoints belong to the live model.
DiagramSerializer
turns the model into a plain serialized object. serializeEnvelope() wraps it
in the portable document envelope when you need generator identity and related
document metadata. The document is data; renderer callbacks and other
post-render wiring are functions, so they are not in the file and must be
supplied again when a host reloads a custom or kit-based diagram.
Save a mounted diagram and mount it again
This browser example creates two visible nodes and one link, saves the first instance's model, and mounts a second instance from the saved JSON. Both hosts have dimensions, so both diagrams paint.
jsimport { fromDocument, render } from '@grafloria/element';
import { DiagramSerializer } from '@grafloria/engine';
const nodes = [
{
id: 'intake',
type: 'rect',
position: { x: 40, y: 60 },
size: { width: 120, height: 48 },
label: 'Intake',
},
{
id: 'review',
type: 'rect',
position: { x: 260, y: 60 },
size: { width: 120, height: 48 },
label: 'Review',
},
];
const edges = [
{ id: 'intake-to-review', source: 'intake', target: 'review', label: 'checks' },
];
const firstHost = document.getElementById('first-diagram');
const secondHost = document.getElementById('second-diagram');
if (!firstHost || !secondHost) {
throw new Error('Both diagram hosts are required');
}
firstHost.style.height = '400px';
secondHost.style.height = '400px';
const first = render({ nodes, edges }, firstHost);
const saved = JSON.stringify(new DiagramSerializer().serialize(first.getModel()));
const loaded = fromDocument(saved);
const second = render({ nodes: [], edges: [] }, secondHost);
second.setNodes(loaded.nodes);
second.setEdges(loaded.edges);
first.renderNow();
second.renderNow();
The first call returns the live instance. The serializer reads its model, and
fromDocument() converts the saved object or JSON string back into a spec that
render can mount. The second canvas
therefore receives the saved node IDs, link endpoints, labels, and geometry.
Provide any application-owned custom-node painter or kit wiring again when
loading a document; those callbacks are not serializable.
Use the instance for the rendered surface and its model for document data. If you only project nodes and edges back into plain specs, details outside that projection can be lost. Use the full document path when ports, groups, metadata, or edited waypoints must survive.
Was this page helpful?