Compare commits

..

141 Commits

Author SHA1 Message Date
autofix-ci[bot]
4b63214a72 [autofix.ci] apply automated fixes 2025-08-26 09:38:09 +00:00
darshanr0107
4937ebc058 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into knsv/mindmap-refactoring 2025-08-26 15:01:48 +05:30
Sidharth Vinod
00f5700320 Merge pull request #6898 from mermaid-js/user-defined-layout-detection
feat: Add getUserDefinedConfig to merge init config and directives
2025-08-26 09:17:40 +00:00
darshanr0107
50127f3ffe fix: review comments related to getUserDefinedConfig tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-26 13:17:38 +05:30
Sidharth Vinod
29bb0e3dca Merge pull request #6900 from mermaid-js/update-timings
Update E2E Timings
2025-08-26 12:46:05 +05:30
Shubham P
1221de4c2d Merge pull request #6896 from mermaid-js/renovate/peter-evans-create-pull-request-digest
chore(deps): update peter-evans/create-pull-request digest to 18e4695
2025-08-26 06:38:36 +00:00
github-actions[bot]
c41e08cb7a chore: update E2E timings 2025-08-26 04:13:51 +00:00
darshanr0107
4760ed8893 fix: add unit tests for getUserDefinedConfig across different scenarios
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 19:13:58 +05:30
darshanr0107
31ecf31c2e refactor: remove layout-specific checks and create generic function
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 18:26:03 +05:30
renovate[bot]
b52766653c chore(deps): update peter-evans/create-pull-request digest to 18e4695 2025-08-25 11:22:30 +00:00
Sidharth Vinod
6d9fad01a9 Merge pull request #6887 from mermaid-js/docs/flowchart-image-node-doc-fix
docs(flowchart): fix image node documentation
2025-08-25 11:09:44 +00:00
darshanr0107
8322a63598 feat: add helper to differentiate user-defined layout from default
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 16:17:04 +05:30
omkarht
075e1b5e1f Merge branch 'develop' into docs/flowchart-image-node-doc-fix 2025-08-25 15:47:23 +05:30
omkarht
3c9bd7be29 Merge branch 'develop' into docs/flowchart-image-node-doc-fix 2025-08-25 12:58:52 +05:30
Shubham P
6995248443 Merge pull request #6704 from mermaid-js/6637-add-new-participant-types-to-sequence-diagrams
6637:Add support for new participant types in sequence diagrams
2025-08-25 07:28:44 +00:00
omkarht
93467a6fce fix: removed changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-25 12:58:17 +05:30
omkarht
95d48e3497 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-25 12:42:03 +05:30
darshanr0107
e0a5a2489d fix: tidy tree layout tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-22 19:19:19 +05:30
darshanr0107
bd400a5130 chore: cleanup unused parameters , reuse existing types
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-22 19:09:38 +05:30
darshanr0107
d35f84f337 chore: fix review feedback in tidy-tree and mindmap (types, cleanup)
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-22 18:15:17 +05:30
darshanr0107
8dadb853a0 fix: failing unit test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-22 12:36:56 +05:30
Shubham P
29886b8dd4 Merge pull request #6886 from mermaid-js/6721/Bidirectional-arrows--render-incorrectly-with-autonumber-in-sequence-diagrams
6721: Correct rendering of bidirectional arrows with auto number
2025-08-21 15:57:22 +00:00
omkarht
e438e035bc chore: added changeset 2025-08-21 19:34:27 +05:30
Alois Klink
2bc5b6d2fa docs(flowchart): fix image node documentation 2025-08-21 19:26:54 +05:30
darshanr0107
e0b45c2d2b chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-21 19:15:30 +05:30
darshanr0107
d4c76968e9 fix: correct rendering of bidirectional arrows with autonumber
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-21 19:09:39 +05:30
Knut Sveidqvist
a700e8bf97 Removing unused file 2025-08-21 13:02:03 +02:00
omkarht
2cfebef122 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-21 13:13:41 +05:30
darshanr0107
61d9143acb fix: cose-bilkent as the default layout for mindmaps
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 21:00:27 +05:30
darshanr0107
c88f74a6ee fix: update tidy tree test cases
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 17:44:44 +05:30
darshanr0107
6377d6f64d fix: increase padding for circle shape in mindmap
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 16:28:55 +05:30
darshanr0107
1b0bc05fc2 fix: edge issue in tidy tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 15:54:44 +05:30
darshanr0107
45edeb9307 fix: undo changes in tidy tree layout file that were breaking CI
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 15:08:48 +05:30
darshanr0107
211974b2b7 chore: remove disallowed vitepress path and regenerate lockfile
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 14:28:06 +05:30
darshanr0107
1f5ad3e315 fix: build issues
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 14:07:00 +05:30
darshanr0107
d7848e8a3d fix: add visual tests for tidy tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 13:56:08 +05:30
darshanr0107
89b9f0df70 Merge branch 'knsv/mindmap-refactoring' of https://github.com/mermaid-js/mermaid into knsv/mindmap-refactoring 2025-08-20 12:01:44 +05:30
darshanr0107
e9011567bd fix: edge intersection issue in tidy tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 12:01:10 +05:30
Sidharth Vinod
7171237b96 Merge pull request #6865 from mermaid-js/renovate/peter-evans-create-pull-request-digest
chore(deps): update peter-evans/create-pull-request digest to cb4d3bf
2025-08-20 00:07:42 +05:30
autofix-ci[bot]
0429970d58 [autofix.ci] apply automated fixes 2025-08-19 13:43:51 +00:00
darshanr0107
ecad9cee6c fix: label styles in bang shape
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-19 19:08:57 +05:30
Shubham P
066883f4cd Merge branch 'develop' into renovate/peter-evans-create-pull-request-digest 2025-08-19 19:01:50 +05:30
darshanr0107
1e8a9f76a9 fix: padding issues in bang, rounded rect, sqaure shapes
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-19 18:56:00 +05:30
autofix-ci[bot]
e42fdf1c54 [autofix.ci] apply automated fixes 2025-08-19 08:34:28 +00:00
darshanr0107
c75566ddc3 fix: adjust rounded rectangle styling to resolve Argos snapshot failure
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-19 13:59:20 +05:30
omkarht
7bdcf93412 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-19 13:54:32 +05:30
omkarht
d86e46b705 fix: refactored test cases
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-19 13:44:11 +05:30
omkarht
71e09bcaef fix: refactored code and adjusted shape, shape label
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-18 18:21:41 +05:30
autofix-ci[bot]
c534d3d364 [autofix.ci] apply automated fixes 2025-08-18 11:46:12 +00:00
renovate[bot]
4db72f5357 chore(deps): update peter-evans/create-pull-request digest to cb4d3bf 2025-08-18 11:31:20 +00:00
darshanr0107
7e9577dffd fix: remove border for mindmap rounded rect shape
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-18 13:16:35 +05:30
omkarht
cba659d097 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-18 12:16:44 +05:30
darshanr0107
180dc7bdff Merge branch 'knsv/mindmap-refactoring' of https://github.com/mermaid-js/mermaid into knsv/mindmap-refactoring 2025-08-14 18:21:38 +05:30
darshanr0107
cc9581842d fix: increase the radius and remove border for rounded rect
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 18:21:17 +05:30
autofix-ci[bot]
d782e4bb17 [autofix.ci] apply automated fixes 2025-08-14 11:07:17 +00:00
darshanr0107
ba9ad9385b fix: shape for default mindmap node
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 16:31:48 +05:30
darshanr0107
91edfa40f7 fix: organize imports and remove render.d.ts file
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 13:20:27 +05:30
darshanr0107
c8b00bb929 fix: revert the curve setting for edges
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-14 13:13:48 +05:30
darshanr0107
57eadbf6af docs: update mindmaps docs and move instructions under configuration and deployment
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-13 19:31:10 +05:30
darshanr0107
a906adce26 Merge branch 'develop' into knsv/mindmap-refactoring 2025-08-13 14:07:02 +05:30
darshanr0107
a6d26ef6c3 fix: remove cose-bilkent algorithm from tiny
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-12 18:38:22 +05:30
darshanr0107
2b3f94eb7d docs: update mindmap docs to specify tidy tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-12 16:35:43 +05:30
omkarht
9f6ee53382 fix: refactored parsing logic for new syntax
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-12 14:20:47 +05:30
omkarht
3248bf3da4 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-12 13:01:18 +05:30
darshanr0107
dd36046e23 docs: update tidy-tree Readme
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-11 14:22:08 +05:30
darshanr0107
1507435e15 chore: remove unwanted logs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-11 13:51:10 +05:30
omkarht
e7a7ff8a2a fix: refactored code
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-11 13:20:49 +05:30
omkarht
68fc68c239 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-11 13:15:44 +05:30
autofix-ci[bot]
769b362005 [autofix.ci] apply automated fixes 2025-08-08 14:53:47 +00:00
omkarht
e4d3aa4610 fix: refactored documentation, test cases and sequenceDiagram.jison file
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 20:18:21 +05:30
omkarht
716548548a fix: rendering test cases
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 19:31:56 +05:30
omkarht
4bece53a3c Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-08 16:08:54 +05:30
darshanr0107
68c01b76bf fix: failing unit tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 14:32:25 +05:30
darshanr0107
28717e108d chore: remove duplicate code and throw generic error if nodes are not present
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 13:52:22 +05:30
omkarht
297be4a868 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-08 13:40:38 +05:30
omkarht
fb6ace73b5 docs: updated documentation for new syntax
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 13:30:00 +05:30
darshanr0107
688d9b383d chore: update changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 13:16:46 +05:30
darshanr0107
e68424d748 fix: updated changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-08 12:45:16 +05:30
autofix-ci[bot]
bf362673fc [autofix.ci] apply automated fixes 2025-08-07 15:26:12 +00:00
omkarht
d042b21b12 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-07 20:47:08 +05:30
omkarht
677ff82d13 chore: implemented new syntx
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 20:45:23 +05:30
darshanr0107
204a9a338f fix: fix failing sequence diagram
unit test

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 18:54:45 +05:30
Knut Sveidqvist
981829a426 Updated order of lexer statements in the grammar 2025-08-07 15:21:13 +02:00
omkarht
327a5aa9fd chore: added logs in sequenceDiagram.jison
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 18:07:13 +05:30
darshanr0107
6a6a39ff33 chore: remove disallowed path and regenerate dependencies
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 18:00:10 +05:30
autofix-ci[bot]
b296db9a33 [autofix.ci] apply automated fixes 2025-08-07 11:39:39 +00:00
darshanr0107
01ce84d8ee fix: lint and build issues
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 17:04:01 +05:30
darshanr0107
f48e663d4c Merge branch 'develop' into knsv/mindmap-refactoring 2025-08-07 13:58:19 +05:30
darshanr0107
a4aa2bd355 fix: failing test case caused by invalid expected value
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 13:52:41 +05:30
darshanr0107
b51b9d50c2 fix: failing e2e for mindmaps
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-07 13:10:28 +05:30
omkarht
848f69a75c Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-08-06 17:14:45 +05:30
omkarht
99dbeba407 fix: refactored code
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 17:10:55 +05:30
omkarht
d525acc05b chore: Add documentation for the syntax of the newly added participant type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 17:08:57 +05:30
darshanr0107
b61780f735 fix: failing unit tests
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 16:35:54 +05:30
darshanr0107
074f18dfb8 fix: build issues
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-05 20:22:17 +05:30
darshanr0107
d7308b0f43 fix: build issues for cose - bilkent
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-05 19:16:02 +05:30
darshanr0107
2f1860386a fix: build for tidy-tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-05 15:35:42 +05:30
darshanr0107
f0bca7da55 Merge branch 'develop' into knsv/mindmap-refactoring 2025-08-04 20:07:15 +05:30
Sidharth Vinod
6fcdf5bfcc Merge pull request #6807 from mermaid-js/refactor/layout-algorithms-to-packages
Extract tidy-tree layout into separate package
2025-08-04 19:44:55 +05:30
darshanr0107
e2ce0450c1 fix:separate out tidy-tree and elk from deafult loaders
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-04 19:40:34 +05:30
darshanr0107
c95c64139d Decouple layout loaders from core package, register tidy-tree and elk loaders separately
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-04 14:25:08 +05:30
shubhamparikh2704
a7f12f1baa fix: fixed workflow
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-01 16:28:30 +05:30
darshanr0107
2a8653de2b move tidy-tree-layout to separate package
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-01 14:05:28 +05:30
darshanr0107
a92c3bb251 extract tidy-tree layout into separate package
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-01 13:36:35 +05:30
darshanr0107
3677abe9e5 Merge branch 'develop' into knsv/mindmap-refactoring 2025-07-30 19:53:22 +05:30
darshanr0107
95847ad236 clean up unused files, types, and commented code; remove unnecessary d3 mock test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-30 18:48:23 +05:30
darshanr0107
e0152fb873 fix: resolve duplicate keys in lockfile and fix test failures
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-30 15:33:03 +05:30
darshanr0107
2298b96d8e Merge branch 'develop' into knsv/mindmap-refactoring 2025-07-30 15:07:32 +05:30
darshanr0107
5db83365b6 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into knsv/mindmap-refactoring 2025-07-30 14:59:39 +05:30
omkarht
4915545429 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-07-29 16:26:31 +05:30
darshanr0107
341a81a113 add sample mind maps rendered with all layouts for baseline validation
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-29 12:51:39 +05:30
darshanr0107
8a62b4cace fix: apply intersection logic for edges in tidy-tree layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-29 12:21:42 +05:30
omkarht
334fe87bc6 fix: fixed failing test cases
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-28 18:33:58 +05:30
omkarht
283e7810d2 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-07-28 17:26:44 +05:30
omkarht
237d01d510 fix: shifted matchAsActorOrParticipant function to sequenceDB file from sequenceDiagram.jison file
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-28 16:47:54 +05:30
darshanr0107
ccafc20917 add support for cloud and bang shape
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-24 18:07:25 +05:30
darshanr0107
d5cb4eaa59 add support for elk layout in mindmap
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-23 20:11:38 +05:30
darshanr0107
425fb7ee33 fix: handle undefined edge list in insertEdge to prevent runtime error
Fixes a runtime error in cose-bilkent layout caused by insertEdge calling .filter() on an undefined edge list. Now defaults to an empty array to prevent the crash.

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-23 17:18:48 +05:30
darshanr0107
cd6f8e5a24 tidy-tree as the default layout for mindmaps
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-07-21 18:41:18 +05:30
omkarht
afeb761296 Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-07-08 13:53:51 +05:30
omkarht
3abcfbb8d2 added unit tests 2025-07-08 13:48:27 +05:30
omkarht
ee82694645 refactor(lexer): added contextual handling for new participant types as actors 2025-07-08 13:47:41 +05:30
omkarht
012530e98e added changeset 2025-07-01 19:02:57 +05:30
omkarht
a4a27611dd fix: minor refinement 2025-07-01 18:11:41 +05:30
omkarht
5055ade44e Merge branch 'develop' into 6637-add-new-participant-types-to-sequence-diagrams 2025-07-01 13:29:18 +05:30
omkarht
b61bec8faf added test cases for new participant types of sequence diagram 2025-07-01 12:58:04 +05:30
omkarht
76d073b027 chore: adjusted database and queue shape 2025-06-27 17:56:03 +05:30
omkarht
cc476d59d1 6637-Add new participant types to Sequence Diagrams 2025-06-27 13:42:19 +05:30
Knut Sveidqvist
f0445b74d1 #6646 Tidy-tree layout in place for Mindmaps 2025-06-12 13:33:05 +02:00
Knut Sveidqvist
ba52eef257 Tidy-tree getting closer handling of multi point edges 2025-06-12 11:21:35 +02:00
Knut Sveidqvist
c13ce2a5c0 Tidy-tree getting closer, now algorithm works 2025-06-11 20:58:54 +02:00
Knut Sveidqvist
d2463f41b5 Tidy-tree WIP 2025-06-11 18:04:21 +02:00
Knut Sveidqvist
eadb343292 Tidy-tree POC 2025-06-11 16:55:58 +02:00
Knut Sveidqvist
e7208622f7 Merge branch '6088-fix-for-diamond-intersections 2025-06-06 20:14:02 +02:00
Knut Sveidqvist
fbae611406 Refresh 2025-06-06 20:10:19 +02:00
Knut Sveidqvist
34027bc589 Repackaging cose-bilkins 2025-06-06 20:03:34 +02:00
Knut Sveidqvist
f2eef37599 Updated renderinig flow for mindmaps 2025-06-06 11:20:31 +02:00
Knut Sveidqvist
1e3ea13323 Fix for when last point is on the intersection 2024-11-29 09:07:47 +01:00
Knut Sveidqvist
4c8c48cde9 Generic solution for intersection of shapes with elk 2024-11-28 14:31:54 +01:00
Knut Sveidqvist
c8e50276e8 Added changeset 2024-11-27 15:54:05 +01:00
Knut Sveidqvist
1e6419a63f #6088 Updated offset calculations 2024-11-27 15:52:24 +01:00
112 changed files with 10744 additions and 6006 deletions

View File

@@ -33,6 +33,11 @@ export const packageOptions = {
packageName: 'mermaid-layout-elk',
file: 'layouts.ts',
},
'mermaid-layout-tidy-tree': {
name: 'mermaid-layout-tidy-tree',
packageName: 'mermaid-layout-tidy-tree',
file: 'index.ts',
},
examples: {
name: 'mermaid-examples',
packageName: 'examples',

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Handle arrows correctly when auto number is enabled

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.

View File

@@ -0,0 +1,7 @@
---
'mermaid': minor
'@mermaid-js/layout-tidy-tree': minor
'@mermaid-js/layout-elk': minor
---
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes

View File

@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
uses: peter-evans/create-pull-request@1310d7dab503600742045e6fd4b84dda64352858
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
with:
add-paths: |
cypress/timings.json

View File

@@ -35,7 +35,7 @@ jobs:
# 2) No unwanted vitepress paths
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
fi
# 3) Lockfile only changes when package.json changes

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ node_modules/
coverage/
.idea/
.pnpm-store/
.instructions/
dist
v8-compile-cache-0

View File

@@ -0,0 +1,79 @@
import { imgSnapshotTest } from '../../helpers/util.ts';
describe('Mindmap Tidy Tree', () => {
it('1-tidy-tree: should render a simple mindmap without children', () => {
imgSnapshotTest(
` ---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
B
`
);
});
it('2-tidy-tree: should render a simple mindmap', () => {
imgSnapshotTest(
` ---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
`
);
});
it('3-tidy-tree: should render a mindmap with different shapes', () => {
imgSnapshotTest(
` ---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
`
);
});
it('4-tidy-tree: should render a mindmap with children', () => {
imgSnapshotTest(
` ---
config:
layout: tidy-tree
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
`
);
});
});

View File

@@ -159,12 +159,10 @@ root
});
it('square shape', () => {
imgSnapshotTest(
`
mindmap
`mindmap
root[
The root
]
`,
]`,
{},
undefined,
shouldHaveRoot
@@ -172,12 +170,10 @@ mindmap
});
it('rounded rect shape', () => {
imgSnapshotTest(
`
mindmap
`mindmap
root((
The root
))
`,
))`,
{},
undefined,
shouldHaveRoot
@@ -185,12 +181,10 @@ mindmap
});
it('circle shape', () => {
imgSnapshotTest(
`
mindmap
`mindmap
root(
The root
)
`,
)`,
{},
undefined,
shouldHaveRoot
@@ -198,10 +192,8 @@ mindmap
});
it('default shape', () => {
imgSnapshotTest(
`
mindmap
The root
`,
`mindmap
The root`,
{},
undefined,
shouldHaveRoot
@@ -209,12 +201,10 @@ mindmap
});
it('adding children', () => {
imgSnapshotTest(
`
mindmap
`mindmap
The root
child1
child2
`,
child2`,
{},
undefined,
shouldHaveRoot
@@ -222,13 +212,11 @@ mindmap
});
it('adding grand children', () => {
imgSnapshotTest(
`
mindmap
`mindmap
The root
child1
child2
child3
`,
child3`,
{},
undefined,
shouldHaveRoot

View File

@@ -0,0 +1,659 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
const looks = ['classic'];
const participantTypes = [
{ type: 'participant', display: 'participant' },
{ type: 'actor', display: 'actor' },
{ type: 'boundary', display: 'boundary' },
{ type: 'control', display: 'control' },
{ type: 'entity', display: 'entity' },
{ type: 'database', display: 'database' },
{ type: 'collections', display: 'collections' },
{ type: 'queue', display: 'queue' },
];
const restrictedTypes = ['boundary', 'control', 'entity', 'database', 'collections', 'queue'];
const interactionTypes = ['->>', '-->>', '->', '-->', '-x', '--x', '->>+', '-->>+'];
const notePositions = ['left of', 'right of', 'over'];
function getParticipantLine(name, type, alias) {
if (restrictedTypes.includes(type)) {
return ` participant ${name}@{ "type" : "${type}" }\n`;
} else if (alias) {
return ` participant ${name}@{ "type" : "${type}" } \n`;
} else {
return ` participant ${name}@{ "type" : "${type}" }\n`;
}
}
looks.forEach((look) => {
describe(`Sequence Diagram Tests - ${look} look`, () => {
it('should render all participant types', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.forEach((pt, index) => {
const name = `${pt.display}${index}`;
diagramCode += getParticipantLine(name, pt.type);
});
for (let i = 0; i < participantTypes.length - 1; i++) {
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
}
imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } });
});
it('should render all interaction types', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += getParticipantLine('A', 'actor');
diagramCode += getParticipantLine('B', 'boundary');
interactionTypes.forEach((interaction, index) => {
diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`;
});
imgSnapshotTest(diagramCode, { look });
});
it('should render participant creation and destruction', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.forEach((pt, index) => {
const name = `${pt.display}${index}`;
diagramCode += getParticipantLine('A', pt.type);
diagramCode += getParticipantLine('B', pt.type);
diagramCode += ` create participant ${name}@{ "type" : "${pt.type}" }\n`;
diagramCode += ` A ->> ${name}: Hello ${pt.display}\n`;
if (index % 2 === 0) {
diagramCode += ` destroy ${name}\n`;
}
});
imgSnapshotTest(diagramCode, { look });
});
it('should render notes in all positions', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += getParticipantLine('A', 'actor');
diagramCode += getParticipantLine('B', 'boundary');
notePositions.forEach((position, index) => {
diagramCode += ` Note ${position} A: Note ${position} ${index}\n`;
});
diagramCode += ` A ->> B: Message with notes\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render parallel interactions', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 4).forEach((pt, index) => {
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
});
diagramCode += ` par Parallel actions\n`;
for (let i = 0; i < 3; i += 2) {
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
if (i < participantTypes.length - 2) {
diagramCode += ` and\n`;
}
}
diagramCode += ` end\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render alternative flows', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += getParticipantLine('A', 'actor');
diagramCode += getParticipantLine('B', 'boundary');
diagramCode += ` alt Successful case\n`;
diagramCode += ` A ->> B: Request\n`;
diagramCode += ` B -->> A: Success\n`;
diagramCode += ` else Failure case\n`;
diagramCode += ` A ->> B: Request\n`;
diagramCode += ` B --x A: Failure\n`;
diagramCode += ` end\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render loops', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 3).forEach((pt, index) => {
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
});
diagramCode += ` loop For each participant\n`;
for (let i = 0; i < 3; i++) {
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Message ${i}\n`;
}
diagramCode += ` end\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render boxes around groups', () => {
let diagramCode = `sequenceDiagram\n`;
diagramCode += ` box Group 1\n`;
participantTypes.slice(0, 3).forEach((pt, index) => {
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
});
diagramCode += ` end\n`;
diagramCode += ` box rgb(200,220,255) Group 2\n`;
participantTypes.slice(3, 6).forEach((pt, index) => {
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
});
diagramCode += ` end\n`;
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[3].display}0: Cross-group message\n`;
imgSnapshotTest(diagramCode, { look });
});
it('should render with different font settings', () => {
let diagramCode = `sequenceDiagram\n`;
participantTypes.slice(0, 3).forEach((pt, index) => {
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
});
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Regular message\n`;
diagramCode += ` Note right of ${participantTypes[1].display}1: Regular note\n`;
imgSnapshotTest(diagramCode, {
look,
sequence: {
actorFontFamily: 'courier',
actorFontSize: 14,
messageFontFamily: 'Arial',
messageFontSize: 12,
noteFontFamily: 'times',
noteFontSize: 16,
noteAlign: 'left',
},
});
});
});
});
// Additional tests for specific combinations
describe('Sequence Diagram Special Cases', () => {
it('should render complex sequence with all features', () => {
const diagramCode = `
sequenceDiagram
box rgb(200,220,255) Authentication
actor User
participant LoginUI@{ "type": "boundary" }
participant AuthService@{ "type": "control" }
participant UserDB@{ "type": "database" }
end
box rgb(200,255,220) Order Processing
participant Order@{ "type": "entity" }
participant OrderQueue@{ "type": "queue" }
participant AuditLogs@{ "type": "collections" }
end
User ->> LoginUI: Enter credentials
LoginUI ->> AuthService: Validate
AuthService ->> UserDB: Query user
UserDB -->> AuthService: User data
alt Valid credentials
AuthService -->> LoginUI: Success
LoginUI -->> User: Welcome
par Place order
User ->> Order: New order
Order ->> OrderQueue: Process
and
Order ->> AuditLogs: Record
end
loop Until confirmed
OrderQueue ->> Order: Update status
Order -->> User: Notification
end
else Invalid credentials
AuthService --x LoginUI: Failure
LoginUI --x User: Retry
end
`;
imgSnapshotTest(diagramCode, {});
});
it('should render with wrapped messages and notes', () => {
const diagramCode = `
sequenceDiagram
participant A
participant B
A ->> B: This is a very long message that should wrap properly in the diagram rendering
Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram
par Wrapped parallel
A ->> B: Parallel message 1<br>with explicit line break
and
B ->> A: Parallel message 2<br>with explicit line break
end
loop Wrapped loop
Note right of B: This is a long note<br>in a loop
A ->> B: Message in loop
end
`;
imgSnapshotTest(diagramCode, { sequence: { wrap: true } });
});
describe('Sequence Diagram Rendering with Different Participant Types', () => {
it('should render a sequence diagram with various participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
participant User@{ "type": "actor" }
participant AuthService@{ "type": "control" }
participant UI@{ "type": "boundary" }
participant OrderController@{ "type": "control" }
participant Product@{ "type": "entity" }
participant MongoDB@{ "type": "database" }
participant Products@{ "type": "collections" }
participant OrderQueue@{ "type": "queue" }
User ->> UI: Login request
UI ->> AuthService: Validate credentials
AuthService -->> UI: Authentication token
UI ->> OrderController: Place order
OrderController ->> Product: Check availability
Product -->> OrderController: Available
OrderController ->> MongoDB: Save order
MongoDB -->> OrderController: Order saved
OrderController ->> OrderQueue: Process payment
OrderQueue -->> User: Order confirmation
`
);
});
it('should render participant creation and destruction with different types', () => {
imgSnapshotTest(`
sequenceDiagram
participant Alice@{ "type" : "boundary" }
Alice->>Bob: Hello Bob, how are you ?
Bob->>Alice: Fine, thank you. And you?
create participant Carl@{ "type" : "control" }
Alice->>Carl: Hi Carl!
create actor D as Donald
Carl->>D: Hi!
destroy Carl
Alice-xCarl: We are too many
destroy Bob
Bob->>Alice: I agree
`);
});
it('should handle complex interactions between different participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
box rgb(200,220,255) Authentication
participant User@{ "type": "actor" }
participant LoginUI@{ "type": "boundary" }
participant AuthService@{ "type": "control" }
participant UserDB@{ "type": "database" }
end
box rgb(200,255,220) Order Processing
participant Order@{ "type": "entity" }
participant OrderQueue@{ "type": "queue" }
participant AuditLogs@{ "type": "collections" }
end
User ->> LoginUI: Enter credentials
LoginUI ->> AuthService: Validate
AuthService ->> UserDB: Query user
UserDB -->> AuthService: User data
alt Valid credentials
AuthService -->> LoginUI: Success
LoginUI -->> User: Welcome
par Place order
User ->> Order: New order
Order ->> OrderQueue: Process
and
Order ->> AuditLogs: Record
end
loop Until confirmed
OrderQueue ->> Order: Update status
Order -->> User: Notification
end
else Invalid credentials
AuthService --x LoginUI: Failure
LoginUI --x User: Retry
end
`,
{ sequence: { useMaxWidth: false } }
);
});
it('should render parallel processes with different participant types', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Customer@{ "type": "actor" }
participant Frontend@{ "type": "participant" }
participant PaymentService@{ "type": "boundary" }
participant InventoryManager@{ "type": "control" }
participant Order@{ "type": "entity" }
participant OrdersDB@{ "type": "database" }
participant NotificationQueue@{ "type": "queue" }
Customer ->> Frontend: Place order
Frontend ->> Order: Create order
par Parallel Processing
Order ->> PaymentService: Process payment
and
Order ->> InventoryManager: Reserve items
end
PaymentService -->> Order: Payment confirmed
InventoryManager -->> Order: Items reserved
Order ->> OrdersDB: Save finalized order
OrdersDB -->> Order: Order saved
Order ->> NotificationQueue: Send confirmation
NotificationQueue -->> Customer: Order confirmation
`
);
});
});
it('should render different participant types with notes and loops', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Admin
participant Dashboard
participant AuthService@{ "type" : "boundary" }
participant UserManager@{ "type" : "control" }
participant UserProfile@{ "type" : "entity" }
participant UserDB@{ "type" : "database" }
participant Logs@{ "type" : "database" }
Admin ->> Dashboard: Open user management
loop Authentication check
Dashboard ->> AuthService: Verify admin rights
AuthService ->> Dashboard: Access granted
end
Dashboard ->> UserManager: List users
UserManager ->> UserDB: Query users
UserDB ->> UserManager: Return user data
Note right of UserDB: Encrypted data<br/>requires decryption
UserManager ->> UserProfile: Format profiles
UserProfile ->> UserManager: Formatted data
UserManager ->> Dashboard: Display users
Dashboard ->> Logs: Record access
Logs ->> Admin: Audit trail
`
);
});
it('should render different participant types with alternative flows', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Client
participant MobileApp
participant CloudService@{ "type" : "boundary" }
participant DataProcessor@{ "type" : "control" }
participant Transaction@{ "type" : "entity" }
participant TransactionsDB@{ "type" : "database" }
participant EventBus@{ "type" : "queue" }
Client ->> MobileApp: Initiate transaction
MobileApp ->> CloudService: Authenticate
alt Authentication successful
CloudService -->> MobileApp: Auth token
MobileApp ->> DataProcessor: Process data
DataProcessor ->> Transaction: Create transaction
Transaction ->> TransactionsDB: Save record
TransactionsDB -->> Transaction: Confirmation
Transaction ->> EventBus: Publish event
EventBus -->> Client: Notification
else Authentication failed
CloudService -->> MobileApp: Error
MobileApp -->> Client: Show error
end
`
);
});
it('should render different participant types with wrapping text', () => {
imgSnapshotTest(
`
sequenceDiagram
participant B@{ "type" : "boundary" }
participant C@{ "type" : "control" }
participant E@{ "type" : "entity" }
participant DB@{ "type" : "database" }
participant COL@{ "type" : "collections" }
participant Q@{ "type" : "queue" }
FE ->> B: Another long message<br/>with explicit<br/>line breaks
B -->> FE: Response message that is also quite long and needs to wrap
FE ->> C: Process data
C ->> E: Validate
E -->> C: Validation result
C ->> DB: Save
DB -->> C: Save result
C ->> COL: Log
COL -->> Q: Forward
Q -->> LongNameUser: Final response with confirmation of all actions taken
`,
{ sequence: { wrap: true } }
);
});
describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => {
it('should render long notes left of boundary', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "boundary" }
actor Bob
Alice->>Bob: Hola
Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long notes left of control', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "control" }
actor Bob
Alice->>Bob: Hola
Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long notes right of entity', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "entity" }
actor Bob
Alice->>Bob: Hola
Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long notes right of database', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "database" }
actor Bob
Alice->>Bob: Hola
Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long notes over collections', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "collections" }
actor Bob
Alice->>Bob: Hola
Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long notes over queue', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "queue" }
actor Bob
Alice->>Bob: Hola
Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render notes over actor and boundary', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Alice
participant Charlie@{ "type" : "boundary" }
note over Alice: Some note
note over Charlie: Other note
`,
{}
);
});
it('should render long messages from database to collections', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob@{ "type" : "collections" }
Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render wrapped long messages from control to entity', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob@{ "type" : "entity" }
Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long messages from queue to boundary', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob@{ "type" : "boundary" }
Alice->>Bob: I'm short
Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
`,
{}
);
});
it('should render wrapped long messages from actor to database', () => {
imgSnapshotTest(
`
sequenceDiagram
actor Alice
participant Bob@{ "type" : "database" }
Alice->>Bob: I'm short
Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
`,
{}
);
});
});
describe('svg size', () => {
it('should render a sequence diagram when useMaxWidth is true (default)', () => {
renderGraph(
`
sequenceDiagram
actor Alice
participant Bob@{ "type" : "boundary" }
participant John@{ "type" : "control" }
Alice ->> Bob: Hello Bob, how are you?
Bob-->>John: How about you John?
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
Bob-->Alice: Checking with John...
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
`,
{ sequence: { useMaxWidth: true } }
);
cy.get('svg').should((svg) => {
expect(svg).to.have.attr('width', '100%');
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05);
});
});
it('should render a sequence diagram when useMaxWidth is false', () => {
renderGraph(
`
sequenceDiagram
actor Alice
participant Bob@{ "type" : "boundary" }
participant John@{ "type" : "control" }
Alice ->> Bob: Hello Bob, how are you?
Bob-->>John: How about you John?
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
Bob-->Alice: Checking with John...
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
`,
{ sequence: { useMaxWidth: false } }
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
expect(width).to.be.within(820 * 0.95, 820 * 1.05);
expect(svg).to.not.have.attr('style');
});
});
});
});

View File

@@ -893,6 +893,17 @@ describe('Sequence diagram', () => {
}
);
});
it('should handle bidirectional arrows with autonumber', () => {
imgSnapshotTest(`
sequenceDiagram
autonumber
participant A
participant B
A<<->>B: This is a bidirectional message
A->B: This is a normal message`);
});
it('should support actor links and properties when not mirrored EXPERIMENTAL: USE WITH CAUTION', () => {
//Be aware that the syntax for "properties" is likely to be changed.
imgSnapshotTest(

View File

@@ -130,6 +130,76 @@
</head>
<body>
<pre id="diagram4" class="mermaid2">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
B
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre id="diagram4" class="mermaid">
---
config:
@@ -191,8 +261,145 @@ treemap
"Item B2": 25
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: tidy-tree
---
flowchart TB
A --> n0["1"]
A --> n1["2"]
A --> n2["3"]
A --> n3["4"] --> Q & R & S & T
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart TB
A --> n0["1"]
A --> n1["2"]
A --> n2["3"]
A --> n3["4"] --> Q & R & S & T
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: dagre
---
mindmap
root((mindmap is a long thing))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
---
flowchart LR
AB["apa@apa@"] --> B(("`apa@apa`"))
root{mindmap} --- Origins --- Europe
Origins --> Asia
root --- Background --- Rich
Background --- Poor
subgraph apa
Background
Poor
end
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart LR
root{mindmap} --- Origins --- Europe
Origins --> Asia
root --- Background --- Rich
Background --- Poor
</pre>
<pre id="diagram4" class="mermaid2">
flowchart
@@ -274,6 +481,44 @@ config:
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart LR
a
subgraph s0["APA"]
subgraph s8["APA"]
subgraph s1["APA"]
D{"X"}
E[Q]
end
subgraph s3["BAPA"]
F[Q]
I
end
D --> I
D --> I
D --> I
I{"X"}
end
end
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart LR
a
D{"Use the editor"}
D -- Mermaid js --> I{"fa:fa-code Text"}
D-->I
D-->I
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---

View File

@@ -0,0 +1,376 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
A
B
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
id)I am a cloud(
id))I am a bang((
Tools
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<pre class="mermaid">
---
config:
layout: dagre
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<pre class="mermaid">
---
config:
layout: elk
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<pre class="mermaid">
---
config:
layout: cose-bilkent
---
mindmap
((This is a mindmap))
child1
grandchild 1
grandchild 2
child2
grandchild 3
grandchild 4
child3
grandchild 5
grandchild 6
</pre>
<hr />
<script type="module">
import mermaid from '/mermaid.esm.mjs';
import tidytree from '/mermaid-layout-tidy-tree.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.registerLayoutLoaders(tidytree);
mermaid.initialize({
theme: 'default',
logLevel: 3,
securityLevel: 'loose',
});
</script>
</body>
</html>

View File

@@ -1,5 +1,6 @@
import externalExample from './mermaid-example-diagram.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
import tidyTree from './mermaid-layout-tidy-tree.esm.mjs';
import zenUml from './mermaid-zenuml.esm.mjs';
import mermaid from './mermaid.esm.mjs';
@@ -65,6 +66,7 @@ const contentLoaded = async function () {
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
mermaid.registerLayoutLoaders(layouts);
mermaid.registerLayoutLoaders(tidyTree);
mermaid.initialize(graphObj.mermaid);
/**
* CC-BY-4.0

View File

@@ -2,219 +2,223 @@
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
"duration": 6297
"duration": 6162
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 2187
"duration": 2148
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3509
"duration": 3585
},
{
"spec": "cypress/integration/other/iife.spec.js",
"duration": 2218
"duration": 2099
},
{
"spec": "cypress/integration/other/interaction.spec.js",
"duration": 12104
"duration": 12119
},
{
"spec": "cypress/integration/other/rerender.spec.js",
"duration": 2151
"duration": 2063
},
{
"spec": "cypress/integration/other/xss.spec.js",
"duration": 33064
"duration": 31921
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
"duration": 3488
"duration": 3385
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 106
"duration": 108
},
{
"spec": "cypress/integration/rendering/block.spec.js",
"duration": 18317
"duration": 18063
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
"duration": 5592
"duration": 5519
},
{
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 39358
"duration": 40040
},
{
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 37160
"duration": 38665
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 23660
"duration": 22836
},
{
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 36866
"duration": 37096
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 17334
"duration": 16452
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 9871
"duration": 10387
},
{
"spec": "cypress/integration/rendering/current.spec.js",
"duration": 2833
"duration": 2803
},
{
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 85321
"duration": 86891
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 15673
"duration": 15206
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 3724
"duration": 3540
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 41178
"duration": 41975
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 29966
"duration": 30909
},
{
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
"duration": 7689
"duration": 7881
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 24709
"duration": 24294
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 45565
"duration": 47652
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 31144
"duration": 32049
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 20808
"duration": 20248
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 49985
"duration": 51202
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 273272
"duration": 283546
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 55880
"duration": 57257
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
"duration": 3271
"duration": 3352
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
"duration": 7293
"duration": 7423
},
{
"spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 7861
"duration": 7804
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
"duration": 3922
"duration": 3847
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 2726
"duration": 2637
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 11670
"duration": 11658
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 146020
"duration": 149500
},
{
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 114244
"duration": 115427
},
{
"spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 5036
"duration": 4801
},
{
"spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 6545
"duration": 6786
},
{
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 9097
"duration": 9422
},
{
"spec": "cypress/integration/rendering/radar.spec.js",
"duration": 5676
"duration": 5652
},
{
"spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2795
"duration": 2787
},
{
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 51660
"duration": 53631
},
{
"spec": "cypress/integration/rendering/sankey.spec.ts",
"duration": 6957
"duration": 7075
},
{
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
"duration": 20446
},
{
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 36026
"duration": 37326
},
{
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 29551
"duration": 29208
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 17364
"duration": 16328
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
"duration": 30209
"duration": 30541
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 8699
"duration": 8611
},
{
"spec": "cypress/integration/rendering/treemap.spec.ts",
"duration": 12168
"duration": 11878
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 21453
"duration": 20400
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 3577
"duration": 3528
}
]
}

View File

@@ -29,7 +29,8 @@ In GitHub, you first [**fork a mermaid repository**](https://github.com/mermaid-
Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with.
> **💡 Tip** > [Here is a GitHub document that gives an overview of the process](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
> **💡 Tip**
> [Here is a GitHub document that gives an overview of the process](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
```bash
git clone git@github.com/your-fork/mermaid

View File

@@ -33,7 +33,8 @@ mindmap
## Join the Development
> **💡 Tip** > **Check out our** [**detailed contribution guide**](./contributing.md).
> **💡 Tip**
> **Check out our** [**detailed contribution guide**](./contributing.md).
Where to start:
@@ -47,7 +48,8 @@ Where to start:
## A Question Or a Suggestion?
> **💡 Tip** > **Have a look at** [**how to open an issue**](./questions-and-suggestions.md).
> **💡 Tip**
> **Have a look at** [**how to open an issue**](./questions-and-suggestions.md).
If you have faced a vulnerability [report it to us](./security.md).

View File

@@ -22,7 +22,6 @@ While directives allow you to change most of the default configuration settings,
Mermaid basically supports two types of configuration options to be overridden by directives.
1. _General/Top Level configurations_ : These are the configurations that are available and applied to all the diagram. **Some of the most important top-level** configurations are:
- theme
- fontFamily
- logLevel

View File

@@ -19,6 +19,7 @@
- [addDirective](functions/addDirective.md)
- [getConfig](functions/getConfig.md)
- [getSiteConfig](functions/getSiteConfig.md)
- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
- [reset](functions/reset.md)
- [sanitize](functions/sanitize.md)
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)

View File

@@ -0,0 +1,19 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md).
[**mermaid**](../../README.md)
---
# Function: getUserDefinedConfig()
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
## Returns
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)

View File

@@ -10,10 +10,6 @@
# mermaid
## Classes
- [UnknownDiagramError](classes/UnknownDiagramError.md)
## Interfaces
- [DetailedError](interfaces/DetailedError.md)
@@ -27,6 +23,7 @@
- [RenderOptions](interfaces/RenderOptions.md)
- [RenderResult](interfaces/RenderResult.md)
- [RunOptions](interfaces/RunOptions.md)
- [UnknownDiagramError](interfaces/UnknownDiagramError.md)
## Type Aliases

View File

@@ -1,159 +0,0 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md).
[**mermaid**](../../README.md)
---
# Class: UnknownDiagramError
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
## Extends
- `Error`
## Constructors
### new UnknownDiagramError()
> **new UnknownDiagramError**(`message`): [`UnknownDiagramError`](UnknownDiagramError.md)
Defined in: [packages/mermaid/src/errors.ts:2](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L2)
#### Parameters
##### message
`string`
#### Returns
[`UnknownDiagramError`](UnknownDiagramError.md)
#### Overrides
`Error.constructor`
## Properties
### cause?
> `optional` **cause**: `unknown`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
#### Inherited from
`Error.cause`
---
### message
> **message**: `string`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
#### Inherited from
`Error.message`
---
### name
> **name**: `string`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
#### Inherited from
`Error.name`
---
### stack?
> `optional` **stack**: `string`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
#### Inherited from
`Error.stack`
---
### prepareStackTrace()?
> `static` `optional` **prepareStackTrace**: (`err`, `stackTraces`) => `any`
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:143
Optional override for formatting stack traces
#### Parameters
##### err
`Error`
##### stackTraces
`CallSite`\[]
#### Returns
`any`
#### See
<https://v8.dev/docs/stack-trace-api#customizing-stack-traces>
#### Inherited from
`Error.prepareStackTrace`
---
### stackTraceLimit
> `static` **stackTraceLimit**: `number`
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:145
#### Inherited from
`Error.stackTraceLimit`
## Methods
### captureStackTrace()
> `static` **captureStackTrace**(`targetObject`, `constructorOpt`?): `void`
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:136
Create .stack property on a target object
#### Parameters
##### targetObject
`object`
##### constructorOpt?
`Function`
#### Returns
`void`
#### Inherited from
`Error.captureStackTrace`

View File

@@ -10,7 +10,7 @@
# Interface: LayoutData
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
Defined in: [packages/mermaid/src/rendering-util/types.ts:178](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L178)
## Indexable
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148)
Defined in: [packages/mermaid/src/rendering-util/types.ts:181](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L181)
---
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co
> **edges**: `Edge`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
Defined in: [packages/mermaid/src/rendering-util/types.ts:180](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L180)
---
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co
> **nodes**: `Node`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
Defined in: [packages/mermaid/src/rendering-util/types.ts:179](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L179)

View File

@@ -32,7 +32,7 @@ page.
### detectType()
> **detectType**: (`text`, `config`?) => `string`
> **detectType**: (`text`, `config?`) => `string`
Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449)
@@ -105,7 +105,7 @@ An array of objects with the id of the diagram.
### ~~init()~~
> **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`>
> **init**: (`config?`, `nodes?`, `callback?`) => `Promise`<`void`>
Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
@@ -117,7 +117,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/
[`MermaidConfig`](MermaidConfig.md)
**Deprecated**, please set configuration in [initialize](Mermaid.md#initialize).
**Deprecated**, please set configuration in [initialize](#initialize).
##### nodes?
@@ -141,13 +141,13 @@ Called once for each rendered diagram's id.
#### Deprecated
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
Use [initialize](#initialize) and [run](#run) instead.
Renders the mermaid diagrams
#### Deprecated
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
Use [initialize](#initialize) and [run](#run) instead.
---
@@ -176,7 +176,7 @@ Configuration object for mermaid.
### ~~mermaidAPI~~
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }; `render`: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
@@ -184,73 +184,81 @@ Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/
#### Deprecated
Use [parse](Mermaid.md#parse) and [render](Mermaid.md#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
Use [parse](#parse) and [render](#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
---
### parse()
> **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>
> **parse**: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }
Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
#### Call Signature
> (`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>
Parse the text and validate the syntax.
#### Parameters
##### Parameters
##### text
###### text
`string`
The mermaid diagram definition.
##### parseOptions
###### parseOptions
[`ParseOptions`](ParseOptions.md) & `object`
Options for parsing.
#### Returns
##### Returns
`Promise`<`false` | [`ParseResult`](ParseResult.md)>
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
#### See
##### See
[ParseOptions](ParseOptions.md)
#### Throws
##### Throws
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
#### Call Signature
> (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>
Parse the text and validate the syntax.
#### Parameters
##### Parameters
##### text
###### text
`string`
The mermaid diagram definition.
##### parseOptions?
###### parseOptions?
[`ParseOptions`](ParseOptions.md)
Options for parsing.
#### Returns
##### Returns
`Promise`<[`ParseResult`](ParseResult.md)>
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
#### See
##### See
[ParseOptions](ParseOptions.md)
#### Throws
##### Throws
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
@@ -332,7 +340,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/
### render()
> **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>
> **render**: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>
Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)

View File

@@ -10,7 +10,7 @@
# Interface: ParseOptions
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.

View File

@@ -10,7 +10,7 @@
# Interface: ParseResult
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
Defined in: [packages/mermaid/src/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104)
The config passed as YAML frontmatter or directives
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
The diagram type, e.g. 'flowchart', 'sequence', etc.

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mer
> `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132)
Bind function to be called after the svg has been inserted into the DOM.
This is necessary for adding event listeners to the elements in the svg.
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122)
The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string`
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
The svg code for the rendered graph.

View File

@@ -0,0 +1,65 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md).
[**mermaid**](../../README.md)
---
# Interface: UnknownDiagramError
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
## Extends
- `Error`
## Properties
### cause?
> `optional` **cause**: `unknown`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
#### Inherited from
`Error.cause`
---
### message
> **message**: `string`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
#### Inherited from
`Error.message`
---
### name
> **name**: `string`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
#### Inherited from
`Error.name`
---
### stack?
> `optional` **stack**: `string`
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
#### Inherited from
`Error.stack`

View File

@@ -10,6 +10,6 @@
# Type Alias: InternalHelpers
> **InternalHelpers**: _typeof_ `internalHelpers`
> **InternalHelpers** = _typeof_ `internalHelpers`
Defined in: [packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33)

View File

@@ -10,7 +10,7 @@
# Type Alias: ParseErrorFunction()
> **ParseErrorFunction**: (`err`, `hash`?) => `void`
> **ParseErrorFunction** = (`err`, `hash?`) => `void`
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)

View File

@@ -10,6 +10,6 @@
# Type Alias: SVG
> **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
> **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)

View File

@@ -10,6 +10,6 @@
# Type Alias: SVGGroup
> **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
> **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)

63
docs/config/tidy-tree.md Normal file
View File

@@ -0,0 +1,63 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/tidy-tree.md](../../packages/mermaid/src/docs/config/tidy-tree.md).
# Tidy-tree Layout Instructions
Instructions to use the Tidy-tree layout algorithm.
## Getting Started
### Installation
```bash
npm install non-layered-tidy-tree-layout
# or
yarn add non-layered-tidy-tree-layout
```
There's also a built version: `dist/non-layered-tidy-tree-layout.js` for use with browser `<script>` tag, or as a Javascript module.
## Tidy tree Layouts
Mermaid also supports a Tidy Tree layout for mindmaps.
```
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
### With bundlers
```sh
npm install @mermaid-js/layout-tidy-tree
```
```ts
import mermaid from 'mermaid';
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
```
### With CDN
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
</script>
```

View File

@@ -29,7 +29,6 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chatgpt.com/g/g-684cc36f30208191b21383b88650a45d-mermaid-chart-diagrams-and-charts)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)

View File

@@ -35,13 +35,11 @@ The Mermaid Chart team is excited to introduce a new Visual Editor for Flowchart
Learn more:
- Visual Editor For Flowcharts
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-releases-new-visual-editor-for-flowcharts)
- [Demo video](https://www.youtube.com/watch?v=5aja0gijoO0)
- Visual Editor For Sequence diagrams
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-unveils-visual-editor-for-sequence-diagrams)
- [Demo video](https://youtu.be/imc2u5_N6Dc)

View File

@@ -139,7 +139,6 @@ The following unfinished features are not supported in the short term.
- [ ] Legend
- [x] System Context
- [x] Person(alias, label, ?descr, ?sprite, ?tags, $link)
- [x] Person_Ext
- [x] System(alias, label, ?descr, ?sprite, ?tags, $link)
@@ -153,7 +152,6 @@ The following unfinished features are not supported in the short term.
- [x] System_Boundary
- [x] Container diagram
- [x] Container(alias, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] ContainerDb
- [x] ContainerQueue
@@ -163,7 +161,6 @@ The following unfinished features are not supported in the short term.
- [x] Container_Boundary(alias, label, ?tags, $link)
- [x] Component diagram
- [x] Component(alias, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] ComponentDb
- [x] ComponentQueue
@@ -172,18 +169,15 @@ The following unfinished features are not supported in the short term.
- [x] ComponentQueue_Ext
- [x] Dynamic diagram
- [x] RelIndex(index, from, to, label, ?tags, $link)
- [x] Deployment diagram
- [x] Deployment_Node(alias, label, ?type, ?descr, ?sprite, ?tags, $link)
- [x] Node(alias, label, ?type, ?descr, ?sprite, ?tags, $link): short name of Deployment_Node()
- [x] Node_L(alias, label, ?type, ?descr, ?sprite, ?tags, $link): left aligned Node()
- [x] Node_R(alias, label, ?type, ?descr, ?sprite, ?tags, $link): right aligned Node()
- [x] Relationship Types
- [x] Rel(from, to, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] BiRel (bidirectional relationship)
- [x] Rel_U, Rel_Up

View File

@@ -324,53 +324,56 @@ This syntax creates a node A as a rectangle. It renders in the same way as `A["A
Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases:
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
| Comment Right | Curly Brace | `brace-r` | Adds a comment | |
| Comment with braces on both sides | Curly Braces | `braces` | Adds a comment | |
| Data Input/Output | Lean Right | `lean-r` | Represents input or output | `in-out`, `lean-right` |
| Data Input/Output | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` |
| Database | Cylinder | `cyl` | Database storage | `cylinder`, `database`, `db` |
| Decision | Diamond | `diam` | Decision-making step | `decision`, `diamond`, `question` |
| Delay | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` |
| Direct Access Storage | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` |
| Disk Storage | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` |
| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` |
| Document | Document | `doc` | Represents a document | `doc`, `document` |
| Event | Rounded Rectangle | `rounded` | Represents an event | `event` |
| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` |
| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` |
| Internal Storage | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` |
| Junction | Filled Circle | `f-circ` | Junction point | `filled-circle`, `junction` |
| Lined Document | Lined Document | `lin-doc` | Lined document | `lined-document` |
| Lined/Shaded Process | Lined Rectangle | `lin-rect` | Lined process shape | `lin-proc`, `lined-process`, `lined-rectangle`, `shaded-process` |
| Loop Limit | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` |
| Manual File | Flipped Triangle | `flip-tri` | Manual file operation | `flipped-triangle`, `manual-file` |
| Manual Input | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` |
| Manual Operation | Trapezoid Base Top | `trap-t` | Represents a manual task | `inv-trapezoid`, `manual`, `trapezoid-top` |
| Multi-Document | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` |
| Multi-Process | Stacked Rectangle | `st-rect` | Multiple processes | `processes`, `procs`, `stacked-rectangle` |
| Odd | Odd | `odd` | Odd shape | |
| Paper Tape | Flag | `flag` | Paper tape | `paper-tape` |
| Prepare Conditional | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` |
| Priority Action | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid`, `trapezoid-bottom` |
| Process | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` |
| Start | Circle | `circle` | Starting point | `circ` |
| Start | Small Circle | `sm-circ` | Small starting point | `small-circle`, `start` |
| Stop | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` |
| Stop | Framed Circle | `fr-circ` | Stop point | `framed-circle`, `stop` |
| Stored Data | Bow Tie Rectangle | `bow-rect` | Stored data | `bow-tie-rectangle`, `stored-data` |
| Subprocess | Framed Rectangle | `fr-rect` | Subprocess | `framed-rectangle`, `subproc`, `subprocess`, `subroutine` |
| Summary | Crossed Circle | `cross-circ` | Summary | `crossed-circle`, `summary` |
| Tagged Document | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` |
| Tagged Process | Tagged Rectangle | `tag-rect` | Tagged process | `tag-proc`, `tagged-process`, `tagged-rectangle` |
| Terminal Point | Stadium | `stadium` | Terminal point | `pill`, `terminal` |
| Text Block | Text Block | `text` | Text block | |
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | ----------------- | ------------------------------ | ---------------------------------------------------------------- |
| Bang | Bang | `bang` | Bang | `bang` |
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
| Cloud | Cloud | `cloud` | cloud | `cloud` |
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
| Comment Right | Curly Brace | `brace-r` | Adds a comment | |
| Comment with braces on both sides | Curly Braces | `braces` | Adds a comment | |
| Data Input/Output | Lean Right | `lean-r` | Represents input or output | `in-out`, `lean-right` |
| Data Input/Output | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` |
| Database | Cylinder | `cyl` | Database storage | `cylinder`, `database`, `db` |
| Decision | Diamond | `diam` | Decision-making step | `decision`, `diamond`, `question` |
| Default Mindmap Node | defaultMindmapNode | `default-mindmap` | defaultMindmapNode | `default-mindmap`, `defaultMindmapNode` |
| Delay | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` |
| Direct Access Storage | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` |
| Disk Storage | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` |
| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` |
| Document | Document | `doc` | Represents a document | `doc`, `document` |
| Event | Rounded Rectangle | `rounded` | Represents an event | `event` |
| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` |
| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` |
| Internal Storage | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` |
| Junction | Filled Circle | `f-circ` | Junction point | `filled-circle`, `junction` |
| Lined Document | Lined Document | `lin-doc` | Lined document | `lined-document` |
| Lined/Shaded Process | Lined Rectangle | `lin-rect` | Lined process shape | `lin-proc`, `lined-process`, `lined-rectangle`, `shaded-process` |
| Loop Limit | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` |
| Manual File | Flipped Triangle | `flip-tri` | Manual file operation | `flipped-triangle`, `manual-file` |
| Manual Input | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` |
| Manual Operation | Trapezoid Base Top | `trap-t` | Represents a manual task | `inv-trapezoid`, `manual`, `trapezoid-top` |
| Multi-Document | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` |
| Multi-Process | Stacked Rectangle | `st-rect` | Multiple processes | `processes`, `procs`, `stacked-rectangle` |
| Odd | Odd | `odd` | Odd shape | |
| Paper Tape | Flag | `flag` | Paper tape | `paper-tape` |
| Prepare Conditional | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` |
| Priority Action | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid`, `trapezoid-bottom` |
| Process | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` |
| Start | Circle | `circle` | Starting point | `circ` |
| Start | Small Circle | `sm-circ` | Small starting point | `small-circle`, `start` |
| Stop | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` |
| Stop | Framed Circle | `fr-circ` | Stop point | `framed-circle`, `stop` |
| Stored Data | Bow Tie Rectangle | `bow-rect` | Stored data | `bow-tie-rectangle`, `stored-data` |
| Subprocess | Framed Rectangle | `fr-rect` | Subprocess | `framed-rectangle`, `subproc`, `subprocess`, `subroutine` |
| Summary | Crossed Circle | `cross-circ` | Summary | `crossed-circle`, `summary` |
| Tagged Document | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` |
| Tagged Process | Tagged Rectangle | `tag-rect` | Tagged process | `tag-proc`, `tagged-process`, `tagged-rectangle` |
| Terminal Point | Stadium | `stadium` | Terminal point | `pill`, `terminal` |
| Text Block | Text Block | `text` | Text block | |
### Example Flowchart with New Shapes
@@ -983,11 +986,23 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
```mermaid-example
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
```mermaid
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
## Links between nodes

View File

@@ -360,7 +360,8 @@ gantt
weekday monday
```
> **Warning** > `millisecond` and `second` support was added in v10.3.0
> **Warning**
> `millisecond` and `second` support was added in v10.3.0
## Output in compact mode

View File

@@ -314,3 +314,22 @@ You can also refer the [implementation in the live editor](https://github.com/me
cspell:locale en,en-gb
cspell:ignore Buzan
--->
## Layouts
Mermaid also supports a Tidy Tree layout for mindmaps.
```
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)

View File

@@ -74,6 +74,126 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
### Boundary
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "boundary" }
participant Bob
Alice->>Bob: Request from boundary
Bob->>Alice: Response to boundary
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "boundary" }
participant Bob
Alice->>Bob: Request from boundary
Bob->>Alice: Response to boundary
```
### Control
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob
Alice->>Bob: Control request
Bob->>Alice: Control response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob
Alice->>Bob: Control request
Bob->>Alice: Control response
```
### Entity
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "entity" }
participant Bob
Alice->>Bob: Entity request
Bob->>Alice: Entity response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "entity" }
participant Bob
Alice->>Bob: Entity request
Bob->>Alice: Entity response
```
### Database
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob
Alice->>Bob: DB query
Bob->>Alice: DB result
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob
Alice->>Bob: DB query
Bob->>Alice: DB result
```
### Collections
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "collections" }
participant Bob
Alice->>Bob: Collections request
Bob->>Alice: Collections response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "collections" }
participant Bob
Alice->>Bob: Collections request
Bob->>Alice: Collections response
```
### Queue
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob
Alice->>Bob: Queue message
Bob->>Alice: Queue response
```
```mermaid
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob
Alice->>Bob: Queue message
Bob->>Alice: Queue response
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -17,6 +17,7 @@ export default tseslint.config(
...tseslint.configs.stylisticTypeChecked,
{
ignores: [
'**/*.d.ts',
'**/dist/',
'**/node_modules/',
'.git/',

View File

@@ -2,7 +2,7 @@
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
> [!NOTE]
> [!NOTE]
> The ELK Layout engine will not be available in all providers that support mermaid by default.
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
@@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts);
- `elk.mrtree`: Multi-root tree layout
- `elk.sporeOverlap`: Spore overlap layout
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively -->
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively. -->

View File

@@ -0,0 +1,9 @@
export interface TreeData {
parentById: Record<string, string>;
childrenById: Record<string, string[]>;
}
export declare const findCommonAncestor: (
id1: string,
id2: string,
{ parentById }: TreeData
) => string;

View File

@@ -4,7 +4,8 @@ import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from '
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
type Node = LayoutData['nodes'][number];
// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers
const epsilon = 0.0001;
interface LabelData {
width: number;
height: number;
@@ -13,11 +14,20 @@ interface LabelData {
}
interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: unknown[];
children?: LayoutData['nodes'];
labelData?: LabelData;
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
}
interface Point {
x: number;
y: number;
}
function distance(p1?: Point, p2?: Point): number {
if (!p1 || !p2) {
return 0;
}
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
export const render = async (
data4Layout: LayoutData,
svg: SVG,
@@ -51,15 +61,30 @@ export const render = async (
// Add the element to the DOM
if (!node.isGroup) {
// Create a clean node object for ELK with only the properties it expects
const child: NodeWithVertex = {
...node,
id: node.id,
width: node.width,
height: node.height,
// Store the original node data for later use
label: node.label,
isGroup: node.isGroup,
shape: node.shape,
padding: node.padding,
cssClasses: node.cssClasses,
cssStyles: node.cssStyles,
look: node.look,
// Include parentId for subgraph processing
parentId: node.parentId,
};
graph.children.push(child);
nodeDb[node.id] = child;
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
const boundingBox = childNodeEl.node()!.getBBox();
// Store the domId separately for rendering, not in the ELK graph
child.domId = childNodeEl;
child.calcIntersect = node.calcIntersect;
child.width = boundingBox.width;
child.height = boundingBox.height;
} else {
@@ -459,302 +484,6 @@ export const render = async (
}
}
function intersectLine(
p1: { y: number; x: number },
p2: { y: number; x: number },
q1: { x: any; y: any },
q2: { x: any; y: any }
) {
log.debug('UIO intersectLine', p1, p2, q1, q2);
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
// p7 and p473.
// let a1, a2, b1, b2, c1, c2;
// let r1, r2, r3, r4;
// let denom, offset, num;
// let x, y;
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
// b1 y + c1 = 0.
const a1 = p2.y - p1.y;
const b1 = p1.x - p2.x;
const c1 = p2.x * p1.y - p1.x * p2.y;
// Compute r3 and r4.
const r3 = a1 * q1.x + b1 * q1.y + c1;
const r4 = a1 * q2.x + b1 * q2.y + c1;
const epsilon = 1e-6;
// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
return /*DON'T_INTERSECT*/;
}
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
const a2 = q2.y - q1.y;
const b2 = q1.x - q2.x;
const c2 = q2.x * q1.y - q1.x * q2.y;
// Compute r1 and r2
const r1 = a2 * p1.x + b2 * p1.y + c2;
const r2 = a2 * p2.x + b2 * p2.y + c2;
// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
return /*DON'T_INTERSECT*/;
}
// Line segments intersect: compute intersection point.
const denom = a1 * b2 - a2 * b1;
if (denom === 0) {
return /*COLLINEAR*/;
}
const offset = Math.abs(denom / 2);
// The denom/2 is to get rounding instead of truncating. It
// is added or subtracted to the numerator, depending upon the
// sign of the numerator.
let num = b1 * c2 - b2 * c1;
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
num = a2 * c1 - a1 * c2;
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
return { x: x, y: y };
}
function sameSign(r1: number, r2: number) {
return r1 * r2 > 0;
}
const diamondIntersection = (
bounds: { x: any; y: any; width: any; height: any },
outsidePoint: { x: number; y: number },
insidePoint: any
) => {
const x1 = bounds.x;
const y1 = bounds.y;
const w = bounds.width; //+ bounds.padding;
const h = bounds.height; // + bounds.padding;
const polyPoints = [
{ x: x1, y: y1 - h / 2 },
{ x: x1 + w / 2, y: y1 },
{ x: x1, y: y1 + h / 2 },
{ x: x1 - w / 2, y: y1 },
];
log.debug(
`APA16 diamondIntersection calc abc89:
outsidePoint: ${JSON.stringify(outsidePoint)}
insidePoint : ${JSON.stringify(insidePoint)}
node-bounds : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
JSON.stringify(polyPoints)
);
const intersections = [];
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
polyPoints.forEach(function (entry) {
minX = Math.min(minX, entry.x);
minY = Math.min(minY, entry.y);
});
const left = x1 - w / 2 - minX;
const top = y1 - h / 2 - minY;
for (let i = 0; i < polyPoints.length; i++) {
const p1 = polyPoints[i];
const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
const intersect = intersectLine(
bounds,
outsidePoint,
{ x: left + p1.x, y: top + p1.y },
{ x: left + p2.x, y: top + p2.y }
);
if (intersect) {
intersections.push(intersect);
}
}
if (!intersections.length) {
return bounds;
}
log.debug('UIO intersections', intersections);
if (intersections.length > 1) {
// More intersections, find the one nearest to edge end point
intersections.sort(function (p, q) {
const pdx = p.x - outsidePoint.x;
const pdy = p.y - outsidePoint.y;
const distp = Math.sqrt(pdx * pdx + pdy * pdy);
const qdx = q.x - outsidePoint.x;
const qdy = q.y - outsidePoint.y;
const distq = Math.sqrt(qdx * qdx + qdy * qdy);
return distp < distq ? -1 : distp === distq ? 0 : 1;
});
}
return intersections[0];
};
const intersection = (
node: { x: any; y: any; width: number; height: number },
outsidePoint: { x: number; y: number },
insidePoint: { x: number; y: number }
) => {
log.debug(`intersection calc abc89:
outsidePoint: ${JSON.stringify(outsidePoint)}
insidePoint : ${JSON.stringify(insidePoint)}
node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`);
const x = node.x;
const y = node.y;
const dx = Math.abs(x - insidePoint.x);
// const dy = Math.abs(y - insidePoint.y);
const w = node.width / 2;
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
const h = node.height / 2;
const Q = Math.abs(outsidePoint.y - insidePoint.y);
const R = Math.abs(outsidePoint.x - insidePoint.x);
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
// Intersection is top or bottom of rect.
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
r = (R * q) / Q;
const res = {
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
};
if (r === 0) {
res.x = outsidePoint.x;
res.y = outsidePoint.y;
}
if (R === 0) {
res.x = outsidePoint.x;
}
if (Q === 0) {
res.y = outsidePoint.y;
}
log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line
return res;
} else {
// Intersection on sides of rect
if (insidePoint.x < outsidePoint.x) {
r = outsidePoint.x - w - x;
} else {
// r = outsidePoint.x - w - x;
r = x - w - outsidePoint.x;
}
const q = (Q * r) / R;
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w;
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
// let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
if (r === 0) {
_x = outsidePoint.x;
_y = outsidePoint.y;
}
if (R === 0) {
_x = outsidePoint.x;
}
if (Q === 0) {
_y = outsidePoint.y;
}
return { x: _x, y: _y };
}
};
const outsideNode = (
node: { x: any; y: any; width: number; height: number },
point: { x: number; y: number }
) => {
const x = node.x;
const y = node.y;
const dx = Math.abs(point.x - x);
const dy = Math.abs(point.y - y);
const w = node.width / 2;
const h = node.height / 2;
if (dx >= w || dy >= h) {
return true;
}
return false;
};
/**
* This function will page a path and node where the last point(s) in the path is inside the node
* and return an update path ending by the border of the node.
*/
const cutPathAtIntersect = (
_points: any[],
bounds: { x: any; y: any; width: any; height: any; padding: any },
isDiamond: boolean
) => {
log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
const points: any[] = [];
let lastPointOutside = _points[0];
let isInside = false;
_points.forEach((point: any) => {
// check if point is inside the boundary rect
if (!outsideNode(bounds, point) && !isInside) {
// First point inside the rect found
// Calc the intersection coord between the point and the last point outside the rect
let inter;
if (isDiamond) {
const inter2 = diamondIntersection(bounds, lastPointOutside, point);
const distance = Math.sqrt(
(lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2
);
if (distance > 1) {
inter = inter2;
}
}
if (!inter) {
inter = intersection(bounds, lastPointOutside, point);
}
// Check case where the intersection is the same as the last point
let pointPresent = false;
points.forEach((p) => {
pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
});
// if (!pointPresent) {
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
points.push(inter);
} else {
log.debug('abc88 no intersect', inter, points);
}
// points.push(inter);
isInside = true;
} else {
// Outside
log.debug('abc88 outside', point, lastPointOutside, points);
lastPointOutside = point;
// points.push(point);
if (!isInside) {
points.push(point);
}
}
});
return points;
};
// @ts-ignore - ELK is not typed
const elk = new ELK();
const element = svg.select('g');
@@ -869,11 +598,16 @@ export const render = async (
delete node.height;
}
});
elkGraph.edges.forEach((edge: any) => {
log.debug('APA01 processing edges, count:', elkGraph.edges.length);
elkGraph.edges.forEach((edge: any, index: number) => {
log.debug('APA01 processing edge', index, ':', edge);
const source = edge.sources[0];
const target = edge.targets[0];
log.debug('APA01 source:', source, 'target:', target);
log.debug('APA01 nodeDb[source]:', nodeDb[source]);
log.debug('APA01 nodeDb[target]:', nodeDb[target]);
if (nodeDb[source].parentId !== nodeDb[target].parentId) {
if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) {
const ancestorId = findCommonAncestor(source, target, parentLookupDb);
// an edge that breaks a subgraph has been identified, set configuration accordingly
setIncludeChildrenPolicy(source, ancestorId);
@@ -881,7 +615,37 @@ export const render = async (
}
});
const g = await elk.layout(elkGraph);
log.debug('APA01 before');
log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2));
log.debug('APA01 elkGraph.children length:', elkGraph.children?.length);
log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length);
// Validate that all edge references exist as nodes
elkGraph.edges?.forEach((edge: any, index: number) => {
log.debug(`APA01 validating edge ${index}:`, edge);
if (edge.sources) {
edge.sources.forEach((sourceId: any) => {
const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId);
log.debug(`APA01 source ${sourceId} exists:`, sourceExists);
});
}
if (edge.targets) {
edge.targets.forEach((targetId: any) => {
const targetExists = elkGraph.children?.some((child: any) => child.id === targetId);
log.debug(`APA01 target ${targetId} exists:`, targetExists);
});
}
});
let g;
try {
g = await elk.layout(elkGraph);
log.debug('APA01 after - success');
log.debug('APA01 layout result:', JSON.stringify(g, null, 2));
} catch (error) {
log.error('APA01 ELK layout error:', error);
throw error;
}
// debugger;
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
@@ -969,42 +733,37 @@ export const render = async (
startNode.innerHTML
);
}
if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
edge.points.unshift({
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
});
}
if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
edge.points.push({
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
});
}
edge.points = cutPathAtIntersect(
edge.points.reverse(),
{
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
width: sw,
height: startNode.height,
padding: startNode.padding,
},
startNode.shape === 'diamond' || startNode.shape === 'diam'
).reverse();
if (startNode.calcIntersect) {
const intersection = startNode.calcIntersect(
{
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
width: startNode.width,
height: startNode.height,
},
edge.points[0]
);
edge.points = cutPathAtIntersect(
edge.points,
{
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
width: ew,
height: endNode.height,
padding: endNode.padding,
},
endNode.shape === 'diamond' || endNode.shape === 'diam'
);
if (distance(intersection, edge.points[0]) > epsilon) {
edge.points.unshift(intersection);
}
}
if (endNode.calcIntersect) {
const intersection = endNode.calcIntersect(
{
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
width: endNode.width,
height: endNode.height,
},
edge.points[edge.points.length - 1]
);
if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) {
edge.points.push(intersection);
}
}
const paths = insertEdge(
edgesEl,
@@ -1015,7 +774,6 @@ export const render = async (
endNode,
data4Layout.diagramId
);
log.info('APA12 edge points after insert', JSON.stringify(edge.points));
edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;

View File

@@ -5,6 +5,6 @@
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts"],
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -0,0 +1,59 @@
# @mermaid-js/layout-tidy-tree
This package provides a bidirectional tidy tree layout engine for Mermaid based on the non-layered-tidy-tree-layout algorithm.
> [!NOTE]
> The Tidy Tree Layout engine will not be available in all providers that support mermaid by default.
> The websites will have to install the @mermaid-js/layout-tidy-tree package to use the Tidy Tree layout engine.
## Usage
```
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
A
B
```
### With bundlers
```sh
npm install @mermaid-js/layout-tidy-tree
```
```ts
import mermaid from 'mermaid';
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
```
### With CDN
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
</script>
```
## Tidy Tree Layout Overview
tidy-tree: The bidirectional tidy tree layout
The bidirectional tidy tree layout algorithm creates two separate trees that grow horizontally in opposite directions from a central root node:
Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
This creates a balanced, symmetric layout that is ideal for mindmaps, organizational charts, and other tree-based diagrams.
Layout Structure:
[Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
↓ ↓ ↓ ↓
[GrandChild] [GrandChild] [GrandChild] [GrandChild]

View File

@@ -0,0 +1,46 @@
{
"name": "@mermaid-js/layout-tidy-tree",
"version": "0.1.0",
"description": "Tidy-tree layout engine for mermaid",
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
"types": "dist/layouts.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-layout-tidy-tree.core.mjs",
"types": "./dist/layouts.d.ts"
},
"./": "./"
},
"keywords": [
"diagram",
"markdown",
"tidy-tree",
"mermaid",
"layout"
],
"scripts": {},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"contributors": [
"Knut Sveidqvist",
"Sidharth Vinod"
],
"license": "MIT",
"dependencies": {
"d3": "^7.9.0",
"non-layered-tidy-tree-layout": "^2.0.2"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"mermaid": "workspace:^"
},
"peerDependencies": {
"mermaid": "^11.0.2"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,50 @@
/**
* Bidirectional Tidy-Tree Layout Algorithm for Generic Diagrams
*
* This module provides a layout algorithm implementation using the
* non-layered-tidy-tree-layout algorithm for positioning nodes and edges
* in tree structures with a bidirectional approach.
*
* The algorithm creates two separate trees that grow horizontally in opposite
* directions from a central root node:
* - Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
* - Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
*
* This creates a balanced, symmetric layout that is ideal for mindmaps,
* organizational charts, and other tree-based diagrams.
*
* The algorithm follows the unified rendering pattern and can be used
* by any diagram type that provides compatible LayoutData.
*/
/**
* Render function for the bidirectional tidy-tree layout algorithm
*
* This function follows the unified rendering pattern used by all layout algorithms.
* It takes LayoutData, inserts nodes into DOM, runs the bidirectional tidy-tree layout algorithm,
* and renders the positioned elements to the SVG.
*
* Features:
* - Alternates root children between left and right trees
* - Left tree grows horizontally to the left (rotated 90° counterclockwise)
* - Right tree grows horizontally to the right (rotated 90° clockwise)
* - Uses tidy-tree algorithm for optimal spacing within each tree
* - Creates symmetric, balanced layouts
* - Maintains proper edge connections between all nodes
*
* Layout Structure:
* ```
* [Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
* ↓ ↓ ↓ ↓
* [GrandChild] [GrandChild] [GrandChild] [GrandChild]
* ```
*
* @param layoutData - Layout data containing nodes, edges, and configuration
* @param svg - SVG element to render to
* @param helpers - Internal helper functions for rendering
* @param options - Rendering options
*/
export { default } from './layouts.js';
export * from './types.js';
export * from './layout.js';
export { render } from './render.js';

View File

@@ -0,0 +1,409 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { executeTidyTreeLayout, validateLayoutData } from './layout.js';
import type { LayoutResult } from './types.js';
import type { LayoutData, MermaidConfig } from 'mermaid';
// Mock non-layered-tidy-tree-layout
vi.mock('non-layered-tidy-tree-layout', () => ({
BoundingBox: vi.fn().mockImplementation(() => ({})),
Layout: vi.fn().mockImplementation(() => ({
layout: vi.fn().mockImplementation((treeData) => {
const result = { ...treeData };
if (result.id?.toString().startsWith('virtual-root')) {
result.x = 0;
result.y = 0;
} else {
result.x = 100;
result.y = 50;
}
if (result.children) {
result.children.forEach((child: any, index: number) => {
child.x = 50 + index * 100;
child.y = 100;
if (child.children) {
child.children.forEach((grandchild: any, gIndex: number) => {
grandchild.x = 25 + gIndex * 50;
grandchild.y = 200;
});
}
});
}
return {
result,
boundingBox: {
left: 0,
right: 200,
top: 0,
bottom: 250,
},
};
}),
})),
}));
describe('Tidy-Tree Layout Algorithm', () => {
let mockConfig: MermaidConfig;
let mockLayoutData: LayoutData;
beforeEach(() => {
mockConfig = {
theme: 'default',
} as MermaidConfig;
mockLayoutData = {
nodes: [
{
id: 'root',
label: 'Root',
isGroup: false,
shape: 'rect',
width: 100,
height: 50,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child1',
label: 'Child 1',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child2',
label: 'Child 2',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child3',
label: 'Child 3',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'child4',
label: 'Child 4',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
],
edges: [
{
id: 'root_child1',
start: 'root',
end: 'child1',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_child2',
start: 'root',
end: 'child2',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_child3',
start: 'root',
end: 'child3',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_child4',
start: 'root',
end: 'child4',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
],
config: mockConfig,
direction: 'TB',
type: 'test',
diagramId: 'test-diagram',
markers: [],
};
});
describe('validateLayoutData', () => {
it('should validate correct layout data', () => {
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
});
it('should throw error for missing data', () => {
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
});
it('should throw error for missing config', () => {
const invalidData = { ...mockLayoutData, config: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
});
it('should throw error for invalid nodes array', () => {
const invalidData = { ...mockLayoutData, nodes: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
});
it('should throw error for invalid edges array', () => {
const invalidData = { ...mockLayoutData, edges: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
});
});
describe('executeTidyTreeLayout function', () => {
it('should execute layout algorithm successfully', async () => {
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
expect(result).toBeDefined();
expect(result.nodes).toBeDefined();
expect(result.edges).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true);
expect(Array.isArray(result.edges)).toBe(true);
});
it('should return positioned nodes with coordinates', async () => {
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
expect(result.nodes.length).toBeGreaterThan(0);
result.nodes.forEach((node) => {
expect(node.x).toBeDefined();
expect(node.y).toBeDefined();
expect(typeof node.x).toBe('number');
expect(typeof node.y).toBe('number');
});
});
it('should return positioned edges with coordinates', async () => {
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
expect(result.edges.length).toBeGreaterThan(0);
result.edges.forEach((edge) => {
expect(edge.startX).toBeDefined();
expect(edge.startY).toBeDefined();
expect(edge.midX).toBeDefined();
expect(edge.midY).toBeDefined();
expect(edge.endX).toBeDefined();
expect(edge.endY).toBeDefined();
});
});
it('should handle empty layout data gracefully', async () => {
const emptyData: LayoutData = {
...mockLayoutData,
nodes: [],
edges: [],
};
await expect(executeTidyTreeLayout(emptyData)).rejects.toThrow(
'No nodes found in layout data'
);
});
it('should throw error for missing nodes', async () => {
const invalidData = { ...mockLayoutData, nodes: [] };
await expect(executeTidyTreeLayout(invalidData)).rejects.toThrow(
'No nodes found in layout data'
);
});
it('should handle empty edges (single node tree)', async () => {
const singleNodeData = {
...mockLayoutData,
edges: [],
nodes: [mockLayoutData.nodes[0]],
};
const result = await executeTidyTreeLayout(singleNodeData);
expect(result).toBeDefined();
expect(result.nodes).toHaveLength(1);
expect(result.edges).toHaveLength(0);
});
it('should create bidirectional dual-tree layout with alternating left/right children', async () => {
const result = await executeTidyTreeLayout(mockLayoutData);
expect(result).toBeDefined();
expect(result.nodes).toHaveLength(5);
const rootNode = result.nodes.find((node) => node.id === 'root');
expect(rootNode).toBeDefined();
expect(rootNode!.x).toBe(0);
expect(rootNode!.y).toBe(20);
const child1 = result.nodes.find((node) => node.id === 'child1');
const child2 = result.nodes.find((node) => node.id === 'child2');
const child3 = result.nodes.find((node) => node.id === 'child3');
const child4 = result.nodes.find((node) => node.id === 'child4');
expect(child1).toBeDefined();
expect(child2).toBeDefined();
expect(child3).toBeDefined();
expect(child4).toBeDefined();
expect(child1!.x).toBeLessThan(rootNode!.x);
expect(child2!.x).toBeGreaterThan(rootNode!.x);
expect(child3!.x).toBeLessThan(rootNode!.x);
expect(child4!.x).toBeGreaterThan(rootNode!.x);
expect(child1!.x).toBeLessThan(-100);
expect(child3!.x).toBeLessThan(-100);
expect(child2!.x).toBeGreaterThan(100);
expect(child4!.x).toBeGreaterThan(100);
});
it('should correctly transpose coordinates to prevent high nodes from covering nodes above them', async () => {
const testData = {
...mockLayoutData,
nodes: [
{
id: 'root',
label: 'Root',
isGroup: false,
shape: 'rect' as const,
width: 100,
height: 50,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'tall-child',
label: 'Tall Child',
isGroup: false,
shape: 'rect' as const,
width: 80,
height: 120,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: 'short-child',
label: 'Short Child',
isGroup: false,
shape: 'rect' as const,
width: 80,
height: 30,
padding: 10,
x: 0,
y: 0,
cssClasses: '',
cssStyles: [],
look: 'default',
},
],
edges: [
{
id: 'root_tall',
start: 'root',
end: 'tall-child',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
{
id: 'root_short',
start: 'root',
end: 'short-child',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
],
};
const result = await executeTidyTreeLayout(testData);
expect(result).toBeDefined();
expect(result.nodes).toHaveLength(3);
const rootNode = result.nodes.find((node) => node.id === 'root');
const tallChild = result.nodes.find((node) => node.id === 'tall-child');
const shortChild = result.nodes.find((node) => node.id === 'short-child');
expect(rootNode).toBeDefined();
expect(tallChild).toBeDefined();
expect(shortChild).toBeDefined();
expect(tallChild!.x).not.toBe(shortChild!.x);
expect(tallChild!.width).toBe(80);
expect(tallChild!.height).toBe(120);
expect(shortChild!.width).toBe(80);
expect(shortChild!.height).toBe(30);
const yDifference = Math.abs(tallChild!.y - shortChild!.y);
expect(yDifference).toBeGreaterThanOrEqual(0);
});
});
});

View File

@@ -0,0 +1,629 @@
import type { LayoutData } from 'mermaid';
import type { Bounds, Point } from 'mermaid/src/types.js';
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
import type {
Edge,
LayoutResult,
Node,
PositionedEdge,
PositionedNode,
TidyTreeNode,
} from './types.js';
/**
* Execute the tidy-tree layout algorithm on generic layout data
*
* This function takes layout data and uses the non-layered-tidy-tree-layout
* algorithm to calculate optimal node positions for tree structures.
*
* @param data - The layout data containing nodes, edges, and configuration
* @param config - Mermaid configuration object
* @returns Promise resolving to layout result with positioned nodes and edges
*/
export function executeTidyTreeLayout(data: LayoutData): Promise<LayoutResult> {
let intersectionShift = 50;
return new Promise((resolve, reject) => {
try {
if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) {
throw new Error('No nodes found in layout data');
}
if (!data.edges || !Array.isArray(data.edges)) {
data.edges = [];
}
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
const gap = 20;
const bottomPadding = 40;
intersectionShift = 30;
const bb = new BoundingBox(gap, bottomPadding);
const layout = new Layout(bb);
let leftResult = null;
let rightResult = null;
if (leftTree) {
const leftLayoutResult = layout.layout(leftTree);
leftResult = leftLayoutResult.result;
}
if (rightTree) {
const rightLayoutResult = layout.layout(rightTree);
rightResult = rightLayoutResult.result;
}
const positionedNodes = combineAndPositionTrees(rootNode, leftResult, rightResult);
const positionedEdges = calculateEdgePositions(
data.edges,
positionedNodes,
intersectionShift
);
resolve({
nodes: positionedNodes,
edges: positionedEdges,
});
} catch (error) {
reject(error);
}
});
}
/**
* Convert LayoutData to dual-tree format (left and right trees)
*
* This function builds two separate tree structures from the nodes and edges,
* alternating children between left and right trees.
*/
function convertToDualTreeFormat(data: LayoutData): {
leftTree: TidyTreeNode | null;
rightTree: TidyTreeNode | null;
rootNode: TidyTreeNode;
} {
const { nodes, edges } = data;
const nodeMap = new Map<string, Node>();
nodes.forEach((node) => nodeMap.set(node.id, node));
const children = new Map<string, string[]>();
const parents = new Map<string, string>();
edges.forEach((edge) => {
const parentId = edge.start;
const childId = edge.end;
if (parentId && childId) {
if (!children.has(parentId)) {
children.set(parentId, []);
}
children.get(parentId)!.push(childId);
parents.set(childId, parentId);
}
});
const rootNodeData = nodes.find((node) => !parents.has(node.id));
if (!rootNodeData && nodes.length === 0) {
throw new Error('No nodes available to create tree');
}
const actualRoot = rootNodeData ?? nodes[0];
const rootNode: TidyTreeNode = {
id: actualRoot.id,
width: actualRoot.width ?? 100,
height: actualRoot.height ?? 50,
_originalNode: actualRoot,
};
const rootChildren = children.get(actualRoot.id) ?? [];
const leftChildren: string[] = [];
const rightChildren: string[] = [];
rootChildren.forEach((childId, index) => {
if (index % 2 === 0) {
leftChildren.push(childId);
} else {
rightChildren.push(childId);
}
});
const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null;
const rightTree =
rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null;
return { leftTree, rightTree, rootNode };
}
/**
* Build a subtree from a list of root children
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
*/
function buildSubTree(
rootChildren: string[],
children: Map<string, string[]>,
nodeMap: Map<string, Node>
): TidyTreeNode {
const virtualRoot: TidyTreeNode = {
id: `virtual-root-${Math.random()}`,
width: 1,
height: 1,
children: rootChildren
.map((childId) => nodeMap.get(childId))
.filter((child): child is Node => child !== undefined)
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
};
return virtualRoot;
}
/**
* Recursively convert a node and its children to tidy-tree format
* This version transposes width/height for horizontal tree layout
*/
function convertNodeToTidyTreeTransposed(
node: Node,
children: Map<string, string[]>,
nodeMap: Map<string, Node>
): TidyTreeNode {
const childIds = children.get(node.id) ?? [];
const childNodes = childIds
.map((childId) => nodeMap.get(childId))
.filter((child): child is Node => child !== undefined)
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
return {
id: node.id,
width: node.height ?? 50,
height: node.width ?? 100,
children: childNodes.length > 0 ? childNodes : undefined,
_originalNode: node,
};
}
/**
* Combine and position the left and right trees around the root node
* Creates a bidirectional layout where left tree grows left and right tree grows right
*/
function combineAndPositionTrees(
rootNode: TidyTreeNode,
leftResult: TidyTreeNode | null,
rightResult: TidyTreeNode | null
): PositionedNode[] {
const positionedNodes: PositionedNode[] = [];
const rootX = 0;
const rootY = 0;
const treeSpacing = rootNode.width / 2 + 30;
const leftTreeNodes: PositionedNode[] = [];
const rightTreeNodes: PositionedNode[] = [];
if (leftResult?.children) {
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
}
if (rightResult?.children) {
positionRightTreeBidirectional(
rightResult.children,
rightTreeNodes,
rootX + treeSpacing,
rootY
);
}
let leftTreeCenterY = 0;
let rightTreeCenterY = 0;
if (leftTreeNodes.length > 0) {
const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
(a, b) => b - a
);
const firstLevelLeftX = leftTreeXPositions[0];
const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
if (firstLevelLeftNodes.length > 0) {
const leftMinY = Math.min(
...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2)
);
const leftMaxY = Math.max(
...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2)
);
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
}
}
if (rightTreeNodes.length > 0) {
const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
(a, b) => a - b
);
const firstLevelRightX = rightTreeXPositions[0];
const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
if (firstLevelRightNodes.length > 0) {
const rightMinY = Math.min(
...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2)
);
const rightMaxY = Math.max(
...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2)
);
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
}
}
const leftTreeOffset = -leftTreeCenterY;
const rightTreeOffset = -rightTreeCenterY;
positionedNodes.push({
id: String(rootNode.id),
x: rootX,
y: rootY + 20,
section: 'root',
width: rootNode._originalNode?.width ?? rootNode.width,
height: rootNode._originalNode?.height ?? rootNode.height,
originalNode: rootNode._originalNode,
});
const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({
id: node.id,
x: node.x - (node.width ?? 0) / 2,
y: node.y + leftTreeOffset + (node.height ?? 0) / 2,
section: 'left' as const,
width: node.width,
height: node.height,
originalNode: node.originalNode,
}));
const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({
id: node.id,
x: node.x + (node.width ?? 0) / 2,
y: node.y + rightTreeOffset + (node.height ?? 0) / 2,
section: 'right' as const,
width: node.width,
height: node.height,
originalNode: node.originalNode,
}));
positionedNodes.push(...leftTreeNodesWithOffset);
positionedNodes.push(...rightTreeNodesWithOffset);
return positionedNodes;
}
/**
* Position nodes from the left tree in a bidirectional layout (grows to the left)
* Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left
*/
function positionLeftTreeBidirectional(
nodes: TidyTreeNode[],
positionedNodes: PositionedNode[],
offsetX: number,
offsetY: number
): void {
nodes.forEach((node) => {
const distanceFromRoot = node.y ?? 0;
const verticalPosition = node.x ?? 0;
const originalWidth = node._originalNode?.width ?? 100;
const originalHeight = node._originalNode?.height ?? 50;
const adjustedY = offsetY + verticalPosition;
positionedNodes.push({
id: String(node.id),
x: offsetX - distanceFromRoot,
y: adjustedY,
width: originalWidth,
height: originalHeight,
originalNode: node._originalNode,
});
if (node.children) {
positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
}
});
}
/**
* Position nodes from the right tree in a bidirectional layout (grows to the right)
* Rotates the tree 90 degrees clockwise so it grows horizontally to the right
*/
function positionRightTreeBidirectional(
nodes: TidyTreeNode[],
positionedNodes: PositionedNode[],
offsetX: number,
offsetY: number
): void {
nodes.forEach((node) => {
const distanceFromRoot = node.y ?? 0;
const verticalPosition = node.x ?? 0;
const originalWidth = node._originalNode?.width ?? 100;
const originalHeight = node._originalNode?.height ?? 50;
const adjustedY = offsetY + verticalPosition;
positionedNodes.push({
id: String(node.id),
x: offsetX + distanceFromRoot,
y: adjustedY,
width: originalWidth,
height: originalHeight,
originalNode: node._originalNode,
});
if (node.children) {
positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
}
});
}
/**
* Calculate the intersection point of a line with a circle
* @param circle - Circle coordinates and radius
* @param lineStart - Starting point of the line
* @param lineEnd - Ending point of the line
* @returns The intersection point
*/
function computeCircleEdgeIntersection(circle: Bounds, lineStart: Point, lineEnd: Point): Point {
const radius = Math.min(circle.width, circle.height) / 2;
const dx = lineEnd.x - lineStart.x;
const dy = lineEnd.y - lineStart.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
return lineStart;
}
const nx = dx / length;
const ny = dy / length;
return {
x: circle.x - nx * radius,
y: circle.y - ny * radius,
};
}
function intersection(node: PositionedNode, outsidePoint: Point, insidePoint: Point): Point {
const x = node.x;
const y = node.y;
if (!node.width || !node.height) {
return { x: outsidePoint.x, y: outsidePoint.y };
}
const dx = Math.abs(x - insidePoint.x);
const w = node?.width / 2;
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
const h = node.height / 2;
const Q = Math.abs(outsidePoint.y - insidePoint.y);
const R = Math.abs(outsidePoint.x - insidePoint.x);
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
// Intersection is top or bottom of rect.
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
r = (R * q) / Q;
const res = {
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
};
if (r === 0) {
res.x = outsidePoint.x;
res.y = outsidePoint.y;
}
if (R === 0) {
res.x = outsidePoint.x;
}
if (Q === 0) {
res.y = outsidePoint.y;
}
return res;
} else {
if (insidePoint.x < outsidePoint.x) {
r = outsidePoint.x - w - x;
} else {
r = x - w - outsidePoint.x;
}
const q = (Q * r) / R;
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
if (r === 0) {
_x = outsidePoint.x;
_y = outsidePoint.y;
}
if (R === 0) {
_x = outsidePoint.x;
}
if (Q === 0) {
_y = outsidePoint.y;
}
return { x: _x, y: _y };
}
}
/**
* Calculate edge positions based on positioned nodes
* Now includes tree membership and node dimensions for precise edge calculations
* Edges now stop at shape boundaries instead of extending to centers
*/
function calculateEdgePositions(
edges: Edge[],
positionedNodes: PositionedNode[],
intersectionShift: number
): PositionedEdge[] {
const nodeInfo = new Map<string, PositionedNode>();
positionedNodes.forEach((node) => {
nodeInfo.set(node.id, node);
});
return edges.map((edge) => {
const sourceNode = nodeInfo.get(edge.start ?? '');
const targetNode = nodeInfo.get(edge.end ?? '');
if (!sourceNode || !targetNode) {
return {
id: edge.id,
source: edge.start ?? '',
target: edge.end ?? '',
startX: 0,
startY: 0,
midX: 0,
midY: 0,
endX: 0,
endY: 0,
points: [{ x: 0, y: 0 }],
sourceSection: undefined,
targetSection: undefined,
sourceWidth: undefined,
sourceHeight: undefined,
targetWidth: undefined,
targetHeight: undefined,
};
}
const sourceCenter = { x: sourceNode.x, y: sourceNode.y };
const targetCenter = { x: targetNode.x, y: targetNode.y };
const isSourceRound = ['circle', 'cloud', 'bang'].includes(
sourceNode.originalNode?.shape ?? ''
);
const isTargetRound = ['circle', 'cloud', 'bang'].includes(
targetNode.originalNode?.shape ?? ''
);
let startPos = isSourceRound
? computeCircleEdgeIntersection(
{
x: sourceNode.x,
y: sourceNode.y,
width: sourceNode.width ?? 100,
height: sourceNode.height ?? 100,
},
targetCenter,
sourceCenter
)
: intersection(sourceNode, sourceCenter, targetCenter);
let endPos = isTargetRound
? computeCircleEdgeIntersection(
{
x: targetNode.x,
y: targetNode.y,
width: targetNode.width ?? 100,
height: targetNode.height ?? 100,
},
sourceCenter,
targetCenter
)
: intersection(targetNode, targetCenter, sourceCenter);
const midX = (startPos.x + endPos.x) / 2;
const midY = (startPos.y + endPos.y) / 2;
const points = [startPos];
if (sourceNode.section === 'left') {
points.push({
x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
y: sourceNode.y,
});
} else if (sourceNode.section === 'right') {
points.push({
x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
y: sourceNode.y,
});
}
if (targetNode.section === 'left') {
points.push({
x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
y: targetNode.y,
});
} else if (targetNode.section === 'right') {
points.push({
x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
y: targetNode.y,
});
}
points.push(endPos);
const secondPoint = points.length > 1 ? points[1] : targetCenter;
startPos = isSourceRound
? computeCircleEdgeIntersection(
{
x: sourceNode.x,
y: sourceNode.y,
width: sourceNode.width ?? 100,
height: sourceNode.height ?? 100,
},
secondPoint,
sourceCenter
)
: intersection(sourceNode, secondPoint, sourceCenter);
points[0] = startPos;
const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter;
endPos = isTargetRound
? computeCircleEdgeIntersection(
{
x: targetNode.x,
y: targetNode.y,
width: targetNode.width ?? 100,
height: targetNode.height ?? 100,
},
secondLastPoint,
targetCenter
)
: intersection(targetNode, secondLastPoint, targetCenter);
points[points.length - 1] = endPos;
return {
id: edge.id,
source: edge.start ?? '',
target: edge.end ?? '',
startX: startPos.x,
startY: startPos.y,
midX,
midY,
endX: endPos.x,
endY: endPos.y,
points,
sourceSection: sourceNode?.section,
targetSection: targetNode?.section,
sourceWidth: sourceNode?.width,
sourceHeight: sourceNode?.height,
targetWidth: targetNode?.width,
targetHeight: targetNode?.height,
};
});
}
/**
* Validate layout data structure
* @param data - The data to validate
* @returns True if data is valid, throws error otherwise
*/
export function validateLayoutData(data: LayoutData): boolean {
if (!data) {
throw new Error('Layout data is required');
}
if (!data.config) {
throw new Error('Configuration is required in layout data');
}
if (!Array.isArray(data.nodes)) {
throw new Error('Nodes array is required in layout data');
}
if (!Array.isArray(data.edges)) {
throw new Error('Edges array is required in layout data');
}
return true;
}

View File

@@ -0,0 +1,13 @@
import type { LayoutLoaderDefinition } from 'mermaid';
const loader = async () => await import(`./render.js`);
const tidyTreeLayout: LayoutLoaderDefinition[] = [
{
name: 'tidy-tree',
loader,
algorithm: 'tidy-tree',
},
];
export default tidyTreeLayout;

View File

@@ -0,0 +1,18 @@
declare module 'non-layered-tidy-tree-layout' {
export class BoundingBox {
constructor(gap: number, bottomPadding: number);
}
export class Layout {
constructor(boundingBox: BoundingBox);
layout(data: any): {
result: any;
boundingBox: {
left: number;
right: number;
top: number;
bottom: number;
};
};
}
}

View File

@@ -0,0 +1,180 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
import { executeTidyTreeLayout } from './layout.js';
interface NodeWithPosition {
id: string;
x?: number;
y?: number;
width?: number;
height?: number;
domId?: any;
[key: string]: any;
}
/**
* Render function for bidirectional tidy-tree layout algorithm
*
* This follows the same pattern as ELK and dagre renderers:
* 1. Insert nodes into DOM to get their actual dimensions
* 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
* 3. Position the nodes and edges based on layout results
*
* The bidirectional layout creates two trees that grow horizontally in opposite
* directions from a central root node:
* - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
* - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
*/
export const render = async (
data4Layout: LayoutData,
svg: SVG,
{
insertCluster,
insertEdge,
insertEdgeLabel,
insertMarkers,
insertNode,
log,
positionEdgeLabel,
}: InternalHelpers,
{ algorithm: _algorithm }: RenderOptions
) => {
const nodeDb: Record<string, NodeWithPosition> = {};
const clusterDb: Record<string, any> = {};
const element = svg.select('g');
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
const edgePaths = element.insert('g').attr('class', 'edgePaths');
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
const nodes = element.insert('g').attr('class', 'nodes');
// Step 1: Insert nodes into DOM to get their actual dimensions
log.debug('Inserting nodes into DOM for dimension calculation');
await Promise.all(
data4Layout.nodes.map(async (node) => {
if (node.isGroup) {
const clusterNode: NodeWithPosition = {
...node,
id: node.id,
width: node.width,
height: node.height,
};
clusterDb[node.id] = clusterNode;
nodeDb[node.id] = clusterNode;
await insertCluster(subGraphsEl, node);
} else {
const nodeWithPosition: NodeWithPosition = {
...node,
id: node.id,
width: node.width,
height: node.height,
};
nodeDb[node.id] = nodeWithPosition;
const nodeEl = await insertNode(nodes, node, {
config: data4Layout.config,
dir: data4Layout.direction || 'TB',
});
const boundingBox = nodeEl.node()!.getBBox();
nodeWithPosition.width = boundingBox.width;
nodeWithPosition.height = boundingBox.height;
nodeWithPosition.domId = nodeEl;
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
}
})
);
// Step 2: Run the bidirectional tidy-tree layout algorithm
log.debug('Running bidirectional tidy-tree layout algorithm');
const updatedLayoutData = {
...data4Layout,
nodes: data4Layout.nodes.map((node) => {
const nodeWithDimensions = nodeDb[node.id];
return {
...node,
width: nodeWithDimensions.width ?? node.width ?? 100,
height: nodeWithDimensions.height ?? node.height ?? 50,
};
}),
};
const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
// Step 3: Position the nodes based on bidirectional layout results
log.debug('Positioning nodes based on bidirectional layout results');
layoutResult.nodes.forEach((positionedNode) => {
const node = nodeDb[positionedNode.id];
if (node?.domId) {
// Position the node at the calculated coordinates from bidirectional layout
// The layout algorithm has already calculated positions for:
// - Root node at center (0, 0)
// - Left tree nodes with negative x coordinates (growing left)
// - Right tree nodes with positive x coordinates (growing right)
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
// Store the final position
node.x = positionedNode.x;
node.y = positionedNode.y;
// Step 3: Position the nodes based on bidirectional layout results
log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
}
});
log.debug('Inserting and positioning edges');
await Promise.all(
data4Layout.edges.map(async (edge) => {
await insertEdgeLabel(edgeLabels, edge);
const startNode = nodeDb[edge.start ?? ''];
const endNode = nodeDb[edge.end ?? ''];
if (startNode && endNode) {
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
if (positionedEdge) {
log.debug('APA01 positionedEdge', positionedEdge);
const edgeWithPath = {
...edge,
points: positionedEdge.points,
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
} else {
const edgeWithPath = {
...edge,
points: [
{ x: startNode.x ?? 0, y: startNode.y ?? 0 },
{ x: endNode.x ?? 0, y: endNode.y ?? 0 },
],
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
}
}
})
);
log.debug('Bidirectional tidy-tree rendering completed');
};

View File

@@ -0,0 +1,69 @@
import type { LayoutData } from 'mermaid';
export type Node = LayoutData['nodes'][number];
export type Edge = LayoutData['edges'][number];
/**
* Positioned node after layout calculation
*/
export interface PositionedNode {
id: string;
x: number;
y: number;
section?: 'root' | 'left' | 'right';
width?: number;
height?: number;
originalNode?: Node;
[key: string]: unknown;
}
/**
* Positioned edge after layout calculation
*/
export interface PositionedEdge {
id: string;
source: string;
target: string;
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
sourceSection?: 'root' | 'left' | 'right';
targetSection?: 'root' | 'left' | 'right';
sourceWidth?: number;
sourceHeight?: number;
targetWidth?: number;
targetHeight?: number;
[key: string]: unknown;
}
/**
* Result of layout algorithm execution
*/
export interface LayoutResult {
nodes: PositionedNode[];
edges: PositionedEdge[];
}
/**
* Tidy-tree node structure compatible with non-layered-tidy-tree-layout
*/
export interface TidyTreeNode {
id: string | number;
width: number;
height: number;
x?: number;
y?: number;
children?: TidyTreeNode[];
_originalNode?: Node;
}
/**
* Tidy-tree layout configuration
*/
export interface TidyTreeLayoutConfig {
gap: number;
bottomPadding: number;
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -154,7 +154,6 @@
### Minor Changes
- [#6408](https://github.com/mermaid-js/mermaid/pull/6408) [`ad65313`](https://github.com/mermaid-js/mermaid/commit/ad653138e16765d095613a6e5de86dc5e52ac8f0) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix: restore curve type configuration functionality for flowcharts. This fixes the issue where curve type settings were not being applied when configured through any of the following methods:
- Config
- Init directive (%%{ init: { 'flowchart': { 'curve': '...' } } }%%)
- LinkStyle command (linkStyle default interpolate ...)
@@ -173,14 +172,12 @@
### Minor Changes
- [#6187](https://github.com/mermaid-js/mermaid/pull/6187) [`7809b5a`](https://github.com/mermaid-js/mermaid/commit/7809b5a93fae127f45727071f5ff14325222c518) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line
- [#6136](https://github.com/mermaid-js/mermaid/pull/6136) [`ec0d9c3`](https://github.com/mermaid-js/mermaid/commit/ec0d9c389aa6018043187654044c1e0b5aa4f600) Thanks [@knsv](https://github.com/knsv)! - Adding support for animation of flowchart edges
- [#6373](https://github.com/mermaid-js/mermaid/pull/6373) [`05bdf0e`](https://github.com/mermaid-js/mermaid/commit/05bdf0e20e2629fe77513218fbd4e28e65f75882) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Upgrade Requirement and ER diagram to use the common renderer flow
- Added support for directions
- Added support for hand drawn look
@@ -229,7 +226,6 @@
- [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type
- [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes:
- Updates the class diagram to the new unified way of rendering.
- Includes a new "classBox" shape to be used in diagrams
- Other updates such as:

View File

@@ -82,7 +82,7 @@
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.0.0",
"marked": "^15.0.7",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
@@ -123,8 +123,8 @@
"rimraf": "^6.0.1",
"start-server-and-test": "^2.0.10",
"type-fest": "^4.35.0",
"typedoc": "^0.27.8",
"typedoc-plugin-markdown": "^4.4.2",
"typedoc": "^0.28.9",
"typedoc-plugin-markdown": "^4.8.0",
"typescript": "~5.7.3",
"unist-util-flatmap": "^1.0.0",
"unist-util-visit": "^5.0.0",

View File

@@ -169,53 +169,56 @@ This Markdown should be kept.
describe('buildShapeDoc', () => {
it('should build shapesTable based on the shapeDefs', () => {
expect(buildShapeDoc()).toMatchInlineSnapshot(`
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
| Comment Right | Curly Brace | \`brace-r\` | Adds a comment | |
| Comment with braces on both sides | Curly Braces | \`braces\` | Adds a comment | |
| Data Input/Output | Lean Right | \`lean-r\` | Represents input or output | \`in-out\`, \`lean-right\` |
| Data Input/Output | Lean Left | \`lean-l\` | Represents output or input | \`lean-left\`, \`out-in\` |
| Database | Cylinder | \`cyl\` | Database storage | \`cylinder\`, \`database\`, \`db\` |
| Decision | Diamond | \`diam\` | Decision-making step | \`decision\`, \`diamond\`, \`question\` |
| Delay | Half-Rounded Rectangle | \`delay\` | Represents a delay | \`half-rounded-rectangle\` |
| Direct Access Storage | Horizontal Cylinder | \`h-cyl\` | Direct access storage | \`das\`, \`horizontal-cylinder\` |
| Disk Storage | Lined Cylinder | \`lin-cyl\` | Disk storage | \`disk\`, \`lined-cylinder\` |
| Display | Curved Trapezoid | \`curv-trap\` | Represents a display | \`curved-trapezoid\`, \`display\` |
| Divided Process | Divided Rectangle | \`div-rect\` | Divided process shape | \`div-proc\`, \`divided-process\`, \`divided-rectangle\` |
| Document | Document | \`doc\` | Represents a document | \`doc\`, \`document\` |
| Event | Rounded Rectangle | \`rounded\` | Represents an event | \`event\` |
| Extract | Triangle | \`tri\` | Extraction process | \`extract\`, \`triangle\` |
| Fork/Join | Filled Rectangle | \`fork\` | Fork or join in process flow | \`join\` |
| Internal Storage | Window Pane | \`win-pane\` | Internal storage | \`internal-storage\`, \`window-pane\` |
| Junction | Filled Circle | \`f-circ\` | Junction point | \`filled-circle\`, \`junction\` |
| Lined Document | Lined Document | \`lin-doc\` | Lined document | \`lined-document\` |
| Lined/Shaded Process | Lined Rectangle | \`lin-rect\` | Lined process shape | \`lin-proc\`, \`lined-process\`, \`lined-rectangle\`, \`shaded-process\` |
| Loop Limit | Trapezoidal Pentagon | \`notch-pent\` | Loop limit step | \`loop-limit\`, \`notched-pentagon\` |
| Manual File | Flipped Triangle | \`flip-tri\` | Manual file operation | \`flipped-triangle\`, \`manual-file\` |
| Manual Input | Sloped Rectangle | \`sl-rect\` | Manual input step | \`manual-input\`, \`sloped-rectangle\` |
| Manual Operation | Trapezoid Base Top | \`trap-t\` | Represents a manual task | \`inv-trapezoid\`, \`manual\`, \`trapezoid-top\` |
| Multi-Document | Stacked Document | \`docs\` | Multiple documents | \`documents\`, \`st-doc\`, \`stacked-document\` |
| Multi-Process | Stacked Rectangle | \`st-rect\` | Multiple processes | \`processes\`, \`procs\`, \`stacked-rectangle\` |
| Odd | Odd | \`odd\` | Odd shape | |
| Paper Tape | Flag | \`flag\` | Paper tape | \`paper-tape\` |
| Prepare Conditional | Hexagon | \`hex\` | Preparation or condition step | \`hexagon\`, \`prepare\` |
| Priority Action | Trapezoid Base Bottom | \`trap-b\` | Priority action | \`priority\`, \`trapezoid\`, \`trapezoid-bottom\` |
| Process | Rectangle | \`rect\` | Standard process shape | \`proc\`, \`process\`, \`rectangle\` |
| Start | Circle | \`circle\` | Starting point | \`circ\` |
| Start | Small Circle | \`sm-circ\` | Small starting point | \`small-circle\`, \`start\` |
| Stop | Double Circle | \`dbl-circ\` | Represents a stop point | \`double-circle\` |
| Stop | Framed Circle | \`fr-circ\` | Stop point | \`framed-circle\`, \`stop\` |
| Stored Data | Bow Tie Rectangle | \`bow-rect\` | Stored data | \`bow-tie-rectangle\`, \`stored-data\` |
| Subprocess | Framed Rectangle | \`fr-rect\` | Subprocess | \`framed-rectangle\`, \`subproc\`, \`subprocess\`, \`subroutine\` |
| Summary | Crossed Circle | \`cross-circ\` | Summary | \`crossed-circle\`, \`summary\` |
| Tagged Document | Tagged Document | \`tag-doc\` | Tagged document | \`tag-doc\`, \`tagged-document\` |
| Tagged Process | Tagged Rectangle | \`tag-rect\` | Tagged process | \`tag-proc\`, \`tagged-process\`, \`tagged-rectangle\` |
| Terminal Point | Stadium | \`stadium\` | Terminal point | \`pill\`, \`terminal\` |
| Text Block | Text Block | \`text\` | Text block | |
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | ----------------- | ------------------------------ | ---------------------------------------------------------------- |
| Bang | Bang | \`bang\` | Bang | \`bang\` |
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
| Cloud | Cloud | \`cloud\` | cloud | \`cloud\` |
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
| Comment Right | Curly Brace | \`brace-r\` | Adds a comment | |
| Comment with braces on both sides | Curly Braces | \`braces\` | Adds a comment | |
| Data Input/Output | Lean Right | \`lean-r\` | Represents input or output | \`in-out\`, \`lean-right\` |
| Data Input/Output | Lean Left | \`lean-l\` | Represents output or input | \`lean-left\`, \`out-in\` |
| Database | Cylinder | \`cyl\` | Database storage | \`cylinder\`, \`database\`, \`db\` |
| Decision | Diamond | \`diam\` | Decision-making step | \`decision\`, \`diamond\`, \`question\` |
| Default Mindmap Node | defaultMindmapNode | \`default-mindmap\` | defaultMindmapNode | \`default-mindmap\`, \`defaultMindmapNode\` |
| Delay | Half-Rounded Rectangle | \`delay\` | Represents a delay | \`half-rounded-rectangle\` |
| Direct Access Storage | Horizontal Cylinder | \`h-cyl\` | Direct access storage | \`das\`, \`horizontal-cylinder\` |
| Disk Storage | Lined Cylinder | \`lin-cyl\` | Disk storage | \`disk\`, \`lined-cylinder\` |
| Display | Curved Trapezoid | \`curv-trap\` | Represents a display | \`curved-trapezoid\`, \`display\` |
| Divided Process | Divided Rectangle | \`div-rect\` | Divided process shape | \`div-proc\`, \`divided-process\`, \`divided-rectangle\` |
| Document | Document | \`doc\` | Represents a document | \`doc\`, \`document\` |
| Event | Rounded Rectangle | \`rounded\` | Represents an event | \`event\` |
| Extract | Triangle | \`tri\` | Extraction process | \`extract\`, \`triangle\` |
| Fork/Join | Filled Rectangle | \`fork\` | Fork or join in process flow | \`join\` |
| Internal Storage | Window Pane | \`win-pane\` | Internal storage | \`internal-storage\`, \`window-pane\` |
| Junction | Filled Circle | \`f-circ\` | Junction point | \`filled-circle\`, \`junction\` |
| Lined Document | Lined Document | \`lin-doc\` | Lined document | \`lined-document\` |
| Lined/Shaded Process | Lined Rectangle | \`lin-rect\` | Lined process shape | \`lin-proc\`, \`lined-process\`, \`lined-rectangle\`, \`shaded-process\` |
| Loop Limit | Trapezoidal Pentagon | \`notch-pent\` | Loop limit step | \`loop-limit\`, \`notched-pentagon\` |
| Manual File | Flipped Triangle | \`flip-tri\` | Manual file operation | \`flipped-triangle\`, \`manual-file\` |
| Manual Input | Sloped Rectangle | \`sl-rect\` | Manual input step | \`manual-input\`, \`sloped-rectangle\` |
| Manual Operation | Trapezoid Base Top | \`trap-t\` | Represents a manual task | \`inv-trapezoid\`, \`manual\`, \`trapezoid-top\` |
| Multi-Document | Stacked Document | \`docs\` | Multiple documents | \`documents\`, \`st-doc\`, \`stacked-document\` |
| Multi-Process | Stacked Rectangle | \`st-rect\` | Multiple processes | \`processes\`, \`procs\`, \`stacked-rectangle\` |
| Odd | Odd | \`odd\` | Odd shape | |
| Paper Tape | Flag | \`flag\` | Paper tape | \`paper-tape\` |
| Prepare Conditional | Hexagon | \`hex\` | Preparation or condition step | \`hexagon\`, \`prepare\` |
| Priority Action | Trapezoid Base Bottom | \`trap-b\` | Priority action | \`priority\`, \`trapezoid\`, \`trapezoid-bottom\` |
| Process | Rectangle | \`rect\` | Standard process shape | \`proc\`, \`process\`, \`rectangle\` |
| Start | Circle | \`circle\` | Starting point | \`circ\` |
| Start | Small Circle | \`sm-circ\` | Small starting point | \`small-circle\`, \`start\` |
| Stop | Double Circle | \`dbl-circ\` | Represents a stop point | \`double-circle\` |
| Stop | Framed Circle | \`fr-circ\` | Stop point | \`framed-circle\`, \`stop\` |
| Stored Data | Bow Tie Rectangle | \`bow-rect\` | Stored data | \`bow-tie-rectangle\`, \`stored-data\` |
| Subprocess | Framed Rectangle | \`fr-rect\` | Subprocess | \`framed-rectangle\`, \`subproc\`, \`subprocess\`, \`subroutine\` |
| Summary | Crossed Circle | \`cross-circ\` | Summary | \`crossed-circle\`, \`summary\` |
| Tagged Document | Tagged Document | \`tag-doc\` | Tagged document | \`tag-doc\`, \`tagged-document\` |
| Tagged Process | Tagged Rectangle | \`tag-rect\` | Tagged process | \`tag-proc\`, \`tagged-process\`, \`tagged-rectangle\` |
| Terminal Point | Stadium | \`stadium\` | Terminal point | \`pill\`, \`terminal\` |
| Text Block | Text Block | \`text\` | Text block | |
"
`);
});

View File

@@ -78,3 +78,187 @@ describe('when working with site config', () => {
expect(config_4.altFontFamily).toBeUndefined();
});
});
describe('getUserDefinedConfig', () => {
beforeEach(() => {
configApi.reset();
});
it('should return empty object when no user config is defined', () => {
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toEqual({});
});
it('should return config from initialize only', () => {
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
configApi.saveConfigFromInitialize(initConfig);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toEqual(initConfig);
});
it('should return config from directives only', () => {
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
const directive2: MermaidConfig = { theme: 'forest' };
configApi.addDirective(directive1);
configApi.addDirective(directive2);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"fontFamily": "Arial",
"fontSize": 14,
"layout": "elk",
"theme": "forest",
}
`);
});
it('should combine initialize config and directives', () => {
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
const directive2: MermaidConfig = { theme: 'forest' };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive1);
configApi.addDirective(directive2);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"fontFamily": "Arial",
"fontSize": 14,
"layout": "elk",
"theme": "forest",
}
`);
});
it('should handle nested config objects properly', () => {
const initConfig: MermaidConfig = {
flowchart: { nodeSpacing: 50, rankSpacing: 100 },
theme: 'default',
};
const directive: MermaidConfig = {
flowchart: { nodeSpacing: 75, curve: 'basis' },
mindmap: { padding: 20 },
};
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"flowchart": {
"curve": "basis",
"nodeSpacing": 75,
"rankSpacing": 100,
},
"mindmap": {
"padding": 20,
},
"theme": "default",
}
`);
});
it('should handle complex nested overrides', () => {
const initConfig: MermaidConfig = {
flowchart: {
nodeSpacing: 50,
rankSpacing: 100,
curve: 'linear',
},
theme: 'default',
};
const directive1: MermaidConfig = {
flowchart: {
nodeSpacing: 75,
},
fontSize: 12,
};
const directive2: MermaidConfig = {
flowchart: {
curve: 'basis',
nodeSpacing: 100,
},
mindmap: {
padding: 15,
},
};
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive1);
configApi.addDirective(directive2);
const userConfig = configApi.getUserDefinedConfig();
expect(userConfig).toMatchInlineSnapshot(`
{
"flowchart": {
"curve": "basis",
"nodeSpacing": 100,
"rankSpacing": 100,
},
"fontSize": 12,
"mindmap": {
"padding": 15,
},
"theme": "default",
}
`);
});
it('should return independent copies (not references)', () => {
const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
configApi.saveConfigFromInitialize(initConfig);
const userConfig1 = configApi.getUserDefinedConfig();
const userConfig2 = configApi.getUserDefinedConfig();
userConfig1.theme = 'neutral';
userConfig1.flowchart!.nodeSpacing = 999;
expect(userConfig2).toMatchInlineSnapshot(`
{
"flowchart": {
"nodeSpacing": 50,
},
"theme": "dark",
}
`);
});
it('should handle edge cases with undefined values', () => {
const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"fontSize": 14,
"layout": undefined,
"theme": "dark",
}
`);
});
it('should retain config from initialize after reset', () => {
const initConfig: MermaidConfig = { theme: 'dark' };
const directive: MermaidConfig = { layout: 'elk' };
configApi.saveConfigFromInitialize(initConfig);
configApi.addDirective(directive);
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
{
"layout": "elk",
"theme": "dark",
}
`);
configApi.reset();
});
});

View File

@@ -248,3 +248,17 @@ const checkConfig = (config: MermaidConfig) => {
issueWarning('LAZY_LOAD_DEPRECATED');
}
};
export const getUserDefinedConfig = (): MermaidConfig => {
let userConfig: MermaidConfig = {};
if (configFromInitialize) {
userConfig = assignWithDepth(userConfig, configFromInitialize);
}
for (const d of directives) {
userConfig = assignWithDepth(userConfig, d);
}
return userConfig;
};

View File

@@ -1075,6 +1075,10 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
export interface MindmapDiagramConfig extends BaseDiagramConfig {
padding?: number;
maxNodeWidth?: number;
/**
* Layout algorithm to use for positioning mindmap nodes
*/
layoutAlgorithm?: string;
}
/**
* The object containing configurations specific for kanban diagrams

View File

@@ -1,5 +1,3 @@
// tests to check that comments are removed
import { cleanupComments } from './comments.js';
import { describe, it, expect } from 'vitest';
@@ -10,12 +8,12 @@ describe('comments', () => {
%% This is a comment
%% This is another comment
graph TD
A-->B
A-->B
%% This is a comment
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
A-->B
"
`);
});
@@ -29,9 +27,9 @@ graph TD
%%{ init: {'theme': 'space before init'}}%%
%%{init: {'theme': 'space after ending'}}%%
graph TD
A-->B
A-->B
B-->C
B-->C
%% This is a comment
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
@@ -39,9 +37,9 @@ graph TD
%%{ init: {'theme': 'space before init'}}%%
%%{init: {'theme': 'space after ending'}}%%
graph TD
A-->B
A-->B
B-->C
B-->C
"
`);
});
@@ -50,14 +48,14 @@ graph TD
const text = `
%% This is a comment
graph TD
A-->B
%% This is a comment
C-->D
A-->B
%% This is a comment
C-->D
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
C-->D
A-->B
C-->D
"
`);
});
@@ -70,11 +68,11 @@ graph TD
%% This is a comment
graph TD
A-->B
A-->B
`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
A-->B
"
`);
});
@@ -82,12 +80,12 @@ graph TD
it('should remove comments at end of text with no newline', () => {
const text = `
graph TD
A-->B
A-->B
%% This is a comment`;
expect(cleanupComments(text)).toMatchInlineSnapshot(`
"graph TD
A-->B
A-->B
"
`);
});

View File

@@ -4,5 +4,6 @@
* @returns cleaned text
*/
export const cleanupComments = (text: string): string => {
return text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '').trimStart();
const cleaned = text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
return cleaned.trimStart();
};

View File

@@ -1,6 +1,5 @@
import type { Position } from 'cytoscape';
import type { LayoutOptions, Position } from 'cytoscape';
import cytoscape from 'cytoscape';
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
import fcose from 'cytoscape-fcose';
import { select } from 'd3';
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
@@ -41,7 +40,7 @@ registerIconPacks([
icons: architectureIcons,
},
]);
cytoscape.use(fcose);
cytoscape.use(fcose as any);
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
services.forEach((service) => {
@@ -429,7 +428,7 @@ function layoutArchitecture(
},
alignmentConstraint,
relativePlacementConstraint,
} as FcoseLayoutOptions);
} as LayoutOptions);
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
layout.one('layoutstop', () => {

View File

@@ -0,0 +1,297 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MindmapDB } from './mindmapDb.js';
import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js';
import type { Edge } from '../../rendering-util/types.js';
// Mock the getConfig function
vi.mock('../../diagram-api/diagramAPI.js', () => ({
getConfig: vi.fn(() => ({
mindmap: {
layoutAlgorithm: 'cose-bilkent',
padding: 10,
maxNodeWidth: 200,
useMaxWidth: true,
},
})),
}));
describe('MindmapDb getData function', () => {
let db: MindmapDB;
beforeEach(() => {
db = new MindmapDB();
// Clear the database before each test
db.clear();
});
describe('getData', () => {
it('should return empty data when no mindmap is set', () => {
const result = db.getData();
expect(result.nodes).toEqual([]);
expect(result.edges).toEqual([]);
expect(result.config).toBeDefined();
expect(result.rootNode).toBeUndefined();
});
it('should return structured data for simple mindmap', () => {
// Create a simple mindmap structure
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child1', 'Child 1', 0);
db.addNode(1, 'child2', 'Child 2', 0);
const result = db.getData();
expect(result.nodes).toHaveLength(3);
expect(result.edges).toHaveLength(2);
expect(result.config).toBeDefined();
expect(result.rootNode).toBeDefined();
// Check root node
const rootNode = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '0');
expect(rootNode).toBeDefined();
expect(rootNode?.label).toBe('Root Node');
expect(rootNode?.level).toBe(0);
// Check child nodes
const child1 = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '1');
expect(child1).toBeDefined();
expect(child1?.label).toBe('Child 1');
expect(child1?.level).toBe(1);
// Check edges
expect(result.edges).toContainEqual(
expect.objectContaining({
start: '0',
end: '1',
depth: 0,
})
);
});
it('should return structured data for hierarchical mindmap', () => {
// Create a hierarchical mindmap structure
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child1', 'Child 1', 0);
db.addNode(2, 'grandchild1', 'Grandchild 1', 0);
db.addNode(2, 'grandchild2', 'Grandchild 2', 0);
db.addNode(1, 'child2', 'Child 2', 0);
const result = db.getData();
expect(result.nodes).toHaveLength(5);
expect(result.edges).toHaveLength(4);
// Check that all levels are represented
const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level);
expect(levels).toContain(0); // root
expect(levels).toContain(1); // children
expect(levels).toContain(2); // grandchildren
// Check edge relationships
const edgeRelations = result.edges.map(
(e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}`
);
expect(edgeRelations).toContain('0->1'); // root to child1
expect(edgeRelations).toContain('1->2'); // child1 to grandchild1
expect(edgeRelations).toContain('1->3'); // child1 to grandchild2
expect(edgeRelations).toContain('0->4'); // root to child2
});
it('should preserve node properties in processed data', () => {
// Add a node with specific properties
db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle
// Set additional properties
const mindmap = db.getMindmap();
if (mindmap) {
mindmap.width = 150;
mindmap.height = 75;
mindmap.padding = 15;
mindmap.section = 1;
mindmap.class = 'custom-class';
mindmap.icon = 'star';
}
const result = db.getData();
expect(result.nodes).toHaveLength(1);
const node = result.nodes[0] as MindmapLayoutNode;
expect(node.type).toBe(2);
expect(node.width).toBe(150);
expect(node.height).toBe(75);
expect(node.padding).toBe(15);
expect(node.section).toBeUndefined(); // Root node has undefined section
expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class');
expect(node.icon).toBe('star');
});
it('should generate unique edge IDs', () => {
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child1', 'Child 1', 0);
db.addNode(1, 'child2', 'Child 2', 0);
db.addNode(1, 'child3', 'Child 3', 0);
const result = db.getData();
const edgeIds = result.edges.map((e: Edge) => e.id);
const uniqueIds = new Set(edgeIds);
expect(edgeIds).toHaveLength(3);
expect(uniqueIds.size).toBe(3); // All IDs should be unique
});
it('should handle nodes with missing optional properties', () => {
db.addNode(0, 'root', 'Root Node', 0);
const result = db.getData();
const node = result.nodes[0] as MindmapLayoutNode;
// Should handle undefined/missing properties gracefully
expect(node.section).toBeUndefined(); // Root node has undefined section
expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes
expect(node.icon).toBeUndefined();
expect(node.x).toBeUndefined();
expect(node.y).toBeUndefined();
});
it('should assign correct section classes based on sibling position', () => {
// Create the example mindmap structure:
// A
// a0
// aa0
// a1
// aaa
// a2
db.addNode(0, 'A', 'A', 0); // Root
db.addNode(1, 'a0', 'a0', 0); // First child of root
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
db.addNode(1, 'a1', 'a1', 0); // Second child of root
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
db.addNode(1, 'a2', 'a2', 0); // Third child of root
const result = db.getData();
// Find nodes by their labels
const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode;
const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode;
const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode;
const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode;
const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode;
// Check section assignments
expect(nodeA.section).toBeUndefined(); // Root has undefined section
expect(nodeA0.section).toBe(0); // First child of root
expect(nodeAa0.section).toBe(0); // Inherits from parent a0
expect(nodeA1.section).toBe(1); // Second child of root
expect(nodeAaa.section).toBe(1); // Inherits from parent a1
expect(nodeA2.section).toBe(2); // Third child of root
// Check CSS classes
expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1');
expect(nodeA0.cssClasses).toBe('mindmap-node section-0');
expect(nodeAa0.cssClasses).toBe('mindmap-node section-0');
expect(nodeA1.cssClasses).toBe('mindmap-node section-1');
expect(nodeAaa.cssClasses).toBe('mindmap-node section-1');
expect(nodeA2.cssClasses).toBe('mindmap-node section-2');
});
it('should preserve custom classes while adding section classes', () => {
db.addNode(0, 'root', 'Root Node', 0);
db.addNode(1, 'child', 'Child Node', 0);
// Add custom classes to nodes
const mindmap = db.getMindmap();
if (mindmap) {
mindmap.class = 'custom-root-class';
if (mindmap.children?.[0]) {
mindmap.children[0].class = 'custom-child-class';
}
}
const result = db.getData();
const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode;
const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode;
// Should include both section classes and custom classes
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class');
expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class');
});
it('should not create any fake root nodes', () => {
// Create a simple mindmap
db.addNode(0, 'A', 'A', 0);
db.addNode(1, 'a0', 'a0', 0);
db.addNode(1, 'a1', 'a1', 0);
const result = db.getData();
// Check that we only have the expected nodes
expect(result.nodes).toHaveLength(3);
expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']);
// Check that there's no node with label "mindmap" or any other fake root
const mindmapNode = result.nodes.find((n) => n.label === 'mindmap');
expect(mindmapNode).toBeUndefined();
// Verify the root node has the correct classes
const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1');
expect(rootNode.level).toBe(0);
});
it('should assign correct section classes to edges', () => {
// Create the example mindmap structure:
// A
// a0
// aa0
// a1
// aaa
// a2
db.addNode(0, 'A', 'A', 0); // Root
db.addNode(1, 'a0', 'a0', 0); // First child of root
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
db.addNode(1, 'a1', 'a1', 0); // Second child of root
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
db.addNode(1, 'a2', 'a2', 0); // Third child of root
const result = db.getData();
// Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2
expect(result.edges).toHaveLength(5);
// Find edges by their start and end nodes
const edgeA_a0 = result.edges.find(
(e) => e.start === '0' && e.end === '1'
) as MindmapLayoutEdge;
const edgeA0_aa0 = result.edges.find(
(e) => e.start === '1' && e.end === '2'
) as MindmapLayoutEdge;
const edgeA_a1 = result.edges.find(
(e) => e.start === '0' && e.end === '3'
) as MindmapLayoutEdge;
const edgeA1_aaa = result.edges.find(
(e) => e.start === '3' && e.end === '4'
) as MindmapLayoutEdge;
const edgeA_a2 = result.edges.find(
(e) => e.start === '0' && e.end === '5'
) as MindmapLayoutEdge;
// Check edge classes
expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1
expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2
expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1
expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2
expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1
// Check section assignments match the child nodes
expect(edgeA_a0.section).toBe(0);
expect(edgeA0_aa0.section).toBe(0);
expect(edgeA_a1.section).toBe(1);
expect(edgeA1_aaa.section).toBe(1);
expect(edgeA_a2.section).toBe(2);
});
});
});

View File

@@ -4,6 +4,21 @@ import { sanitizeText } from '../../diagrams/common/common.js';
import { log } from '../../logger.js';
import type { MindmapNode } from './mindmapTypes.js';
import defaultConfig from '../../defaultConfig.js';
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
// Extend Node type for mindmap-specific properties
export type MindmapLayoutNode = Node & {
level: number;
nodeId: string;
type: number;
section?: number;
};
// Extend Edge type for mindmap-specific properties
export type MindmapLayoutEdge = Edge & {
depth: number;
section?: number;
};
const nodeType = {
DEFAULT: 0,
@@ -27,7 +42,6 @@ export class MindmapDB {
this.nodeType = nodeType;
this.clear();
this.getType = this.getType.bind(this);
this.getMindmap = this.getMindmap.bind(this);
this.getElementById = this.getElementById.bind(this);
this.getParent = this.getParent.bind(this);
this.getMindmap = this.getMindmap.bind(this);
@@ -156,6 +170,215 @@ export class MindmapDB {
}
}
/**
* Assign section numbers to nodes based on their position relative to root
* @param node - The mindmap node to process
* @param sectionNumber - The section number to assign (undefined for root)
*/
public assignSections(node: MindmapNode, sectionNumber?: number): void {
// For root node, section should be undefined (not -1)
if (node.level === 0) {
node.section = undefined;
} else {
// For non-root nodes, assign the section number
node.section = sectionNumber;
}
// For root node's children, assign section numbers based on their index
// For other nodes, inherit parent's section number
if (node.children) {
for (const [index, child] of node.children.entries()) {
const childSectionNumber = node.level === 0 ? index : sectionNumber;
this.assignSections(child, childSectionNumber);
}
}
}
/**
* Convert mindmap tree structure to flat array of nodes
* @param node - The mindmap node to process
* @param processedNodes - Array to collect processed nodes
*/
public flattenNodes(node: MindmapNode, processedNodes: MindmapLayoutNode[]): void {
// Build CSS classes for the node
const cssClasses = ['mindmap-node'];
// Add section-specific classes
if (node.level === 0) {
// Root node gets special classes
cssClasses.push('section-root', 'section--1');
} else if (node.section !== undefined) {
// Child nodes get section class based on their section number
cssClasses.push(`section-${node.section}`);
}
// Add any custom classes from the node
if (node.class) {
cssClasses.push(node.class);
}
const classes = cssClasses.join(' ');
// Map mindmap node type to valid shape name
const getShapeFromType = (type: number) => {
switch (type) {
case nodeType.CIRCLE:
return 'circle';
case nodeType.RECT:
return 'rect';
case nodeType.ROUNDED_RECT:
return 'rounded';
case nodeType.CLOUD:
return 'cloud';
case nodeType.BANG:
return 'bang';
case nodeType.HEXAGON:
return 'hexagon';
case nodeType.DEFAULT:
return 'defaultMindmapNode';
case nodeType.NO_BORDER:
default:
return 'rect';
}
};
const processedNode: MindmapLayoutNode = {
id: node.id.toString(),
domId: 'node_' + node.id.toString(),
label: node.descr,
isGroup: false,
shape: getShapeFromType(node.type),
width: node.width,
height: node.height ?? 0,
padding: node.padding,
cssClasses: classes,
cssStyles: [],
look: 'default',
icon: node.icon,
x: node.x,
y: node.y,
// Mindmap-specific properties
level: node.level,
nodeId: node.nodeId,
type: node.type,
section: node.section,
};
processedNodes.push(processedNode);
// Recursively process children
if (node.children) {
for (const child of node.children) {
this.flattenNodes(child, processedNodes);
}
}
}
/**
* Generate edges from parent-child relationships in mindmap tree
* @param node - The mindmap node to process
* @param edges - Array to collect edges
*/
public generateEdges(node: MindmapNode, edges: MindmapLayoutEdge[]): void {
if (!node.children) {
return;
}
for (const child of node.children) {
// Build CSS classes for the edge
let edgeClasses = 'edge';
// Add section-specific classes based on the child's section
if (child.section !== undefined) {
edgeClasses += ` section-edge-${child.section}`;
}
// Add depth class based on the parent's level + 1 (depth of the edge)
const edgeDepth = node.level + 1;
edgeClasses += ` edge-depth-${edgeDepth}`;
const edge: MindmapLayoutEdge = {
id: `edge_${node.id}_${child.id}`,
start: node.id.toString(),
end: child.id.toString(),
type: 'normal',
curve: 'basis',
thickness: 'normal',
look: 'default',
classes: edgeClasses,
// Store mindmap-specific data
depth: node.level,
section: child.section,
};
edges.push(edge);
// Recursively process child edges
this.generateEdges(child, edges);
}
}
/**
* Get structured data for layout algorithms
* Following the pattern established by ER diagrams
* @returns Structured data containing nodes, edges, and config
*/
public getData(): LayoutData {
const mindmapRoot = this.getMindmap();
const config = getConfig();
if (!mindmapRoot) {
return {
nodes: [],
edges: [],
config,
};
}
log.debug('getData: mindmapRoot', mindmapRoot, config);
// Assign section numbers to all nodes based on their position relative to root
this.assignSections(mindmapRoot);
// Convert tree structure to flat arrays
const processedNodes: MindmapLayoutNode[] = [];
const processedEdges: MindmapLayoutEdge[] = [];
this.flattenNodes(mindmapRoot, processedNodes);
this.generateEdges(mindmapRoot, processedEdges);
log.debug(
`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`
);
// Create shapes map for ELK compatibility
const shapes = new Map<string, any>();
for (const node of processedNodes) {
shapes.set(node.id, {
shape: node.shape,
width: node.width,
height: node.height,
padding: node.padding,
});
}
return {
nodes: processedNodes,
edges: processedEdges,
config,
// Store the root node for mindmap-specific layout algorithms
rootNode: mindmapRoot,
// Properties required by dagre layout algorithm
markers: [], // Mindmaps don't use markers
direction: 'TB', // Top-to-bottom direction for mindmaps
nodeSpacing: 50, // Default spacing between nodes
rankSpacing: 50, // Default spacing between ranks
// Add shapes for ELK compatibility
shapes: Object.fromEntries(shapes),
// Additional properties that layout algorithms might expect
type: 'mindmap',
diagramId: 'mindmap-' + Date.now(),
};
}
// Expose logger to grammar
public getLogger() {
return log;
}

View File

@@ -1,200 +1,84 @@
import cytoscape from 'cytoscape';
// @ts-expect-error No types available
import coseBilkent from 'cytoscape-cose-bilkent';
import { select } from 'd3';
import type { MermaidConfig } from '../../config.type.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DrawDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import type { D3Element } from '../../types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
import { drawNode, positionNode } from './svgDraw.js';
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import type { LayoutData } from '../../rendering-util/types.js';
import type { FilledMindMapNode } from './mindmapTypes.js';
import defaultConfig from '../../defaultConfig.js';
import type { MindmapDB } from './mindmapDb.js';
// Inject the layout algorithm into cytoscape
cytoscape.use(coseBilkent);
async function drawNodes(
db: MindmapDB,
svg: D3Element,
mindmap: FilledMindMapNode,
section: number,
conf: MermaidConfig
) {
await drawNode(db, svg, mindmap, section, conf);
if (mindmap.children) {
await Promise.all(
mindmap.children.map((child, index) =>
drawNodes(db, svg, child, section < 0 ? index : section, conf)
)
);
}
}
declare module 'cytoscape' {
interface EdgeSingular {
_private: {
bodyBounds: unknown;
rscratch: {
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
};
};
}
}
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
cy.edges().map((edge, id) => {
const data = edge.data();
if (edge[0]._private.bodyBounds) {
const bounds = edge[0]._private.rscratch;
log.trace('Edge: ', id, data);
edgesEl
.insert('path')
.attr(
'd',
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
)
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
/**
* Update the layout data with actual node dimensions after drawing
*/
function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) {
const updateNode = (node: FilledMindMapNode) => {
// Find the corresponding node in the layout data
const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString());
if (layoutNode) {
// Update with the actual dimensions calculated by drawNode
layoutNode.width = node.width;
layoutNode.height = node.height;
log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height);
}
});
}
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
cy.add({
group: 'nodes',
data: {
id: mindmap.id.toString(),
labelText: mindmap.descr,
height: mindmap.height,
width: mindmap.width,
level: level,
nodeId: mindmap.id,
padding: mindmap.padding,
type: mindmap.type,
},
position: {
x: mindmap.x!,
y: mindmap.y!,
},
});
if (mindmap.children) {
mindmap.children.forEach((child) => {
addNodes(child, cy, conf, level + 1);
cy.add({
group: 'edges',
data: {
id: `${mindmap.id}_${child.id}`,
source: mindmap.id,
target: child.id,
depth: level,
section: child.section,
},
});
});
}
}
// Recursively update children
node.children?.forEach(updateNode);
};
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
return new Promise((resolve) => {
// Add temporary render element
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
const cy = cytoscape({
container: document.getElementById('cy'), // container to render in
style: [
{
selector: 'edge',
style: {
'curve-style': 'bezier',
},
},
],
});
// Remove element after layout
renderEl.remove();
addNodes(node, cy, conf, 0);
// Make cytoscape care about the dimensions of the nodes
cy.nodes().forEach(function (n) {
n.layoutDimensions = () => {
const data = n.data();
return { w: data.width, h: data.height };
};
});
cy.layout({
name: 'cose-bilkent',
// @ts-ignore Types for cose-bilkent are not correct?
quality: 'proof',
styleEnabled: false,
animate: false,
}).run();
cy.ready((e) => {
log.info('Ready', e);
resolve(cy);
});
});
}
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
cy.nodes().map((node, id) => {
const data = node.data();
data.x = node.position().x;
data.y = node.position().y;
positionNode(db, data);
const el = db.getElementById(data.nodeId);
log.info('id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
el.attr(
'transform',
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
);
el.attr('attr', `apa-${id})`);
});
updateNode(mindmapRoot);
}
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
log.debug('Rendering mindmap diagram\n' + text);
const { securityLevel, mindmap: conf, layout } = getConfig();
// Draw the nodes first to get their dimensions, then update the layout data
const db = diagObj.db as MindmapDB;
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
// into the Layout data format
const data4Layout = db.getData();
// Create the root SVG - the element is the div containing the SVG element
const svg = getDiagramElement(id, securityLevel);
data4Layout.type = diagObj.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout, {
fallback: 'cose-bilkent',
});
// For mindmap diagrams, prioritize mindmap-specific layout algorithm configuration
data4Layout.diagramId = id;
// Ensure required properties are set for compatibility with different layout algorithms
data4Layout.markers = ['point'];
data4Layout.direction = 'TB';
const mm = db.getMindmap();
if (!mm) {
return;
}
const conf = getConfig();
conf.htmlLabels = false;
const svg = selectSvgElement(id);
// Draw the graph and start with drawing the nodes without proper position
// this gives us the size of the nodes and we can set the positions later
const edgesElem = svg.append('g');
edgesElem.attr('class', 'mindmap-edges');
const nodesElem = svg.append('g');
nodesElem.attr('class', 'mindmap-nodes');
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
// Next step is to layout the mindmap, giving each node a position
const cy = await layoutMindmap(mm, conf);
// After this we can draw, first the edges and the then nodes with the correct position
drawEdges(edgesElem, cy);
positionNodes(db, cy);
data4Layout.nodes.forEach((node) => {
node.from = 'mindmap';
if (node.shape === 'rounded') {
node.radius = 15;
node.taper = 15;
node.stroke = 'none';
} else if (node.shape === 'rect') {
node.height = 46;
node.width = 92;
}
});
// Use the unified rendering system
await render(data4Layout, svg);
// Setup the view box and size of the svg element
setupGraphViewbox(
undefined,
setupViewPortForSVG(
svg,
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
conf?.padding ?? defaultConfig.mindmap.padding,
'mindmapDiagram',
conf?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
);
};

View File

@@ -64,6 +64,12 @@ const getStyles: DiagramStylesProvider = (options) =>
.section-root text {
fill: ${options.gitBranchLabel0};
}
.section-root span {
color: ${options.gitBranchLabel0};
}
.section-2 span {
color: ${options.gitBranchLabel0};
}
.icon-container {
height:100%;
display: flex;

View File

@@ -14,7 +14,7 @@
// Special states for recognizing aliases
// A special state for grabbing text up to the first comment/newline
%x ID ALIAS LINE
%x ID ALIAS LINE CONFIG CONFIG_DATA
%x acc_title
%x acc_descr
@@ -28,6 +28,11 @@
\%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[0-9]+(?=[ \n]+) return 'NUM';
<ID>\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
@@ -231,6 +236,8 @@ participant_statement
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
| 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
;
note_statement
@@ -301,6 +308,23 @@ signal
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
;
actor_with_config
: ACTOR config_object
{
$$ = {
type: 'addParticipant',
actor: $1,
config: $2
};
}
;
config_object
: CONFIG_START CONFIG_CONTENT CONFIG_END
{
$$ = $2.trim();
}
;
// actor
// : actor_participant
// | actor_actor
@@ -313,7 +337,7 @@ signaltype
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }

View File

@@ -1,4 +1,5 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import * as yaml from 'js-yaml';
import type { DiagramDB } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { ImperativeState } from '../../utils/imperativeState.js';
@@ -13,6 +14,7 @@ import {
setDiagramTitle,
} from '../common/commonDb.js';
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
import type { ParticipantMetaData } from '../../types.js';
interface SequenceState {
prevActor?: string;
@@ -75,6 +77,17 @@ const PLACEMENT = {
OVER: 2,
} as const;
export const PARTICIPANT_TYPE = {
ACTOR: 'actor',
BOUNDARY: 'boundary',
COLLECTIONS: 'collections',
CONTROL: 'control',
DATABASE: 'database',
ENTITY: 'entity',
PARTICIPANT: 'participant',
QUEUE: 'queue',
} as const;
export class SequenceDB implements DiagramDB {
private readonly state = new ImperativeState<SequenceState>(() => ({
prevActor: undefined,
@@ -119,9 +132,22 @@ export class SequenceDB implements DiagramDB {
id: string,
name: string,
description: { text: string; wrap?: boolean | null; type: string },
type: string
type: string,
metadata?: any
) {
let assignedBox = this.state.records.currentBox;
let doc;
if (metadata !== undefined) {
let yamlData;
// detect if shapeData contains a newline character
if (!metadata.includes('\n')) {
yamlData = '{\n' + metadata + '\n}';
} else {
yamlData = metadata + '\n';
}
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData;
}
type = doc?.type ?? type;
const old = this.state.records.actors.get(id);
if (old) {
// If already set and trying to set to a new one throw error
@@ -518,7 +544,7 @@ export class SequenceDB implements DiagramDB {
});
break;
case 'addParticipant':
this.addActor(param.actor, param.actor, param.description, param.draw);
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
break;
case 'createParticipant':
if (this.state.records.actors.has(param.actor)) {
@@ -527,7 +553,7 @@ export class SequenceDB implements DiagramDB {
);
}
this.state.records.lastCreated = param.actor;
this.addActor(param.actor, param.actor, param.description, param.draw);
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
break;
case 'destroyParticipant':

View File

@@ -1368,7 +1368,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
it('should handle box without description', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
box Aqua
box aqua
participant a as Alice
participant b as Bob
end
@@ -1384,7 +1384,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
const boxes = diagram.db.getBoxes();
expect(boxes[0].name).toBeFalsy();
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
expect(boxes[0].fill).toEqual('Aqua');
expect(boxes[0].fill).toEqual('aqua');
});
it('should handle simple actor creation', async () => {
@@ -2058,4 +2058,272 @@ Bob->>Alice:Got it!
expect(messages[0].from).toBe('Alice');
expect(messages[0].to).toBe('Bob');
});
describe('when parsing extended participant syntax', () => {
it('should parse participants with different quote styles and whitespace', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob@{ "type" : "database" }
participant Carl@{ type: "database" }
participant David@{ "type" : 'database' }
participant Eve@{ type: 'database' }
participant Favela@{ "type" : "database" }
Bob->>+Alice: Hi Alice
Alice->>+Bob: Hi Bob
`);
const actors = diagram.db.getActors();
expect(actors.get('Alice').type).toBe('database');
expect(actors.get('Alice').description).toBe('Alice');
expect(actors.get('Bob').type).toBe('database');
expect(actors.get('Bob').description).toBe('Bob');
expect(actors.get('Carl').type).toBe('database');
expect(actors.get('Carl').description).toBe('Carl');
expect(actors.get('David').type).toBe('database');
expect(actors.get('David').description).toBe('David');
expect(actors.get('Eve').type).toBe('database');
expect(actors.get('Eve').description).toBe('Eve');
expect(actors.get('Favela').type).toBe('database');
expect(actors.get('Favela').description).toBe('Favela');
// Verify messages were parsed correctly
const messages = diagram.db.getMessages();
expect(messages.length).toBe(4); // 2 messages + 2 activation messages
expect(messages[0].from).toBe('Bob');
expect(messages[0].to).toBe('Alice');
expect(messages[0].message).toBe('Hi Alice');
expect(messages[2].from).toBe('Alice'); // Second message (index 2 due to activation)
expect(messages[2].to).toBe('Bob');
expect(messages[2].message).toBe('Hi Bob');
});
it('should parse mixed participant types with extended syntax', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant lead
participant dsa@{ "type" : "queue" }
API->>+Database: getUserb
Database-->>-API: userb
dsa --> Database: hello
`);
// Verify actors were created
const actors = diagram.db.getActors();
expect(actors.get('lead').type).toBe('participant');
expect(actors.get('lead').description).toBe('lead');
// Participant with extended syntax
expect(actors.get('dsa').type).toBe('queue');
expect(actors.get('dsa').description).toBe('dsa');
// Implicitly created actors (from messages)
expect(actors.get('API').type).toBe('participant');
expect(actors.get('API').description).toBe('API');
expect(actors.get('Database').type).toBe('participant');
expect(actors.get('Database').description).toBe('Database');
// Verify messages were parsed correctly
const messages = diagram.db.getMessages();
expect(messages.length).toBe(5); // 3 messages + 2 activation messages
// First message with activation
expect(messages[0].from).toBe('API');
expect(messages[0].to).toBe('Database');
expect(messages[0].message).toBe('getUserb');
expect(messages[0].activate).toBe(true);
// Second message with deactivation
expect(messages[2].from).toBe('Database');
expect(messages[2].to).toBe('API');
expect(messages[2].message).toBe('userb');
// Third message
expect(messages[4].from).toBe('dsa');
expect(messages[4].to).toBe('Database');
expect(messages[4].message).toBe('hello');
});
it('should fail for malformed JSON in participant definition', async () => {
const invalidDiagram = `
sequenceDiagram
participant D@{ "type: "entity" }
participant E@{ "type": "dat
abase }
`;
let error = false;
try {
await mermaidAPI.parse(invalidDiagram);
} catch (e) {
error = true;
}
expect(error).toBe(true);
});
it('should fail for missing colon separator', async () => {
const invalidDiagram = `
sequenceDiagram
participant C@{ "type" "control" }
C ->> C: action
`;
let error = false;
try {
await mermaidAPI.parse(invalidDiagram);
} catch (e) {
error = true;
}
expect(error).toBe(true);
});
it('should fail for missing closing brace', async () => {
const invalidDiagram = `
sequenceDiagram
participant E@{ "type": "entity"
E ->> E: process
`;
let error = false;
try {
await mermaidAPI.parse(invalidDiagram);
} catch (e) {
error = true;
}
expect(error).toBe(true);
});
});
describe('participant type parsing', () => {
it('should parse boundary participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant boundary@{ "type" : "boundary" }
boundary->boundary: test
`);
const actors = diagram.db.getActors();
expect(actors.get('boundary').type).toBe('boundary');
expect(actors.get('boundary').description).toBe('boundary');
});
it('should parse control participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant C@{ "type" : "control" }
C->C: test
`);
const actors = diagram.db.getActors();
expect(actors.get('C').type).toBe('control');
expect(actors.get('C').description).toBe('C');
});
it('should parse entity participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant E@{ "type" : "entity" }
E->E: test
`);
const actors = diagram.db.getActors();
expect(actors.get('E').type).toBe('entity');
expect(actors.get('E').description).toBe('E');
});
it('should parse database participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant D@{ "type" : "database" }
D->D: test
`);
const actors = diagram.db.getActors();
expect(actors.get('D').type).toBe('database');
expect(actors.get('D').description).toBe('D');
});
it('should parse collections participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant L@{ "type" : "collections" }
L->L: test
`);
const actors = diagram.db.getActors();
expect(actors.get('L').type).toBe('collections');
expect(actors.get('L').description).toBe('L');
});
it('should parse queue participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Q@{ "type" : "queue" }
Q->Q: test
`);
const actors = diagram.db.getActors();
expect(actors.get('Q').type).toBe('queue');
expect(actors.get('Q').description).toBe('Q');
});
});
describe('participant type parsing', () => {
it('should parse actor participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant A@{ "type" : "queue" }
A->A: test
`);
const actors = diagram.db.getActors();
expect(actors.get('A').type).toBe('queue');
expect(actors.get('A').description).toBe('A');
});
it('should parse participant participant', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant P@{ "type" : "database" }
P->P: test
`);
const actors = diagram.db.getActors();
expect(actors.get('P').type).toBe('database');
expect(actors.get('P').description).toBe('P');
});
it('should parse boundary using actor keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant Alice@{ "type" : "collections" }
participant Bob@{ "type" : "control" }
Alice->>Bob: Hello Bob, how are you?
`);
const actors = diagram.db.getActors();
expect(actors.get('Alice').type).toBe('collections');
expect(actors.get('Bob').type).toBe('control');
expect(actors.get('Bob').description).toBe('Bob');
});
it('should parse control using participant keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant C@{ "type" : "control" }
C->C: test
`);
const actors = diagram.db.getActors();
expect(actors.get('C').type).toBe('control');
expect(actors.get('C').description).toBe('C');
});
it('should parse entity using actor keyword', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
participant E@{ "type" : "entity" }
E->E: test
`);
const actors = diagram.db.getActors();
expect(actors.get('E').type).toBe('entity');
expect(actors.get('E').description).toBe('E');
});
});
});

View File

@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { Diagram } from '../../Diagram.js';
import { PARTICIPANT_TYPE } from './sequenceDb.js';
let conf = {};
@@ -476,7 +477,29 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
// add node number
if (sequenceVisible || conf.showSequenceNumbers) {
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
const isBidirectional =
type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID ||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED;
if (isBidirectional) {
const SEQUENCE_NUMBER_RADIUS = 6;
if (startx < stopx) {
line.attr('x1', startx + 2 * SEQUENCE_NUMBER_RADIUS);
} else {
line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
}
}
diagram
.append('line')
.attr('x1', startx)
.attr('y1', lineStartY)
.attr('x2', startx)
.attr('y2', lineStartY)
.attr('stroke-width', 0)
.attr('marker-start', 'url(' + url + '#sequencenumber)');
diagram
.append('text')
.attr('x', startx)
@@ -724,11 +747,19 @@ function adjustCreatedDestroyedData(
msgModel.startx = msgModel.startx - adjustment;
}
}
const actorArray = [
PARTICIPANT_TYPE.ACTOR,
PARTICIPANT_TYPE.CONTROL,
PARTICIPANT_TYPE.ENTITY,
PARTICIPANT_TYPE.DATABASE,
];
// if it is a create message
if (createdActors.get(msg.to) == index) {
const actor = actors.get(msg.to);
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
const adjustment = actorArray.includes(actor.type)
? ACTOR_TYPE_WIDTH / 2 + 3
: actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
actor.starty = lineStartY - actor.height / 2;
bounds.bumpVerticalPos(actor.height / 2);
@@ -737,7 +768,7 @@ function adjustCreatedDestroyedData(
else if (destroyedActors.get(msg.from) == index) {
const actor = actors.get(msg.from);
if (conf.mirrorActors) {
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
senderAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
@@ -747,7 +778,9 @@ function adjustCreatedDestroyedData(
else if (destroyedActors.get(msg.to) == index) {
const actor = actors.get(msg.to);
if (conf.mirrorActors) {
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
const adjustment = actorArray.includes(actor.type)
? ACTOR_TYPE_WIDTH / 2 + 3
: actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
@@ -1065,10 +1098,11 @@ export const draw = async function (_text: string, id: string, _version: string,
for (const box of bounds.models.boxes) {
box.height = bounds.getVerticalPos() - box.y;
bounds.insert(box.x, box.y, box.x + box.width, box.height);
box.startx = box.x;
box.starty = box.y;
box.stopx = box.startx + box.width;
box.stopy = box.starty + box.height;
const boxPadding = conf.boxMargin * 2;
box.startx = box.x - boxPadding;
box.starty = box.y - boxPadding * 0.25;
box.stopx = box.startx + box.width + 2 * boxPadding;
box.stopy = box.starty + box.height + boxPadding * 0.75;
box.stroke = 'rgb(0,0,0, 0.5)';
svgDraw.drawBox(diagram, box, conf);
}
@@ -1333,6 +1367,9 @@ async function calculateActorMargins(
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
}, 0);
const standardBoxPadding = conf.boxMargin * 8;
totalWidth += standardBoxPadding;
totalWidth -= 2 * conf.boxTextMargin;
if (box.wrap) {
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);

View File

@@ -12,6 +12,11 @@ const getStyles = (options) =>
.actor-line {
stroke: ${options.actorLineColor};
}
.innerArc {
stroke-width: 1.5;
stroke-dasharray: none;
}
.messageLine0 {
stroke-width: 1.5;
@@ -115,6 +120,7 @@ const getStyles = (options) =>
fill: ${options.actorBkg};
stroke-width: 2px;
}
`;
export default getStyles;

View File

@@ -415,6 +415,600 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
return height;
};
/**
* Draws an actor in the diagram with the attached line
*
* @param {any} elem - The diagram we'll draw to.
* @param {any} actor - The actor to draw.
* @param {any} conf - DrawText implementation discriminator object
* @param {boolean} isFooter - If the actor is the footer one
*/
const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + actor.height;
const boxplusLineGroup = elem.append('g').lower();
var g = boxplusLineGroup;
if (!isFooter) {
actorCnt++;
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
}
g.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
g = boxplusLineGroup.append('g');
actor.actorCnt = actorCnt;
if (actor.links != null) {
g.attr('id', 'root-' + actorCnt);
}
}
const rect = svgDrawCommon.getNoteRect();
var cssclass = 'actor';
if (actor.properties?.class) {
cssclass = actor.properties.class;
} else {
rect.fill = '#eaeaea';
}
if (isFooter) {
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssclass += ` ${TOP_ACTOR_CLASS}`;
}
rect.x = actor.x;
rect.y = actorY;
rect.width = actor.width;
rect.height = actor.height;
rect.class = cssclass;
rect.name = actor.name;
// DRAW STACKED RECTANGLES
const offset = 6;
const shadowRect = {
...rect,
x: rect.x + (isFooter ? -offset : -offset),
y: rect.y + (isFooter ? +offset : +offset),
class: 'actor',
};
const rectElem = drawRect(g, rect); // draw main rectangle on top
drawRect(g, shadowRect);
actor.rectData = rect;
if (actor.properties?.icon) {
const iconSrc = actor.properties.icon.trim();
if (iconSrc.charAt(0) === '@') {
svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
} else {
svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
}
}
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
g,
rect.x - offset,
rect.y + offset,
rect.width,
rect.height,
{ class: `actor ${ACTOR_BOX_CLASS}` },
conf
);
let height = actor.height;
if (rectElem.node) {
const bounds = rectElem.node().getBBox();
actor.height = bounds.height;
height = bounds.height;
}
return height;
};
const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + actor.height;
const boxplusLineGroup = elem.append('g').lower();
let g = boxplusLineGroup;
if (!isFooter) {
actorCnt++;
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
}
g.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
g = boxplusLineGroup.append('g');
actor.actorCnt = actorCnt;
if (actor.links != null) {
g.attr('id', 'root-' + actorCnt);
}
}
const rect = svgDrawCommon.getNoteRect();
let cssclass = 'actor';
if (actor.properties?.class) {
cssclass = actor.properties.class;
} else {
rect.fill = '#eaeaea';
}
if (isFooter) {
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssclass += ` ${TOP_ACTOR_CLASS}`;
}
rect.x = actor.x;
rect.y = actorY;
rect.width = actor.width;
rect.height = actor.height;
rect.class = cssclass;
rect.name = actor.name;
// Cylinder dimensions
const ry = rect.height / 2;
const rx = ry / (2.5 + rect.height / 50);
// Cylinder base group
const cylinderGroup = g.append('g');
const cylinderArc = g.append('g');
// Main cylinder body
cylinderGroup
.append('path')
.attr(
'd',
`M ${rect.x},${rect.y + ry}
a ${rx},${ry} 0 0 0 0,${rect.height}
h ${rect.width - 2 * rx}
a ${rx},${ry} 0 0 0 0,-${rect.height}
Z
`
)
.attr('class', cssclass);
cylinderArc
.append('path')
.attr(
'd',
`M ${rect.x},${rect.y + ry}
a ${rx},${ry} 0 0 0 0,${rect.height}`
)
.attr('stroke', '#666')
.attr('stroke-width', '1px')
.attr('class', cssclass);
cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`);
actor.rectData = rect;
if (actor.properties?.icon) {
const iconSrc = actor.properties.icon.trim();
const iconX = rect.x + rect.width - 20;
const iconY = rect.y + 10;
if (iconSrc.charAt(0) === '@') {
svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
} else {
svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
}
}
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
g,
rect.x,
rect.y,
rect.width,
rect.height,
{ class: `actor ${ACTOR_BOX_CLASS}` },
conf
);
let height = actor.height;
const lastPath = cylinderGroup.select('path:last-child');
if (lastPath.node()) {
const bounds = lastPath.node().getBBox();
actor.height = bounds.height;
height = bounds.height;
}
return height;
};
const drawActorTypeControl = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + 75;
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
let cssClass = ACTOR_MAN_FIGURE_CLASS;
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
const rect = svgDrawCommon.getNoteRect();
rect.x = actor.x;
rect.y = actorY;
rect.fill = '#eaeaea';
rect.width = actor.width;
rect.height = actor.height;
rect.class = 'actor';
const cx = actor.x + actor.width / 2;
const cy = actorY + 30;
const r = 18;
actElem
.append('defs')
.append('marker')
.attr('id', 'filled-head-control')
.attr('refX', 11)
.attr('refY', 5.8)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', '172.5')
.append('path')
.attr('d', 'M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z');
// Draw the base circle
actElem
.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', r)
.attr('fill', '#eaeaf7')
.attr('stroke', '#666')
.attr('stroke-width', 1.2);
// Draw looping arrow as arc path
actElem
.append('line')
.attr('marker-end', 'url(#filled-head-control)')
.attr('transform', `translate(${cx}, ${cy - r})`);
const bounds = actElem.node().getBBox();
actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0);
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
rect.x,
rect.y + r + (isFooter ? 5 : 10),
rect.width,
rect.height,
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
conf
);
return actor.height;
};
const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + 75;
const line = elem.append('g').lower();
const actElem = elem.append('g');
let cssClass = ACTOR_MAN_FIGURE_CLASS;
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
const rect = svgDrawCommon.getNoteRect();
rect.x = actor.x;
rect.y = actorY;
rect.fill = '#eaeaea';
rect.width = actor.width;
rect.height = actor.height;
rect.class = 'actor';
const cx = actor.x + actor.width / 2;
const cy = actorY + (!isFooter ? 25 : 10);
const r = 18;
actElem
.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', r)
.attr('width', actor.width)
.attr('height', actor.height);
actElem
.append('line')
.attr('x1', cx - r)
.attr('x2', cx + r)
.attr('y1', cy + r)
.attr('y2', cy + r)
.attr('stroke', '#333')
.attr('stroke-width', 2);
const bounds = actElem.node().getBBox();
actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0);
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
rect.x,
rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY + r - 5) / 2),
rect.width,
rect.height,
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
conf
);
if (!isFooter) {
actElem.attr('transform', `translate(${0}, ${r / 2})`);
} else {
actElem.attr('transform', `translate(${0}, ${r / 2})`);
}
return actor.height;
};
const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + actor.height + 2 * conf.boxTextMargin;
const boxplusLineGroup = elem.append('g').lower();
let g = boxplusLineGroup;
if (!isFooter) {
actorCnt++;
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
}
g.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
g = boxplusLineGroup.append('g');
actor.actorCnt = actorCnt;
if (actor.links != null) {
g.attr('id', 'root-' + actorCnt);
}
}
const rect = svgDrawCommon.getNoteRect();
let cssclass = 'actor';
if (actor.properties?.class) {
cssclass = actor.properties.class;
} else {
rect.fill = '#eaeaea';
}
if (isFooter) {
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssclass += ` ${TOP_ACTOR_CLASS}`;
}
rect.x = actor.x;
rect.y = actorY;
rect.width = actor.width;
rect.height = actor.height;
rect.class = cssclass;
rect.name = actor.name;
// Cylinder dimensions
rect.x = actor.x;
rect.y = actorY;
const w = rect.width / 4;
const h = rect.width / 4;
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
// Cylinder base group
const cylinderGroup = g.append('g');
const d = `
M ${rect.x},${rect.y + ry}
a ${rx},${ry} 0 0 0 ${w},0
a ${rx},${ry} 0 0 0 -${w},0
l 0,${h - 2 * ry}
a ${rx},${ry} 0 0 0 ${w},0
l 0,-${h - 2 * ry}
`;
// Draw the main cylinder body
cylinderGroup
.append('path')
.attr('d', d)
.attr('fill', '#eaeaea')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('class', cssclass);
if (!isFooter) {
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${(rect.height + ry) / 4})`);
} else {
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
}
actor.rectData = rect;
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
g,
rect.x,
rect.y + (!isFooter ? (rect.height + ry) / 2 : (rect.height + h) / 4),
rect.width,
rect.height,
{ class: `actor ${ACTOR_BOX_CLASS}` },
conf
);
const lastPath = cylinderGroup.select('path:last-child');
if (lastPath.node()) {
const bounds = lastPath.node().getBBox();
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
}
return actor.height;
};
const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + 80;
const radius = 30;
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
.attr('y1', centerY)
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line 200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999')
.attr('name', actor.name);
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
let cssClass = ACTOR_MAN_FIGURE_CLASS;
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
cssClass += ` ${TOP_ACTOR_CLASS}`;
}
actElem.attr('class', cssClass);
actElem.attr('name', actor.name);
const rect = svgDrawCommon.getNoteRect();
rect.x = actor.x;
rect.y = actorY;
rect.fill = '#eaeaea';
rect.width = actor.width;
rect.height = actor.height;
rect.class = 'actor';
actElem
.append('line')
.attr('id', 'actor-man-torso' + actorCnt)
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
.attr('y1', actorY + 10)
.attr('x2', actor.x + actor.width / 2 - 15)
.attr('y2', actorY + 10);
actElem
.append('line')
.attr('id', 'actor-man-arms' + actorCnt)
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
.attr('y1', actorY + 0) // starting Y
.attr('x2', actor.x + actor.width / 2 - radius * 2.5)
.attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
actElem
.append('circle')
.attr('cx', actor.x + actor.width / 2)
.attr('cy', actorY + 10)
.attr('r', radius);
const bounds = actElem.node().getBBox();
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
actElem,
rect.x,
rect.y + (!isFooter ? radius / 2 + 3 : radius / 2 - 4),
rect.width,
rect.height,
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
conf
);
if (!isFooter) {
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
} else {
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
}
return actor.height;
};
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
@@ -516,6 +1110,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
return await drawActorTypeActor(elem, actor, conf, isFooter);
case 'participant':
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
case 'boundary':
return await drawActorTypeBoundary(elem, actor, conf, isFooter);
case 'control':
return await drawActorTypeControl(elem, actor, conf, isFooter);
case 'entity':
return await drawActorTypeEntity(elem, actor, conf, isFooter);
case 'database':
return await drawActorTypeDatabase(elem, actor, conf, isFooter);
case 'collections':
return await drawActorTypeCollections(elem, actor, conf, isFooter);
case 'queue':
return await drawActorTypeQueue(elem, actor, conf, isFooter);
}
};

View File

@@ -203,6 +203,7 @@ function sidebarConfig() {
{ text: 'Accessibility', link: '/config/accessibility' },
{ text: 'Mermaid CLI', link: '/config/mermaidCLI' },
{ text: 'FAQ', link: '/config/faq' },
{ text: 'Tidy tree layout Instructions', link: '/config/tidy-tree' },
],
},
];

View File

@@ -17,7 +17,6 @@ While directives allow you to change most of the default configuration settings,
Mermaid basically supports two types of configuration options to be overridden by directives.
1. _General/Top Level configurations_ : These are the configurations that are available and applied to all the diagram. **Some of the most important top-level** configurations are:
- theme
- fontFamily
- logLevel

View File

@@ -0,0 +1,57 @@
# Tidy-tree Layout Instructions
Instructions to use the Tidy-tree layout algorithm.
## Getting Started
### Installation
```bash
npm install non-layered-tidy-tree-layout
# or
yarn add non-layered-tidy-tree-layout
```
There's also a built version: `dist/non-layered-tidy-tree-layout.js` for use with browser `<script>` tag, or as a Javascript module.
## Tidy tree Layouts
Mermaid also supports a Tidy Tree layout for mindmaps.
```
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
### With bundlers
```sh
npm install @mermaid-js/layout-tidy-tree
```
```ts
import mermaid from 'mermaid';
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
```
### With CDN
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
mermaid.registerLayoutLoaders(tidyTreeLayouts);
</script>
```

View File

@@ -23,7 +23,6 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chatgpt.com/g/g-684cc36f30208191b21383b88650a45d-mermaid-chart-diagrams-and-charts)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)

View File

@@ -33,13 +33,11 @@ The Mermaid Chart team is excited to introduce a new Visual Editor for Flowchart
Learn more:
- Visual Editor For Flowcharts
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-releases-new-visual-editor-for-flowcharts)
- [Demo video](https://www.youtube.com/watch?v=5aja0gijoO0)
- Visual Editor For Sequence diagrams
- [Blog post](https://www.mermaidchart.com/blog/posts/mermaid-chart-unveils-visual-editor-for-sequence-diagrams)
- [Demo video](https://youtu.be/imc2u5_N6Dc)

View File

@@ -83,7 +83,6 @@ The following unfinished features are not supported in the short term.
- [ ] Legend
- [x] System Context
- [x] Person(alias, label, ?descr, ?sprite, ?tags, $link)
- [x] Person_Ext
- [x] System(alias, label, ?descr, ?sprite, ?tags, $link)
@@ -97,7 +96,6 @@ The following unfinished features are not supported in the short term.
- [x] System_Boundary
- [x] Container diagram
- [x] Container(alias, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] ContainerDb
- [x] ContainerQueue
@@ -107,7 +105,6 @@ The following unfinished features are not supported in the short term.
- [x] Container_Boundary(alias, label, ?tags, $link)
- [x] Component diagram
- [x] Component(alias, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] ComponentDb
- [x] ComponentQueue
@@ -116,18 +113,15 @@ The following unfinished features are not supported in the short term.
- [x] ComponentQueue_Ext
- [x] Dynamic diagram
- [x] RelIndex(index, from, to, label, ?tags, $link)
- [x] Deployment diagram
- [x] Deployment_Node(alias, label, ?type, ?descr, ?sprite, ?tags, $link)
- [x] Node(alias, label, ?type, ?descr, ?sprite, ?tags, $link): short name of Deployment_Node()
- [x] Node_L(alias, label, ?type, ?descr, ?sprite, ?tags, $link): left aligned Node()
- [x] Node_R(alias, label, ?type, ?descr, ?sprite, ?tags, $link): right aligned Node()
- [x] Relationship Types
- [x] Rel(from, to, label, ?techn, ?descr, ?sprite, ?tags, $link)
- [x] BiRel (bidirectional relationship)
- [x] Rel_U, Rel_Up

View File

@@ -590,11 +590,17 @@ flowchart TD
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
```mermaid
flowchart TD
%% My image with a constrained aspect ratio
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
```
## Links between nodes

View File

@@ -209,3 +209,22 @@ You can also refer the [implementation in the live editor](https://github.com/me
cspell:locale en,en-gb
cspell:ignore Buzan
--->
## Layouts
Mermaid also supports a Tidy Tree layout for mindmaps.
```
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)

View File

@@ -46,6 +46,78 @@ sequenceDiagram
Bob->>Alice: Hi Alice
```
### Boundary
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "boundary" }
participant Bob
Alice->>Bob: Request from boundary
Bob->>Alice: Response to boundary
```
### Control
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "control" }
participant Bob
Alice->>Bob: Control request
Bob->>Alice: Control response
```
### Entity
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "entity" }
participant Bob
Alice->>Bob: Entity request
Bob->>Alice: Entity response
```
### Database
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "database" }
participant Bob
Alice->>Bob: DB query
Bob->>Alice: DB result
```
### Collections
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "collections" }
participant Bob
Alice->>Bob: Collections request
Bob->>Alice: Collections response
```
### Queue
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
```mermaid-example
sequenceDiagram
participant Alice@{ "type" : "queue" }
participant Bob
Alice->>Bob: Queue message
Bob->>Alice: Queue response
```
### Aliases
The actor can have a convenient identifier and a descriptive label.

View File

@@ -1,4 +1,5 @@
import { cleanupComments } from './diagram-api/comments.js';
import { detectType } from './diagram-api/detectType.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
import type { DiagramMetadata } from './diagram-api/types.js';
import utils, { cleanAndMerge, removeDirectives } from './utils.js';
@@ -18,6 +19,7 @@ const cleanupText = (code: string) => {
const processFrontmatter = (code: string) => {
const { text, metadata } = extractFrontMatter(code);
const diagramType = detectType(text);
const { displayMode, title, config = {} } = metadata;
if (displayMode) {
// Needs to be supported for legacy reasons
@@ -26,6 +28,9 @@ const processFrontmatter = (code: string) => {
}
config.gantt.displayMode = displayMode;
}
if (diagramType === 'mindmap' && !config.layout) {
config.layout = 'cose-bilkent'; // Default layout for mindmap
}
return { title, config, text };
};

View File

@@ -0,0 +1,148 @@
import { insertNode } from './rendering-elements/nodes.js';
import type { LayoutData, NonClusterNode } from './types.ts';
import type { Selection } from 'd3';
import { getConfig } from '../diagram-api/diagramAPI.js';
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
// Update type:
type D3Selection<T extends SVGElement = SVGElement> = Selection<
T,
unknown,
Element | null,
unknown
>;
/**
* Creates a graph by merging the graph construction and DOM element insertion.
*
* This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes)
* into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions
* are computed using each node's bounding box.
*
* @param element - The D3 selection in which the SVG groups are inserted.
* @param data4Layout - The layout data containing nodes and edges.
* @returns A promise resolving to an object containing the Graphology graph and the inserted groups.
*/
export async function createGraphWithElements(
element: D3Selection,
data4Layout: LayoutData
): Promise<{
graph: graphlib.Graph;
groups: {
clusters: D3Selection<SVGGElement>;
edgePaths: D3Selection<SVGGElement>;
edgeLabels: D3Selection<SVGGElement>;
nodes: D3Selection<SVGGElement>;
rootGroups: D3Selection<SVGGElement>;
};
nodeElements: Map<string, D3Selection<SVGElement | SVGGElement>>;
}> {
// Create a directed, multi graph.
const graph = new graphlib.Graph({
multigraph: true,
compound: true,
});
const edgesToProcess = [...data4Layout.edges];
const config = getConfig();
// Create groups for clusters, edge paths, edge labels, and nodes.
const clusters = element.insert('g').attr('class', 'clusters');
const edgePaths = element.insert('g').attr('class', 'edges edgePath');
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
const nodesGroup = element.insert('g').attr('class', 'nodes');
const rootGroups = element.insert('g').attr('class', 'root');
const nodeElements = new Map<string, D3Selection<SVGElement | SVGGElement>>();
// Insert nodes into the DOM and add them to the graph.
await Promise.all(
data4Layout.nodes.map(async (node) => {
if (node.isGroup) {
graph.setNode(node.id, { ...node });
} else {
const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir });
const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
nodeElements.set(node.id, childNodeEl as D3Selection<SVGElement | SVGGElement>);
node.width = boundingBox.width;
node.height = boundingBox.height;
graph.setNode(node.id, { ...node });
}
})
);
// Add edges to the graph.
for (const edge of edgesToProcess) {
if (edge.label && edge.label?.length > 0) {
// Create a label node for the edge
const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`;
const labelNode = {
id: labelNodeId,
label: edge.label,
edgeStart: edge.start,
edgeEnd: edge.end,
shape: 'labelRect',
width: 0, // Will be updated after insertion
height: 0, // Will be updated after insertion
isEdgeLabel: true,
isDummy: true,
isGroup: false,
parentId: edge.parentId,
...(edge.dir ? { dir: edge.dir } : {}),
} as NonClusterNode;
// Insert the label node into the DOM
const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: edge.dir });
const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
// Update node dimensions
labelNode.width = boundingBox.width;
labelNode.height = boundingBox.height;
// Add to graph and tracking maps
graph.setNode(labelNodeId, { ...labelNode });
nodeElements.set(labelNodeId, labelNodeEl as D3Selection<SVGElement | SVGGElement>);
data4Layout.nodes.push(labelNode);
// Create two edges to replace the original one
const edgeToLabel = {
...edge,
id: `${edge.id}-to-label`,
end: labelNodeId,
label: undefined,
isLabelEdge: true,
arrowTypeEnd: 'none',
arrowTypeStart: 'none',
};
const edgeFromLabel = {
...edge,
id: `${edge.id}-from-label`,
start: labelNodeId,
end: edge.end,
label: undefined,
isLabelEdge: true,
arrowTypeStart: 'none',
arrowTypeEnd: 'arrow_point',
};
graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel });
graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel });
data4Layout.edges.push(edgeToLabel, edgeFromLabel);
const edgeIdToRemove = edge.id;
data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove);
const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id);
if (indexInOriginal !== -1) {
data4Layout.edges.splice(indexInOriginal, 1);
}
} else {
// Regular edge without label
graph.setEdge(edge.id, edge.start, edge.end, { ...edge });
const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id);
if (!edgeExists) {
data4Layout.edges.push(edge);
}
}
}
return {
graph,
groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups },
nodeElements,
};
}

View File

@@ -0,0 +1,269 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
addNodes,
addEdges,
extractPositionedNodes,
extractPositionedEdges,
} from './cytoscape-setup.js';
import type { Node, Edge } from '../../types.js';
// Mock cytoscape
const mockCy = {
add: vi.fn(),
nodes: vi.fn(),
edges: vi.fn(),
};
vi.mock('cytoscape', () => {
const mockCytoscape = vi.fn(() => mockCy) as any;
mockCytoscape.use = vi.fn();
return {
default: mockCytoscape,
};
});
vi.mock('cytoscape-cose-bilkent', () => ({
default: vi.fn(),
}));
describe('Cytoscape Setup', () => {
let mockNodes: Node[];
let mockEdges: Edge[];
beforeEach(() => {
vi.clearAllMocks();
mockNodes = [
{
id: '1',
label: 'Root',
isGroup: false,
shape: 'rect',
width: 100,
height: 50,
padding: 10,
x: 100,
y: 100,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: '2',
label: 'Child 1',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 150,
y: 150,
cssClasses: '',
cssStyles: [],
look: 'default',
},
];
mockEdges = [
{
id: '1_2',
start: '1',
end: '2',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
];
});
describe('addNodes', () => {
it('should add nodes to cytoscape', () => {
addNodes([mockNodes[0]], mockCy as unknown as any);
expect(mockCy.add).toHaveBeenCalledWith({
group: 'nodes',
data: {
id: '1',
labelText: 'Root',
height: 50,
width: 100,
padding: 10,
isGroup: false,
shape: 'rect',
cssClasses: '',
cssStyles: [],
look: 'default',
},
position: {
x: 100,
y: 100,
},
});
});
it('should add multiple nodes to cytoscape', () => {
addNodes(mockNodes, mockCy as unknown as any);
expect(mockCy.add).toHaveBeenCalledTimes(2);
expect(mockCy.add).toHaveBeenCalledWith({
group: 'nodes',
data: {
id: '1',
labelText: 'Root',
height: 50,
width: 100,
padding: 10,
isGroup: false,
shape: 'rect',
cssClasses: '',
cssStyles: [],
look: 'default',
},
position: {
x: 100,
y: 100,
},
});
expect(mockCy.add).toHaveBeenCalledWith({
group: 'nodes',
data: {
id: '2',
labelText: 'Child 1',
height: 40,
width: 80,
padding: 10,
isGroup: false,
shape: 'rect',
cssClasses: '',
cssStyles: [],
look: 'default',
},
position: {
x: 150,
y: 150,
},
});
});
});
describe('addEdges', () => {
it('should add edges to cytoscape', () => {
addEdges(mockEdges, mockCy as unknown as any);
expect(mockCy.add).toHaveBeenCalledWith({
group: 'edges',
data: {
id: '1_2',
source: '1',
target: '2',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
});
});
});
describe('extractPositionedNodes', () => {
it('should extract positioned nodes from cytoscape', () => {
const mockCytoscapeNodes = [
{
data: () => ({
id: '1',
labelText: 'Root',
width: 100,
height: 50,
padding: 10,
isGroup: false,
shape: 'rect',
}),
position: () => ({ x: 100, y: 100 }),
},
{
data: () => ({
id: '2',
labelText: 'Child 1',
width: 80,
height: 40,
padding: 10,
isGroup: false,
shape: 'rect',
}),
position: () => ({ x: 150, y: 150 }),
},
];
mockCy.nodes.mockReturnValue({
map: (fn: unknown) => mockCytoscapeNodes.map(fn as any),
});
const result = extractPositionedNodes(mockCy as unknown as any);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: '1',
x: 100,
y: 100,
labelText: 'Root',
width: 100,
height: 50,
padding: 10,
isGroup: false,
shape: 'rect',
});
});
});
describe('extractPositionedEdges', () => {
it('should extract positioned edges from cytoscape', () => {
const mockCytoscapeEdges = [
{
data: () => ({
id: '1_2',
source: '1',
target: '2',
type: 'edge',
}),
_private: {
rscratch: {
startX: 100,
startY: 100,
midX: 125,
midY: 125,
endX: 150,
endY: 150,
},
},
},
];
mockCy.edges.mockReturnValue({
map: (fn: unknown) => mockCytoscapeEdges.map(fn as any),
});
const result = extractPositionedEdges(mockCy as unknown as any);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: '1_2',
source: '1',
target: '2',
type: 'edge',
startX: 100,
startY: 100,
midX: 125,
midY: 125,
endX: 150,
endY: 150,
});
});
});
});

View File

@@ -0,0 +1,207 @@
import cytoscape from 'cytoscape';
import coseBilkent from 'cytoscape-cose-bilkent';
import { select } from 'd3';
import { log } from '../../../logger.js';
import type { LayoutData, Node, Edge } from '../../types.js';
import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js';
// Inject the layout algorithm into cytoscape
cytoscape.use(coseBilkent);
/**
* Declare module augmentation for cytoscape edge types
*/
declare module 'cytoscape' {
interface EdgeSingular {
_private: {
bodyBounds: unknown;
rscratch: {
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
};
};
}
}
/**
* Add nodes to cytoscape instance from provided node array
* This function processes only the nodes provided in the data structure
* @param nodes - Array of nodes to add
* @param cy - The cytoscape instance
*/
export function addNodes(nodes: Node[], cy: cytoscape.Core): void {
nodes.forEach((node) => {
const nodeData: Record<string, unknown> = {
id: node.id,
labelText: node.label,
height: node.height,
width: node.width,
padding: node.padding ?? 0,
};
// Add any additional properties from the node
Object.keys(node).forEach((key) => {
if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) {
nodeData[key] = (node as unknown as Record<string, unknown>)[key];
}
});
cy.add({
group: 'nodes',
data: nodeData,
position: {
x: node.x ?? 0,
y: node.y ?? 0,
},
});
});
}
/**
* Add edges to cytoscape instance from provided edge array
* This function processes only the edges provided in the data structure
* @param edges - Array of edges to add
* @param cy - The cytoscape instance
*/
export function addEdges(edges: Edge[], cy: cytoscape.Core): void {
edges.forEach((edge) => {
const edgeData: Record<string, unknown> = {
id: edge.id,
source: edge.start,
target: edge.end,
};
// Add any additional properties from the edge
Object.keys(edge).forEach((key) => {
if (!['id', 'start', 'end'].includes(key)) {
edgeData[key] = (edge as unknown as Record<string, unknown>)[key];
}
});
cy.add({
group: 'edges',
data: edgeData,
});
});
}
/**
* Create and configure cytoscape instance
* @param data - Layout data containing nodes and edges
* @returns Promise resolving to configured cytoscape instance
*/
export function createCytoscapeInstance(data: LayoutData): Promise<cytoscape.Core> {
return new Promise((resolve) => {
// Add temporary render element
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
const cy = cytoscape({
container: document.getElementById('cy'), // container to render in
style: [
{
selector: 'edge',
style: {
'curve-style': 'bezier',
},
},
],
});
// Remove element after layout
renderEl.remove();
// Add all nodes and edges to cytoscape using the generic functions
addNodes(data.nodes, cy);
addEdges(data.edges, cy);
// Make cytoscape care about the dimensions of the nodes
cy.nodes().forEach(function (n) {
n.layoutDimensions = () => {
const nodeData = n.data();
return { w: nodeData.width, h: nodeData.height };
};
});
// Configure and run the cose-bilkent layout
const layoutConfig: CytoscapeLayoutConfig = {
name: 'cose-bilkent',
// @ts-ignore Types for cose-bilkent are not correct?
quality: 'proof',
styleEnabled: false,
animate: false,
};
cy.layout(layoutConfig).run();
cy.ready((e) => {
log.info('Cytoscape ready', e);
resolve(cy);
});
});
}
/**
* Extract positioned nodes from cytoscape instance
* @param cy - The cytoscape instance after layout
* @returns Array of positioned nodes
*/
export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] {
return cy.nodes().map((node) => {
const data = node.data();
const position = node.position();
// Create a positioned node with all original data plus position
const positionedNode: PositionedNode = {
id: data.id,
x: position.x,
y: position.y,
};
// Add all other properties from the original data
Object.keys(data).forEach((key) => {
if (key !== 'id') {
positionedNode[key] = data[key];
}
});
return positionedNode;
});
}
/**
* Extract positioned edges from cytoscape instance
* @param cy - The cytoscape instance after layout
* @returns Array of positioned edges
*/
export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] {
return cy.edges().map((edge) => {
const data = edge.data();
const rscratch = edge._private.rscratch;
// Create a positioned edge with all original data plus position
const positionedEdge: PositionedEdge = {
id: data.id,
source: data.source,
target: data.target,
startX: rscratch.startX,
startY: rscratch.startY,
midX: rscratch.midX,
midY: rscratch.midY,
endX: rscratch.endX,
endY: rscratch.endY,
};
// Add all other properties from the original data
Object.keys(data).forEach((key) => {
if (!['id', 'source', 'target'].includes(key)) {
positionedEdge[key] = data[key];
}
});
return positionedEdge;
});
}

View File

@@ -0,0 +1,25 @@
import { render as renderWithCoseBilkent } from './render.js';
/**
* Cose-Bilkent Layout Algorithm for Generic Diagrams
*
* This module provides a layout algorithm implementation using Cytoscape
* with the cose-bilkent algorithm for positioning nodes and edges.
*
* The algorithm follows the unified rendering pattern and can be used
* by any diagram type that provides compatible LayoutData.
*/
/**
* Render function for the cose-bilkent layout algorithm
*
* This function follows the unified rendering pattern used by all layout algorithms.
* It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
* and renders the positioned elements to the SVG.
*
* @param layoutData - Layout data containing nodes, edges, and configuration
* @param svg - SVG element to render to
* @param helpers - Internal helper functions for rendering
* @param options - Rendering options
*/
export const render = renderWithCoseBilkent;

View File

@@ -0,0 +1,251 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { validateLayoutData, executeCoseBilkentLayout } from './layout.js';
import type { LayoutResult } from './types.js';
import type { MindmapNode } from '../../../diagrams/mindmap/mindmapTypes.js';
import type { MermaidConfig } from '../../../config.type.js';
import type { LayoutData } from '../../types.js';
// Mock cytoscape and cytoscape-cose-bilkent before importing the modules
vi.mock('cytoscape', () => {
const mockCy = {
add: vi.fn(),
nodes: vi.fn(() => ({
forEach: vi.fn(),
map: vi.fn((fn) => [
fn({
data: () => ({
id: '1',
nodeId: '1',
labelText: 'Root',
level: 0,
type: 0,
width: 100,
height: 50,
padding: 10,
}),
position: () => ({ x: 100, y: 100 }),
}),
]),
})),
edges: vi.fn(() => ({
map: vi.fn((fn) => [
fn({
data: () => ({
id: '1_2',
source: '1',
target: '2',
depth: 0,
}),
_private: {
rscratch: {
startX: 100,
startY: 100,
midX: 150,
midY: 150,
endX: 200,
endY: 200,
},
},
}),
]),
})),
layout: vi.fn(() => ({
run: vi.fn(),
})),
ready: vi.fn((callback) => callback({})),
};
const mockCytoscape = vi.fn(() => mockCy);
(mockCytoscape as any).use = vi.fn();
return {
default: mockCytoscape,
};
});
vi.mock('cytoscape-cose-bilkent', () => ({
default: vi.fn(),
}));
vi.mock('d3', () => ({
select: vi.fn(() => ({
append: vi.fn(() => ({
attr: vi.fn(() => ({
attr: vi.fn(() => ({
remove: vi.fn(),
})),
})),
})),
})),
}));
describe('Cose-Bilkent Layout Algorithm', () => {
let mockConfig: MermaidConfig;
let mockRootNode: MindmapNode;
let mockLayoutData: LayoutData;
beforeEach(() => {
mockConfig = {
mindmap: {
layoutAlgorithm: 'cose-bilkent',
padding: 10,
maxNodeWidth: 200,
useMaxWidth: true,
},
} as MermaidConfig;
mockRootNode = {
id: 1,
nodeId: '1',
level: 0,
descr: 'Root',
type: 0,
width: 100,
height: 50,
padding: 10,
x: 0,
y: 0,
children: [
{
id: 2,
nodeId: '2',
level: 1,
descr: 'Child 1',
type: 0,
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
},
],
} as MindmapNode;
mockLayoutData = {
nodes: [
{
id: '1',
nodeId: '1',
level: 0,
descr: 'Root',
type: 0,
width: 100,
height: 50,
padding: 10,
isGroup: false,
},
{
id: '2',
nodeId: '2',
level: 1,
descr: 'Child 1',
type: 0,
width: 80,
height: 40,
padding: 10,
isGroup: false,
},
],
edges: [
{
id: '1_2',
source: '1',
target: '2',
depth: 0,
},
],
config: mockConfig,
rootNode: mockRootNode,
};
});
describe('validateLayoutData', () => {
it('should validate correct layout data', () => {
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
});
it('should throw error for missing data', () => {
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
});
it('should throw error for missing root node', () => {
const invalidData = { ...mockLayoutData, rootNode: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Root node is required');
});
it('should throw error for missing config', () => {
const invalidData = { ...mockLayoutData, config: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
});
it('should throw error for invalid nodes array', () => {
const invalidData = { ...mockLayoutData, nodes: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('No nodes found in layout data');
});
it('should throw error for invalid edges array', () => {
const invalidData = { ...mockLayoutData, edges: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
});
});
describe('layout function', () => {
it('should execute layout algorithm successfully', async () => {
const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toBeDefined();
expect(result.edges).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true);
expect(Array.isArray(result.edges)).toBe(true);
});
it('should return positioned nodes with coordinates', async () => {
const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig);
expect(result.nodes.length).toBeGreaterThan(0);
result.nodes.forEach((node) => {
expect(node.x).toBeDefined();
expect(node.y).toBeDefined();
expect(typeof node.x).toBe('number');
expect(typeof node.y).toBe('number');
});
});
it('should return positioned edges with coordinates', async () => {
const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig);
expect(result.edges.length).toBeGreaterThan(0);
result.edges.forEach((edge) => {
expect(edge.startX).toBeDefined();
expect(edge.startY).toBeDefined();
expect(edge.midX).toBeDefined();
expect(edge.midY).toBeDefined();
expect(edge.endX).toBeDefined();
expect(edge.endY).toBeDefined();
});
});
it('should handle empty mindmap data gracefully', async () => {
const emptyData: LayoutData = {
nodes: [],
edges: [],
config: mockConfig,
rootNode: mockRootNode,
};
const result: LayoutResult = await executeCoseBilkentLayout(emptyData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toBeDefined();
expect(result.edges).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true);
expect(Array.isArray(result.edges)).toBe(true);
});
it('should throw error for invalid data', async () => {
const invalidData = { ...mockLayoutData, rootNode: null as any };
await expect(executeCoseBilkentLayout(invalidData, mockConfig)).rejects.toThrow();
});
});
});

View File

@@ -0,0 +1,77 @@
import type { MermaidConfig } from '../../../config.type.js';
import { log } from '../../../logger.js';
import type { LayoutData } from '../../types.js';
import type { LayoutResult } from './types.js';
import {
createCytoscapeInstance,
extractPositionedNodes,
extractPositionedEdges,
} from './cytoscape-setup.js';
/**
* Execute the cose-bilkent layout algorithm on generic layout data
*
* This function takes layout data and uses Cytoscape with the cose-bilkent
* algorithm to calculate optimal node positions and edge paths.
*
* @param data - The layout data containing nodes, edges, and configuration
* @param config - Mermaid configuration object
* @returns Promise resolving to layout result with positioned nodes and edges
*/
export async function executeCoseBilkentLayout(
data: LayoutData,
_config: MermaidConfig
): Promise<LayoutResult> {
log.debug('Starting cose-bilkent layout algorithm');
try {
// Validate layout data structure
validateLayoutData(data);
// Create and configure cytoscape instance
const cy = await createCytoscapeInstance(data);
// Extract positioned nodes and edges after layout
const positionedNodes = extractPositionedNodes(cy);
const positionedEdges = extractPositionedEdges(cy);
log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`);
return {
nodes: positionedNodes,
edges: positionedEdges,
};
} catch (error) {
log.error('Error in cose-bilkent layout algorithm:', error);
throw error;
}
}
/**
* Validate layout data structure
* @param data - The data to validate
* @returns True if data is valid, throws error otherwise
*/
export function validateLayoutData(data: LayoutData): boolean {
if (!data) {
throw new Error('Layout data is required');
}
if (!data.config) {
throw new Error('Configuration is required in layout data');
}
if (!data.rootNode) {
throw new Error('Root node is required');
}
if (!data.nodes || !Array.isArray(data.nodes)) {
throw new Error('No nodes found in layout data');
}
if (!Array.isArray(data.edges)) {
throw new Error('Edges array is required in layout data');
}
return true;
}

View File

@@ -0,0 +1,197 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { executeCoseBilkentLayout } from './layout.js';
import type { D3Selection } from '../../../types.js';
type Node = Record<string, unknown>;
interface NodeWithPosition extends Node {
x?: number;
y?: number;
domId?: string | SVGGroup | D3Selection<SVGAElement>;
width?: number;
height?: number;
id?: string;
}
/**
* Render function for cose-bilkent layout algorithm
*
* This follows the same pattern as ELK and dagre renderers:
* 1. Insert nodes into DOM to get their actual dimensions
* 2. Run the layout algorithm to calculate positions
* 3. Position the nodes and edges based on layout results
*/
export const render = async (
data4Layout: LayoutData,
svg: SVG,
{
insertCluster,
insertEdge,
insertEdgeLabel,
insertMarkers,
insertNode,
log,
positionEdgeLabel,
}: InternalHelpers,
{ algorithm: _algorithm }: RenderOptions
) => {
const nodeDb: Record<string, NodeWithPosition> = {};
const clusterDb: Record<string, any> = {};
// Insert markers for edges
const element = svg.select('g');
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
// Create container groups
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
const edgePaths = element.insert('g').attr('class', 'edgePaths');
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
const nodes = element.insert('g').attr('class', 'nodes');
// Step 1: Insert nodes into DOM to get their actual dimensions
log.debug('Inserting nodes into DOM for dimension calculation');
await Promise.all(
data4Layout.nodes.map(async (node) => {
if (node.isGroup) {
// Handle subgraphs/clusters
const clusterNode: NodeWithPosition = { ...node };
clusterDb[node.id] = clusterNode;
nodeDb[node.id] = clusterNode;
// Insert cluster to get dimensions
await insertCluster(subGraphsEl, node);
} else {
// Handle regular nodes
const nodeWithPosition: NodeWithPosition = { ...node };
nodeDb[node.id] = nodeWithPosition;
// Insert node to get actual dimensions
const nodeEl = await insertNode(nodes, node, {
config: data4Layout.config,
dir: data4Layout.direction || 'TB',
});
// Get the actual bounding box after insertion
const boundingBox = nodeEl.node()!.getBBox();
nodeWithPosition.width = boundingBox.width;
nodeWithPosition.height = boundingBox.height;
nodeWithPosition.domId = nodeEl;
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
}
})
);
// Step 2: Run the cose-bilkent layout algorithm
log.debug('Running cose-bilkent layout algorithm');
// Update the layout data with actual dimensions
const updatedLayoutData = {
...data4Layout,
nodes: data4Layout.nodes.map((node) => {
const nodeWithDimensions = nodeDb[node.id];
return {
...node,
width: nodeWithDimensions.width,
height: nodeWithDimensions.height,
};
}),
};
const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
// Step 3: Position the nodes based on layout results
log.debug('Positioning nodes based on layout results');
layoutResult.nodes.forEach((positionedNode) => {
const node = nodeDb[positionedNode.id];
if (node?.domId) {
// Position the node at the calculated coordinates
// The positionedNode.x/y represents the center of the node, so use directly
(node.domId as D3Selection<SVGAElement>).attr(
'transform',
`translate(${positionedNode.x}, ${positionedNode.y})`
);
// Store the final position
node.x = positionedNode.x;
node.y = positionedNode.y;
log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
}
});
layoutResult.edges.forEach((positionedEdge) => {
const edge = data4Layout.edges.find((e) => e.id === positionedEdge.id);
if (edge) {
// Update the edge data with positioned coordinates
edge.points = [
{ x: positionedEdge.startX, y: positionedEdge.startY },
{ x: positionedEdge.midX, y: positionedEdge.midY },
{ x: positionedEdge.endX, y: positionedEdge.endY },
];
}
});
// Step 4: Insert and position edges
log.debug('Inserting and positioning edges');
await Promise.all(
data4Layout.edges.map(async (edge) => {
// Insert edge label first
const _edgeLabel = await insertEdgeLabel(edgeLabels, edge);
// Get start and end nodes
const startNode = nodeDb[edge.start ?? ''];
const endNode = nodeDb[edge.end ?? ''];
if (startNode && endNode) {
// Find the positioned edge data
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
if (positionedEdge) {
log.debug('APA01 positionedEdge', positionedEdge);
// Create edge path with positioned coordinates
const edgeWithPath = { ...edge };
// Insert the edge path
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
// Position the edge label
positionEdgeLabel(edgeWithPath, paths);
} else {
// Fallback: create a simple straight line between nodes
const edgeWithPath = {
...edge,
points: [
{ x: startNode.x || 0, y: startNode.y || 0 },
{ x: endNode.x || 0, y: endNode.y || 0 },
],
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
}
}
})
);
log.debug('Cose-bilkent rendering completed');
};

View File

@@ -0,0 +1,43 @@
/**
* Positioned node after layout calculation
*/
export interface PositionedNode {
id: string;
x: number;
y: number;
[key: string]: unknown; // Allow additional properties
}
/**
* Positioned edge after layout calculation
*/
export interface PositionedEdge {
id: string;
source: string;
target: string;
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
[key: string]: unknown; // Allow additional properties
}
/**
* Result of layout algorithm execution
*/
export interface LayoutResult {
nodes: PositionedNode[];
edges: PositionedEdge[];
}
/**
* Cytoscape layout configuration
*/
export interface CytoscapeLayoutConfig {
name: 'cose-bilkent';
quality: 'proof';
styleEnabled: boolean;
animate: boolean;
}

View File

@@ -39,6 +39,14 @@ const registerDefaultLayoutLoaders = () => {
name: 'dagre',
loader: async () => await import('./layout-algorithms/dagre/index.js'),
},
...(includeLargeFeatures
? [
{
name: 'cose-bilkent',
loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'),
},
]
: []),
]);
};

View File

@@ -438,7 +438,6 @@ const fixCorners = function (lineData) {
}
return newLineData;
};
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
const { handDrawnSeed } = getConfig();
let points = edge.points;
@@ -622,9 +621,9 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
// lineData.forEach((point) => {
// elem
// .append('circle')
// .style('stroke', 'blue')
// .style('fill', 'blue')
// .attr('r', 3)
// .style('stroke', 'red')
// .style('fill', 'red')
// .attr('r', 1)
// .attr('cx', point.x)
// .attr('cy', point.y);
// });

View File

@@ -2,64 +2,63 @@
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
*/
function intersectLine(p1, p2, q1, q2) {
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
// p7 and p473.
{
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
// p7 and p473.
var a1, a2, b1, b2, c1, c2;
var r1, r2, r3, r4;
var denom, offset, num;
var x, y;
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
// b1 y + c1 = 0.
const a1 = p2.y - p1.y;
const b1 = p1.x - p2.x;
const c1 = p2.x * p1.y - p1.x * p2.y;
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
// b1 y + c1 = 0.
a1 = p2.y - p1.y;
b1 = p1.x - p2.x;
c1 = p2.x * p1.y - p1.x * p2.y;
// Compute r3 and r4.
const r3 = a1 * q1.x + b1 * q1.y + c1;
const r4 = a1 * q2.x + b1 * q2.y + c1;
// Compute r3 and r4.
r3 = a1 * q1.x + b1 * q1.y + c1;
r4 = a1 * q2.x + b1 * q2.y + c1;
const epsilon = 1e-6;
// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
return /*DON'T_INTERSECT*/;
// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
return /*DON'T_INTERSECT*/;
}
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
const a2 = q2.y - q1.y;
const b2 = q1.x - q2.x;
const c2 = q2.x * q1.y - q1.x * q2.y;
// Compute r1 and r2
const r1 = a2 * p1.x + b2 * p1.y + c2;
const r2 = a2 * p2.x + b2 * p2.y + c2;
// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
return /*DON'T_INTERSECT*/;
}
// Line segments intersect: compute intersection point.
const denom = a1 * b2 - a2 * b1;
if (denom === 0) {
return /*COLLINEAR*/;
}
const offset = Math.abs(denom / 2);
// The denom/2 is to get rounding instead of truncating. It
// is added or subtracted to the numerator, depending upon the
// sign of the numerator.
let num = b1 * c2 - b2 * c1;
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
num = a2 * c1 - a1 * c2;
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
return { x: x, y: y };
}
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
a2 = q2.y - q1.y;
b2 = q1.x - q2.x;
c2 = q2.x * q1.y - q1.x * q2.y;
// Compute r1 and r2
r1 = a2 * p1.x + b2 * p1.y + c2;
r2 = a2 * p2.x + b2 * p2.y + c2;
// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
return /*DON'T_INTERSECT*/;
}
// Line segments intersect: compute intersection point.
denom = a1 * b2 - a2 * b1;
if (denom === 0) {
return /*COLLINEAR*/;
}
offset = Math.abs(denom / 2);
// The denom/2 is to get rounding instead of truncating. It
// is added or subtracted to the numerator, depending upon the
// sign of the numerator.
num = b1 * c2 - b2 * c1;
x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
num = a2 * c1 - a1 * c2;
y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
return { x: x, y: y };
}
function sameSign(r1, r2) {

View File

@@ -61,6 +61,9 @@ import { erBox } from './shapes/erBox.js';
import { classBox } from './shapes/classBox.js';
import { requirementBox } from './shapes/requirementBox.js';
import { kanbanItem } from './shapes/kanbanItem.js';
import { bang } from './shapes/bang.js';
import { cloud } from './shapes/cloud.js';
import { defaultMindmapNode } from './shapes/defaultMindmapNode.js';
type ShapeHandler = <T extends SVGGraphicsElement>(
parent: D3Selection<T>,
@@ -135,6 +138,30 @@ export const shapesDefs = [
aliases: ['circ'],
handler: circle,
},
{
semanticName: 'Bang',
name: 'Bang',
shortName: 'bang',
description: 'Bang',
aliases: ['bang'],
handler: bang,
},
{
semanticName: 'Default Mindmap Node',
name: 'defaultMindmapNode',
shortName: 'default-mindmap',
description: 'defaultMindmapNode',
aliases: ['default-mindmap', 'defaultMindmapNode'],
handler: defaultMindmapNode,
},
{
semanticName: 'Cloud',
name: 'Cloud',
shortName: 'cloud',
description: 'cloud',
aliases: ['cloud'],
handler: cloud,
},
{
semanticName: 'Decision',
name: 'Diamond',

View File

@@ -0,0 +1,81 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';
import { handleUndefinedAttr } from '../../../utils.js';
import type { Bounds, Point } from '../../../types.js';
export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
parent,
node,
getNodeClasses(node)
);
const w = bbox.width + 10 * halfPadding;
const h = bbox.height + 8 * halfPadding;
const r = 0.15 * w;
const { cssStyles } = node;
const minWidth = bbox.width + 20;
const minHeight = bbox.height + 20;
const effectiveWidth = Math.max(w, minWidth);
const effectiveHeight = Math.max(h, minHeight);
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
let bangElem;
const path = `M0 0
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1}
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0}
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0}
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${effectiveHeight * 0.1}
a${r},${r} 1 0,0 ${effectiveWidth * 0.15},${effectiveHeight * 0.33}
a${r * 0.8},${r * 0.8} 1 0,0 0,${effectiveHeight * 0.34}
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.15},${effectiveHeight * 0.33}
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${effectiveHeight * 0.15}
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${-1 * effectiveHeight * 0.15}
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
a${r * 0.8},${r * 0.8} 1 0,0 0,${-1 * effectiveHeight * 0.34}
a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
H0 V0 Z`;
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const roughNode = rc.path(path, options);
bangElem = shapeSvg.insert(() => roughNode, ':first-child');
bangElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
} else {
bangElem = shapeSvg
.insert('path', ':first-child')
.attr('class', 'basic label-container')
.attr('style', nodeStyles)
.attr('d', path);
}
// Translate the path (center the shape)
bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`);
updateNodeBounds(node, bangElem);
node.calcIntersect = function (bounds: Bounds, point: Point) {
return intersect.rect(bounds, point);
};
node.intersect = function (point) {
log.info('Bang intersect', node, point);
return intersect.rect(node, point);
};
return shapeSvg;
}

View File

@@ -1,18 +1,19 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';
import { log } from '../../../logger.js';
import type { Bounds, D3Selection, Point } from '../../../types.js';
import { handleUndefinedAttr } from '../../../utils.js';
import type { Node } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
export async function circle<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
const radius = bbox.width / 2 + halfPadding;
const radius =
node.from === 'mindmap' ? bbox.width / 2 + halfPadding * 2 : bbox.width / 2 + halfPadding;
let circleElem;
const { cssStyles } = node;
@@ -35,7 +36,10 @@ export async function circle<T extends SVGGraphicsElement>(parent: D3Selection<T
}
updateNodeBounds(node, circleElem);
node.calcIntersect = function (bounds: Bounds, point: Point) {
const radius = bounds.width / 2;
return intersect.circle(bounds, radius, point);
};
node.intersect = function (point) {
log.info('Circle intersect', node, radius, point);
return intersect.circle(node, radius, point);

View File

@@ -0,0 +1,80 @@
import rough from 'roughjs';
import { log } from '../../../logger.js';
import type { Bounds, D3Selection, Point } from '../../../types.js';
import { handleUndefinedAttr } from '../../../utils.js';
import type { Node } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
parent,
node,
getNodeClasses(node)
);
const w = bbox.width + 2 * halfPadding;
const h = bbox.height + 2 * halfPadding;
// Cloud radii
const r1 = 0.15 * w;
const r2 = 0.25 * w;
const r3 = 0.35 * w;
const r4 = 0.2 * w;
const { cssStyles } = node;
let cloudElem;
// Cloud path
const path = `M0 0
a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1}
a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1}
a${r2},${r2} 1 0,1 ${w * 0.35},${w * 0.2}
a${r1},${r1} 1 0,1 ${w * 0.15},${h * 0.35}
a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${h * 0.65}
a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15}
a${r3},${r3} 1 0,1 ${-1 * w * 0.5},0
a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15}
a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35}
a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65}
H0 V0 Z`;
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const roughNode = rc.path(path, options);
cloudElem = shapeSvg.insert(() => roughNode, ':first-child');
cloudElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
} else {
cloudElem = shapeSvg
.insert('path', ':first-child')
.attr('class', 'basic label-container')
.attr('style', nodeStyles)
.attr('d', path);
}
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
// Center the shape
cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
updateNodeBounds(node, cloudElem);
node.calcIntersect = function (bounds: Bounds, point: Point) {
return intersect.rect(bounds, point);
};
node.intersect = function (point) {
log.info('Cloud intersect', node, point);
return intersect.rect(node, point);
};
return shapeSvg;
}

View File

@@ -0,0 +1,64 @@
import type { Bounds, D3Selection, Point } from '../../../types.js';
import type { Node } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String } from './handDrawnShapeStyles.js';
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
export async function defaultMindmapNode<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
parent,
node,
getNodeClasses(node)
);
const w = bbox.width + 8 * halfPadding;
const h = bbox.height + 2 * halfPadding;
const rd = 5;
const rectPath = `
M${-w / 2} ${h / 2 - rd}
v${-h + 2 * rd}
q0,-${rd} ${rd},-${rd}
h${w - 2 * rd}
q${rd},0 ${rd},${rd}
v${h - 2 * rd}
q0,${rd} -${rd},${rd}
h${-w + 2 * rd}
q-${rd},0 -${rd},-${rd}
Z
`;
const bg = shapeSvg
.append('path')
.attr('id', 'node-' + node.id)
.attr('class', 'node-bkg node-' + node.type)
.attr('style', nodeStyles)
.attr('d', rectPath);
shapeSvg
.append('line')
.attr('class', 'node-line-')
.attr('x1', -w / 2)
.attr('y1', h / 2)
.attr('x2', w / 2)
.attr('y2', h / 2);
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
shapeSvg.append(() => label.node());
updateNodeBounds(node, bg);
node.calcIntersect = function (bounds: Bounds, point: Point) {
return intersect.rect(bounds, point);
};
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
}

Some files were not shown because too many files have changed in this diff Show More