# Navigate diagrams

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](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/build-runnable-workflows): this page adds the minimap, zoom-level controls and rendered-tier feedback, plus touch navigation. The Angular example uses [`DiagramCanvasComponent`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-angular-components-diagramcanvascomponent).

:::code-group
```js title="JavaScript"
import { 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();
```
```ts title="Angular"
import { 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;
  }
}
```
```tsx title="Qwik"
import { 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>
  );
});
```
```tsx title="React"
import { 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 title="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: none` prevents native browser gestures from interrupting pointer movement.
- The zoom values in the sample are choices for its buttons, not universal thresholds. `minZoom` and `maxZoom` clamp camera movement; use the tier readout and your own diagram to choose useful stops.

## See it running

- [Minimap & controls](https://grafloria.com/demos/misc/minimap-and-controls.html) — see the live minimap, its moving camera rectangle, zoom controls, and fit-view behavior. [Demo source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/misc/minimap-and-controls.html)
- [Contextual zoom (LOD)](https://grafloria.com/demos/interaction/contextual-zoom.html) — compare the high, medium, sketch, and low tiers as zoom changes. [Demo source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/contextual-zoom.html)
- [Touch device](https://grafloria.com/demos/interaction/touch-device.html) — try pan, pinch, tap-to-select, and node dragging on a touch screen or in device emulation. [Demo source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/touch-device.html)

## Related

- [DiagramInstance](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-diagraminstance)
- [Canvas plugins](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-ext-components)
- [The element API](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core)
- [Angular canvas component](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-angular-components-diagramcanvascomponent)
