sirena 0.1.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.
- checksums.yaml +7 -0
- data/.github/workflows/build_deploy.yml +59 -0
- data/.github/workflows/links.yml +85 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +68 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.rubocop_todo.yml +70 -0
- data/ARCHITECTURE.md +744 -0
- data/Gemfile +12 -0
- data/LICENSE +25 -0
- data/README.adoc +357 -0
- data/Rakefile +11 -0
- data/docs/.gitignore +1 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +182 -0
- data/docs/_diagram_types/architecture-diagram.adoc +314 -0
- data/docs/_diagram_types/block-diagram.adoc +345 -0
- data/docs/_diagram_types/c4-diagram.adoc +559 -0
- data/docs/_diagram_types/class-diagram.adoc +816 -0
- data/docs/_diagram_types/er-diagram.adoc +719 -0
- data/docs/_diagram_types/error-diagram.adoc +114 -0
- data/docs/_diagram_types/examples/flowchart-examples.adoc +29 -0
- data/docs/_diagram_types/flowchart.adoc +488 -0
- data/docs/_diagram_types/gantt-chart.adoc +502 -0
- data/docs/_diagram_types/git-graph.adoc +600 -0
- data/docs/_diagram_types/index.adoc +192 -0
- data/docs/_diagram_types/info-diagram.adoc +103 -0
- data/docs/_diagram_types/kanban-diagram.adoc +262 -0
- data/docs/_diagram_types/mindmap.adoc +603 -0
- data/docs/_diagram_types/packet-diagram.adoc +378 -0
- data/docs/_diagram_types/pie-chart.adoc +335 -0
- data/docs/_diagram_types/quadrant-chart.adoc +406 -0
- data/docs/_diagram_types/radar-chart.adoc +528 -0
- data/docs/_diagram_types/requirement-diagram.adoc +416 -0
- data/docs/_diagram_types/sankey-diagram.adoc +357 -0
- data/docs/_diagram_types/sequence-diagram.adoc +664 -0
- data/docs/_diagram_types/state-diagram.adoc +658 -0
- data/docs/_diagram_types/timeline.adoc +352 -0
- data/docs/_diagram_types/treemap-diagram.adoc +462 -0
- data/docs/_diagram_types/user-journey.adoc +602 -0
- data/docs/_features/index.adoc +129 -0
- data/docs/_guides/cli-reference.adoc +203 -0
- data/docs/_guides/index.adoc +56 -0
- data/docs/_guides/installation.adoc +100 -0
- data/docs/_guides/quick-start.adoc +132 -0
- data/docs/_pages/comparison.adoc +441 -0
- data/docs/_pages/compatibility.adoc +300 -0
- data/docs/_pages/index.adoc +39 -0
- data/docs/_references/index.adoc +103 -0
- data/docs/_tutorials/index.adoc +57 -0
- data/docs/index.adoc +166 -0
- data/docs/lychee.toml +54 -0
- data/examples/.gitignore +10 -0
- data/examples/README.adoc +196 -0
- data/examples/README.md +64 -0
- data/examples/architecture/01-basic-services.mmd +9 -0
- data/examples/architecture/01-basic-services.svg +37 -0
- data/examples/architecture/02-service-groups.mmd +16 -0
- data/examples/architecture/02-service-groups.svg +55 -0
- data/examples/architecture/README.adoc +79 -0
- data/examples/block/01-basic-blocks.mmd +13 -0
- data/examples/block/01-basic-blocks.svg +44 -0
- data/examples/block/02-block-shapes.mmd +13 -0
- data/examples/block/02-block-shapes.svg +47 -0
- data/examples/block/README.adoc +85 -0
- data/examples/c4/01-context-diagram.mmd +10 -0
- data/examples/c4/01-context-diagram.svg +45 -0
- data/examples/c4/02-container-diagram.mmd +24 -0
- data/examples/c4/02-container-diagram.svg +105 -0
- data/examples/c4/README.adoc +92 -0
- data/examples/class_diagram/01-basic-classes.mmd +61 -0
- data/examples/class_diagram/01-basic-classes.svg +117 -0
- data/examples/class_diagram/02-relationships.mmd +61 -0
- data/examples/class_diagram/02-relationships.svg +129 -0
- data/examples/class_diagram/README.adoc +93 -0
- data/examples/er_diagram/01-basic-entities.mmd +64 -0
- data/examples/er_diagram/01-basic-entities.svg +5 -0
- data/examples/er_diagram/02-cardinality.mmd +57 -0
- data/examples/er_diagram/02-cardinality.svg +125 -0
- data/examples/er_diagram/README.adoc +88 -0
- data/examples/error/01-basic-error.mmd +1 -0
- data/examples/error/01-basic-error.svg +13 -0
- data/examples/error/02-error-display.mmd +1 -0
- data/examples/error/02-error-display.svg +13 -0
- data/examples/error/README.adoc +71 -0
- data/examples/error_message_example.svg +13 -0
- data/examples/flowchart/00-original.mmd +13 -0
- data/examples/flowchart/00-original.svg +5 -0
- data/examples/flowchart/01-basic-flow.mmd +7 -0
- data/examples/flowchart/01-basic-flow.svg +52 -0
- data/examples/flowchart/01-basic-flow.yml +13 -0
- data/examples/flowchart/02*.svg +87 -0
- data/examples/flowchart/02-node-shapes.mmd +9 -0
- data/examples/flowchart/02-node-shapes.svg +33 -0
- data/examples/flowchart/03-edge-types.mmd +7 -0
- data/examples/flowchart/03-edge-types.svg +53 -0
- data/examples/flowchart/04-subgraphs.mmd +9 -0
- data/examples/flowchart/04-subgraphs.svg +33 -0
- data/examples/flowchart/05-styling.mmd +9 -0
- data/examples/flowchart/05-styling.svg +33 -0
- data/examples/flowchart/06-complex-flow.mmd +8 -0
- data/examples/flowchart/06-complex-flow.svg +59 -0
- data/examples/flowchart/README.adoc +167 -0
- data/examples/gantt/01-simple-timeline.* +14 -0
- data/examples/gantt/01-simple-timeline.mmd +6 -0
- data/examples/gantt/01-simple-timeline.svg +26 -0
- data/examples/gantt/02-task-dependencies.mmd +6 -0
- data/examples/gantt/02-task-dependencies.svg +26 -0
- data/examples/gantt/README.adoc +86 -0
- data/examples/git_graph/01-linear-history.mmd +12 -0
- data/examples/git_graph/01-linear-history.svg +26 -0
- data/examples/git_graph/02-branching.mmd +12 -0
- data/examples/git_graph/02-branching.svg +26 -0
- data/examples/git_graph/README.adoc +73 -0
- data/examples/info/02-showinfo.mmd +1 -0
- data/examples/info/02-showinfo.svg +10 -0
- data/examples/info/README.adoc +58 -0
- data/examples/info_showinfo_example.svg +10 -0
- data/examples/kanban/01-simple-board.mmd +8 -0
- data/examples/kanban/01-simple-board.svg +43 -0
- data/examples/kanban/02-workflow.mmd +8 -0
- data/examples/kanban/02-workflow.svg +43 -0
- data/examples/kanban/README.adoc +79 -0
- data/examples/mindmap/01-simple-tree.mmd +19 -0
- data/examples/mindmap/01-simple-tree.svg +61 -0
- data/examples/mindmap/02-knowledge-map.mmd +19 -0
- data/examples/mindmap/02-knowledge-map.svg +61 -0
- data/examples/mindmap/README.adoc +77 -0
- data/examples/packet/01-basic-packet.* +17 -0
- data/examples/packet/01-basic-packet.mmd +4 -0
- data/examples/packet/01-basic-packet.svg +82 -0
- data/examples/packet/README.adoc +58 -0
- data/examples/pie/01-simple-chart.mmd +5 -0
- data/examples/pie/01-simple-chart.svg +17 -0
- data/examples/pie/02-labeled-slices.mmd +6 -0
- data/examples/pie/02-labeled-slices.svg +19 -0
- data/examples/pie/README.adoc +75 -0
- data/examples/quadrant/01-basic-quadrant.mmd +13 -0
- data/examples/quadrant/01-basic-quadrant.svg +33 -0
- data/examples/quadrant/02-positioned-items.mmd +14 -0
- data/examples/quadrant/02-positioned-items.svg +35 -0
- data/examples/quadrant/README.adoc +84 -0
- data/examples/radar/01-simple-radar.* +5 -0
- data/examples/radar/01-simple-radar.mmd +3 -0
- data/examples/radar/01-simple-radar.svg +25 -0
- data/examples/radar/02-multiple-curves.mmd +4 -0
- data/examples/radar/02-multiple-curves.svg +43 -0
- data/examples/radar/README.adoc +75 -0
- data/examples/requirement/01-basic-requirements.mmd +23 -0
- data/examples/requirement/01-basic-requirements.svg +49 -0
- data/examples/requirement/02-risk-levels.mmd +23 -0
- data/examples/requirement/02-risk-levels.svg +49 -0
- data/examples/requirement/README.adoc +85 -0
- data/examples/sankey/01-simple-flow.mmd +7 -0
- data/examples/sankey/01-simple-flow.svg +34 -0
- data/examples/sankey/02-multi-stage.mmd +11 -0
- data/examples/sankey/02-multi-stage.svg +44 -0
- data/examples/sankey/README.adoc +74 -0
- data/examples/sequence/01-basic-sequence.mmd +27 -0
- data/examples/sequence/01-basic-sequence.svg +5 -0
- data/examples/sequence/02-activations.mmd +17 -0
- data/examples/sequence/02-activations.svg +78 -0
- data/examples/sequence/README.adoc +86 -0
- data/examples/state_diagram/01-simple-states.mmd +29 -0
- data/examples/state_diagram/01-simple-states.svg +5 -0
- data/examples/state_diagram/02-composite.mmd +19 -0
- data/examples/state_diagram/02-composite.svg +81 -0
- data/examples/state_diagram/README.adoc +90 -0
- data/examples/timeline/01-simple-timeline.mmd +11 -0
- data/examples/timeline/01-simple-timeline.svg +36 -0
- data/examples/timeline/02-periods.mmd +15 -0
- data/examples/timeline/02-periods.svg +47 -0
- data/examples/timeline/README.adoc +78 -0
- data/examples/treemap/01-basic-treemap.mmd +12 -0
- data/examples/treemap/01-basic-treemap.svg +59 -0
- data/examples/treemap/README.adoc +59 -0
- data/examples/user_journey/01-simple-journey.mmd +23 -0
- data/examples/user_journey/01-simple-journey.svg +5 -0
- data/examples/user_journey/02-multi-actor.mmd +18 -0
- data/examples/user_journey/02-multi-actor.svg +129 -0
- data/examples/user_journey/README.adoc +81 -0
- data/examples/xychart/01-line-chart.mmd +5 -0
- data/examples/xychart/01-line-chart.svg +43 -0
- data/examples/xychart/02-bar-chart.mmd +7 -0
- data/examples/xychart/02-bar-chart.svg +48 -0
- data/examples/xychart/README.adoc +80 -0
- data/exe/sirena +7 -0
- data/lib/sirena/cli.rb +138 -0
- data/lib/sirena/commands/batch.rb +117 -0
- data/lib/sirena/commands/render.rb +80 -0
- data/lib/sirena/commands/types.rb +29 -0
- data/lib/sirena/commands/version.rb +24 -0
- data/lib/sirena/diagram/architecture.rb +46 -0
- data/lib/sirena/diagram/base.rb +61 -0
- data/lib/sirena/diagram/block.rb +81 -0
- data/lib/sirena/diagram/c4.rb +328 -0
- data/lib/sirena/diagram/class_diagram.rb +385 -0
- data/lib/sirena/diagram/er_diagram.rb +238 -0
- data/lib/sirena/diagram/error.rb +38 -0
- data/lib/sirena/diagram/flowchart.rb +160 -0
- data/lib/sirena/diagram/gantt.rb +71 -0
- data/lib/sirena/diagram/git_graph.rb +36 -0
- data/lib/sirena/diagram/info.rb +38 -0
- data/lib/sirena/diagram/kanban.rb +178 -0
- data/lib/sirena/diagram/mindmap.rb +54 -0
- data/lib/sirena/diagram/packet.rb +79 -0
- data/lib/sirena/diagram/pie.rb +115 -0
- data/lib/sirena/diagram/quadrant.rb +138 -0
- data/lib/sirena/diagram/radar.rb +52 -0
- data/lib/sirena/diagram/requirement.rb +133 -0
- data/lib/sirena/diagram/sankey.rb +217 -0
- data/lib/sirena/diagram/sequence.rb +242 -0
- data/lib/sirena/diagram/state_diagram.rb +237 -0
- data/lib/sirena/diagram/timeline.rb +171 -0
- data/lib/sirena/diagram/treemap.rb +84 -0
- data/lib/sirena/diagram/user_journey.rb +149 -0
- data/lib/sirena/diagram/xy_chart.rb +76 -0
- data/lib/sirena/diagram.rb +8 -0
- data/lib/sirena/diagram_registry.rb +101 -0
- data/lib/sirena/engine.rb +292 -0
- data/lib/sirena/parser/architecture.rb +41 -0
- data/lib/sirena/parser/base.rb +41 -0
- data/lib/sirena/parser/block.rb +72 -0
- data/lib/sirena/parser/c4.rb +53 -0
- data/lib/sirena/parser/class_diagram.rb +63 -0
- data/lib/sirena/parser/er_diagram.rb +40 -0
- data/lib/sirena/parser/error.rb +49 -0
- data/lib/sirena/parser/flowchart.rb +71 -0
- data/lib/sirena/parser/gantt.rb +60 -0
- data/lib/sirena/parser/git_graph.rb +95 -0
- data/lib/sirena/parser/grammars/architecture.rb +145 -0
- data/lib/sirena/parser/grammars/block.rb +190 -0
- data/lib/sirena/parser/grammars/c4.rb +226 -0
- data/lib/sirena/parser/grammars/class_diagram.rb +284 -0
- data/lib/sirena/parser/grammars/common.rb +84 -0
- data/lib/sirena/parser/grammars/er_diagram.rb +114 -0
- data/lib/sirena/parser/grammars/error.rb +40 -0
- data/lib/sirena/parser/grammars/flowchart.rb +298 -0
- data/lib/sirena/parser/grammars/gantt.rb +252 -0
- data/lib/sirena/parser/grammars/git_graph.rb +167 -0
- data/lib/sirena/parser/grammars/info.rb +58 -0
- data/lib/sirena/parser/grammars/kanban.rb +83 -0
- data/lib/sirena/parser/grammars/mindmap.rb +115 -0
- data/lib/sirena/parser/grammars/packet.rb +73 -0
- data/lib/sirena/parser/grammars/pie.rb +128 -0
- data/lib/sirena/parser/grammars/quadrant.rb +199 -0
- data/lib/sirena/parser/grammars/radar.rb +150 -0
- data/lib/sirena/parser/grammars/requirement.rb +188 -0
- data/lib/sirena/parser/grammars/sankey.rb +104 -0
- data/lib/sirena/parser/grammars/sequence.rb +247 -0
- data/lib/sirena/parser/grammars/state_diagram.rb +172 -0
- data/lib/sirena/parser/grammars/timeline.rb +142 -0
- data/lib/sirena/parser/grammars/treemap.rb +120 -0
- data/lib/sirena/parser/grammars/xy_chart.rb +120 -0
- data/lib/sirena/parser/info.rb +49 -0
- data/lib/sirena/parser/kanban.rb +97 -0
- data/lib/sirena/parser/mindmap.rb +106 -0
- data/lib/sirena/parser/packet.rb +76 -0
- data/lib/sirena/parser/pie.rb +49 -0
- data/lib/sirena/parser/quadrant.rb +57 -0
- data/lib/sirena/parser/radar.rb +104 -0
- data/lib/sirena/parser/requirement.rb +70 -0
- data/lib/sirena/parser/sankey.rb +64 -0
- data/lib/sirena/parser/sequence.rb +51 -0
- data/lib/sirena/parser/state_diagram.rb +69 -0
- data/lib/sirena/parser/timeline.rb +57 -0
- data/lib/sirena/parser/transforms/architecture.rb +97 -0
- data/lib/sirena/parser/transforms/block.rb +254 -0
- data/lib/sirena/parser/transforms/c4.rb +347 -0
- data/lib/sirena/parser/transforms/class_diagram.rb +352 -0
- data/lib/sirena/parser/transforms/er_diagram.rb +169 -0
- data/lib/sirena/parser/transforms/error.rb +58 -0
- data/lib/sirena/parser/transforms/flowchart.rb +293 -0
- data/lib/sirena/parser/transforms/gantt.rb +215 -0
- data/lib/sirena/parser/transforms/git_graph.rb +160 -0
- data/lib/sirena/parser/transforms/info.rb +58 -0
- data/lib/sirena/parser/transforms/kanban.rb +176 -0
- data/lib/sirena/parser/transforms/mindmap.rb +227 -0
- data/lib/sirena/parser/transforms/packet.rb +63 -0
- data/lib/sirena/parser/transforms/pie.rb +143 -0
- data/lib/sirena/parser/transforms/quadrant.rb +177 -0
- data/lib/sirena/parser/transforms/radar.rb +126 -0
- data/lib/sirena/parser/transforms/requirement.rb +272 -0
- data/lib/sirena/parser/transforms/sankey.rb +122 -0
- data/lib/sirena/parser/transforms/sequence.rb +342 -0
- data/lib/sirena/parser/transforms/state_diagram.rb +292 -0
- data/lib/sirena/parser/transforms/timeline.rb +177 -0
- data/lib/sirena/parser/transforms/treemap.rb +81 -0
- data/lib/sirena/parser/transforms/xy_chart.rb +132 -0
- data/lib/sirena/parser/treemap.rb +98 -0
- data/lib/sirena/parser/user_journey.rb +120 -0
- data/lib/sirena/parser/xy_chart.rb +114 -0
- data/lib/sirena/parser.rb +8 -0
- data/lib/sirena/renderer/architecture.rb +251 -0
- data/lib/sirena/renderer/base.rb +251 -0
- data/lib/sirena/renderer/block.rb +286 -0
- data/lib/sirena/renderer/c4.rb +490 -0
- data/lib/sirena/renderer/class_diagram.rb +499 -0
- data/lib/sirena/renderer/er_diagram.rb +417 -0
- data/lib/sirena/renderer/error.rb +131 -0
- data/lib/sirena/renderer/flowchart.rb +301 -0
- data/lib/sirena/renderer/gantt.rb +331 -0
- data/lib/sirena/renderer/git_graph.rb +368 -0
- data/lib/sirena/renderer/info.rb +93 -0
- data/lib/sirena/renderer/kanban.rb +295 -0
- data/lib/sirena/renderer/mindmap.rb +396 -0
- data/lib/sirena/renderer/packet.rb +239 -0
- data/lib/sirena/renderer/pie.rb +235 -0
- data/lib/sirena/renderer/quadrant.rb +292 -0
- data/lib/sirena/renderer/radar.rb +323 -0
- data/lib/sirena/renderer/requirement.rb +371 -0
- data/lib/sirena/renderer/sankey.rb +255 -0
- data/lib/sirena/renderer/sequence.rb +424 -0
- data/lib/sirena/renderer/state_diagram.rb +328 -0
- data/lib/sirena/renderer/timeline.rb +304 -0
- data/lib/sirena/renderer/treemap.rb +152 -0
- data/lib/sirena/renderer/user_journey.rb +331 -0
- data/lib/sirena/renderer/xy_chart.rb +452 -0
- data/lib/sirena/renderer.rb +8 -0
- data/lib/sirena/svg/circle.rb +41 -0
- data/lib/sirena/svg/document.rb +103 -0
- data/lib/sirena/svg/element.rb +65 -0
- data/lib/sirena/svg/ellipse.rb +33 -0
- data/lib/sirena/svg/group.rb +71 -0
- data/lib/sirena/svg/line.rb +49 -0
- data/lib/sirena/svg/path.rb +76 -0
- data/lib/sirena/svg/polygon.rb +43 -0
- data/lib/sirena/svg/polyline.rb +35 -0
- data/lib/sirena/svg/rect.rb +57 -0
- data/lib/sirena/svg/style.rb +44 -0
- data/lib/sirena/svg/text.rb +72 -0
- data/lib/sirena/svg.rb +19 -0
- data/lib/sirena/text_measurement.rb +71 -0
- data/lib/sirena/theme/builtin/dark.yml +70 -0
- data/lib/sirena/theme/builtin/default.yml +80 -0
- data/lib/sirena/theme/builtin/high_contrast.yml +70 -0
- data/lib/sirena/theme/builtin/light.yml +70 -0
- data/lib/sirena/theme/color_palette.rb +48 -0
- data/lib/sirena/theme/effect_styles.rb +28 -0
- data/lib/sirena/theme/registry.rb +41 -0
- data/lib/sirena/theme/shape_styles.rb +28 -0
- data/lib/sirena/theme/spacing_config.rb +24 -0
- data/lib/sirena/theme/typography.rb +30 -0
- data/lib/sirena/theme.rb +69 -0
- data/lib/sirena/transform/architecture.rb +273 -0
- data/lib/sirena/transform/base.rb +199 -0
- data/lib/sirena/transform/block.rb +215 -0
- data/lib/sirena/transform/c4.rb +288 -0
- data/lib/sirena/transform/class_diagram.rb +296 -0
- data/lib/sirena/transform/er_diagram.rb +204 -0
- data/lib/sirena/transform/error.rb +39 -0
- data/lib/sirena/transform/flowchart.rb +161 -0
- data/lib/sirena/transform/gantt.rb +253 -0
- data/lib/sirena/transform/git_graph.rb +283 -0
- data/lib/sirena/transform/info.rb +39 -0
- data/lib/sirena/transform/kanban.rb +180 -0
- data/lib/sirena/transform/mindmap.rb +251 -0
- data/lib/sirena/transform/packet.rb +185 -0
- data/lib/sirena/transform/pie.rb +62 -0
- data/lib/sirena/transform/quadrant.rb +167 -0
- data/lib/sirena/transform/radar.rb +227 -0
- data/lib/sirena/transform/requirement.rb +233 -0
- data/lib/sirena/transform/sankey.rb +212 -0
- data/lib/sirena/transform/sequence.rb +143 -0
- data/lib/sirena/transform/state_diagram.rb +228 -0
- data/lib/sirena/transform/timeline.rb +139 -0
- data/lib/sirena/transform/treemap.rb +120 -0
- data/lib/sirena/transform/user_journey.rb +207 -0
- data/lib/sirena/transform/xy_chart.rb +273 -0
- data/lib/sirena/transform.rb +8 -0
- data/lib/sirena/version.rb +5 -0
- data/lib/sirena.rb +328 -0
- data/lib/tasks/benchmark.rake +532 -0
- data/lib/tasks/examples.rake +468 -0
- data/lib/tasks/generate_mermaid_fixtures.rake +363 -0
- data/lib/tasks/mermaid_fixtures.rake +46 -0
- data/scripts/extract_mermaid_tests.rb +493 -0
- data/scripts/rename_to_sirena.rb +73 -0
- data/sirena.gemspec +47 -0
- metadata +529 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "../diagram/sankey"
|
|
6
|
+
|
|
7
|
+
module Sirena
|
|
8
|
+
module Transform
|
|
9
|
+
# Sankey transformer for converting Sankey models to renderable structure.
|
|
10
|
+
#
|
|
11
|
+
# Handles layer assignment, node positioning, flow path calculation,
|
|
12
|
+
# and flow width proportional to values.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a Sankey diagram
|
|
15
|
+
# transform = SankeyTransform.new
|
|
16
|
+
# data = transform.to_graph(sankey_diagram)
|
|
17
|
+
class SankeyTransform < Base
|
|
18
|
+
# Node height and spacing
|
|
19
|
+
NODE_HEIGHT = 40
|
|
20
|
+
NODE_SPACING = 30
|
|
21
|
+
LAYER_SPACING = 150
|
|
22
|
+
MIN_NODE_WIDTH = 20
|
|
23
|
+
MAX_NODE_WIDTH = 40
|
|
24
|
+
|
|
25
|
+
# Converts a Sankey diagram to a layout structure with calculated positions.
|
|
26
|
+
#
|
|
27
|
+
# @param diagram [Diagram::SankeyDiagram] the sankey diagram to transform
|
|
28
|
+
# @return [Hash] data structure for rendering
|
|
29
|
+
# @raise [TransformError] if diagram is invalid
|
|
30
|
+
def to_graph(diagram)
|
|
31
|
+
raise TransformError, "Invalid diagram" unless diagram.valid?
|
|
32
|
+
|
|
33
|
+
@diagram = diagram
|
|
34
|
+
@node_layers = {}
|
|
35
|
+
@node_positions = {}
|
|
36
|
+
|
|
37
|
+
# Assign nodes to layers (left to right)
|
|
38
|
+
assign_layers
|
|
39
|
+
|
|
40
|
+
# Calculate vertical positions within layers
|
|
41
|
+
calculate_positions
|
|
42
|
+
|
|
43
|
+
# Build transformation result
|
|
44
|
+
{
|
|
45
|
+
id: "sankey",
|
|
46
|
+
title: diagram.title,
|
|
47
|
+
acc_title: diagram.acc_title,
|
|
48
|
+
acc_description: diagram.acc_description,
|
|
49
|
+
nodes: transform_nodes,
|
|
50
|
+
flows: transform_flows,
|
|
51
|
+
metadata: {
|
|
52
|
+
node_count: diagram.nodes.length,
|
|
53
|
+
flow_count: diagram.flows.length,
|
|
54
|
+
total_flow: diagram.total_flow,
|
|
55
|
+
max_flow: diagram.max_flow,
|
|
56
|
+
layer_count: @node_layers.values.max.to_i + 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def assign_layers
|
|
64
|
+
# Use topological sorting to assign layers
|
|
65
|
+
# Source nodes (no inflow) start at layer 0
|
|
66
|
+
# Each subsequent layer is one step from previous
|
|
67
|
+
|
|
68
|
+
visited = Set.new
|
|
69
|
+
node_ids = @diagram.all_node_ids
|
|
70
|
+
|
|
71
|
+
# Start with source nodes at layer 0
|
|
72
|
+
source_nodes = @diagram.source_nodes
|
|
73
|
+
source_nodes.each do |node_id|
|
|
74
|
+
@node_layers[node_id] = 0
|
|
75
|
+
visited.add(node_id)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# BFS to assign layers
|
|
79
|
+
queue = source_nodes.dup
|
|
80
|
+
while !queue.empty?
|
|
81
|
+
current_id = queue.shift
|
|
82
|
+
current_layer = @node_layers[current_id]
|
|
83
|
+
|
|
84
|
+
# Process all flows from this node
|
|
85
|
+
@diagram.flows_from(current_id).each do |flow|
|
|
86
|
+
target_id = flow.target
|
|
87
|
+
next if visited.include?(target_id)
|
|
88
|
+
|
|
89
|
+
# Assign target to next layer
|
|
90
|
+
target_layer = current_layer + 1
|
|
91
|
+
@node_layers[target_id] = [
|
|
92
|
+
@node_layers[target_id] || 0,
|
|
93
|
+
target_layer
|
|
94
|
+
].max
|
|
95
|
+
|
|
96
|
+
unless queue.include?(target_id)
|
|
97
|
+
queue << target_id
|
|
98
|
+
visited.add(target_id)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Handle any nodes not reachable from sources (cycles, isolated nodes)
|
|
104
|
+
node_ids.each do |node_id|
|
|
105
|
+
unless @node_layers.key?(node_id)
|
|
106
|
+
@node_layers[node_id] = 0
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def calculate_positions
|
|
112
|
+
# Group nodes by layer
|
|
113
|
+
layers = Hash.new { |h, k| h[k] = [] }
|
|
114
|
+
@node_layers.each do |node_id, layer|
|
|
115
|
+
layers[layer] << node_id
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Calculate vertical positions for each layer
|
|
119
|
+
layers.each do |layer_num, node_ids|
|
|
120
|
+
# Sort nodes by total flow magnitude for better layout
|
|
121
|
+
sorted_nodes = node_ids.sort_by do |id|
|
|
122
|
+
-(@diagram.total_inflow(id) + @diagram.total_outflow(id))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Position nodes vertically with spacing
|
|
126
|
+
y_offset = 0
|
|
127
|
+
sorted_nodes.each do |node_id|
|
|
128
|
+
x = layer_num * LAYER_SPACING
|
|
129
|
+
y = y_offset
|
|
130
|
+
|
|
131
|
+
@node_positions[node_id] = {
|
|
132
|
+
x: x,
|
|
133
|
+
y: y,
|
|
134
|
+
width: calculate_node_width(node_id),
|
|
135
|
+
height: NODE_HEIGHT
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
y_offset += NODE_HEIGHT + NODE_SPACING
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def calculate_node_width(node_id)
|
|
144
|
+
# Node width proportional to flow through it
|
|
145
|
+
total_flow = @diagram.total_inflow(node_id) +
|
|
146
|
+
@diagram.total_outflow(node_id)
|
|
147
|
+
|
|
148
|
+
if total_flow > 0 && @diagram.max_flow > 0
|
|
149
|
+
ratio = total_flow / (@diagram.max_flow * 2)
|
|
150
|
+
width = MIN_NODE_WIDTH + (ratio * (MAX_NODE_WIDTH - MIN_NODE_WIDTH))
|
|
151
|
+
width.round
|
|
152
|
+
else
|
|
153
|
+
MIN_NODE_WIDTH
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def transform_nodes
|
|
158
|
+
@diagram.nodes.map do |node|
|
|
159
|
+
position = @node_positions[node.id] || { x: 0, y: 0, width: MIN_NODE_WIDTH, height: NODE_HEIGHT }
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
id: node.id,
|
|
163
|
+
label: node.display_label,
|
|
164
|
+
layer: @node_layers[node.id] || 0,
|
|
165
|
+
x: position[:x],
|
|
166
|
+
y: position[:y],
|
|
167
|
+
width: position[:width],
|
|
168
|
+
height: position[:height],
|
|
169
|
+
inflow: @diagram.total_inflow(node.id),
|
|
170
|
+
outflow: @diagram.total_outflow(node.id)
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def transform_flows
|
|
176
|
+
@diagram.flows.map.with_index do |flow, index|
|
|
177
|
+
source_pos = @node_positions[flow.source]
|
|
178
|
+
target_pos = @node_positions[flow.target]
|
|
179
|
+
|
|
180
|
+
# Calculate flow width proportional to value
|
|
181
|
+
flow_width = calculate_flow_width(flow.value)
|
|
182
|
+
|
|
183
|
+
{
|
|
184
|
+
id: "flow_#{index}",
|
|
185
|
+
source: flow.source,
|
|
186
|
+
target: flow.target,
|
|
187
|
+
value: flow.value,
|
|
188
|
+
label: flow.label,
|
|
189
|
+
width: flow_width,
|
|
190
|
+
source_x: source_pos ? source_pos[:x] + source_pos[:width] : 0,
|
|
191
|
+
source_y: source_pos ? source_pos[:y] + (source_pos[:height] / 2.0) : 0,
|
|
192
|
+
target_x: target_pos ? target_pos[:x] : LAYER_SPACING,
|
|
193
|
+
target_y: target_pos ? target_pos[:y] + (target_pos[:height] / 2.0) : 0,
|
|
194
|
+
self_loop: flow.self_loop?
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def calculate_flow_width(value)
|
|
200
|
+
# Flow width proportional to value
|
|
201
|
+
return 1 if @diagram.max_flow.zero?
|
|
202
|
+
|
|
203
|
+
ratio = value / @diagram.max_flow
|
|
204
|
+
min_width = 2
|
|
205
|
+
max_width = 50
|
|
206
|
+
|
|
207
|
+
width = min_width + (ratio * (max_width - min_width))
|
|
208
|
+
[width.round, min_width].max
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/sequence'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Sequence transformer for converting sequence models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed sequence diagram model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles participant
|
|
12
|
+
# positioning, message routing, and lifeline calculation.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a sequence diagram
|
|
15
|
+
# transform = SequenceTransform.new
|
|
16
|
+
# graph = transform.to_graph(sequence_diagram)
|
|
17
|
+
class SequenceTransform < Base
|
|
18
|
+
# Default font size for text measurement
|
|
19
|
+
DEFAULT_FONT_SIZE = 14
|
|
20
|
+
|
|
21
|
+
# Minimum spacing between participants
|
|
22
|
+
PARTICIPANT_SPACING = 150
|
|
23
|
+
|
|
24
|
+
# Width of participant box
|
|
25
|
+
PARTICIPANT_WIDTH = 120
|
|
26
|
+
|
|
27
|
+
# Height of participant box
|
|
28
|
+
PARTICIPANT_HEIGHT = 40
|
|
29
|
+
|
|
30
|
+
# Vertical spacing between messages
|
|
31
|
+
MESSAGE_SPACING = 60
|
|
32
|
+
|
|
33
|
+
# Converts a sequence diagram to a graph structure.
|
|
34
|
+
#
|
|
35
|
+
# @param diagram [Diagram::Sequence] the sequence diagram to transform
|
|
36
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
37
|
+
# @raise [TransformError] if diagram is invalid
|
|
38
|
+
def to_graph(diagram)
|
|
39
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
id: diagram.id || 'sequence',
|
|
43
|
+
children: transform_participants(diagram),
|
|
44
|
+
edges: transform_messages(diagram),
|
|
45
|
+
layoutOptions: layout_options(diagram),
|
|
46
|
+
metadata: {
|
|
47
|
+
participants: diagram.participants.map(&:id),
|
|
48
|
+
message_count: diagram.messages.length,
|
|
49
|
+
notes: diagram.notes
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def transform_participants(diagram)
|
|
57
|
+
diagram.participants.map.with_index do |participant, index|
|
|
58
|
+
{
|
|
59
|
+
id: participant.id,
|
|
60
|
+
width: PARTICIPANT_WIDTH,
|
|
61
|
+
height: PARTICIPANT_HEIGHT,
|
|
62
|
+
labels: [
|
|
63
|
+
{
|
|
64
|
+
text: participant.label,
|
|
65
|
+
width: measure_text(participant.label,
|
|
66
|
+
font_size: DEFAULT_FONT_SIZE)[:width],
|
|
67
|
+
height: measure_text(participant.label,
|
|
68
|
+
font_size: DEFAULT_FONT_SIZE)[:height]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
metadata: {
|
|
72
|
+
actor_type: participant.actor_type,
|
|
73
|
+
index: index,
|
|
74
|
+
lifeline_length: calculate_lifeline_length(diagram)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def transform_messages(diagram)
|
|
81
|
+
return [] if diagram.messages.nil? || diagram.messages.empty?
|
|
82
|
+
|
|
83
|
+
diagram.messages.map.with_index do |message, index|
|
|
84
|
+
{
|
|
85
|
+
id: "msg_#{index}",
|
|
86
|
+
sources: [message.from_id],
|
|
87
|
+
targets: [message.to_id],
|
|
88
|
+
labels: message_labels(message),
|
|
89
|
+
metadata: {
|
|
90
|
+
arrow_type: message.arrow_type,
|
|
91
|
+
message_index: index,
|
|
92
|
+
message_text: message.message_text
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def message_labels(message)
|
|
99
|
+
return [] if message.message_text.nil? || message.message_text.empty?
|
|
100
|
+
|
|
101
|
+
label_dims = measure_text(
|
|
102
|
+
message.message_text,
|
|
103
|
+
font_size: DEFAULT_FONT_SIZE
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
[
|
|
107
|
+
{
|
|
108
|
+
text: message.message_text,
|
|
109
|
+
width: label_dims[:width],
|
|
110
|
+
height: label_dims[:height]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def calculate_lifeline_length(diagram)
|
|
116
|
+
# Calculate total height needed for all messages
|
|
117
|
+
message_count = diagram.messages.length
|
|
118
|
+
base_height = message_count * MESSAGE_SPACING
|
|
119
|
+
|
|
120
|
+
# Add extra space for notes if present
|
|
121
|
+
note_count = diagram.notes&.length || 0
|
|
122
|
+
base_height + (note_count * 30)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def layout_options(_diagram)
|
|
126
|
+
# Sequence diagrams use layered algorithm for vertical timeline
|
|
127
|
+
# DIRECTION_DOWN ensures participants are arranged horizontally at top
|
|
128
|
+
# with messages flowing downward in chronological order
|
|
129
|
+
build_elk_options(
|
|
130
|
+
algorithm: ALGORITHM_LAYERED,
|
|
131
|
+
direction: DIRECTION_DOWN,
|
|
132
|
+
ElkOptions::NODE_NODE_SPACING => PARTICIPANT_SPACING,
|
|
133
|
+
ElkOptions::LAYER_SPACING => MESSAGE_SPACING,
|
|
134
|
+
ElkOptions::EDGE_NODE_SPACING => 20,
|
|
135
|
+
ElkOptions::EDGE_EDGE_SPACING => 15,
|
|
136
|
+
# SIMPLE node placement maintains participant order
|
|
137
|
+
ElkOptions::NODE_PLACEMENT => 'SIMPLE',
|
|
138
|
+
ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES'
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/state_diagram'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# State diagram transformer for converting state diagram models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed state diagram model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles state node dimension
|
|
12
|
+
# calculation, transition mapping, and layout configuration.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a state diagram
|
|
15
|
+
# transform = StateDiagramTransform.new
|
|
16
|
+
# graph = transform.to_graph(state_diagram)
|
|
17
|
+
class StateDiagramTransform < Base
|
|
18
|
+
# Default font size for text measurement
|
|
19
|
+
DEFAULT_FONT_SIZE = 14
|
|
20
|
+
|
|
21
|
+
# Converts a state diagram to a graph structure.
|
|
22
|
+
#
|
|
23
|
+
# @param diagram [Diagram::StateDiagram] the state diagram to transform
|
|
24
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
25
|
+
# @raise [TransformError] if diagram is invalid
|
|
26
|
+
def to_graph(diagram)
|
|
27
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
id: diagram.id || 'state_diagram',
|
|
31
|
+
children: transform_states(diagram),
|
|
32
|
+
edges: transform_transitions(diagram),
|
|
33
|
+
layoutOptions: layout_options(diagram)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def transform_states(diagram)
|
|
40
|
+
diagram.states.map do |state|
|
|
41
|
+
dims = calculate_state_dimensions(state)
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
id: state.id,
|
|
45
|
+
width: dims[:width],
|
|
46
|
+
height: dims[:height],
|
|
47
|
+
labels: state_labels(state, dims),
|
|
48
|
+
metadata: {
|
|
49
|
+
state_type: state.state_type,
|
|
50
|
+
description: state.description
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def transform_transitions(diagram)
|
|
57
|
+
return [] if diagram.transitions.nil? || diagram.transitions.empty?
|
|
58
|
+
|
|
59
|
+
diagram.transitions.map do |transition|
|
|
60
|
+
{
|
|
61
|
+
id: "#{transition.from_id}_to_#{transition.to_id}",
|
|
62
|
+
sources: [transition.from_id],
|
|
63
|
+
targets: [transition.to_id],
|
|
64
|
+
labels: transition_labels(transition),
|
|
65
|
+
metadata: {
|
|
66
|
+
trigger: transition.trigger,
|
|
67
|
+
guard_condition: transition.guard_condition
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def state_labels(state, dims)
|
|
74
|
+
labels = []
|
|
75
|
+
|
|
76
|
+
# Add state label
|
|
77
|
+
if state.label && !state.label.empty?
|
|
78
|
+
labels << {
|
|
79
|
+
text: state.label,
|
|
80
|
+
width: dims[:label_width],
|
|
81
|
+
height: dims[:label_height]
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Add description if present
|
|
86
|
+
if state.description && !state.description.empty?
|
|
87
|
+
desc_dims = measure_text(
|
|
88
|
+
state.description,
|
|
89
|
+
font_size: DEFAULT_FONT_SIZE - 2
|
|
90
|
+
)
|
|
91
|
+
labels << {
|
|
92
|
+
text: state.description,
|
|
93
|
+
width: desc_dims[:width],
|
|
94
|
+
height: desc_dims[:height]
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
labels
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def transition_labels(transition)
|
|
102
|
+
label = transition.label
|
|
103
|
+
return [] if label.nil? || label.empty?
|
|
104
|
+
|
|
105
|
+
label_dims = measure_text(label, font_size: DEFAULT_FONT_SIZE)
|
|
106
|
+
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
text: label,
|
|
110
|
+
width: label_dims[:width],
|
|
111
|
+
height: label_dims[:height]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def calculate_state_dimensions(state)
|
|
117
|
+
label_text = state.label || state.id
|
|
118
|
+
label_dims = measure_text(
|
|
119
|
+
label_text,
|
|
120
|
+
font_size: DEFAULT_FONT_SIZE
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Adjust dimensions based on state type
|
|
124
|
+
state_dims = case state.state_type
|
|
125
|
+
when 'start', 'end'
|
|
126
|
+
calculate_terminal_dimensions
|
|
127
|
+
when 'choice'
|
|
128
|
+
calculate_choice_dimensions(label_dims)
|
|
129
|
+
when 'fork', 'join'
|
|
130
|
+
calculate_fork_join_dimensions
|
|
131
|
+
else
|
|
132
|
+
calculate_normal_state_dimensions(
|
|
133
|
+
label_dims,
|
|
134
|
+
state.description
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
width: state_dims[:width],
|
|
140
|
+
height: state_dims[:height],
|
|
141
|
+
label_width: label_dims[:width],
|
|
142
|
+
label_height: label_dims[:height]
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def calculate_terminal_dimensions
|
|
147
|
+
# Start and end states are small circles
|
|
148
|
+
{
|
|
149
|
+
width: 30,
|
|
150
|
+
height: 30
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def calculate_choice_dimensions(label_dims)
|
|
155
|
+
# Choice states are diamonds
|
|
156
|
+
size = [label_dims[:width], label_dims[:height]].max + 40
|
|
157
|
+
{
|
|
158
|
+
width: size,
|
|
159
|
+
height: size
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def calculate_fork_join_dimensions
|
|
164
|
+
# Fork and join are represented as thick bars
|
|
165
|
+
{
|
|
166
|
+
width: 100,
|
|
167
|
+
height: 10
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def calculate_normal_state_dimensions(label_dims, description)
|
|
172
|
+
# Normal states are rounded rectangles
|
|
173
|
+
width = label_dims[:width] + 40
|
|
174
|
+
height = label_dims[:height] + 30
|
|
175
|
+
|
|
176
|
+
# Add extra height if there's a description
|
|
177
|
+
if description && !description.empty?
|
|
178
|
+
desc_dims = measure_text(
|
|
179
|
+
description,
|
|
180
|
+
font_size: DEFAULT_FONT_SIZE - 2
|
|
181
|
+
)
|
|
182
|
+
height += desc_dims[:height] + 10
|
|
183
|
+
width = [width, desc_dims[:width] + 40].max
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Minimum dimensions
|
|
187
|
+
width = [width, 100].max
|
|
188
|
+
height = [height, 50].max
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
width: width,
|
|
192
|
+
height: height
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def layout_options(diagram)
|
|
197
|
+
# State diagrams use layered algorithm for state machine flow
|
|
198
|
+
# This ensures proper hierarchical layout of states with clear
|
|
199
|
+
# transition paths from start to end states
|
|
200
|
+
build_elk_options(
|
|
201
|
+
algorithm: ALGORITHM_LAYERED,
|
|
202
|
+
direction: direction_to_layout(diagram.direction),
|
|
203
|
+
ElkOptions::NODE_NODE_SPACING => 60,
|
|
204
|
+
ElkOptions::LAYER_SPACING => 60,
|
|
205
|
+
ElkOptions::EDGE_NODE_SPACING => 40,
|
|
206
|
+
ElkOptions::EDGE_EDGE_SPACING => 30,
|
|
207
|
+
# SIMPLE node placement for predictable state flow
|
|
208
|
+
ElkOptions::NODE_PLACEMENT => 'SIMPLE'
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def direction_to_layout(direction)
|
|
213
|
+
case direction
|
|
214
|
+
when 'TD', 'TB'
|
|
215
|
+
DIRECTION_DOWN
|
|
216
|
+
when 'LR'
|
|
217
|
+
DIRECTION_RIGHT
|
|
218
|
+
when 'RL'
|
|
219
|
+
DIRECTION_LEFT
|
|
220
|
+
when 'BT'
|
|
221
|
+
DIRECTION_UP
|
|
222
|
+
else
|
|
223
|
+
DIRECTION_DOWN # Default direction
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|