Grafloria keeps graph intent in the model and derives node positions and link paths when it lays out and renders the diagram. You describe relationships such as “connect api to wallets” and “avoid obstacles”; you do not need to store the pixels that satisfy those relationships.
How the parts work together
mermaidflowchart LR I["Nodes, groups, and edges"] --> M["DiagramModel: semantic document"] M --> L["DiagramEngine.layout()"] L --> P["Node positions"] M --> R["Router"] R --> Q["Link paths"] P --> V["Rendered diagram"] Q --> V
The model stores node and group relationships, ports, link endpoints, routing choices, and optional waypoints. A layout algorithm assigns positions from that information. The router then turns the endpoints and routing choice into a path. When a node moves, its links follow; an obstacle-avoiding route can recalculate around the new obstacle positions.
This separation also keeps persistence meaningful: save the document's intent and constraints, not a collection of coordinates that only describes one rendering.
Describe relationships as data
Use render to mount a real diagram and receive its DiagramInstance. The sample uses an architecture layout so the nodes need no hand-written positions. The source, target, and sourceHandle fields express the relationship and attachment; router expresses how the path should travel.
tsimport { render } from '@grafloria/element';
const container = document.getElementById('diagram')!;
container.style.height = '400px';
const edges = [
{
id: 'api-to-wallets',
source: 'api',
target: 'wallets',
sourceHandle: 'right',
targetHandle: 'left',
router: 'avoid',
connector: 'rounded',
label: 'requests',
},
];
const instance = render(
{
layout: 'architecture',
nodes: [
{ id: 'api', label: 'API', size: { width: 150, height: 70 } },
{ id: 'wall', label: 'Obstacle', size: { width: 150, height: 110 } },
{ id: 'wallets', label: 'Wallets', size: { width: 150, height: 70 } },
],
edges,
},
container,
);
instance.fitView();
html<div id="diagram" style="height: 400px"></div>
The mounted instance shows labelled API, Obstacle, and Wallets nodes with a rounded, obstacle-aware requests link. fitView() frames the content in the sized container. The instance is the handle you keep when the application needs to repaint, query the model, or invoke the engine.
Read type and router as separate decisions
An EdgeSpec answers two different questions:
typedescribes the line's shape: direct, smooth, orthogonal, or bezier.routerdescribes the path it takes:straight,orthogonal,manhattan, oravoid.
For example, an orthogonal edge can use the obstacle-avoiding router while retaining right-angle geometry:
tsconst orthogonalEdge = {
source: 'api',
target: 'wallets',
type: 'orthogonal',
router: 'avoid',
connector: 'rounded',
};
Leave handles out when the renderer can choose the default port facing the other node. Use a port id or a side such as 'right' when the attachment is part of the diagram's meaning. Use waypoints only when the document must preserve explicit interior bends; those points are world coordinates, not a replacement for the relationship between the endpoints.
Choose and rerun a layout
The DiagramEngine owns layout behavior. Obtain it from the mounted instance and call layout() with a registered name and optional layout settings:
tsimport { render } from '@grafloria/element';
const container = document.getElementById('diagram')!;
container.style.height = '400px';
const instance = render(
{
nodes: [
{ id: 'api', label: 'API' },
{ id: 'wallets', label: 'Wallets' },
],
edges: [{ source: 'api', target: 'wallets', router: 'avoid' }],
},
container,
);
async function layOutLeftToRight(): Promise<void> {
const engine = instance.getEngine();
await engine.layout('dagre', { rankSpacing: 80 });
instance.renderNow();
}
void layOutLeftToRight();
The call commits new node positions to the live model and returns a layout result; the repaint then shows the new positions and recomputed link routes. Calling engine.layout() with no name selects the automatic layout, while an unknown name throws instead of silently doing nothing.
The built-in choices cover common graph shapes: use elk or dagre for layered flows, tree for hierarchies, force or community for networks, and grid, circular, or radial for catalogs. Use architecture when zones and their internal rows are part of the visual structure rather than a ranking problem.
What belongs in the document
Store the facts that remain true when the drawing changes:
- node and group ids, labels, data, ports, and membership;
- link endpoints, endpoint handles, labels, connector and router choices;
- deliberate waypoints when a user-edited bend must round-trip.
Do not treat the current node positions or generated route polyline as the graph's business meaning. A layout recomputes positions from the graph, and a router recomputes paths from endpoints, obstacles, anchors, and waypoints. This is why the same document can be laid out again after loading, resizing, or moving a node.
For a hand-edited edge, the endpoint remains semantic and the saved points or waypoints preserve the user's route. Reconnecting an endpoint runs connection validation again; bending a path updates the model and remains undoable through the shared command history.
Was this page helpful?