Skip to content
D
Documentation

Model and documents

concept
2 min readUpdated

Grafloria keeps diagram data in a headless model and uses one JSON document to persist that data. A framework binding may give you friendlier specs, but the same nodes, links, groups, ports, and viewport sit underneath each binding.

mermaid
flowchart TD
  S["spec or JSON"] --> R["render()"]
  R --> I["DiagramInstance"]
  I --> M["DiagramModel"]
  M --> N["NodeModel"]
  N --> P["PortModel"]
  M --> L["LinkModel"]
  M --> G["GroupModel"]
  M --> J["JSON document"]
  J --> F["fromDocument()"]
  F --> R

The diagram model

DiagramModel is the document's root. It owns maps of nodes, links, and groups, plus the viewport. Use the model when you need to inspect or mutate diagram data; use the engine for behavior such as commands, history, validation, and layout.

Nodes

NodeModel represents a rendered item. Its type selects the renderer, position and size describe its geometry, and data carries your application payload. Set a label or other application value with setData() rather than putting application state in geometry.

Every node receives four deterministic bidirectional ports—top, right, bottom, and left—when it is constructed. That gives an ordinary flowchart connection points without extra configuration. Add a PortModel when a connection needs explicit direction, a data type, a side, a label, or connection limits.

LinkModel connects a source port to a target port. Its pathType expresses the intended geometry (direct, orthogonal, smooth, or bezier); routing and painting turn that intent into pixels. Pin an endpoint to a port by storing that port's ID. A link can also use the node's available ports without custom port setup.

Groups

GroupModel is a semantic container, not a visual annotation. Its members can move together, it can collapse or expand, and groups can nest. Add members by ID after adding both the group and the member nodes to the diagram.

This small model creates two nodes, connects their right and left ports, and puts them in a group:

ts
import {
  DiagramModel,
  GroupModel,
  LinkModel,
  NodeModel,
} from '@grafloria/engine';

const diagram = new DiagramModel('order-flow');

const intake = new NodeModel({
  id: 'intake',
  type: 'task',
  position: { x: 40, y: 70 },
  size: { width: 140, height: 60 },
});
intake.setData('label', 'Intake');

const review = new NodeModel({
  id: 'review',
  type: 'task',
  position: { x: 280, y: 70 },
  size: { width: 140, height: 60 },
});
review.setData('label', 'Review');

diagram.addNode(intake);
diagram.addNode(review);

const sourcePort = intake.getPortBySide('right');
const targetPort = review.getPortBySide('left');
if (!sourcePort || !targetPort) {
  throw new Error('The default ports are missing');
}

diagram.addLink(new LinkModel(sourcePort.id, targetPort.id, 'orthogonal'));

const reviewGroup = new GroupModel({ id: 'review-group', name: 'Order review' });
diagram.addGroup(reviewGroup);
reviewGroup.addMember('intake', diagram);
reviewGroup.addMember('review', diagram);

The model now contains two nodes, one link, and one group with two members. The port IDs—not screen coordinates—define the link endpoints, so moving a node does not change which ports the link connects.

The document is the persistence boundary

DiagramSerializer converts a model to a plain serializable object and restores a model from that object. The serialized diagram includes the schema version, identity and metadata, name, nodes, links, groups, and viewport. Save the result as JSON; do not save a renderer instance or a framework component.

The serializer also accepts the portable document envelope. Its checksum is verified during loading, and schema migrations run through DiagramModel.fromJSON().

Restore a saved document into a real canvas

fromDocument restores a saved document to a loaded model/spec. render also accepts the saved JSON string directly. The following is a complete browser entry point. The sized host matters: without a height, the renderer has no visible canvas.

ts
import { DiagramSerializer } from '@grafloria/engine';
import { fromDocument, render } from '@grafloria/element';
import {
  DiagramModel,
  GroupModel,
  LinkModel,
  NodeModel,
} from '@grafloria/engine';

const host = document.getElementById('canvas');
if (!host) {
  throw new Error('Missing #canvas');
}
host.style.height = '400px';

const diagram = new DiagramModel('order-flow');
const intake = new NodeModel({
  id: 'intake',
  type: 'task',
  position: { x: 40, y: 70 },
  size: { width: 140, height: 60 },
});
intake.setData('label', 'Intake');
const review = new NodeModel({
  id: 'review',
  type: 'task',
  position: { x: 280, y: 70 },
  size: { width: 140, height: 60 },
});
review.setData('label', 'Review');
diagram.addNode(intake);
diagram.addNode(review);

const sourcePort = intake.getPortBySide('right');
const targetPort = review.getPortBySide('left');
if (!sourcePort || !targetPort) {
  throw new Error('The default ports are missing');
}
diagram.addLink(new LinkModel(sourcePort.id, targetPort.id, 'orthogonal'));

const group = new GroupModel({ id: 'review-group', name: 'Order review' });
diagram.addGroup(group);
group.addMember('intake', diagram);
group.addMember('review', diagram);

const serializer = new DiagramSerializer();
const savedJson = JSON.stringify(serializer.serialize(diagram));
const loaded = fromDocument(savedJson);
console.log(loaded.model.getNodes().length);
const instance = render(savedJson, host);

console.log(instance.getModel().getNodes().length);

The mounted canvas shows the two nodes, their orthogonal link, and the group frame. Loading the JSON reconstructs the model data; rendering still belongs to the new live DiagramInstance, so renderer functions and other runtime wiring are not part of the document.

Choose the right layer

  • Use the instance for rendering, specs, events, viewport operations, and export.
  • Use the model for node, link, group, port, and document data.
  • Use the engine for behavior such as layout, validation, and history.

For the next layer of detail, read Ports and connection rules, Groups and containment, or Text and lossless round trips.

Was this page helpful?