elkrb 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +364 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +695 -557
  5. data/exe/elkrb +36 -2
  6. data/lib/elkrb/atomic_destination.rb +53 -0
  7. data/lib/elkrb/best_effort_write.rb +49 -0
  8. data/lib/elkrb/cli/layout_flag_options.rb +54 -0
  9. data/lib/elkrb/cli.rb +155 -82
  10. data/lib/elkrb/command_resolver.rb +186 -0
  11. data/lib/elkrb/commands/batch_command.rb +28 -5
  12. data/lib/elkrb/commands/convert_command.rb +22 -52
  13. data/lib/elkrb/commands/diagram_command.rb +234 -102
  14. data/lib/elkrb/commands/options_command.rb +87 -0
  15. data/lib/elkrb/commands/render_command.rb +6 -1
  16. data/lib/elkrb/commands/validate_command.rb +146 -91
  17. data/lib/elkrb/errors.rb +20 -1
  18. data/lib/elkrb/format_sniffer.rb +362 -0
  19. data/lib/elkrb/geometry/bezier.rb +14 -4
  20. data/lib/elkrb/geometry/point.rb +5 -6
  21. data/lib/elkrb/geometry/rectangle.rb +2 -2
  22. data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
  23. data/lib/elkrb/graph/edge.rb +89 -3
  24. data/lib/elkrb/graph/graph.rb +43 -15
  25. data/lib/elkrb/graph/label.rb +16 -2
  26. data/lib/elkrb/graph/node.rb +13 -2
  27. data/lib/elkrb/graph/node_constraints.rb +139 -8
  28. data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
  29. data/lib/elkrb/graph/port.rb +29 -21
  30. data/lib/elkrb/graph/read_only_mapping.rb +21 -0
  31. data/lib/elkrb/graphviz_wrapper.rb +152 -44
  32. data/lib/elkrb/layout/algorithm_registry.rb +65 -8
  33. data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
  34. data/lib/elkrb/layout/algorithms/box.rb +47 -20
  35. data/lib/elkrb/layout/algorithms/disco.rb +150 -61
  36. data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
  37. data/lib/elkrb/layout/algorithms/force.rb +145 -86
  38. data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
  39. data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
  40. data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
  41. data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
  42. data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
  43. data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
  44. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
  45. data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
  46. data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
  47. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
  48. data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
  49. data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
  50. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
  51. data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
  52. data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
  53. data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
  54. data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
  55. data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
  56. data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
  57. data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
  58. data/lib/elkrb/layout/algorithms/layered.rb +548 -13
  59. data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
  60. data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
  61. data/lib/elkrb/layout/algorithms/radial.rb +188 -29
  62. data/lib/elkrb/layout/algorithms/random.rb +131 -17
  63. data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
  64. data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
  65. data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
  66. data/lib/elkrb/layout/algorithms/stress.rb +391 -84
  67. data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
  68. data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
  69. data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
  70. data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
  71. data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
  72. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
  73. data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
  74. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
  75. data/lib/elkrb/layout/edge_router.rb +533 -337
  76. data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
  77. data/lib/elkrb/layout/java_random.rb +59 -0
  78. data/lib/elkrb/layout/label_placer.rb +165 -157
  79. data/lib/elkrb/layout/layout_engine.rb +46 -23
  80. data/lib/elkrb/layout/node_index.rb +149 -0
  81. data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
  82. data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
  83. data/lib/elkrb/layout/polyomino/grid.rb +106 -0
  84. data/lib/elkrb/layout/polyomino/packer.rb +95 -0
  85. data/lib/elkrb/layout/polyomino/piece.rb +52 -0
  86. data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
  87. data/lib/elkrb/layout/spore/body.rb +65 -0
  88. data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
  89. data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
  90. data/lib/elkrb/layout/spore/gap.rb +76 -0
  91. data/lib/elkrb/layout/spore/geometry.rb +104 -0
  92. data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
  93. data/lib/elkrb/layout/spore/placement.rb +57 -0
  94. data/lib/elkrb/layout/spore/rays.rb +119 -0
  95. data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
  96. data/lib/elkrb/layout/spore/triangulation.rb +159 -0
  97. data/lib/elkrb/layout_flags.rb +69 -0
  98. data/lib/elkrb/options/decimal.rb +50 -0
  99. data/lib/elkrb/options/element_options.rb +43 -0
  100. data/lib/elkrb/options/elk_padding.rb +20 -14
  101. data/lib/elkrb/options/k_vector.rb +6 -7
  102. data/lib/elkrb/options/k_vector_chain.rb +12 -20
  103. data/lib/elkrb/options/option_map.rb +85 -0
  104. data/lib/elkrb/options/registry.rb +335 -0
  105. data/lib/elkrb/options/resolver.rb +151 -0
  106. data/lib/elkrb/options/spellings.rb +62 -0
  107. data/lib/elkrb/options/unhonoured_report.rb +194 -0
  108. data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
  109. data/lib/elkrb/parsers/elkt/parser.rb +424 -0
  110. data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
  111. data/lib/elkrb/parsers/elkt/token.rb +40 -0
  112. data/lib/elkrb/parsers/elkt_parser.rb +32 -230
  113. data/lib/elkrb/serializers/dot_serializer.rb +342 -54
  114. data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
  115. data/lib/elkrb/version.rb +1 -1
  116. data/lib/elkrb.rb +60 -61
  117. metadata +71 -49
  118. data/.rspec +0 -3
  119. data/.rubocop.yml +0 -11
  120. data/Gemfile +0 -13
  121. data/Rakefile +0 -64
  122. data/benchmarks/README.md +0 -172
  123. data/benchmarks/elkjs_benchmark.js +0 -140
  124. data/benchmarks/elkrb_benchmark.rb +0 -145
  125. data/benchmarks/fixtures/graphs.json +0 -10777
  126. data/benchmarks/generate_report.rb +0 -241
  127. data/benchmarks/generate_test_graphs.rb +0 -154
  128. data/benchmarks/results/elkrb_results.json +0 -280
  129. data/benchmarks/results/elkrb_summary.json +0 -285
  130. data/elkrb.gemspec +0 -39
  131. data/examples/dot_export_demo.rb +0 -133
  132. data/examples/hierarchical_graph.rb +0 -19
  133. data/examples/layout_constraints_demo.rb +0 -272
  134. data/examples/port_constraints_demo.rb +0 -291
  135. data/examples/self_loop_demo.rb +0 -391
  136. data/examples/simple_graph.rb +0 -50
  137. data/examples/spline_routing_demo.rb +0 -235
  138. data/lib/elkrb/graph/layout_options.rb +0 -247
  139. data/sig/elkrb/constraints.rbs +0 -114
  140. data/sig/elkrb/geometry.rbs +0 -61
  141. data/sig/elkrb/graph.rbs +0 -112
  142. data/sig/elkrb/layout.rbs +0 -107
  143. data/sig/elkrb/options.rbs +0 -81
  144. data/sig/elkrb.rbs +0 -32
data/README.adoc CHANGED
@@ -1,8 +1,8 @@
1
1
  = ElkRb
2
2
 
3
3
  image:https://img.shields.io/gem/v/elkrb.svg[RubyGems Version]
4
- image:https://img.shields.io/github/license/metanorma/elkrb.svg[License]
5
- image:https://github.com/metanorma/elkrb/actions/workflows/test.yml/badge.svg["Build", link="https://github.com/metanorma/elkrb/actions/workflows/test.yml"]
4
+ image:https://img.shields.io/github/license/claricle/elkrb.svg[License]
5
+ image:https://github.com/claricle/elkrb/actions/workflows/rake.yml/badge.svg["Build", link="https://github.com/claricle/elkrb/actions/workflows/rake.yml"]
6
6
 
7
7
  == Purpose
8
8
 
@@ -22,90 +22,50 @@ The library is designed for box-based diagrams including:
22
22
  ElkRb is built on top of the `lutaml-model` serialization framework, providing
23
23
  flexible YAML and JSON serialization with ELK JSON format compatibility.
24
24
 
25
- ElkRb aims to be fully compatible with:
25
+ [[compatibility-with-elk]]
26
+ == Compatibility with ELK
26
27
 
27
- * Java-based ELK (https://www.eclipse.org/elk/)
28
- * JavaScript-based elkjs (https://github.com/kieler/elkjs) (verifiable
29
- compatible in specs)
28
+ ElkRb is format-compatible with elkjs 0.11.0: it reads and writes the ELK JSON
29
+ graph format.
30
30
 
31
- == Features
32
-
33
- Supports all 15 ELK layout algorithms from Java implementation:
34
-
35
- Layered:: (Sugiyama) hierarchical diagrams with directed flow
36
- Force:: organic, symmetric layouts using force simulation
37
- Stress:: quality-focused layout with stress minimization
38
- Box:: simple grid-based arrangement
39
- Fixed:: preserve existing positions
40
- Random:: random node placement
41
- MRTree:: multi-rooted tree layout
42
- Radial:: circular/radial node arrangement
43
- RectPacking:: efficient rectangle packing
44
- TopdownPacking:: grid-based treemap layout
45
- Libavoid:: "A*" pathfinding connector routing
46
- VertiFlex:: vertical flexible column layout
47
- DISCO:: disconnected component layout
48
- SPOrE Overlap:: overlap removal optimization
49
- SPOrE Compaction:: whitespace compaction
50
-
51
-
52
- Advanced edge routing
53
-
54
- ** ORTHOGONAL: 90-degree bends
55
- ** POLYLINE: Straight segments
56
- ** SPLINES: Smooth Bezier curves
57
-
58
-
59
- Advanced port constraints
60
-
61
- * Port side specification (NORTH, SOUTH, EAST, WEST)
62
- * Automatic side detection
63
- * Port ordering within sides
64
-
65
-
66
- Self-loop support
31
+ Coordinate parity with elkjs is exactly what the tiers in
32
+ `spec/elkrb/golden_spec.rb` say it is. Cases in the `exact` tier compare
33
+ coordinates; cases in the looser `structural` tier do not, and a case that
34
+ carries a `pending:` reason in `spec/support/golden_cases.rb` is known to
35
+ differ.
67
36
 
68
- * Edges connecting nodes to themselves
69
- * Multiple self-loops per node
70
- * Configurable routing styles
37
+ Layered places nodes with Brandes-Köpf only; every `nodePlacement.strategy`
38
+ value falls back to it.
71
39
 
40
+ Not every option ELK defines changes the result here. Each option elkrb knows
41
+ carries a status (`honoured`, `partial`, `accepted` or `unsupported`); see
42
+ <<option-status>>.
72
43
 
73
- Export formats
74
-
75
- * ELK JSON format
76
- * YAML format (Ruby-specific)
77
- * Graphviz DOT format
78
-
79
-
80
- Advanced features
81
-
82
- * Edge routing with bend points and port awareness
83
- * Hierarchical graph support with recursive layout
84
- * Automatic label placement for nodes, edges, and ports
85
- * EdgeSection model with orthogonal routing
86
-
44
+ == Features
87
45
 
88
- Port-based node connections
46
+ Layout algorithms:: every algorithm in the registry (see
47
+ <<layout-algorithms>>), including layered, force, stress, box, fixed, random,
48
+ MRTree, radial, RectPacking, TopdownPacking, DISCO, SPOrE Overlap, SPOrE
49
+ Compaction, Libavoid and VertiFlex.
89
50
 
90
- * Explicit port definitions
91
- * Port-to-port edge routing
92
- * Port side detection for labels
51
+ Edge routing:: ORTHOGONAL, POLYLINE and SPLINES, with bend points on the
52
+ computed edge sections.
93
53
 
94
- ELK JSON format compatibility
54
+ Ports:: ports with an explicit side and index, placed on the node border, and
55
+ port-to-port edges.
95
56
 
96
- * Full elkjs v0.11.0 compatibility
97
- * YAML/JSON serialization via Lutaml::Model
57
+ Self-loops:: edges whose source and target are the same node, or ports of the
58
+ same node, routed outside the node border.
98
59
 
99
- Comprehensive layout options
60
+ Hierarchy:: nested graphs laid out recursively.
100
61
 
101
- * Option parsers (ElkPadding, KVector, KVectorChain)
102
- * Algorithm-specific options
103
- * Layout categories and metadata
62
+ Labels:: automatic placement of node, edge and port labels.
104
63
 
105
- Command-line interface
64
+ Export formats:: ELK JSON, YAML (Ruby-specific), Graphviz DOT and ELKT.
106
65
 
107
- * Layout command with format options
108
- * Algorithm listing and query
66
+ Command-line interface:: `layout`, `algorithms`, `options`, `diagram`,
67
+ `convert`, `render`, `validate`, `batch` and `version`; see
68
+ <<command-line-interface>>.
109
69
 
110
70
  == Architecture
111
71
 
@@ -141,7 +101,7 @@ Input Graph (YAML/JSON/Hash)
141
101
  └────┬────┘
142
102
  │
143
103
  ▼
144
- Output (YAML/JSON/Hash)
104
+ Output (Graph; serialize with to_json / to_yaml)
145
105
  ----
146
106
 
147
107
  .Graph model structure
@@ -189,651 +149,681 @@ $ gem install elkrb
189
149
 
190
150
  === Basic graph layout
191
151
 
192
- A layout can be performed by defining a graph structure as a Ruby hash and
193
- passing it to the `Elkrb::LayoutEngine`.
152
+ `Elkrb.layout` takes a graph as a Ruby hash (or an `Elkrb::Graph::Graph`) and
153
+ returns an `Elkrb::Graph::Graph` with positions filled in. Read the result
154
+ through attribute readers.
155
+
156
+ Put the layout options in the graph's own `layoutOptions`; that is the primary
157
+ way to configure a layout.
194
158
 
195
159
  [source,ruby]
196
160
  ----
197
161
  require "elkrb"
198
162
 
199
- # Define a simple graph
200
163
  graph = {
201
164
  id: "root",
202
165
  layoutOptions: {
203
- "elk.algorithm" => "layered"
166
+ "elk.algorithm" => "layered",
167
+ "elk.direction" => "DOWN",
204
168
  },
205
169
  children: [
206
170
  { id: "n1", width: 100, height: 60 },
207
171
  { id: "n2", width: 100, height: 60 },
208
- { id: "n3", width: 100, height: 60 }
172
+ { id: "n3", width: 100, height: 60 },
209
173
  ],
210
174
  edges: [
211
175
  { id: "e1", sources: ["n1"], targets: ["n2"] },
212
- { id: "e2", sources: ["n1"], targets: ["n3"] }
213
- ]
176
+ { id: "e2", sources: ["n1"], targets: ["n3"] },
177
+ ],
214
178
  }
215
179
 
216
- # Layout the graph
217
- engine = Elkrb::LayoutEngine.new
218
- result = engine.layout(graph)
180
+ result = Elkrb.layout(graph)
181
+
182
+ result.children.each do |node|
183
+ puts "#{node.id}: x=#{node.x} y=#{node.y}"
184
+ end
185
+ puts "root: #{result.width} x #{result.height}"
186
+ ----
219
187
 
220
- # Access computed positions
221
- puts result[:children][0][:x] # => 0.0
222
- puts result[:children][0][:y] # => 0.0
223
- puts result[:children][1][:x] # => 150.0
188
+ Prints:
189
+
190
+ [source]
191
+ ----
192
+ n1: x=28.666666666666664 y=12.0
193
+ n2: x=12.0 y=92.0
194
+ n3: x=132.0 y=92.0
195
+ root: 244.0 x 164.0
224
196
  ----
225
197
 
226
- A more structured approach uses the model classes to create the graph.
198
+ A Hash argument is converted to a fresh graph and left untouched. A
199
+ `Graph` argument is laid out in place and returned. The model classes build the
200
+ same graph:
227
201
 
228
202
  [source,ruby]
229
203
  ----
230
204
  require "elkrb"
231
205
 
232
- # Create graph using model classes
233
206
  graph = Elkrb::Graph::Graph.new(id: "root")
207
+ graph.children = [
208
+ Elkrb::Graph::Node.new(id: "n1", width: 100, height: 60),
209
+ Elkrb::Graph::Node.new(id: "n2", width: 100, height: 60),
210
+ ]
211
+ graph.edges = [
212
+ Elkrb::Graph::Edge.new(id: "e1", sources: ["n1"], targets: ["n2"]),
213
+ ]
214
+ graph.layout_options = { "elk.algorithm" => "layered", "elk.direction" => "DOWN" }
215
+
216
+ result = Elkrb.layout(graph)
234
217
 
235
- # Add nodes
236
- node1 = Elkrb::Graph::Node.new(
237
- id: "n1",
238
- width: 100,
239
- height: 60
240
- )
241
- node2 = Elkrb::Graph::Node.new(
242
- id: "n2",
243
- width: 100,
244
- height: 60
245
- )
246
-
247
- graph.children = [node1, node2]
248
-
249
- # Add edge
250
- edge = Elkrb::Graph::Edge.new(
251
- id: "e1",
252
- sources: ["n1"],
253
- targets: ["n2"]
254
- )
255
- graph.edges = [edge]
256
-
257
- # Set layout options
258
- graph.layout_options = Elkrb::Graph::LayoutOptions.new(
259
- algorithm: "layered",
260
- direction: "DOWN"
261
- )
262
-
263
- # Layout
264
- engine = Elkrb::LayoutEngine.new
265
- result = engine.layout(graph)
266
-
267
- # Serialize result
218
+ puts result.equal?(graph)
268
219
  puts result.to_yaml
269
- puts result.to_json
270
220
  ----
271
221
 
272
- === UML Class diagram layout
222
+ Prints:
223
+
224
+ [source]
225
+ ----
226
+ true
227
+ ---
228
+ id: root
229
+ x: 0.0
230
+ y: 0.0
231
+ width: 124.0
232
+ height: 164.0
233
+ children:
234
+ - id: n1
235
+ x: 12.0
236
+ y: 12.0
237
+ width: 100.0
238
+ height: 60.0
239
+ - id: n2
240
+ x: 12.0
241
+ y: 92.0
242
+ width: 100.0
243
+ height: 60.0
244
+ edges:
245
+ - id: e1
246
+ sources:
247
+ - n1
248
+ targets:
249
+ - n2
250
+ sections:
251
+ - id: e1_s0
252
+ start_point:
253
+ x: 62.0
254
+ y: 72.0
255
+ end_point:
256
+ x: 62.0
257
+ y: 92.0
258
+ incoming_shape: n1
259
+ outgoing_shape: n2
260
+ container: root
261
+ layout_options:
262
+ elk.algorithm: layered
263
+ elk.direction: DOWN
264
+ ----
265
+
266
+ `result.to_json` writes the same graph in ELK JSON.
267
+
268
+ === Call-level options
269
+
270
+ Options can also be passed to `Elkrb.layout` itself, as globals for that one
271
+ call.
273
272
 
274
273
  [source,ruby]
275
274
  ----
276
275
  require "elkrb"
277
276
 
278
- # UML class diagram with ports for connections
279
277
  graph = {
280
- id: "uml_diagram",
281
- layoutOptions: {
282
- "elk.algorithm" => "layered",
283
- "elk.direction" => "DOWN",
284
- "elk.spacing.nodeNode" => 50
285
- },
278
+ id: "root",
286
279
  children: [
287
- {
288
- id: "Person",
289
- width: 120,
290
- height: 80,
291
- labels: [
292
- { text: "Person", width: 100, height: 20 }
293
- ],
294
- ports: [
295
- { id: "p1", x: 60, y: 0 }, # Top center
296
- { id: "p2", x: 60, y: 80 } # Bottom center
297
- ]
298
- },
299
- {
300
- id: "Employee",
301
- width: 120,
302
- height: 80,
303
- labels: [
304
- { text: "Employee", width: 100, height: 20 }
305
- ],
306
- ports: [
307
- { id: "p3", x: 60, y: 0 }
308
- ]
309
- },
310
- {
311
- id: "Customer",
312
- width: 120,
313
- height: 80,
314
- labels: [
315
- { text: "Customer", width: 100, height: 20 }
316
- ],
317
- ports: [
318
- { id: "p4", x: 60, y: 0 }
319
- ]
320
- }
280
+ { id: "n1", width: 100, height: 60 },
281
+ { id: "n2", width: 100, height: 60 },
321
282
  ],
322
- edges: [
323
- {
324
- id: "inheritance1",
325
- sources: ["Employee"],
326
- targets: ["Person"],
327
- layoutOptions: {
328
- "elk.edgeRouting" => "ORTHOGONAL"
329
- }
330
- },
331
- {
332
- id: "inheritance2",
333
- sources: ["Customer"],
334
- targets: ["Person"],
335
- layoutOptions: {
336
- "elk.edgeRouting" => "ORTHOGONAL"
337
- }
338
- }
339
- ]
283
+ edges: [{ id: "e1", sources: ["n1"], targets: ["n2"] }],
340
284
  }
341
285
 
342
- engine = Elkrb::LayoutEngine.new
343
- result = engine.layout(graph)
286
+ down = Elkrb.layout(graph, "elk.direction" => "DOWN")
287
+ puts "DOWN: n2 at (#{down.children[1].x}, #{down.children[1].y})"
288
+
289
+ right = Elkrb.layout(graph, "elk.direction" => "RIGHT")
290
+ puts "RIGHT: n2 at (#{right.children[1].x}, #{right.children[1].y})"
291
+ ----
292
+
293
+ Prints:
294
+
295
+ [source]
296
+ ----
297
+ DOWN: n2 at (12.0, 92.0)
298
+ RIGHT: n2 at (132.0, 12.0)
344
299
  ----
345
300
 
346
- === EXPRESS-G diagram layout
301
+ `algorithm:` is the call-level spelling of `"elk.algorithm"`.
302
+
303
+ Options are read in one order: the graph's own `layoutOptions`, then its
304
+ `properties`, then the call-level options, then the registry default. A graph
305
+ that pins an algorithm keeps it, whatever the call passes:
347
306
 
348
307
  [source,ruby]
349
308
  ----
350
309
  require "elkrb"
351
310
 
352
- # EXPRESS-G entity-relationship diagram
353
311
  graph = {
354
- id: "express_g",
355
- layoutOptions: {
356
- "elk.algorithm" => "layered",
357
- "elk.direction" => "RIGHT",
358
- "elk.edgeRouting" => "ORTHOGONAL"
359
- },
312
+ id: "root",
313
+ layoutOptions: { "elk.algorithm" => "box" },
360
314
  children: [
361
- {
362
- id: "Entity1",
363
- width: 100,
364
- height: 60,
365
- labels: [{ text: "Entity", width: 80, height: 15 }]
366
- },
367
- {
368
- id: "Attribute1",
369
- width: 80,
370
- height: 40,
371
- labels: [{ text: "name", width: 60, height: 15 }]
372
- },
373
- {
374
- id: "Attribute2",
375
- width: 80,
376
- height: 40,
377
- labels: [{ text: "value", width: 60, height: 15 }]
378
- }
315
+ { id: "n1", width: 100, height: 60 },
316
+ { id: "n2", width: 100, height: 60 },
379
317
  ],
380
- edges: [
381
- { id: "e1", sources: ["Entity1"], targets: ["Attribute1"] },
382
- { id: "e2", sources: ["Entity1"], targets: ["Attribute2"] }
383
- ]
318
+ edges: [{ id: "e1", sources: ["n1"], targets: ["n2"] }],
384
319
  }
385
320
 
386
- engine = Elkrb::LayoutEngine.new
387
- result = engine.layout(graph)
388
- ----
389
-
390
- === Layout from YAML File
321
+ pinned = Elkrb.layout(graph, algorithm: "force")
322
+ unpinned = Elkrb.layout(graph.except(:layoutOptions), algorithm: "box")
391
323
 
392
- [source,ruby]
324
+ positions = ->(g) { g.children.map { |n| [n.x, n.y] } }
325
+ puts positions.call(pinned) == positions.call(unpinned)
393
326
  ----
394
- require "elkrb"
395
-
396
- # Load graph from YAML
397
- yaml_content = File.read("diagram.yml")
398
- graph = Elkrb::Graph::Graph.from_yaml(yaml_content)
399
327
 
400
- # Layout
401
- engine = Elkrb::LayoutEngine.new
402
- result = engine.layout(graph)
328
+ Prints:
403
329
 
404
- # Save result
405
- File.write("diagram_laid_out.yml", result.to_yaml)
330
+ [source]
331
+ ----
332
+ true
406
333
  ----
407
334
 
408
- == Layout algorithms
409
-
410
- === Layered layout
411
-
412
- The layered (Sugiyama) algorithm is ELK's flagship layout, ideal for
413
- hierarchical diagrams with a natural flow direction.
414
-
415
- ==== Use cases
416
-
417
- * UML class diagrams (inheritance hierarchies)
418
- * Flowcharts and process diagrams
419
- * Data flow diagrams
420
- * Organization charts
421
- * Dependency graphs
422
-
423
- ==== Key options
335
+ === UML class diagram layout
424
336
 
425
337
  [source,ruby]
426
338
  ----
427
- layoutOptions: {
428
- "elk.algorithm" => "layered",
429
- "elk.direction" => "DOWN", # DOWN, UP, LEFT, RIGHT
430
- "elk.spacing.nodeNode" => 50,
431
- "elk.layered.crossingMinimization.strategy" => "LAYER_SWEEP",
432
- "elk.layered.nodePlacement.strategy" => "NETWORK_SIMPLEX",
433
- "elk.edgeRouting" => "ORTHOGONAL" # ORTHOGONAL, POLYLINE, SPLINES
434
- }
435
- ----
436
-
437
- === Force-Directed Layout
339
+ require "elkrb"
438
340
 
439
- Creates organic, symmetric layouts using force simulation.
341
+ graph = {
342
+ id: "uml_diagram",
343
+ layoutOptions: {
344
+ "elk.algorithm" => "layered",
345
+ "elk.direction" => "DOWN",
346
+ "elk.spacing.nodeNode" => 50,
347
+ },
348
+ children: [
349
+ { id: "Person", width: 120, height: 80,
350
+ labels: [{ text: "Person", width: 100, height: 20 }] },
351
+ { id: "Employee", width: 120, height: 80,
352
+ labels: [{ text: "Employee", width: 100, height: 20 }] },
353
+ { id: "Customer", width: 120, height: 80,
354
+ labels: [{ text: "Customer", width: 100, height: 20 }] },
355
+ ],
356
+ edges: [
357
+ { id: "inheritance1", sources: ["Employee"], targets: ["Person"] },
358
+ { id: "inheritance2", sources: ["Customer"], targets: ["Person"] },
359
+ ],
360
+ }
440
361
 
441
- Use cases:
362
+ result = Elkrb.layout(graph)
442
363
 
443
- * Network diagrams
444
- * Social graphs
445
- * Mind maps
446
- * General undirected graphs
364
+ result.children.each do |node|
365
+ puts "#{node.id}: (#{node.x}, #{node.y})"
366
+ end
367
+ ----
447
368
 
448
- Options:
369
+ Prints:
449
370
 
450
- [source,ruby]
371
+ [source]
451
372
  ----
452
- layoutOptions: {
453
- "elk.algorithm" => "force",
454
- "elk.force.repulsion" => 5.0,
455
- "elk.force.temperature" => 0.001
456
- }
373
+ Person: (32.0, 112.0)
374
+ Employee: (182.0, 12.0)
375
+ Customer: (12.0, 12.0)
457
376
  ----
458
377
 
459
- === Stress Minimization
378
+ === Layout from YAML
460
379
 
461
- Quality-focused layout optimizing edge lengths and crossings.
380
+ [source,ruby]
381
+ ----
382
+ require "elkrb"
462
383
 
463
- Use cases:
384
+ yaml = <<~YAML
385
+ id: root
386
+ layout_options:
387
+ elk.algorithm: layered
388
+ elk.direction: RIGHT
389
+ children:
390
+ - id: a
391
+ width: 80
392
+ height: 40
393
+ - id: b
394
+ width: 80
395
+ height: 40
396
+ edges:
397
+ - id: e1
398
+ sources: [a]
399
+ targets: [b]
400
+ YAML
401
+
402
+ graph = Elkrb::Graph::Graph.from_yaml(yaml)
403
+ result = Elkrb.layout(graph)
464
404
 
465
- * High-quality graph visualization
466
- * Research diagrams
467
- * Publication-ready layouts
405
+ puts result.children.map { |n| "#{n.id}: (#{n.x}, #{n.y})" }
406
+ File.write("diagram_laid_out.yml", result.to_yaml)
407
+ puts File.size?("diagram_laid_out.yml").positive?
408
+ ----
468
409
 
469
- Options:
410
+ Prints:
470
411
 
471
- [source,ruby]
412
+ [source]
472
413
  ----
473
- layoutOptions: {
474
- "elk.algorithm" => "stress"
475
- }
414
+ a: (12.0, 12.0)
415
+ b: (112.0, 12.0)
416
+ true
476
417
  ----
477
418
 
478
- === Box Layout
419
+ YAML uses snake_case keys (`layout_options`); JSON uses the ELK camelCase keys
420
+ (`layoutOptions`).
479
421
 
480
- Simple rectangular packing of nodes.
481
-
482
- Use cases:
483
-
484
- * Simple container layouts
485
- * Grid-like arrangements
486
- * Dashboards
422
+ [[layout-algorithms]]
423
+ == Layout algorithms
487
424
 
488
- Options:
425
+ Select an algorithm with `"elk.algorithm"`. The registry is the list of what
426
+ is available:
489
427
 
490
428
  [source,ruby]
491
429
  ----
492
- layoutOptions: {
493
- "elk.algorithm" => "box"
494
- }
430
+ require "elkrb"
431
+
432
+ puts Elkrb.known_layout_algorithms.size
433
+ Elkrb.known_layout_algorithms.each do |algorithm|
434
+ puts "#{algorithm[:id]}: #{algorithm[:name]}"
435
+ end
495
436
  ----
496
437
 
497
- === MRTree Layout
438
+ Prints:
498
439
 
499
- Multi-rooted tree layout for forest structures.
440
+ [source]
441
+ ----
442
+ 15
443
+ box: Box
444
+ disco: DISCO
445
+ fixed: Fixed
446
+ force: Force-Directed
447
+ layered: Layered (Sugiyama)
448
+ libavoid: Libavoid
449
+ mrtree: Multi-Rooted Tree
450
+ radial: Radial
451
+ random: Random
452
+ rectpacking: Rectangle Packing
453
+ spore_compaction: SPOrE Compaction
454
+ spore_overlap: SPOrE Overlap Removal
455
+ stress: Stress Minimization
456
+ topdownpacking: TopdownPacking
457
+ vertiflex: VertiFlex
458
+ ----
500
459
 
501
- Use cases:
460
+ Each entry also carries `:description`, `:category`, `:supports_hierarchy` and
461
+ `:supported_options`, the option ids that algorithm reads.
502
462
 
503
- * Multiple inheritance hierarchies
504
- * Forest data structures
505
- * Parallel tree structures
506
- * Multi-root taxonomies
463
+ [cols="1,3",options="header"]
464
+ |===
465
+ | Algorithm | Use it for
507
466
 
508
- Options:
467
+ | `layered`
468
+ | Directed, hierarchical diagrams with a flow direction: UML class diagrams,
469
+ flowcharts, data flow diagrams, organization charts, dependency graphs.
470
+ Supports nested graphs.
509
471
 
510
- [source,ruby]
511
- ----
512
- layoutOptions: {
513
- "elk.algorithm" => "mrtree",
514
- "elk.direction" => "DOWN",
515
- "elk.spacing.nodeNode" => 30
516
- }
517
- ----
472
+ | `force`, `stress`
473
+ | Undirected network diagrams and social graphs.
518
474
 
519
- === Radial Layout
475
+ | `mrtree`
476
+ | Forests and trees with several roots. Supports nested graphs.
520
477
 
521
- Arranges nodes in a circular pattern around the center.
478
+ | `radial`
479
+ | Hub-and-spoke arrangements around a centre.
522
480
 
523
- Use cases:
481
+ | `box`, `rectpacking`, `topdownpacking`, `vertiflex`
482
+ | Grid-like and packed arrangements; dashboards and tile layouts.
524
483
 
525
- * Network diagrams
526
- * Circular visualizations
527
- * Hub-and-spoke diagrams
528
- * Cyclic structures
484
+ | `disco`
485
+ | Graphs made of several disconnected components.
529
486
 
530
- Options:
487
+ | `spore_overlap`, `spore_compaction`
488
+ | Post-processing: remove overlaps between nodes, or compact whitespace.
531
489
 
532
- [source,ruby]
533
- ----
534
- layoutOptions: {
535
- "elk.algorithm" => "radial",
536
- "elk.spacing.nodeNode" => 50
537
- }
538
- ----
490
+ | `libavoid`
491
+ | Connector routing around obstacles.
539
492
 
540
- === RectPacking Layout
493
+ | `fixed`, `random`
494
+ | Keep the positions the graph already carries, or place nodes at random.
495
+ |===
541
496
 
542
- Efficient rectangle packing using shelf algorithms.
497
+ [[option-status]]
498
+ === Option status
543
499
 
544
- Use cases:
500
+ Options are validated against `Elkrb::Options::Registry`. `Elkrb.known_layout_options`
501
+ returns every known option id with its type, default, allowed values and
502
+ status. The status says how much of the option elkrb acts on: `honoured`,
503
+ `partial`, `accepted` (parsed and validated, but it does not change the
504
+ result) or `unsupported`.
545
505
 
546
- * Dashboards
547
- * Tile layouts
548
- * Space-efficient arrangements
549
- * Component grids
506
+ Run `elkrb options` to list every option with its status, or
507
+ `bundle exec rake docs:all` to write the full option and elkjs comparison
508
+ pages under `docs/`.
550
509
 
551
- Options:
510
+ By default an option elkrb does not fully honour logs a warning through
511
+ `Elkrb.logger`. Pass `strict: true` to raise `Elkrb::Error` instead:
552
512
 
553
513
  [source,ruby]
554
514
  ----
555
- layoutOptions: {
556
- "elk.algorithm" => "rectpacking",
557
- "elk.spacing.nodeNode" => 15
558
- }
559
- ----
560
-
561
- === DISCO Layout
515
+ require "elkrb"
562
516
 
563
- Disconnected component layout - identifies and arranges separate graph components.
517
+ direction = Elkrb.known_layout_options["elk.direction"]
518
+ puts direction[:status]
519
+ puts direction[:values].inspect
564
520
 
565
- Use cases:
521
+ graph = {
522
+ id: "root",
523
+ layoutOptions: { "elk.layered.nodePlacement.strategy" => "NETWORK_SIMPLEX" },
524
+ children: [{ id: "n1", width: 40, height: 40 }],
525
+ }
566
526
 
567
- * Graphs with multiple disconnected parts
568
- * Component-based visualizations
569
- * Separate module layouts
570
- * Multi-cluster diagrams
527
+ begin
528
+ Elkrb.layout(graph, strict: true)
529
+ rescue Elkrb::Error => e
530
+ puts e.message
531
+ end
532
+ ----
571
533
 
572
- Options:
534
+ Prints:
573
535
 
574
- [source,ruby]
536
+ [source]
575
537
  ----
576
- layoutOptions: {
577
- "elk.algorithm" => "disco",
578
- "disco.componentCompaction.strategy" => "NONE", # NONE, ROW, COLUMN, GRID
579
- "disco.componentCompaction.componentLayoutAlgorithm" => "layered",
580
- "disco.spacing.componentComponent" => 30
581
- }
538
+ partial
539
+ ["UNDEFINED", "RIGHT", "LEFT", "DOWN", "UP"]
540
+ strict mode: unknown or unhonoured layout option(s): elk.layered.nodePlacement.strategy (accepted, not honoured)
582
541
  ----
583
542
 
584
- === SPOrE Overlap Removal
585
-
586
- Removes overlaps between nodes while preserving the overall structure.
543
+ A value of the wrong type, such as `"abc"` for `elk.spacing.nodeNode`, raises
544
+ `Elkrb::ValidationError`.
587
545
 
588
- Use cases:
546
+ == Advanced features
589
547
 
590
- * Post-processing for force-directed layouts
591
- * Cleaning up manually positioned diagrams
592
- * Overlap resolution
593
- * Layout refinement
548
+ === Edge routing
594
549
 
595
- Options:
550
+ Choose the routing style with `"elk.edgeRouting"` (`ORTHOGONAL`, `POLYLINE`
551
+ or `SPLINES`). The computed route is on the edge's sections:
596
552
 
597
553
  [source,ruby]
598
554
  ----
599
- layoutOptions: {
600
- "elk.algorithm" => "sporeOverlap",
601
- "elk.spacing.nodeNode" => 10,
602
- "sporeOverlap.maxIterations" => 100
603
- }
604
- ----
605
-
606
- === SPOrE Compaction
555
+ require "elkrb"
607
556
 
608
- Compacts layouts by removing whitespace while maintaining structure.
557
+ graph = {
558
+ id: "root",
559
+ layoutOptions: {
560
+ "elk.algorithm" => "layered",
561
+ "elk.direction" => "DOWN",
562
+ "elk.edgeRouting" => "ORTHOGONAL",
563
+ },
564
+ children: [
565
+ { id: "a", width: 60, height: 40 },
566
+ { id: "b", width: 60, height: 40 },
567
+ { id: "c", width: 60, height: 40 },
568
+ ],
569
+ edges: [
570
+ { id: "e1", sources: ["a"], targets: ["b"] },
571
+ { id: "e2", sources: ["a"], targets: ["c"] },
572
+ ],
573
+ }
609
574
 
610
- Use cases:
575
+ result = Elkrb.layout(graph)
611
576
 
612
- * Minimizing diagram size
613
- * Reducing whitespace
614
- * Creating compact layouts
615
- * Space optimization
577
+ result.edges.each do |edge|
578
+ section = edge.sections.first
579
+ bends = section.bend_points.map { |p| [p.x.round(1), p.y.round(1)] }
580
+ puts "#{edge.id}: (#{section.start_point.x.round(1)}, " \
581
+ "#{section.start_point.y.round(1)}) -> " \
582
+ "(#{section.end_point.x.round(1)}, #{section.end_point.y.round(1)}) " \
583
+ "bends=#{bends}"
584
+ end
585
+ ----
616
586
 
617
- Options:
587
+ Prints:
618
588
 
619
- [source,ruby]
589
+ [source]
620
590
  ----
621
- layoutOptions: {
622
- "elk.algorithm" => "sporeCompaction",
623
- "sporeCompaction.compactionStrategy" => "BOTH", # HORIZONTAL, VERTICAL, BOTH
624
- "elk.spacing.nodeNode" => 10,
625
- "sporeCompaction.normalize" => true
626
- }
591
+ e1: (42.0, 52.0) -> (42.0, 72.0) bends=[]
592
+ e2: (62.0, 52.0) -> (122.0, 72.0) bends=[[62.0, 62.0], [122.0, 62.0]]
627
593
  ----
628
594
 
629
- == Advanced features
630
-
631
- === Edge routing
632
-
633
- ElkRb supports advanced edge routing with bend points and port awareness.
595
+ With `"elk.edgeRouting" => "SPLINES"` the section carries the control points of
596
+ the curve in `bend_points`:
634
597
 
635
598
  [source,ruby]
636
599
  ----
637
- # Enable edge routing with orthogonal bend points
600
+ require "elkrb"
601
+
638
602
  graph = {
639
- layoutOptions: {
640
- "elk.algorithm" => "layered",
641
- "elk.edgeRouting" => "ORTHOGONAL"
642
- },
643
- # ... nodes and edges
603
+ id: "root",
604
+ layoutOptions: { "elk.algorithm" => "layered", "elk.edgeRouting" => "SPLINES" },
605
+ children: [
606
+ { id: "a", width: 60, height: 40 },
607
+ { id: "b", width: 60, height: 40 },
608
+ ],
609
+ edges: [{ id: "e1", sources: ["a"], targets: ["b"] }],
644
610
  }
645
611
 
646
612
  result = Elkrb.layout(graph)
613
+ section = result.edges.first.sections.first
614
+ puts section.bend_points.size
615
+ puts section.start_point.x
616
+ ----
647
617
 
648
- # Access edge sections with bend points
649
- edge_section = result[:edges][0][:sections][0]
650
- puts edge_section[:startPoint] # { x: 50.0, y: 60.0 }
651
- puts edge_section[:endPoint] # { x: 150.0, y: 60.0 }
652
- puts edge_section[:bendPoints] # [{ x: 100.0, y: 30.0 }, ...]
618
+ Prints:
619
+
620
+ [source]
621
+ ----
622
+ 2
623
+ 72.0
653
624
  ----
654
625
 
655
626
  === Hierarchical graphs
656
627
 
657
- Nested node structures are automatically handled with recursive layout.
628
+ A node with `children` is laid out recursively. The parent is sized to contain
629
+ its children, and child coordinates are relative to the parent.
658
630
 
659
631
  [source,ruby]
660
632
  ----
633
+ require "elkrb"
634
+
661
635
  graph = {
662
- layoutOptions: {
663
- "elk.algorithm" => "layered",
664
- "hierarchical" => true
665
- },
636
+ id: "root",
637
+ layoutOptions: { "elk.algorithm" => "layered" },
666
638
  children: [
667
639
  {
668
640
  id: "parent",
669
- width: 300,
670
- height: 200,
671
641
  children: [
672
642
  { id: "child1", width: 80, height: 60 },
673
- { id: "child2", width: 80, height: 60 }
643
+ { id: "child2", width: 80, height: 60 },
674
644
  ],
675
- edges: [
676
- { sources: ["child1"], targets: ["child2"] }
677
- ]
678
- }
679
- ]
645
+ edges: [{ id: "inner", sources: ["child1"], targets: ["child2"] }],
646
+ },
647
+ ],
680
648
  }
681
649
 
682
650
  result = Elkrb.layout(graph)
683
- # Parent bounds automatically calculated to contain children
684
- # Child nodes recursively laid out within parent
651
+
652
+ parent = result.children.first
653
+ puts "parent: #{parent.width} x #{parent.height}"
654
+ parent.children.each { |child| puts "#{child.id}: (#{child.x}, #{child.y})" }
655
+ ----
656
+
657
+ Prints:
658
+
659
+ [source]
660
+ ----
661
+ parent: 204.0 x 84.0
662
+ child1: (12.0, 12.0)
663
+ child2: (112.0, 12.0)
685
664
  ----
686
665
 
687
666
  === Label placement
688
667
 
689
- Automatic label positioning for nodes, edges, and ports.
668
+ Labels on nodes, edges and ports are positioned by the layout:
690
669
 
691
670
  [source,ruby]
692
671
  ----
672
+ require "elkrb"
673
+
693
674
  graph = {
675
+ id: "root",
694
676
  children: [
695
- {
696
- id: "n1",
697
- width: 100,
698
- height: 60,
699
- labels: [
700
- { text: "Node Label", width: 80, height: 20 }
701
- ],
702
- layoutOptions: {
703
- "node.label.placement" => "INSIDE CENTER" # or OUTSIDE TOP, etc.
704
- },
705
- ports: [
706
- {
707
- id: "p1",
708
- x: 0,
709
- y: 30,
710
- labels: [
711
- { text: "Port", width: 30, height: 15 }
712
- ]
713
- }
714
- ]
715
- }
677
+ { id: "n1", width: 100, height: 60,
678
+ labels: [{ text: "Node Label", width: 80, height: 20 }] },
679
+ { id: "n2", width: 100, height: 60 },
716
680
  ],
717
681
  edges: [
718
- {
719
- sources: ["n1"],
720
- targets: ["n2"],
721
- labels: [
722
- { text: "Edge Label", width: 60, height: 15 }
723
- ]
724
- }
725
- ]
682
+ { id: "e1", sources: ["n1"], targets: ["n2"],
683
+ labels: [{ text: "Edge Label", width: 60, height: 15 }] },
684
+ ],
726
685
  }
727
686
 
728
687
  result = Elkrb.layout(graph)
729
- # Labels automatically positioned based on placement options
730
- # Port labels placed according to port side detection
731
- # Edge labels placed at center of edge path
732
- ----
733
688
 
734
- Label placement options:
689
+ node_label = result.children.first.labels.first
690
+ edge_label = result.edges.first.labels.first
691
+ puts "#{node_label.text}: (#{node_label.x}, #{node_label.y})"
692
+ puts "#{edge_label.text}: (#{edge_label.x}, #{edge_label.y})"
693
+ ----
735
694
 
736
- * `node.label.placement` - INSIDE/OUTSIDE + TOP/BOTTOM/LEFT/RIGHT/CENTER
737
- * `port.label.placement` - Same format as node labels
738
- * `label.padding` - Internal spacing within labels
739
- * `label.margin` - External spacing around labels
740
- * `label.placement.disabled` - Disable automatic placement
695
+ Prints:
741
696
 
742
- == Advanced features
697
+ [source]
698
+ ----
699
+ Node Label: (0.0, 0.0)
700
+ Edge Label: (92.0, 34.5)
701
+ ----
743
702
 
744
- === Spline Edge Routing
703
+ === Ports
745
704
 
746
- Use smooth Bezier curves for aesthetically pleasing edge routing:
705
+ A port has a `side` (`NORTH`, `SOUTH`, `EAST` or `WEST`) and an `index` that
706
+ orders it along that side. Edges name ports as their sources and targets.
747
707
 
748
708
  [source,ruby]
749
709
  ----
750
- graph.layout_options = Elkrb::Graph::LayoutOptions.new(
751
- algorithm: "layered",
752
- edge_routing: "SPLINES",
753
- spline_curvature: 0.5
754
- )
755
- ----
710
+ require "elkrb"
711
+
712
+ graph = Elkrb::Graph::Graph.new(id: "root")
713
+ a = Elkrb::Graph::Node.new(id: "a", width: 100, height: 60)
714
+ a.ports = [Elkrb::Graph::Port.new(id: "a_out", side: "EAST", index: 0)]
715
+ b = Elkrb::Graph::Node.new(id: "b", width: 100, height: 60)
716
+ b.ports = [Elkrb::Graph::Port.new(id: "b_in", side: "WEST", index: 0)]
717
+ graph.children = [a, b]
718
+ graph.edges = [
719
+ Elkrb::Graph::Edge.new(id: "e1", sources: ["a_out"], targets: ["b_in"]),
720
+ ]
756
721
 
757
- // See link:SPLINE_ROUTING_GUIDE.md[Spline Routing Guide] for details.
722
+ result = Elkrb.layout(graph)
758
723
 
759
- === Advanced Port Constraints
724
+ result.children.each do |node|
725
+ node.ports.each do |port|
726
+ puts "#{port.id}: side=#{port.side} at (#{port.x}, #{port.y})"
727
+ end
728
+ end
729
+ ----
760
730
 
761
- Control port placement and ordering on nodes:
731
+ Prints:
762
732
 
763
- [source,ruby]
733
+ [source]
764
734
  ----
765
- port = Elkrb::Graph::Port.new(
766
- id: "p1",
767
- side: "WEST",
768
- index: 0
769
- )
735
+ a_out: side=EAST at (100.0, 30.0)
736
+ b_in: side=WEST at (0.0, 30.0)
770
737
  ----
771
738
 
772
- // See link:PORT_CONSTRAINTS_GUIDE.md[Port Constraints Guide] for details.
739
+ See `examples/port_constraints_demo.rb` for more.
773
740
 
774
- === Self-loop Support
741
+ === Self-loops
775
742
 
776
- Create edges connecting nodes to themselves:
743
+ An edge whose source and target are the same node, or ports of the same node,
744
+ is a self-loop. It starts and ends on the node (or port) border and its route
745
+ runs outside the node:
777
746
 
778
747
  [source,ruby]
779
748
  ----
780
- edge = Elkrb::Graph::Edge.new(
781
- id: "loop",
782
- sources: ["n1"],
783
- targets: ["n1"]
784
- )
785
- ----
786
-
787
- // See link:SELF_LOOP_GUIDE.md[Self-loop Guide] for details.
749
+ require "elkrb"
788
750
 
751
+ graph = {
752
+ id: "root",
753
+ children: [{ id: "n1", width: 100, height: 60 }],
754
+ edges: [{ id: "loop", sources: ["n1"], targets: ["n1"] }],
755
+ }
789
756
 
790
- === Graphviz DOT Export
757
+ result = Elkrb.layout(graph)
791
758
 
792
- NOTE: DOT export requires Graphviz installed on your system. ElkRb does not
793
- include Graphviz itself, only the DOT serialization functionality.
759
+ section = result.edges.first.sections.first
760
+ puts "sections: #{result.edges.first.sections.size}"
761
+ puts "start: (#{section.start_point.x}, #{section.start_point.y})"
762
+ puts "bends: #{section.bend_points.map { |p| [p.x, p.y] }.inspect}"
763
+ puts "end: (#{section.end_point.x}, #{section.end_point.y})"
764
+ ----
794
765
 
795
- Export layouts to DOT format:
766
+ Prints:
796
767
 
797
- [source,ruby]
768
+ [source]
798
769
  ----
799
- result = Elkrb.layout(graph)
800
- dot_string = Elkrb.export_dot(result)
801
- File.write("output.dot", dot_string)
770
+ sections: 1
771
+ start: (112.0, 37.0)
772
+ bends: [[172.0, 37.0], [172.0, 47.0]]
773
+ end: (112.0, 47.0)
802
774
  ----
803
775
 
804
- // See link:DOT_SERIALIZER_GUIDE.md[DOT Export Guide] for details.
776
+ See `examples/self_loop_demo.rb` for more.
805
777
 
778
+ === Graphviz DOT export
806
779
 
807
- == Performance
780
+ `Elkrb.export_dot` serializes a graph to DOT text. It does not need Graphviz;
781
+ only rendering the DOT to an image does (`elkrb render`).
808
782
 
809
- ElkRb provides production-ready performance for most use cases.
810
-
811
- TODO: See link:docs/PERFORMANCE.adoc[Performance Benchmarks] for detailed
812
- comparisons with elkjs and Java ELK.
813
-
814
- Quick overview of algorithm performance:
783
+ [source,ruby]
784
+ ----
785
+ require "elkrb"
815
786
 
816
- * Fast algorithms (< 10ms): Box, Radial, VertiFlex
817
- * Medium algorithms (10-50ms): Layered, Stress, MRTree
818
- * Complex algorithms (50-500ms): Force, RectPacking, Libavoid
787
+ graph = {
788
+ id: "root",
789
+ children: [
790
+ { id: "a", width: 60, height: 40 },
791
+ { id: "b", width: 60, height: 40 },
792
+ ],
793
+ edges: [{ id: "e1", sources: ["a"], targets: ["b"] }],
794
+ }
819
795
 
820
- Suitable for:
796
+ result = Elkrb.layout(graph)
797
+ puts Elkrb.export_dot(result, rankdir: "LR")
798
+ ----
821
799
 
822
- * Real-time layout of small to medium graphs (< 100 nodes)
823
- * Batch processing of large graphs
824
- * Interactive diagram editors
800
+ Prints:
825
801
 
802
+ [source]
803
+ ----
804
+ digraph G
805
+ {
806
+ rankdir=LR
807
+ size="2.2777777777777777,0.8888888888888888"
808
+ a [label="a", width=0.83, height=0.56, fixedsize=true, shape=box]
809
+ b [label="b", width=0.83, height=0.56, fixedsize=true, shape=box]
810
+ a -> b
811
+ }
812
+ ----
826
813
 
827
814
  == Migration from elkjs
828
815
 
829
- If you're migrating from elkjs, see
830
- link:docs/MIGRATION_FROM_ELKJS.adoc[Migration Guide] for a smooth transition.
816
+ The elkjs call `elk.layout(graph).then(...)` becomes `Elkrb.layout(graph)`:
831
817
 
832
- Key differences:
818
+ * It returns an `Elkrb::Graph::Graph` synchronously, not a Promise of a plain
819
+ object. Read it with attribute readers (`result.children[0].x`), or call
820
+ `to_json` for the ELK JSON.
821
+ * The graph may be a Hash with ELK's camelCase keys (`layoutOptions`), the
822
+ same shape elkjs takes.
823
+ * Call-level options are the second argument.
824
+ * YAML round-tripping is available through `to_yaml` and `from_yaml`.
833
825
 
834
- * Ruby API instead of JavaScript Promise-based API
835
- * Additional algorithms (TopdownPacking, Libavoid, VertiFlex)
836
- * YAML serialization support
826
+ See <<compatibility-with-elk>> for where results can differ.
837
827
 
838
828
  == Data Model
839
829
 
@@ -846,7 +836,9 @@ The root container for nodes and edges.
846
836
  `width, height`:: Dimensions
847
837
  `children`:: Array of Node objects
848
838
  `edges`:: Array of Edge objects
849
- `layout_options`:: LayoutOptions object
839
+ `labels`:: Array of Label objects
840
+ `ports`:: Array of Port objects
841
+ `layout_options`:: Hash of ELK option ids to values
850
842
  `properties`:: Custom properties hash
851
843
 
852
844
  === Node
@@ -855,12 +847,13 @@ Represents a graph node (box, shape, entity).
855
847
 
856
848
  `id`:: Unique identifier
857
849
  `x, y`:: Position coordinates
858
- `width, height`:: Required dimensions
850
+ `width, height`:: Dimensions
859
851
  `labels`:: Array of Label objects
860
852
  `ports`:: Array of Port objects
861
853
  `children`:: Nested nodes (for hierarchical graphs)
862
854
  `edges`:: Local edges
863
855
  `layout_options`:: Node-specific layout options
856
+ `constraints`:: Placement constraints
864
857
  `properties`:: Custom properties
865
858
 
866
859
  === Edge
@@ -874,6 +867,8 @@ Represents connections between nodes.
874
867
  `sections`:: Computed routing (EdgeSection objects)
875
868
  `layout_options`:: Edge-specific layout options
876
869
  `properties`:: Custom properties
870
+ `junction_points`:: Junction points of the routed edge
871
+ `container`:: Id of the container the edge belongs to
877
872
 
878
873
  === Port
879
874
 
@@ -882,8 +877,12 @@ Explicit attachment points on node borders.
882
877
  `id`:: Unique identifier
883
878
  `x, y`:: Position relative to node
884
879
  `width, height`:: Port dimensions
880
+ `side`:: `NORTH`, `SOUTH`, `EAST` or `WEST` (`nil` until set)
881
+ `index`:: Order along the side (`nil` until set or assigned by layout)
882
+ `offset`:: Port offset (`nil` until set)
885
883
  `labels`:: Array of Label objects
886
884
  `layout_options`:: Port-specific layout options
885
+ `properties`:: Custom properties
887
886
 
888
887
  === Label
889
888
 
@@ -893,37 +892,84 @@ Text labels for nodes, edges, or ports.
893
892
  `text`:: Label text
894
893
  `x, y`:: Position coordinates
895
894
  `width, height`:: Label dimensions
895
+ `layout_options`:: Label-specific layout options
896
+ `properties`:: Custom properties
897
+
898
+ === Layout options
899
+
900
+ `layout_options` is a Hash on every model (graph, node, edge, port, label).
901
+ Keys are ELK option ids; values are whatever that option takes. Symbol keys
902
+ are stringified on assignment, so `:"elk.direction"` and `"elk.direction"` are
903
+ equivalent.
904
+
905
+ [source,ruby]
906
+ ----
907
+ require "elkrb"
896
908
 
897
- === LayoutOptions
909
+ graph = Elkrb::Graph::Graph.new(id: "root")
910
+ graph.layout_options = { "elk.direction": "DOWN", "elk.algorithm" => "layered" }
911
+ p graph.layout_options.keys
912
+
913
+ begin
914
+ graph.layout_options = { "text" => "a" }
915
+ rescue Elkrb::ValidationError => e
916
+ puts e.message
917
+ end
918
+ ----
898
919
 
899
- Configuration for layout algorithms.
920
+ Prints:
900
921
 
901
- `algorithm`:: Algorithm name (layered, force, stress, etc.)
902
- `direction`:: Layout direction (DOWN, UP, LEFT, RIGHT)
903
- `spacing_node_node`:: Node-to-node spacing
904
- `spacing_edge_node`:: Edge-to-node spacing
905
- `edge_routing`:: Edge routing style (ORTHOGONAL, POLYLINE, SPLINES)
906
- `properties`:: Additional algorithm-specific options
922
+ [source]
923
+ ----
924
+ ["elk.direction", "elk.algorithm"]
925
+ layoutOptions cannot use `text` or `elements` as a key; both are reserved by lutaml-model
926
+ ----
907
927
 
928
+ [[command-line-interface]]
908
929
  == Command-Line Interface
909
930
 
910
- The `elkrb` command provides utilities for layout operations.
931
+ The `elkrb` command lays out, converts, validates and renders graph files.
932
+ `elkrb help` lists the commands; `elkrb help COMMAND` lists a command's flags.
933
+ Every command takes `--verbose`.
934
+
935
+ Input files are JSON, YAML or ELKT. The format is taken from the file
936
+ extension, and sniffed from the content when the extension is not one of
937
+ those.
938
+
939
+ === layout
911
940
 
912
- === Layout Command
941
+ Lays out a graph and writes the result.
913
942
 
914
943
  [source,sh]
915
944
  ----
916
- # Layout a graph from file
917
- $ elkrb layout input.yml --output result.yml
945
+ $ elkrb layout input.json # JSON to stdout
946
+ $ elkrb layout input.json --output result.json
947
+ $ elkrb layout input.json --format yaml --output result.yml
948
+ $ elkrb layout input.json --algorithm layered --direction DOWN --spacing 50
949
+ ----
918
950
 
919
- # Specify algorithm
920
- $ elkrb layout input.yml --algorithm layered --output result.yml
951
+ The output format is `--format` when supplied. Otherwise, a `.yml` or `.yaml`
952
+ `--output` extension selects YAML (case-insensitively); stdout and every other
953
+ extension use JSON. Flags that set a layout option write it onto the root graph,
954
+ where it outranks the same option in the file.
921
955
 
922
- # Output JSON
923
- $ elkrb layout input.yml --format json --output result.json
924
- ----
956
+ [cols="1,3",options="header"]
957
+ |===
958
+ | Flag | Meaning
959
+
960
+ | `--algorithm NAME` | Layout algorithm. Without it, the file's own `elk.algorithm`, else `layered`.
961
+ | `-o, --output FILE` | Output file (default: stdout).
962
+ | `--format json\|yaml` | Explicit output format; otherwise `.yml`/`.yaml` selects YAML and every other destination selects JSON.
963
+ | `--spacing N` | Node spacing (`elk.spacing.nodeNode`).
964
+ | `--layer-spacing N` | Layer spacing, for the layered algorithm.
965
+ | `--direction DIR` | `DOWN`, `RIGHT`, ...; applied by the layered and mrtree algorithms.
966
+ | `--edge-routing STYLE` | Edge routing strategy.
967
+ | `--padding-top N`, `--padding-bottom N`, `--padding-left N`, `--padding-right N` | Padding.
968
+ |===
925
969
 
926
- === Available Algorithms
970
+ === algorithms
971
+
972
+ Lists the registered algorithms.
927
973
 
928
974
  [source,sh]
929
975
  ----
@@ -935,43 +981,124 @@ Available Layout Algorithms:
935
981
  Name: Box
936
982
  Description: Arranges nodes in a grid pattern
937
983
 
984
+ disco
985
+ Name: DISCO
986
+ Description: Layout for disconnected graph components
987
+
938
988
  fixed
939
989
  Name: Fixed
940
990
  Description: Keeps nodes at their current positions
941
991
 
942
- force
943
- Name: Force-Directed
944
- Description: Physics-based layout using attractive and repulsive forces
945
- Category: force
992
+ ...
993
+ ----
994
+
995
+ `...` stands for the remaining algorithms. Categories other than `general`,
996
+ and hierarchy support, are listed per algorithm.
946
997
 
947
- layered
948
- Name: Layered (Sugiyama)
949
- Description: Hierarchical layout using the Sugiyama framework
950
- Category: hierarchical
951
- Supports Hierarchy: Yes
998
+ === options
952
999
 
953
- random
954
- Name: Random
955
- Description: Places nodes at random positions
1000
+ Lists every known layout option and its implementation status. Pass an
1001
+ algorithm name to list only the options that algorithm supports; `--json`
1002
+ writes the same information as JSON.
956
1003
 
957
- stress
958
- Name: Stress Minimization
959
- Description: High-quality layout using stress majorization
960
- Category: force
1004
+ [source,sh]
1005
+ ----
1006
+ $ elkrb options
1007
+ $ elkrb options layered --json
961
1008
  ----
962
1009
 
1010
+ === diagram
963
1011
 
964
- == Benchmarking
1012
+ Lays out a graph and writes a diagram in one step. The output format comes
1013
+ from the `--output` extension: `.json`, `.yml`/`.yaml`, `.dot`/`.gv`, `.elkt`,
1014
+ or the image formats `.png`, `.svg`, `.pdf`, `.ps` and `.eps`. Image formats
1015
+ need Graphviz.
1016
+
1017
+ [source,sh]
1018
+ ----
1019
+ $ elkrb diagram input.json --output diagram.svg
1020
+ ✓ Diagram created: diagram.svg
1021
+ ----
1022
+
1023
+ Flags: `--output FILE` (required), `--algorithm`, `--direction`, `--spacing`,
1024
+ `--edge-routing` (as for `layout`), `--format` (used when the extension is not
1025
+ recognised) and `--preview` (open the result in the default viewer).
965
1026
 
966
- ElkRb provides production-ready performance.
1027
+ === convert
967
1028
 
968
- ElkRb can be run in a benchmark mode to compare its performance against
969
- elkjs and Java ELK (TODO).
1029
+ Converts a graph file between JSON, YAML, DOT and ELKT, without laying it out.
970
1030
 
1031
+ [source,sh]
1032
+ ----
1033
+ $ elkrb convert input.json --output input.dot
1034
+ ✓ Converted input.json → input.dot (dot)
1035
+ ----
1036
+
1037
+ Flags: `--output FILE` (required) and `--format`. The output extension picks
1038
+ the format (`.json`, `.yml`/`.yaml`, `.dot`/`.gv`, `.elkt`).
1039
+
1040
+ === render
1041
+
1042
+ Renders a DOT file to an image with Graphviz.
1043
+
1044
+ [source,sh]
1045
+ ----
1046
+ $ elkrb render input.dot --output input.png
1047
+ ✓ Rendered input.dot → input.png
1048
+ ----
1049
+
1050
+ Flags: `--output FILE` (required), `--engine` (Graphviz engine such as `dot`,
1051
+ `neato`, `fdp`; default `dot`) and `--dpi` (default 96). It fails when
1052
+ Graphviz is not installed.
1053
+
1054
+ === validate
1055
+
1056
+ Checks the structure of a graph file.
1057
+
1058
+ [source,sh]
1059
+ ----
1060
+ $ elkrb validate input.json
1061
+ ✅ input.json is valid
1062
+ ----
1063
+
1064
+ `--strict` also requires every node to have a positive `width` and `height`,
1065
+ and `layoutOptions` to be a Hash.
1066
+
1067
+ === batch
1068
+
1069
+ Lays out every `.json`, `.yml`, `.yaml` and `.elkt` file in a directory.
971
1070
 
972
1071
  [source,sh]
973
1072
  ----
974
- # Run all ElkRb benchmarks and generate report
1073
+ $ elkrb batch graphs/ --output-dir out/
1074
+ ✓ Diagram created: out/a.svg
1075
+ ✓ Diagram created: out/b.svg
1076
+
1077
+ ✓ Processed 2 file(s) → out/
1078
+ ----
1079
+
1080
+ Flags: `--output-dir DIR` (required), `--format` (default `svg`),
1081
+ `--algorithm`, `--direction` and `--edge-routing`. The command fails when any
1082
+ file fails.
1083
+
1084
+ === version
1085
+
1086
+ [source,sh]
1087
+ ----
1088
+ $ elkrb version
1089
+ elkrb version 2.0.0
1090
+ ----
1091
+
1092
+ == Benchmarking
1093
+
1094
+ `rake` tasks time each algorithm on four generated graphs (10 to 200 nodes) and
1095
+ write the results to `benchmarks/results/`. ElkRb does not publish performance
1096
+ figures in this README: the committed results are a snapshot from an earlier
1097
+ release, so measure on your own hardware.
1098
+
1099
+ [source,sh]
1100
+ ----
1101
+ # Run all ElkRb benchmarks and generate the report
975
1102
  $ rake benchmark:all
976
1103
 
977
1104
  # Generate test graphs
@@ -983,10 +1110,11 @@ $ rake benchmark:elkrb
983
1110
  # Run elkjs benchmarks (requires Node.js and elkjs)
984
1111
  $ rake benchmark:elkjs
985
1112
 
986
- # Generate performance report
1113
+ # Generate the report (writes docs/PERFORMANCE.adoc)
987
1114
  $ rake benchmark:report
988
1115
  ----
989
1116
 
1117
+ See `benchmarks/README.md` for the method.
990
1118
 
991
1119
  == Development
992
1120
 
@@ -1005,6 +1133,16 @@ $ bundle exec rubocop
1005
1133
  $ bundle exec rubocop -A # Auto-correct
1006
1134
  ----
1007
1135
 
1136
+ === Examples
1137
+
1138
+ `examples/*.rb` are runnable scripts, and `spec/examples_spec.rb` runs every
1139
+ one of them.
1140
+
1141
+ [source,sh]
1142
+ ----
1143
+ $ bundle exec ruby examples/simple_graph.rb
1144
+ ----
1145
+
1008
1146
  == References
1009
1147
 
1010
1148
  Other libraries that implement ELK: