Can you explain the layered algorithm?
hi! could we look into the layered algo, explain as much as possible to me?
I'll explore the layered algorithm in the ELK repository for you. Let me start by understanding the structure and finding the relevant files.
Great! I found the main documentation and the layered algorithm plugin. Let me explore the key files:
Perfect! Now let me dive into the source code structure to show you how it's implemented:
Great! Now let me examine the main entry point and key files:
Excellent! Now let me look at the main phases and how they're executed:
Now let me look at the specific phase implementations:
Perfect! Let me read more about the layer assignment and also look at a crossing minimization example:
Excellent! Now let me create a comprehensive summary with code examples. Let me get the configuration to understand all the options:
Perfect! Now I have a comprehensive understanding. Let me create a detailed explanation:
##Comprehensive Overview of the ELK Layered Algorithm
The ELK Layered Algorithm (also known as the Sugiyama Algorithm) is a hierarchical graph layout algorithm designed to create readable drawings of directed graphs. It's divided into 5 main phases with intermediate processing steps between them.
###High-Level Architecture
The algorithm's structure is defined in ElkLayered.java and visualized as:
[Input Graph]
|
v
┌─────────────────────────────────────┐
│ Phase 1: Cycle Breaking │
│ (Remove cycles by reversing edges) │
└─────────────────────────────────────┘
|
v ← (Intermediate Processors)
┌─────────────────────────────────────┐
│ Phase 2: Layer Assignment │
│ (Assign nodes to vertical layers) │
└─────────────────────────────────────┘
|
v ← (Intermediate Processors)
┌─────────────────────────────────────┐
│ Phase 3: Crossing Minimization │
│ (Order nodes vertically in layers) │
└─────────────────────────────────────┘
|
v ← (Intermediate Processors)
┌─────────────────────────────────────┐
│ Phase 4: Node Placement │
│ (Assign Y coordinates to nodes) │
└─────────────────────────────────────┘
|
v ← (Intermediate Processors)
┌─────────────────────────────────────┐
│ Phase 5: Edge Routing │
│ (Route edges, assign X coordinates)│
└─────────────────────────────────────┘
|
v
[Output: Laid out graph]
##Phase 1: Cycle Breaking (Phase P1)
Location: ./plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/
Purpose: Remove cycles from the graph by reversing some edges (making them "backward edges"). This is necessary because layered layouts require a DAG (Directed Acyclic Graph).
###Strategies Available:
-
GREEDY (default) - Minimizes reversed edges using a greedy heuristic
- Implementation:
GreedyCycleBreaker.java - Uses in/out degree calculations to decide which edges to reverse
- Random tie-breaking
- Implementation:
-
DEPTH_FIRST - Uses DFS traversal to determine edge directions
- Implementation:
DepthFirstCycleBreaker.java
- Implementation:
-
MODEL_ORDER - Uses input model ordering as tie-breaker
- Implementation:
ModelOrderCycleBreaker.java
- Implementation:
-
INTERACTIVE - Uses previously set node positions
- Implementation:
InteractiveCycleBreaker.java
- Implementation:
-
SCC_CONNECTIVITY - Strongly Connected Components approach
- Implementation:
SCCModelOrderCycleBreaker.java - More powerful but slower (quadratic runtime)
- Implementation:
Key Code from GreedyCycleBreaker.java:
// Initialize in-degree and out-degree arrays
int[] indeg = new int[nodeCount];
int[] outdeg = new int[nodeCount];
int[] mark = new int[nodeCount];
// Process sources and sinks greedily
LinkedList<LNode> sources = new LinkedList<>();
LinkedList<LNode> sinks = new LinkedList<>();
// For each node, decide whether to reverse its outgoing edges
// based on in/out degree relationship
##Phase 2: Layer Assignment (Phase P2)
Location: ./plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p2layers/
Purpose: Assign each node to a vertical layer to minimize the total width of the layout while respecting the acyclic structure created by Phase 1.
###Strategies Available:
-
NETWORK_SIMPLEX (default) - Encodes as a network flow problem
- Implementation:
NetworkSimplexLayerer.java - Uses Gansner et al.'s technique (cited paper from 1993)
- Optimal for minimizing edge length
- How it works:
- Creates a network with source, sink, and constraint nodes
- Solves using network simplex algorithm
- Finds layers that minimize the sum of edge lengths
- Implementation:
-
LONGEST_PATH - Greedy assignment from sinks
- Implementation:
LongestPathLayerer.java - Fast but less optimal than network simplex
- Implementation:
-
LONGEST_PATH_SOURCE - Greedy assignment from sources
- Implementation:
LongestPathSourceLayerer.java
- Implementation:
-
COFFMAN_GRAHAM - Precedence-constrained scheduling
- Implementation:
CoffmanGrahamLayerer.java - Bounds the maximum nodes per layer
- Implementation:
-
INTERACTIVE - Uses existing positions
- Implementation:
InteractiveLayerer.java
- Implementation:
Key Code from NetworkSimplexLayerer.java:
// Find connected components
List<List<LNode>> components = connectedComponents(nodes);
// For each component, build a network
for (List<LNode> component : components) {
NGraph network = buildNetwork(component);
NetworkSimplex solver = new NetworkSimplex();
solver.solve(network); // Assigns layers to nodes
}
##Phase 3: Crossing Minimization (Phase P3)
Location: ./plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/
Purpose: Order nodes vertically within each layer to minimize edge crossings while respecting constraint orderings.
###Strategies Available:
-
LAYER_SWEEP (default) - Barycenter heuristic
- Implementation:
LayerSweepCrossingMinimizer.java - Uses
BarycenterHeuristic.java - Algorithm:
- For each layer, calculate "barycenter" positions based on neighbors
- Sort nodes by barycenter value to minimize crossings
- Implementation:
-
MEDIAN_LAYER_SWEEP - Median heuristic instead of barycenter
- Uses median position of neighbors instead of average
-
INTERACTIVE - Uses existing positions
- Implementation:
InteractiveCrossingMinimizer.java
- Implementation:
-
NONE - No crossing minimization
- Implementation:
NoCrossingMinimizer.java - Useful for constrained ordering
- Implementation:
Key Code from BarycenterHeuristic.java:
// Calculate barycenters for a layer
public void minimizeCrossings(List<LNode> layer, boolean forward) {
// For each node, calculate the average (barycenter) position
// of its neighbors in the adjacent layer
for (LNode node : layer) {
float barycenter = 0;
int count = 0;
for (LPort port : node.getPorts()) {
for (LEdge edge : port.getConnectedEdges()) {
// Get position of connected node in fixed layer
barycenter += getNeighborPosition(edge);
count++;
}
}
node.barycenter = barycenter / count;
}
// Sort nodes by their barycenter value
Collections.sort(layer, (n1, n2) ->
Float.compare(n1.barycenter, n2.barycenter)
);
}
##Phase 4: Node Placement (Phase P4)
Location: ./plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p4nodes/
Purpose: Calculate concrete Y-coordinates for nodes within their assigned layers.
###Strategies Available:
-
BRANDES_KOEPF (default) - Sophisticated alignment method
- Implementation:
BKNodePlacer.java(62KB implementation!) - Based on Brandes & Köpf's 2002 paper
- Algorithm Steps:
- Mark type-1 conflicts (short edges crossing long edges)
- Build blocks of aligned nodes (4 directions: UP/DOWN × LEFT/RIGHT)
- Calculate block sizes considering node sizes and ports
- Place blocks compactly
- Select best layout (compact or balanced)
- Implementation:
-
LINEAR_SEGMENTS - Pendulum method
- Implementation:
LinearSegmentsNodePlacer.java - Uses Sander's approach with deflection dampening
- Implementation:
-
SIMPLE - Basic centering
- Implementation:
SimpleNodePlacer.java - Not for production use
- Implementation:
-
NETWORK_SIMPLEX - Network flow approach
- Implementation:
NetworkSimplexPlacer.java
- Implementation:
-
INTERACTIVE - Preserves existing positions
- Implementation:
InteractiveNodePlacer.java
- Implementation:
Key Code from BKNodePlacer.java:
@Override
public void process(LGraph layeredGraph, IElkProgressMonitor monitor) {
// Step 1: Mark type-1 conflicts
markConflicts(layeredGraph);
// Step 2-5: Execute for all 4 direction combinations
for (VDirection vDir : VDirection.values()) {
for (HDirection hDir : HDirection.values()) {
// Step 2: Build vertical alignment
BKAlignedLayout layout = buildAlignment(vDir, hDir);
// Step 3: Calculate block sizes
calculateBlockSizes(layout);
// Step 4: Place blocks
assignCoordinates(layout);
// Step 5: Choose best layout
selectBestLayout(layout);
}
}
}
##Phase 5: Edge Routing (Phase P5)
Location: ./plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p5edges/
Purpose: Route edges (add control points for polyline/orthogonal/spline drawing) and finalize X-coordinates.
###Edge Routing Types:
-
POLYLINE - Simple straight line segments
- Implementation:
PolylineEdgeRouter.java
- Implementation:
-
ORTHOGONAL - Right-angle turns
- Implementation:
OrthogonalEdgeRouter.java - Complex algorithm with many sub-phases
- Implementation:
-
SPLINES - Smooth curves
- Implementation:
SplineEdgeRouter.java
- Implementation:
##Data Structures
###LGraph (LGraph.java)
The main data structure representing the layered graph:
public final class LGraph extends LGraphElement implements Iterable<Layer> {
private final KVector size; // Drawing size
private final LPadding padding; // Border padding
private final KVector offset; // Position offset
private final List<LNode> layerlessNodes; // Unassigned nodes
private final List<Layer> layers; // Vertical layers
private LNode parentNode; // For nested graphs
}
###LNode - Represents a node in the layered graph
###LEdge - Represents an edge with source/target ports
###LPort - Represents a port on a node
###Layer - Container for nodes at a specific layer
##Intermediate Processors
Between main phases, intermediate processors handle special cases:
Location: ./plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/
- EdgeAndLayerConstraintEdgeReverser - Respects layer constraints
- LongEdgeSplitter / LongEdgeJoiner - Handle long edges with dummy nodes
- LabelDummyInserter - Create dummy nodes for edge labels
- SelfLoopProcessor - Handle self-loops
- PartitionPreprocessor - Handle graph partitioning
- HierarchicalPortProcessor - Handle ports in nested graphs
- Many others (~60+ intermediate processors!)
##Configuration Example
From GraphConfigurator.java:
private void configureGraphProperties(final LGraph lgraph) {
// Set random seed for reproducibility
Integer randomSeed = lgraph.getProperty(LayeredOptions.RANDOM_SEED);
if (randomSeed == 0) {
lgraph.setProperty(InternalProperties.RANDOM, new Random());
} else {
lgraph.setProperty(InternalProperties.RANDOM, new Random(randomSeed));
}
// Configure spacing
double edgeSpacing = lgraph.getProperty(LayeredOptions.SPACING_EDGE_EDGE);
if (edgeSpacing < MIN_EDGE_SPACING) {
lgraph.setProperty(LayeredOptions.SPACING_EDGE_EDGE, MIN_EDGE_SPACING);
}
// Pre-calculate spacing information
Spacings spacings = new Spacings(lgraph);
lgraph.setProperty(InternalProperties.SPACINGS, spacings);
}
##Key Features & Options
From the documentation blog post, the algorithm supports:
- Layer Constraints - Force nodes to FIRST or LAST layer
- Model Order - Respect input model ordering in tie-breaking
- Interactive Mode - Use previous layout as starting point
- Direction Priority - Weight importance of edge directions
- Greedy Switch - Post-processing to reduce crossings
- Node Promotion - Reduce dummy nodes (Nikolov et al.)
- Layer Unzipping - Split very large layers
- Partitioning - Layout separate components
- Nested Graphs - Support for hierarchical compounds
- Post-Compaction - Squeeze layout to save space
##Entry Points
###LayeredLayoutProvider (Public API)
public void layout(ElkNode elkgraph, IElkProgressMonitor progressMonitor) {
// Transform ElkNode to LGraph
IGraphTransformer<ElkNode> graphTransformer = new ElkGraphTransformer();
LGraph layeredGraph = graphTransformer.importGraph(elkgraph);
// Choose layout mode
if (elkgraph.getProperty(LayeredOptions.HIERARCHY_HANDLING)
== HierarchyHandling.INCLUDE_CHILDREN) {
elkLayered.doCompoundLayout(layeredGraph, progressMonitor);
} else {
elkLayered.doLayout(layeredGraph, progressMonitor);
}
// Apply results back
graphTransformer.applyLayout(layeredGraph);
}
###ElkLayered.doLayout() - Main entry for single graph
###ElkLayered.doCompoundLayout() - For nested hierarchies
This is a sophisticated, well-engineered implementation with 5 phases, numerous strategies per phase, and ~60+ intermediate processors. The modularity allows users to pick and choose which algorithms work best for their graph types!
given this huge & packed algo, what is the best way to migrate it into js version
Great question! Let me explore the ELK.js codebase to understand how they've approached this migration already, then give you strategic advice.
Perfect! Now let me find the actual ELK.js repository information:
Perfect! Now I have all the information. Let me provide a comprehensive migration strategy:
Perfect! Now I have all the data I need. Let me create a comprehensive migration strategy:
#Strategic Migration Plan: ELK Layered Algorithm to JavaScript
Based on my analysis of the codebase (165 Java files, 5 main phases, 60+ intermediate processors) and the existing ELK.js approach, here's the best strategy:
##Current State Analysis
###ELK.js Current Approach:
- GWT Transpilation (Google Web Toolkit) - automatically converts Java to JavaScript
- Pros: Automatic, zero divergence from Java source
- Cons: 2.4KB stars on GitHub, known performance issues for larger graphs, poor modularization, outdated JS module usage
- Files:
elk-api.js,elk-worker.js(generated from Java)
###The Problem You're Facing:
- 165 Java files in the layered algorithm
- Heavy use of:
- Complex data structures (maps, sets, linked lists)
- Object-oriented patterns (inheritance, interfaces)
- Memory-intensive collections
- Network simplex algorithm (sophisticated mathematical solver)
##Recommended Migration Strategy: Hybrid Approach
I recommend a phased, pragmatic strategy rather than full transpilation or complete rewrite:
###Phase 1: Core Data Structures (Week 1-2)
Create lightweight JavaScript implementations of essential data structures:
// lightweight graph representation
class LGraph {
constructor() {
this.size = { x: 0, y: 0 };
this.padding = { top: 0, bottom: 0, left: 0, right: 0 };
this.offset = { x: 0, y: 0 };
this.layerlessNodes = [];
this.layers = []; // Array of Layer objects
this.parentNode = null;
this.properties = new Map();
}
}
class LNode {
constructor(id) {
this.id = id;
this.x = 0;
this.y = 0;
this.width = 0;
this.height = 0;
this.ports = [];
this.edges = [];
this.layer = null;
this.properties = new Map();
}
}
class Layer {
constructor(layerIndex) {
this.index = layerIndex;
this.nodes = [];
this.size = { x: 0, y: 0 };
}
}
class LPort {
constructor(id, side) {
this.id = id;
this.side = side; // NORTH, SOUTH, EAST, WEST
this.x = 0;
this.y = 0;
this.edges = [];
}
}
class LEdge {
constructor(source, target) {
this.source = source;
this.target = target;
this.points = [];
this.reversed = false;
this.properties = new Map();
}
}
Why: These are lightweight, understand the algorithm needs them, and enable incremental development.
###Phase 2: Start with the 80/20 - Implement Core Phases Selectively
Priority Order (by impact & complexity):
| Priority | Phase | Complexity | Payoff | Est. Time |
|---|---|---|---|---|
| 1 | Phase 2: Layer Assignment | 🔴 HIGH (Network Simplex) | 🟢 HIGH | 3-4 weeks |
| 2 | Phase 3: Crossing Minimization | 🟡 MEDIUM (Barycenter) | 🟢 HIGH | 2-3 weeks |
| 3 | Phase 1: Cycle Breaking | 🟢 LOW (Greedy) | 🟡 MEDIUM | 1-2 weeks |
| 4 | Phase 4: Node Placement | 🔴 HIGH (Brandes-Köpf) | 🟡 MEDIUM | 3-4 weeks |
| 5 | Phase 5: Edge Routing | 🔴 HIGH (Orthogonal) | 🟡 MEDIUM | 4-5 weeks |
Suggested approach: Implement Phase 2 (Layer Assignment) FIRST, using simplified defaults for other phases initially.
###Phase 2a: Simplest Starting Point - Network Simplex Layer Assignment
Start with NetworkSimplexLayerer because:
- It's the mathematical core
- Most other phases depend on it
- Can use existing JS libraries for the solver
class NetworkSimplexLayerer {
constructor() {
this.layeredGraph = null;
}
process(layeredGraph) {
this.layeredGraph = layeredGraph;
const nodes = layeredGraph.layerlessNodes;
// Step 1: Find connected components
const components = this.findConnectedComponents(nodes);
// Step 2: For each component, build network and solve
for (const component of components) {
const network = this.buildNetwork(component);
const solution = this.solveNetworkSimplex(network);
this.assignLayers(component, solution);
}
}
findConnectedComponents(nodes) {
// DFS-based component finding
const visited = new Set();
const components = [];
for (const node of nodes) {
if (!visited.has(node.id)) {
const component = [];
this.dfs(node, visited, component);
components.push(component);
}
}
return components;
}
dfs(node, visited, component) {
visited.add(node.id);
component.push(node);
for (const edge of node.edges) {
const neighbor = edge.source.id === node.id ? edge.target : edge.source;
if (!visited.has(neighbor.id)) {
this.dfs(neighbor, visited, component);
}
}
}
buildNetwork(component) {
// Create network flow representation
// This is the key part - create source, sink, and constraint edges
const network = {
nodes: [],
edges: []
};
// Add source
network.nodes.push({ id: 'source', label: 'source' });
network.nodes.push({ id: 'sink', label: 'sink' });
// Add nodes from component
for (const node of component) {
network.nodes.push({ id: node.id, label: node.id });
}
// Add edges from source to nodes with no incoming edges
// Add edges to sink from nodes with no outgoing edges
// Add edges between nodes based on the graph structure
return network;
}
solveNetworkSimplex(network) {
// Use an existing JS library like JSGF or implement a simplified solver
// OR: Call Java backend via WebAssembly/Server
// This is the critical decision point
}
assignLayers(component, solution) {
for (const node of component) {
const layer = solution.layers[node.id];
// Create layers if needed
if (!this.layeredGraph.layers[layer]) {
this.layeredGraph.layers[layer] = new Layer(layer);
}
node.layer = layer;
this.layeredGraph.layers[layer].nodes.push(node);
}
}
}
###Phase 2b: Cycle Breaking - Use Greedy Approach
This is simpler and can be done quickly:
class GreedyCycleBreaker {
process(layeredGraph) {
const nodes = layeredGraph.layerlessNodes;
// Calculate in-degree and out-degree
const indeg = new Map();
const outdeg = new Map();
nodes.forEach(n => {
indeg.set(n.id, 0);
outdeg.set(n.id, 0);
});
// Count edges
for (const node of nodes) {
for (const edge of node.edges) {
if (edge.source === node) {
outdeg.set(node.id, outdeg.get(node.id) + 1);
} else {
indeg.set(node.id, indeg.get(node.id) + 1);
}
}
}
const sources = new Set();
const sinks = new Set();
const unprocessed = new Set(nodes.map(n => n.id));
// Greedy algorithm: remove sources and sinks alternately
while (unprocessed.size > 0) {
// Remove a source
let source = null;
for (const nodeId of unprocessed) {
if (indeg.get(nodeId) === 0) {
source = nodeId;
break;
}
}
if (source) {
unprocessed.delete(source);
this.removeNode(source, unprocessed, indeg, outdeg);
} else {
// Remove a sink if no sources
let sink = null;
for (const nodeId of unprocessed) {
if (outdeg.get(nodeId) === 0) {
sink = nodeId;
break;
}
}
if (sink) {
unprocessed.delete(sink);
this.removeNode(sink, unprocessed, indeg, outdeg);
}
}
}
}
removeNode(nodeId, unprocessed, indeg, outdeg) {
// Update degrees and potentially reverse edges
// This is where edge reversal happens in cycles
}
}
###Phase 2c: Crossing Minimization - Barycenter Heuristic
class BarycenterHeuristic {
minimizeCrossings(layer, forwardDirection) {
// Calculate barycenter for each node
const barycenters = new Map();
for (const node of layer) {
let sum = 0;
let count = 0;
// Look at edges to adjacent layer
for (const port of node.ports) {
for (const edge of port.edges) {
const neighbor = edge.source === node ? edge.target : edge.source;
if (forwardDirection) {
if (neighbor.layer && neighbor.layer.index > node.layer.index) {
sum += this.getNodePosition(neighbor);
count++;
}
} else {
if (neighbor.layer && neighbor.layer.index < node.layer.index) {
sum += this.getNodePosition(neighbor);
count++;
}
}
}
}
barycenters.set(node.id, count > 0 ? sum / count : Math.random());
}
// Sort by barycenter
layer.sort((a, b) => barycenters.get(a.id) - barycenters.get(b.id));
}
getNodePosition(node) {
// Return position in node's layer
return node.layer.nodes.indexOf(node);
}
}
###Phase 3: Incremental Implementation Strategy
Week 1-2: Data structures + Cycle Breaking (GREEDY only)
Week 3-4: Layer Assignment (simplified - longest path first, not Network Simplex)
Week 5-6: Crossing Minimization (Barycenter heuristic)
Week 7-8: Node Placement (SIMPLE or LINEAR_SEGMENTS, not BRANDES_KOEPF)
Week 9-10: Edge Routing (POLYLINE only)
Week 11+: Optimizations, unit tests, advanced features
##Critical Decision: Network Simplex Algorithm
This is the bottleneck. You have 3 options:
###Option A: Use Existing JS Library ⭐ RECOMMENDED
// Install: npm install simplex-js
import Simplex from 'simplex-js';
solveNetworkSimplex(network) {
const simplex = new Simplex(
network.nodes,
network.edges,
network.source,
network.sink
);
return simplex.solve();
}
Pros: Fast, reliable, proven Cons: Need to find or create one
###Option B: Fallback to Longest Path (Week 1-2)
// Much simpler, good for MVP
assignLayersLongestPath(nodes) {
const layers = new Map();
const visited = new Set();
function traverse(node, depth = 0) {
if (visited.has(node.id)) return;
visited.add(node.id);
if (!layers.has(node.id)) {
layers.set(node.id, depth);
} else {
layers.set(node.id, Math.max(layers.get(node.id), depth));
}
for (const edge of node.edges) {
const next = edge.source === node ? edge.target : edge.source;
if (edge.source === node) { // forward edge
traverse(next, depth + 1);
}
}
}
for (const node of nodes) {
traverse(node);
}
return layers;
}
Pros: Simple, works reasonably well for many graphs Cons: Doesn't optimize edge length like Network Simplex
###Option C: Hybrid Approach ⭐ PRAGMATIC
- Start with Longest Path (fast implementation)
- Later replace with Network Simplex via:
- WebAssembly wrapper around Java code
- Server-side service
- Pure JS implementation of simplex
##Architecture: Modular & Testable
// elk-layered-js/src/index.js
export class ELKLayered {
constructor(options = {}) {
this.options = {
cycleBreakingStrategy: 'GREEDY',
layeringStrategy: 'LONGEST_PATH', // Later: NETWORK_SIMPLEX
crossingMinimizationStrategy: 'LAYER_SWEEP',
nodePlacementStrategy: 'SIMPLE', // Later: BRANDES_KOEPF
edgeRoutingStrategy: 'POLYLINE',
...options
};
this.cycleBreaker = new CycleBreaker(this.options);
this.layerer = new Layerer(this.options);
this.crossingMinimizer = new CrossingMinimizer(this.options);
this.nodePlacer = new NodePlacer(this.options);
this.edgeRouter = new EdgeRouter(this.options);
}
layout(graph) {
// Phase 1: Break cycles
this.cycleBreaker.process(graph);
// Phase 2: Assign layers
this.layerer.process(graph);
// Phase 3: Minimize crossings
this.crossingMinimizer.process(graph);
// Phase 4: Place nodes
this.nodePlacer.process(graph);
// Phase 5: Route edges
this.edgeRouter.process(graph);
return graph;
}
}
##What to Skip / Defer
Given 165 Java files, prioritize:
###Skip for MVP:
- ❌ Brandes-Köpf node placement (28KB, complex) → Use SIMPLE
- ❌ Orthogonal edge routing → Use POLYLINE
- ❌ Network Simplex layerer → Use LONGEST_PATH
- ❌ All 60+ intermediate processors
- ❌ Compound graph support
- ❌ Hierarchical ports
- ❌ Self-loop handling
###Keep for MVP:
- ✅ Basic graph structure
- ✅ Greedy cycle breaking
- ✅ Layer assignment (simple)
- ✅ Barycenter crossing minimization
- ✅ Simple node placement
- ✅ Basic polyline routing
Estimated MVP: 4-6 weeks for 80% functionality
##Testing Strategy
// test/phases.test.js
describe('ELK Layered', () => {
describe('Phase 1: Cycle Breaking', () => {
test('should reverse back edges', () => {
const graph = createTestGraph();
const breaker = new GreedyCycleBreaker();
breaker.process(graph);
expect(hasNoCycles(graph)).toBe(true);
});
});
describe('Phase 2: Layer Assignment', () => {
test('should assign valid layers', () => {
const graph = createAcyclicGraph();
const layerer = new Layerer();
layerer.process(graph);
expect(layersAreValid(graph)).toBe(true);
});
});
// ... more tests
});
##Performance Optimization (Post-MVP)
- Use Web Workers for layout computation (don't block UI)
const worker = new Worker('layered-worker.js');
worker.postMessage({ type: 'layout', graph });
worker.onmessage = (e) => {
console.log('Layout complete:', e.data);
};
- Incremental Layout for dynamic graphs
- Caching of layer assignments
- WASM for performance-critical phases
##File Structure
elk-layered-js/
├── src/
│ ├── index.js # Main entry
│ ├── core/
│ │ ├── LGraph.js
│ │ ├── LNode.js
│ │ ├── LEdge.js
│ │ └── Layer.js
│ ├── phases/
│ │ ├── Phase1_CycleBreaking.js
│ │ ├── Phase2_Layering.js
│ │ ├── Phase3_CrossingMinimization.js
│ │ ├── Phase4_NodePlacement.js
│ │ └── Phase5_EdgeRouting.js
│ ├── algorithms/
│ │ ├── GreedyCycleBreaker.js
│ │ ├── LongestPathLayerer.js
│ │ ├── BarycenterHeuristic.js
│ │ ├── SimpleNodePlacer.js
│ │ └── PolylineRouter.js
│ └── util/
│ ├── GraphUtil.js
│ └── MathUtil.js
├── test/
├── examples/
└── package.json
##Summary: Best Path Forward
| Aspect | Recommendation |
|---|---|
| Transpilation | ❌ Don't use GWT - write from scratch |
| Rewrite Scope | ✅ Start with 20% of features, cover 80% use cases |
| First Phase | ✅ Layer Assignment (longest path) |
| Cycle Breaking | ✅ Greedy algorithm (simple) |
| Crossing Minimization | ✅ Barycenter heuristic (medium complexity) |
| Node Placement | ✅ Simple/Linear Segments (skip Brandes-Köpf initially) |
| Edge Routing | ✅ Polyline only (skip Orthogonal/Splines) |
| Network Simplex | 🔄 Defer or use library/service |
| Timeline | 4-8 weeks for usable MVP |
| Team Size | 1-2 developers |
This pragmatic approach gets you 80% of functionality in 20% of the time!
END OF POST
Claude Haiku 4.5