@runiq/layout-base
v1.2.0
Published
Layout engine interfaces and adapters for Runiq
Downloads
354
Readme
@runiq/layout-base
Layout engine package for Runiq diagram system. Provides automatic graph layout using ELK (Eclipse Layout Kernel).
Features
- ✅ ELK Layout Engine - Modern, actively maintained (replaced Dagre in Oct 2025)
- ✅ Multiple Algorithms - Layered (hierarchical), Force, Stress, Tree, Radial, Box
- ✅ Superior Quality - Better edge routing, crossing minimization
- ✅ Manual Positioning - Support for fixed node positions (hybrid layout)
- ✅ Flexible Configuration - Direction, spacing, and algorithm options
- ✅ High Performance - Handles 50+ nodes efficiently
- ✅ TypeScript - Full type safety
Installation
pnpm add @runiq/layout-baseUsage
Basic Usage
import { ElkLayoutEngine } from '@runiq/layout-base';
import type { DiagramAst } from '@runiq/core';
const engine = new ElkLayoutEngine();
const diagram: DiagramAst = {
astVersion: '1.0',
nodes: [
{ id: 'A', shape: 'rounded', label: 'Start' },
{ id: 'B', shape: 'rect', label: 'Process' },
{ id: 'C', shape: 'rounded', label: 'End' },
],
edges: [
{ from: 'A', to: 'B' },
{ from: 'B', to: 'C' },
],
};
const result = await engine.layout(diagram);
console.log(result.nodes); // Positioned nodes with x, y, width, height
console.log(result.edges); // Routed edges with points
console.log(result.size); // Overall diagram dimensionsLayout Options
// Direction
await engine.layout(diagram, { direction: 'LR' }); // Left-to-Right
await engine.layout(diagram, { direction: 'TB' }); // Top-to-Bottom (default)
await engine.layout(diagram, { direction: 'BT' }); // Bottom-to-Top
await engine.layout(diagram, { direction: 'RL' }); // Right-to-Left
// Spacing
await engine.layout(diagram, { spacing: 50 }); // Compact
await engine.layout(diagram, { spacing: 80 }); // Default
await engine.layout(diagram, { spacing: 150 }); // Spacious
// Combined
await engine.layout(diagram, {
direction: 'LR',
spacing: 100,
});Result Structure
interface LaidOutDiagram {
nodes: PositionedNode[];
edges: RoutedEdge[];
size: { width: number; height: number };
}
interface PositionedNode {
id: string;
x: number; // Top-left corner
y: number; // Top-left corner
width: number;
height: number;
}
interface RoutedEdge {
from: string;
to: string;
points: { x: number; y: number }[]; // Routing waypoints
}ELK Algorithm Details
The layout engine uses ELK's layered algorithm by default, which is optimal for:
- ✅ Flowcharts
- ✅ Process diagrams
- ✅ State machines
- ✅ Hierarchical graphs
- ✅ Decision trees
ELK Configuration
The engine automatically configures ELK with optimal settings:
{
'elk.algorithm': 'layered',
'elk.direction': 'DOWN',
'elk.spacing.nodeNode': '80',
'elk.layered.spacing.nodeNodeBetweenLayers': '80',
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
'elk.padding': '[top=20,left=20,bottom=20,right=20]',
}Migration from Dagre
If you were using the old Dagre-based layout engine, migration is straightforward:
Before (Dagre)
import { DagreLayoutEngine } from '@runiq/layout-base';
const engine = new DagreLayoutEngine();
const result = await engine.layout(diagram);After (ELK)
import { ElkLayoutEngine } from '@runiq/layout-base';
const engine = new ElkLayoutEngine();
const result = await engine.layout(diagram);Changes:
- ✅ Better layout quality (fewer edge crossings)
- ✅ More reliable edge routing
- ✅ Better handling of complex branching
- ✅ Support for manual positioning (future)
- ⚠️ Bundle size increased: 50KB → 300KB (acceptable tradeoff)
Performance
Benchmarks on a modern laptop:
| Nodes | Layout Time | Notes | | ----- | ----------- | --------------------------- | | 10 | ~10ms | Instant | | 50 | ~50ms | Fast | | 100 | ~100ms | Still responsive | | 500+ | ~500ms+ | May want progress indicator |
ELK is efficient for most real-world diagrams (< 100 nodes).
Why ELK?
We migrated from Dagre to ELK in October 2025 because:
- Dagre is Abandoned - No updates since 2022, multiple known bugs
- Mermaid Migrated Too - Mermaid.js moved to ELK for the same reasons
- Better Quality - ELK produces superior layouts with fewer edge crossings
- Active Maintenance - ELK is backed by Eclipse Foundation
- More Features - Multiple algorithms, better configuration options
- Manual Positioning - Supports hybrid manual/auto layout (future feature)
Available Algorithms
ELK supports multiple layout algorithms via the algorithm option:
- ✅ layered (default) - Hierarchical layout for flowcharts, processes
- ✅ force - Force-directed layout for networks, organic graphs
- ✅ stress - Stress minimization for similarity graphs
- ✅ radial - Radial tree layout
- ✅ mrtree - Multi-root tree layout
See Force-Directed Networks Guide for details.
Future Features
Planned enhancements:
- 🔜 Hybrid Layout - Mix manual + automatic positioning
- 🔜 Constraint System - Relative positioning (e.g., "B below A")
- 🔜 Subgraph Support - Nested diagram containers
- 🔜 Port Constraints - Control edge attachment points
API
ElkLayoutEngine
class ElkLayoutEngine implements LayoutEngine {
id: string; // 'elk'
supportsManualPositions: boolean; // true
layout(diagram: DiagramAst, options?: LayoutOptions): Promise<LaidOutDiagram>;
}LayoutOptions
interface LayoutOptions {
direction?: 'TB' | 'BT' | 'LR' | 'RL'; // Layout direction
spacing?: number; // Node spacing in pixels
}Examples
Flowchart
const flowchart: DiagramAst = {
astVersion: '1.0',
nodes: [
{ id: 'Start', shape: 'rounded' },
{ id: 'Process', shape: 'rect' },
{ id: 'Decision', shape: 'rhombus' },
{ id: 'Action1', shape: 'rect' },
{ id: 'Action2', shape: 'rect' },
{ id: 'End', shape: 'rounded' },
],
edges: [
{ from: 'Start', to: 'Process' },
{ from: 'Process', to: 'Decision' },
{ from: 'Decision', to: 'Action1', label: 'Yes' },
{ from: 'Decision', to: 'Action2', label: 'No' },
{ from: 'Action1', to: 'End' },
{ from: 'Action2', to: 'End' },
],
};
const layout = await engine.layout(flowchart);Linear Chain
const chain: DiagramAst = {
astVersion: '1.0',
direction: 'LR',
nodes: [
{ id: 'Step1', shape: 'rect', label: 'Input' },
{ id: 'Step2', shape: 'rect', label: 'Transform' },
{ id: 'Step3', shape: 'rect', label: 'Validate' },
{ id: 'Step4', shape: 'rect', label: 'Output' },
],
edges: [
{ from: 'Step1', to: 'Step2' },
{ from: 'Step2', to: 'Step3' },
{ from: 'Step3', to: 'Step4' },
],
};
const layout = await engine.layout(chain, { spacing: 120 });Complex Branching
const branches: DiagramAst = {
astVersion: '1.0',
nodes: Array.from({ length: 20 }, (_, i) => ({
id: `Node${i}`,
shape: 'rect',
})),
edges: [
// Linear backbone
...Array.from({ length: 19 }, (_, i) => ({
from: `Node${i}`,
to: `Node${i + 1}`,
})),
// Cross-connections
{ from: 'Node0', to: 'Node5' },
{ from: 'Node5', to: 'Node10' },
{ from: 'Node10', to: 'Node15' },
],
};
const layout = await engine.layout(branches);Testing
Run tests:
pnpm testThe package includes comprehensive tests:
- ✅ Basic layouts (single, chain, branching)
- ✅ Direction options (TB, LR, BT, RL)
- ✅ Spacing configuration
- ✅ Edge routing
- ✅ Node labels
- ✅ Different shape sizes
- ✅ Error handling
- ✅ Large diagrams (20-50 nodes)
- ✅ Deterministic output
- ✅ Edge crossing minimization
24 tests, all passing
Resources
- ELK Documentation: https://www.eclipse.org/elk/
- ELK.js (JavaScript wrapper): https://github.com/kieler/elkjs
- ELK Algorithms: https://www.eclipse.org/elk/reference.html
- Runiq Core Types:
@runiq/corepackage
License
MIT
Contributing
Issues and PRs welcome! Please include tests for new features.
Changelog
v0.1.0 (October 2025)
- ✅ Migrated from Dagre to ELK
- ✅ Implemented layered algorithm
- ✅ 24 comprehensive tests
- ✅ Direction and spacing options
- ✅ Superior layout quality
- ✅ Faster performance on large diagrams
