Can you explain the layered algorithm?

master
c831ba4Merge pull request #1177 from skieffer/legacy-properties-in-docs
Avatar of Lichin LinLichin Linposted

hi! could we look into the layered algo, explain as much as possible to me?

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

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:

  1. 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
  2. DEPTH_FIRST - Uses DFS traversal to determine edge directions

    • Implementation: DepthFirstCycleBreaker.java
  3. MODEL_ORDER - Uses input model ordering as tie-breaker

    • Implementation: ModelOrderCycleBreaker.java
  4. INTERACTIVE - Uses previously set node positions

    • Implementation: InteractiveCycleBreaker.java
  5. SCC_CONNECTIVITY - Strongly Connected Components approach

    • Implementation: SCCModelOrderCycleBreaker.java
    • More powerful but slower (quadratic runtime)

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:

  1. 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
  2. LONGEST_PATH - Greedy assignment from sinks

    • Implementation: LongestPathLayerer.java
    • Fast but less optimal than network simplex
  3. LONGEST_PATH_SOURCE - Greedy assignment from sources

    • Implementation: LongestPathSourceLayerer.java
  4. COFFMAN_GRAHAM - Precedence-constrained scheduling

    • Implementation: CoffmanGrahamLayerer.java
    • Bounds the maximum nodes per layer
  5. INTERACTIVE - Uses existing positions

    • Implementation: InteractiveLayerer.java

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:

  1. 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
  2. MEDIAN_LAYER_SWEEP - Median heuristic instead of barycenter

    • Uses median position of neighbors instead of average
  3. INTERACTIVE - Uses existing positions

    • Implementation: InteractiveCrossingMinimizer.java
  4. NONE - No crossing minimization

    • Implementation: NoCrossingMinimizer.java
    • Useful for constrained ordering

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:

  1. 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)
  2. LINEAR_SEGMENTS - Pendulum method

    • Implementation: LinearSegmentsNodePlacer.java
    • Uses Sander's approach with deflection dampening
  3. SIMPLE - Basic centering

    • Implementation: SimpleNodePlacer.java
    • Not for production use
  4. NETWORK_SIMPLEX - Network flow approach

    • Implementation: NetworkSimplexPlacer.java
  5. INTERACTIVE - Preserves existing positions

    • Implementation: InteractiveNodePlacer.java

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:

  1. POLYLINE - Simple straight line segments

    • Implementation: PolylineEdgeRouter.java
  2. ORTHOGONAL - Right-angle turns

    • Implementation: OrthogonalEdgeRouter.java
    • Complex algorithm with many sub-phases
  3. SPLINES - Smooth curves

    • Implementation: SplineEdgeRouter.java

##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:

  1. Layer Constraints - Force nodes to FIRST or LAST layer
  2. Model Order - Respect input model ordering in tie-breaking
  3. Interactive Mode - Use previous layout as starting point
  4. Direction Priority - Weight importance of edge directions
  5. Greedy Switch - Post-processing to reduce crossings
  6. Node Promotion - Reduce dummy nodes (Nikolov et al.)
  7. Layer Unzipping - Split very large layers
  8. Partitioning - Layout separate components
  9. Nested Graphs - Support for hierarchical compounds
  10. 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!

Avatar of Lichin LinLichin Lincommented

given this huge & packed algo, what is the best way to migrate it into js version

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

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

PriorityPhaseComplexityPayoffEst. Time
1Phase 2: Layer Assignment🔴 HIGH (Network Simplex)🟢 HIGH3-4 weeks
2Phase 3: Crossing Minimization🟡 MEDIUM (Barycenter)🟢 HIGH2-3 weeks
3Phase 1: Cycle Breaking🟢 LOW (Greedy)🟡 MEDIUM1-2 weeks
4Phase 4: Node Placement🔴 HIGH (Brandes-Köpf)🟡 MEDIUM3-4 weeks
5Phase 5: Edge Routing🔴 HIGH (Orthogonal)🟡 MEDIUM4-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)

  1. 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);
};
  1. Incremental Layout for dynamic graphs
  2. Caching of layer assignments
  3. 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

AspectRecommendation
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
Timeline4-8 weeks for usable MVP
Team Size1-2 developers

This pragmatic approach gets you 80% of functionality in 20% of the time!


END OF POST

Log in to add a comment
Claude Haiku 4.5
Can you explain the layered algorithm? — eclipse-elk/elk