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,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sirena
|
|
4
|
+
module Transform
|
|
5
|
+
# Transforms a Kanban diagram into a positioned layout structure.
|
|
6
|
+
#
|
|
7
|
+
# The layout algorithm handles:
|
|
8
|
+
# - Columns positioned horizontally
|
|
9
|
+
# - Cards stacked vertically within columns
|
|
10
|
+
# - Proper spacing and sizing
|
|
11
|
+
#
|
|
12
|
+
# @example Transform a kanban board
|
|
13
|
+
# transform = Transform::Kanban.new
|
|
14
|
+
# layout = transform.to_graph(diagram)
|
|
15
|
+
class Kanban
|
|
16
|
+
# Horizontal spacing between columns
|
|
17
|
+
COLUMN_HORIZONTAL_SPACING = 60
|
|
18
|
+
|
|
19
|
+
# Vertical spacing between cards
|
|
20
|
+
CARD_VERTICAL_SPACING = 15
|
|
21
|
+
|
|
22
|
+
# Column dimensions
|
|
23
|
+
COLUMN_WIDTH = 200
|
|
24
|
+
COLUMN_HEADER_HEIGHT = 50
|
|
25
|
+
COLUMN_PADDING = 10
|
|
26
|
+
|
|
27
|
+
# Card dimensions
|
|
28
|
+
CARD_HEIGHT = 80
|
|
29
|
+
CARD_PADDING = 10
|
|
30
|
+
|
|
31
|
+
# Metadata display height per item
|
|
32
|
+
METADATA_LINE_HEIGHT = 18
|
|
33
|
+
|
|
34
|
+
# Transforms the diagram into a layout structure.
|
|
35
|
+
#
|
|
36
|
+
# @param diagram [Diagram::Kanban] the kanban diagram
|
|
37
|
+
# @return [Hash] layout data with columns, cards, and dimensions
|
|
38
|
+
def to_graph(diagram)
|
|
39
|
+
return empty_graph if diagram.columns.empty?
|
|
40
|
+
|
|
41
|
+
# Position columns horizontally
|
|
42
|
+
positioned_columns = position_columns(diagram.columns)
|
|
43
|
+
|
|
44
|
+
# Position cards within each column
|
|
45
|
+
positioned_cards = position_cards(positioned_columns)
|
|
46
|
+
|
|
47
|
+
# Calculate overall bounds
|
|
48
|
+
bounds = calculate_bounds(positioned_columns, positioned_cards)
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
columns: positioned_columns,
|
|
52
|
+
cards: positioned_cards,
|
|
53
|
+
width: bounds[:width],
|
|
54
|
+
height: bounds[:height]
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def empty_graph
|
|
61
|
+
{
|
|
62
|
+
columns: [],
|
|
63
|
+
cards: [],
|
|
64
|
+
width: 0,
|
|
65
|
+
height: 0
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Positions columns horizontally
|
|
70
|
+
#
|
|
71
|
+
# @param columns [Array<Diagram::KanbanColumn>] columns to position
|
|
72
|
+
# @return [Array<Hash>] positioned columns
|
|
73
|
+
def position_columns(columns)
|
|
74
|
+
positioned = []
|
|
75
|
+
current_x = 0
|
|
76
|
+
|
|
77
|
+
columns.each do |column|
|
|
78
|
+
positioned << {
|
|
79
|
+
id: column.id,
|
|
80
|
+
title: column.title,
|
|
81
|
+
x: current_x,
|
|
82
|
+
y: 0,
|
|
83
|
+
width: COLUMN_WIDTH,
|
|
84
|
+
height: calculate_column_height(column),
|
|
85
|
+
card_count: column.cards.size,
|
|
86
|
+
original: column
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
current_x += COLUMN_WIDTH + COLUMN_HORIZONTAL_SPACING
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
positioned
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Positions cards within their columns
|
|
96
|
+
#
|
|
97
|
+
# @param positioned_columns [Array<Hash>] positioned columns
|
|
98
|
+
# @return [Array<Hash>] positioned cards
|
|
99
|
+
def position_cards(positioned_columns)
|
|
100
|
+
cards = []
|
|
101
|
+
|
|
102
|
+
positioned_columns.each do |column_data|
|
|
103
|
+
column = column_data[:original]
|
|
104
|
+
column_x = column_data[:x]
|
|
105
|
+
current_y = COLUMN_HEADER_HEIGHT + COLUMN_PADDING
|
|
106
|
+
|
|
107
|
+
column.cards.each do |card|
|
|
108
|
+
card_height = calculate_card_height(card)
|
|
109
|
+
|
|
110
|
+
cards << {
|
|
111
|
+
id: card.id,
|
|
112
|
+
text: card.text,
|
|
113
|
+
column_id: column.id,
|
|
114
|
+
x: column_x + COLUMN_PADDING,
|
|
115
|
+
y: current_y,
|
|
116
|
+
width: COLUMN_WIDTH - (COLUMN_PADDING * 2),
|
|
117
|
+
height: card_height,
|
|
118
|
+
metadata: card.metadata,
|
|
119
|
+
has_metadata: card.has_metadata?,
|
|
120
|
+
original: card
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
current_y += card_height + CARD_VERTICAL_SPACING
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
cards
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Calculates the height needed for a column
|
|
131
|
+
#
|
|
132
|
+
# @param column [Diagram::KanbanColumn] column
|
|
133
|
+
# @return [Numeric] column height
|
|
134
|
+
def calculate_column_height(column)
|
|
135
|
+
return COLUMN_HEADER_HEIGHT + COLUMN_PADDING if column.cards.empty?
|
|
136
|
+
|
|
137
|
+
# Header + padding + sum of card heights + spacing between cards
|
|
138
|
+
total_card_height = column.cards.sum { |card| calculate_card_height(card) }
|
|
139
|
+
total_spacing = (column.cards.size - 1) * CARD_VERTICAL_SPACING
|
|
140
|
+
bottom_padding = COLUMN_PADDING
|
|
141
|
+
|
|
142
|
+
COLUMN_HEADER_HEIGHT + COLUMN_PADDING +
|
|
143
|
+
total_card_height + total_spacing + bottom_padding
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Calculates the height needed for a card
|
|
147
|
+
#
|
|
148
|
+
# @param card [Diagram::KanbanCard] card
|
|
149
|
+
# @return [Numeric] card height
|
|
150
|
+
def calculate_card_height(card)
|
|
151
|
+
base_height = CARD_HEIGHT
|
|
152
|
+
|
|
153
|
+
# Add height for metadata if present
|
|
154
|
+
if card.has_metadata?
|
|
155
|
+
metadata_count = card.metadata.size
|
|
156
|
+
base_height + (metadata_count * METADATA_LINE_HEIGHT)
|
|
157
|
+
else
|
|
158
|
+
base_height
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Calculates the bounding box for the entire board
|
|
163
|
+
#
|
|
164
|
+
# @param columns [Array<Hash>] positioned columns
|
|
165
|
+
# @param cards [Array<Hash>] positioned cards
|
|
166
|
+
# @return [Hash] width and height
|
|
167
|
+
def calculate_bounds(columns, cards)
|
|
168
|
+
return { width: 0, height: 0 } if columns.empty?
|
|
169
|
+
|
|
170
|
+
max_x = columns.map { |c| c[:x] + c[:width] }.max
|
|
171
|
+
max_y = columns.map { |c| c[:height] }.max
|
|
172
|
+
|
|
173
|
+
{
|
|
174
|
+
width: max_x,
|
|
175
|
+
height: max_y
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sirena
|
|
4
|
+
module Transform
|
|
5
|
+
# Transforms a Mindmap diagram into a positioned layout structure.
|
|
6
|
+
#
|
|
7
|
+
# The layout algorithm handles:
|
|
8
|
+
# - Tree-based layout with root at center
|
|
9
|
+
# - Radial positioning of branches
|
|
10
|
+
# - Level-based spacing
|
|
11
|
+
# - Connection path calculation
|
|
12
|
+
#
|
|
13
|
+
# @example Transform a mindmap
|
|
14
|
+
# transform = Transform::Mindmap.new
|
|
15
|
+
# layout = transform.to_graph(diagram)
|
|
16
|
+
class Mindmap
|
|
17
|
+
# Horizontal spacing between sibling nodes
|
|
18
|
+
NODE_HORIZONTAL_SPACING = 120
|
|
19
|
+
|
|
20
|
+
# Vertical spacing between levels
|
|
21
|
+
LEVEL_VERTICAL_SPACING = 80
|
|
22
|
+
|
|
23
|
+
# Default node dimensions
|
|
24
|
+
DEFAULT_NODE_WIDTH = 100
|
|
25
|
+
DEFAULT_NODE_HEIGHT = 40
|
|
26
|
+
|
|
27
|
+
# Padding for root node
|
|
28
|
+
ROOT_PADDING = 20
|
|
29
|
+
|
|
30
|
+
# Transforms the diagram into a layout structure.
|
|
31
|
+
#
|
|
32
|
+
# @param diagram [Diagram::Mindmap] the mindmap diagram
|
|
33
|
+
# @return [Hash] layout data with nodes and connections
|
|
34
|
+
def to_graph(diagram)
|
|
35
|
+
return empty_graph unless diagram.root
|
|
36
|
+
|
|
37
|
+
# Position nodes using tree layout
|
|
38
|
+
positioned_nodes = position_tree(diagram.root)
|
|
39
|
+
|
|
40
|
+
# Build connections between nodes
|
|
41
|
+
connections = build_connections(diagram.root)
|
|
42
|
+
|
|
43
|
+
# Calculate bounds
|
|
44
|
+
bounds = calculate_bounds(positioned_nodes)
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
nodes: positioned_nodes,
|
|
48
|
+
connections: connections,
|
|
49
|
+
width: bounds[:width],
|
|
50
|
+
height: bounds[:height],
|
|
51
|
+
root: positioned_nodes.first
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def empty_graph
|
|
58
|
+
{
|
|
59
|
+
nodes: [],
|
|
60
|
+
connections: [],
|
|
61
|
+
width: 0,
|
|
62
|
+
height: 0,
|
|
63
|
+
root: nil
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Positions nodes in a tree layout
|
|
68
|
+
#
|
|
69
|
+
# @param root [Diagram::Mindmap::MindmapNode] root node
|
|
70
|
+
# @return [Array<Hash>] positioned nodes
|
|
71
|
+
def position_tree(root)
|
|
72
|
+
nodes = []
|
|
73
|
+
|
|
74
|
+
# Start with root at center-top
|
|
75
|
+
root_width = estimate_node_width(root)
|
|
76
|
+
root_height = estimate_node_height(root)
|
|
77
|
+
|
|
78
|
+
# Calculate tree width to center root
|
|
79
|
+
tree_width = calculate_tree_width(root)
|
|
80
|
+
root_x = tree_width / 2
|
|
81
|
+
|
|
82
|
+
# Position root
|
|
83
|
+
nodes << {
|
|
84
|
+
id: root.id,
|
|
85
|
+
content: root.content,
|
|
86
|
+
x: root_x,
|
|
87
|
+
y: ROOT_PADDING,
|
|
88
|
+
width: root_width,
|
|
89
|
+
height: root_height,
|
|
90
|
+
level: root.level,
|
|
91
|
+
shape: root.shape,
|
|
92
|
+
icon: root.icon,
|
|
93
|
+
classes: root.classes,
|
|
94
|
+
original: root
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Position children recursively
|
|
98
|
+
if root.children.any?
|
|
99
|
+
position_children(
|
|
100
|
+
root,
|
|
101
|
+
root_x,
|
|
102
|
+
ROOT_PADDING + root_height + LEVEL_VERTICAL_SPACING,
|
|
103
|
+
nodes
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
nodes
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Positions children of a node
|
|
111
|
+
#
|
|
112
|
+
# @param parent [Diagram::Mindmap::MindmapNode] parent node
|
|
113
|
+
# @param parent_x [Numeric] parent X position
|
|
114
|
+
# @param y [Numeric] Y position for this level
|
|
115
|
+
# @param nodes [Array<Hash>] accumulator for positioned nodes
|
|
116
|
+
def position_children(parent, parent_x, y, nodes)
|
|
117
|
+
children = parent.children
|
|
118
|
+
return if children.empty?
|
|
119
|
+
|
|
120
|
+
# Calculate total width needed for all children
|
|
121
|
+
total_width = children.sum { |c| estimate_subtree_width(c) }
|
|
122
|
+
total_width += (children.size - 1) * NODE_HORIZONTAL_SPACING
|
|
123
|
+
|
|
124
|
+
# Start x position (centered under parent)
|
|
125
|
+
start_x = parent_x - (total_width / 2)
|
|
126
|
+
current_x = start_x
|
|
127
|
+
|
|
128
|
+
children.each do |child|
|
|
129
|
+
child_width = estimate_node_width(child)
|
|
130
|
+
child_height = estimate_node_height(child)
|
|
131
|
+
subtree_width = estimate_subtree_width(child)
|
|
132
|
+
|
|
133
|
+
# Center the node within its subtree space
|
|
134
|
+
node_x = current_x + (subtree_width / 2)
|
|
135
|
+
|
|
136
|
+
nodes << {
|
|
137
|
+
id: child.id,
|
|
138
|
+
content: child.content,
|
|
139
|
+
x: node_x,
|
|
140
|
+
y: y,
|
|
141
|
+
width: child_width,
|
|
142
|
+
height: child_height,
|
|
143
|
+
level: child.level,
|
|
144
|
+
shape: child.shape,
|
|
145
|
+
icon: child.icon,
|
|
146
|
+
classes: child.classes,
|
|
147
|
+
parent_id: parent.id,
|
|
148
|
+
original: child
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Recursively position grandchildren
|
|
152
|
+
if child.children.any?
|
|
153
|
+
position_children(
|
|
154
|
+
child,
|
|
155
|
+
node_x,
|
|
156
|
+
y + child_height + LEVEL_VERTICAL_SPACING,
|
|
157
|
+
nodes
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
current_x += subtree_width + NODE_HORIZONTAL_SPACING
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Estimates the width of a single node based on content and shape
|
|
166
|
+
#
|
|
167
|
+
# @param node [Diagram::Mindmap::MindmapNode] node
|
|
168
|
+
# @return [Numeric] estimated width
|
|
169
|
+
def estimate_node_width(node)
|
|
170
|
+
# Base width on content length
|
|
171
|
+
content_length = node.content.to_s.length
|
|
172
|
+
base_width = [content_length * 8 + 20, DEFAULT_NODE_WIDTH].max
|
|
173
|
+
|
|
174
|
+
# Adjust for shape
|
|
175
|
+
case node.shape
|
|
176
|
+
when "circle", "hexagon"
|
|
177
|
+
base_width * 1.2
|
|
178
|
+
else
|
|
179
|
+
base_width
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Estimates the height of a node
|
|
184
|
+
#
|
|
185
|
+
# @param node [Diagram::Mindmap::MindmapNode] node
|
|
186
|
+
# @return [Numeric] estimated height
|
|
187
|
+
def estimate_node_height(node)
|
|
188
|
+
DEFAULT_NODE_HEIGHT
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Calculates the total width needed for a subtree
|
|
192
|
+
#
|
|
193
|
+
# @param node [Diagram::Mindmap::MindmapNode] root of subtree
|
|
194
|
+
# @return [Numeric] total width
|
|
195
|
+
def estimate_subtree_width(node)
|
|
196
|
+
node_width = estimate_node_width(node)
|
|
197
|
+
return node_width if node.children.empty?
|
|
198
|
+
|
|
199
|
+
# Width is max of node width or sum of children widths
|
|
200
|
+
children_width = node.children.sum { |c| estimate_subtree_width(c) }
|
|
201
|
+
children_width += (node.children.size - 1) * NODE_HORIZONTAL_SPACING
|
|
202
|
+
|
|
203
|
+
[node_width, children_width].max
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Calculates the total width of the entire tree
|
|
207
|
+
#
|
|
208
|
+
# @param root [Diagram::Mindmap::MindmapNode] root node
|
|
209
|
+
# @return [Numeric] tree width
|
|
210
|
+
def calculate_tree_width(root)
|
|
211
|
+
estimate_subtree_width(root) + ROOT_PADDING * 2
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Builds connections between parent and child nodes
|
|
215
|
+
#
|
|
216
|
+
# @param node [Diagram::Mindmap::MindmapNode] current node
|
|
217
|
+
# @param connections [Array<Hash>] accumulator
|
|
218
|
+
# @return [Array<Hash>] all connections
|
|
219
|
+
def build_connections(node, connections = [])
|
|
220
|
+
node.children.each do |child|
|
|
221
|
+
connections << {
|
|
222
|
+
from: node.id,
|
|
223
|
+
to: child.id,
|
|
224
|
+
type: :parent_child
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Recursively build connections for children
|
|
228
|
+
build_connections(child, connections)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
connections
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Calculates the bounding box for all positioned nodes
|
|
235
|
+
#
|
|
236
|
+
# @param nodes [Array<Hash>] positioned nodes
|
|
237
|
+
# @return [Hash] width and height
|
|
238
|
+
def calculate_bounds(nodes)
|
|
239
|
+
return { width: 0, height: 0 } if nodes.empty?
|
|
240
|
+
|
|
241
|
+
max_x = nodes.map { |n| n[:x] + n[:width] / 2 }.max
|
|
242
|
+
max_y = nodes.map { |n| n[:y] + n[:height] }.max
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
width: max_x + ROOT_PADDING,
|
|
246
|
+
height: max_y + ROOT_PADDING
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sirena
|
|
4
|
+
module Transform
|
|
5
|
+
# Transforms a PacketDiagram into a positioned layout structure.
|
|
6
|
+
#
|
|
7
|
+
# The layout algorithm handles:
|
|
8
|
+
# - Organizing fields into rows based on bit positions
|
|
9
|
+
# - Calculating cell positions in the packet grid
|
|
10
|
+
# - Handling fields that span multiple bits
|
|
11
|
+
# - Typical packet width is 32 bits per row
|
|
12
|
+
#
|
|
13
|
+
# @example Transform a packet diagram
|
|
14
|
+
# transform = Transform::Packet.new
|
|
15
|
+
# layout = transform.to_graph(diagram)
|
|
16
|
+
class Packet
|
|
17
|
+
# Default number of bits per row (standard packet width)
|
|
18
|
+
BITS_PER_ROW = 32
|
|
19
|
+
|
|
20
|
+
# Cell dimensions
|
|
21
|
+
CELL_WIDTH = 30
|
|
22
|
+
CELL_HEIGHT = 40
|
|
23
|
+
|
|
24
|
+
# Padding around the diagram
|
|
25
|
+
PADDING = 40
|
|
26
|
+
|
|
27
|
+
# Header height for bit position markers
|
|
28
|
+
HEADER_HEIGHT = 30
|
|
29
|
+
|
|
30
|
+
# Title spacing
|
|
31
|
+
TITLE_HEIGHT = 40
|
|
32
|
+
TITLE_MARGIN = 20
|
|
33
|
+
|
|
34
|
+
# Transforms the diagram into a layout structure.
|
|
35
|
+
#
|
|
36
|
+
# @param diagram [Diagram::PacketDiagram] the packet diagram
|
|
37
|
+
# @return [Hash] layout data with positioned fields and dimensions
|
|
38
|
+
def to_graph(diagram)
|
|
39
|
+
return empty_layout if diagram.fields.empty?
|
|
40
|
+
|
|
41
|
+
# Calculate the number of rows needed
|
|
42
|
+
row_count = diagram.row_count(BITS_PER_ROW)
|
|
43
|
+
|
|
44
|
+
# Position each field
|
|
45
|
+
positioned_fields = position_fields(diagram.fields, row_count)
|
|
46
|
+
|
|
47
|
+
# Calculate dimensions
|
|
48
|
+
width = BITS_PER_ROW * CELL_WIDTH + (PADDING * 2)
|
|
49
|
+
content_height = row_count * CELL_HEIGHT + HEADER_HEIGHT
|
|
50
|
+
title_offset = diagram.title ? TITLE_HEIGHT + TITLE_MARGIN : 0
|
|
51
|
+
height = content_height + (PADDING * 2) + title_offset
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
fields: positioned_fields,
|
|
55
|
+
row_count: row_count,
|
|
56
|
+
bits_per_row: BITS_PER_ROW,
|
|
57
|
+
cell_width: CELL_WIDTH,
|
|
58
|
+
cell_height: CELL_HEIGHT,
|
|
59
|
+
padding: PADDING,
|
|
60
|
+
header_height: HEADER_HEIGHT,
|
|
61
|
+
title_height: diagram.title ? TITLE_HEIGHT : 0,
|
|
62
|
+
title_margin: diagram.title ? TITLE_MARGIN : 0,
|
|
63
|
+
width: width,
|
|
64
|
+
height: height,
|
|
65
|
+
title: diagram.title
|
|
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
|
+
fields: [],
|
|
77
|
+
row_count: 0,
|
|
78
|
+
bits_per_row: BITS_PER_ROW,
|
|
79
|
+
cell_width: CELL_WIDTH,
|
|
80
|
+
cell_height: CELL_HEIGHT,
|
|
81
|
+
padding: PADDING,
|
|
82
|
+
header_height: HEADER_HEIGHT,
|
|
83
|
+
title_height: 0,
|
|
84
|
+
title_margin: 0,
|
|
85
|
+
width: PADDING * 2,
|
|
86
|
+
height: PADDING * 2,
|
|
87
|
+
title: nil
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Positions all fields in the grid.
|
|
92
|
+
#
|
|
93
|
+
# @param fields [Array<Diagram::PacketField>] fields to position
|
|
94
|
+
# @param row_count [Integer] total number of rows
|
|
95
|
+
# @return [Array<Hash>] positioned fields with coordinates
|
|
96
|
+
def position_fields(fields, row_count)
|
|
97
|
+
positioned = []
|
|
98
|
+
|
|
99
|
+
fields.each do |field|
|
|
100
|
+
# Handle fields that may span multiple rows
|
|
101
|
+
if field.spans_rows?(BITS_PER_ROW)
|
|
102
|
+
# Split into multiple visual segments
|
|
103
|
+
positioned.concat(split_field_across_rows(field))
|
|
104
|
+
else
|
|
105
|
+
# Single row field
|
|
106
|
+
positioned << position_single_field(field)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
positioned
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Positions a field that fits in a single row.
|
|
114
|
+
#
|
|
115
|
+
# @param field [Diagram::PacketField] field to position
|
|
116
|
+
# @return [Hash] positioned field data
|
|
117
|
+
def position_single_field(field)
|
|
118
|
+
row = field.start_row(BITS_PER_ROW)
|
|
119
|
+
start_col = field.start_bit_in_row(BITS_PER_ROW)
|
|
120
|
+
end_col = field.end_bit_in_row(BITS_PER_ROW)
|
|
121
|
+
|
|
122
|
+
x = PADDING + (start_col * CELL_WIDTH)
|
|
123
|
+
y = PADDING + HEADER_HEIGHT + (row * CELL_HEIGHT)
|
|
124
|
+
width = (end_col - start_col + 1) * CELL_WIDTH
|
|
125
|
+
height = CELL_HEIGHT
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
label: field.label,
|
|
129
|
+
bit_start: field.bit_start,
|
|
130
|
+
bit_end: field.bit_end,
|
|
131
|
+
x: x,
|
|
132
|
+
y: y,
|
|
133
|
+
width: width,
|
|
134
|
+
height: height,
|
|
135
|
+
row: row,
|
|
136
|
+
start_col: start_col,
|
|
137
|
+
end_col: end_col
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Splits a field that spans multiple rows into visual segments.
|
|
142
|
+
#
|
|
143
|
+
# @param field [Diagram::PacketField] field to split
|
|
144
|
+
# @return [Array<Hash>] array of positioned segments
|
|
145
|
+
def split_field_across_rows(field)
|
|
146
|
+
segments = []
|
|
147
|
+
current_bit = field.bit_start
|
|
148
|
+
|
|
149
|
+
while current_bit <= field.bit_end
|
|
150
|
+
row = current_bit / BITS_PER_ROW
|
|
151
|
+
start_col = current_bit % BITS_PER_ROW
|
|
152
|
+
|
|
153
|
+
# Determine end column for this row
|
|
154
|
+
row_end_bit = ((row + 1) * BITS_PER_ROW) - 1
|
|
155
|
+
segment_end_bit = [field.bit_end, row_end_bit].min
|
|
156
|
+
end_col = segment_end_bit % BITS_PER_ROW
|
|
157
|
+
|
|
158
|
+
x = PADDING + (start_col * CELL_WIDTH)
|
|
159
|
+
y = PADDING + HEADER_HEIGHT + (row * CELL_HEIGHT)
|
|
160
|
+
width = (end_col - start_col + 1) * CELL_WIDTH
|
|
161
|
+
height = CELL_HEIGHT
|
|
162
|
+
|
|
163
|
+
segments << {
|
|
164
|
+
label: field.label,
|
|
165
|
+
bit_start: current_bit,
|
|
166
|
+
bit_end: segment_end_bit,
|
|
167
|
+
x: x,
|
|
168
|
+
y: y,
|
|
169
|
+
width: width,
|
|
170
|
+
height: height,
|
|
171
|
+
row: row,
|
|
172
|
+
start_col: start_col,
|
|
173
|
+
end_col: end_col,
|
|
174
|
+
is_continuation: current_bit > field.bit_start,
|
|
175
|
+
is_final: segment_end_bit == field.bit_end
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
current_bit = segment_end_bit + 1
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
segments
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/pie'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Pie chart transformer for converting pie models to renderable structure.
|
|
9
|
+
#
|
|
10
|
+
# Unlike flowcharts and sequence diagrams which require complex layout
|
|
11
|
+
# computation, pie charts have a fixed circular layout. This transformer
|
|
12
|
+
# simply validates and prepares the diagram data for direct rendering.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a pie chart
|
|
15
|
+
# transform = PieTransform.new
|
|
16
|
+
# data = transform.to_graph(pie_diagram)
|
|
17
|
+
class PieTransform < Base
|
|
18
|
+
# Converts a pie diagram to a simple data structure.
|
|
19
|
+
#
|
|
20
|
+
# Pie charts don't need graph layout computation since they have
|
|
21
|
+
# a fixed circular layout. This method validates the diagram and
|
|
22
|
+
# returns a simple structure for the renderer.
|
|
23
|
+
#
|
|
24
|
+
# @param diagram [Diagram::Pie] the pie diagram to transform
|
|
25
|
+
# @return [Hash] data structure for rendering
|
|
26
|
+
# @raise [TransformError] if diagram is invalid
|
|
27
|
+
def to_graph(diagram)
|
|
28
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
id: diagram.id || 'pie',
|
|
32
|
+
title: diagram.title,
|
|
33
|
+
show_data: diagram.show_data || false,
|
|
34
|
+
acc_title: diagram.acc_title,
|
|
35
|
+
acc_description: diagram.acc_description,
|
|
36
|
+
slices: transform_slices(diagram),
|
|
37
|
+
metadata: {
|
|
38
|
+
total_value: diagram.total_value,
|
|
39
|
+
slice_count: (diagram.slices || []).length
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def transform_slices(diagram)
|
|
47
|
+
return [] if diagram.slices.nil? || diagram.slices.empty?
|
|
48
|
+
|
|
49
|
+
diagram.slices.map.with_index do |slice, index|
|
|
50
|
+
{
|
|
51
|
+
id: "slice_#{index}",
|
|
52
|
+
label: slice.label,
|
|
53
|
+
value: slice.value,
|
|
54
|
+
percentage: slice.percentage(diagram.total_value),
|
|
55
|
+
angle: diagram.slice_angle(slice),
|
|
56
|
+
index: index
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|