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

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 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, with supporting utilities in 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.

// 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. 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):

Parameter Value Purpose
rankdir "LR" Left-to-right layout flow
nodesep 60 Horizontal separation between nodes (pixels)
ranksep 110 Vertical separation between ranks (pixels)
// 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:

// 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.

// 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

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 autoArrange() Main entry point and orchestration
src/utils/autoArrange.js Internal filtering Lines 9-22, table categorization
src/utils/autoArrange.js Graph construction Lines 37-52, dagre setup
src/utils/autoArrange.js Coordinate application Lines 56-84, placement logic
src/utils/utils.js getTableHeight() Visual height calculation
src/data/constants.js Height constants Base values for dimension math
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, 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: 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →