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,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/quadrant'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Quadrant chart transformer for converting quadrant models to
|
|
9
|
+
# renderable structure.
|
|
10
|
+
#
|
|
11
|
+
# Like pie charts, quadrant charts have a fixed layout structure
|
|
12
|
+
# (2x2 grid). This transformer validates the diagram and prepares
|
|
13
|
+
# the data with computed SVG coordinates for rendering.
|
|
14
|
+
#
|
|
15
|
+
# @example Transform a quadrant chart
|
|
16
|
+
# transform = QuadrantTransform.new
|
|
17
|
+
# data = transform.to_graph(quadrant_diagram)
|
|
18
|
+
class QuadrantTransform < Base
|
|
19
|
+
# Default dimensions for quadrant chart
|
|
20
|
+
DEFAULT_WIDTH = 800
|
|
21
|
+
DEFAULT_HEIGHT = 600
|
|
22
|
+
DEFAULT_MARGIN = 80
|
|
23
|
+
|
|
24
|
+
# Converts a quadrant diagram to a renderable data structure.
|
|
25
|
+
#
|
|
26
|
+
# @param diagram [Diagram::QuadrantChart] the quadrant diagram
|
|
27
|
+
# @return [Hash] data structure for rendering with SVG coordinates
|
|
28
|
+
# @raise [TransformError] if diagram is invalid
|
|
29
|
+
def to_graph(diagram)
|
|
30
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
31
|
+
|
|
32
|
+
# Calculate dimensions
|
|
33
|
+
width = DEFAULT_WIDTH
|
|
34
|
+
height = DEFAULT_HEIGHT
|
|
35
|
+
margin = DEFAULT_MARGIN
|
|
36
|
+
|
|
37
|
+
# Chart area (excluding margins)
|
|
38
|
+
chart_width = width - (margin * 2)
|
|
39
|
+
chart_height = height - (margin * 2)
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
id: diagram.id || 'quadrant',
|
|
43
|
+
title: diagram.title,
|
|
44
|
+
dimensions: {
|
|
45
|
+
width: width,
|
|
46
|
+
height: height,
|
|
47
|
+
margin: margin,
|
|
48
|
+
chart_width: chart_width,
|
|
49
|
+
chart_height: chart_height,
|
|
50
|
+
chart_x: margin,
|
|
51
|
+
chart_y: margin
|
|
52
|
+
},
|
|
53
|
+
axes: {
|
|
54
|
+
x_left: diagram.x_axis_left || '',
|
|
55
|
+
x_right: diagram.x_axis_right || '',
|
|
56
|
+
y_bottom: diagram.y_axis_bottom || '',
|
|
57
|
+
y_top: diagram.y_axis_top || ''
|
|
58
|
+
},
|
|
59
|
+
quadrants: {
|
|
60
|
+
q1: {
|
|
61
|
+
label: diagram.quadrant_1_label,
|
|
62
|
+
number: 1,
|
|
63
|
+
bounds: calculate_quadrant_bounds(1, margin, chart_width,
|
|
64
|
+
chart_height)
|
|
65
|
+
},
|
|
66
|
+
q2: {
|
|
67
|
+
label: diagram.quadrant_2_label,
|
|
68
|
+
number: 2,
|
|
69
|
+
bounds: calculate_quadrant_bounds(2, margin, chart_width,
|
|
70
|
+
chart_height)
|
|
71
|
+
},
|
|
72
|
+
q3: {
|
|
73
|
+
label: diagram.quadrant_3_label,
|
|
74
|
+
number: 3,
|
|
75
|
+
bounds: calculate_quadrant_bounds(3, margin, chart_width,
|
|
76
|
+
chart_height)
|
|
77
|
+
},
|
|
78
|
+
q4: {
|
|
79
|
+
label: diagram.quadrant_4_label,
|
|
80
|
+
number: 4,
|
|
81
|
+
bounds: calculate_quadrant_bounds(4, margin, chart_width,
|
|
82
|
+
chart_height)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
points: transform_points(diagram, margin, chart_width, chart_height)
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Calculate bounds for a specific quadrant.
|
|
92
|
+
#
|
|
93
|
+
# @param quadrant [Integer] quadrant number (1-4)
|
|
94
|
+
# @param margin [Float] chart margin
|
|
95
|
+
# @param chart_width [Float] width of chart area
|
|
96
|
+
# @param chart_height [Float] height of chart area
|
|
97
|
+
# @return [Hash] bounds with x, y, width, height
|
|
98
|
+
def calculate_quadrant_bounds(quadrant, margin, chart_width,
|
|
99
|
+
chart_height)
|
|
100
|
+
half_width = chart_width / 2.0
|
|
101
|
+
half_height = chart_height / 2.0
|
|
102
|
+
|
|
103
|
+
case quadrant
|
|
104
|
+
when 1 # Top-right
|
|
105
|
+
{
|
|
106
|
+
x: margin + half_width,
|
|
107
|
+
y: margin,
|
|
108
|
+
width: half_width,
|
|
109
|
+
height: half_height
|
|
110
|
+
}
|
|
111
|
+
when 2 # Top-left
|
|
112
|
+
{
|
|
113
|
+
x: margin,
|
|
114
|
+
y: margin,
|
|
115
|
+
width: half_width,
|
|
116
|
+
height: half_height
|
|
117
|
+
}
|
|
118
|
+
when 3 # Bottom-left
|
|
119
|
+
{
|
|
120
|
+
x: margin,
|
|
121
|
+
y: margin + half_height,
|
|
122
|
+
width: half_width,
|
|
123
|
+
height: half_height
|
|
124
|
+
}
|
|
125
|
+
when 4 # Bottom-right
|
|
126
|
+
{
|
|
127
|
+
x: margin + half_width,
|
|
128
|
+
y: margin + half_height,
|
|
129
|
+
width: half_width,
|
|
130
|
+
height: half_height
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Transform points with calculated SVG coordinates.
|
|
136
|
+
#
|
|
137
|
+
# @param diagram [Diagram::QuadrantChart] the diagram
|
|
138
|
+
# @param margin [Float] chart margin
|
|
139
|
+
# @param chart_width [Float] width of chart area
|
|
140
|
+
# @param chart_height [Float] height of chart area
|
|
141
|
+
# @return [Array<Hash>] points with SVG coordinates
|
|
142
|
+
def transform_points(diagram, margin, chart_width, chart_height)
|
|
143
|
+
diagram.points.map.with_index do |point, index|
|
|
144
|
+
# Convert normalized coordinates (0-1) to SVG coordinates
|
|
145
|
+
# Note: Y-axis is inverted in SVG (0 at top)
|
|
146
|
+
svg_x = margin + (point.x * chart_width)
|
|
147
|
+
svg_y = margin + ((1.0 - point.y) * chart_height)
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
id: "point_#{index}",
|
|
151
|
+
label: point.label,
|
|
152
|
+
x: point.x,
|
|
153
|
+
y: point.y,
|
|
154
|
+
svg_x: svg_x,
|
|
155
|
+
svg_y: svg_y,
|
|
156
|
+
quadrant: point.quadrant,
|
|
157
|
+
radius: point.radius || 6,
|
|
158
|
+
color: point.color,
|
|
159
|
+
stroke_color: point.stroke_color,
|
|
160
|
+
stroke_width: point.stroke_width || 2,
|
|
161
|
+
index: index
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sirena
|
|
4
|
+
module Transform
|
|
5
|
+
# Transforms a RadarChart diagram into a positioned layout structure.
|
|
6
|
+
#
|
|
7
|
+
# The layout algorithm handles:
|
|
8
|
+
# - Radial axis positioning (360° / num_axes)
|
|
9
|
+
# - Data point plotting on each axis
|
|
10
|
+
# - Value normalization and scaling
|
|
11
|
+
# - Polar to cartesian coordinate conversion
|
|
12
|
+
# - Polygon formation for each dataset
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a radar chart
|
|
15
|
+
# transform = Transform::Radar.new
|
|
16
|
+
# layout = transform.to_graph(diagram)
|
|
17
|
+
class Radar
|
|
18
|
+
# Default radius of the chart
|
|
19
|
+
DEFAULT_RADIUS = 200
|
|
20
|
+
|
|
21
|
+
# Padding around the chart
|
|
22
|
+
PADDING = 80
|
|
23
|
+
|
|
24
|
+
# Label distance from the chart center
|
|
25
|
+
LABEL_OFFSET = 30
|
|
26
|
+
|
|
27
|
+
# Number of grid circles to draw
|
|
28
|
+
GRID_CIRCLES = 5
|
|
29
|
+
|
|
30
|
+
# Transforms the diagram into a layout structure.
|
|
31
|
+
#
|
|
32
|
+
# @param diagram [Diagram::RadarChart] the radar chart diagram
|
|
33
|
+
# @return [Hash] layout data with axes, curves, and dimensions
|
|
34
|
+
def to_graph(diagram)
|
|
35
|
+
num_axes = diagram.axes.length
|
|
36
|
+
return empty_layout if num_axes == 0
|
|
37
|
+
|
|
38
|
+
# Calculate value range
|
|
39
|
+
min_value, max_value = calculate_value_range(diagram)
|
|
40
|
+
|
|
41
|
+
# Position axes radially
|
|
42
|
+
positioned_axes = position_axes(diagram.axes, num_axes)
|
|
43
|
+
|
|
44
|
+
# Position data points for each curve
|
|
45
|
+
positioned_curves = position_curves(
|
|
46
|
+
diagram.curves,
|
|
47
|
+
positioned_axes,
|
|
48
|
+
min_value,
|
|
49
|
+
max_value
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Calculate grid circles for reference
|
|
53
|
+
grid_circles = calculate_grid_circles(min_value, max_value)
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
axes: positioned_axes,
|
|
57
|
+
curves: positioned_curves,
|
|
58
|
+
grid_circles: grid_circles,
|
|
59
|
+
center_x: DEFAULT_RADIUS + PADDING,
|
|
60
|
+
center_y: DEFAULT_RADIUS + PADDING,
|
|
61
|
+
radius: DEFAULT_RADIUS,
|
|
62
|
+
width: (DEFAULT_RADIUS + PADDING) * 2,
|
|
63
|
+
height: (DEFAULT_RADIUS + PADDING) * 2,
|
|
64
|
+
min_value: min_value,
|
|
65
|
+
max_value: max_value
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Returns an empty layout structure.
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash] empty layout
|
|
74
|
+
def empty_layout
|
|
75
|
+
{
|
|
76
|
+
axes: [],
|
|
77
|
+
curves: [],
|
|
78
|
+
grid_circles: [],
|
|
79
|
+
center_x: PADDING,
|
|
80
|
+
center_y: PADDING,
|
|
81
|
+
radius: DEFAULT_RADIUS,
|
|
82
|
+
width: PADDING * 2,
|
|
83
|
+
height: PADDING * 2,
|
|
84
|
+
min_value: 0,
|
|
85
|
+
max_value: 0
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Calculates the value range from all curves.
|
|
90
|
+
#
|
|
91
|
+
# @param diagram [Diagram::RadarChart] diagram
|
|
92
|
+
# @return [Array<Numeric, Numeric>] min and max values
|
|
93
|
+
def calculate_value_range(diagram)
|
|
94
|
+
all_values = diagram.curves.flat_map { |c| c.values.values }
|
|
95
|
+
|
|
96
|
+
# Use configured min/max if available
|
|
97
|
+
min_value = diagram.options[:min] || all_values.min || 0
|
|
98
|
+
max_value = diagram.options[:max] || all_values.max || 100
|
|
99
|
+
|
|
100
|
+
# Ensure max > min
|
|
101
|
+
max_value = min_value + 1 if max_value <= min_value
|
|
102
|
+
|
|
103
|
+
[min_value, max_value]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Positions axes radially around the center.
|
|
107
|
+
#
|
|
108
|
+
# @param axes [Array<Diagram::RadarAxis>] axes
|
|
109
|
+
# @param num_axes [Integer] number of axes
|
|
110
|
+
# @return [Array<Hash>] positioned axes
|
|
111
|
+
def position_axes(axes, num_axes)
|
|
112
|
+
positioned = []
|
|
113
|
+
angle_step = 360.0 / num_axes
|
|
114
|
+
|
|
115
|
+
axes.each_with_index do |axis, idx|
|
|
116
|
+
# Calculate angle (start at top, go clockwise)
|
|
117
|
+
angle_degrees = idx * angle_step - 90 # -90 to start at top
|
|
118
|
+
angle_radians = angle_degrees * Math::PI / 180.0
|
|
119
|
+
|
|
120
|
+
# Calculate end point of axis line
|
|
121
|
+
end_x = Math.cos(angle_radians) * DEFAULT_RADIUS
|
|
122
|
+
end_y = Math.sin(angle_radians) * DEFAULT_RADIUS
|
|
123
|
+
|
|
124
|
+
# Calculate label position (beyond the end point)
|
|
125
|
+
label_radius = DEFAULT_RADIUS + LABEL_OFFSET
|
|
126
|
+
label_x = Math.cos(angle_radians) * label_radius
|
|
127
|
+
label_y = Math.sin(angle_radians) * label_radius
|
|
128
|
+
|
|
129
|
+
positioned << {
|
|
130
|
+
id: axis.id,
|
|
131
|
+
label: axis.label,
|
|
132
|
+
angle_degrees: angle_degrees,
|
|
133
|
+
angle_radians: angle_radians,
|
|
134
|
+
end_x: end_x,
|
|
135
|
+
end_y: end_y,
|
|
136
|
+
label_x: label_x,
|
|
137
|
+
label_y: label_y,
|
|
138
|
+
index: idx
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
positioned
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Positions data points for all curves.
|
|
146
|
+
#
|
|
147
|
+
# @param curves [Array<Diagram::RadarCurve>] curves
|
|
148
|
+
# @param positioned_axes [Array<Hash>] positioned axes
|
|
149
|
+
# @param min_value [Numeric] minimum value
|
|
150
|
+
# @param max_value [Numeric] maximum value
|
|
151
|
+
# @return [Array<Hash>] positioned curves with points
|
|
152
|
+
def position_curves(curves, positioned_axes, min_value, max_value)
|
|
153
|
+
positioned = []
|
|
154
|
+
|
|
155
|
+
curves.each do |curve|
|
|
156
|
+
points = []
|
|
157
|
+
|
|
158
|
+
positioned_axes.each do |axis|
|
|
159
|
+
value = curve.value_for(axis[:id])
|
|
160
|
+
|
|
161
|
+
# Normalize value to 0-1 range
|
|
162
|
+
normalized = normalize_value(value, min_value, max_value)
|
|
163
|
+
|
|
164
|
+
# Calculate radius for this value
|
|
165
|
+
radius = normalized * DEFAULT_RADIUS
|
|
166
|
+
|
|
167
|
+
# Convert to cartesian coordinates
|
|
168
|
+
x = Math.cos(axis[:angle_radians]) * radius
|
|
169
|
+
y = Math.sin(axis[:angle_radians]) * radius
|
|
170
|
+
|
|
171
|
+
points << {
|
|
172
|
+
axis_id: axis[:id],
|
|
173
|
+
value: value,
|
|
174
|
+
normalized: normalized,
|
|
175
|
+
x: x,
|
|
176
|
+
y: y,
|
|
177
|
+
angle: axis[:angle_radians]
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
positioned << {
|
|
182
|
+
id: curve.id,
|
|
183
|
+
label: curve.label,
|
|
184
|
+
points: points
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
positioned
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Normalizes a value to the 0-1 range.
|
|
192
|
+
#
|
|
193
|
+
# @param value [Numeric] value to normalize
|
|
194
|
+
# @param min_value [Numeric] minimum value
|
|
195
|
+
# @param max_value [Numeric] maximum value
|
|
196
|
+
# @return [Numeric] normalized value
|
|
197
|
+
def normalize_value(value, min_value, max_value)
|
|
198
|
+
return 0 if max_value == min_value
|
|
199
|
+
|
|
200
|
+
((value - min_value).to_f / (max_value - min_value)).clamp(0, 1)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Calculates grid circle positions.
|
|
204
|
+
#
|
|
205
|
+
# @param min_value [Numeric] minimum value
|
|
206
|
+
# @param max_value [Numeric] maximum value
|
|
207
|
+
# @return [Array<Hash>] grid circles with radius and label
|
|
208
|
+
def calculate_grid_circles(min_value, max_value)
|
|
209
|
+
circles = []
|
|
210
|
+
|
|
211
|
+
GRID_CIRCLES.times do |i|
|
|
212
|
+
fraction = (i + 1).to_f / GRID_CIRCLES
|
|
213
|
+
radius = DEFAULT_RADIUS * fraction
|
|
214
|
+
value = min_value + (max_value - min_value) * fraction
|
|
215
|
+
|
|
216
|
+
circles << {
|
|
217
|
+
radius: radius,
|
|
218
|
+
value: value,
|
|
219
|
+
fraction: fraction
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
circles
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/requirement'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Requirement diagram transformer for converting requirement models to positioned layouts.
|
|
9
|
+
#
|
|
10
|
+
# Converts a requirement diagram model into a positioned layout structure.
|
|
11
|
+
# Handles requirement and element positioning, relationship routing,
|
|
12
|
+
# and hierarchical layout based on dependencies.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a requirement diagram
|
|
15
|
+
# transform = RequirementTransform.new
|
|
16
|
+
# layout = transform.to_layout(requirement_diagram)
|
|
17
|
+
class RequirementTransform < Base
|
|
18
|
+
# Default dimensions
|
|
19
|
+
DEFAULT_REQ_WIDTH = 180
|
|
20
|
+
DEFAULT_REQ_HEIGHT = 140
|
|
21
|
+
DEFAULT_ELEM_WIDTH = 150
|
|
22
|
+
DEFAULT_ELEM_HEIGHT = 80
|
|
23
|
+
DEFAULT_SPACING_X = 100
|
|
24
|
+
DEFAULT_SPACING_Y = 80
|
|
25
|
+
DEFAULT_PADDING = 20
|
|
26
|
+
|
|
27
|
+
# Converts a requirement diagram to a positioned layout structure.
|
|
28
|
+
#
|
|
29
|
+
# @param diagram [Diagram::RequirementDiagram] the requirement diagram to transform
|
|
30
|
+
# @return [Hash] positioned layout hash
|
|
31
|
+
# @raise [TransformError] if diagram is invalid
|
|
32
|
+
def to_graph(diagram)
|
|
33
|
+
raise TransformError, 'Diagram cannot be nil' if diagram.nil?
|
|
34
|
+
|
|
35
|
+
# Calculate positions for requirements and elements
|
|
36
|
+
nodes_layout = calculate_node_positions(diagram)
|
|
37
|
+
|
|
38
|
+
# Calculate relationship routes
|
|
39
|
+
relationships_layout = calculate_relationships(diagram, nodes_layout)
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
requirements: nodes_layout[:requirements],
|
|
43
|
+
elements: nodes_layout[:elements],
|
|
44
|
+
relationships: relationships_layout,
|
|
45
|
+
width: nodes_layout[:width],
|
|
46
|
+
height: nodes_layout[:height]
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def calculate_node_positions(diagram)
|
|
53
|
+
requirements = diagram.requirements
|
|
54
|
+
elements = diagram.elements
|
|
55
|
+
relationships = diagram.relationships
|
|
56
|
+
|
|
57
|
+
# Build dependency graph to determine layout
|
|
58
|
+
levels = build_dependency_levels(requirements, elements, relationships)
|
|
59
|
+
|
|
60
|
+
positioned_requirements = {}
|
|
61
|
+
positioned_elements = {}
|
|
62
|
+
|
|
63
|
+
current_y = DEFAULT_PADDING
|
|
64
|
+
max_width = 0
|
|
65
|
+
|
|
66
|
+
levels.each_with_index do |level_nodes, level_idx|
|
|
67
|
+
current_x = DEFAULT_PADDING
|
|
68
|
+
max_height = 0
|
|
69
|
+
|
|
70
|
+
level_nodes.each do |node|
|
|
71
|
+
if node[:type] == :requirement
|
|
72
|
+
req = node[:object]
|
|
73
|
+
dims = calculate_requirement_dimensions(req)
|
|
74
|
+
|
|
75
|
+
positioned_requirements[req.name] = {
|
|
76
|
+
requirement: req,
|
|
77
|
+
x: current_x,
|
|
78
|
+
y: current_y,
|
|
79
|
+
width: dims[:width],
|
|
80
|
+
height: dims[:height],
|
|
81
|
+
level: level_idx
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
current_x += dims[:width] + DEFAULT_SPACING_X
|
|
85
|
+
max_height = [max_height, dims[:height]].max
|
|
86
|
+
else
|
|
87
|
+
elem = node[:object]
|
|
88
|
+
dims = calculate_element_dimensions(elem)
|
|
89
|
+
|
|
90
|
+
positioned_elements[elem.name] = {
|
|
91
|
+
element: elem,
|
|
92
|
+
x: current_x,
|
|
93
|
+
y: current_y,
|
|
94
|
+
width: dims[:width],
|
|
95
|
+
height: dims[:height],
|
|
96
|
+
level: level_idx
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
current_x += dims[:width] + DEFAULT_SPACING_X
|
|
100
|
+
max_height = [max_height, dims[:height]].max
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
max_width = [max_width, current_x].max
|
|
105
|
+
current_y += max_height + DEFAULT_SPACING_Y
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
requirements: positioned_requirements,
|
|
110
|
+
elements: positioned_elements,
|
|
111
|
+
width: max_width + DEFAULT_PADDING,
|
|
112
|
+
height: current_y + DEFAULT_PADDING
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_dependency_levels(requirements, elements, relationships)
|
|
117
|
+
# Build a simple level-based layout
|
|
118
|
+
# Level 0: Elements
|
|
119
|
+
# Level 1: Requirements that depend on elements
|
|
120
|
+
# Level 2+: Requirements that depend on other requirements
|
|
121
|
+
|
|
122
|
+
nodes_by_name = {}
|
|
123
|
+
requirements.each { |r| nodes_by_name[r.name] = { type: :requirement, object: r, level: nil } }
|
|
124
|
+
elements.each { |e| nodes_by_name[e.name] = { type: :element, object: e, level: nil } }
|
|
125
|
+
|
|
126
|
+
# Start with elements at level 0
|
|
127
|
+
elements.each { |e| nodes_by_name[e.name][:level] = 0 }
|
|
128
|
+
|
|
129
|
+
# Build dependency map
|
|
130
|
+
dependencies = Hash.new { |h, k| h[k] = [] }
|
|
131
|
+
relationships.each do |rel|
|
|
132
|
+
# Source depends on target (arrow direction)
|
|
133
|
+
dependencies[rel.source] << rel.target
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Calculate levels for requirements
|
|
137
|
+
changed = true
|
|
138
|
+
max_iterations = 10
|
|
139
|
+
iterations = 0
|
|
140
|
+
|
|
141
|
+
while changed && iterations < max_iterations
|
|
142
|
+
changed = false
|
|
143
|
+
iterations += 1
|
|
144
|
+
|
|
145
|
+
requirements.each do |req|
|
|
146
|
+
node = nodes_by_name[req.name]
|
|
147
|
+
next if node[:level]
|
|
148
|
+
|
|
149
|
+
deps = dependencies[req.name]
|
|
150
|
+
if deps.empty?
|
|
151
|
+
# No dependencies, place at level 1
|
|
152
|
+
node[:level] = 1
|
|
153
|
+
changed = true
|
|
154
|
+
else
|
|
155
|
+
# Check if all dependencies have levels
|
|
156
|
+
dep_levels = deps.map { |d| nodes_by_name[d]&.dig(:level) }.compact
|
|
157
|
+
if dep_levels.size == deps.size
|
|
158
|
+
# All dependencies have levels
|
|
159
|
+
max_dep_level = dep_levels.max || 0
|
|
160
|
+
node[:level] = max_dep_level + 1
|
|
161
|
+
changed = true
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Assign default level to any remaining nodes
|
|
168
|
+
nodes_by_name.each do |_name, node|
|
|
169
|
+
node[:level] ||= 1 if node[:type] == :requirement
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Group by level
|
|
173
|
+
levels = []
|
|
174
|
+
nodes_by_name.values.group_by { |n| n[:level] }.sort.each do |_level, nodes|
|
|
175
|
+
levels << nodes
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
levels
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def calculate_requirement_dimensions(requirement)
|
|
182
|
+
# Calculate based on text content
|
|
183
|
+
text = requirement.text || ''
|
|
184
|
+
text_lines = text.length > 0 ? ((text.length / 25.0).ceil) : 1
|
|
185
|
+
|
|
186
|
+
width = DEFAULT_REQ_WIDTH
|
|
187
|
+
height = DEFAULT_REQ_HEIGHT + (text_lines - 1) * 20
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
width: width,
|
|
191
|
+
height: [height, DEFAULT_REQ_HEIGHT].max
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def calculate_element_dimensions(element)
|
|
196
|
+
{
|
|
197
|
+
width: DEFAULT_ELEM_WIDTH,
|
|
198
|
+
height: DEFAULT_ELEM_HEIGHT
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def calculate_relationships(diagram, nodes_layout)
|
|
203
|
+
requirements = nodes_layout[:requirements]
|
|
204
|
+
elements = nodes_layout[:elements]
|
|
205
|
+
all_nodes = requirements.merge(elements)
|
|
206
|
+
|
|
207
|
+
diagram.relationships.map do |rel|
|
|
208
|
+
source_node = all_nodes[rel.source]
|
|
209
|
+
target_node = all_nodes[rel.target]
|
|
210
|
+
|
|
211
|
+
next unless source_node && target_node
|
|
212
|
+
|
|
213
|
+
# Calculate connection points
|
|
214
|
+
source_x = source_node[:x] + source_node[:width] / 2
|
|
215
|
+
source_y = source_node[:y] + source_node[:height]
|
|
216
|
+
target_x = target_node[:x] + target_node[:width] / 2
|
|
217
|
+
target_y = target_node[:y]
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
relationship: rel,
|
|
221
|
+
source: rel.source,
|
|
222
|
+
target: rel.target,
|
|
223
|
+
type: rel.type,
|
|
224
|
+
from_x: source_x,
|
|
225
|
+
from_y: source_y,
|
|
226
|
+
to_x: target_x,
|
|
227
|
+
to_y: target_y
|
|
228
|
+
}
|
|
229
|
+
end.compact
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|