Skip to content
D
Documentation

How Grafloria works

concept
3 min readUpdated

Grafloria puts one headless engine underneath its JavaScript, React, Vue, Angular, and Qwik surfaces. The engine owns behavior—commands, history, layout, validation, and collaboration—while the model owns the diagram data.

mermaid
flowchart TD
  B["Framework binding"] --> I["DiagramInstance"]
  I --> M["DiagramModel\nnodes, links, groups, viewport"]
  I --> E["DiagramEngine\ncommands, layout, validation, history"]
  M --> R["Renderer\npositions, routes, SVG"]
  E --> R

Start at the binding

Use the binding's canvas component when your application already uses a framework. The component mounts a real diagram; its nodes and edges describe the initial or controlled graph, and its instance callback gives you the shared handle.

React

tsx
import { GrafloriaFlow } from '@grafloria/react';

const nodes = [
  { id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

export default function App() {
  return (
    <div style={{ height: '100vh' }}>
      <GrafloriaFlow
        defaultNodes={nodes}
        defaultEdges={edges}
        plugins
        onInit={(instance) => instance.fitView()}
      />
    </div>
  );
}

The mounted canvas shows two connected nodes. plugins adds the minimap, zoom and fit controls, and background grid. The onInit callback receives the live instance after mounting.

Vue

vue
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';

const nodes = [
  { id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
</script>

<template>
  <div style="height: 100vh">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :plugins="true" />
  </div>
</template>

Vue renders the same connected diagram. Use v-model:nodes and v-model:edges when the Vue application owns the live arrays; use the default-* props when the canvas owns them after mount.

Angular

ts
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';

@Component({
  selector: 'app-root',
  imports: [DiagramCanvasComponent],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      [plugins]="true" style="display:block; height:100vh" />
  `,
})
export class AppComponent {
  nodes = [
    { id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
    { id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
  ];
  edges = [{ id: 'e1', source: 'a', target: 'b' }];
}

DiagramCanvasComponent uses Angular's two-way nodes and edges binding here. A drag changes the live model and writes the next arrays back through those bindings.

Qwik

tsx
import { component$, $ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';

const nodes = [
  { id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

export default component$(() => (
  <div style={{ height: '100vh' }}>
    <GrafloriaFlow
      defaultNodes={nodes}
      defaultEdges={edges}
      onInit$={$((instance: DiagramInstance) => instance.fitView())}
    />
  </div>
));

Qwik uses a QRL callback, but the callback receives the same instance and the canvas shows the same graph. The binding is a thin surface over the shared engine rather than a separate graph engine.

The model is the document

The model is the single source of truth. A DiagramModel contains nodes, links, groups, and viewport data. A node is a NodeModel; an input edge uses EdgeSpec. A GroupSpec describes a zone around child nodes.

Bindings turn plain specs into live models and reconcile later changes by id. Existing ids keep their live objects, new ids create objects, and missing ids are removed. This lets selection, listeners, and renderer state survive ordinary updates. The same document can be persisted, snapshotted, exported, or shared.

For data queries, use instance.getModel(). For example, the model gives you getNodes(), getLinks(), and getGroups() without reaching into the renderer.

The instance is the shared handle

Plain JavaScript uses render to mount a data spec and return the live DiagramInstance:

ts
import { render } from '@grafloria/element';

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

const instance = render({
  nodes: [
    { id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
    { id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
  ],
  edges: [{ id: 'e1', source: 'a', target: 'b' }],
}, host);

instance.fitView();
instance.on('selection:change', ({ nodes }) => console.log(nodes));

Use the instance for specs, painting, events, camera, export, and text round-trips. setNodes() and setEdges() reconcile data; render() queues a coalesced repaint and renderNow() repaints synchronously; fitView() frames all content. Dispose the instance from your application's unmount or close handler, not immediately after mounting.

Geometry comes from layout and routing

Nodes, ports, links, groups, directions, and constraints express semantic intent. Rendering and layout determine positions and paths. Use the DiagramEngine for behavior below the instance:

ts
import type { DiagramInstance } from '@grafloria/renderer';

async function arrange(instance: DiagramInstance): Promise<void> {
  await instance.getEngine().layout('elk');
  instance.renderNow();
}

This lays out the already-mounted diagram and then paints the resulting geometry. Changing node data does not re-run a declarative layout prop; call the engine's layout method explicitly and repaint when you drive the instance yourself.

Commands, history, and events

User gestures become commands on one history. Dragging, connecting, deleting, and grouping therefore share undo and redo without application wiring. In React, Vue, and Qwik, reach the engine through the instance; Angular's canvas also exposes its corresponding component methods.

ts
import type { DiagramInstance } from '@grafloria/renderer';

async function undoAndRedo(instance: DiagramInstance): Promise<void> {
  await instance.getEngine().undo();
  await instance.getEngine().redo();
}

The instance event map is the common event surface. on() returns an unsubscribe function, and the framework bindings surface the same changes as React callbacks, Vue emits, Angular outputs, or Qwik QRL callbacks. Use the component event first when the binding provides it; use instance.on() for events that belong to the shared instance.

Appearance and extension points

The renderer ships LIGHT_THEME and DARK_THEME. Pass a theme to the component or call instance.setTheme() to change the mounted diagram's appearance. The renderer also ships registerTool for canvas tools; it returns a disposer that restores the previous tool with the same id.

Use the shipped layout adapters, themes, commands, and validators before writing replacements. The live demo gallery shows the same mounted surfaces in operation.

Next steps

Was this page helpful?