# DiagramCanvas

A frame that a picture larger than it can be panned and zoomed inside.

- Group: Diagrams
- Import: `import { DiagramCanvas } from '@misoto22/folio/diagrams'`
- Page: https://ui.misoto22.com/components/diagram-canvas/
- Related: diagram-minimap, architecture-figure, scroll-area

## When to reach for it

Any oversized figure — an SVG, an image, a table that will not fold. It knows nothing about nodes, which is what makes it reusable.

## Anatomy

- **Frame** (required) — The outer box: a fixed height — 24rem unless height says otherwise — a hairline, the diagram ground, and overflow hidden. It is the window the artwork is bigger than.
- **Viewport** (required) — The focusable layer inside it: role="group" with a name, tabIndex 0, the drag handlers, the key handler and a focus ring drawn inside the frame. This is the tab stop, and it is what makes the keys pressable at all.
- **Stage** (required) — The wrapper the translate and scale are applied to. The transform is on the wrapper and never on the child, so the figure inside keeps its own coordinate space and every measurement taken inside it stays true.
- **Zoom controls** — The cluster pinned to the bottom corner: zoom out, the current percentage — which is also the reset button, named for what it does — and zoom in. controls={false} removes the cluster and leaves the keys.
- **Keyboard hint** (required) — A visually hidden paragraph, referenced by aria-describedby, saying that the frame drags and which keys pan, zoom and reset. Nothing on screen carries that sentence.

## Best practices

### Do

- Name it. label is the group’s accessible name and it defaults to "Diagram canvas", so two canvases on one page announce the same thing until each is given its own.
- Strip the figure inside to its artwork with heading={false}, legend="hidden" and cards={false}. Everything in the frame pans and zooms together, so a title left on travels away from the diagram it names and a key leaves the frame at exactly the zoom that made a reader want it.
- Move the view through the ref — zoomIn, zoomOut, reset, centerOn — rather than re-rendering the child at a new size. centerOn takes a point in the content’s own coordinates, which is what a minimap reports back.
- Hand onViewChange straight to a minimap. Each view carries the frame it was measured against as well as the scale and the offset, and that pair is the whole of the arithmetic a viewport rectangle is — the frame being the one number nothing outside the canvas can measure.

### Don’t

- Nothing clamps the pan. The offset is whatever the drag or the arrow keys left it at, so the artwork can be pushed entirely outside the frame; 0 and the reset button are the whole way back, and controls={false} without a replacement takes the pointer’s half of that away.
- The scale stops at 0.35. A figure more than about three times the frame cannot be zoomed out far enough to be seen whole, so the frame has to be sized for the diagram — a minimap answers where you are, never what is there.
- The wheel is left alone and the thumb is not: the frame sets touch-action to none, so a finger dragged inside it pans the diagram and never scrolls the page. A full-width canvas in an article is a band a touch reader has to swipe around rather than through, which is the argument for giving it a height short enough to leave page beside it.

## Accessibility

- The frame is a real tab stop, so the keyboard controls can be pressed at all.
- A plain wheel scrolls the page. Zoom needs the platform modifier, so the canvas is never a scroll trap in the middle of an article.

## Keyboard

- + / = — Zooms in about the centre of the frame.
- - — Zooms out.
- 0 — Resets the scale and the offset together.
- ← / → / ↑ / ↓ — Pans. Shift pans further per press.

## DiagramCanvas

A frame that a picture larger than it can be moved around inside. Pan with a drag, zoom with the controls or with ⌘/Ctrl and the wheel, and reset with a key. That is the whole of it — this is deliberately a VIEWPORT and not a diagram editor: nothing here knows what a node is, so it works for any oversized figure, an SVG, an image, a table that will not fold. Three decisions here are not obvious. **A plain wheel scrolls the page, not the diagram.** A canvas that swallows the wheel is a scroll trap: a reader flicking down an article hits the figure and the page stops moving for no reason they can see. Zooming needs the modifier — which is also the platform gesture for zoom everywhere else, and is what a trackpad pinch already sends. **The transform is on a wrapper, not on the content.** The child keeps its own coordinate space, so a figure inside can still be measured, exported and read by anything that walks it. Scaling the child directly would make every `getBoundingClientRect` inside it a lie. **Keyboard first, and the frame is a real tab stop.** `+` / `-` / `0` and the arrow keys move the view; the frame takes focus so they can be pressed at all. A canvas that only answers a drag is a canvas half the readers cannot operate.

### Props

- `children` (required) — `ReactNode`.
- `className` — `string`.
- `height` — `string` default `'24rem'`. How tall the frame is. Anything CSS accepts.
- `onViewChange` — `(view: CanvasView) => void`. Told about every view change, for a minimap or a percentage readout.
- `controls` — `boolean` default `true`. Hides the built-in zoom controls, for a caller supplying its own.
- `label` — `string` default `'Diagram canvas'`. Names the region for assistive tech.
- `ref` — `Ref<DiagramCanvasHandle>`.

## Example — default

```tsx
import { ArchitectureFigure, DiagramCanvas } from '@misoto22/folio/diagrams'

<DiagramCanvas height="16rem" label="Request path">
  <ArchitectureFigure spec={SPEC} heading={false} legend="hidden" cards={false} />
</DiagramCanvas>
```

## Example — your own controls

```tsx
import { Button } from '@misoto22/folio'
import { ArchitectureFigure, DiagramCanvas, type DiagramCanvasHandle } from '@misoto22/folio/diagrams'

<div className="flex flex-col gap-3">
  <DiagramCanvas ref={canvas} controls={false} height="13rem" label="Request path">
    <ArchitectureFigure spec={SPEC} heading={false} legend="hidden" cards={false} />
  </DiagramCanvas>
  <div className="flex items-center gap-2">
    <Button size="sm" variant="ghost" iconOnly aria-label="Zoom out" onClick={() => canvas.current?.zoomOut()}>
      <RiSubtractLine size={14} aria-hidden />
    </Button>
    <Button size="sm" variant="ghost" iconOnly aria-label="Zoom in" onClick={() => canvas.current?.zoomIn()}>
      <RiAddLine size={14} aria-hidden />
    </Button>
    <Button size="sm" variant="secondary" onClick={() => canvas.current?.reset()}>
      <RiResetLeftLine size={14} aria-hidden /> Reset
    </Button>
  </div>
</div>
```

## Example — following the view

```tsx
import { Button } from '@misoto22/folio'

<div className="flex flex-col gap-3">
  <DiagramCanvas ref={canvas} height="13rem" label="Request path" onViewChange={setView}>
    <ArchitectureFigure spec={SPEC} heading={false} legend="hidden" cards={false} />
  </DiagramCanvas>
  <div className="flex flex-wrap items-center gap-4">
    <span className="mono-meta text-(--ink-2)">
      {Math.round(view.scale * 100)}% at {Math.round(view.x)}, {Math.round(view.y)}
    </span>
    <Button size="sm" variant="secondary" onClick={() => canvas.current?.centerOn(820, 84)}>
      Centre on the far end
    </Button>
  </div>
</div>
```
