Compare commits

..

1 Commits

Author SHA1 Message Date
omkarht
89b29898d2 implemented demo usecase diagram with antlr flow 2025-09-02 14:05:20 +05:30
119 changed files with 7231 additions and 5261 deletions

View File

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

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Render newlines as spaces in class diagrams

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

@@ -5,7 +5,6 @@ bmatrix
braintree braintree
catmull catmull
compositTitleSize compositTitleSize
cose
curv curv
doublecircle doublecircle
elems elems

View File

@@ -1,5 +1,4 @@
BRANDES BRANDES
Buzan
circo circo
handDrawn handDrawn
KOEPF KOEPF

View File

@@ -35,7 +35,7 @@ jobs:
# 2) No unwanted vitepress paths # 2) No unwanted vitepress paths
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then 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 fi
# 3) Lockfile only changes when package.json changes # 3) Lockfile only changes when package.json changes

1
.gitignore vendored
View File

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

View File

@@ -1,79 +0,0 @@
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<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,10 +159,12 @@ root
}); });
it('square shape', () => { it('square shape', () => {
imgSnapshotTest( imgSnapshotTest(
`mindmap `
mindmap
root[ root[
The root The root
]`, ]
`,
{}, {},
undefined, undefined,
shouldHaveRoot shouldHaveRoot
@@ -170,10 +172,12 @@ root
}); });
it('rounded rect shape', () => { it('rounded rect shape', () => {
imgSnapshotTest( imgSnapshotTest(
`mindmap `
mindmap
root(( root((
The root The root
))`, ))
`,
{}, {},
undefined, undefined,
shouldHaveRoot shouldHaveRoot
@@ -181,10 +185,12 @@ root
}); });
it('circle shape', () => { it('circle shape', () => {
imgSnapshotTest( imgSnapshotTest(
`mindmap `
mindmap
root( root(
The root The root
)`, )
`,
{}, {},
undefined, undefined,
shouldHaveRoot shouldHaveRoot
@@ -192,8 +198,10 @@ root
}); });
it('default shape', () => { it('default shape', () => {
imgSnapshotTest( imgSnapshotTest(
`mindmap `
The root`, mindmap
The root
`,
{}, {},
undefined, undefined,
shouldHaveRoot shouldHaveRoot
@@ -201,10 +209,12 @@ root
}); });
it('adding children', () => { it('adding children', () => {
imgSnapshotTest( imgSnapshotTest(
`mindmap `
mindmap
The root The root
child1 child1
child2`, child2
`,
{}, {},
undefined, undefined,
shouldHaveRoot shouldHaveRoot
@@ -212,11 +222,13 @@ root
}); });
it('adding grand children', () => { it('adding grand children', () => {
imgSnapshotTest( imgSnapshotTest(
`mindmap `
mindmap
The root The root
child1 child1
child2 child2
child3`, child3
`,
{}, {},
undefined, undefined,
shouldHaveRoot shouldHaveRoot
@@ -228,21 +240,25 @@ root
`mindmap `mindmap
id1[\`**Start** with id1[\`**Start** with
a second line 😎\`] a second line 😎\`]
id2[\`The dog in **the** hog... a *very long text* about it Word!\`]` id2[\`The dog in **the** hog... a *very long text* about it
Word!\`]
`
); );
}); });
}); });
describe('Include char sequence "graph" in text (#6795)', () => { describe('Include char sequence "graph" in text (#6795)', () => {
it('has a label with char sequence "graph"', () => { it('has a label with char sequence "graph"', () => {
imgSnapshotTest( imgSnapshotTest(
` mindmap `
mindmap
root root
Photograph Photograph
Waterfall Waterfall
Landscape Landscape
Geography Geography
Mountains Mountains
Rocks`, Rocks
`,
{ flowchart: { defaultRenderer: 'elk' } } { flowchart: { defaultRenderer: 'elk' } }
); );
}); });

View File

@@ -130,76 +130,6 @@
</head> </head>
<body> <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"> <pre id="diagram4" class="mermaid">
--- ---
config: config:
@@ -261,145 +191,8 @@ treemap
"Item B2": 25 "Item B2": 25
</pre> </pre>
<pre id="diagram4" class="mermaid2"> <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 flowchart LR
root{mindmap} --- Origins --- Europe AB["apa@apa@"] --> B(("`apa@apa`"))
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>
<pre id="diagram4" class="mermaid2"> <pre id="diagram4" class="mermaid2">
flowchart flowchart
@@ -481,44 +274,6 @@ config:
</pre> </pre>
<pre id="diagram4" class="mermaid2"> <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: config:
layout: elk layout: elk
--- ---

View File

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

View File

@@ -2,227 +2,223 @@
"durations": [ "durations": [
{ {
"spec": "cypress/integration/other/configuration.spec.js", "spec": "cypress/integration/other/configuration.spec.js",
"duration": 5841 "duration": 6162
}, },
{ {
"spec": "cypress/integration/other/external-diagrams.spec.js", "spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 2138 "duration": 2148
}, },
{ {
"spec": "cypress/integration/other/ghsa.spec.js", "spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3370 "duration": 3585
}, },
{ {
"spec": "cypress/integration/other/iife.spec.js", "spec": "cypress/integration/other/iife.spec.js",
"duration": 2052 "duration": 2099
}, },
{ {
"spec": "cypress/integration/other/interaction.spec.js", "spec": "cypress/integration/other/interaction.spec.js",
"duration": 12243 "duration": 12119
}, },
{ {
"spec": "cypress/integration/other/rerender.spec.js", "spec": "cypress/integration/other/rerender.spec.js",
"duration": 2065 "duration": 2063
}, },
{ {
"spec": "cypress/integration/other/xss.spec.js", "spec": "cypress/integration/other/xss.spec.js",
"duration": 31288 "duration": 31921
}, },
{ {
"spec": "cypress/integration/rendering/appli.spec.js", "spec": "cypress/integration/rendering/appli.spec.js",
"duration": 3421 "duration": 3385
}, },
{ {
"spec": "cypress/integration/rendering/architecture.spec.ts", "spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 97 "duration": 108
}, },
{ {
"spec": "cypress/integration/rendering/block.spec.js", "spec": "cypress/integration/rendering/block.spec.js",
"duration": 18500 "duration": 18063
}, },
{ {
"spec": "cypress/integration/rendering/c4.spec.js", "spec": "cypress/integration/rendering/c4.spec.js",
"duration": 5793 "duration": 5519
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js", "spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 40966 "duration": 40040
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js", "spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 39176 "duration": 38665
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js", "spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 23468 "duration": 22836
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js", "spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 38291 "duration": 37096
}, },
{ {
"spec": "cypress/integration/rendering/classDiagram.spec.js", "spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 16949 "duration": 16452
}, },
{ {
"spec": "cypress/integration/rendering/conf-and-directives.spec.js", "spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 9480 "duration": 10387
}, },
{ {
"spec": "cypress/integration/rendering/current.spec.js", "spec": "cypress/integration/rendering/current.spec.js",
"duration": 2753 "duration": 2803
}, },
{ {
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js", "spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 88028 "duration": 86891
}, },
{ {
"spec": "cypress/integration/rendering/erDiagram.spec.js", "spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 15615 "duration": 15206
}, },
{ {
"spec": "cypress/integration/rendering/errorDiagram.spec.js", "spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 3706 "duration": 3540
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-elk.spec.js", "spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 43905 "duration": 41975
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js", "spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 31217 "duration": 30909
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-icon.spec.js", "spec": "cypress/integration/rendering/flowchart-icon.spec.js",
"duration": 7531 "duration": 7881
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts", "spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 25423 "duration": 24294
}, },
{ {
"spec": "cypress/integration/rendering/flowchart-v2.spec.js", "spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 49664 "duration": 47652
}, },
{ {
"spec": "cypress/integration/rendering/flowchart.spec.js", "spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 32525 "duration": 32049
}, },
{ {
"spec": "cypress/integration/rendering/gantt.spec.js", "spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 20915 "duration": 20248
}, },
{ {
"spec": "cypress/integration/rendering/gitGraph.spec.js", "spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 53556 "duration": 51202
}, },
{ {
"spec": "cypress/integration/rendering/iconShape.spec.ts", "spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 283038 "duration": 283546
}, },
{ {
"spec": "cypress/integration/rendering/imageShape.spec.ts", "spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 59434 "duration": 57257
}, },
{ {
"spec": "cypress/integration/rendering/info.spec.ts", "spec": "cypress/integration/rendering/info.spec.ts",
"duration": 3101 "duration": 3352
}, },
{ {
"spec": "cypress/integration/rendering/journey.spec.js", "spec": "cypress/integration/rendering/journey.spec.js",
"duration": 7099 "duration": 7423
}, },
{ {
"spec": "cypress/integration/rendering/kanban.spec.ts", "spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 7567 "duration": 7804
}, },
{ {
"spec": "cypress/integration/rendering/katex.spec.js", "spec": "cypress/integration/rendering/katex.spec.js",
"duration": 3817 "duration": 3847
}, },
{ {
"spec": "cypress/integration/rendering/marker_unique_id.spec.js", "spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 2624 "duration": 2637
},
{
"spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js",
"duration": 4246
}, },
{ {
"spec": "cypress/integration/rendering/mindmap.spec.ts", "spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 11967 "duration": 11658
}, },
{ {
"spec": "cypress/integration/rendering/newShapes.spec.ts", "spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 151914 "duration": 149500
}, },
{ {
"spec": "cypress/integration/rendering/oldShapes.spec.ts", "spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 116698 "duration": 115427
}, },
{ {
"spec": "cypress/integration/rendering/packet.spec.ts", "spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 4967 "duration": 4801
}, },
{ {
"spec": "cypress/integration/rendering/pie.spec.ts", "spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 6700 "duration": 6786
}, },
{ {
"spec": "cypress/integration/rendering/quadrantChart.spec.js", "spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 8963 "duration": 9422
}, },
{ {
"spec": "cypress/integration/rendering/radar.spec.js", "spec": "cypress/integration/rendering/radar.spec.js",
"duration": 5540 "duration": 5652
}, },
{ {
"spec": "cypress/integration/rendering/requirement.spec.js", "spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2782 "duration": 2787
}, },
{ {
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js", "spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 54797 "duration": 53631
}, },
{ {
"spec": "cypress/integration/rendering/sankey.spec.ts", "spec": "cypress/integration/rendering/sankey.spec.ts",
"duration": 6914 "duration": 7075
}, },
{ {
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js", "spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
"duration": 20481 "duration": 20446
}, },
{ {
"spec": "cypress/integration/rendering/sequencediagram.spec.js", "spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 38490 "duration": 37326
}, },
{ {
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js", "spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 30766 "duration": 29208
}, },
{ {
"spec": "cypress/integration/rendering/stateDiagram.spec.js", "spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 16705 "duration": 16328
}, },
{ {
"spec": "cypress/integration/rendering/theme.spec.js", "spec": "cypress/integration/rendering/theme.spec.js",
"duration": 30928 "duration": 30541
}, },
{ {
"spec": "cypress/integration/rendering/timeline.spec.ts", "spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 8424 "duration": 8611
}, },
{ {
"spec": "cypress/integration/rendering/treemap.spec.ts", "spec": "cypress/integration/rendering/treemap.spec.ts",
"duration": 12533 "duration": 11878
}, },
{ {
"spec": "cypress/integration/rendering/xyChart.spec.js", "spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 21197 "duration": 20400
}, },
{ {
"spec": "cypress/integration/rendering/zenuml.spec.js", "spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 3455 "duration": 3528
} }
] ]
} }

View File

@@ -1,40 +0,0 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/layouts.md](../../packages/mermaid/src/docs/config/layouts.md).
# Layouts
This page lists the available layout algorithms supported in Mermaid diagrams.
## Supported Layouts
- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/)
- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree)
- **cose-bilkent**: Cose Bilkent layout for force-directed graphs
- **dagre**: Dagre layout for layered graphs
## How to Use
You can specify the layout in your diagram's YAML config or initialization options. For example:
```mermaid-example
---
config:
layout: elk
---
graph TD;
A-->B;
B-->C;
```
```mermaid
---
config:
layout: elk
---
graph TD;
A-->B;
B-->C;
```

View File

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

View File

@@ -0,0 +1,159 @@
> **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 # Interface: LayoutData
Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L168) 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)
## Indexable ## Indexable
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.co
> **config**: [`MermaidConfig`](MermaidConfig.md) > **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171) 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)
--- ---
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.co
> **edges**: `Edge`\[] > **edges**: `Edge`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170) 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)
--- ---
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.co
> **nodes**: `Node`\[] > **nodes**: `Node`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169) 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)

View File

@@ -32,7 +32,7 @@ page.
### detectType() ### 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) 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()~~
> **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) 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) [`MermaidConfig`](MermaidConfig.md)
**Deprecated**, please set configuration in [initialize](#initialize). **Deprecated**, please set configuration in [initialize](Mermaid.md#initialize).
##### nodes? ##### nodes?
@@ -141,13 +141,13 @@ Called once for each rendered diagram's id.
#### Deprecated #### Deprecated
Use [initialize](#initialize) and [run](#run) instead. Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
Renders the mermaid diagrams Renders the mermaid diagrams
#### Deprecated #### Deprecated
Use [initialize](#initialize) and [run](#run) instead. Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
--- ---
@@ -176,7 +176,7 @@ Configuration object for mermaid.
### ~~mermaidAPI~~ ### ~~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) Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
@@ -184,81 +184,73 @@ Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/
#### Deprecated #### Deprecated
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. 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.
--- ---
### parse() ### 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) 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. Parse the text and validate the syntax.
##### Parameters #### Parameters
###### text ##### text
`string` `string`
The mermaid diagram definition. The mermaid diagram definition.
###### parseOptions ##### parseOptions
[`ParseOptions`](ParseOptions.md) & `object` [`ParseOptions`](ParseOptions.md) & `object`
Options for parsing. Options for parsing.
##### Returns #### Returns
`Promise`<`false` | [`ParseResult`](ParseResult.md)> `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`. 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) [ParseOptions](ParseOptions.md)
##### Throws #### Throws
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set. 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. Parse the text and validate the syntax.
##### Parameters #### Parameters
###### text ##### text
`string` `string`
The mermaid diagram definition. The mermaid diagram definition.
###### parseOptions? ##### parseOptions?
[`ParseOptions`](ParseOptions.md) [`ParseOptions`](ParseOptions.md)
Options for parsing. Options for parsing.
##### Returns #### Returns
`Promise`<[`ParseResult`](ParseResult.md)> `Promise`<[`ParseResult`](ParseResult.md)>
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`. 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) [ParseOptions](ParseOptions.md)
##### Throws #### Throws
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set. Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
@@ -340,7 +332,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/
### render() ### 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) 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 # Interface: ParseOptions
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:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
## Properties ## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean` > `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93) Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid. If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called. The `parseError` function will not be called.

View File

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

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult # Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114) Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
## Properties ## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/me
> `optional` **bindFunctions**: (`element`) => `void` > `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132) Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
Bind function to be called after the svg has been inserted into the DOM. 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. 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` > **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122) Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
The diagram type, e.g. 'flowchart', 'sequence', etc. The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string` > **svg**: `string`
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118) Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
The svg code for the rendered graph. The svg code for the rendered graph.

View File

@@ -1,65 +0,0 @@
> **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 # 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) 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() # 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) 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 # 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) 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 # 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) 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)

View File

@@ -1,89 +0,0 @@
> **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
The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps.
## Features
- Organizes nodes in a tidy, non-overlapping tree
- Ideal for mindmaps and hierarchical data
- Automatically adjusts spacing for readability
## Example Usage
```mermaid-example
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
```mermaid
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
```mermaid-example
---
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
```
```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<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
```
## Note
- Currently, tidy-tree is primarily supported for mindmap diagrams.

View File

@@ -326,9 +326,7 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | | **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` | | 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` | | Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` | | Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` | | Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |

View File

@@ -314,22 +314,3 @@ You can also refer the [implementation in the live editor](https://github.com/me
cspell:locale en,en-gb cspell:locale en,en-gb
cspell:ignore Buzan 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

@@ -138,7 +138,7 @@ xychart
## Chart Theme Variables ## Chart Theme Variables
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax: Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
```yaml ```yaml
--- ---
@@ -163,52 +163,6 @@ config:
| yAxisLineColor | Color of the y-axis line | | yAxisLineColor | Color of the y-axis line |
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" | | plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
### Setting Colors for Lines and Bars
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
```mermaid-example
---
config:
themeVariables:
xyChart:
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
---
xychart
title "Different Colors in xyChart"
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
y-axis "valuesY" 0 --> 50
%% Black line
line [10,20,30,40]
%% Blue bar
bar [20,30,25,35]
%% Green bar
bar [15,25,20,30]
%% Red line
line [5,15,25,35]
```
```mermaid
---
config:
themeVariables:
xyChart:
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
---
xychart
title "Different Colors in xyChart"
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
y-axis "valuesY" 0 --> 50
%% Black line
line [10,20,30,40]
%% Blue bar
bar [20,30,25,35]
%% Green bar
bar [15,25,20,30]
%% Red line
line [5,15,25,35]
```
## Example on config and theme ## Example on config and theme
```mermaid-example ```mermaid-example

View File

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

View File

@@ -1,16 +1,5 @@
# @mermaid-js/layout-elk # @mermaid-js/layout-elk
## 0.2.0
### Minor Changes
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- Updated dependencies [[`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800), [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20), [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05), [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8)]:
- mermaid@11.11.0
## 0.1.9 ## 0.1.9
### Patch Changes ### Patch Changes

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. 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 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. > 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.mrtree`: Multi-root tree layout
- `elk.sporeOverlap`: Spore overlap 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

@@ -1,6 +1,6 @@
{ {
"name": "@mermaid-js/layout-elk", "name": "@mermaid-js/layout-elk",
"version": "0.2.0", "version": "0.1.9",
"description": "ELK layout engine for mermaid", "description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs", "module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts", "types": "dist/layouts.d.ts",

View File

@@ -1,9 +0,0 @@
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,8 +4,7 @@ import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from '
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js'; import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
type Node = LayoutData['nodes'][number]; 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 { interface LabelData {
width: number; width: number;
height: number; height: number;
@@ -14,20 +13,11 @@ interface LabelData {
} }
interface NodeWithVertex extends Omit<Node, 'domId'> { interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: LayoutData['nodes']; children?: unknown[];
labelData?: LabelData; labelData?: LabelData;
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>; 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 ( export const render = async (
data4Layout: LayoutData, data4Layout: LayoutData,
svg: SVG, svg: SVG,
@@ -61,30 +51,15 @@ export const render = async (
// Add the element to the DOM // Add the element to the DOM
if (!node.isGroup) { if (!node.isGroup) {
// Create a clean node object for ELK with only the properties it expects
const child: NodeWithVertex = { const child: NodeWithVertex = {
id: node.id, ...node,
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); graph.children.push(child);
nodeDb[node.id] = child; nodeDb[node.id] = child;
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir }); const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
const boundingBox = childNodeEl.node()!.getBBox(); const boundingBox = childNodeEl.node()!.getBBox();
// Store the domId separately for rendering, not in the ELK graph
child.domId = childNodeEl; child.domId = childNodeEl;
child.calcIntersect = node.calcIntersect;
child.width = boundingBox.width; child.width = boundingBox.width;
child.height = boundingBox.height; child.height = boundingBox.height;
} else { } else {
@@ -484,6 +459,302 @@ 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 // @ts-ignore - ELK is not typed
const elk = new ELK(); const elk = new ELK();
const element = svg.select('g'); const element = svg.select('g');
@@ -598,16 +869,11 @@ export const render = async (
delete node.height; delete node.height;
} }
}); });
log.debug('APA01 processing edges, count:', elkGraph.edges.length); elkGraph.edges.forEach((edge: any) => {
elkGraph.edges.forEach((edge: any, index: number) => {
log.debug('APA01 processing edge', index, ':', edge);
const source = edge.sources[0]; const source = edge.sources[0];
const target = edge.targets[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] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) { if (nodeDb[source].parentId !== nodeDb[target].parentId) {
const ancestorId = findCommonAncestor(source, target, parentLookupDb); const ancestorId = findCommonAncestor(source, target, parentLookupDb);
// an edge that breaks a subgraph has been identified, set configuration accordingly // an edge that breaks a subgraph has been identified, set configuration accordingly
setIncludeChildrenPolicy(source, ancestorId); setIncludeChildrenPolicy(source, ancestorId);
@@ -615,37 +881,7 @@ export const render = async (
} }
}); });
log.debug('APA01 before'); const g = await elk.layout(elkGraph);
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; // debugger;
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0); await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
@@ -733,38 +969,43 @@ export const render = async (
startNode.innerHTML startNode.innerHTML
); );
} }
if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
if (startNode.calcIntersect) { edge.points.unshift({
const intersection = startNode.calcIntersect( x: startNode.offset.posX + startNode.width / 2,
{ y: startNode.offset.posY + startNode.height / 2,
x: startNode.offset.posX + startNode.width / 2, });
y: startNode.offset.posY + startNode.height / 2,
width: startNode.width,
height: startNode.height,
},
edge.points[0]
);
if (distance(intersection, edge.points[0]) > epsilon) {
edge.points.unshift(intersection);
}
} }
if (endNode.calcIntersect) { if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
const intersection = endNode.calcIntersect( edge.points.push({
{ x: endNode.offset.posX + endNode.width / 2,
x: endNode.offset.posX + endNode.width / 2, y: endNode.offset.posY + endNode.height / 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);
}
} }
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();
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'
);
const paths = insertEdge( const paths = insertEdge(
edgesEl, edgesEl,
edge, edge,
@@ -774,6 +1015,7 @@ export const render = async (
endNode, endNode,
data4Layout.diagramId 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.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; edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;

View File

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

View File

@@ -1,12 +0,0 @@
# @mermaid-js/layout-tidy-tree
## 0.2.0
### Minor Changes
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- Updated dependencies [[`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800), [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20), [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05), [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8)]:
- mermaid@11.11.0

View File

@@ -1,59 +0,0 @@
# @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

@@ -1,46 +0,0 @@
{
"name": "@mermaid-js/layout-tidy-tree",
"version": "0.2.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

@@ -1,50 +0,0 @@
/**
* 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

@@ -1,409 +0,0 @@
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

@@ -1,629 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,180 +0,0 @@
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

@@ -1,69 +0,0 @@
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

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

View File

@@ -1,19 +1,5 @@
# mermaid # mermaid
## 11.11.0
### Minor Changes
- [#6704](https://github.com/mermaid-js/mermaid/pull/6704) [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05) Thanks [@omkarht](https://github.com/omkarht)! - feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- [#6905](https://github.com/mermaid-js/mermaid/pull/6905) [`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Render newlines as spaces in class diagrams
- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled
## 11.10.0 ## 11.10.0
### Minor Changes ### Minor Changes
@@ -243,6 +229,7 @@
- [#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 - [#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: - [#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. - Updates the class diagram to the new unified way of rendering.
- Includes a new "classBox" shape to be used in diagrams - Includes a new "classBox" shape to be used in diagrams
- Other updates such as: - Other updates such as:

View File

@@ -1,6 +1,6 @@
{ {
"name": "mermaid", "name": "mermaid",
"version": "11.11.0", "version": "11.10.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module", "type": "module",
"module": "./dist/mermaid.core.mjs", "module": "./dist/mermaid.core.mjs",
@@ -82,7 +82,7 @@
"katex": "^0.16.22", "katex": "^0.16.22",
"khroma": "^2.1.0", "khroma": "^2.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.7", "marked": "^16.0.0",
"roughjs": "^4.6.6", "roughjs": "^4.6.6",
"stylis": "^4.3.6", "stylis": "^4.3.6",
"ts-dedent": "^2.2.0", "ts-dedent": "^2.2.0",
@@ -123,8 +123,8 @@
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"start-server-and-test": "^2.0.10", "start-server-and-test": "^2.0.10",
"type-fest": "^4.35.0", "type-fest": "^4.35.0",
"typedoc": "^0.28.9", "typedoc": "^0.27.8",
"typedoc-plugin-markdown": "^4.8.0", "typedoc-plugin-markdown": "^4.4.2",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"unist-util-flatmap": "^1.0.0", "unist-util-flatmap": "^1.0.0",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",

View File

@@ -171,9 +171,7 @@ This Markdown should be kept.
expect(buildShapeDoc()).toMatchInlineSnapshot(` expect(buildShapeDoc()).toMatchInlineSnapshot(`
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | "| **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\` | | 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\` | | Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` | | Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` | | Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js'; import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js'; import { registerDiagram } from './diagramAPI.js';
import { treemap } from '../diagrams/treemap/detector.js'; import { treemap } from '../diagrams/treemap/detector.js';
import usecase from '../diagrams/useCase/useCaseDetector.js';
import '../type.d.ts'; import '../type.d.ts';
let hasLoadedDiagrams = false; let hasLoadedDiagrams = false;
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
xychart, xychart,
block, block,
radar, radar,
treemap treemap,
usecase
); );
}; };

View File

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

View File

@@ -1070,14 +1070,6 @@ describe('given a class diagram with members and methods ', function () {
parser.parse(str); parser.parse(str);
}); });
it('should handle an empty class body with {}', function () {
const str = 'classDiagram\nclass EmptyClass {}';
parser.parse(str);
const actual = parser.yy.getClass('EmptyClass');
expect(actual.label).toBe('EmptyClass');
expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(0);
});
}); });
}); });

View File

@@ -293,7 +293,6 @@ classStatement
: classIdentifier : classIdentifier
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);} | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);} | classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
| classIdentifier STRUCT_START STRUCT_STOP {}
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);} | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
; ;
@@ -302,15 +301,8 @@ classIdentifier
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);} | CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
; ;
emptyBody
:
| SPACE emptyBody
| NEWLINE emptyBody
;
annotationStatement annotationStatement
: ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); } :ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
; ;
members members

View File

@@ -1,297 +0,0 @@
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

@@ -1,26 +1,9 @@
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import { v4 } from 'uuid';
import type { D3Element } from '../../types.js'; import type { D3Element } from '../../types.js';
import { sanitizeText } from '../../diagrams/common/common.js'; import { sanitizeText } from '../../diagrams/common/common.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import type { MindmapNode } from './mindmapTypes.js'; import type { MindmapNode } from './mindmapTypes.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
import { getUserDefinedConfig } from '../../config.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 = { const nodeType = {
DEFAULT: 0, DEFAULT: 0,
@@ -44,6 +27,7 @@ export class MindmapDB {
this.nodeType = nodeType; this.nodeType = nodeType;
this.clear(); this.clear();
this.getType = this.getType.bind(this); this.getType = this.getType.bind(this);
this.getMindmap = this.getMindmap.bind(this);
this.getElementById = this.getElementById.bind(this); this.getElementById = this.getElementById.bind(this);
this.getParent = this.getParent.bind(this); this.getParent = this.getParent.bind(this);
this.getMindmap = this.getMindmap.bind(this); this.getMindmap = this.getMindmap.bind(this);
@@ -172,223 +156,6 @@ 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 'mindmapCircle';
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();
const userDefinedConfig = getUserDefinedConfig();
const hasUserDefinedLayout = userDefinedConfig.layout !== undefined;
const finalConfig = config;
if (!hasUserDefinedLayout) {
finalConfig.layout = 'cose-bilkent';
}
if (!mindmapRoot) {
return {
nodes: [],
edges: [],
config: finalConfig,
};
}
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: finalConfig,
// Store the root node for mindmap-specific layout algorithms
rootNode: mindmapRoot,
// Properties required by dagre layout algorithm
markers: ['point'], // 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-' + v4(),
};
}
// Expose logger to grammar
public getLogger() { public getLogger() {
return log; return log;
} }

View File

@@ -1,83 +1,200 @@
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 type { DrawDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; import type { D3Element } from '../../types.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { LayoutData } from '../../rendering-util/types.js'; import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
import type { FilledMindMapNode } from './mindmapTypes.js'; import { drawNode, positionNode } from './svgDraw.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
import type { MindmapDB } from './mindmapDb.js'; import type { MindmapDB } from './mindmapDb.js';
// Inject the layout algorithm into cytoscape
cytoscape.use(coseBilkent);
/** async function drawNodes(
* Update the layout data with actual node dimensions after drawing db: MindmapDB,
*/ svg: D3Element,
function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) { mindmap: FilledMindMapNode,
const updateNode = (node: FilledMindMapNode) => { section: number,
// Find the corresponding node in the layout data conf: MermaidConfig
const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString()); ) {
if (layoutNode) { await drawNode(db, svg, mindmap, section, conf);
// Update with the actual dimensions calculated by drawNode if (mindmap.children) {
layoutNode.width = node.width; await Promise.all(
layoutNode.height = node.height; mindmap.children.map((child, index) =>
log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height); 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);
} }
});
}
// Recursively update children function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
node.children?.forEach(updateNode); 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,
},
});
});
}
}
updateNode(mindmapRoot); 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})`);
});
} }
export const draw: DrawDefinition = async (text, id, _version, diagObj) => { export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
log.debug('Rendering mindmap diagram\n' + text); log.debug('Rendering mindmap diagram\n' + text);
// Draw the nodes first to get their dimensions, then update the layout data
const db = diagObj.db as MindmapDB; 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, data4Layout.config.securityLevel);
data4Layout.type = diagObj.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(data4Layout.config.layout, {
fallback: 'cose-bilkent',
});
data4Layout.diagramId = id;
const mm = db.getMindmap(); const mm = db.getMindmap();
if (!mm) { if (!mm) {
return; return;
} }
data4Layout.nodes.forEach((node) => { const conf = getConfig();
if (node.shape === 'rounded') { conf.htmlLabels = false;
node.radius = 15;
node.taper = 15;
node.stroke = 'none';
node.width = 0;
node.padding = 15;
} else if (node.shape === 'circle') {
node.padding = 10;
} else if (node.shape === 'rect') {
node.width = 0;
node.padding = 10;
}
});
// Use the unified rendering system const svg = selectSvgElement(id);
await render(data4Layout, svg);
// Setup the view box and size of the svg element using config from data4Layout // Draw the graph and start with drawing the nodes without proper position
setupViewPortForSVG( // 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);
// Setup the view box and size of the svg element
setupGraphViewbox(
undefined,
svg, svg,
data4Layout.config.mindmap?.padding ?? defaultConfig.mindmap.padding, conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
'mindmapDiagram', conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
data4Layout.config.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
); );
}; };

View File

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

View File

@@ -1368,7 +1368,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
it('should handle box without description', async () => { it('should handle box without description', async () => {
const diagram = await Diagram.fromText(` const diagram = await Diagram.fromText(`
sequenceDiagram sequenceDiagram
box aqua box Aqua
participant a as Alice participant a as Alice
participant b as Bob participant b as Bob
end end
@@ -1384,7 +1384,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
const boxes = diagram.db.getBoxes(); const boxes = diagram.db.getBoxes();
expect(boxes[0].name).toBeFalsy(); expect(boxes[0].name).toBeFalsy();
expect(boxes[0].actorKeys).toEqual(['a', 'b']); 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 () => { it('should handle simple actor creation', async () => {

View File

@@ -0,0 +1,124 @@
const getStyles = (options) =>
`
.usecase-diagram {
font-family: ${options.fontFamily};
font-size: ${options.fontSize};
}
/* Actor styles */
.usecase-actor-man {
stroke: ${options.actorBorder};
fill: ${options.actorBkg};
stroke-width: 2px;
}
.usecase-actor-man circle {
fill: ${options.useCaseActorBkg};
stroke: ${options.useCaseActorBorder};
stroke-width: 2px;
}
.usecase-actor-man line {
stroke: ${options.useCaseActorBorder};
stroke-width: 2px;
stroke-linecap: round;
}
.usecase-actor-man text {
font-family: ${options.fontFamily};
font-size: 14px;
font-weight: normal;
fill: ${options.useCaseActorTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Use case styles */
.usecase-usecase {
fill: ${options.useCaseUseCaseBkg};
stroke: ${options.useCaseUseCaseBorder};
stroke-width: 1px;
}
.usecase-usecase text {
font-family: ${options.fontFamily};
font-size: 12px;
fill: ${options.useCaseUseCaseTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* System boundary styles */
.usecase-system-boundary {
fill: ${options.useCaseSystemBoundaryBkg};
stroke: ${options.useCaseSystemBoundaryBorder};
stroke-width: 2px;
stroke-dasharray: 5,5;
}
.usecase-system-boundary text {
font-family: ${options.fontFamily};
font-size: 14px;
font-weight: bold;
fill: ${options.useCaseSystemBoundaryTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Arrow and relationship styles */
.usecase-arrow {
stroke: ${'red'};
stroke-width: 2px;
fill: none;
}
.usecase-arrow-label {
font-family: ${options.fontFamily};
font-size: 12px;
fill: ${options.useCaseArrowTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Node styles for standalone nodes */
.usecase-node {
fill: ${options.useCaseUseCaseBkg};
stroke: ${options.useCaseUseCaseBorder};
stroke-width: 1px;
}
.usecase-node text {
font-family: ${options.fontFamily};
font-size: 12px;
fill: ${options.useCaseUseCaseTextColor};
text-anchor: middle;
dominant-baseline: central;
}
/* Hover effects */
.usecase-actor-man:hover circle {
fill: ${options.useCaseActorBkg};
stroke: ${options.useCaseArrowColor};
}
.usecase-actor-man:hover line {
stroke: ${options.useCaseArrowColor};
}
.usecase-actor-man:hover text {
fill: ${options.useCaseArrowColor};
font-weight: bold;
}
.usecase-usecase:hover {
fill: ${options.useCaseSystemBoundaryBkg};
stroke: ${options.useCaseArrowColor};
}
.usecase-usecase:hover text {
fill: ${options.useCaseArrowColor};
font-weight: bold;
}
`;
export default getStyles;

View File

@@ -0,0 +1,586 @@
// Simple actor type for useCase diagrams
interface Actor {
type: 'actor';
name: string;
metadata?: Record<string, string>;
}
// Simple use case type
interface UseCase {
type: 'useCase';
name: string;
}
// System boundary type
interface SystemBoundary {
type: 'systemBoundary';
name: string;
useCases: UseCase[];
metadata?: Record<string, string>;
}
// System boundary metadata type
interface SystemBoundaryMetadata {
type: 'systemBoundaryMetadata';
name: string; // boundary name
metadata: Record<string, string>;
}
// Actor-UseCase relationship type
interface ActorUseCaseRelationship {
type: 'actorUseCaseRelationship';
from: string; // actor name
to: string; // use case name
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
// Node type
interface Node {
type: 'node';
id: string; // node ID (e.g., 'a', 'b', 'c')
label: string; // node label (e.g., 'Go through code')
}
// Actor-Node relationship type
interface ActorNodeRelationship {
type: 'actorNodeRelationship';
from: string; // actor name
to: string; // node ID
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
// Inline Actor-Node relationship type
interface InlineActorNodeRelationship {
type: 'inlineActorNodeRelationship';
actor: string; // actor name
node: Node; // node definition
arrow: string; // '-->' or '->'
label?: string; // edge label (optional)
}
export class UseCaseDB {
private actors: Actor[] = [];
private systemBoundaries: SystemBoundary[] = [];
private systemBoundaryMetadata: SystemBoundaryMetadata[] = [];
private useCases: UseCase[] = [];
private relationships: ActorUseCaseRelationship[] = [];
private nodes: Node[] = [];
private nodeRelationships: ActorNodeRelationship[] = [];
private inlineRelationships: InlineActorNodeRelationship[] = [];
constructor() {
this.clear();
}
clear(): void {
this.actors = [];
this.systemBoundaries = [];
this.systemBoundaryMetadata = [];
this.useCases = [];
this.relationships = [];
this.nodes = [];
this.nodeRelationships = [];
this.inlineRelationships = [];
}
addActor(actor: Actor): void {
this.actors.push(actor);
}
addSystemBoundary(boundary: SystemBoundary): void {
this.systemBoundaries.push(boundary);
}
addSystemBoundaryMetadata(metadata: SystemBoundaryMetadata): void {
this.systemBoundaryMetadata.push(metadata);
// Apply metadata to existing system boundary
const boundary = this.systemBoundaries.find(b => b.name === metadata.name);
if (boundary) {
boundary.metadata = metadata.metadata;
}
}
addUseCase(useCase: UseCase): void {
this.useCases.push(useCase);
}
addRelationship(relationship: ActorUseCaseRelationship): void {
this.relationships.push(relationship);
}
addNode(node: Node): void {
this.nodes.push(node);
}
addNodeRelationship(relationship: ActorNodeRelationship): void {
this.nodeRelationships.push(relationship);
}
addInlineRelationship(relationship: InlineActorNodeRelationship): void {
this.inlineRelationships.push(relationship);
// Also add the node and actor separately
this.addNode(relationship.node);
// Add actor if not already exists
const actorExists = this.actors.some(actor => actor.name === relationship.actor);
if (!actorExists) {
this.addActor({
type: 'actor',
name: relationship.actor
});
}
}
getActors(): Actor[] {
return this.actors;
}
getSystemBoundaries(): SystemBoundary[] {
return this.systemBoundaries;
}
getSystemBoundaryMetadata(): SystemBoundaryMetadata[] {
return this.systemBoundaryMetadata;
}
getUseCases(): UseCase[] {
return this.useCases;
}
getRelationships(): ActorUseCaseRelationship[] {
return this.relationships;
}
getNodes(): Node[] {
return this.nodes;
}
getNodeRelationships(): ActorNodeRelationship[] {
return this.nodeRelationships;
}
getInlineRelationships(): InlineActorNodeRelationship[] {
return this.inlineRelationships;
}
parse(text: string): void {
this.clear();
// For now, use the simple parser with enhanced metadata support
// TODO: Integrate ANTLR parser in the future
// Simple parser for usecase diagrams (fallback)
const lines = text.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('%'));
let foundUsecase = false;
let inSystemBoundary = false;
let currentBoundary: SystemBoundary | null = null;
let inMetadataBlock = false;
let currentMetadataName = '';
let currentMetadataContent = '';
for (const line of lines) {
if (line === 'usecase') {
foundUsecase = true;
continue;
}
if (!foundUsecase) {
continue
};
if (line.startsWith('actor ')) {
const actorPart = line.substring(6).trim();
if (actorPart) {
// Check if this is an inline actor-node relationship
if (this.isInlineActorNodeRelationshipLine(actorPart)) {
const relationship = this.parseInlineActorNodeRelationshipLine(actorPart);
if (relationship) {
this.addInlineRelationship(relationship);
}
} else {
const actors = this.parseActorList(actorPart);
actors.forEach((actor: Actor) => this.addActor(actor));
}
}
} else if (line.startsWith('systemBoundary ')) {
const boundaryPart = line.substring(15).trim();
if (boundaryPart.endsWith(' {')) {
// New curly brace syntax: systemBoundary Name {
const boundaryName = boundaryPart.substring(0, boundaryPart.length - 2).trim();
currentBoundary = {
type: 'systemBoundary',
name: boundaryName,
useCases: []
};
inSystemBoundary = true;
} else if (boundaryPart) {
// Old syntax: systemBoundary Name (followed by 'end')
currentBoundary = {
type: 'systemBoundary',
name: boundaryPart,
useCases: []
};
inSystemBoundary = true;
}
} else if (line === 'end' || (line === '}' && !inMetadataBlock)) {
if (inSystemBoundary && currentBoundary) {
this.addSystemBoundary(currentBoundary);
currentBoundary = null;
inSystemBoundary = false;
}
} else if (inSystemBoundary && currentBoundary && line) {
// This is a use case inside the system boundary
const useCase: UseCase = {
type: 'useCase',
name: line
};
currentBoundary.useCases.push(useCase);
} else if (line && !inSystemBoundary) {
// Handle multi-line metadata blocks
if (inMetadataBlock) {
if (line.includes('}')) {
// End of metadata block
currentMetadataContent += line.replace('}', '').trim();
const metadata = this.parseMetadataContent(currentMetadataName, currentMetadataContent);
if (metadata) {
this.addSystemBoundaryMetadata(metadata);
}
inMetadataBlock = false;
currentMetadataName = '';
currentMetadataContent = '';
} else {
// Continue collecting metadata content
currentMetadataContent += line.trim() + ' ';
}
} else if (line.includes('@{')) {
// Start of metadata block
const match = line.match(/^(\w+)@\{(.*)$/);
if (match) {
currentMetadataName = match[1];
const content = match[2].trim();
if (content.includes('}')) {
// Single line metadata
const metadata = this.parseMetadataContent(currentMetadataName, content.replace('}', ''));
if (metadata) {
this.addSystemBoundaryMetadata(metadata);
}
} else {
// Multi-line metadata
inMetadataBlock = true;
currentMetadataContent = content + ' ';
}
}
} else if (this.isRelationshipLine(line)) {
// Check if this is a relationship (actor --> usecase or actor --> node)
const relationship = this.parseRelationshipLine(line);
if (relationship) {
if (relationship.type === 'actorUseCaseRelationship') {
this.addRelationship(relationship);
} else if (relationship.type === 'actorNodeRelationship') {
this.addNodeRelationship(relationship);
}
}
} else {
// This is a standalone use case
const useCase: UseCase = {
type: 'useCase',
name: line
};
this.addUseCase(useCase);
}
}
}
}
private parseActorList(actorPart: string): Actor[] {
// Smart split by comma that respects metadata braces
const actorNames = this.smartSplitActors(actorPart);
return actorNames.map(actorName => this.parseActorWithMetadata(actorName));
}
private smartSplitActors(input: string): string[] {
const actors: string[] = [];
let current = '';
let braceDepth = 0;
let inQuotes = false;
let quoteChar = '';
for (const char of input) {
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
current += char;
} else if (inQuotes && char === quoteChar) {
inQuotes = false;
quoteChar = '';
current += char;
} else if (!inQuotes && char === '{') {
braceDepth++;
current += char;
} else if (!inQuotes && char === '}') {
braceDepth--;
current += char;
} else if (!inQuotes && char === ',' && braceDepth === 0) {
// This is a real separator, not inside metadata
if (current.trim()) {
actors.push(current.trim());
}
current = '';
} else {
current += char;
}
}
// Add the last actor
if (current.trim()) {
actors.push(current.trim());
}
return actors;
}
private parseActorWithMetadata(actorPart: string): Actor {
// Check if there's metadata (contains @{...})
const metadataRegex = /^([^@]+)@{([^}]*)}$/;
const metadataMatch = metadataRegex.exec(actorPart);
if (metadataMatch) {
const name = metadataMatch[1].trim();
const metadataStr = metadataMatch[2].trim();
const metadata = this.parseMetadataString(metadataStr);
return {
type: 'actor',
name,
metadata
};
} else {
// No metadata, just return the name
return {
type: 'actor',
name: actorPart
};
}
}
private parseMetadataString(metadataStr: string): Record<string, string> {
const metadata: Record<string, string> = {};
if (!metadataStr.trim()) {
return metadata;
}
// Split by comma and parse key-value pairs
const pairs = metadataStr.split(',');
for (const pair of pairs) {
const colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
const key = pair.substring(0, colonIndex).trim();
let value = pair.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
metadata[key] = value;
}
}
return metadata;
}
private isRelationshipLine(line: string): boolean {
return line.includes('-->') || line.includes('->');
}
private parseRelationshipLine(line: string): ActorUseCaseRelationship | ActorNodeRelationship | null {
let arrow = '';
let label: string | undefined;
let parts: string[] = [];
// Check for labeled arrows first (--label--> or --label->)
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
if (labeledArrowMatch) {
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
arrow = labeledArrowMatch[2];
// Extract label from arrow
const labelMatch = arrow.match(/^--(\w+)--?>$/);
if (labelMatch) {
label = labelMatch[1];
}
} else if (line.includes('-->')) {
arrow = '-->';
parts = line.split('-->').map(part => part.trim());
} else if (line.includes('->')) {
arrow = '->';
parts = line.split('->').map(part => part.trim());
}
if (parts.length === 2 && parts[0] && parts[1]) {
// Check if target is a node definition (contains parentheses)
if (this.isNodeDefinitionString(parts[1])) {
const node = this.parseNodeDefinitionString(parts[1]);
if (node) {
this.addNode(node);
return {
type: 'actorNodeRelationship',
from: parts[0],
to: node.id,
arrow,
label
};
}
} else {
return {
type: 'actorUseCaseRelationship',
from: parts[0],
to: parts[1],
arrow,
label
};
}
}
return null;
}
private isInlineActorNodeRelationshipLine(line: string): boolean {
// Check for pattern: ActorName --> nodeId(label) or ActorName --label--> nodeId(label)
const hasArrow = line.includes('-->') || line.includes('->') || !!line.match(/--\w+-->/);
const hasNodeDefinition = line.includes('(') && line.includes(')');
return hasArrow && hasNodeDefinition;
}
private parseInlineActorNodeRelationshipLine(line: string): InlineActorNodeRelationship | null {
let arrow = '';
let label: string | undefined;
let parts: string[] = [];
// Check for labeled arrows first (--label--> or --label->)
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
if (labeledArrowMatch) {
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
arrow = labeledArrowMatch[2];
// Extract label from arrow
const labelMatch = arrow.match(/^--(\w+)--?>$/);
if (labelMatch) {
label = labelMatch[1];
}
} else if (line.includes('-->')) {
arrow = '-->';
parts = line.split('-->').map(part => part.trim());
} else if (line.includes('->')) {
arrow = '->';
parts = line.split('->').map(part => part.trim());
}
if (parts.length === 2 && parts[0] && parts[1]) {
const node = this.parseNodeDefinitionString(parts[1]);
if (node) {
return {
type: 'inlineActorNodeRelationship',
actor: parts[0],
node,
arrow,
label
};
}
}
return null;
}
private isNodeDefinitionString(str: string): boolean {
return str.includes('(') && str.includes(')');
}
private parseNodeDefinitionString(str: string): Node | null {
const match = str.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/);
if (match) {
return {
type: 'node',
id: match[1],
label: match[2]
};
}
return null;
}
private isSystemBoundaryMetadataLine(line: string): boolean {
// Check for pattern: boundaryName@{...}
return line.includes('@{') && line.includes('}');
}
private parseSystemBoundaryMetadataLine(line: string): SystemBoundaryMetadata | null {
// Parse pattern: boundaryName@{key: value, key2: value2}
const match = line.match(/^(\w+)@\{(.+)\}$/);
if (!match) {
return null;
}
const name = match[1];
const metadataContent = match[2];
const metadata: Record<string, string> = {};
// Parse key-value pairs
const pairs = metadataContent.split(',').map(pair => pair.trim());
for (const pair of pairs) {
const colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
const key = pair.substring(0, colonIndex).trim();
let value = pair.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
metadata[key] = value;
}
}
return {
type: 'systemBoundaryMetadata',
name,
metadata
};
}
private parseMetadataContent(name: string, content: string): SystemBoundaryMetadata | null {
const metadata: Record<string, string> = {};
// Parse key-value pairs from content
const pairs = content.split(',').map(pair => pair.trim()).filter(pair => pair);
for (const pair of pairs) {
const colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
const key = pair.substring(0, colonIndex).trim();
let value = pair.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
metadata[key] = value;
}
}
return {
type: 'systemBoundaryMetadata',
name,
metadata
};
}
}

View File

@@ -0,0 +1,24 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'usecase';
const detector: DiagramDetector = (txt) => {
return /^\s*usecase/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./useCaseDiagram.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { UseCaseDB } from './useCaseDb.js';
import styles from './styles.js';
import renderer from './useCaseRenderer.js';
// Shared database instance
let db: UseCaseDB;
// Create a simple parser that integrates with our custom parser
const parser = {
parse: (text: string) => {
// Use the shared database instance
db.parse(text);
},
};
export const diagram: DiagramDefinition = {
parser,
get db() {
if (!db) {
db = new UseCaseDB();
}
return db;
},
renderer,
styles,
init: (cnf) => {
// Initialize configuration if needed
if (!db) {
db = new UseCaseDB();
}
},
};

View File

@@ -0,0 +1,619 @@
import { select } from 'd3';
import type { Diagram } from '../../Diagram.js';
import type { UseCaseDB } from './useCaseDb.js';
import { log } from '../../logger.js';
// Position interfaces
interface NodePosition {
name: string; // node ID (for relationship matching)
label: string; // node label (for display)
x: number;
y: number;
width: number;
height: number;
}
// Constants for actor rendering
const ACTOR_TYPE_WIDTH = 36; // 18 * 2 from sequence diagram
const ACTOR_MAN_FIGURE_CLASS = 'usecase-actor-man';
const ACTOR_SPACING = 120; // Horizontal spacing between actors
const ACTOR_HEIGHT = 80; // Height of actor figure
const MARGIN = 50; // Margin around the diagram
// Simple actor interface for positioning
interface ActorPosition {
name: string;
x: number;
y: number;
width: number;
height: number;
metadata?: Record<string, string>;
}
// System boundary interface for positioning
interface SystemBoundaryPosition {
name: string;
x: number;
y: number;
width: number;
height: number;
useCases: UseCasePosition[];
metadata?: Record<string, string>;
}
// Use case interface for positioning
interface UseCasePosition {
name: string;
x: number;
y: number;
width: number;
height: number;
}
/**
* Draws a stick figure actor similar to sequence diagrams but optimized for useCase
*/
const drawActorTypeActor = (elem: any, actor: ActorPosition, conf: any): number => {
const center = actor.x + actor.width / 2;
const actorY = actor.y;
// Create actor group
const actElem = elem.append('g');
actElem.attr('class', ACTOR_MAN_FIGURE_CLASS);
actElem.attr('name', actor.name);
// Draw stick figure
// Head (circle)
actElem
.append('circle')
.attr('cx', center)
.attr('cy', actorY + 15)
.attr('r', 10);
// Body (torso line)
actElem
.append('line')
.attr('x1', center)
.attr('y1', actorY + 25)
.attr('x2', center)
.attr('y2', actorY + 50)
.style('stroke', 'black');
// Arms (horizontal line)
actElem
.append('line')
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
.attr('y1', actorY + 35)
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
.style('stroke', 'black')
.attr('y2', actorY + 35);
// Left leg
actElem
.append('line')
.attr('x1', center)
.attr('y1', actorY + 50)
.attr('x2', center - ACTOR_TYPE_WIDTH / 2)
.style('stroke', 'black')
.attr('y2', actorY + 70);
// Right leg
actElem
.append('line')
.attr('x1', center)
.attr('y1', actorY + 50)
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
.attr('y2', actorY + 70)
.style('stroke', 'black');
// Actor name text
const textY = actorY + ACTOR_HEIGHT + 15;
drawActorText(actor.name, actElem, actor.x, textY, actor.width, 20);
return ACTOR_HEIGHT; // Total height including text and metadata
};
/**
* Draws text for actor name - simplified version of sequence diagram text drawing
*/
const drawActorText = (content: string, g: any, x: number, y: number, width: number, height: number): void => {
g.append('text')
.attr('x', x + width / 2)
.attr('y', y + height / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.text(content);
};
/**
* Draws a system boundary box with use cases inside
*/
const drawSystemBoundary = (g: any, boundary: SystemBoundaryPosition, conf: any): void => {
// Determine boundary type from metadata (default to 'rect')
const boundaryType = boundary.metadata?.type || 'rect';
if (boundaryType === 'package') {
// Draw package-style boundary with title box
const titleHeight = 25;
const titleWidth = Math.max(100, boundary.name.length * 8 + 20);
// Draw main boundary rectangle
g.append('rect')
.attr('x', boundary.x)
.attr('y', boundary.y + titleHeight)
.attr('width', boundary.width)
.attr('height', boundary.height - titleHeight)
.attr('class', 'usecase-system-boundary')
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('stroke-width', 2);
// Draw title box
g.append('rect')
.attr('x', boundary.x)
.attr('y', boundary.y)
.attr('width', titleWidth)
.attr('height', titleHeight)
.attr('class', 'usecase-system-boundary')
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('stroke-width', 2);
// Draw title text
g.append('text')
.attr('x', boundary.x + titleWidth / 2)
.attr('y', boundary.y + titleHeight / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.style('font-size', '14px')
.style('font-weight', 'bold')
.style('font-family', 'Arial, sans-serif')
.style('fill', '#333')
.text(boundary.name);
} else {
// Draw rect-style boundary (default)
g.append('rect')
.attr('x', boundary.x)
.attr('y', boundary.y)
.attr('width', boundary.width)
.attr('height', boundary.height)
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,5');
// Draw boundary title
g.append('text')
.attr('x', boundary.x + 10)
.attr('y', boundary.y + 20)
.style('font-size', '16px')
.style('font-weight', 'bold')
.style('font-family', 'Arial, sans-serif')
.style('fill', '#333')
.text(boundary.name);
}
// Draw use cases inside the boundary
boundary.useCases.forEach((useCase) => {
// Draw use case oval
g.append('ellipse')
.attr('cx', useCase.x + useCase.width / 2)
.attr('cy', useCase.y + useCase.height / 2)
.attr('rx', useCase.width / 2)
.attr('ry', useCase.height / 2)
.attr('class', 'usecase-usecase')
.attr('fill', 'none')
.attr('stroke', '#333');
// Draw use case text
g.append('text')
.attr('x', useCase.x + useCase.width / 2)
.attr('y', useCase.y + useCase.height / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.text(useCase.name);
});
};
/**
* Draws a standalone node as an oval
*/
const drawNode = (g: any, nodePos: NodePosition): void => {
const nodeGroup = g.append('g').attr('class', `node-${nodePos.name}`);
// Draw oval background
nodeGroup.append('ellipse')
.attr('cx', nodePos.x + nodePos.width / 2)
.attr('cy', nodePos.y + nodePos.height / 2)
.attr('rx', nodePos.width / 2)
.attr('ry', nodePos.height / 2)
.attr('fill', 'none')
.attr('stroke', '#333')
.attr('class', 'usecase-node');
// Add node label
nodeGroup.append('text')
.attr('x', nodePos.x + nodePos.width / 2)
.attr('y', nodePos.y + nodePos.height / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.text(nodePos.label);
};
/**
* Draws an arrow relationship between entities (actor-to-usecase or actor-to-actor)
*/
const drawRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], boundaryPositions: SystemBoundaryPosition[], conf: any): void => {
// Find the source entity (always an actor)
const fromEntity = actorPositions.find(a => a.name === relationship.from);
if (!fromEntity) {
return;
}
// Find the target entity (could be a use case or another actor)
let toEntity: UseCasePosition | ActorPosition | undefined;
let isTargetUseCase = false;
// First check if target is a use case in system boundaries
for (const boundary of boundaryPositions) {
toEntity = boundary.useCases.find(uc => uc.name === relationship.to);
if (toEntity) {
isTargetUseCase = true;
break;
}
}
// If not found in boundaries, check if target is another actor
toEntity ??= actorPositions.find(a => a.name === relationship.to);
if (!toEntity) {
return;
}
// Calculate connection points
const fromCenterX = fromEntity.x + fromEntity.width / 2;
const fromCenterY = fromEntity.y + fromEntity.height / 2;
// For use cases, connect to the edge (left side), for actors connect to center
const toCenterX = isTargetUseCase ? toEntity.x : toEntity.x + toEntity.width / 2;
const toCenterY = isTargetUseCase ? toEntity.y + toEntity.height / 2 : toEntity.y + toEntity.height / 2;
// Draw arrow line
g.append('line')
.attr('x1', fromCenterX)
.attr('y1', fromCenterY)
.attr('x2', toCenterX)
.attr('y2', toCenterY)
.attr('class', 'usecase-arrow')
.attr('stroke', '#333')
.attr('marker-end', 'url(#arrowhead)');
// Add edge label if present
if (relationship.label) {
const midX = (fromCenterX + toCenterX) / 2;
const midY = (fromCenterY + toCenterY) / 2;
g.append('text')
.attr('x', midX)
.attr('y', midY - 5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('class', 'usecase-arrow-label')
.attr('stroke', '#333')
.attr('font-weight', 200)
.text(relationship.label);
}
// Add arrowhead marker definition if not already added
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
if (defs.select('#arrowhead').empty()) {
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 3)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L0,6 L9,3 z')
.attr('fill', '#333');
}
};
/**
* Draws an arrow relationship between an actor and a standalone node
*/
const drawNodeRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
// Find the actor position
const actor = actorPositions.find(a => a.name === relationship.from);
if (!actor) {return};
// Find the node position
const node = nodePositions.find(n => n.name === relationship.to);
if (!node) {return};
// Calculate connection points
const actorCenterX = actor.x + actor.width / 2;
const actorCenterY = actor.y + actor.height / 2;
// For nodes (which are like use cases), connect to the edge (left side)
const nodeCenterX = node.x;
const nodeCenterY = node.y + node.height / 2;
// Draw arrow line
g.append('line')
.attr('x1', actorCenterX)
.attr('y1', actorCenterY)
.attr('x2', nodeCenterX)
.attr('y2', nodeCenterY)
.attr('stroke', '#333')
.attr('stroke-width', 2)
.attr('marker-end', 'url(#arrowhead)');
// Add edge label if present
if (relationship.label) {
const midX = (actorCenterX + nodeCenterX) / 2;
const midY = (actorCenterY + nodeCenterY) / 2;
g.append('text')
.attr('x', midX)
.attr('y', midY - 5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '12px')
.attr('font-family', 'Arial, sans-serif')
.attr('fill', '#333')
.text(relationship.label);
}
// Add arrowhead marker definition if not already added
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
if (defs.select('#arrowhead').empty()) {
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 3)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L0,6 L9,3 z')
.attr('fill', '#333');
}
};
/**
* Draws an arrow relationship from an inline actor-node definition
*/
const drawInlineRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
// Find the actor position
const actor = actorPositions.find(a => a.name === relationship.actor);
if (!actor) {return};
// Find the node position by node ID
const node = nodePositions.find(n => n.name === relationship.node.id);
if (!node) {return};
// Calculate connection points
const actorCenterX = actor.x + actor.width / 2;
const actorCenterY = actor.y + actor.height / 2;
// For nodes (which are like use cases), connect to the edge (left side)
const nodeCenterX = node.x;
const nodeCenterY = node.y + node.height / 2;
// Draw arrow line
g.append('line')
.attr('x1', actorCenterX)
.attr('y1', actorCenterY)
.attr('x2', nodeCenterX)
.attr('y2', nodeCenterY)
.attr('stroke', '#333')
.attr('stroke-width', 1)
.attr('marker-end', 'url(#arrowhead)');
// Add edge label if present
if (relationship.label) {
const midX = (actorCenterX + nodeCenterX) / 2;
const midY = (actorCenterY + nodeCenterY) / 2;
g.append('text')
.attr('x', midX)
.attr('y', midY - 5)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '12px')
.attr('font-family', 'Arial, sans-serif')
.attr('fill', '#333')
.text(relationship.label);
}
// Add arrowhead marker definition if not already added
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
if (defs.select('#arrowhead').empty()) {
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 3)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,0 L0,6 L9,3 z')
.attr('fill', '#333');
}
};
/**
* Main draw function for useCase diagrams
*/
const draw = (text: string, id: string, version: string, diagram: Diagram): void => {
const db = diagram.db as UseCaseDB;
log.debug('Drawing useCase diagram', id);
const actors = db.getActors();
const systemBoundaries = db.getSystemBoundaries();
const useCases = db.getUseCases();
const relationships = db.getRelationships();
const nodes = db.getNodes();
const nodeRelationships = db.getNodeRelationships();
const inlineRelationships = db.getInlineRelationships();
// Create SVG container - use the same approach as other diagrams
const svg = select(`[id="${id}"]`);
svg.selectAll('*').remove();
if (actors.length === 0 && systemBoundaries.length === 0 && useCases.length === 0 && relationships.length === 0 && nodes.length === 0 && nodeRelationships.length === 0 && inlineRelationships.length === 0) {
// Empty diagram
svg.attr('width', 200);
svg.attr('height', 100);
return;
}
// Calculate layout
let currentX = MARGIN;
let currentY = MARGIN;
let maxHeight = 0;
// Position actors
const actorPositions: ActorPosition[] = actors.map((actor, index) => ({
name: actor.name,
x: currentX + index * ACTOR_SPACING,
y: currentY,
width: ACTOR_TYPE_WIDTH + 20, // Extra width for text
height: ACTOR_HEIGHT,
metadata: actor.metadata
}));
if (actors.length > 0) {
currentX += actors.length * ACTOR_SPACING;
maxHeight = Math.max(maxHeight, ACTOR_HEIGHT + 50);
}
// Position system boundaries
const boundaryPositions: SystemBoundaryPosition[] = systemBoundaries.map((boundary, index) => {
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
const boundaryHeight = 150;
const position: SystemBoundaryPosition = {
name: boundary.name,
x: currentX + index * (boundaryWidth + 50),
y: currentY,
width: boundaryWidth,
height: boundaryHeight,
metadata: boundary.metadata,
useCases: boundary.useCases.map((useCase, ucIndex) => ({
name: useCase.name,
x: currentX + index * (boundaryWidth + 50) + 20 + ucIndex * 100,
y: currentY + 40,
width: 80,
height: 40
}))
};
return position;
});
if (systemBoundaries.length > 0) {
const totalBoundaryWidth = systemBoundaries.reduce((sum, boundary, index) => {
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
return sum + boundaryWidth + (index > 0 ? 50 : 0);
}, 0);
currentX += totalBoundaryWidth;
maxHeight = Math.max(maxHeight, 150);
}
// Position standalone nodes
const nodePositions: NodePosition[] = [];
if (nodes.length > 0) {
currentX += 50; // Add some spacing
nodes.forEach((node, index) => {
const nodeWidth = Math.max(100, node.label.length * 8);
const nodeHeight = 40;
nodePositions.push({
name: node.id,
label: node.label,
x: currentX,
y: MARGIN + 50,
width: nodeWidth,
height: nodeHeight
});
currentX += nodeWidth + 50;
});
maxHeight = Math.max(maxHeight, 90);
}
// Create main group
const g = svg.append('g').attr('class', 'usecase-diagram');
// Default configuration
const conf = {
actorFontSize: '14px',
actorFontFamily: 'Arial, sans-serif',
actorFontWeight: 'normal'
};
// Draw all actors
actorPositions.forEach((actorPos) => {
const height = drawActorTypeActor(g, actorPos, conf);
maxHeight = Math.max(maxHeight, height);
});
// Draw system boundaries
boundaryPositions.forEach((boundaryPos) => {
drawSystemBoundary(g, boundaryPos, conf);
});
// Draw standalone nodes
nodePositions.forEach((nodePos) => {
drawNode(g, nodePos);
});
// Draw relationships (arrows)
relationships.forEach((relationship) => {
drawRelationship(g, relationship, actorPositions, boundaryPositions, conf);
});
// Draw node relationships (arrows to standalone nodes)
nodeRelationships.forEach((relationship) => {
drawNodeRelationship(g, relationship, actorPositions, nodePositions, conf);
});
// Draw inline relationships (from inline actor-node definitions)
inlineRelationships.forEach((relationship) => {
drawInlineRelationship(g, relationship, actorPositions, nodePositions, conf);
});
// Calculate total dimensions
let totalWidth = MARGIN;
if (actors.length > 0) {
totalWidth = Math.max(totalWidth, actorPositions[actorPositions.length - 1].x + actorPositions[actorPositions.length - 1].width + MARGIN);
}
if (systemBoundaries.length > 0) {
totalWidth = Math.max(totalWidth, boundaryPositions[boundaryPositions.length - 1].x + boundaryPositions[boundaryPositions.length - 1].width + MARGIN);
}
if (nodePositions.length > 0) {
totalWidth = Math.max(totalWidth, nodePositions[nodePositions.length - 1].x + nodePositions[nodePositions.length - 1].width + MARGIN);
}
const totalHeight = MARGIN + maxHeight + MARGIN;
// Set SVG dimensions
svg.attr('width', totalWidth);
svg.attr('height', totalHeight);
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
};
export default {
draw,
};

View File

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

View File

@@ -1,6 +1,6 @@
# Frequently Asked Questions # Frequently Asked Questions
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712) 1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785) 1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621) 1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136) 1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)

View File

@@ -1,24 +0,0 @@
# Layouts
This page lists the available layout algorithms supported in Mermaid diagrams.
## Supported Layouts
- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/)
- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree)
- **cose-bilkent**: Cose Bilkent layout for force-directed graphs
- **dagre**: Dagre layout for layered graphs
## How to Use
You can specify the layout in your diagram's YAML config or initialization options. For example:
```mermaid
---
config:
layout: elk
---
graph TD;
A-->B;
B-->C;
```

View File

@@ -1,49 +0,0 @@
# Tidy-tree Layout
The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps.
## Features
- Organizes nodes in a tidy, non-overlapping tree
- Ideal for mindmaps and hierarchical data
- Automatically adjusts spacing for readability
## Example Usage
```mermaid-example
---
config:
layout: tidy-tree
---
mindmap
root((mindmap is a long thing))
A
B
C
D
```
```mermaid-example
---
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
```
## Note
- Currently, tidy-tree is primarily supported for mindmap diagrams.

View File

@@ -209,22 +209,3 @@ You can also refer the [implementation in the live editor](https://github.com/me
cspell:locale en,en-gb cspell:locale en,en-gb
cspell:ignore Buzan 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

@@ -126,7 +126,7 @@ xychart
## Chart Theme Variables ## Chart Theme Variables
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax: Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
```yaml ```yaml
--- ---
@@ -151,31 +151,6 @@ config:
| yAxisLineColor | Color of the y-axis line | | yAxisLineColor | Color of the y-axis line |
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" | | plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
### Setting Colors for Lines and Bars
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
```mermaid-example
---
config:
themeVariables:
xyChart:
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
---
xychart
title "Different Colors in xyChart"
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
y-axis "valuesY" 0 --> 50
%% Black line
line [10,20,30,40]
%% Blue bar
bar [20,30,25,35]
%% Green bar
bar [15,25,20,30]
%% Red line
line [5,15,25,35]
```
## Example on config and theme ## Example on config and theme
```mermaid-example ```mermaid-example

View File

@@ -26,7 +26,6 @@ const processFrontmatter = (code: string) => {
} }
config.gantt.displayMode = displayMode; config.gantt.displayMode = displayMode;
} }
return { title, config, text }; return { title, config, text };
}; };

View File

@@ -1,148 +0,0 @@
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

@@ -1,265 +0,0 @@
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,
};
});
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

@@ -1,207 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,236 +0,0 @@
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,
};
});
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

@@ -1,77 +0,0 @@
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

@@ -1,197 +0,0 @@
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

@@ -1,43 +0,0 @@
/**
* 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,14 +39,6 @@ const registerDefaultLayoutLoaders = () => {
name: 'dagre', name: 'dagre',
loader: async () => await import('./layout-algorithms/dagre/index.js'), 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,6 +438,7 @@ const fixCorners = function (lineData) {
} }
return newLineData; return newLineData;
}; };
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) { export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
const { handDrawnSeed } = getConfig(); const { handDrawnSeed } = getConfig();
let points = edge.points; let points = edge.points;
@@ -621,9 +622,9 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
// lineData.forEach((point) => { // lineData.forEach((point) => {
// elem // elem
// .append('circle') // .append('circle')
// .style('stroke', 'red') // .style('stroke', 'blue')
// .style('fill', 'red') // .style('fill', 'blue')
// .attr('r', 1) // .attr('r', 3)
// .attr('cx', point.x) // .attr('cx', point.x)
// .attr('cy', point.y); // .attr('cy', point.y);
// }); // });

View File

@@ -2,63 +2,64 @@
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect. * 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) { function intersectLine(p1, p2, q1, q2) {
{ // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, // p7 and p473.
// p7 and p473.
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + var a1, a2, b1, b2, c1, c2;
// b1 y + c1 = 0. var r1, r2, r3, r4;
const a1 = p2.y - p1.y; var denom, offset, num;
const b1 = p1.x - p2.x; var x, y;
const c1 = p2.x * p1.y - p1.x * p2.y;
// Compute r3 and r4. // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
const r3 = a1 * q1.x + b1 * q1.y + c1; // b1 y + c1 = 0.
const r4 = a1 * q2.x + b1 * q2.y + c1; a1 = p2.y - p1.y;
b1 = p1.x - p2.x;
c1 = p2.x * p1.y - p1.x * p2.y;
const epsilon = 1e-6; // Compute r3 and r4.
r3 = a1 * q1.x + b1 * q1.y + c1;
r4 = a1 * q2.x + b1 * q2.y + c1;
// Check signs of r3 and r4. If both point 3 and point 4 lie on // 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. // same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
return /*DON'T_INTERSECT*/; 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) { function sameSign(r1, r2) {

View File

@@ -61,10 +61,6 @@ import { erBox } from './shapes/erBox.js';
import { classBox } from './shapes/classBox.js'; import { classBox } from './shapes/classBox.js';
import { requirementBox } from './shapes/requirementBox.js'; import { requirementBox } from './shapes/requirementBox.js';
import { kanbanItem } from './shapes/kanbanItem.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';
import { mindmapCircle } from './shapes/mindmapCircle.js';
type ShapeHandler = <T extends SVGGraphicsElement>( type ShapeHandler = <T extends SVGGraphicsElement>(
parent: D3Selection<T>, parent: D3Selection<T>,
@@ -139,22 +135,6 @@ export const shapesDefs = [
aliases: ['circ'], aliases: ['circ'],
handler: circle, handler: circle,
}, },
{
semanticName: 'Bang',
name: 'Bang',
shortName: 'bang',
description: 'Bang',
aliases: ['bang'],
handler: bang,
},
{
semanticName: 'Cloud',
name: 'Cloud',
shortName: 'cloud',
description: 'cloud',
aliases: ['cloud'],
handler: cloud,
},
{ {
semanticName: 'Decision', semanticName: 'Decision',
name: 'Diamond', name: 'Diamond',
@@ -496,9 +476,6 @@ const generateShapeMap = () => {
// Kanban diagram // Kanban diagram
kanbanItem, kanbanItem,
//Mindmap diagram
mindmapCircle,
defaultMindmapNode,
// class diagram // class diagram
classBox, classBox,

View File

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

View File

@@ -1,80 +0,0 @@
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

@@ -1,64 +0,0 @@
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;
}

View File

@@ -6,7 +6,6 @@ import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
import rough from 'roughjs'; import rough from 'roughjs';
import type { D3Selection } from '../../../types.js'; import type { D3Selection } from '../../../types.js';
import { handleUndefinedAttr } from '../../../utils.js'; import { handleUndefinedAttr } from '../../../utils.js';
import type { Bounds, Point } from '../../../types.js';
export async function drawRect<T extends SVGGraphicsElement>( export async function drawRect<T extends SVGGraphicsElement>(
parent: D3Selection<T>, parent: D3Selection<T>,
@@ -63,10 +62,6 @@ export async function drawRect<T extends SVGGraphicsElement>(
updateNodeBounds(node, rect); updateNodeBounds(node, rect);
node.calcIntersect = function (bounds: Bounds, point: Point) {
return intersect.rect(bounds, point);
};
node.intersect = function (point) { node.intersect = function (point) {
return intersect.rect(node, point); return intersect.rect(node, point);
}; };

View File

@@ -1,13 +0,0 @@
import { circle } from './circle.js';
import type { Node, MindmapOptions } from '../../types.js';
import type { D3Selection } from '../../../types.js';
export async function mindmapCircle<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
) {
const options = {
padding: node.padding ?? 0,
} as MindmapOptions;
return circle(parent, node, options);
}

View File

@@ -1,3 +1,4 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js'; import intersect from '../intersect/index.js';
import type { Node } from '../../types.js'; import type { Node } from '../../types.js';
@@ -5,7 +6,6 @@ import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs'; import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js'; import { insertPolygonShape } from './insertPolygonShape.js';
import type { D3Selection } from '../../../types.js'; import type { D3Selection } from '../../../types.js';
import type { Bounds, Point } from '../../../types.js';
export const createDecisionBoxPathD = (x: number, y: number, size: number): string => { export const createDecisionBoxPathD = (x: number, y: number, size: number): string => {
return [ return [
@@ -61,26 +61,17 @@ export async function question<T extends SVGGraphicsElement>(parent: D3Selection
} }
updateNodeBounds(node, polygon); updateNodeBounds(node, polygon);
node.calcIntersect = function (bounds: Bounds, point: Point) {
const s = bounds.width;
// Define polygon points
const points = [
{ x: s / 2, y: 0 },
{ x: s, y: -s / 2 },
{ x: s / 2, y: -s },
{ x: 0, y: -s / 2 },
];
// Calculate the intersection point
const res = intersect.polygon(bounds, points, point);
return { x: res.x - 0.5, y: res.y - 0.5 }; // Adjusted result
};
node.intersect = function (point) { node.intersect = function (point) {
// @ts-ignore TODO fix this (KNSV) log.debug(
return this.calcIntersect(node as Bounds, point); 'APA12 Intersect called SPLIT\npoint:',
point,
'\nnode:\n',
node,
'\nres:',
intersect.polygon(node, points, point)
);
return intersect.polygon(node, points, point);
}; };
return shapeSvg; return shapeSvg;

View File

@@ -98,19 +98,18 @@ export async function roundedRect<T extends SVGGraphicsElement>(
const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2; const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2; const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
const radius = node.radius || 5; const radius = 5;
const taper = node.taper || 5; // Taper width for the rounded corners const taper = 5; // Taper width for the rounded corners
const { cssStyles } = node; const { cssStyles } = node;
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason // @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg); const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {}); const options = userNodeOverrides(node, {});
if (node.stroke) {
options.stroke = node.stroke;
}
if (node.look !== 'handDrawn') { if (node.look !== 'handDrawn') {
options.roughness = 0; options.roughness = 0;
options.fillStyle = 'solid'; options.fillStyle = 'solid';
} }
const points = [ const points = [
// Top edge (left to right) // Top edge (left to right)
{ x: -w / 2 + taper, y: -h / 2 }, // Top-left corner start (1) { x: -w / 2 + taper, y: -h / 2 }, // Top-left corner start (1)

View File

@@ -7,7 +7,7 @@ export async function squareRect<T extends SVGGraphicsElement>(parent: D3Selecti
rx: 0, rx: 0,
ry: 0, ry: 0,
classes: '', classes: '',
labelPaddingX: node.labelPaddingX ?? (node?.padding || 0) * 2, labelPaddingX: (node?.padding || 0) * 2,
labelPaddingY: (node?.padding || 0) * 1, labelPaddingY: (node?.padding || 0) * 1,
} as RectOptions; } as RectOptions;
return drawRect(parent, node, options); return drawRect(parent, node, options);

View File

@@ -2,7 +2,6 @@ export type MarkdownWordType = 'normal' | 'strong' | 'em';
import type { MermaidConfig } from '../config.type.js'; import type { MermaidConfig } from '../config.type.js';
import type { ClusterShapeID } from './rendering-elements/clusters.js'; import type { ClusterShapeID } from './rendering-elements/clusters.js';
import type { ShapeID } from './rendering-elements/shapes.js'; import type { ShapeID } from './rendering-elements/shapes.js';
import type { Bounds, Point } from '../types.js';
export interface MarkdownWord { export interface MarkdownWord {
content: string; content: string;
type: MarkdownWordType; type: MarkdownWordType;
@@ -39,12 +38,11 @@ interface BaseNode {
linkTarget?: string; linkTarget?: string;
tooltip?: string; tooltip?: string;
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
isGroup?: boolean; isGroup: boolean;
width?: number; width?: number;
height?: number; height?: number;
// Specific properties for State Diagram nodes TODO remove and use generic properties // Specific properties for State Diagram nodes TODO remove and use generic properties
intersect?: (point: any) => any; intersect?: (point: any) => any;
calcIntersect?: (bounds: Bounds, point: Point) => any;
// Non-generic properties // Non-generic properties
rx?: number; // Used for rounded corners in Rect, Ellipse, etc.Maybe it to specialized RectNode, EllipseNode, etc. rx?: number; // Used for rounded corners in Rect, Ellipse, etc.Maybe it to specialized RectNode, EllipseNode, etc.
@@ -60,8 +58,6 @@ interface BaseNode {
borderStyle?: string; borderStyle?: string;
borderWidth?: number; borderWidth?: number;
labelTextColor?: string; labelTextColor?: string;
labelPaddingX?: number;
labelPaddingY?: number;
// Flowchart specific properties // Flowchart specific properties
x?: number; x?: number;
@@ -76,25 +72,16 @@ interface BaseNode {
defaultWidth?: number; defaultWidth?: number;
imageAspectRatio?: number; imageAspectRatio?: number;
constraint?: 'on' | 'off'; constraint?: 'on' | 'off';
children?: NodeChildren;
nodeId?: string;
level?: number;
descr?: string;
type?: number;
radius?: number;
taper?: number;
stroke?: string;
} }
/** /**
* Group/cluster nodes, e.g. nodes that contain other nodes. * Group/cluster nodes, e.g. nodes that contain other nodes.
*/ */
export type NodeChildren = Node[];
export interface ClusterNode extends BaseNode { export interface ClusterNode extends BaseNode {
shape?: ClusterShapeID; shape?: ClusterShapeID;
isGroup: true; isGroup: true;
} }
export interface NonClusterNode extends BaseNode { export interface NonClusterNode extends BaseNode {
shape?: ShapeID; shape?: ShapeID;
isGroup: false; isGroup: false;
@@ -126,7 +113,7 @@ export interface Edge {
start?: string; start?: string;
stroke?: string; stroke?: string;
text?: string; text?: string;
type?: string; type: string;
// Class Diagram specific properties // Class Diagram specific properties
startLabelRight?: string; startLabelRight?: string;
endLabelLeft?: string; endLabelLeft?: string;
@@ -139,12 +126,6 @@ export interface Edge {
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted'; thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
look?: string; look?: string;
isUserDefinedId?: boolean; isUserDefinedId?: boolean;
points?: Point[];
parentId?: string;
dir?: string;
source?: string;
target?: string;
depth?: number;
} }
export interface RectOptions { export interface RectOptions {
@@ -155,10 +136,6 @@ export interface RectOptions {
classes: string; classes: string;
} }
export interface MindmapOptions {
padding: number;
}
// Extending the Node interface for specific types if needed // Extending the Node interface for specific types if needed
export type ClassDiagramNode = Node & { export type ClassDiagramNode = Node & {
memberData: any; // Specific property for class diagram nodes memberData: any; // Specific property for class diagram nodes
@@ -194,7 +171,6 @@ export interface ShapeRenderOptions {
config: MermaidConfig; config: MermaidConfig;
/** Some shapes render differently if a diagram has a direction `LR` */ /** Some shapes render differently if a diagram has a direction `LR` */
dir?: Node['dir']; dir?: Node['dir'];
padding?: number;
} }
export type KanbanNode = Node & { export type KanbanNode = Node & {

View File

@@ -977,7 +977,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
- useMaxWidth - useMaxWidth
- padding - padding
- maxNodeWidth - maxNodeWidth
- layoutAlgorithm
properties: properties:
padding: padding:
type: number type: number
@@ -985,10 +984,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
maxNodeWidth: maxNodeWidth:
type: number type: number
default: 200 default: 200
layoutAlgorithm:
description: Layout algorithm to use for positioning mindmap nodes
type: string
default: 'cose-bilkent'
KanbanDiagramConfig: KanbanDiagramConfig:
title: Kanban Diagram Config title: Kanban Diagram Config

View File

@@ -48,10 +48,6 @@ export interface Point {
x: number; x: number;
y: number; y: number;
} }
export interface Bounds extends Point {
width: number;
height: number;
}
export interface TextDimensionConfig { export interface TextDimensionConfig {
fontSize?: number; fontSize?: number;

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