mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-12 03:49:43 +02:00
Compare commits
2 Commits
develop
...
aggregatio
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d32f6afd35 | ||
![]() |
5a167835cc |
@@ -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',
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix: Render newlines as spaces in class diagrams
|
|
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add IDs in architecture diagrams
|
|
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix: Ensure edge label color is applied when using classDef with edge IDs
|
|
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': minor
|
|
||||||
'@mermaid-js/layout-tidy-tree': minor
|
|
||||||
'@mermaid-js/layout-elk': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
|
|
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
feat: Add IDs in architecture diagrams
|
|
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
chore: revert marked dependency from ^15.0.7 to ^16.0.0
|
|
||||||
|
|
||||||
- Reverted marked package version to ^16.0.0 for better compatibility
|
|
||||||
- This is a dependency update that maintains API compatibility
|
|
||||||
- All tests pass with the updated version
|
|
5
.changeset/tough-clocks-flow.md
Normal file
5
.changeset/tough-clocks-flow.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: Add support for aggregation relationships in ER diagram
|
@@ -5,10 +5,8 @@ bmatrix
|
|||||||
braintree
|
braintree
|
||||||
catmull
|
catmull
|
||||||
compositTitleSize
|
compositTitleSize
|
||||||
cose
|
|
||||||
curv
|
curv
|
||||||
doublecircle
|
doublecircle
|
||||||
elem
|
|
||||||
elems
|
elems
|
||||||
gantt
|
gantt
|
||||||
gitgraph
|
gitgraph
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
BRANDES
|
BRANDES
|
||||||
Buzan
|
|
||||||
circo
|
circo
|
||||||
handDrawn
|
handDrawn
|
||||||
KOEPF
|
KOEPF
|
||||||
|
2
.github/workflows/validate-lockfile.yml
vendored
2
.github/workflows/validate-lockfile.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -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
|
||||||
|
@@ -98,12 +98,12 @@ describe('Configuration', () => {
|
|||||||
it('should handle arrowMarkerAbsolute set to true', () => {
|
it('should handle arrowMarkerAbsolute set to true', () => {
|
||||||
renderGraph(
|
renderGraph(
|
||||||
`flowchart TD
|
`flowchart TD
|
||||||
A[Christmas] -->|Get money| B(Go shopping)
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
B --> C{Let me think}
|
B --> C{Let me think}
|
||||||
C -->|One| D[Laptop]
|
C -->|One| D[Laptop]
|
||||||
C -->|Two| E[iPhone]
|
C -->|Two| E[iPhone]
|
||||||
C -->|Three| F[fa:fa-car Car]
|
C -->|Three| F[fa:fa-car Car]
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
arrowMarkerAbsolute: true,
|
arrowMarkerAbsolute: true,
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,8 @@ describe('Configuration', () => {
|
|||||||
cy.get('path')
|
cy.get('path')
|
||||||
.first()
|
.first()
|
||||||
.should('have.attr', 'marker-end')
|
.should('have.attr', 'marker-end')
|
||||||
.and('include', 'url(http://localhost');
|
.should('exist')
|
||||||
|
.and('include', 'url(http\\:\\/\\/localhost');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should not taint the initial configuration when using multiple directives', () => {
|
it('should not taint the initial configuration when using multiple directives', () => {
|
||||||
|
@@ -524,18 +524,5 @@ describe('Class diagram', () => {
|
|||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
it('should handle an empty class body with empty braces', () => {
|
|
||||||
imgSnapshotTest(
|
|
||||||
` classDiagram
|
|
||||||
class FooBase~T~ {}
|
|
||||||
class Bar {
|
|
||||||
+Zip
|
|
||||||
+Zap()
|
|
||||||
}
|
|
||||||
FooBase <|-- Ba
|
|
||||||
`,
|
|
||||||
{ flowchart: { defaultRenderer: 'elk' } }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -369,4 +369,94 @@ ORDER ||--|{ LINE-ITEM : contains
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Aggregation Relationships', () => {
|
||||||
|
it('should render basic aggregation relationships', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
erDiagram
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
PROJECT <>.. TASK : manages
|
||||||
|
TEAM <> MEMBER : consists_of
|
||||||
|
`,
|
||||||
|
{ logLevel: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render aggregation with entity attributes', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
erDiagram
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
DEPARTMENT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
string location
|
||||||
|
}
|
||||||
|
EMPLOYEE {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int department_id FK
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ logLevel: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render aggregation with quoted labels', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
erDiagram
|
||||||
|
UNIVERSITY <> COLLEGE : "has multiple"
|
||||||
|
COLLEGE <> DEPARTMENT : "contains"
|
||||||
|
DEPARTMENT <> FACULTY : "employs"
|
||||||
|
`,
|
||||||
|
{ logLevel: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render mixed relationship types', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
erDiagram
|
||||||
|
CUSTOMER ||--o{ ORDER : places
|
||||||
|
ORDER ||--|{ ORDER_ITEM : contains
|
||||||
|
PRODUCT <> ORDER_ITEM : "aggregated in"
|
||||||
|
WAREHOUSE <>.. PRODUCT : "stores"
|
||||||
|
`,
|
||||||
|
{ logLevel: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render aggregation with entity aliases', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
erDiagram
|
||||||
|
d[DEPARTMENT]
|
||||||
|
e[EMPLOYEE]
|
||||||
|
p[PROJECT]
|
||||||
|
t[TASK]
|
||||||
|
|
||||||
|
d <> e : contains
|
||||||
|
p <>.. t : manages
|
||||||
|
|
||||||
|
`,
|
||||||
|
{ logLevel: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render complex aggregation scenarios', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
erDiagram
|
||||||
|
COMPANY <> DEPARTMENT : owns
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
EMPLOYEE <> PROJECT : works_on
|
||||||
|
PROJECT <> TASK : consists_of
|
||||||
|
TASK <> SUBTASK : includes
|
||||||
|
`,
|
||||||
|
{ logLevel: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -109,7 +109,7 @@ describe('Flowchart ELK', () => {
|
|||||||
const style = svg.attr('style');
|
const style = svg.attr('style');
|
||||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||||
verifyNumber(maxWidthValue, 380, 15);
|
verifyNumber(maxWidthValue, 380);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
|
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
|
||||||
@@ -128,7 +128,7 @@ describe('Flowchart ELK', () => {
|
|||||||
const width = parseFloat(svg.attr('width'));
|
const width = parseFloat(svg.attr('width'));
|
||||||
// use within because the absolute value can be slightly different depending on the environment ±5%
|
// use within because the absolute value can be slightly different depending on the environment ±5%
|
||||||
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
||||||
verifyNumber(width, 380, 15);
|
verifyNumber(width, 380);
|
||||||
expect(svg).to.not.have.attr('style');
|
expect(svg).to.not.have.attr('style');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1186,17 +1186,4 @@ end
|
|||||||
imgSnapshotTest(graph, { htmlLabels: false });
|
imgSnapshotTest(graph, { htmlLabels: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('V2 - 17: should apply class def colour to edge label', () => {
|
|
||||||
imgSnapshotTest(
|
|
||||||
` graph LR
|
|
||||||
id1(Start) link@-- "Label" -->id2(Stop)
|
|
||||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
|
||||||
|
|
||||||
class id2 myClass
|
|
||||||
classDef myClass fill:#bbf,stroke:#f66,stroke-width:2px,color:white,stroke-dasharray: 5 5
|
|
||||||
class link myClass
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -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
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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' } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -32,8 +32,26 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Recursive:wght@300..1000&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.recursive-mermaid {
|
||||||
|
font-family: 'Recursive', sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings:
|
||||||
|
'slnt' 0,
|
||||||
|
'CASL' 0,
|
||||||
|
'CRSV' 0.5,
|
||||||
|
'MONO' 0;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* background: rgb(221, 208, 208); */
|
/* background: rgb(221, 208, 208); */
|
||||||
/* background: #333; */
|
/* background: #333; */
|
||||||
@@ -45,7 +63,9 @@
|
|||||||
h1 {
|
h1 {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
.mermaid {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
.mermaid2 {
|
.mermaid2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -83,6 +103,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.class2 {
|
||||||
|
fill: red;
|
||||||
|
fill-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* tspan {
|
/* tspan {
|
||||||
font-size: 6px !important;
|
font-size: 6px !important;
|
||||||
} */
|
} */
|
||||||
@@ -106,194 +131,6 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
flowchart-elk TB
|
|
||||||
c1-->a2
|
|
||||||
subgraph one
|
|
||||||
a1-->a2
|
|
||||||
end
|
|
||||||
subgraph two
|
|
||||||
b1-->b2
|
|
||||||
end
|
|
||||||
subgraph three
|
|
||||||
c1-->c2
|
|
||||||
end
|
|
||||||
one --> two
|
|
||||||
three --> two
|
|
||||||
two --> c2
|
|
||||||
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
flowchart TB
|
|
||||||
|
|
||||||
process_C
|
|
||||||
subgraph container_Alpha
|
|
||||||
subgraph process_B
|
|
||||||
pppB
|
|
||||||
end
|
|
||||||
subgraph process_A
|
|
||||||
pppA
|
|
||||||
end
|
|
||||||
process_B-->|via_AWSBatch|container_Beta
|
|
||||||
process_A-->|messages|container_Beta
|
|
||||||
end
|
|
||||||
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
flowchart TB
|
|
||||||
subgraph container_Beta
|
|
||||||
process_C
|
|
||||||
end
|
|
||||||
subgraph container_Alpha
|
|
||||||
subgraph process_B
|
|
||||||
pppB
|
|
||||||
end
|
|
||||||
subgraph process_A
|
|
||||||
pppA
|
|
||||||
end
|
|
||||||
process_B-->|via_AWSBatch|container_Beta
|
|
||||||
process_A-->|messages|container_Beta
|
|
||||||
end
|
|
||||||
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
flowchart TB
|
|
||||||
subgraph container_Beta
|
|
||||||
process_C
|
|
||||||
end
|
|
||||||
|
|
||||||
process_B-->|via_AWSBatch|container_Beta
|
|
||||||
|
|
||||||
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
classDiagram
|
|
||||||
note "I love this diagram!\nDo you love it?"
|
|
||||||
Class01 <|-- AveryLongClass : Cool
|
|
||||||
<<interface>> Class01
|
|
||||||
Class03 "1" *-- "*" Class04
|
|
||||||
Class05 "1" o-- "many" Class06
|
|
||||||
Class07 "1" .. "*" Class08
|
|
||||||
Class09 "1" --> "*" C2 : Where am i?
|
|
||||||
Class09 "*" --* "*" C3
|
|
||||||
Class09 "1" --|> "1" Class07
|
|
||||||
Class12 <|.. Class08
|
|
||||||
Class11 ..>Class12
|
|
||||||
Class07 : equals()
|
|
||||||
Class07 : Object[] elementData
|
|
||||||
Class01 : size()
|
|
||||||
Class01 : int chimp
|
|
||||||
Class01 : int gorilla
|
|
||||||
Class01 : -int privateChimp
|
|
||||||
Class01 : +int publicGorilla
|
|
||||||
Class01 : #int protectedMarmoset
|
|
||||||
Class08 <--> C2: Cool label
|
|
||||||
class Class10 {
|
|
||||||
<<service>>
|
|
||||||
int id
|
|
||||||
test()
|
|
||||||
}
|
|
||||||
note for Class10 "Cool class\nI said it's very cool class!"
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
requirementDiagram
|
|
||||||
requirement test_req {
|
|
||||||
id: 1
|
|
||||||
text: the test text.
|
|
||||||
risk: high
|
|
||||||
verifymethod: test
|
|
||||||
}
|
|
||||||
|
|
||||||
element test_entity {
|
|
||||||
type: simulation
|
|
||||||
}
|
|
||||||
|
|
||||||
test_entity - satisfies -> test_req
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
flowchart-elk TB
|
|
||||||
internet
|
|
||||||
nat
|
|
||||||
router
|
|
||||||
compute1
|
|
||||||
|
|
||||||
subgraph project
|
|
||||||
router
|
|
||||||
nat
|
|
||||||
subgraph subnet1
|
|
||||||
compute1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
%% router --> subnet1
|
|
||||||
subnet1 --> nat
|
|
||||||
%% nat --> internet
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
---
|
|
||||||
flowchart-elk TB
|
|
||||||
internet
|
|
||||||
nat
|
|
||||||
router
|
|
||||||
lb1
|
|
||||||
lb2
|
|
||||||
compute1
|
|
||||||
compute2
|
|
||||||
subgraph project
|
|
||||||
router
|
|
||||||
nat
|
|
||||||
subgraph subnet1
|
|
||||||
compute1
|
|
||||||
lb1
|
|
||||||
end
|
|
||||||
subgraph subnet2
|
|
||||||
compute2
|
|
||||||
lb2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
internet --> router
|
|
||||||
router --> subnet1 & subnet2
|
|
||||||
subnet1 & subnet2 --> nat --> internet
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -320,149 +157,84 @@ treemap
|
|||||||
"Leaf 2.2": 25
|
"Leaf 2.2": 25
|
||||||
"Leaf 2.3": 12
|
"Leaf 2.3": 12
|
||||||
|
|
||||||
</pre>
|
classDef class1 fill:red,color:blue,stroke:#FFD600;
|
||||||
<pre id="diagram5" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
flowchart:
|
|
||||||
curve: rounded
|
|
||||||
---
|
|
||||||
flowchart LR
|
|
||||||
I["fa:fa-code Text"] -- Mermaid js --> D["Use<br/>the<br/>editor!"]
|
|
||||||
I --> D & D
|
|
||||||
D@{ shape: question}
|
|
||||||
I@{ shape: question}
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" 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<br/>and features
|
|
||||||
On Automatic creation
|
|
||||||
Uses
|
|
||||||
Creative techniques
|
|
||||||
Strategic planning
|
|
||||||
Argument mapping
|
|
||||||
Tools
|
|
||||||
Pen and paper
|
|
||||||
Mermaid
|
|
||||||
|
|
||||||
</pre>
|
</pre
|
||||||
<pre id="diagram4" class="mermaid">
|
>
|
||||||
---
|
<pre id="diagram4" class="mermaid2">
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
flowchart:
|
|
||||||
curve: linear
|
|
||||||
---
|
|
||||||
flowchart LR
|
|
||||||
A[A] --> B[B]
|
|
||||||
A[A] --- B([C])
|
|
||||||
A@{ shape: diamond}
|
|
||||||
%%B@{ shape: diamond}
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
flowchart:
|
|
||||||
curve: linear
|
|
||||||
---
|
|
||||||
flowchart LR
|
|
||||||
A[A] -- Mermaid js --> B[B]
|
|
||||||
A[A] -- Mermaid js --- B[B]
|
|
||||||
A@{ shape: diamond}
|
|
||||||
B@{ shape: diamond}
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
flowchart:
|
|
||||||
curve: rounded
|
|
||||||
---
|
|
||||||
flowchart LR
|
|
||||||
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
|
||||||
I --> D & D
|
|
||||||
D@{ shape: question}
|
|
||||||
I@{ shape: question}
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
flowchart:
|
|
||||||
curve: rounded
|
|
||||||
elk:
|
|
||||||
nodePlacementStrategy: NETWORK_SIMPLEX
|
|
||||||
---
|
|
||||||
flowchart LR
|
|
||||||
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
|
||||||
D --> I & I
|
|
||||||
a["a"]
|
|
||||||
D@{ shape: trap-b}
|
|
||||||
I@{ shape: lean-l}
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid">
|
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
treemap:
|
||||||
|
valueFormat: '$0,0'
|
||||||
---
|
---
|
||||||
flowchart LR
|
treemap
|
||||||
%% subgraph s1["Untitled subgraph"]
|
"Budget"
|
||||||
C["Evaluate"]
|
"Operations"
|
||||||
%% end
|
"Salaries": 7000
|
||||||
|
"Equipment": 2000
|
||||||
|
"Supplies": 1000
|
||||||
|
"Marketing"
|
||||||
|
"Advertising": 4000
|
||||||
|
"Events": 1000
|
||||||
|
|
||||||
B --> C
|
</pre
|
||||||
</pre>
|
>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
treemap
|
||||||
config:
|
title Accessible Treemap Title
|
||||||
layout: elk
|
"Category A"
|
||||||
flowchart:
|
"Item A1": 10
|
||||||
//curve: linear
|
"Item A2": 20
|
||||||
---
|
"Category B"
|
||||||
flowchart LR
|
"Item B1": 15
|
||||||
%% A ==> B
|
"Item B2": 25
|
||||||
%% A2 --> B2
|
|
||||||
A{A} --> B((Bo boo)) & B & B & B
|
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
|
||||||
config:
|
|
||||||
layout: elk
|
|
||||||
theme: default
|
|
||||||
look: classic
|
|
||||||
---
|
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph s1["APA"]
|
AB["apa@apa@"] --> B(("`apa@apa`"))
|
||||||
D{"Use the editor"}
|
|
||||||
end
|
|
||||||
subgraph S2["S2"]
|
|
||||||
s1
|
|
||||||
I>"fa:fa-code Text"]
|
|
||||||
E["E"]
|
|
||||||
end
|
|
||||||
D -- Mermaid js --> I
|
|
||||||
D --> I & E
|
|
||||||
E --> I
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
flowchart
|
||||||
|
D(("for D"))
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
A e1@==> B
|
||||||
|
e1@{ animate: true}
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
A e1@--> B
|
||||||
|
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
|
||||||
|
class e1 animate
|
||||||
|
</pre>
|
||||||
|
<h2>infinite</h2>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
A e1@--> B
|
||||||
|
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||||
|
class e1 animate
|
||||||
|
</pre>
|
||||||
|
<h2>Mermaid - edge-animation-slow</h2>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
A e1@--> B
|
||||||
|
e1@{ animation: fast}
|
||||||
|
</pre>
|
||||||
|
<h2>Mermaid - edge-animation-fast</h2>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
A e1@--> B
|
||||||
|
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
|
||||||
|
class e1 edge-animation-fast
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
|
|
||||||
|
info </pre
|
||||||
|
>
|
||||||
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -487,7 +259,7 @@ config:
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -500,7 +272,7 @@ config:
|
|||||||
D-->I
|
D-->I
|
||||||
D-->I
|
D-->I
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -539,7 +311,7 @@ flowchart LR
|
|||||||
n8@{ shape: rect}
|
n8@{ shape: rect}
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -555,7 +327,7 @@ flowchart LR
|
|||||||
|
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -564,7 +336,7 @@ flowchart LR
|
|||||||
A{A} --> B & C
|
A{A} --> B & C
|
||||||
</pre
|
</pre
|
||||||
>
|
>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -576,7 +348,7 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
</pre
|
</pre
|
||||||
>
|
>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -594,7 +366,7 @@ flowchart LR
|
|||||||
|
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
kanban:
|
kanban:
|
||||||
@@ -613,81 +385,81 @@ kanban
|
|||||||
task3[💻 Develop login feature]@{ ticket: 103 }
|
task3[💻 Develop login feature]@{ ticket: 103 }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
|
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
|
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
|
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
|
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
|
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
|
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
|
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
|
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
|
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
|
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
kanban
|
kanban
|
||||||
id2[In progress]
|
id2[In progress]
|
||||||
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
|
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid2">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
kanban:
|
kanban:
|
||||||
@@ -751,22 +523,18 @@ kanban
|
|||||||
alert('It worked');
|
alert('It worked');
|
||||||
}
|
}
|
||||||
await mermaid.initialize({
|
await mermaid.initialize({
|
||||||
// theme: 'base',
|
// theme: 'forest',
|
||||||
// theme: 'default',
|
// theme: 'default',
|
||||||
// theme: 'forest',
|
// theme: 'forest',
|
||||||
// handDrawnSeed: 12,
|
// handDrawnSeed: 12,
|
||||||
// look: 'handDrawn',
|
// look: 'handDrawn',
|
||||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||||
// layout: 'dagre',
|
// layout: 'dagre',
|
||||||
layout: 'elk',
|
// layout: 'elk',
|
||||||
// layout: 'fixed',
|
// layout: 'fixed',
|
||||||
// htmlLabels: false,
|
// htmlLabels: false,
|
||||||
flowchart: { titleTopMargin: 10 },
|
flowchart: { titleTopMargin: 10 },
|
||||||
|
fontFamily: "'Recursive', sans-serif",
|
||||||
// fontFamily: 'Caveat',
|
|
||||||
// fontFamily: 'Kalam',
|
|
||||||
// fontFamily: 'courier',
|
|
||||||
fontFamily: 'arial',
|
|
||||||
sequence: {
|
sequence: {
|
||||||
actorFontFamily: 'courier',
|
actorFontFamily: 'courier',
|
||||||
noteFontFamily: 'courier',
|
noteFontFamily: 'courier',
|
||||||
|
@@ -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<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<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<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<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>
|
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
158
demos/er.html
158
demos/er.html
@@ -169,6 +169,164 @@
|
|||||||
</pre>
|
</pre>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<!-- Aggregation Examples -->
|
||||||
|
<h2>Aggregation Examples</h2>
|
||||||
|
|
||||||
|
<h3>Basic Aggregation (Solid Line)</h3>
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
DEPARTMENT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
string location
|
||||||
|
}
|
||||||
|
EMPLOYEE {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int department_id FK
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Dashed Aggregation</h3>
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
PROJECT <>.. TASK : manages
|
||||||
|
PROJECT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
string description
|
||||||
|
}
|
||||||
|
TASK {
|
||||||
|
int id PK
|
||||||
|
string title
|
||||||
|
int project_id FK
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Aggregation with Different Cardinalities</h3>
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
COMPANY <> DEPARTMENT : owns
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
EMPLOYEE <> PROJECT : works_on
|
||||||
|
PROJECT <> TASK : consists_of
|
||||||
|
|
||||||
|
COMPANY {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
DEPARTMENT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int company_id FK
|
||||||
|
}
|
||||||
|
EMPLOYEE {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int department_id FK
|
||||||
|
}
|
||||||
|
PROJECT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int employee_id FK
|
||||||
|
}
|
||||||
|
TASK {
|
||||||
|
int id PK
|
||||||
|
string title
|
||||||
|
int project_id FK
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Two-way Aggregation</h3>
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
TEAM <> MEMBER : consists_of
|
||||||
|
TEAM {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
MEMBER {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int team_id FK
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Complex Aggregation with Labels</h3>
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
UNIVERSITY <> COLLEGE : "has multiple"
|
||||||
|
COLLEGE <> DEPARTMENT : "contains"
|
||||||
|
DEPARTMENT <> FACULTY : "employs"
|
||||||
|
FACULTY <> STUDENT : "teaches"
|
||||||
|
|
||||||
|
UNIVERSITY {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
COLLEGE {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int university_id FK
|
||||||
|
}
|
||||||
|
DEPARTMENT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int college_id FK
|
||||||
|
}
|
||||||
|
FACULTY {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int department_id FK
|
||||||
|
}
|
||||||
|
STUDENT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int faculty_id FK
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Mixed Relationship Types</h3>
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
CUSTOMER ||--o{ ORDER : places
|
||||||
|
ORDER ||--|{ ORDER_ITEM : contains
|
||||||
|
PRODUCT <> ORDER_ITEM : "aggregated in"
|
||||||
|
WAREHOUSE <>.. PRODUCT : "stores"
|
||||||
|
|
||||||
|
CUSTOMER {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
ORDER {
|
||||||
|
int id PK
|
||||||
|
int customer_id FK
|
||||||
|
date order_date
|
||||||
|
}
|
||||||
|
ORDER_ITEM {
|
||||||
|
int id PK
|
||||||
|
int order_id FK
|
||||||
|
int product_id FK
|
||||||
|
int quantity
|
||||||
|
}
|
||||||
|
PRODUCT {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
int warehouse_id FK
|
||||||
|
}
|
||||||
|
WAREHOUSE {
|
||||||
|
int id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<hr />
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import mermaid from './mermaid.esm.mjs';
|
import mermaid from './mermaid.esm.mjs';
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
# 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)
|
||||||
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
||||||
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
||||||
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||||
|
@@ -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;
|
|
||||||
```
|
|
@@ -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
|
||||||
|
|
||||||
|
159
docs/config/setup/mermaid/classes/UnknownDiagramError.md
Normal file
159
docs/config/setup/mermaid/classes/UnknownDiagramError.md
Normal 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`
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: ExternalDiagramDefinition
|
# Interface: ExternalDiagramDefinition
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
|
Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L94)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/me
|
|||||||
|
|
||||||
> **detector**: `DiagramDetector`
|
> **detector**: `DiagramDetector`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L98)
|
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/me
|
|||||||
|
|
||||||
> **id**: `string`
|
> **id**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
|
Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L95)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/me
|
|||||||
|
|
||||||
> **loader**: `DiagramLoader`
|
> **loader**: `DiagramLoader`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L99)
|
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
|
||||||
|
@@ -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)
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: LayoutLoaderDefinition
|
# Interface: LayoutLoaderDefinition
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
|
Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L21)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.co
|
|||||||
|
|
||||||
> `optional` **algorithm**: `string`
|
> `optional` **algorithm**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L27)
|
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.co
|
|||||||
|
|
||||||
> **loader**: `LayoutLoader`
|
> **loader**: `LayoutLoader`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L26)
|
Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.co
|
|||||||
|
|
||||||
> **name**: `string`
|
> **name**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:25](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L25)
|
Defined in: [packages/mermaid/src/rendering-util/render.ts:22](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L22)
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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.
|
||||||
|
@@ -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.
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: RenderOptions
|
# Interface: RenderOptions
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L10)
|
Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,4 +18,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.co
|
|||||||
|
|
||||||
> `optional` **algorithm**: `string`
|
> `optional` **algorithm**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L11)
|
Defined in: [packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8)
|
||||||
|
@@ -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.
|
||||||
|
@@ -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`
|
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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: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:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)
|
||||||
|
@@ -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:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130)
|
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)
|
||||||
|
@@ -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.
|
|
@@ -209,6 +209,42 @@ erDiagram
|
|||||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Aggregation
|
||||||
|
|
||||||
|
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
|
||||||
|
|
||||||
|
| Value | Alias for | Description |
|
||||||
|
| :---: | :------------------: | ------------------------------ |
|
||||||
|
| <> | _aggregation_ | Basic aggregation (solid line) |
|
||||||
|
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
erDiagram
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
PROJECT <>.. TASK : manages
|
||||||
|
TEAM <> MEMBER : consists_of
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
PROJECT <>.. TASK : manages
|
||||||
|
TEAM <> MEMBER : consists_of
|
||||||
|
```
|
||||||
|
|
||||||
|
In these examples:
|
||||||
|
|
||||||
|
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
|
||||||
|
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
|
||||||
|
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
|
||||||
|
|
||||||
|
**Aggregation vs Association**
|
||||||
|
|
||||||
|
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
|
||||||
|
- **Association** (`||--`, `}o--`): General relationship between entities
|
||||||
|
|
||||||
### Attributes
|
### Attributes
|
||||||
|
|
||||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
||||||
|
@@ -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` |
|
||||||
|
@@ -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)
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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/',
|
||||||
|
@@ -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 -->
|
||||||
|
@@ -1,67 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
intersection,
|
|
||||||
ensureTrulyOutside,
|
|
||||||
makeInsidePoint,
|
|
||||||
tryNodeIntersect,
|
|
||||||
replaceEndpoint,
|
|
||||||
type RectLike,
|
|
||||||
type P,
|
|
||||||
} from '../geometry.js';
|
|
||||||
|
|
||||||
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
|
|
||||||
|
|
||||||
describe('geometry helpers', () => {
|
|
||||||
it('intersection: vertical approach hits bottom border', () => {
|
|
||||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
|
||||||
const h = rect.height / 2; // 25
|
|
||||||
const outside: P = { x: 0, y: 100 };
|
|
||||||
const inside: P = { x: 0, y: 0 };
|
|
||||||
const res = intersection(rect, outside, inside);
|
|
||||||
expect(approx(res.x, 0)).toBe(true);
|
|
||||||
expect(approx(res.y, h)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ensureTrulyOutside nudges near-boundary point outward', () => {
|
|
||||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
|
||||||
// near bottom boundary (y ~ h)
|
|
||||||
const near: P = { x: 0, y: rect.height / 2 - 0.2 };
|
|
||||||
const out = ensureTrulyOutside(rect, near, 10);
|
|
||||||
expect(out.y).toBeGreaterThan(rect.height / 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('makeInsidePoint keeps x for vertical and y from center', () => {
|
|
||||||
const rect: RectLike = { x: 10, y: 5, width: 100, height: 50 };
|
|
||||||
const outside: P = { x: 10, y: 40 };
|
|
||||||
const center: P = { x: 99, y: -123 }; // center y should be used
|
|
||||||
const inside = makeInsidePoint(rect, outside, center);
|
|
||||||
expect(inside.x).toBe(outside.x);
|
|
||||||
expect(inside.y).toBe(center.y);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tryNodeIntersect returns null for wrong-side intersections', () => {
|
|
||||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
|
||||||
const outside: P = { x: -50, y: 0 };
|
|
||||||
const node = { intersect: () => ({ x: 10, y: 0 }) } as any; // right side of center
|
|
||||||
const res = tryNodeIntersect(node, rect, outside);
|
|
||||||
expect(res).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('replaceEndpoint dedup removes end/start appropriately', () => {
|
|
||||||
const pts: P[] = [
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
{ x: 1, y: 1 },
|
|
||||||
];
|
|
||||||
// remove duplicate end
|
|
||||||
replaceEndpoint(pts, 'end', { x: 1, y: 1 });
|
|
||||||
expect(pts.length).toBe(1);
|
|
||||||
|
|
||||||
const pts2: P[] = [
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
{ x: 1, y: 1 },
|
|
||||||
];
|
|
||||||
// remove duplicate start
|
|
||||||
replaceEndpoint(pts2, 'start', { x: 0, y: 0 });
|
|
||||||
expect(pts2.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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;
|
|
@@ -1,209 +0,0 @@
|
|||||||
/* Geometry utilities extracted from render.ts for reuse and testing */
|
|
||||||
|
|
||||||
export interface P {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RectLike {
|
|
||||||
x: number; // center x
|
|
||||||
y: number; // center y
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
padding?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeLike {
|
|
||||||
intersect?: (p: P) => P | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EPS = 1;
|
|
||||||
export const PUSH_OUT = 10;
|
|
||||||
|
|
||||||
export const onBorder = (bounds: RectLike, p: P, tol = 0.5): boolean => {
|
|
||||||
const halfW = bounds.width / 2;
|
|
||||||
const halfH = bounds.height / 2;
|
|
||||||
const left = bounds.x - halfW;
|
|
||||||
const right = bounds.x + halfW;
|
|
||||||
const top = bounds.y - halfH;
|
|
||||||
const bottom = bounds.y + halfH;
|
|
||||||
|
|
||||||
const onLeft = Math.abs(p.x - left) <= tol && p.y >= top - tol && p.y <= bottom + tol;
|
|
||||||
const onRight = Math.abs(p.x - right) <= tol && p.y >= top - tol && p.y <= bottom + tol;
|
|
||||||
const onTop = Math.abs(p.y - top) <= tol && p.x >= left - tol && p.x <= right + tol;
|
|
||||||
const onBottom = Math.abs(p.y - bottom) <= tol && p.x >= left - tol && p.x <= right + tol;
|
|
||||||
return onLeft || onRight || onTop || onBottom;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute intersection between a rectangle (center x/y, width/height) and the line
|
|
||||||
* segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
|
|
||||||
*
|
|
||||||
* This version avoids snapping to outsidePoint when certain variables evaluate to 0
|
|
||||||
* (previously caused vertical top/bottom cases to miss the border). It only enforces
|
|
||||||
* axis-constant behavior for purely vertical/horizontal approaches.
|
|
||||||
*/
|
|
||||||
export const intersection = (node: RectLike, outsidePoint: P, insidePoint: P): P => {
|
|
||||||
const x = node.x;
|
|
||||||
const y = node.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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keep axis-constant special-cases only
|
|
||||||
if (R === 0) {
|
|
||||||
res.x = outsidePoint.x;
|
|
||||||
}
|
|
||||||
if (Q === 0) {
|
|
||||||
res.y = outsidePoint.y;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
} else {
|
|
||||||
// Intersection on sides of rect
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Only handle axis-constant cases
|
|
||||||
if (R === 0) {
|
|
||||||
_x = outsidePoint.x;
|
|
||||||
}
|
|
||||||
if (Q === 0) {
|
|
||||||
_y = outsidePoint.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { x: _x, y: _y };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const outsideNode = (node: RectLike, point: P): boolean => {
|
|
||||||
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;
|
|
||||||
return dx >= w || dy >= h;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ensureTrulyOutside = (bounds: RectLike, p: P, push = PUSH_OUT): P => {
|
|
||||||
const dx = Math.abs(p.x - bounds.x);
|
|
||||||
const dy = Math.abs(p.y - bounds.y);
|
|
||||||
const w = bounds.width / 2;
|
|
||||||
const h = bounds.height / 2;
|
|
||||||
if (Math.abs(dx - w) < EPS || Math.abs(dy - h) < EPS) {
|
|
||||||
const dirX = p.x - bounds.x;
|
|
||||||
const dirY = p.y - bounds.y;
|
|
||||||
const len = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
||||||
if (len > 0) {
|
|
||||||
return {
|
|
||||||
x: bounds.x + (dirX / len) * (len + push),
|
|
||||||
y: bounds.y + (dirY / len) * (len + push),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const makeInsidePoint = (bounds: RectLike, outside: P, center: P): P => {
|
|
||||||
const isVertical = Math.abs(outside.x - bounds.x) < EPS;
|
|
||||||
const isHorizontal = Math.abs(outside.y - bounds.y) < EPS;
|
|
||||||
return {
|
|
||||||
x: isVertical
|
|
||||||
? outside.x
|
|
||||||
: outside.x < bounds.x
|
|
||||||
? bounds.x - bounds.width / 4
|
|
||||||
: bounds.x + bounds.width / 4,
|
|
||||||
y: isHorizontal ? outside.y : center.y,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryNodeIntersect = (node: NodeLike, bounds: RectLike, outside: P): P | null => {
|
|
||||||
if (!node?.intersect) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const res = node.intersect(outside);
|
|
||||||
if (!res) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const wrongSide =
|
|
||||||
(outside.x < bounds.x && res.x > bounds.x) || (outside.x > bounds.x && res.x < bounds.x);
|
|
||||||
if (wrongSide) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const dist = Math.hypot(outside.x - res.x, outside.y - res.y);
|
|
||||||
if (dist <= EPS) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fallbackIntersection = (bounds: RectLike, outside: P, center: P): P => {
|
|
||||||
const inside = makeInsidePoint(bounds, outside, center);
|
|
||||||
return intersection(bounds, outside, inside);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const computeNodeIntersection = (
|
|
||||||
node: NodeLike,
|
|
||||||
bounds: RectLike,
|
|
||||||
outside: P,
|
|
||||||
center: P
|
|
||||||
): P => {
|
|
||||||
const outside2 = ensureTrulyOutside(bounds, outside);
|
|
||||||
return tryNodeIntersect(node, bounds, outside2) ?? fallbackIntersection(bounds, outside2, center);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const replaceEndpoint = (
|
|
||||||
points: P[],
|
|
||||||
which: 'start' | 'end',
|
|
||||||
value: P | null | undefined,
|
|
||||||
tol = 0.1
|
|
||||||
) => {
|
|
||||||
if (!value || points.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (which === 'start') {
|
|
||||||
if (
|
|
||||||
points.length > 0 &&
|
|
||||||
Math.abs(points[0].x - value.x) < tol &&
|
|
||||||
Math.abs(points[0].y - value.y) < tol
|
|
||||||
) {
|
|
||||||
// duplicate start remove it
|
|
||||||
points.shift();
|
|
||||||
} else {
|
|
||||||
points[0] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const last = points.length - 1;
|
|
||||||
if (
|
|
||||||
points.length > 0 &&
|
|
||||||
Math.abs(points[last].x - value.x) < tol &&
|
|
||||||
Math.abs(points[last].y - value.y) < tol
|
|
||||||
) {
|
|
||||||
// duplicate end remove it
|
|
||||||
points.pop();
|
|
||||||
} else {
|
|
||||||
points[last] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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]
|
|
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@mermaid-js/layout-tidy-tree",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Tidy-tree layout engine for mermaid",
|
|
||||||
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
|
|
||||||
"types": "dist/layouts.d.ts",
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./dist/mermaid-layout-tidy-tree.core.mjs",
|
|
||||||
"types": "./dist/layouts.d.ts"
|
|
||||||
},
|
|
||||||
"./": "./"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"diagram",
|
|
||||||
"markdown",
|
|
||||||
"tidy-tree",
|
|
||||||
"mermaid",
|
|
||||||
"layout"
|
|
||||||
],
|
|
||||||
"scripts": {},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/mermaid-js/mermaid"
|
|
||||||
},
|
|
||||||
"contributors": [
|
|
||||||
"Knut Sveidqvist",
|
|
||||||
"Sidharth Vinod"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"d3": "^7.9.0",
|
|
||||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/d3": "^7.4.3",
|
|
||||||
"mermaid": "workspace:^"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"mermaid": "^11.0.2"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
]
|
|
||||||
}
|
|
@@ -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';
|
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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;
|
|
||||||
}
|
|
@@ -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;
|
|
@@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
};
|
|
@@ -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;
|
|
||||||
}
|
|
@@ -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"]
|
|
||||||
}
|
|
@@ -229,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:
|
||||||
|
@@ -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",
|
||||||
|
@@ -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\` |
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
@@ -3,7 +3,6 @@ import type * as d3 from 'd3';
|
|||||||
import type { SetOptional, SetRequired } from 'type-fest';
|
import type { SetOptional, SetRequired } from 'type-fest';
|
||||||
import type { Diagram } from '../Diagram.js';
|
import type { Diagram } from '../Diagram.js';
|
||||||
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
||||||
import type { DiagramOrientation } from '../diagrams/git/gitGraphTypes.js';
|
|
||||||
|
|
||||||
export interface DiagramMetadata {
|
export interface DiagramMetadata {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -36,8 +35,7 @@ export interface DiagramDB {
|
|||||||
getAccTitle?: () => string;
|
getAccTitle?: () => string;
|
||||||
setAccDescription?: (description: string) => void;
|
setAccDescription?: (description: string) => void;
|
||||||
getAccDescription?: () => string;
|
getAccDescription?: () => string;
|
||||||
getDirection?: () => string | undefined;
|
|
||||||
setDirection?: (dir: DiagramOrientation) => void;
|
|
||||||
setDisplayMode?: (title: string) => void;
|
setDisplayMode?: (title: string) => void;
|
||||||
bindFunctions?: (element: Element) => void;
|
bindFunctions?: (element: Element) => void;
|
||||||
}
|
}
|
||||||
|
@@ -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', () => {
|
||||||
|
@@ -1,48 +0,0 @@
|
|||||||
import { describe } from 'vitest';
|
|
||||||
import { draw } from './architectureRenderer.js';
|
|
||||||
import { Diagram } from '../../Diagram.js';
|
|
||||||
import { addDetector } from '../../diagram-api/detectType.js';
|
|
||||||
import architectureDetector from './architectureDetector.js';
|
|
||||||
import { ensureNodeFromSelector, jsdomIt } from '../../tests/util.js';
|
|
||||||
|
|
||||||
const { id, detector, loader } = architectureDetector;
|
|
||||||
|
|
||||||
addDetector(id, detector, loader); // Add architecture schemas to Mermaid
|
|
||||||
|
|
||||||
describe('architecture diagram SVGs', () => {
|
|
||||||
jsdomIt('should add ids', async () => {
|
|
||||||
const svgNode = await drawDiagram(`
|
|
||||||
architecture-beta
|
|
||||||
group api(cloud)[API]
|
|
||||||
|
|
||||||
service db(database)[Database] in api
|
|
||||||
service disk1(disk)[Storage] in api
|
|
||||||
service disk2(disk)[Storage] in api
|
|
||||||
service server(server)[Server] in api
|
|
||||||
|
|
||||||
db:L -- R:server
|
|
||||||
disk1:T -- B:server
|
|
||||||
disk2:T -- B:db
|
|
||||||
`);
|
|
||||||
|
|
||||||
const nodesForGroup = svgNode.querySelectorAll(`#group-api`);
|
|
||||||
expect(nodesForGroup.length).toBe(1);
|
|
||||||
|
|
||||||
const serviceIds = [...svgNode.querySelectorAll(`[id^=service-]`)].map(({ id }) => id).sort();
|
|
||||||
expect(serviceIds).toStrictEqual([
|
|
||||||
'service-db',
|
|
||||||
'service-disk1',
|
|
||||||
'service-disk2',
|
|
||||||
'service-server',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const edgeIds = [...svgNode.querySelectorAll(`.edge[id^=L_]`)].map(({ id }) => id).sort();
|
|
||||||
expect(edgeIds).toStrictEqual(['L_db_server_0', 'L_disk1_server_0', 'L_disk2_db_0']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function drawDiagram(diagramText: string): Promise<Element> {
|
|
||||||
const diagram = await Diagram.fromText(diagramText, {});
|
|
||||||
await draw('NOT_USED', 'svg', '1.0.0', diagram);
|
|
||||||
return ensureNodeFromSelector('#svg');
|
|
||||||
}
|
|
@@ -20,7 +20,6 @@ import {
|
|||||||
type ArchitectureJunction,
|
type ArchitectureJunction,
|
||||||
type ArchitectureService,
|
type ArchitectureService,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import { getEdgeId } from '../../utils.js';
|
|
||||||
|
|
||||||
export const drawEdges = async function (
|
export const drawEdges = async function (
|
||||||
edgesEl: D3Element,
|
edgesEl: D3Element,
|
||||||
@@ -92,8 +91,7 @@ export const drawEdges = async function (
|
|||||||
|
|
||||||
g.insert('path')
|
g.insert('path')
|
||||||
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
|
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
|
||||||
.attr('class', 'edge')
|
.attr('class', 'edge');
|
||||||
.attr('id', getEdgeId(source, target, { prefix: 'L' }));
|
|
||||||
|
|
||||||
if (sourceArrow) {
|
if (sourceArrow) {
|
||||||
const xShift = isArchitectureDirectionX(sourceDir)
|
const xShift = isArchitectureDirectionX(sourceDir)
|
||||||
@@ -208,9 +206,8 @@ export const drawGroups = async function (
|
|||||||
if (data.type === 'group') {
|
if (data.type === 'group') {
|
||||||
const { h, w, x1, y1 } = node.boundingBox();
|
const { h, w, x1, y1 } = node.boundingBox();
|
||||||
|
|
||||||
const groupsNode = groupsEl.append('rect');
|
groupsEl
|
||||||
groupsNode
|
.append('rect')
|
||||||
.attr('id', `group-${data.id}`)
|
|
||||||
.attr('x', x1 + halfIconSize)
|
.attr('x', x1 + halfIconSize)
|
||||||
.attr('y', y1 + halfIconSize)
|
.attr('y', y1 + halfIconSize)
|
||||||
.attr('width', w)
|
.attr('width', w)
|
||||||
@@ -265,7 +262,6 @@ export const drawGroups = async function (
|
|||||||
')'
|
')'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
db.setElementForId(data.id, groupsNode);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -346,9 +342,9 @@ export const drawServices = async function (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceElem.attr('id', `service-${service.id}`).attr('class', 'architecture-service');
|
serviceElem.attr('class', 'architecture-service');
|
||||||
|
|
||||||
const { width, height } = serviceElem.node().getBBox();
|
const { width, height } = serviceElem._groups[0][0].getBBox();
|
||||||
service.width = width;
|
service.width = width;
|
||||||
service.height = height;
|
service.height = height;
|
||||||
db.setElementForId(service.id, serviceElem);
|
db.setElementForId(service.id, serviceElem);
|
||||||
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -2,6 +2,7 @@ import { log } from '../../logger.js';
|
|||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
import type { Edge, Node } from '../../rendering-util/types.js';
|
import type { Edge, Node } from '../../rendering-util/types.js';
|
||||||
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
|
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
|
||||||
|
import { AggregationType } from './erTypes.js';
|
||||||
import {
|
import {
|
||||||
setAccTitle,
|
setAccTitle,
|
||||||
getAccTitle,
|
getAccTitle,
|
||||||
@@ -33,6 +34,11 @@ export class ErDB implements DiagramDB {
|
|||||||
IDENTIFYING: 'IDENTIFYING',
|
IDENTIFYING: 'IDENTIFYING',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private Aggregation = {
|
||||||
|
AGGREGATION: AggregationType.AGGREGATION,
|
||||||
|
AGGREGATION_DASHED: AggregationType.AGGREGATION_DASHED,
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.clear();
|
this.clear();
|
||||||
this.addEntity = this.addEntity.bind(this);
|
this.addEntity = this.addEntity.bind(this);
|
||||||
@@ -131,6 +137,31 @@ export class ErDB implements DiagramDB {
|
|||||||
return this.relationships;
|
return this.relationships;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate aggregation relationship
|
||||||
|
* @param rSpec - The relationship specification to validate
|
||||||
|
* @returns boolean indicating if the aggregation relationship is valid
|
||||||
|
*/
|
||||||
|
public validateAggregationRelationship(rSpec: RelSpec): boolean {
|
||||||
|
const isAggregation =
|
||||||
|
rSpec.relType === this.Aggregation.AGGREGATION ||
|
||||||
|
rSpec.relType === this.Aggregation.AGGREGATION_DASHED;
|
||||||
|
|
||||||
|
if (!isAggregation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validCardinalities = [
|
||||||
|
this.Cardinality.ZERO_OR_ONE,
|
||||||
|
this.Cardinality.ZERO_OR_MORE,
|
||||||
|
this.Cardinality.ONE_OR_MORE,
|
||||||
|
this.Cardinality.ONLY_ONE,
|
||||||
|
this.Cardinality.MD_PARENT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return validCardinalities.includes(rSpec.cardA) && validCardinalities.includes(rSpec.cardB);
|
||||||
|
}
|
||||||
|
|
||||||
public getDirection() {
|
public getDirection() {
|
||||||
return this.direction;
|
return this.direction;
|
||||||
}
|
}
|
||||||
@@ -248,4 +279,17 @@ export class ErDB implements DiagramDB {
|
|||||||
public setDiagramTitle = setDiagramTitle;
|
public setDiagramTitle = setDiagramTitle;
|
||||||
public getDiagramTitle = getDiagramTitle;
|
public getDiagramTitle = getDiagramTitle;
|
||||||
public getConfig = () => getConfig().er;
|
public getConfig = () => getConfig().er;
|
||||||
|
|
||||||
|
// Getter methods for aggregation constants
|
||||||
|
public get AggregationConstants() {
|
||||||
|
return this.Aggregation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get CardinalityConstants() {
|
||||||
|
return this.Cardinality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get IdentificationConstants() {
|
||||||
|
return this.Identification;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,10 @@ const ERMarkers = {
|
|||||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
|
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
|
||||||
MD_PARENT_END: 'MD_PARENT_END',
|
MD_PARENT_END: 'MD_PARENT_END',
|
||||||
MD_PARENT_START: 'MD_PARENT_START',
|
MD_PARENT_START: 'MD_PARENT_START',
|
||||||
|
AGGREGATION_START: 'AGGREGATION_START',
|
||||||
|
AGGREGATION_END: 'AGGREGATION_END',
|
||||||
|
AGGREGATION_DASHED_START: 'AGGREGATION_DASHED_START',
|
||||||
|
AGGREGATION_DASHED_END: 'AGGREGATION_DASHED_END',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,6 +184,66 @@ const insertMarkers = function (elem, conf) {
|
|||||||
.attr('fill', 'none')
|
.attr('fill', 'none')
|
||||||
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||||
|
|
||||||
|
// Aggregation markers (hollow diamond)
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', ERMarkers.AGGREGATION_START)
|
||||||
|
.attr('refX', 0)
|
||||||
|
.attr('refY', 9)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 18)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('stroke', conf.stroke)
|
||||||
|
.attr('fill', 'white')
|
||||||
|
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', ERMarkers.AGGREGATION_END)
|
||||||
|
.attr('refX', 20)
|
||||||
|
.attr('refY', 9)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 18)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('stroke', conf.stroke)
|
||||||
|
.attr('fill', 'white')
|
||||||
|
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
|
||||||
|
|
||||||
|
// Dashed aggregation markers
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', ERMarkers.AGGREGATION_DASHED_START)
|
||||||
|
.attr('refX', 0)
|
||||||
|
.attr('refY', 9)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 18)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('stroke', conf.stroke)
|
||||||
|
.attr('fill', 'white')
|
||||||
|
.attr('stroke-dasharray', '3,3')
|
||||||
|
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', ERMarkers.AGGREGATION_DASHED_END)
|
||||||
|
.attr('refX', 20)
|
||||||
|
.attr('refY', 9)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 18)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('stroke', conf.stroke)
|
||||||
|
.attr('fill', 'white')
|
||||||
|
.attr('stroke-dasharray', '3,3')
|
||||||
|
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -448,6 +448,11 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
|||||||
svgPath.attr('stroke-dasharray', '8,8');
|
svgPath.attr('stroke-dasharray', '8,8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle aggregation relationship styling
|
||||||
|
if (rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED) {
|
||||||
|
svgPath.attr('stroke-dasharray', '8,8');
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Understand this better
|
// TODO: Understand this better
|
||||||
let url = '';
|
let url = '';
|
||||||
if (conf.arrowMarkerAbsolute) {
|
if (conf.arrowMarkerAbsolute) {
|
||||||
@@ -503,6 +508,15 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle aggregation markers
|
||||||
|
if (
|
||||||
|
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION ||
|
||||||
|
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED
|
||||||
|
) {
|
||||||
|
// Add aggregation marker at the start (entity B side)
|
||||||
|
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.AGGREGATION_START + ')');
|
||||||
|
}
|
||||||
|
|
||||||
// Now label the relationship
|
// Now label the relationship
|
||||||
|
|
||||||
// Find the half-way point
|
// Find the half-way point
|
||||||
|
@@ -35,3 +35,15 @@ export interface EntityClass {
|
|||||||
styles: string[];
|
styles: string[];
|
||||||
textStyles: string[];
|
textStyles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aggregation relationship types
|
||||||
|
export const AggregationType = {
|
||||||
|
AGGREGATION: 'AGGREGATION',
|
||||||
|
AGGREGATION_DASHED: 'AGGREGATION_DASHED',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Line types for aggregation
|
||||||
|
export const AggregationLineType = {
|
||||||
|
SOLID: 'SOLID',
|
||||||
|
DASHED: 'DASHED',
|
||||||
|
} as const;
|
||||||
|
@@ -72,6 +72,8 @@ o\| return 'ZERO_OR_ONE';
|
|||||||
o\{ return 'ZERO_OR_MORE';
|
o\{ return 'ZERO_OR_MORE';
|
||||||
\|\{ return 'ONE_OR_MORE';
|
\|\{ return 'ONE_OR_MORE';
|
||||||
\s*u return 'MD_PARENT';
|
\s*u return 'MD_PARENT';
|
||||||
|
"<>.." return 'AGGREGATION_DASHED';
|
||||||
|
"<>" return 'AGGREGATION';
|
||||||
\.\. return 'NON_IDENTIFYING';
|
\.\. return 'NON_IDENTIFYING';
|
||||||
\-\- return 'IDENTIFYING';
|
\-\- return 'IDENTIFYING';
|
||||||
"to" return 'IDENTIFYING';
|
"to" return 'IDENTIFYING';
|
||||||
@@ -167,6 +169,47 @@ statement
|
|||||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||||
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
|
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
|
||||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||||
|
| entityName aggregationRelSpec entityName COLON role
|
||||||
|
{
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addEntity($3);
|
||||||
|
yy.addRelationship($1, $5, $3, $2);
|
||||||
|
}
|
||||||
|
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
|
||||||
|
{
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addEntity($5);
|
||||||
|
yy.addRelationship($1, $9, $5, $4);
|
||||||
|
yy.setClass([$1], $3);
|
||||||
|
yy.setClass([$5], $7);
|
||||||
|
}
|
||||||
|
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName COLON role
|
||||||
|
{
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addEntity($5);
|
||||||
|
yy.addRelationship($1, $7, $5, $4);
|
||||||
|
yy.setClass([$1], $3);
|
||||||
|
}
|
||||||
|
| entityName aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
|
||||||
|
{
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addEntity($3);
|
||||||
|
yy.addRelationship($1, $7, $3, $2);
|
||||||
|
yy.setClass([$3], $5);
|
||||||
|
}
|
||||||
|
| entityName 'AGGREGATION' entityName COLON role
|
||||||
|
{
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addEntity($3);
|
||||||
|
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION', cardB: 'ZERO_OR_MORE' });
|
||||||
|
}
|
||||||
|
| entityName 'AGGREGATION_DASHED' entityName COLON role
|
||||||
|
{
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addEntity($3);
|
||||||
|
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION_DASHED', cardB: 'ZERO_OR_MORE' });
|
||||||
|
}
|
||||||
|
|
||||||
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
|
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||||
@@ -272,6 +315,17 @@ relSpec
|
|||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
||||||
|
aggregationRelSpec
|
||||||
|
: 'AGGREGATION' cardinality cardinality
|
||||||
|
{
|
||||||
|
$$ = { cardA: $2, relType: $1, cardB: $3 };
|
||||||
|
}
|
||||||
|
| 'AGGREGATION_DASHED' cardinality cardinality
|
||||||
|
{
|
||||||
|
$$ = { cardA: $2, relType: $1, cardB: $3 };
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
cardinality
|
cardinality
|
||||||
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
|
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
|
||||||
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
||||||
@@ -283,6 +337,8 @@ cardinality
|
|||||||
relType
|
relType
|
||||||
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
|
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
|
||||||
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
|
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
|
||||||
|
| 'AGGREGATION' { $$ = yy.Aggregation.AGGREGATION; }
|
||||||
|
| 'AGGREGATION_DASHED' { $$ = yy.Aggregation.AGGREGATION_DASHED; }
|
||||||
;
|
;
|
||||||
|
|
||||||
role
|
role
|
||||||
|
@@ -1001,4 +1001,75 @@ describe('when parsing ER diagram it...', function () {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('aggregation relationships', function () {
|
||||||
|
it('should parse basic aggregation syntax', function () {
|
||||||
|
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.getEntities().size).toBe(2);
|
||||||
|
expect(rels.length).toBe(1);
|
||||||
|
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||||
|
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||||
|
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||||
|
expect(rels[0].roleA).toBe('contains');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse dashed aggregation syntax', function () {
|
||||||
|
erDiagram.parser.parse('erDiagram\nPROJECT <>.. TASK : manages');
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.getEntities().size).toBe(2);
|
||||||
|
expect(rels.length).toBe(1);
|
||||||
|
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||||
|
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||||
|
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||||
|
expect(rels[0].roleA).toBe('manages');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse aggregation with quoted labels', function () {
|
||||||
|
erDiagram.parser.parse('erDiagram\nUNIVERSITY <> COLLEGE : "has multiple"');
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.getEntities().size).toBe(2);
|
||||||
|
expect(rels.length).toBe(1);
|
||||||
|
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||||
|
expect(rels[0].roleA).toBe('has multiple');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse multiple aggregation relationships', function () {
|
||||||
|
erDiagram.parser.parse(
|
||||||
|
'erDiagram\nDEPARTMENT <> EMPLOYEE : contains\nPROJECT <>.. TASK : manages'
|
||||||
|
);
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.getEntities().size).toBe(4);
|
||||||
|
expect(rels.length).toBe(2);
|
||||||
|
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||||
|
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse aggregation with entity aliases', function () {
|
||||||
|
erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd <> e : contains');
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.getEntities().size).toBe(2);
|
||||||
|
expect(rels.length).toBe(1);
|
||||||
|
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||||
|
expect(erDb.getEntity('d').alias).toBe('DEPARTMENT');
|
||||||
|
expect(erDb.getEntity('e').alias).toBe('EMPLOYEE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate aggregation relationships', function () {
|
||||||
|
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed relationship types', function () {
|
||||||
|
erDiagram.parser.parse(
|
||||||
|
'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT <> ORDER_ITEM : "aggregated in"'
|
||||||
|
);
|
||||||
|
const rels = erDb.getRelationships();
|
||||||
|
expect(erDb.getEntities().size).toBe(4);
|
||||||
|
expect(rels.length).toBe(2);
|
||||||
|
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||||
|
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -68,6 +68,32 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
|||||||
stroke: ${options.lineColor} !important;
|
stroke: ${options.lineColor} !important;
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aggregation {
|
||||||
|
stroke: ${options.lineColor};
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aggregation-dashed {
|
||||||
|
stroke: ${options.lineColor};
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-dasharray: 8,8;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aggregation-marker {
|
||||||
|
fill: white !important;
|
||||||
|
stroke: ${options.lineColor} !important;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aggregation-marker-dashed {
|
||||||
|
fill: white !important;
|
||||||
|
stroke: ${options.lineColor} !important;
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-dasharray: 3,3;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default getStyles;
|
export default getStyles;
|
||||||
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 () => {
|
||||||
|
@@ -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' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@@ -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)
|
||||||
|
@@ -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;
|
|
||||||
```
|
|
@@ -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.
|
|
@@ -151,6 +151,35 @@ erDiagram
|
|||||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Aggregation
|
||||||
|
|
||||||
|
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
|
||||||
|
|
||||||
|
| Value | Alias for | Description |
|
||||||
|
| :---: | :------------------: | ------------------------------ |
|
||||||
|
| <> | _aggregation_ | Basic aggregation (solid line) |
|
||||||
|
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
erDiagram
|
||||||
|
DEPARTMENT <> EMPLOYEE : contains
|
||||||
|
PROJECT <>.. TASK : manages
|
||||||
|
TEAM <> MEMBER : consists_of
|
||||||
|
```
|
||||||
|
|
||||||
|
In these examples:
|
||||||
|
|
||||||
|
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
|
||||||
|
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
|
||||||
|
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
|
||||||
|
|
||||||
|
**Aggregation vs Association**
|
||||||
|
|
||||||
|
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
|
||||||
|
- **Association** (`||--`, `}o--`): General relationship between entities
|
||||||
|
|
||||||
### Attributes
|
### Attributes
|
||||||
|
|
||||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
||||||
|
@@ -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)
|
|
||||||
|
@@ -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
|
||||||
|
@@ -26,7 +26,6 @@ const processFrontmatter = (code: string) => {
|
|||||||
}
|
}
|
||||||
config.gantt.displayMode = displayMode;
|
config.gantt.displayMode = displayMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { title, config, text };
|
return { title, config, text };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
@@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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;
|
|
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user