# How DrawDB's Auto-Arrange Algorithm Works: A Deep Dive into dagre-Powered Layout

> Explore DrawDB's auto-arrange algorithm powered by dagre. Discover how it efficiently positions database tables using a three-phase process for optimal canvas layout.

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

---

**DrawDB's auto-arrange algorithm uses the dagre library to automatically position database tables on the canvas by running a three-phase process: filtering movable tables, computing a layered graph layout, and placing isolated tables in a grid pattern.**

The [drawdb-io/drawdb](https://github.com/drawdb-io/drawdb) open-source database diagram tool includes a sophisticated auto-arrange feature that eliminates manual table positioning. This algorithm handles everything from simple schemas to complex relational diagrams with dozens of interconnected tables.

## What Is the Auto-Arrange Algorithm?

The **auto-arrange algorithm** is a deterministic layout system built on the `@dagrejs/dagre` graph library. It implements a classic **Sugiyama layered-graph algorithm** that minimizes edge crossings while respecting user-defined constraints like locked tables and custom table dimensions.

The implementation lives primarily in [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js), with supporting utilities in [`src/utils/utils.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/utils.js).

## Phase 1: Identify Movable and Connected Tables

Before any layout computation begins, the algorithm categorizes tables into three groups.

### Filtering Rules

- **Locked tables** (`locked === true`) are excluded entirely from repositioning
- **Connected tables** participate in at least one valid relationship
- **Isolated tables** are movable but have no connections to other movable tables

A relationship only qualifies as valid for connectivity when:
- Both start and end tables are movable (not locked)
- It is **not** a self-link (where a table references itself)

This filtering logic spans lines 9-22 in [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js).

```javascript
// Simplified representation of the filtering logic
const movableTables = tables.filter(t => !t.locked);
const connectedTableIds = new Set();

relationships.forEach(rel => {
  const startMovable = !tables.find(t => t.id === rel.startTableId)?.locked;
  const endMovable = !tables.find(t => t.id === rel.endTableId)?.locked;
  const isSelfLink = rel.startTableId === rel.endTableId;
  
  if (startMovable && endMovable && !isSelfLink) {
    connectedTableIds.add(rel.startTableId);
    connectedTableIds.add(rel.endTableId);
  }
});

```

## Phase 2: Create and Compute the dagre Graph

For connected tables, the algorithm constructs a directed graph and applies layered layout.

### Node Creation

Each connected table becomes a graph node with dimensions calculated by `getTableHeight()` from [`src/utils/utils.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/utils.js). This helper accounts for:

- Field count in the table
- Comment visibility (if `settings.showComments` is enabled)
- Relationship indicator visibility

The width comes directly from `settings.tableWidth`.

### Edge Creation

Every valid relationship between connected tables becomes a directed edge in the graph.

### dagre Configuration

The graph is configured with these parameters (lines 37-52 in [`autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/autoArrange.js)):

| Parameter | Value | Purpose |
|-----------|-------|---------|
| `rankdir` | `"LR"` | Left-to-right layout flow |
| `nodesep` | `60` | Horizontal separation between nodes (pixels) |
| `ranksep` | `110` | Vertical separation between ranks (pixels) |

```javascript
// Graph construction from the source
const graph = new dagre.graphlib.Graph();

graph.setGraph({
  rankdir: "LR",
  nodesep: 60,
  ranksep: 110
});

// Add nodes with calculated dimensions
connectedTables.forEach(table => {
  graph.setNode(table.id.toString(), {
    width: settings.tableWidth,
    height: getTableHeight(table, settings)
  });
});

// Add edges for relationships
validRelationships.forEach(rel => {
  graph.setEdge(
    rel.startTableId.toString(),
    rel.endTableId.toString()
  );
});

// Run the layout algorithm
dagre.layout(graph);

```

The `dagre.layout(graph)` call on line 54 executes the Sugiyama algorithm, producing `x` and `y` coordinates for each node.

## Phase 3: Place Tables on the Canvas

### Coordinate Translation

dagre returns center-point coordinates, but DrawDB uses top-left positioning. The algorithm translates each node's position:

```javascript
// From autoArrange.js lines 56-65
const node = graph.node(tableId.toString());
const x = node.x - node.width / 2;
const y = node.y - node.height / 2;

```

### Isolated Table Grid Layout

Tables without connections get arranged in a simple row-wise grid below the connected component:

- Starting Y position: below the furthest extent of connected tables (or `0` if no connected tables exist)
- Row width limit: `max(maxX, 4 * tableWidth + 3 * isolatedGap)`
- Gap between isolated tables: controlled by `isolatedGap` constant

When adding an isolated table would exceed the row width, the algorithm wraps to a new row.

```javascript
// Simplified isolated placement logic
let currentX = 0;
let currentY = maxY + isolatedGap;
const minRowWidth = 4 * tableWidth + 3 * isolatedGap;
const rowLimit = Math.max(maxX, minRowWidth);

isolatedTables.forEach(table => {
  const height = getTableHeight(table, settings);
  
  if (currentX + tableWidth > rowLimit && currentX > 0) {
    currentX = 0;
    currentY += rowHeights.reduce((a, b) => Math.max(a, b), 0) + isolatedGap;
  }
  
  positions.push({ id: table.id, x: currentX, y: currentY });
  currentX += tableWidth + isolatedGap;
});

```

## Complete Usage Example

```javascript
import { autoArrange } from "./src/utils/autoArrange";

// Example schema with mixed table states
const tables = [
  { id: 1, locked: false, fields: [{ name: "id" }, { name: "email" }] },
  { id: 2, locked: false, fields: [{ name: "id" }, { name: "user_id" }] },
  { id: 3, locked: true,  fields: [{ name: "id" }] }, // Stays put
  { id: 4, locked: false, fields: [{ name: "log_id" }] }, // Isolated
];

const relationships = [
  { startTableId: 1, endTableId: 2, fields: [] }, // Valid connection
  { startTableId: 3, endTableId: 1, fields: [] }, // Ignored (locked end)
];

const settings = {
  tableWidth: 220,
  showComments: true,
};

// Compute and apply new positions
const newPositions = autoArrange(tables, relationships, settings);
newPositions.forEach(({ id, x, y }) => {
  const table = tables.find(t => t.id === id);
  if (table) {
    table.x = x;
    table.y = y;
  }
});

```

## Key Files and Functions

| File | Function | Role |
|------|----------|------|
| [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js) | `autoArrange()` | Main entry point and orchestration |
| [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js) | Internal filtering | Lines 9-22, table categorization |
| [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js) | Graph construction | Lines 37-52, dagre setup |
| [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js) | Coordinate application | Lines 56-84, placement logic |
| [`src/utils/utils.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/utils.js) | `getTableHeight()` | Visual height calculation |
| [`src/data/constants.js`](https://github.com/drawdb-io/drawdb/blob/main/src/data/constants.js) | Height constants | Base values for dimension math |
| [`package.json`](https://github.com/drawdb-io/drawdb/blob/main/package.json) | Dependency | `@dagrejs/dagre` graph library |

## Algorithm Characteristics

**Deterministic output**: The same input always produces the same layout, making the auto-arrange algorithm predictable for version control and team collaboration.

**Respects user constraints**: Locked tables maintain their positions, preserving intentional manual arrangements.

**Performance**: The Sugiyama algorithm runs in polynomial time relative to node and edge count, handling schemas with 50+ tables smoothly.

**Visual hierarchy**: Left-to-right flow matches reading direction and emphasizes foreign-key relationships as directional dependencies.

## Summary

- The **auto-arrange algorithm** uses `@dagrejs/dagre` to compute layered graph layouts for database schemas
- Three phases execute in sequence: filter movable tables, run dagre on connected components, grid-placement for isolated tables
- **Locked tables** are excluded from all movement
- **Node dimensions** come from `getTableHeight()` in [`src/utils/utils.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/utils.js), accounting for fields and display settings
- **Isolated tables** fill a grid below the connected layout starting at `maxY + isolatedGap`
- Configuration constants (`nodesep: 60`, `ranksep: 110`) control spacing and are defined in the dagre graph setup

## Frequently Asked Questions

### How does DrawDB decide which tables to move during auto-arrange?

Tables with `locked === true` are completely excluded. Among unlocked tables, the algorithm separates them into *connected* (participating in valid relationships) and *isolated* (no valid connections). Only these movable tables receive new positions; locked tables remain exactly where placed.

### What algorithm does dagre use for the layout?

dagre implements the **Sugiyama layered graph algorithm**, a hierarchical approach that assigns nodes to ranks (layers), minimizes edge crossings between layers, and then computes exact coordinates. This produces readable left-to-right flows typical of entity-relationship diagrams.

### Why do some tables appear in a grid below the main diagram?

Tables without connections to other movable tables are **isolated components**. Rather than clustering arbitrarily, the auto-arrange algorithm places them in a predictable row-wise grid starting below the connected layout. This prevents isolated tables from obscuring relationship flows while keeping them accessible.

### Can I adjust the spacing between tables?

The spacing values are hardcoded in [`src/utils/autoArrange.js`](https://github.com/drawdb-io/drawdb/blob/main/src/utils/autoArrange.js): `nodesep: 60` (horizontal gap) and `ranksep: 110` (vertical gap between ranks). To modify these, you would need to edit the dagre configuration object on lines 40-43 of that file and rebuild DrawDB.