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,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../svg/document'
|
|
5
|
+
require_relative '../svg/rect'
|
|
6
|
+
require_relative '../svg/line'
|
|
7
|
+
require_relative '../svg/circle'
|
|
8
|
+
require_relative '../svg/text'
|
|
9
|
+
require_relative '../svg/group'
|
|
10
|
+
|
|
11
|
+
module Sirena
|
|
12
|
+
module Renderer
|
|
13
|
+
# Quadrant chart renderer for converting quadrant diagrams to SVG.
|
|
14
|
+
#
|
|
15
|
+
# Converts a QuadrantChart diagram model into SVG with a 2x2 grid,
|
|
16
|
+
# axis labels, quadrant labels, and data points.
|
|
17
|
+
#
|
|
18
|
+
# @example Render a quadrant chart
|
|
19
|
+
# renderer = QuadrantRenderer.new
|
|
20
|
+
# svg = renderer.render(quadrant_graph)
|
|
21
|
+
class QuadrantRenderer < Base
|
|
22
|
+
# Quadrant colors (can be overridden by theme)
|
|
23
|
+
QUADRANT_COLORS = {
|
|
24
|
+
1 => '#e3f2fd', # Light blue
|
|
25
|
+
2 => '#fff3e0', # Light orange
|
|
26
|
+
3 => '#f3e5f5', # Light purple
|
|
27
|
+
4 => '#e8f5e9' # Light green
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Renders a quadrant chart diagram to SVG.
|
|
31
|
+
#
|
|
32
|
+
# @param graph [Hash] the quadrant chart graph structure from transform
|
|
33
|
+
# @return [Svg::Document] the rendered SVG document
|
|
34
|
+
def render(graph)
|
|
35
|
+
svg = create_document(graph)
|
|
36
|
+
|
|
37
|
+
# Render title if present
|
|
38
|
+
render_title(graph, svg) if graph[:title]
|
|
39
|
+
|
|
40
|
+
# Render quadrant grid
|
|
41
|
+
render_quadrants(graph, svg)
|
|
42
|
+
|
|
43
|
+
# Render axes
|
|
44
|
+
render_axes(graph, svg)
|
|
45
|
+
|
|
46
|
+
# Render axis labels
|
|
47
|
+
render_axis_labels(graph, svg)
|
|
48
|
+
|
|
49
|
+
# Render quadrant labels
|
|
50
|
+
render_quadrant_labels(graph, svg)
|
|
51
|
+
|
|
52
|
+
# Render data points
|
|
53
|
+
render_points(graph, svg)
|
|
54
|
+
|
|
55
|
+
svg
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
protected
|
|
59
|
+
|
|
60
|
+
def create_document(graph)
|
|
61
|
+
dims = graph[:dimensions]
|
|
62
|
+
|
|
63
|
+
Svg::Document.new.tap do |doc|
|
|
64
|
+
doc.width = dims[:width]
|
|
65
|
+
doc.height = dims[:height]
|
|
66
|
+
doc.view_box = "0 0 #{dims[:width]} #{dims[:height]}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_title(graph, svg)
|
|
71
|
+
dims = graph[:dimensions]
|
|
72
|
+
|
|
73
|
+
title_text = Svg::Text.new.tap do |t|
|
|
74
|
+
t.x = dims[:width] / 2
|
|
75
|
+
t.y = 30
|
|
76
|
+
t.content = graph[:title]
|
|
77
|
+
t.fill = theme_color(:label_text) || '#000000'
|
|
78
|
+
t.font_family = theme_typography(:font_family) ||
|
|
79
|
+
'Arial, sans-serif'
|
|
80
|
+
t.font_size = (theme_typography(:font_size_large) || 18).to_s
|
|
81
|
+
t.text_anchor = 'middle'
|
|
82
|
+
t.font_weight = 'bold'
|
|
83
|
+
end
|
|
84
|
+
svg << title_text
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_quadrants(graph, svg)
|
|
88
|
+
quadrants = graph[:quadrants]
|
|
89
|
+
|
|
90
|
+
# Render each quadrant background
|
|
91
|
+
quadrants.each do |_key, quadrant|
|
|
92
|
+
bounds = quadrant[:bounds]
|
|
93
|
+
color = get_quadrant_color(quadrant[:number])
|
|
94
|
+
|
|
95
|
+
rect = Svg::Rect.new.tap do |r|
|
|
96
|
+
r.x = bounds[:x]
|
|
97
|
+
r.y = bounds[:y]
|
|
98
|
+
r.width = bounds[:width]
|
|
99
|
+
r.height = bounds[:height]
|
|
100
|
+
r.fill = color
|
|
101
|
+
r.stroke = theme_color(:grid_line) || '#cccccc'
|
|
102
|
+
r.stroke_width = '1'
|
|
103
|
+
r.opacity = '0.3'
|
|
104
|
+
end
|
|
105
|
+
svg << rect
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_axes(graph, svg)
|
|
110
|
+
dims = graph[:dimensions]
|
|
111
|
+
|
|
112
|
+
# Vertical axis (center)
|
|
113
|
+
center_x = dims[:chart_x] + (dims[:chart_width] / 2)
|
|
114
|
+
vertical_line = Svg::Line.new.tap do |line|
|
|
115
|
+
line.x1 = center_x
|
|
116
|
+
line.y1 = dims[:chart_y]
|
|
117
|
+
line.x2 = center_x
|
|
118
|
+
line.y2 = dims[:chart_y] + dims[:chart_height]
|
|
119
|
+
line.stroke = theme_color(:grid_line) || '#666666'
|
|
120
|
+
line.stroke_width = '2'
|
|
121
|
+
end
|
|
122
|
+
svg << vertical_line
|
|
123
|
+
|
|
124
|
+
# Horizontal axis (center)
|
|
125
|
+
center_y = dims[:chart_y] + (dims[:chart_height] / 2)
|
|
126
|
+
horizontal_line = Svg::Line.new.tap do |line|
|
|
127
|
+
line.x1 = dims[:chart_x]
|
|
128
|
+
line.y1 = center_y
|
|
129
|
+
line.x2 = dims[:chart_x] + dims[:chart_width]
|
|
130
|
+
line.y2 = center_y
|
|
131
|
+
line.stroke = theme_color(:grid_line) || '#666666'
|
|
132
|
+
line.stroke_width = '2'
|
|
133
|
+
end
|
|
134
|
+
svg << horizontal_line
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def render_axis_labels(graph, svg)
|
|
138
|
+
dims = graph[:dimensions]
|
|
139
|
+
axes = graph[:axes]
|
|
140
|
+
|
|
141
|
+
# X-axis left label
|
|
142
|
+
render_axis_label(
|
|
143
|
+
axes[:x_left],
|
|
144
|
+
dims[:chart_x] - 10,
|
|
145
|
+
dims[:chart_y] + dims[:chart_height] + 30,
|
|
146
|
+
'end',
|
|
147
|
+
svg
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# X-axis right label
|
|
151
|
+
render_axis_label(
|
|
152
|
+
axes[:x_right],
|
|
153
|
+
dims[:chart_x] + dims[:chart_width] + 10,
|
|
154
|
+
dims[:chart_y] + dims[:chart_height] + 30,
|
|
155
|
+
'start',
|
|
156
|
+
svg
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Y-axis bottom label
|
|
160
|
+
render_axis_label(
|
|
161
|
+
axes[:y_bottom],
|
|
162
|
+
dims[:chart_x] - 30,
|
|
163
|
+
dims[:chart_y] + dims[:chart_height] + 10,
|
|
164
|
+
'middle',
|
|
165
|
+
svg
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Y-axis top label
|
|
169
|
+
render_axis_label(
|
|
170
|
+
axes[:y_top],
|
|
171
|
+
dims[:chart_x] - 30,
|
|
172
|
+
dims[:chart_y] - 10,
|
|
173
|
+
'middle',
|
|
174
|
+
svg
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def render_axis_label(text, x, y, anchor, svg)
|
|
179
|
+
return unless text
|
|
180
|
+
|
|
181
|
+
label = Svg::Text.new.tap do |t|
|
|
182
|
+
t.x = x
|
|
183
|
+
t.y = y
|
|
184
|
+
t.content = text
|
|
185
|
+
t.fill = theme_color(:label_text) || '#666666'
|
|
186
|
+
t.font_family = theme_typography(:font_family) ||
|
|
187
|
+
'Arial, sans-serif'
|
|
188
|
+
t.font_size = (theme_typography(:font_size_small) || 12).to_s
|
|
189
|
+
t.text_anchor = anchor
|
|
190
|
+
end
|
|
191
|
+
svg << label
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def render_quadrant_labels(graph, svg)
|
|
195
|
+
quadrants = graph[:quadrants]
|
|
196
|
+
|
|
197
|
+
quadrants.each do |_key, quadrant|
|
|
198
|
+
next unless quadrant[:label]
|
|
199
|
+
|
|
200
|
+
bounds = quadrant[:bounds]
|
|
201
|
+
|
|
202
|
+
# Calculate center position for label
|
|
203
|
+
label_x = bounds[:x] + (bounds[:width] / 2)
|
|
204
|
+
label_y = bounds[:y] + 20
|
|
205
|
+
|
|
206
|
+
label = Svg::Text.new.tap do |t|
|
|
207
|
+
t.x = label_x
|
|
208
|
+
t.y = label_y
|
|
209
|
+
t.content = quadrant[:label]
|
|
210
|
+
t.fill = theme_color(:label_text) || '#333333'
|
|
211
|
+
t.font_family = theme_typography(:font_family) ||
|
|
212
|
+
'Arial, sans-serif'
|
|
213
|
+
t.font_size = (theme_typography(:font_size_normal) || 14).to_s
|
|
214
|
+
t.text_anchor = 'middle'
|
|
215
|
+
t.font_weight = 'bold'
|
|
216
|
+
t.opacity = '0.7'
|
|
217
|
+
end
|
|
218
|
+
svg << label
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def render_points(graph, svg)
|
|
223
|
+
points = graph[:points] || []
|
|
224
|
+
|
|
225
|
+
points.each do |point|
|
|
226
|
+
render_point(point, svg)
|
|
227
|
+
render_point_label(point, svg)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def render_point(point, svg)
|
|
232
|
+
radius = point[:radius] || 6
|
|
233
|
+
color = point[:color] || get_point_color(point[:quadrant])
|
|
234
|
+
stroke_color = point[:stroke_color] ||
|
|
235
|
+
theme_color(:node_stroke) ||
|
|
236
|
+
'#ffffff'
|
|
237
|
+
stroke_width = point[:stroke_width] || 2
|
|
238
|
+
|
|
239
|
+
circle = Svg::Circle.new.tap do |c|
|
|
240
|
+
c.cx = point[:svg_x]
|
|
241
|
+
c.cy = point[:svg_y]
|
|
242
|
+
c.r = radius
|
|
243
|
+
c.fill = color
|
|
244
|
+
c.stroke = stroke_color
|
|
245
|
+
c.stroke_width = stroke_width.to_s
|
|
246
|
+
c.id = point[:id]
|
|
247
|
+
end
|
|
248
|
+
svg << circle
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def render_point_label(point, svg)
|
|
252
|
+
# Position label slightly above and to the right of point
|
|
253
|
+
label_x = point[:svg_x] + 10
|
|
254
|
+
label_y = point[:svg_y] - 10
|
|
255
|
+
|
|
256
|
+
label = Svg::Text.new.tap do |t|
|
|
257
|
+
t.x = label_x
|
|
258
|
+
t.y = label_y
|
|
259
|
+
t.content = point[:label]
|
|
260
|
+
t.fill = theme_color(:label_text) || '#000000'
|
|
261
|
+
t.font_family = theme_typography(:font_family) ||
|
|
262
|
+
'Arial, sans-serif'
|
|
263
|
+
t.font_size = (theme_typography(:font_size_small) || 11).to_s
|
|
264
|
+
t.text_anchor = 'start'
|
|
265
|
+
end
|
|
266
|
+
svg << label
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def get_quadrant_color(quadrant_number)
|
|
270
|
+
# Try to get from theme first
|
|
271
|
+
color_key = "quadrant_#{quadrant_number}".to_sym
|
|
272
|
+
theme_color(color_key) || QUADRANT_COLORS[quadrant_number]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def get_point_color(quadrant_number)
|
|
276
|
+
# Use darker version of quadrant color for points
|
|
277
|
+
case quadrant_number
|
|
278
|
+
when 1
|
|
279
|
+
theme_color(:primary) || '#2196f3'
|
|
280
|
+
when 2
|
|
281
|
+
theme_color(:secondary) || '#ff9800'
|
|
282
|
+
when 3
|
|
283
|
+
theme_color(:accent) || '#9c27b0'
|
|
284
|
+
when 4
|
|
285
|
+
theme_color(:success) || '#4caf50'
|
|
286
|
+
else
|
|
287
|
+
theme_color(:primary) || '#2196f3'
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../svg/document"
|
|
4
|
+
require_relative "../svg/circle"
|
|
5
|
+
require_relative "../svg/line"
|
|
6
|
+
require_relative "../svg/polygon"
|
|
7
|
+
require_relative "../svg/text"
|
|
8
|
+
require_relative "../svg/group"
|
|
9
|
+
|
|
10
|
+
module Sirena
|
|
11
|
+
module Renderer
|
|
12
|
+
# Renders a Radar Chart layout to SVG.
|
|
13
|
+
#
|
|
14
|
+
# The renderer converts the positioned layout structure from
|
|
15
|
+
# Transform::Radar into an SVG visualization showing:
|
|
16
|
+
# - Circular or polygonal grid
|
|
17
|
+
# - Radial axes from center
|
|
18
|
+
# - Axis labels
|
|
19
|
+
# - Data polygons for each curve/dataset
|
|
20
|
+
# - Legend
|
|
21
|
+
#
|
|
22
|
+
# @example Render a radar chart
|
|
23
|
+
# renderer = Renderer::Radar.new(theme: my_theme)
|
|
24
|
+
# svg = renderer.render(layout)
|
|
25
|
+
class Radar < Base
|
|
26
|
+
# Default colors for datasets (cycling)
|
|
27
|
+
DEFAULT_COLORS = %w[
|
|
28
|
+
#2563eb #7c3aed #db2777 #ea580c #ca8a04
|
|
29
|
+
#16a34a #0891b2 #4f46e5 #c026d3 #dc2626
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# Renders the layout structure to SVG.
|
|
33
|
+
#
|
|
34
|
+
# @param layout [Hash] layout data from Transform::Radar
|
|
35
|
+
# @return [Svg::Document] rendered SVG document
|
|
36
|
+
def render(layout)
|
|
37
|
+
svg = create_document_from_layout(layout)
|
|
38
|
+
|
|
39
|
+
# Store center and radius for rendering
|
|
40
|
+
@center_x = layout[:center_x]
|
|
41
|
+
@center_y = layout[:center_y]
|
|
42
|
+
@radius = layout[:radius]
|
|
43
|
+
|
|
44
|
+
# Render in order: grid, axes, curves, labels, legend
|
|
45
|
+
render_grid(layout, svg)
|
|
46
|
+
render_axes(layout, svg)
|
|
47
|
+
render_curves(layout, svg)
|
|
48
|
+
render_axis_labels(layout, svg)
|
|
49
|
+
render_legend(layout, svg) if should_show_legend?(layout)
|
|
50
|
+
|
|
51
|
+
svg
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# Creates an SVG document with dimensions from layout.
|
|
57
|
+
#
|
|
58
|
+
# @param layout [Hash] layout data
|
|
59
|
+
# @return [Svg::Document] new SVG document
|
|
60
|
+
def create_document_from_layout(layout)
|
|
61
|
+
Svg::Document.new.tap do |doc|
|
|
62
|
+
doc.width = layout[:width]
|
|
63
|
+
doc.height = layout[:height]
|
|
64
|
+
doc.view_box = "0 0 #{doc.width} #{doc.height}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Determines if legend should be shown.
|
|
69
|
+
#
|
|
70
|
+
# @param layout [Hash] layout data
|
|
71
|
+
# @return [Boolean] true if legend should be shown
|
|
72
|
+
def should_show_legend?(layout)
|
|
73
|
+
# Check diagram options, default to true
|
|
74
|
+
layout.dig(:options, :show_legend) != false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Renders the grid (circular or polygonal).
|
|
78
|
+
#
|
|
79
|
+
# @param layout [Hash] layout data
|
|
80
|
+
# @param svg [Svg::Document] SVG document
|
|
81
|
+
# @return [void]
|
|
82
|
+
def render_grid(layout, svg)
|
|
83
|
+
layout[:grid_circles].each do |grid|
|
|
84
|
+
render_grid_circle(grid, svg)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Renders a single grid circle.
|
|
89
|
+
#
|
|
90
|
+
# @param grid [Hash] grid circle data
|
|
91
|
+
# @param svg [Svg::Document] SVG document
|
|
92
|
+
# @return [void]
|
|
93
|
+
def render_grid_circle(grid, svg)
|
|
94
|
+
circle = Svg::Circle.new.tap do |c|
|
|
95
|
+
c.cx = @center_x
|
|
96
|
+
c.cy = @center_y
|
|
97
|
+
c.r = grid[:radius]
|
|
98
|
+
c.fill = "none"
|
|
99
|
+
c.stroke = theme_color(:grid_line) || "#e5e7eb"
|
|
100
|
+
c.stroke_width = "1"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
svg.add_element(circle)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Renders all axes radiating from center.
|
|
107
|
+
#
|
|
108
|
+
# @param layout [Hash] layout data
|
|
109
|
+
# @param svg [Svg::Document] SVG document
|
|
110
|
+
# @return [void]
|
|
111
|
+
def render_axes(layout, svg)
|
|
112
|
+
layout[:axes].each do |axis|
|
|
113
|
+
render_axis_line(axis, svg)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Renders a single axis line.
|
|
118
|
+
#
|
|
119
|
+
# @param axis [Hash] axis data
|
|
120
|
+
# @param svg [Svg::Document] SVG document
|
|
121
|
+
# @return [void]
|
|
122
|
+
def render_axis_line(axis, svg)
|
|
123
|
+
line = Svg::Line.new.tap do |l|
|
|
124
|
+
l.x1 = @center_x
|
|
125
|
+
l.y1 = @center_y
|
|
126
|
+
l.x2 = @center_x + axis[:end_x]
|
|
127
|
+
l.y2 = @center_y + axis[:end_y]
|
|
128
|
+
l.stroke = theme_color(:axis_line) || "#9ca3af"
|
|
129
|
+
l.stroke_width = "1"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
svg.add_element(line)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Renders axis labels.
|
|
136
|
+
#
|
|
137
|
+
# @param layout [Hash] layout data
|
|
138
|
+
# @param svg [Svg::Document] SVG document
|
|
139
|
+
# @return [void]
|
|
140
|
+
def render_axis_labels(layout, svg)
|
|
141
|
+
layout[:axes].each do |axis|
|
|
142
|
+
render_axis_label(axis, svg)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Renders a single axis label.
|
|
147
|
+
#
|
|
148
|
+
# @param axis [Hash] axis data
|
|
149
|
+
# @param svg [Svg::Document] SVG document
|
|
150
|
+
# @return [void]
|
|
151
|
+
def render_axis_label(axis, svg)
|
|
152
|
+
x = @center_x + axis[:label_x]
|
|
153
|
+
y = @center_y + axis[:label_y]
|
|
154
|
+
|
|
155
|
+
text = Svg::Text.new.tap do |t|
|
|
156
|
+
t.x = x
|
|
157
|
+
t.y = y
|
|
158
|
+
t.text_anchor = calculate_text_anchor(axis[:angle_degrees])
|
|
159
|
+
t.dominant_baseline = calculate_dominant_baseline(axis[:angle_degrees])
|
|
160
|
+
t.fill = theme_color(:label_text) || "#000000"
|
|
161
|
+
t.font_size = (theme_typography(:font_size_normal) || 12).to_s
|
|
162
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
163
|
+
t.font_weight = "bold"
|
|
164
|
+
t.content = axis[:label]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
svg.add_element(text)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Calculates text anchor based on angle.
|
|
171
|
+
#
|
|
172
|
+
# @param angle [Numeric] angle in degrees
|
|
173
|
+
# @return [String] text anchor value
|
|
174
|
+
def calculate_text_anchor(angle)
|
|
175
|
+
# Normalize angle to 0-360
|
|
176
|
+
normalized = angle % 360
|
|
177
|
+
normalized += 360 if normalized < 0
|
|
178
|
+
|
|
179
|
+
if normalized > 45 && normalized < 135
|
|
180
|
+
"start"
|
|
181
|
+
elsif normalized > 225 && normalized < 315
|
|
182
|
+
"end"
|
|
183
|
+
else
|
|
184
|
+
"middle"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Calculates dominant baseline based on angle.
|
|
189
|
+
#
|
|
190
|
+
# @param angle [Numeric] angle in degrees
|
|
191
|
+
# @return [String] dominant baseline value
|
|
192
|
+
def calculate_dominant_baseline(angle)
|
|
193
|
+
# Normalize angle to 0-360
|
|
194
|
+
normalized = angle % 360
|
|
195
|
+
normalized += 360 if normalized < 0
|
|
196
|
+
|
|
197
|
+
if normalized > 135 && normalized < 225
|
|
198
|
+
"hanging"
|
|
199
|
+
elsif normalized < 45 || normalized > 315
|
|
200
|
+
"auto"
|
|
201
|
+
else
|
|
202
|
+
"middle"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Renders all data curves as polygons.
|
|
207
|
+
#
|
|
208
|
+
# @param layout [Hash] layout data
|
|
209
|
+
# @param svg [Svg::Document] SVG document
|
|
210
|
+
# @return [void]
|
|
211
|
+
def render_curves(layout, svg)
|
|
212
|
+
layout[:curves].each_with_index do |curve, idx|
|
|
213
|
+
color = get_curve_color(idx)
|
|
214
|
+
render_curve_polygon(curve, color, svg)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Gets color for a curve based on index.
|
|
219
|
+
#
|
|
220
|
+
# @param index [Integer] curve index
|
|
221
|
+
# @return [String] color value
|
|
222
|
+
def get_curve_color(index)
|
|
223
|
+
DEFAULT_COLORS[index % DEFAULT_COLORS.length]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Renders a single curve as a polygon.
|
|
227
|
+
#
|
|
228
|
+
# @param curve [Hash] curve data
|
|
229
|
+
# @param color [String] fill color
|
|
230
|
+
# @param svg [Svg::Document] SVG document
|
|
231
|
+
# @return [void]
|
|
232
|
+
def render_curve_polygon(curve, color, svg)
|
|
233
|
+
return if curve[:points].empty?
|
|
234
|
+
|
|
235
|
+
# Build points array for polygon
|
|
236
|
+
points = curve[:points].map do |point|
|
|
237
|
+
x = @center_x + point[:x]
|
|
238
|
+
y = @center_y + point[:y]
|
|
239
|
+
"#{x},#{y}"
|
|
240
|
+
end.join(" ")
|
|
241
|
+
|
|
242
|
+
polygon = Svg::Polygon.new.tap do |p|
|
|
243
|
+
p.points = points
|
|
244
|
+
p.fill = color
|
|
245
|
+
p.fill_opacity = "0.3"
|
|
246
|
+
p.stroke = color
|
|
247
|
+
p.stroke_width = "2"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
svg.add_element(polygon)
|
|
251
|
+
|
|
252
|
+
# Render data points as small circles
|
|
253
|
+
render_curve_points(curve, color, svg)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Renders data points for a curve.
|
|
257
|
+
#
|
|
258
|
+
# @param curve [Hash] curve data
|
|
259
|
+
# @param color [String] point color
|
|
260
|
+
# @param svg [Svg::Document] SVG document
|
|
261
|
+
# @return [void]
|
|
262
|
+
def render_curve_points(curve, color, svg)
|
|
263
|
+
curve[:points].each do |point|
|
|
264
|
+
x = @center_x + point[:x]
|
|
265
|
+
y = @center_y + point[:y]
|
|
266
|
+
|
|
267
|
+
circle = Svg::Circle.new.tap do |c|
|
|
268
|
+
c.cx = x
|
|
269
|
+
c.cy = y
|
|
270
|
+
c.r = 3
|
|
271
|
+
c.fill = color
|
|
272
|
+
c.stroke = theme_color(:background) || "#ffffff"
|
|
273
|
+
c.stroke_width = "1"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
svg.add_element(circle)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Renders legend for all curves.
|
|
281
|
+
#
|
|
282
|
+
# @param layout [Hash] layout data
|
|
283
|
+
# @param svg [Svg::Document] SVG document
|
|
284
|
+
# @return [void]
|
|
285
|
+
def render_legend(layout, svg)
|
|
286
|
+
return if layout[:curves].empty?
|
|
287
|
+
|
|
288
|
+
legend_x = 20
|
|
289
|
+
legend_y = layout[:height] - 40
|
|
290
|
+
item_height = 20
|
|
291
|
+
|
|
292
|
+
layout[:curves].each_with_index do |curve, idx|
|
|
293
|
+
color = get_curve_color(idx)
|
|
294
|
+
y_offset = idx * item_height
|
|
295
|
+
|
|
296
|
+
# Color box
|
|
297
|
+
box = Svg::Circle.new.tap do |c|
|
|
298
|
+
c.cx = legend_x
|
|
299
|
+
c.cy = legend_y + y_offset
|
|
300
|
+
c.r = 5
|
|
301
|
+
c.fill = color
|
|
302
|
+
c.stroke = "none"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
svg.add_element(box)
|
|
306
|
+
|
|
307
|
+
# Label
|
|
308
|
+
text = Svg::Text.new.tap do |t|
|
|
309
|
+
t.x = legend_x + 15
|
|
310
|
+
t.y = legend_y + y_offset + 4
|
|
311
|
+
t.text_anchor = "start"
|
|
312
|
+
t.fill = theme_color(:label_text) || "#000000"
|
|
313
|
+
t.font_size = (theme_typography(:font_size_small) || 10).to_s
|
|
314
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
315
|
+
t.content = curve[:label]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
svg.add_element(text)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|