A pure-Dart layered graph layout beside a precomputed elkjs reference.
Each example runs through the standalone elk package and is shown beside a precomputed elkjs result for the same graph, options, and SVG renderer — no mermaid, no diagram DSL. Both panels use the same coordinate scale, so spacing and routing differences stay visible. Arrowheads mark confirmed target endpoints in both panels. This is a behavioral comparison, not a claim of pixel or layout parity.
The dashed amber rectangle marks the bounds returned by each layout engine. Select Actual size to inspect routes closely; large diagrams then scroll inside their panel.
A directed graph laid out in layers, top-to-bottom. Note the right-angle (orthogonal) edge routing — the ELK signature.
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "a",
"width": 110,
"height": 44,
"layoutOptions": {}
},
{
"id": "b",
"width": 110,
"height": 44,
"layoutOptions": {}
},
{
"id": "c",
"width": 110,
"height": 44,
"layoutOptions": {}
},
{
"id": "d",
"width": 110,
"height": 44,
"layoutOptions": {}
},
{
"id": "e",
"width": 110,
"height": 44,
"layoutOptions": {}
}
],
"edges": [
{
"id": "e1",
"sources": [
"a"
],
"targets": [
"b"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e2",
"sources": [
"a"
],
"targets": [
"c"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e3",
"sources": [
"b"
],
"targets": [
"d"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e4",
"sources": [
"c"
],
"targets": [
"d"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e5",
"sources": [
"d"
],
"targets": [
"e"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
Compound nodes (clusters) are sized and positioned by the layout; children stay inside their parent. Edges cross cluster borders orthogonally.
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "in",
"width": 100,
"height": 44,
"layoutOptions": {}
},
{
"id": "pool",
"width": 0,
"height": 0,
"layoutOptions": {},
"children": [
{
"id": "w1",
"width": 100,
"height": 44,
"layoutOptions": {}
},
{
"id": "w2",
"width": 100,
"height": 44,
"layoutOptions": {}
}
]
},
{
"id": "out",
"width": 100,
"height": 44,
"layoutOptions": {}
}
],
"edges": [
{
"id": "e1",
"sources": [
"in"
],
"targets": [
"w1"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e2",
"sources": [
"in"
],
"targets": [
"w2"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e3",
"sources": [
"w1"
],
"targets": [
"out"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e4",
"sources": [
"w2"
],
"targets": [
"out"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
The same algorithm with horizontal flow — exactly the shape a package dependency visualization needs. Each node is a package; edges are “depends on”.
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "app",
"width": 120,
"height": 44,
"layoutOptions": {}
},
{
"id": "ui",
"width": 120,
"height": 44,
"layoutOptions": {}
},
{
"id": "core",
"width": 120,
"height": 44,
"layoutOptions": {}
},
{
"id": "elk",
"width": 120,
"height": 44,
"layoutOptions": {}
},
{
"id": "http",
"width": 120,
"height": 44,
"layoutOptions": {}
},
{
"id": "meta",
"width": 120,
"height": 44,
"layoutOptions": {}
}
],
"edges": [
{
"id": "e1",
"sources": [
"app"
],
"targets": [
"ui"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e2",
"sources": [
"app"
],
"targets": [
"core"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e3",
"sources": [
"ui"
],
"targets": [
"core"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e4",
"sources": [
"core"
],
"targets": [
"elk"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e5",
"sources": [
"core"
],
"targets": [
"http"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e6",
"sources": [
"elk"
],
"targets": [
"meta"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "e7",
"sources": [
"http"
],
"targets": [
"meta"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
A left-to-right system contains a vertical service group and a right-to-left worker pool. Cross-boundary edges keep their endpoint labels through all three coordinate frames.
elkjs 0.9.3 ignores child directions under INCLUDE_CHILDREN. Dart honors the DOWN service group and LEFT worker pool, so this layout is taller. Broader hierarchy compatibility remains tracked. Track #78
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "request",
"width": 104,
"height": 42,
"layoutOptions": {}
},
{
"id": "platform",
"width": 0,
"height": 0,
"layoutOptions": {
"elk.direction": "DOWN",
"elk.nodeLabels.placement": "INSIDE V_TOP H_CENTER"
},
"children": [
{
"id": "gateway",
"width": 104,
"height": 42,
"layoutOptions": {}
},
{
"id": "workers",
"width": 0,
"height": 0,
"layoutOptions": {
"elk.direction": "LEFT",
"elk.nodeLabels.placement": "INSIDE V_TOP H_CENTER"
},
"children": [
{
"id": "workerA",
"width": 96,
"height": 40,
"layoutOptions": {}
},
{
"id": "queue",
"width": 88,
"height": 40,
"layoutOptions": {}
},
{
"id": "workerB",
"width": 96,
"height": 40,
"layoutOptions": {}
}
],
"edges": [
{
"id": "queueA",
"sources": [
"queue"
],
"targets": [
"workerA"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "queueB",
"sources": [
"queue"
],
"targets": [
"workerB"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
],
"labels": [
{
"text": "Worker pool",
"width": 100,
"height": 16
}
]
},
{
"id": "cache",
"width": 92,
"height": 40,
"layoutOptions": {}
}
],
"edges": [
{
"id": "dispatch",
"sources": [
"gateway"
],
"targets": [
"queue"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "warm",
"sources": [
"cache"
],
"targets": [
"workerA"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
],
"labels": [
{
"text": "Service platform",
"width": 100,
"height": 16
}
]
},
{
"id": "audit",
"width": 92,
"height": 40,
"layoutOptions": {}
},
{
"id": "response",
"width": 104,
"height": 42,
"layoutOptions": {}
}
],
"edges": [
{
"id": "enter",
"sources": [
"request"
],
"targets": [
"gateway"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "complete",
"sources": [
"workerB"
],
"targets": [
"response"
],
"layoutOptions": {
"elk.edge.thickness": 1
},
"labels": [
{
"text": "result",
"width": 34,
"height": 12,
"layoutOptions": {
"elk.edgeLabels.placement": "TAIL",
"elk.layered.edgeLabels.sideSelection": "ALWAYS_UP"
}
},
{
"text": "200",
"width": 22,
"height": 12,
"layoutOptions": {
"elk.edgeLabels.placement": "HEAD",
"elk.layered.edgeLabels.sideSelection": "ALWAYS_UP"
}
}
]
},
{
"id": "record",
"sources": [
"gateway"
],
"targets": [
"audit"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "publish",
"sources": [
"audit"
],
"targets": [
"response"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
Named ports keep publishers and consumers on deliberate sides. Retry and health loops show route clearance around nodes.
FIXED_SIDE fixes each port side, but permits different positions and node order. Both engines use the same explicit loop clearance; alternative arrangements can still be valid. Track #76
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=10.0,bottom=10.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "ingest",
"width": 100,
"height": 54,
"layoutOptions": {
"elk.portConstraints": "FIXED_SIDE"
},
"ports": [
{
"id": "ingestOut",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "EAST"
}
},
{
"id": "healthOut",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "NORTH"
}
},
{
"id": "healthIn",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "NORTH"
}
}
]
},
{
"id": "broker",
"width": 126,
"height": 94,
"layoutOptions": {
"elk.portConstraints": "FIXED_SIDE"
},
"ports": [
{
"id": "brokerIn",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "WEST"
}
},
{
"id": "ordersOut",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "EAST"
}
},
{
"id": "eventsOut",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "EAST"
}
},
{
"id": "deadLetter",
"width": 8,
"height": 8,
"layoutOptions": {
"elk.port.side": "SOUTH"
}
}
]
},
{
"id": "orders",
"width": 106,
"height": 48,
"layoutOptions": {}
},
{
"id": "billing",
"width": 106,
"height": 48,
"layoutOptions": {}
},
{
"id": "analytics",
"width": 106,
"height": 48,
"layoutOptions": {}
},
{
"id": "archive",
"width": 106,
"height": 48,
"layoutOptions": {}
},
{
"id": "operator",
"width": 106,
"height": 48,
"layoutOptions": {}
},
{
"id": "status",
"width": 96,
"height": 44,
"layoutOptions": {}
}
],
"edges": [
{
"id": "publish",
"sources": [
"ingestOut"
],
"targets": [
"brokerIn"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "ordersFlow",
"sources": [
"ordersOut"
],
"targets": [
"orders"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "billingFlow",
"sources": [
"ordersOut"
],
"targets": [
"billing"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "metricsFlow",
"sources": [
"eventsOut"
],
"targets": [
"analytics"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "archiveFlow",
"sources": [
"eventsOut"
],
"targets": [
"archive"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "retry",
"sources": [
"deadLetter"
],
"targets": [
"brokerIn"
],
"layoutOptions": {
"elk.edge.thickness": 1
},
"labels": [
{
"text": "retry",
"width": 30,
"height": 12,
"layoutOptions": {
"elk.edgeLabels.placement": "CENTER",
"elk.layered.edgeLabels.sideSelection": "ALWAYS_UP"
}
}
]
},
{
"id": "healthLoop",
"sources": [
"healthOut"
],
"targets": [
"healthIn"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "alert",
"sources": [
"broker"
],
"targets": [
"operator"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "dashboard",
"sources": [
"analytics"
],
"targets": [
"status"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
Feedback paths return failed verification and monitoring alerts to earlier stages. Cycle breaking restores every arrow after the layered order is chosen.
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY_MODEL_ORDER",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "detect",
"width": 96,
"height": 42,
"layoutOptions": {}
},
{
"id": "triage",
"width": 96,
"height": 42,
"layoutOptions": {}
},
{
"id": "assign",
"width": 96,
"height": 42,
"layoutOptions": {}
},
{
"id": "diagnose",
"width": 104,
"height": 42,
"layoutOptions": {}
},
{
"id": "mitigate",
"width": 104,
"height": 42,
"layoutOptions": {}
},
{
"id": "verify",
"width": 96,
"height": 42,
"layoutOptions": {}
},
{
"id": "monitor",
"width": 96,
"height": 42,
"layoutOptions": {}
},
{
"id": "resolve",
"width": 96,
"height": 42,
"layoutOptions": {}
},
{
"id": "review",
"width": 96,
"height": 42,
"layoutOptions": {}
}
],
"edges": [
{
"id": "d-t",
"sources": [
"detect"
],
"targets": [
"triage"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "t-a",
"sources": [
"triage"
],
"targets": [
"assign"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "a-d",
"sources": [
"assign"
],
"targets": [
"diagnose"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "d-m",
"sources": [
"diagnose"
],
"targets": [
"mitigate"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "m-v",
"sources": [
"mitigate"
],
"targets": [
"verify"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "v-m",
"sources": [
"verify"
],
"targets": [
"mitigate"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "v-o",
"sources": [
"verify"
],
"targets": [
"monitor"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "o-t",
"sources": [
"monitor"
],
"targets": [
"triage"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "o-r",
"sources": [
"monitor"
],
"targets": [
"resolve"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "r-p",
"sources": [
"resolve"
],
"targets": [
"review"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "p-d",
"sources": [
"review"
],
"targets": [
"detect"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
Three build sources fan into one release hub, then share outgoing trunks to four environments. Junctions expose the merged route structure.
Release → Production → Telemetry → Rollback → Release is a cycle, so one edge must run backward. elkjs chooses Production → Telemetry; Dart chooses Rollback → Release. Follow the arrowheads to read the same directed sequence in either layout. Track #77
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": true,
"elk.layered.nodePlacement.bk.fixedAlignment": "NONE",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "web",
"width": 94,
"height": 42,
"layoutOptions": {}
},
{
"id": "api",
"width": 94,
"height": 42,
"layoutOptions": {}
},
{
"id": "worker",
"width": 94,
"height": 42,
"layoutOptions": {}
},
{
"id": "release",
"width": 112,
"height": 58,
"layoutOptions": {}
},
{
"id": "dev",
"width": 110,
"height": 42,
"layoutOptions": {}
},
{
"id": "staging",
"width": 110,
"height": 42,
"layoutOptions": {}
},
{
"id": "canary",
"width": 88,
"height": 42,
"layoutOptions": {}
},
{
"id": "prod",
"width": 88,
"height": 42,
"layoutOptions": {}
},
{
"id": "telemetry",
"width": 100,
"height": 42,
"layoutOptions": {}
},
{
"id": "rollback",
"width": 100,
"height": 42,
"layoutOptions": {}
}
],
"edges": [
{
"id": "bundle-web",
"sources": [
"web"
],
"targets": [
"release"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "bundle-api",
"sources": [
"api"
],
"targets": [
"release"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "bundle-worker",
"sources": [
"worker"
],
"targets": [
"release"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "deploy-dev",
"sources": [
"release"
],
"targets": [
"dev"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "deploy-staging",
"sources": [
"release"
],
"targets": [
"staging"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "deploy-canary",
"sources": [
"release"
],
"targets": [
"canary"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "deploy-prod",
"sources": [
"release"
],
"targets": [
"prod"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "observe",
"sources": [
"prod"
],
"targets": [
"telemetry"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "revert",
"sources": [
"telemetry"
],
"targets": [
"rollback"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "retryRelease",
"sources": [
"rollback"
],
"targets": [
"release"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
LEFTUP fixes the Brandes-Kopf sweep to its upper alignment. Compare the vertical lanes with the balanced variant.
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "LEFTUP",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "idea",
"width": 88,
"height": 40,
"layoutOptions": {}
},
{
"id": "ux",
"width": 102,
"height": 46,
"layoutOptions": {}
},
{
"id": "apiDesign",
"width": 102,
"height": 42,
"layoutOptions": {}
},
{
"id": "mobile",
"width": 110,
"height": 54,
"layoutOptions": {}
},
{
"id": "backend",
"width": 110,
"height": 48,
"layoutOptions": {}
},
{
"id": "qa",
"width": 92,
"height": 42,
"layoutOptions": {}
},
{
"id": "security",
"width": 100,
"height": 46,
"layoutOptions": {}
},
{
"id": "ship",
"width": 88,
"height": 48,
"layoutOptions": {}
}
],
"edges": [
{
"id": "idea-ux",
"sources": [
"idea"
],
"targets": [
"ux"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "idea-api",
"sources": [
"idea"
],
"targets": [
"apiDesign"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "ux-mobile",
"sources": [
"ux"
],
"targets": [
"mobile"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "api-mobile",
"sources": [
"apiDesign"
],
"targets": [
"mobile"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "api-backend",
"sources": [
"apiDesign"
],
"targets": [
"backend"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "mobile-qa",
"sources": [
"mobile"
],
"targets": [
"qa"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "backend-qa",
"sources": [
"backend"
],
"targets": [
"qa"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "backend-security",
"sources": [
"backend"
],
"targets": [
"security"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "qa-ship",
"sources": [
"qa"
],
"targets": [
"ship"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "security-ship",
"sources": [
"security"
],
"targets": [
"ship"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
BALANCED combines all four BK sweeps for the same graph and input order, producing a different but still overlap-free layout.
{
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.edgeRouting": "ORTHOGONAL",
"elk.randomSeed": 1,
"elk.padding": "[top=12.0,left=12.0,bottom=12.0,right=12.0]",
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
"elk.spacing.nodeNode": 40,
"elk.spacing.edgeNode": 20,
"elk.layered.spacing.nodeNodeBetweenLayers": 40,
"elk.layered.spacing.edgeNodeBetweenLayers": 20,
"elk.spacing.edgeEdge": 10,
"elk.layered.spacing.edgeEdgeBetweenLayers": 10,
"elk.spacing.labelLabel": 0,
"elk.spacing.edgeLabel": 2,
"elk.spacing.labelNode": 5,
"elk.spacing.labelPortHorizontal": 1,
"elk.spacing.labelPortVertical": 1,
"elk.spacing.portsSurrounding": "[top=0.0,bottom=0.0]",
"elk.spacing.nodeSelfLoop": 20,
"elk.layered.mergeEdges": false,
"elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED",
"elk.layered.nodePlacement.bk.edgeStraightening": "NONE",
"elk.layered.cycleBreaking.strategy": "GREEDY",
"elk.layered.considerModelOrder.strategy": "NONE",
"elk.layered.crossingMinimization.forceNodeModelOrder": false
},
"children": [
{
"id": "idea",
"width": 88,
"height": 40,
"layoutOptions": {}
},
{
"id": "ux",
"width": 102,
"height": 46,
"layoutOptions": {}
},
{
"id": "apiDesign",
"width": 102,
"height": 42,
"layoutOptions": {}
},
{
"id": "mobile",
"width": 110,
"height": 54,
"layoutOptions": {}
},
{
"id": "backend",
"width": 110,
"height": 48,
"layoutOptions": {}
},
{
"id": "qa",
"width": 92,
"height": 42,
"layoutOptions": {}
},
{
"id": "security",
"width": 100,
"height": 46,
"layoutOptions": {}
},
{
"id": "ship",
"width": 88,
"height": 48,
"layoutOptions": {}
}
],
"edges": [
{
"id": "idea-ux",
"sources": [
"idea"
],
"targets": [
"ux"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "idea-api",
"sources": [
"idea"
],
"targets": [
"apiDesign"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "ux-mobile",
"sources": [
"ux"
],
"targets": [
"mobile"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "api-mobile",
"sources": [
"apiDesign"
],
"targets": [
"mobile"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "api-backend",
"sources": [
"apiDesign"
],
"targets": [
"backend"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "mobile-qa",
"sources": [
"mobile"
],
"targets": [
"qa"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "backend-qa",
"sources": [
"backend"
],
"targets": [
"qa"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "backend-security",
"sources": [
"backend"
],
"targets": [
"security"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "qa-ship",
"sources": [
"qa"
],
"targets": [
"ship"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
},
{
"id": "security-ship",
"sources": [
"security"
],
"targets": [
"ship"
],
"layoutOptions": {
"elk.edge.thickness": 1
}
}
]
}
Because elk is pure Dart, it runs inside Flutter too. Below, ELK positions real, interactive Flutter widgets
— each card is a Material widget placed at the coordinates ELK computes, with the orthogonal edges drawn by a
CustomPainter. Drag to pan, scroll to zoom, tap a node to select it.
// elk is pure Dart, so it runs in Flutter.
final result = const ElkLayered().layout(ElkGraph(
layoutOptions: ElkLayoutOptions(direction: ElkDirection.down),
children: [
for (final n in nodes)
ElkNode(id: n.id, width: 168, height: 56),
],
edges: [
for (final (from, to) in edges)
ElkEdge(id: '..', sources: [from], targets: [to]),
],
));
// Place each node as a real Flutter widget at ELK's coordinates…
Stack(children: [
for (final spec in nodes)
if (result.nodesById[spec.id] case final n?)
Positioned(
left: n.x, top: n.y,
width: n.width, height: n.height,
child: NodeCard(spec), // a Material card
),
// …and stroke ELK's orthogonal edge routes.
CustomPaint(painter: EdgePainter(result.edges)),
]);
elk is a pure Dart implementation of layered graph layout. It provides an
elkjs-style graph model and supports compound graphs, ports, configurable
spacing, model-order constraints, and orthogonal edge routing.
The implementation follows the same algorithm family as Eclipse Layout Kernel and elkjs, but it is not a translation of either project and does not promise identical coordinates.
import 'package:elk/elk.dart';
void main() {
final result = const ElkLayered().layout(
ElkGraph(
layoutOptions: const ElkLayoutOptions(direction: ElkDirection.down),
children: [
ElkNode(id: 'a', width: 80, height: 40),
ElkNode(id: 'b', width: 80, height: 40),
ElkNode(id: 'c', width: 80, height: 40),
],
edges: [
ElkEdge(id: 'e1', sources: ['a'], targets: ['b']),
ElkEdge(id: 'e2', sources: ['a'], targets: ['c']),
],
),
);
for (final node in result.children) {
print('${node.id}: ${node.x}, ${node.y}');
}
}
Node coordinates are relative to their parent. Use result.nodesById for a
flat map with absolute coordinates.
ElkLayoutOptions controls direction, spacing, node placement, fixed
alignment, hierarchy handling, model-order handling, edge merging, and cycle breaking. For
example:
const options = ElkLayoutOptions(
direction: ElkDirection.right,
spacingBaseValue: 24,
forceNodeModelOrder: true,
);
Edges can refer to a node ID or to an ElkPort ID. A node with children is
laid out as a compound node. Existing elkjs graph JSON can be loaded with
ElkGraph.fromJson.
Set hierarchyHandling on root or compound ElkLayoutOptions. JSON accepts
org.eclipse.elk.hierarchyHandling, elk.hierarchyHandling, and
hierarchyHandling.
| Mode | Behavior |
|---|---|
includeChildren / INCLUDE_CHILDREN |
Routes edges across included compound boundaries. |
separateChildren / SEPARATE_CHILDREN |
Lays out groups independently. Edges internal to each group, including links from its own boundary ports to its children, remain routed; edges crossing a separated boundary remain in the result with empty sections. |
inherit / INHERIT |
Uses the enclosing mode; at the root, resolves to SEPARATE_CHILDREN. |
Omitting the root mode preserves Dart's existing INCLUDE_CHILDREN default.
Omitting a compound override inherits its enclosing mode. An explicit included
child does not reconnect a boundary separated by an ancestor. A direction-only
compound override changes direction without changing the inherited mode; a
hierarchy-only override preserves the inherited direction.
Unlike elkjs 0.9.3, Dart retains cross-boundary edge IDs with empty sections
when an included parent contains a separated child, rather than throwing.
Unrouted edges have no positioned labels. Their original label metadata remains
on the input graph. Consumers should draw only the sections that are present.
Dart also retains its existing mixed-direction support under INCLUDE_CHILDREN.
elkjs 0.9.3 ignores nested directions in that mode. The broader coordinated
hierarchy optimization remains tracked in
issue #78; these modes do
not promise identical coordinates or crossing order.
The comparison tool under tool/validation runs the same graph set through
this package and elkjs and writes side-by-side SVG output:
$ cd tool/validation
$ npm install
$ node run_elkjs.mjs
$ cd ../..
$ dart run tool/validation/compare.dart
The comparison checks layer assignment, node overlap, ordering, and graph bounds. Differences in within-layer ordering and exact coordinates are expected.
The hierarchy tests also read retained elkjs 0.9.3 inputs and outputs from
test/fixtures/hierarchy_handling_elkjs.json. After an intentional reference
update, regenerate them from the package root with
node tool/validation/generate_hierarchy_reference.mjs. Ordinary tests never
regenerate reference data.
MIT. The package contains a vendored derivative of dart_dagre (Apache-2.0).
See NOTICE and lib/src/dagre/LICENSE.