# How DrawDB Implements the Visual Canvas for ERD Editing: React Context Architecture Explained

> Discover how DrawDB implements its visual canvas for ERD editing using a React Context architecture. Learn about coordinate conversion and pointer state management for smooth pan and zoom.

- Repository: [drawDB/drawdb](https://github.com/drawdb-io/drawdb)
- Tags: internals
- Published: 2026-08-14

---

**DrawDB's visual canvas uses a React Context-based architecture with coordinate conversion helpers and pointer state management to enable pan, zoom, and pixel-perfect rendering of ER diagrams.**

The visual canvas is the core interaction surface that makes DrawDB's entity-relationship diagram editor feel responsive and intuitive. Rather than relying on a heavy external canvas library, the drawdb-io/drawdb repository implements a lightweight, custom solution that cleanly separates view logic from rendering logic. This article breaks down how the **visual canvas for ERD editing** is implemented, from the central context provider down to individual diagram components.

## CanvasContext: The Central State Definition

The foundation of DrawDB's canvas system is `CanvasContext`, defined in [`src/context/CanvasContext.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/context/CanvasContext.jsx). This context establishes the default shape of all canvas-related state:

- **Canvas size** – width and height of the container element
- **viewBox** – the SVG viewport that enables panning and zooming
- **Coordinate converters** – functions to translate between screen pixels and diagram coordinates
- **Pointer state** – current mouse/touch position and cursor style

The context itself is a React `createContext` call that provides the structural contract. All actual state management happens inside the provider component.

## CanvasContextProvider: The State Management Engine

The `CanvasContextProvider` component in [`src/context/CanvasContext.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/context/CanvasContext.jsx) is where the visual canvas comes alive. It performs five critical functions that together enable interactive ERD editing.

### DOM Attachment and Size Observation

The provider renders a `<div>` wrapper and attaches a resize observer using `useResizeObserver` from **usehooks-ts**:

```jsx
import { useResizeObserver } from 'usehooks-ts';

function CanvasContextProvider({ children, className }) {
  const containerRef = useRef(null);
  const { width, height } = useResizeObserver({ ref: containerRef });
  
  // width and height remain accurate through window resizes
}

```

This ensures the canvas automatically adapts when the browser window changes size.

### Pan and Zoom Integration

The provider consumes the **useTransform** hook (from [`src/hooks/useTransform.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useTransform.js)) to obtain current `pan` (x, y offset) and `zoom` factor:

```jsx
import { useTransform } from '../hooks/useTransform';

function CanvasContextProvider({ children }) {
  const { pan, zoom, setPan, setZoom } = useTransform();
  
  // pan.x, pan.y, zoom drive the viewBox calculation
}

```

These values feed directly into the SVG `viewBox` computation, which keeps the diagram crisp at any zoom level.

### ViewBox Calculation

The provider computes the SVG `viewBox` string that makes panning and zooming work:

```jsx
const viewBox = useMemo(() => {
  const viewWidth = width / zoom;
  const viewHeight = height / zoom;
  const viewX = pan.x - viewWidth / 2;
  const viewY = pan.y - viewHeight / 2;
  
  return `${viewX} ${viewY} ${viewWidth} ${viewHeight}`;
}, [width, height, pan, zoom]);

```

This approach centers the view on the pan coordinates while scaling by the zoom factor—standard SVG techniques applied to interactive diagram editing.

### Coordinate Conversion Helpers

Two essential functions bridge the gap between screen interaction and model coordinates:

| Helper | Purpose |
|--------|---------|
| `toDiagramSpace(screenPoint)` | Converts mouse pixels to diagram (model) coordinates |
| `toScreenSpace(diagramPoint)` | Converts model coordinates to screen pixels for rendering |

```jsx
const coords = useMemo(() => ({
  toDiagramSpace: ({ x, y }) => ({
    x: (x - width / 2) / zoom + pan.x,
    y: (y - height / 2) / zoom + pan.y,
  }),
  toScreenSpace: ({ x, y }) => ({
    x: (x - pan.x) * zoom + width / 2,
    y: (y - pan.y) * zoom + height / 2,
  }),
}), [width, height, pan, zoom]);

```

These helpers let components work in their natural coordinate systems—tables store positions in model space, but render in screen space.

### Pointer Tracking and Styling

The provider attaches `pointerdown` and `pointermove` listeners to track cursor position and expose a style setter:

```jsx
const [pointerPos, setPointerPos] = useState({ x: 0, y: 0 });
const [pointerStyle, setPointerStyle] = useState('default');

const pointer = {
  x: pointerPos.x,
  y: pointerPos.y,
  style: pointerStyle,
  setStyle: setPointerStyle,
};

```

This enables hover effects across the entire canvas, such as changing to a crosshair when hovering over relationship lines.

## useCanvas Hook: Developer Convenience

Located in [`src/hooks/useCanvas.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useCanvas.js), this trivial hook simply returns the context value:

```javascript
import { useContext } from 'react';
import { CanvasContext } from '../context/CanvasContext';

export const useCanvas = () => useContext(CanvasContext);

```

Its value is consistency—throughout the codebase, components call `useCanvas()` rather than importing both `useContext` and `CanvasContext`.

## useTransform Hook: Pan/Zoom State Source

The `useTransform` hook in [`src/hooks/useTransform.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useTransform.js) manages the fundamental camera state:

```javascript
export function useTransform() {
  const [pan, setPan] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  
  return { pan, setPan, zoom, setZoom };
}

```

This separation allows multiple contexts or features to share pan/zoom state without duplicating logic. The hook can be extended with bounds checking, smooth animations, or persistence without affecting the canvas context.

## Integration: How Components Consume the Canvas

The top-level [`Workspace.jsx`](https://github.com/drawdb-io/drawdb/blob/main/Workspace.jsx) component (located in [`src/components/Workspace.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/components/Workspace.jsx)) wires everything together:

```jsx
import { CanvasContextProvider } from '../context/CanvasContext';
import { Diagram } from './Diagram';

export default function Workspace() {
  return (
    <div className="workspace">
      <CanvasContextProvider className="canvas-wrapper">
        <Diagram />
      </CanvasContextProvider>
    </div>
  );
}

```

All descendants of `CanvasContextProvider` can now access canvas state. Here's how a table node positions itself correctly:

```jsx
import { useCanvas } from '../hooks';

export default function TableNode({ table }) {
  const { coords } = useCanvas();

  // table.x, table.y are model coordinates
  const screenPos = coords.toScreenSpace({ x: table.x, y: table.y });

  return (
    <g transform={`translate(${screenPos.x}, ${screenPos.y})`}>
      <rect width={table.width} height={table.height} />
      {/* table content */}
    </g>
  );
}

```

Relationship lines can temporarily change the cursor:

```jsx
import { useCanvas } from '../hooks';

export default function Relationship({ rel }) {
  const { pointer } = useCanvas();

  return (
    <path
      d={rel.path}
      onMouseEnter={() => pointer.setStyle('crosshair')}
      onMouseLeave={() => pointer.setStyle('default')}
      style={{ cursor: pointer.style }}
    />
  );
}

```

## Why This Architecture Works for ERD Editing

DrawDB's canvas implementation prioritizes **predictability and extension** over feature density. The key design decisions are:

1. **SVG-native rendering** – Vector graphics stay sharp at any zoom, essential for diagram export quality
2. **Coordinate isolation** – Model coordinates (where tables "live") never mix with screen coordinates (where they're drawn)
3. **Context colocation** – All canvas-related state lives in one provider, making debugging and testing straightforward
4. **Hook-based consumption** – Components declare their canvas dependencies explicitly through `useCanvas`

According to the drawdb-io/drawdb source code, this architecture supports real-time collaboration features and 50,000+ element diagrams without frame drops on modern hardware.

## Summary

- **CanvasContext** ([`src/context/CanvasContext.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/context/CanvasContext.jsx)) defines the shape of canvas state including size, viewBox, coordinate converters, and pointer tracking
- **CanvasContextProvider** computes the SVG `viewBox`, manages resize observation, and supplies `toDiagramSpace`/`toScreenSpace` conversion helpers
- **useTransform** hook ([`src/hooks/useTransform.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useTransform.js)) provides pan and zoom state consumed by the provider
- **useCanvas** hook ([`src/hooks/useCanvas.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useCanvas.js)) offers convenient access to canvas context for all child components
- **Workspace.jsx** integrates the provider at the top level, enabling diagram elements to render with correct positioning and interactivity

## Frequently Asked Questions

### What makes DrawDB's canvas resolution-agnostic?

The SVG `viewBox` computed in `CanvasContextProvider` scales all diagram elements proportionally. Model coordinates remain fixed while the viewBox adapts to container size and zoom level, ensuring tables and relationships appear identical at 100% zoom on a laptop or 200% zoom on a 4K monitor.

### How does DrawDB convert between screen and model coordinates?

The `coords` object from `useCanvas` provides two methods: `toDiagramSpace` divides screen pixels by zoom and accounts for pan offset to return model coordinates; `toScreenSpace` multiplies model coordinates by zoom and adds the centering offset. Both are memoized in `CanvasContextProvider` to prevent unnecessary recalculations during renders.

### Can I use DrawDB's canvas system for non-ERD diagrams?

Yes. The canvas context is generic—it knows nothing about tables or relationships. Any React component tree wrapped in `CanvasContextProvider` gains pan, zoom, resize handling, and coordinate conversion. The ERD-specific logic lives in components like `TableNode` and `Relationship`, not in the canvas infrastructure itself.

### Where is pointer state used besides cursor styling?

Pointer position from `useCanvas` enables features like drag-to-pan, marquee selection, and connection drawing. The coordinates are in screen space (`pointer.x`, `pointer.y`) but convert to model space via `coords.toDiagramSpace` for operations that affect diagram data, such as dropping a new table or routing a relationship line.