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,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../diagram/timeline"
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Timeline transformer for converting timeline models to renderable structure.
|
|
9
|
+
#
|
|
10
|
+
# Handles chronological ordering, positioning along the timeline axis,
|
|
11
|
+
# and grouping by sections.
|
|
12
|
+
#
|
|
13
|
+
# @example Transform a timeline
|
|
14
|
+
# transform = TimelineTransform.new
|
|
15
|
+
# data = transform.to_graph(timeline_diagram)
|
|
16
|
+
class TimelineTransform < Base
|
|
17
|
+
# Converts a Timeline diagram to a layout structure with calculated positions.
|
|
18
|
+
#
|
|
19
|
+
# @param diagram [Diagram::Timeline] the timeline diagram to transform
|
|
20
|
+
# @return [Hash] data structure for rendering
|
|
21
|
+
# @raise [TransformError] if diagram is invalid
|
|
22
|
+
def to_graph(diagram)
|
|
23
|
+
raise TransformError, "Invalid diagram" unless diagram.valid?
|
|
24
|
+
|
|
25
|
+
@diagram = diagram
|
|
26
|
+
|
|
27
|
+
# Collect all events and determine timeline bounds
|
|
28
|
+
all_events = collect_all_events(diagram)
|
|
29
|
+
timeline_range = calculate_timeline_range(all_events)
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
id: "timeline",
|
|
33
|
+
title: diagram.title,
|
|
34
|
+
acc_title: diagram.acc_title,
|
|
35
|
+
acc_description: diagram.acc_description,
|
|
36
|
+
sections: transform_sections(diagram, timeline_range),
|
|
37
|
+
events: transform_events(diagram.events, timeline_range),
|
|
38
|
+
timeline: timeline_range,
|
|
39
|
+
metadata: {
|
|
40
|
+
section_count: diagram.sections.length,
|
|
41
|
+
total_events: all_events.length,
|
|
42
|
+
has_sections: diagram.has_sections?
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def collect_all_events(diagram)
|
|
50
|
+
events = diagram.events.dup
|
|
51
|
+
diagram.sections.each do |section|
|
|
52
|
+
events.concat(section.events)
|
|
53
|
+
end
|
|
54
|
+
events
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def calculate_timeline_range(events)
|
|
58
|
+
return default_timeline_range if events.empty?
|
|
59
|
+
|
|
60
|
+
# Extract numeric time values for positioning
|
|
61
|
+
time_values = events.map { |e| extract_numeric_time(e.time) }.compact
|
|
62
|
+
|
|
63
|
+
if time_values.empty?
|
|
64
|
+
return default_timeline_range
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
min_time = time_values.min
|
|
68
|
+
max_time = time_values.max
|
|
69
|
+
|
|
70
|
+
# Add some padding
|
|
71
|
+
padding = ((max_time - min_time) * 0.1).ceil
|
|
72
|
+
padding = 1 if padding < 1
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
min: min_time - padding,
|
|
76
|
+
max: max_time + padding,
|
|
77
|
+
span: (max_time - min_time) + (2 * padding)
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def default_timeline_range
|
|
82
|
+
{
|
|
83
|
+
min: 2000,
|
|
84
|
+
max: 2024,
|
|
85
|
+
span: 24
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def extract_numeric_time(time_str)
|
|
90
|
+
# Try to extract a numeric value from the time string
|
|
91
|
+
# This handles years (2004), dates (2004-01-01), etc.
|
|
92
|
+
return nil if time_str.nil? || time_str.empty?
|
|
93
|
+
|
|
94
|
+
# Extract first continuous sequence of digits
|
|
95
|
+
match = time_str.match(/\d+/)
|
|
96
|
+
match ? match[0].to_i : nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def transform_sections(diagram, timeline_range)
|
|
100
|
+
diagram.sections.map.with_index do |section, index|
|
|
101
|
+
{
|
|
102
|
+
id: "section_#{index}",
|
|
103
|
+
name: section.name,
|
|
104
|
+
events: transform_events(section.events, timeline_range),
|
|
105
|
+
tasks: section.tasks,
|
|
106
|
+
has_events: section.has_events?,
|
|
107
|
+
has_tasks: section.has_tasks?
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def transform_events(events, timeline_range)
|
|
113
|
+
events.map.with_index do |event, index|
|
|
114
|
+
{
|
|
115
|
+
id: "event_#{index}",
|
|
116
|
+
time: event.time,
|
|
117
|
+
descriptions: event.descriptions,
|
|
118
|
+
primary_description: event.primary_description,
|
|
119
|
+
multiple_descriptions: event.multiple_descriptions?,
|
|
120
|
+
x_position: calculate_x_position(event.time, timeline_range)
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def calculate_x_position(time_str, timeline_range)
|
|
126
|
+
numeric_time = extract_numeric_time(time_str)
|
|
127
|
+
return 0 unless numeric_time
|
|
128
|
+
|
|
129
|
+
# Calculate position as percentage along timeline
|
|
130
|
+
# This will be scaled by renderer to actual coordinates
|
|
131
|
+
if timeline_range[:span] > 0
|
|
132
|
+
((numeric_time - timeline_range[:min]).to_f / timeline_range[:span]) * 100.0
|
|
133
|
+
else
|
|
134
|
+
50.0 # Center if no span
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../diagram/treemap'
|
|
4
|
+
|
|
5
|
+
module Sirena
|
|
6
|
+
module Transform
|
|
7
|
+
# Layout calculator for treemap diagrams
|
|
8
|
+
class Treemap
|
|
9
|
+
PADDING = 10
|
|
10
|
+
MIN_CELL_SIZE = 40
|
|
11
|
+
LABEL_HEIGHT = 20
|
|
12
|
+
HEADER_HEIGHT = 40
|
|
13
|
+
|
|
14
|
+
# Transforms the diagram into a layout structure.
|
|
15
|
+
#
|
|
16
|
+
# @param diagram [Diagram::TreemapDiagram] the treemap diagram
|
|
17
|
+
# @return [Hash] layout data with positioned cells and dimensions
|
|
18
|
+
def to_graph(diagram)
|
|
19
|
+
total_value = diagram.total_value
|
|
20
|
+
return default_layout(diagram) if total_value <= 0
|
|
21
|
+
|
|
22
|
+
# Calculate dimensions
|
|
23
|
+
width = calculate_width(diagram)
|
|
24
|
+
height = calculate_height(diagram)
|
|
25
|
+
y_offset = diagram.title ? HEADER_HEIGHT + PADDING : PADDING
|
|
26
|
+
|
|
27
|
+
# Layout root nodes
|
|
28
|
+
cells = []
|
|
29
|
+
x = PADDING
|
|
30
|
+
y = y_offset
|
|
31
|
+
|
|
32
|
+
diagram.root_nodes.each do |node|
|
|
33
|
+
cell = layout_node(node, x, y, width - 2 * PADDING,
|
|
34
|
+
height - y_offset - PADDING, total_value)
|
|
35
|
+
cells << cell if cell
|
|
36
|
+
y += cell[:height] + PADDING if cell
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
width: width,
|
|
41
|
+
height: height,
|
|
42
|
+
title: diagram.title,
|
|
43
|
+
cells: cells,
|
|
44
|
+
class_defs: diagram.class_defs
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def layout_node(node, x, y, available_width, available_height, total_value)
|
|
51
|
+
node_value = node.total_value
|
|
52
|
+
return nil if node_value <= 0
|
|
53
|
+
|
|
54
|
+
# Calculate proportional height
|
|
55
|
+
ratio = node_value / total_value
|
|
56
|
+
height = [available_height * ratio, MIN_CELL_SIZE].max
|
|
57
|
+
|
|
58
|
+
cell = {
|
|
59
|
+
label: node.label,
|
|
60
|
+
value: node_value,
|
|
61
|
+
x: x,
|
|
62
|
+
y: y,
|
|
63
|
+
width: available_width,
|
|
64
|
+
height: height,
|
|
65
|
+
css_class: node.css_class,
|
|
66
|
+
depth: node.depth,
|
|
67
|
+
children: []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# If this node has children, layout them recursively
|
|
71
|
+
if node.branch? && node.children.any?
|
|
72
|
+
child_y = y + LABEL_HEIGHT
|
|
73
|
+
child_height = height - LABEL_HEIGHT - PADDING
|
|
74
|
+
|
|
75
|
+
node.children.each do |child|
|
|
76
|
+
child_cell = layout_node(child, x + PADDING, child_y,
|
|
77
|
+
available_width - 2 * PADDING,
|
|
78
|
+
child_height, node_value)
|
|
79
|
+
if child_cell
|
|
80
|
+
cell[:children] << child_cell
|
|
81
|
+
child_y += child_cell[:height] + PADDING
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
cell
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def calculate_width(diagram)
|
|
90
|
+
# Base width calculation
|
|
91
|
+
800
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def calculate_height(diagram)
|
|
95
|
+
# Calculate based on number of nodes
|
|
96
|
+
node_count = count_nodes(diagram.root_nodes)
|
|
97
|
+
base_height = 400
|
|
98
|
+
additional_height = [node_count * 30, 200].min
|
|
99
|
+
|
|
100
|
+
base_height + additional_height
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def count_nodes(nodes)
|
|
104
|
+
nodes.sum do |node|
|
|
105
|
+
1 + (node.children ? count_nodes(node.children) : 0)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def default_layout(diagram)
|
|
110
|
+
{
|
|
111
|
+
width: 800,
|
|
112
|
+
height: 400,
|
|
113
|
+
title: diagram.title,
|
|
114
|
+
cells: [],
|
|
115
|
+
class_defs: diagram.class_defs
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/user_journey'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# User Journey diagram transformer for converting journey models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed user journey model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles task box sizing
|
|
12
|
+
# based on task name and actor count, sequential flow between tasks,
|
|
13
|
+
# and horizontal timeline layout.
|
|
14
|
+
#
|
|
15
|
+
# @example Transform a user journey
|
|
16
|
+
# transform = UserJourneyTransform.new
|
|
17
|
+
# graph = transform.to_graph(user_journey)
|
|
18
|
+
class UserJourneyTransform < Base
|
|
19
|
+
# Default font size for text measurement
|
|
20
|
+
DEFAULT_FONT_SIZE = 14
|
|
21
|
+
|
|
22
|
+
# Minimum width for a task box
|
|
23
|
+
MIN_TASK_WIDTH = 120
|
|
24
|
+
|
|
25
|
+
# Height per task box
|
|
26
|
+
TASK_HEIGHT = 80
|
|
27
|
+
|
|
28
|
+
# Padding within task box
|
|
29
|
+
TASK_PADDING = 10
|
|
30
|
+
|
|
31
|
+
# Horizontal spacing between tasks
|
|
32
|
+
TASK_SPACING = 60
|
|
33
|
+
|
|
34
|
+
# Vertical spacing between sections
|
|
35
|
+
SECTION_SPACING = 40
|
|
36
|
+
|
|
37
|
+
# Converts a user journey to a graph structure.
|
|
38
|
+
#
|
|
39
|
+
# @param diagram [Diagram::UserJourney] the user journey to transform
|
|
40
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
41
|
+
# @raise [TransformError] if diagram is invalid
|
|
42
|
+
def to_graph(diagram)
|
|
43
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
id: diagram.id || 'user_journey',
|
|
47
|
+
children: transform_tasks(diagram),
|
|
48
|
+
edges: transform_task_flow(diagram),
|
|
49
|
+
layoutOptions: layout_options,
|
|
50
|
+
metadata: {
|
|
51
|
+
title: diagram.title,
|
|
52
|
+
sections: diagram.sections.map(&:name)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def transform_tasks(diagram)
|
|
60
|
+
task_id = 0
|
|
61
|
+
nodes = []
|
|
62
|
+
|
|
63
|
+
diagram.sections.each_with_index do |section, section_idx|
|
|
64
|
+
section.tasks.each do |task|
|
|
65
|
+
dims = calculate_task_dimensions(task)
|
|
66
|
+
|
|
67
|
+
nodes << {
|
|
68
|
+
id: "task_#{task_id}",
|
|
69
|
+
width: dims[:width],
|
|
70
|
+
height: dims[:height],
|
|
71
|
+
labels: task_labels(task),
|
|
72
|
+
metadata: {
|
|
73
|
+
name: task.name,
|
|
74
|
+
score: task.score,
|
|
75
|
+
score_color: task.score_color,
|
|
76
|
+
actors: task.actors,
|
|
77
|
+
section_name: section.name,
|
|
78
|
+
section_index: section_idx
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
task_id += 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
nodes
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def transform_task_flow(diagram)
|
|
90
|
+
# Create sequential edges between tasks
|
|
91
|
+
edges = []
|
|
92
|
+
all_tasks = diagram.all_tasks
|
|
93
|
+
task_id = 0
|
|
94
|
+
|
|
95
|
+
all_tasks.each_with_index do |_task, idx|
|
|
96
|
+
next if idx >= all_tasks.length - 1
|
|
97
|
+
|
|
98
|
+
edges << {
|
|
99
|
+
id: "flow_#{task_id}",
|
|
100
|
+
sources: ["task_#{task_id}"],
|
|
101
|
+
targets: ["task_#{task_id + 1}"],
|
|
102
|
+
metadata: {
|
|
103
|
+
type: 'sequence'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
task_id += 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
edges
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def calculate_task_dimensions(task)
|
|
114
|
+
# Calculate width based on task name and actors
|
|
115
|
+
max_width = MIN_TASK_WIDTH
|
|
116
|
+
|
|
117
|
+
# Check task name width
|
|
118
|
+
name_width = measure_text(
|
|
119
|
+
task.name,
|
|
120
|
+
font_size: DEFAULT_FONT_SIZE + 2
|
|
121
|
+
)[:width]
|
|
122
|
+
max_width = [max_width, name_width].max
|
|
123
|
+
|
|
124
|
+
# Check actors width (displayed as comma-separated list)
|
|
125
|
+
actors_text = task.actors.join(', ')
|
|
126
|
+
actors_width = measure_text(
|
|
127
|
+
actors_text,
|
|
128
|
+
font_size: DEFAULT_FONT_SIZE
|
|
129
|
+
)[:width]
|
|
130
|
+
max_width = [max_width, actors_width].max
|
|
131
|
+
|
|
132
|
+
# Add padding
|
|
133
|
+
total_width = max_width + (TASK_PADDING * 2)
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
width: total_width,
|
|
137
|
+
height: TASK_HEIGHT
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def task_labels(task)
|
|
142
|
+
labels = []
|
|
143
|
+
|
|
144
|
+
# Task name label
|
|
145
|
+
name_dims = measure_text(
|
|
146
|
+
task.name,
|
|
147
|
+
font_size: DEFAULT_FONT_SIZE + 2
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
labels << {
|
|
151
|
+
text: task.name,
|
|
152
|
+
width: name_dims[:width],
|
|
153
|
+
height: name_dims[:height],
|
|
154
|
+
position: :top
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Score label
|
|
158
|
+
score_text = task.score.to_s
|
|
159
|
+
score_dims = measure_text(
|
|
160
|
+
score_text,
|
|
161
|
+
font_size: DEFAULT_FONT_SIZE + 4
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
labels << {
|
|
165
|
+
text: score_text,
|
|
166
|
+
width: score_dims[:width],
|
|
167
|
+
height: score_dims[:height],
|
|
168
|
+
position: :center
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Actors label
|
|
172
|
+
actors_text = task.actors.join(', ')
|
|
173
|
+
actors_dims = measure_text(
|
|
174
|
+
actors_text,
|
|
175
|
+
font_size: DEFAULT_FONT_SIZE - 2
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
labels << {
|
|
179
|
+
text: actors_text,
|
|
180
|
+
width: actors_dims[:width],
|
|
181
|
+
height: actors_dims[:height],
|
|
182
|
+
position: :bottom
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
labels
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def layout_options
|
|
189
|
+
# User journeys use layered algorithm for sequential task flow
|
|
190
|
+
# DIRECTION_RIGHT provides left-to-right horizontal timeline
|
|
191
|
+
# SIMPLE node placement maintains task order in journey sequence
|
|
192
|
+
build_elk_options(
|
|
193
|
+
algorithm: ALGORITHM_LAYERED,
|
|
194
|
+
direction: DIRECTION_RIGHT,
|
|
195
|
+
ElkOptions::NODE_NODE_SPACING => TASK_SPACING,
|
|
196
|
+
ElkOptions::LAYER_SPACING => TASK_SPACING,
|
|
197
|
+
ElkOptions::EDGE_NODE_SPACING => 30,
|
|
198
|
+
ElkOptions::EDGE_EDGE_SPACING => 20,
|
|
199
|
+
# SIMPLE node placement for chronological task ordering
|
|
200
|
+
ElkOptions::NODE_PLACEMENT => 'SIMPLE',
|
|
201
|
+
ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES',
|
|
202
|
+
ElkOptions::HIERARCHY_HANDLING => 'INCLUDE_CHILDREN'
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|