Add viewport controls, make visible detail respond to zoom, and support touch navigation in a mounted diagram. The canvas and camera share one headless model across framework bindings.
Add a minimap, controls, and zoom-level feedback
Use the shipped canvas plugins for the minimap, zoom/fit toolbar, and dotted background; the plugin-enabled canvas also supports the built-in pan, zoom, tap-selection, node-drag, and pinch gestures. Add your own zoom choices through the mounted instance when you want a predictable detail readout: the renderer chooses a level-of-detail tier from the current zoom. The JavaScript, Angular, React, and Vue samples disable the adaptive quality governor so the tier follows zoom directly, matching the contextual-zoom demo.
Each sample mounts a small connected diagram and leaves room for touch navigation. JavaScript, Angular, Qwik, React, and Vue show the minimap and shipped zoom/fit controls; only React and Vue add buttons with a tier readout. Angular displays its chosen zoom. The renderer changes detail as the camera zooms, without application code rewriting node data.
Build on the mounting and typed-data pattern in Build runnable workflows: this page adds the minimap, zoom-level controls and rendered-tier feedback, plus touch navigation. The Angular example uses DiagramCanvasComponent.
jsimport { render } from '@grafloria/element';
import { attachCanvasPlugins } from '@grafloria/renderer';
const nodes = Array.from({ length: 9 }, (_, i) => ({
id: `n${i}`,
position: { x: 90 + (i % 3) * 300, y: 70 + Math.floor(i / 3) * 190 },
size: { width: 170, height: 74 },
data: { label: `Step ${i + 1}` },
}));
const edges = Array.from({ length: 8 }, (_, i) => ({
id: `e${i}`, source: `n${i}`, target: `n${i + 1}`,
}));
const app = document.createElement('main');
app.style.height = '100vh';
const host = document.createElement('div');
host.style.cssText = 'height:100%;touch-action:none';
document.body.append(app);
app.append(host);
const instance = render({ nodes, edges }, host, { renderer: { qualityGovernor: false } });
attachCanvasPlugins(instance, {
background: { variant: 'dots' },
minimap: true,
controls: true,
});
instance.fitView(40);
instance.renderNow();
tsimport { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<main style="height:100vh;display:flex;flex-direction:column">
<div>
<button type="button" (click)="setZoom(1.5)">1.5×</button>
<button type="button" (click)="setZoom(0.7)">0.7×</button>
<button type="button" (click)="setZoom(0.3)">0.3×</button>
<button type="button" (click)="setZoom(0.15)">0.15×</button>
<output>zoom {{ zoom }}×</output>
</div>
<grafloria-diagram-canvas
[(nodes)]="nodes"
[(edges)]="edges"
[plugins]="true"
[rendererConfig]="{ qualityGovernor: false }"
style="display:block;flex:1;min-height:0;touch-action:none" />
</main>
`,
})
export class NavigateDiagramsComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
zoom = 1;
nodes: NodeSpec[] = Array.from({ length: 9 }, (_, i) => ({
id: `n${i}`,
position: { x: 90 + (i % 3) * 300, y: 70 + Math.floor(i / 3) * 190 },
size: { width: 170, height: 74 },
data: { label: `Step ${i + 1}` },
}));
edges: EdgeSpec[] = Array.from({ length: 8 }, (_, i) => ({
id: `e${i}`, source: `n${i}`, target: `n${i + 1}`,
}));
ngAfterViewInit(): void {
this.canvas().fitToContent(40);
}
setZoom(zoom: number): void {
this.canvas().viewportController()?.setZoom(zoom);
this.zoom = zoom;
}
}
tsximport { component$, $, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type EdgeSpec, type NodeSpec } from '@grafloria/qwik';
const nodes: NodeSpec[] = Array.from({ length: 9 }, (_, i) => ({
id: `n${i}`,
position: { x: 90 + (i % 3) * 300, y: 70 + Math.floor(i / 3) * 190 },
size: { width: 170, height: 74 },
data: { label: `Step ${i + 1}` },
}));
const edges: EdgeSpec[] = Array.from({ length: 8 }, (_, i) => ({
id: `e${i}`, source: `n${i}`, target: `n${i + 1}`,
}));
export default component$(() => {
const ready = useSignal(false);
return (
<main style={{ height: '100vh', display: 'flex', flexDirection: 'column', touchAction: 'none' }}>
<output>{ready.value ? 'Diagram ready' : 'Loading diagram'}</output>
<div style={{ flex: '1', minHeight: '0' }}>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
plugins
rendererConfig={{ qualityGovernor: false }}
onInit$={$(() => { ready.value = true; })}
/>
</div>
</main>
);
});
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance, EdgeSpec, NodeSpec } from '@grafloria/react';
const nodes: NodeSpec[] = Array.from({ length: 9 }, (_, i) => ({
id: `n${i}`,
position: { x: 90 + (i % 3) * 300, y: 70 + Math.floor(i / 3) * 190 },
size: { width: 170, height: 74 },
data: { label: `Step ${i + 1}` },
}));
const edges: EdgeSpec[] = Array.from({ length: 8 }, (_, i) => ({
id: `e${i}`, source: `n${i}`, target: `n${i + 1}`,
}));
export default function NavigateDiagrams() {
const instance = useRef<DiagramInstance | null>(null);
const [tier, setTier] = useState('');
const zoomTo = (zoom: number) => {
const api = instance.current;
if (!api) return;
api.viewport.setZoom(zoom);
api.renderNow();
setTier(`Selected ${zoom}×; zoom ${api.viewport.getZoom()}× — ${api.getQualityState().tier}`);
};
return (
<main style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<div>
<button onClick={() => zoomTo(1.5)}>1.5×</button>
<button onClick={() => zoomTo(0.7)}>0.7×</button>
<button onClick={() => zoomTo(0.3)}>0.3×</button>
<button onClick={() => zoomTo(0.15)}>0.15×</button>
<output>{tier}</output>
</div>
<div style={{ flex: 1, minHeight: 0, touchAction: 'none' }}>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
plugins
rendererConfig={{ qualityGovernor: false }}
onInit={(api: DiagramInstance) => {
instance.current = api;
api.fitView(40);
api.renderNow();
setTier(`Current zoom ${api.viewport.getZoom()}× — ${api.getQualityState().tier}`);
}}
/>
</div>
</main>
);
}
vue<script setup lang="ts"> import { ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance, EdgeSpec, NodeSpec } from '@grafloria/vue'; const nodes: NodeSpec[] = Array.from({ length: 9 }, (_, i) => ({ id: `n${i}`, position: { x: 90 + (i % 3) * 300, y: 70 + Math.floor(i / 3) * 190 }, size: { width: 170, height: 74 }, data: { label: `Step ${i + 1}` }, })); const edges: EdgeSpec[] = Array.from({ length: 8 }, (_, i) => ({ id: `e${i}`, source: `n${i}`, target: `n${i + 1}`, })); const tier = ref(''); let instance: DiagramInstance | null = null; function zoomTo(zoom: number): void { if (!instance) return; instance.viewport.setZoom(zoom); instance.renderNow(); tier.value = `Selected ${zoom}×; zoom ${instance.viewport.getZoom()}× — ${instance.getQualityState().tier}`; } function onInit(api: DiagramInstance): void { instance = api; api.fitView(40); api.renderNow(); tier.value = `Current zoom ${api.viewport.getZoom()}× — ${api.getQualityState().tier}`; } </script> <template> <main style="height:100vh;display:flex;flex-direction:column"> <div> <button @click="zoomTo(1.5)">1.5×</button> <button @click="zoomTo(0.7)">0.7×</button> <button @click="zoomTo(0.3)">0.3×</button> <button @click="zoomTo(0.15)">0.15×</button> <output>{{ tier }}</output> </div> <div style="flex:1;min-height:0;touch-action:none"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :plugins="true" :renderer-config="{ qualityGovernor: false }" @init="onInit" /> </div> </main> </template>
In the React and Vue samples, the buttons set zoom to 1.5×, 0.7×, 0.3×, or 0.15× and the readout shows the tier reported after each selection. The tiers are high, medium, sketch, and low at those zoom levels in the contextual-zoom demo, and rendered detail falls as you zoom out. Angular displays its current zoom value. JavaScript and Qwik use the minimap and zoom/fit toolbar; Qwik's status output changes when its canvas initializes.
The minimap mirrors the nodes and camera; its camera rectangle follows navigation. On a touch screen, drag empty canvas to pan, pinch to zoom, tap a node to select it, or drag a node with one finger. The canvas container uses touch-action: none so the browser does not consume those gestures for page scrolling or zooming.
Options that affect navigation
| Option | Type | Default | What it does |
|---|---|---|---|
plugins | boolean | CanvasPluginOptions | Off unless enabled | On a framework canvas, true mounts the minimap, zoom/fit controls, and background grid. In JavaScript, configure those pieces with attachCanvasPlugins(). |
minZoom | number | 0.1 | Sets the camera's lower zoom bound. |
maxZoom | number | 3.0 | Sets the camera's upper zoom bound. |
zoomSensitivity | number | 0.1 | Sets the relative step per wheel notch or keyboard zoom. |
enablePan | boolean | Enabled | Enables canvas panning. |
enableZoom | boolean | Enabled | Enables canvas zooming. Angular names the wheel option enableMouseWheelZoom; touch zoom uses the shared gesture pipeline. |
rendererConfig.qualityGovernor | boolean | GovernorOptions | Enabled | The adaptive governor can adjust detail under load. Set it to false when you want detail tier to follow zoom alone, as in the sample. |
NodeSpec and EdgeSpec are the framework data types used for the mounted graph. The instance-level camera methods and the Angular viewportController() provide the same zoom operation; the Angular example binds the canvas through its component rather than constructing a second camera.
Use the plugins prop when its default minimap, controls, and grid suit the editor. Use attachCanvasPlugins() when mounting through render() and when you need to choose plugin options separately. The instance's viewport controller sets zoom and reads the current zoom; getQualityState() reports the tier actually rendered.
Pitfalls
- Give the canvas a resolved height. It fills its parent; a zero-height parent appears blank.
- Keep touch handling on the canvas area:
touch-action: noneprevents native browser gestures from interrupting pointer movement. - The zoom values in the sample are choices for its buttons, not universal thresholds.
minZoomandmaxZoomclamp camera movement; use the tier readout and your own diagram to choose useful stops.
See it running
- Minimap & controls — see the live minimap, its moving camera rectangle, zoom controls, and fit-view behavior. Demo source
- Contextual zoom (LOD) — compare the high, medium, sketch, and low tiers as zoom changes. Demo source
- Touch device — try pan, pinch, tap-to-select, and node dragging on a touch screen or in device emulation. Demo source
Was this page helpful?