Skip to content
D
Documentation

How Grafloria works

concept
4 min readUpdated

Grafloria separates diagram data from diagram behavior: a headless model stores the document, an engine applies behavior, and a framework binding mounts the result.

mermaid
flowchart TB
  S["Node and edge specs"] --> B["Framework binding"]
  B --> I["DiagramInstance"]
  I --> M["DiagramModel\ndata"]
  I --> E["DiagramEngine\nbehavior"]
  M --> R["Renderer\npixels and export"]
  E --> R
  E --> H["Command history"]

One model, every binding

The engine is headless, so the same document model drives JavaScript, React, Vue, Angular, and Qwik. Each binding converts its component or element props into model objects; the renderer then turns those objects into the visible diagram. Learn the model and the instance once, then apply the same ideas through the binding for your framework.

The React quick start covers mounting the flow and choosing its state-ownership pattern; this page adds the path from binding props to model objects and rendered pixels. Give the canvas a resolved height so it has room to paint.

tsx
import { GrafloriaFlow } from '@grafloria/react';
import { LIGHT_THEME } from '@grafloria/renderer';
import type { DiagramInstance, EdgeSpec, NodeSpec } from '@grafloria/renderer';
import type { ReactElement } from 'react';

const nodes: NodeSpec[] = [
  { id: 'extract', position: { x: 0, y: 0 }, label: 'Extract' },
  { id: 'transform', position: { x: 240, y: 0 }, label: 'Transform' },
];

const edges: EdgeSpec[] = [
  { id: 'extract-transform', source: 'extract', target: 'transform' },
];

export function PipelineDiagram(): ReactElement {
  const onInit = (instance: DiagramInstance): void => {
    instance.fitView(24);
  };

  return (
    <div style={{ height: '320px', width: '640px' }}>
      <GrafloriaFlow
        defaultNodes={nodes}
        defaultEdges={edges}
        theme={LIGHT_THEME}
        fitView
        onInit={onInit}
      />
    </div>
  );
}

This mounts two labelled nodes and a link, then frames them in the canvas. The onInit callback receives the same instance that the other bindings expose through their ready or init mechanism. The Vue, Angular, Qwik, and JavaScript surfaces change how you mount and bind values; they still hand the same kinds of specs to the same model and renderer layers.

The instance is the facade

DiagramInstance is the first object to use after mounting. It covers the rendered surface: reconcile nodes, edges, and groups with setNodes(), setEdges(), and setGroups(); subscribe with on(); frame content with fitView(); repaint with render() or renderNow(); and export with export() or the synchronous SVG and PDF methods.

The instance exposes the two layers below it:

  • DiagramModel owns data: node, link, group, and stroke collections, plus document queries and serialization-related state.
  • DiagramEngine owns behavior: interaction, validation, layout, routing, and command execution.

Use the instance for specs and pixels, the model for data queries, and the engine for behavior. For example, instance.getModel() returns the document model and instance.getEngine() returns the behavior engine. setNodes() and setEdges() reconcile by id: existing ids keep their live objects and listeners, new ids are created, and missing ids are removed. If externally edited data reuses ids and must replace every live object, clear the relevant collection before applying it.

undo() is not a renderer-instance method. Call instance.getEngine().undo(). Angular's canvas component mirrors the operation, but React and Vue reach it through the instance.

The document is the API

The document contains nodes, links, groups, and viewport state. Specs are the input representation; live models are the objects the engine and renderer operate on. This separation lets a document round-trip through framework bindings, persistence, export, and collaboration without making the framework component the source of truth.

Mermaid-compatible text is a second, human-writable representation. instance.exportText() produces text with the lossless Grafloria sidecar by default, and instance.loadText(text) parses and reconciles it into the mounted canvas. importDiagramText also parses diagram text when you need the engine's import result directly. Pure Mermaid text is a best-effort boundary; the sidecar preserves positions and styling that Mermaid alone cannot express.

Groups are part of the document rather than a property of a node or edge. A GroupModel stores membership and the zone's geometry, so pass groups as well as nodes and edges when you need containment to survive a round trip.

Nodes receive deterministic default ports for ordinary flowcharts. When a diagram declares ports, NodeModel exposes them through getPorts() and related queries, while each PortModel carries connection intent.

A port can declare whether it accepts input, output, or both; its side and position; allowed data types; incoming and outgoing caps; whether it can start or end a link; and whether self-links or duplicate links are allowed. The engine combines that port configuration with connection anatomy and any registered validator. During a connection gesture it highlights valid targets and refuses connections that violate the resulting rules. getAvailablePorts() and canConnectTo() let code inspect the same constraints before presenting an action.

One command history for user edits

User gestures become command objects on one shared history. Dragging, connecting, deleting, pasting, and grouping therefore use the same keyboard undo and redo behavior. The framework binding re-emits the changed models after history changes, so application state follows the engine's history rather than maintaining a second stack.

Keep the intent distinction clear:

  • Build, load, import, or synchronize a document with direct model mutations such as diagram.addNode(node) and diagram.addLink(link). Setup is not a user edit, so it does not enter history.
  • Change a diagram on the user's behalf through the engine's command manager. CommandManager executes commands asynchronously, and undo() and redo() operate on the same stack as gestures.

The command manager can batch several commands into one history entry and can refuse a command before it enters history. Use the engine's command path for toolbar actions and automated edits that the user must be able to undo.

Choose the layer

Start with the framework component and its props. Capture the instance in the binding's init or ready callback when you need rendering, events, export, or reconciliation. Reach into the model for document queries, and into the engine for layout, validation, interaction configuration, or history. This keeps the application on the public facade until it needs a specific lower-layer capability.

Continue with the model and documents, the instance and lifecycle, ports and connection rules, or commands and shared history.

Was this page helpful?