Use the mounted DiagramInstance to add editor chrome, then register a tool against that same live canvas. The result is a diagram with a dotted background, minimap, zoom/fit controls, and a rotation gesture that runs through the canvas interaction lifecycle.
Before you start
Install the packages for the JavaScript entry point:
bashnpm install @grafloria/element @grafloria/engine @grafloria/renderer
Give the canvas a height. A mounted diagram without a sized container has no drawing area.
1. Mount the minimap and controls
Call render with data, then pass its returned instance to attachCanvasPlugins. The plugins mount into the diagram: the minimap mirrors the nodes and camera, the controls provide zoom and fit actions, and the background draws dots.
jsimport { render } from '@grafloria/element';
import { attachCanvasPlugins } from '@grafloria/renderer';
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: 120, height: 60 }, data: { label: 'A' } },
{ id: 'b', position: { x: 300, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'B' } },
],
edges: [{ id: 'ab', source: 'a', target: 'b' }],
}, host);
const plugins = attachCanvasPlugins(instance, {
background: { variant: 'dots' },
minimap: true,
controls: true,
});
instance.renderNow();
function disposeCanvas() {
plugins.dispose();
instance.dispose();
}
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[plugins]="true" style="display:block; height:100vh" />
`,
})
export class CanvasToolsComponent {
nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'A' } },
{ id: 'b', position: { x: 300, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'B' } },
];
edges = [{ id: 'ab', source: 'a', target: 'b' }];
}
tsximport { component$ } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
const nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'A' } },
{ id: 'b', position: { x: 300, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'B' } },
];
const edges = [{ id: 'ab', source: 'a', target: 'b' }];
export default component$(() => (
<div style={{ height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} plugins />
</div>
));
tsximport { GrafloriaFlow } from '@grafloria/react';
const nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'A' } },
{ id: 'b', position: { x: 300, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'B' } },
];
const edges = [{ id: 'ab', source: 'a', target: 'b' }];
export function CanvasTools() {
return (
<div style={{ height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} plugins />
</div>
);
}
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; const nodes = [ { id: 'a', position: { x: 60, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'A' } }, { id: 'b', position: { x: 300, y: 80 }, size: { width: 120, height: 60 }, data: { label: 'B' } }, ]; const edges = [{ id: 'ab', source: 'a', target: 'b' }]; </script> <template> <div style="height:400px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :plugins="true" /> </div> </template>
The JavaScript call returns a CanvasPlugins object. Its dispose function removes the background, minimap, controls, and their listeners. Framework bindings mount the same plugins with the plugins prop and clean them up with the component.
The shipped demo is Minimap & controls, with source.
Plugin options
| Option | Type | Default | What it does |
|---|---|---|---|
background | boolean | BackgroundOptions | off | Mounts the background; true uses defaults. |
minimap | boolean | MiniMapOptions | off | Mounts the minimap; its visibility also follows the store's showMinimap. |
controls | boolean | ControlsOptions | off | Mounts the zoom, fit, and lock toolbar. |
bindToStore | boolean | true | Keeps gridEnabled and showMinimap synchronized with the mounted components. |
2. Register a tool on the live instance
Use registerTool with a CanvasTool. The sample uses NodeModel for the live node. hitTest claims the pointer gesture; once claimed, that tool receives the gesture's move, up, and cancel lifecycle. This example rotates a node around its centre and gives the tool an explicit priority above the built-in ladder.
tsimport { render } from '@grafloria/element';
import { registerTool, type CanvasTool, type NodeModel } from '@grafloria/renderer';
const host = document.getElementById('canvas');
if (!host) throw new Error('Missing #canvas');
host.style.height = '400px';
const instance = render({
nodes: [{ id: 'turn', position: { x: 160, y: 120 }, size: { width: 180, height: 80 }, data: { label: 'Turn me' } }],
edges: [],
}, host);
let grab: { node: NodeModel; x: number; y: number; startAngle: number; startRotation: number } | null = null;
const rotateTool: CanvasTool = {
id: 'example-rotate',
priority: 10,
hitTest: (_event, hit) => !!hit.node,
onPointerDown: (event, hit) => {
const node = hit.node;
if (!node) return;
const centre = { x: node.position.x + node.size.width / 2, y: node.position.y + node.size.height / 2 };
grab = {
node,
x: centre.x,
y: centre.y,
startAngle: Math.atan2(event.world.y - centre.y, event.world.x - centre.x),
startRotation: node.rotation,
};
},
onPointerMove: (event) => {
if (!grab) return;
const angle = Math.atan2(event.world.y - grab.y, event.world.x - grab.x);
grab.node.setRotation(grab.startRotation + (angle - grab.startAngle) * 180 / Math.PI);
instance.renderNow();
},
onPointerUp: () => { grab = null; },
onCancel: () => { grab = null; },
};
const removeTool = registerTool(rotateTool);
instance.renderNow();
function disposeCanvas() {
removeTool();
instance.dispose();
}
The mounted instance receives the pointer gesture, and the node rotates as you drag over it. The disposer restores a previous tool with the same id; otherwise it unregisters this tool. Set an explicit priority whenever tools can claim the same gesture: the highest claiming priority wins, while registration order is only a fallback.
For a framework binding, keep the instance supplied by its initialization callback and register the tool there. Store the returned Disposer in the component's teardown path rather than disposing it immediately after registration. The framework-specific mounted surfaces are:
- Angular: use
onInit/the live canvas instance and call the disposer inngOnDestroy. - Qwik: use
onInit$onGrafloriaFlowand retain the disposer in the component's cleanup path. - React: use
onInitand retain the disposer in a ref; call it from effect cleanup. - Vue: use
@initand call the disposer when the component unmounts.
What you have
The canvas now renders real diagram data with a live minimap, dotted background, and viewport controls. A custom tool participates in the mounted pointer lifecycle without replacing the renderer or writing a second event pipeline.
Related
DiagramInstancefor viewport control and model access.- Angular quick start for framework setup.
Was this page helpful?