mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-04 19:44:11 +01:00
Compare commits
134 Commits
demo/useca
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab108a3c1 | ||
|
|
8b1a12e81a | ||
|
|
3840451fda | ||
|
|
cfe9238882 | ||
|
|
c1f2d052be | ||
|
|
bce40e180a | ||
|
|
f47dec3680 | ||
|
|
88dc4beade | ||
|
|
e9232088c0 | ||
|
|
e96614ab86 | ||
|
|
73115cb416 | ||
|
|
480438bd52 | ||
|
|
34fc8bddc4 | ||
|
|
4dd89e439f | ||
|
|
150177c449 | ||
|
|
bf58ed2b53 | ||
|
|
827ced0014 | ||
|
|
133d46bde2 | ||
|
|
e1017266ac | ||
|
|
404fdaf2ff | ||
|
|
e231b692fd | ||
|
|
68c365f906 | ||
|
|
494c7294cb | ||
|
|
fb20ee99eb | ||
|
|
1a22154a3a | ||
|
|
a150f92fb0 | ||
|
|
5d31ded7a0 | ||
|
|
0ed31bfa2c | ||
|
|
51b9185a6b | ||
|
|
b219497847 | ||
|
|
7e96c89be5 | ||
|
|
16a8d0e794 | ||
|
|
7bb9981d8a | ||
|
|
ea3d38bf64 | ||
|
|
8f628b85e5 | ||
|
|
defc922acd | ||
|
|
88ae8d1f2b | ||
|
|
8c7c9ac38a | ||
|
|
0e146d50f7 | ||
|
|
454e1e3927 | ||
|
|
4f9875fd4e | ||
|
|
869709a75f | ||
|
|
85e9ca2a0f | ||
|
|
65d225cb2c | ||
|
|
21eddc3f23 | ||
|
|
4b63214a72 | ||
|
|
4937ebc058 | ||
|
|
202172135d | ||
|
|
b94ab243a8 | ||
|
|
11c8848e1f | ||
|
|
231fcc700f | ||
|
|
8ba7520acc | ||
|
|
e0a5a2489d | ||
|
|
bd400a5130 | ||
|
|
d35f84f337 | ||
|
|
af3bbdc591 | ||
|
|
8813cf2c94 | ||
|
|
d145c0e910 | ||
|
|
8dadb853a0 | ||
|
|
a700e8bf97 | ||
|
|
61d9143acb | ||
|
|
c88f74a6ee | ||
|
|
6377d6f64d | ||
|
|
1b0bc05fc2 | ||
|
|
45edeb9307 | ||
|
|
211974b2b7 | ||
|
|
1f5ad3e315 | ||
|
|
d7848e8a3d | ||
|
|
89b9f0df70 | ||
|
|
e9011567bd | ||
|
|
0429970d58 | ||
|
|
ecad9cee6c | ||
|
|
1e8a9f76a9 | ||
|
|
e42fdf1c54 | ||
|
|
c75566ddc3 | ||
|
|
7e9577dffd | ||
|
|
180dc7bdff | ||
|
|
cc9581842d | ||
|
|
d782e4bb17 | ||
|
|
ba9ad9385b | ||
|
|
91edfa40f7 | ||
|
|
c8b00bb929 | ||
|
|
57eadbf6af | ||
|
|
a906adce26 | ||
|
|
a6d26ef6c3 | ||
|
|
2b3f94eb7d | ||
|
|
dd36046e23 | ||
|
|
1507435e15 | ||
|
|
68c01b76bf | ||
|
|
28717e108d | ||
|
|
688d9b383d | ||
|
|
e68424d748 | ||
|
|
204a9a338f | ||
|
|
6a6a39ff33 | ||
|
|
b296db9a33 | ||
|
|
01ce84d8ee | ||
|
|
f48e663d4c | ||
|
|
a4aa2bd355 | ||
|
|
b51b9d50c2 | ||
|
|
b61780f735 | ||
|
|
074f18dfb8 | ||
|
|
d7308b0f43 | ||
|
|
2f1860386a | ||
|
|
f0bca7da55 | ||
|
|
6fcdf5bfcc | ||
|
|
e2ce0450c1 | ||
|
|
c95c64139d | ||
|
|
a7f12f1baa | ||
|
|
2a8653de2b | ||
|
|
a92c3bb251 | ||
|
|
3677abe9e5 | ||
|
|
95847ad236 | ||
|
|
e0152fb873 | ||
|
|
2298b96d8e | ||
|
|
5db83365b6 | ||
|
|
341a81a113 | ||
|
|
8a62b4cace | ||
|
|
ccafc20917 | ||
|
|
d5cb4eaa59 | ||
|
|
425fb7ee33 | ||
|
|
cd6f8e5a24 | ||
|
|
f0445b74d1 | ||
|
|
ba52eef257 | ||
|
|
c13ce2a5c0 | ||
|
|
d2463f41b5 | ||
|
|
eadb343292 | ||
|
|
e7208622f7 | ||
|
|
fbae611406 | ||
|
|
34027bc589 | ||
|
|
f2eef37599 | ||
|
|
1e3ea13323 | ||
|
|
4c8c48cde9 | ||
|
|
c8e50276e8 | ||
|
|
1e6419a63f |
@@ -33,6 +33,11 @@ export const packageOptions = {
|
||||
packageName: 'mermaid-layout-elk',
|
||||
file: 'layouts.ts',
|
||||
},
|
||||
'mermaid-layout-tidy-tree': {
|
||||
name: 'mermaid-layout-tidy-tree',
|
||||
packageName: 'mermaid-layout-tidy-tree',
|
||||
file: 'index.ts',
|
||||
},
|
||||
examples: {
|
||||
name: 'mermaid-examples',
|
||||
packageName: 'examples',
|
||||
|
||||
7
.changeset/hungry-guests-drive.md
Normal file
7
.changeset/hungry-guests-drive.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
'@mermaid-js/layout-tidy-tree': minor
|
||||
'@mermaid-js/layout-elk': minor
|
||||
---
|
||||
|
||||
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
|
||||
9
.changeset/revert-marked-dependency.md
Normal file
9
.changeset/revert-marked-dependency.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'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,6 +5,7 @@ bmatrix
|
||||
braintree
|
||||
catmull
|
||||
compositTitleSize
|
||||
cose
|
||||
curv
|
||||
doublecircle
|
||||
elems
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
BRANDES
|
||||
Buzan
|
||||
circo
|
||||
handDrawn
|
||||
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
|
||||
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
|
||||
fi
|
||||
|
||||
# 3) Lockfile only changes when package.json changes
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules/
|
||||
coverage/
|
||||
.idea/
|
||||
.pnpm-store/
|
||||
.instructions/
|
||||
|
||||
dist
|
||||
v8-compile-cache-0
|
||||
|
||||
488
COMPONENT_EXAMPLES.md
Normal file
488
COMPONENT_EXAMPLES.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# Mermaid Key Components: Code Examples
|
||||
|
||||
This document provides concrete code examples demonstrating how Mermaid's key components work together.
|
||||
|
||||
## Diagram Type Examples
|
||||
|
||||
### Flowchart Diagram Structure
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/diagrams/flowchart/flowDiagram.ts
|
||||
export const diagram = {
|
||||
parser: flowParser, // JISON parser for flowchart syntax
|
||||
get db() {
|
||||
// Database for managing diagram state
|
||||
return new FlowDB();
|
||||
},
|
||||
renderer, // SVG renderer function
|
||||
styles: flowStyles, // CSS styles specific to flowcharts
|
||||
init: (cnf: MermaidConfig) => {
|
||||
if (!cnf.flowchart) {
|
||||
cnf.flowchart = {};
|
||||
}
|
||||
// Configuration initialization
|
||||
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Sequence Diagram Structure
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser, // JISON parser for sequence syntax
|
||||
get db() {
|
||||
return new SequenceDB(); // Manages participants, messages, loops
|
||||
},
|
||||
renderer, // Draws actors, lifelines, messages
|
||||
styles, // Sequence-specific styling
|
||||
init: (cnf: MermaidConfig) => {
|
||||
if (!cnf.sequence) {
|
||||
cnf.sequence = {};
|
||||
}
|
||||
if (cnf.wrap) {
|
||||
cnf.sequence.wrap = cnf.wrap;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Core API Examples
|
||||
|
||||
### Main API Usage Pattern
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/mermaidAPI.ts
|
||||
|
||||
/**
|
||||
* Parse and validate diagram syntax
|
||||
*/
|
||||
async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult> {
|
||||
addDiagrams(); // Load all diagram types
|
||||
const { code, config } = processAndSetConfigs(text);
|
||||
const diagram = await getDiagramFromText(code);
|
||||
return { diagramType: diagram.type, config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render diagram to SVG
|
||||
*/
|
||||
async function render(
|
||||
id: string,
|
||||
text: string,
|
||||
svgContainingElement?: Element
|
||||
): Promise<RenderResult> {
|
||||
const { diagramType, config } = await parse(text, { suppressErrors: true });
|
||||
|
||||
// Create diagram instance
|
||||
const diagram = new Diagram(text, {}, diagramType);
|
||||
|
||||
// Render to SVG
|
||||
const svgCode = await diagram.renderer.draw(text, id, diagram);
|
||||
|
||||
return {
|
||||
diagramType,
|
||||
svg: svgCode,
|
||||
bindFunctions: diagram.db.bindFunctions,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Diagram Detection Example
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/diagram-api/detectType.ts
|
||||
|
||||
export const detectors: Record<string, DiagramDetector> = {
|
||||
flowchart: {
|
||||
detector: (text: string) => /^\s*graph|flowchart/.test(text),
|
||||
loader: async () => {
|
||||
const { diagram } = await import('../diagrams/flowchart/flowDiagram.js');
|
||||
return { id: 'flowchart', diagram };
|
||||
},
|
||||
},
|
||||
sequence: {
|
||||
detector: (text: string) => /^\s*sequenceDiagram/.test(text),
|
||||
loader: async () => {
|
||||
const { diagram } = await import('../diagrams/sequence/sequenceDiagram.js');
|
||||
return { id: 'sequence', diagram };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const detectType = function (text: string, config?: MermaidConfig): string {
|
||||
for (const [diagramType, { detector }] of Object.entries(detectors)) {
|
||||
if (detector(text, config)) {
|
||||
return diagramType;
|
||||
}
|
||||
}
|
||||
throw new UnknownDiagramError(`No diagram type detected for text: ${text}`);
|
||||
};
|
||||
```
|
||||
|
||||
## Rendering System Examples
|
||||
|
||||
### Layout Algorithm Integration
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/rendering-util/render.ts
|
||||
|
||||
interface LayoutAlgorithm {
|
||||
render(
|
||||
layoutData: LayoutData,
|
||||
svg: SVG,
|
||||
helpers: InternalHelpers,
|
||||
options?: RenderOptions
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
const layoutAlgorithms: Record<string, LayoutLoaderDefinition> = {};
|
||||
|
||||
export const registerLayoutLoaders = (loaders: LayoutLoaderDefinition[]) => {
|
||||
for (const loader of loaders) {
|
||||
layoutAlgorithms[loader.name] = loader;
|
||||
}
|
||||
};
|
||||
|
||||
// Default layout loaders
|
||||
const registerDefaultLayoutLoaders = () => {
|
||||
registerLayoutLoaders([
|
||||
{
|
||||
name: 'dagre',
|
||||
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||
},
|
||||
{
|
||||
name: 'elk',
|
||||
loader: async () => await import('./layout-algorithms/elk/index.js'),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export const render = async (data4Layout: LayoutData, svg: SVG) => {
|
||||
const layoutDefinition = layoutAlgorithms[data4Layout.layoutAlgorithm];
|
||||
const layoutRenderer = await layoutDefinition.loader();
|
||||
return layoutRenderer.render(data4Layout, svg, internalHelpers);
|
||||
};
|
||||
```
|
||||
|
||||
### Unified Renderer Example
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/diagrams/er/erRenderer-unified.ts
|
||||
|
||||
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||
const { securityLevel, er: conf, layout } = getConfig();
|
||||
|
||||
// Extract data from parsed structure into Layout data format
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
// Create the root SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
// Configure layout
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
|
||||
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
|
||||
data4Layout.direction = diag.db.getDirection();
|
||||
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
|
||||
data4Layout.diagramId = id;
|
||||
|
||||
// Render using layout algorithm
|
||||
await render(data4Layout, svg);
|
||||
|
||||
// Setup viewport
|
||||
setupViewPortForSVG(svg, conf.padding, conf.useMaxWidth);
|
||||
};
|
||||
```
|
||||
|
||||
## Class Diagram Renderer Examples
|
||||
|
||||
### Traditional Renderer (v2)
|
||||
|
||||
```javascript
|
||||
// packages/mermaid/src/diagrams/class/classRenderer.js
|
||||
|
||||
export const draw = function (text, id, _version, diagObj) {
|
||||
const conf = getConfig().class;
|
||||
|
||||
// Create directed graph using graphlib
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
});
|
||||
|
||||
g.setGraph({
|
||||
isMultiGraph: true,
|
||||
});
|
||||
|
||||
// Add classes as nodes
|
||||
classes.forEach(function (classDef) {
|
||||
const node = svgDraw.drawClass(diagram, classDef, conf, diagObj);
|
||||
idCache[node.id] = node;
|
||||
g.setNode(node.id, node);
|
||||
});
|
||||
|
||||
// Add relationships as edges
|
||||
relations.forEach(function (relation) {
|
||||
g.setEdge(
|
||||
getGraphId(relation.id1),
|
||||
getGraphId(relation.id2),
|
||||
{ relation: relation },
|
||||
relation.title || 'DEFAULT'
|
||||
);
|
||||
});
|
||||
|
||||
// Apply layout
|
||||
dagreLayout(g);
|
||||
|
||||
// Position nodes
|
||||
g.nodes().forEach(function (v) {
|
||||
if (v !== undefined && g.node(v) !== undefined) {
|
||||
root
|
||||
.select('#' + (diagObj.db.lookUpDomId(v) || v))
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(g.node(v).x - g.node(v).width / 2) +
|
||||
',' +
|
||||
(g.node(v).y - g.node(v).height / 2) +
|
||||
' )'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw edges
|
||||
g.edges().forEach(function (e) {
|
||||
if (e !== undefined && g.edge(e) !== undefined) {
|
||||
svgDraw.drawEdge(diagram, g.edge(e), g.edge(e).relation, conf, diagObj);
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Modern Unified Renderer (v3)
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts
|
||||
|
||||
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||
const { securityLevel, state: conf, layout } = getConfig();
|
||||
|
||||
// Extract data from parsed structure into Layout data format
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
// Create the root SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
// Configure layout settings
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
|
||||
data4Layout.rankSpacing = conf?.rankSpacing || 50;
|
||||
data4Layout.markers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop'];
|
||||
data4Layout.diagramId = id;
|
||||
|
||||
// Use unified rendering system
|
||||
await render(data4Layout, svg);
|
||||
|
||||
// Add title
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'classDiagramTitleText',
|
||||
conf?.titleTopMargin ?? 25,
|
||||
diag.db.getDiagramTitle()
|
||||
);
|
||||
|
||||
setupViewPortForSVG(svg, padding, conf?.useMaxWidth);
|
||||
};
|
||||
```
|
||||
|
||||
## Configuration System Example
|
||||
|
||||
### Type-Safe Configuration
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/config.type.ts
|
||||
|
||||
export interface MermaidConfig {
|
||||
theme?: 'default' | 'dark' | 'forest' | 'neutral';
|
||||
themeVariables?: any;
|
||||
startOnLoad?: boolean;
|
||||
securityLevel?: 'strict' | 'loose' | 'antiscript' | 'sandbox';
|
||||
deterministicIds?: boolean;
|
||||
fontSize?: number;
|
||||
|
||||
// Diagram-specific configurations
|
||||
flowchart?: FlowchartDiagramConfig;
|
||||
sequence?: SequenceDiagramConfig;
|
||||
class?: ClassDiagramConfig;
|
||||
state?: StateDiagramConfig;
|
||||
er?: ErDiagramConfig;
|
||||
// ... other diagram configs
|
||||
}
|
||||
|
||||
export interface FlowchartDiagramConfig extends BaseDiagramConfig {
|
||||
diagramPadding?: number;
|
||||
htmlLabels?: boolean;
|
||||
nodeSpacing?: number;
|
||||
rankSpacing?: number;
|
||||
curve?: CurveStyle;
|
||||
useMaxWidth?: boolean;
|
||||
defaultRenderer?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration API
|
||||
|
||||
```typescript
|
||||
// packages/mermaid/src/config.ts
|
||||
|
||||
let config: MermaidConfig = assignWithDepth({}, defaultConfig);
|
||||
let directives: MermaidConfig = {};
|
||||
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
|
||||
|
||||
export const getConfig = (): MermaidConfig => currentConfig;
|
||||
|
||||
export const setConfig = (conf: MermaidConfig): MermaidConfig => {
|
||||
updateCurrentConfig(conf);
|
||||
return getConfig();
|
||||
};
|
||||
|
||||
export const reset = (): void => {
|
||||
config = assignWithDepth({}, defaultConfig);
|
||||
directives = {};
|
||||
updateCurrentConfig(config);
|
||||
};
|
||||
|
||||
const updateCurrentConfig = (conf: MermaidConfig): void => {
|
||||
currentConfig = assignWithDepth({}, config, directives, conf);
|
||||
};
|
||||
```
|
||||
|
||||
## Web Integration Example
|
||||
|
||||
### Simple Usage
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mermaid">
|
||||
graph TD A[Start] --> B{Decision} B -->|Yes| C[Action 1] B -->|No| D[Action 2]
|
||||
</div>
|
||||
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'dark',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Advanced Programmatic Usage
|
||||
|
||||
```javascript
|
||||
// Advanced integration example
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
// Initialize with configuration
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'strict',
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
},
|
||||
sequence: {
|
||||
actorMargin: 50,
|
||||
boxMargin: 10,
|
||||
boxTextMargin: 5,
|
||||
noteMargin: 10,
|
||||
messageMargin: 35,
|
||||
},
|
||||
});
|
||||
|
||||
// Render specific diagram
|
||||
async function renderDiagram(definition, element) {
|
||||
try {
|
||||
const { svg } = await mermaid.render('graphDiv', definition);
|
||||
element.innerHTML = svg;
|
||||
} catch (error) {
|
||||
console.error('Error rendering diagram:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and validate before rendering
|
||||
async function validateAndRender(definition, element) {
|
||||
try {
|
||||
const parseResult = await mermaid.parse(definition);
|
||||
console.log('Valid diagram type:', parseResult.diagramType);
|
||||
|
||||
const { svg } = await mermaid.render('graphDiv', definition);
|
||||
element.innerHTML = svg;
|
||||
} catch (error) {
|
||||
console.error('Invalid diagram syntax:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Example: Custom Diagram Type
|
||||
|
||||
### Custom Diagram Definition
|
||||
|
||||
```typescript
|
||||
// example-diagram/exampleDiagram.ts
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser: {
|
||||
parse: (text: string) => {
|
||||
// Custom parsing logic
|
||||
},
|
||||
},
|
||||
get db() {
|
||||
return {
|
||||
clear: () => {},
|
||||
// ... other required DB methods
|
||||
};
|
||||
},
|
||||
renderer: {
|
||||
draw: async (text: string, id: string, version: string) => {
|
||||
// Custom rendering logic
|
||||
const svg = d3.select(`#${id}`);
|
||||
// ... create SVG elements
|
||||
},
|
||||
},
|
||||
styles: () => `
|
||||
.example-class {
|
||||
fill: #ff6b6b;
|
||||
stroke: #333;
|
||||
}
|
||||
`,
|
||||
};
|
||||
```
|
||||
|
||||
### Registration and Usage
|
||||
|
||||
```typescript
|
||||
// Register custom diagram
|
||||
import { registerDiagram } from 'mermaid';
|
||||
import { diagram } from './exampleDiagram.js';
|
||||
|
||||
registerDiagram('example', diagram, (text) => text.includes('example'));
|
||||
|
||||
// Now can use in text:
|
||||
const diagramText = `
|
||||
example
|
||||
nodeA --> nodeB
|
||||
`;
|
||||
```
|
||||
|
||||
This comprehensive set of examples demonstrates how Mermaid's modular architecture enables powerful diagram generation while maintaining extensibility and type safety.
|
||||
210
DEMONSTRATION.html
Normal file
210
DEMONSTRATION.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mermaid Functionality Demonstration</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.demo-section {
|
||||
background: white;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.demo-title {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007acc;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mermaid {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.code-block {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
color: #007acc;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🧜♀️ Mermaid Functionality Demonstration</h1>
|
||||
<p>A showcase of Mermaid's key diagram types and capabilities</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">1. Flowchart - Decision Flow</h2>
|
||||
<p><strong>Use case:</strong> Process flows, decision trees, algorithm visualization</p>
|
||||
<div class="code-block">
|
||||
flowchart TD A[Start] --> B{Is it working?} B -->|Yes| C[Great!] B -->|No| D[Debug] D -->
|
||||
E[Fix issues] E --> B C --> F[Deploy]
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
flowchart TD A[Start] --> B{Is it working?} B -->|Yes| C[Great!] B -->|No| D[Debug] D -->
|
||||
E[Fix issues] E --> B C --> F[Deploy]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">2. Sequence Diagram - API Interaction</h2>
|
||||
<p><strong>Use case:</strong> API documentation, system interactions, communication flows</p>
|
||||
<div class="code-block">
|
||||
sequenceDiagram participant C as Client participant S as Server participant DB as Database
|
||||
C->>S: Request user data S->>DB: Query user table DB-->>S: Return user data S-->>C: JSON
|
||||
response Note right of C: Client processes data
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
sequenceDiagram participant C as Client participant S as Server participant DB as Database
|
||||
C->>S: Request user data S->>DB: Query user table DB-->>S: Return user data S-->>C: JSON
|
||||
response Note right of C: Client processes data
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">3. Class Diagram - Software Architecture</h2>
|
||||
<p>
|
||||
<strong>Use case:</strong> Object-oriented design, software architecture, database schemas
|
||||
</p>
|
||||
<div class="code-block">
|
||||
classDiagram class User { +String name +String email +login() +logout() } class Order { +int
|
||||
id +Date date +float total +calculate() } class Product { +String name +float price +int
|
||||
stock } User ||--o{ Order : places Order }|--|| Product : contains
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
classDiagram class User { +String name +String email +login() +logout() } class Order { +int
|
||||
id +Date date +float total +calculate() } class Product { +String name +float price +int
|
||||
stock } User ||--o{ Order : places Order }|--|| Product : contains
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">4. Git Graph - Development Workflow</h2>
|
||||
<p>
|
||||
<strong>Use case:</strong> Git workflow documentation, branching strategies, release
|
||||
processes
|
||||
</p>
|
||||
<div class="code-block">
|
||||
gitGraph commit id: "Initial" commit id: "Setup" branch feature checkout feature commit id:
|
||||
"Feature A" commit id: "Feature B" checkout main merge feature commit id: "Release v1.0"
|
||||
branch hotfix checkout hotfix commit id: "Fix bug" checkout main merge hotfix commit id:
|
||||
"Release v1.1"
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
gitGraph commit id: "Initial" commit id: "Setup" branch feature checkout feature commit id:
|
||||
"Feature A" commit id: "Feature B" checkout main merge feature commit id: "Release v1.0"
|
||||
branch hotfix checkout hotfix commit id: "Fix bug" checkout main merge hotfix commit id:
|
||||
"Release v1.1"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">5. Gantt Chart - Project Timeline</h2>
|
||||
<p><strong>Use case:</strong> Project management, milestone tracking, resource planning</p>
|
||||
<div class="code-block">
|
||||
gantt title Development Timeline dateFormat YYYY-MM-DD section Planning Requirements :done,
|
||||
req, 2024-01-01, 2024-01-07 Design :done, des, 2024-01-08, 2024-01-14 section Development
|
||||
Backend API :active, api, 2024-01-15, 2024-02-15 Frontend :frontend, after api, 20d Testing
|
||||
:test, after frontend, 10d section Deployment Staging :staging, after test, 3d Production
|
||||
:prod, after staging, 2d
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
gantt title Development Timeline dateFormat YYYY-MM-DD section Planning Requirements :done,
|
||||
req, 2024-01-01, 2024-01-07 Design :done, des, 2024-01-08, 2024-01-14 section Development
|
||||
Backend API :active, api, 2024-01-15, 2024-02-15 Frontend :frontend, after api, 20d Testing
|
||||
:test, after frontend, 10d section Deployment Staging :staging, after test, 3d Production
|
||||
:prod, after staging, 2d
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">6. State Diagram - System States</h2>
|
||||
<p><strong>Use case:</strong> State machines, user workflows, system behavior modeling</p>
|
||||
<div class="code-block">
|
||||
stateDiagram-v2 [*] --> Idle Idle --> Loading : start_request Loading --> Success :
|
||||
request_success Loading --> Error : request_failed Success --> Idle : reset Error --> Idle :
|
||||
retry Error --> [*] : give_up
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
stateDiagram-v2 [*] --> Idle Idle --> Loading : start_request Loading --> Success :
|
||||
request_success Loading --> Error : request_failed Success --> Idle : reset Error --> Idle :
|
||||
retry Error --> [*] : give_up
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">7. Pie Chart - Data Visualization</h2>
|
||||
<p><strong>Use case:</strong> Data visualization, market share, resource allocation</p>
|
||||
<div class="code-block">
|
||||
pie title Technology Stack Usage "JavaScript" : 35 "TypeScript" : 25 "Python" : 20 "Java" :
|
||||
15 "Other" : 5
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
pie title Technology Stack Usage "JavaScript" : 35 "TypeScript" : 25 "Python" : 20 "Java" :
|
||||
15 "Other" : 5
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">8. User Journey - UX Flow</h2>
|
||||
<p>
|
||||
<strong>Use case:</strong> User experience mapping, customer journeys, process optimization
|
||||
</p>
|
||||
<div class="code-block">
|
||||
journey title User Registration Journey section Discovery Visit homepage : 5: User Read
|
||||
about features : 4: User Check pricing : 3: User section Registration Click signup : 3: User
|
||||
Fill form : 2: User Verify email : 1: User, System section Onboarding Complete profile : 4:
|
||||
User Take tutorial : 5: User First task : 5: User
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
journey title User Registration Journey section Discovery Visit homepage : 5: User Read
|
||||
about features : 4: User Check pricing : 3: User section Registration Click signup : 3: User
|
||||
Fill form : 2: User Verify email : 1: User, System section Onboarding Complete profile : 4:
|
||||
User Take tutorial : 5: User First task : 5: User
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Mermaid with configuration
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose',
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
},
|
||||
sequence: {
|
||||
actorMargin: 50,
|
||||
width: 150,
|
||||
height: 65,
|
||||
boxMargin: 10,
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: true,
|
||||
leftMargin: 75,
|
||||
gridLineStartPadding: 35,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
249
MERMAID_OVERVIEW.md
Normal file
249
MERMAID_OVERVIEW.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Mermaid: Overview of Main Functionality and Key Components
|
||||
|
||||
## What is Mermaid?
|
||||
|
||||
Mermaid is a JavaScript-based diagramming and charting tool that uses Markdown-inspired text definitions to create and modify complex diagrams. The main purpose of Mermaid is to help documentation catch up with development by enabling users to create easily modifiable diagrams that can be integrated into production scripts and documentation workflows.
|
||||
|
||||
**Core Philosophy**: "Doc-Rot is a Catch-22 that Mermaid helps to solve" - providing a solution to the common problem where diagramming costs precious developer time and gets outdated quickly, but not having diagrams hurts productivity and organizational learning.
|
||||
|
||||
## Main Functionality
|
||||
|
||||
### 1. **Text-to-Diagram Conversion**
|
||||
|
||||
- Converts Markdown-inspired text definitions into SVG diagrams
|
||||
- Supports over 15+ different diagram types
|
||||
- Real-time rendering and preview capabilities
|
||||
- Integration with popular platforms (GitHub, GitLab, etc.)
|
||||
|
||||
### 2. **Interactive Diagram Creation**
|
||||
|
||||
- Live Editor for interactive diagram creation
|
||||
- Web integration module for embedding in web pages
|
||||
- Dynamic rendering and modification capabilities
|
||||
|
||||
### 3. **Extensible Architecture**
|
||||
|
||||
- Plugin system for custom diagram types
|
||||
- Configurable themes and styling
|
||||
- Multiple layout algorithms support
|
||||
- External diagram definition loading
|
||||
|
||||
## Key Components
|
||||
|
||||
### Core Architecture
|
||||
|
||||
#### 1. **Entry Points & API Layer**
|
||||
|
||||
- **`mermaid.ts`**: Main web page integration module, provides the primary interface for users
|
||||
- **`mermaidAPI.ts`**: Core API containing functions for parsing and rendering diagrams
|
||||
- **`Diagram.ts`**: Main diagram abstraction class that handles diagram lifecycle
|
||||
|
||||
#### 2. **Configuration System**
|
||||
|
||||
- **`config.ts`** & **`config.type.ts`**: Centralized configuration management
|
||||
- **`defaultConfig.ts`**: Default configuration values
|
||||
- Supports runtime configuration updates and diagram-specific settings
|
||||
|
||||
#### 3. **Parser & Detection System**
|
||||
|
||||
- **`diagram-api/detectType.ts`**: Automatic diagram type detection from text input
|
||||
- **`diagram-api/loadDiagram.ts`**: Dynamic loading and registration of diagram parsers
|
||||
- **`preprocess.ts`**: Text preprocessing and directive handling
|
||||
|
||||
### Diagram Types (Located in `/src/diagrams/`)
|
||||
|
||||
Mermaid supports a comprehensive set of diagram types, each with its own parser, renderer, and styling:
|
||||
|
||||
#### **Core Diagram Types:**
|
||||
|
||||
1. **Flowchart** - Flow diagrams with nodes, edges, and decision points
|
||||
2. **Sequence** - Interaction diagrams between actors over time
|
||||
3. **Class** - Object-oriented modeling diagrams
|
||||
4. **State** - State machine and statechart diagrams
|
||||
5. **Gantt** - Project timeline and scheduling charts
|
||||
6. **Git** - Git branching and merging visualizations
|
||||
|
||||
#### **Specialized Diagram Types:**
|
||||
|
||||
7. **Pie** - Data visualization pie charts
|
||||
8. **User Journey** - User experience journey mapping
|
||||
9. **C4** - C4 architecture diagrams (Context, Container, Component, Code)
|
||||
10. **ER (Entity-Relationship)** - Database modeling diagrams
|
||||
11. **Mindmap** - Hierarchical mind mapping
|
||||
12. **Architecture** - System architecture diagrams
|
||||
13. **Block** - Block diagrams
|
||||
14. **Kanban** - Kanban board visualization
|
||||
15. **Packet** - Network packet diagrams
|
||||
16. **Quadrant** - Four-quadrant analysis charts
|
||||
17. **Radar** - Radar/spider charts
|
||||
18. **Requirement** - Requirements modeling
|
||||
19. **Sankey** - Flow diagrams showing quantities
|
||||
20. **Timeline** - Chronological event timelines
|
||||
21. **Treemap** - Hierarchical data visualization
|
||||
22. **XYChart** - Scatter plots and line charts
|
||||
|
||||
#### **Diagram Structure Pattern:**
|
||||
|
||||
Each diagram type follows a consistent structure:
|
||||
|
||||
```typescript
|
||||
export const diagram = {
|
||||
parser: parserDefinition, // JISON or custom parser
|
||||
db: databaseClass, // Data management and state
|
||||
renderer: rendererFunction, // SVG rendering logic
|
||||
styles: stylingDefinition, // CSS styling
|
||||
init: initializationFunction, // Setup and configuration
|
||||
};
|
||||
```
|
||||
|
||||
### Rendering System
|
||||
|
||||
#### 1. **Core Rendering (`rendering-util/`)**
|
||||
|
||||
- **`render.ts`**: Main rendering orchestrator with pluggable layout algorithms
|
||||
- **`createGraph.ts`** & **`createText.ts`**: Basic shape and text creation
|
||||
- **`insertElementsForSize.ts`**: Element sizing and positioning
|
||||
- **`setupViewPortForSVG.ts`**: SVG viewport management
|
||||
|
||||
#### 2. **Layout Algorithms (`layout-algorithms/`)**
|
||||
|
||||
- **Dagre**: Default hierarchical layout algorithm
|
||||
- **Elk**: Advanced layout engine with multiple algorithms
|
||||
- **Cose-Bilkent**: Physics-based layout for complex graphs
|
||||
- **Tidy-Tree**: Tree layout algorithm
|
||||
|
||||
#### 3. **Styling & Theming**
|
||||
|
||||
- **`themes/`**: Multiple built-in themes (default, dark, forest, neutral, etc.)
|
||||
- **`styles.ts`**: CSS-in-JS styling system
|
||||
- Theme inheritance and customization capabilities
|
||||
|
||||
### Security & Safety
|
||||
|
||||
#### 1. **Security Levels**
|
||||
|
||||
- **Sandbox Mode**: Renders diagrams in sandboxed iframes to prevent script execution
|
||||
- **Loose Mode**: Standard rendering with sanitization
|
||||
- **Strict Mode**: Enhanced security with limited functionality
|
||||
|
||||
#### 2. **Content Sanitization**
|
||||
|
||||
- **DOMPurify Integration**: XSS protection for user-generated content
|
||||
- Input validation and sanitization
|
||||
- Safe handling of external content
|
||||
|
||||
### Extension & Integration System
|
||||
|
||||
#### 1. **External Diagram Support**
|
||||
|
||||
- Dynamic loading of custom diagram types
|
||||
- Plugin architecture for extending functionality
|
||||
- External diagram definition interface
|
||||
|
||||
#### 2. **Layout Extension**
|
||||
|
||||
- Pluggable layout algorithms
|
||||
- Custom layout loader registration
|
||||
- Runtime algorithm selection
|
||||
|
||||
#### 3. **Icon System**
|
||||
|
||||
- **`icons.ts`**: Icon pack management
|
||||
- SVG icon rendering
|
||||
- Customizable icon libraries
|
||||
|
||||
### Package Structure (Monorepo)
|
||||
|
||||
The repository uses a monorepo structure with pnpm workspaces:
|
||||
|
||||
#### **Main Packages:**
|
||||
|
||||
- **`packages/mermaid/`**: Core Mermaid library
|
||||
- **`packages/mermaid-example-diagram/`**: Example external diagram
|
||||
- **`packages/mermaid-layout-elk/`**: Elk layout algorithm
|
||||
- **`packages/mermaid-layout-tidy-tree/`**: Tidy tree layout
|
||||
- **`packages/mermaid-zenuml/`**: ZenUML integration
|
||||
- **`packages/parser/`**: Parser utilities
|
||||
- **`packages/tiny/`**: Minimal Mermaid build
|
||||
|
||||
#### **Supporting Infrastructure:**
|
||||
|
||||
- **`docs/`**: Documentation and website content
|
||||
- **`demos/`**: Example implementations
|
||||
- **`cypress/`**: End-to-end testing
|
||||
- **`tests/`**: Unit and integration tests
|
||||
|
||||
### Development & Build System
|
||||
|
||||
#### 1. **Build Pipeline**
|
||||
|
||||
- **ESBuild**: Fast JavaScript bundling
|
||||
- **TypeScript**: Type checking and compilation
|
||||
- **Vite**: Development server and hot reloading
|
||||
|
||||
#### 2. **Testing Strategy**
|
||||
|
||||
- **Vitest**: Unit testing framework
|
||||
- **Cypress**: End-to-end visual regression testing
|
||||
- **Applitools**: Visual testing integration
|
||||
- **Argos**: PR visual regression testing
|
||||
|
||||
#### 3. **Code Quality**
|
||||
|
||||
- **ESLint**: JavaScript/TypeScript linting
|
||||
- **Prettier**: Code formatting
|
||||
- **Husky**: Git hooks for pre-commit checks
|
||||
- **CSpell**: Spell checking for documentation
|
||||
|
||||
## Integration Ecosystem
|
||||
|
||||
### Official Integrations
|
||||
|
||||
- **Mermaid Chart**: Commercial diagram editor and collaboration platform
|
||||
- **GitHub**: Native Mermaid support in GitHub Markdown
|
||||
- **GitLab**: Built-in Mermaid rendering
|
||||
|
||||
### Community Integrations
|
||||
|
||||
- **Productivity Tools**: Notion, Confluence, Obsidian, etc.
|
||||
- **Development Tools**: VS Code, IntelliJ, various editors
|
||||
- **Documentation**: GitBook, Docusaurus, VuePress, etc.
|
||||
- **Communication**: Slack, Discord, Microsoft Teams
|
||||
- **CMS/Wiki Systems**: MediaWiki, TiddlyWiki, etc.
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. **Direct Web Integration**
|
||||
|
||||
```javascript
|
||||
import mermaid from 'mermaid';
|
||||
mermaid.initialize({ startOnLoad: true });
|
||||
```
|
||||
|
||||
### 2. **Programmatic API Usage**
|
||||
|
||||
```javascript
|
||||
const { svg } = await mermaid.render('graphDiv', 'graph TD; A-->B');
|
||||
```
|
||||
|
||||
### 3. **Configuration & Customization**
|
||||
|
||||
```javascript
|
||||
mermaid.initialize({
|
||||
theme: 'dark',
|
||||
flowchart: { useMaxWidth: true },
|
||||
sequence: { actorMargin: 50 },
|
||||
});
|
||||
```
|
||||
|
||||
## Future Extensibility
|
||||
|
||||
The architecture is designed for extensibility through:
|
||||
|
||||
- **Plugin System**: Easy addition of new diagram types
|
||||
- **Layout Algorithms**: Pluggable layout engines
|
||||
- **Theming**: Comprehensive theme customization
|
||||
- **Output Formats**: Support for multiple output formats
|
||||
- **Integration APIs**: Well-defined interfaces for third-party integration
|
||||
|
||||
This modular architecture makes Mermaid a powerful platform for diagram generation that can grow and adapt to new use cases while maintaining backward compatibility and performance.
|
||||
79
cypress/integration/rendering/mindmap-tidy-tree.spec.js
Normal file
79
cypress/integration/rendering/mindmap-tidy-tree.spec.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util.ts';
|
||||
|
||||
describe('Mindmap Tidy Tree', () => {
|
||||
it('1-tidy-tree: should render a simple mindmap without children', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
`
|
||||
);
|
||||
});
|
||||
it('2-tidy-tree: should render a simple mindmap', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
`
|
||||
);
|
||||
});
|
||||
it('3-tidy-tree: should render a mindmap with different shapes', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<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,12 +159,10 @@ root
|
||||
});
|
||||
it('square shape', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
`mindmap
|
||||
root[
|
||||
The root
|
||||
]
|
||||
`,
|
||||
]`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -172,12 +170,10 @@ mindmap
|
||||
});
|
||||
it('rounded rect shape', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
`mindmap
|
||||
root((
|
||||
The root
|
||||
))
|
||||
`,
|
||||
))`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -185,12 +181,10 @@ mindmap
|
||||
});
|
||||
it('circle shape', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
`mindmap
|
||||
root(
|
||||
The root
|
||||
)
|
||||
`,
|
||||
)`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -198,10 +192,8 @@ mindmap
|
||||
});
|
||||
it('default shape', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
The root
|
||||
`,
|
||||
`mindmap
|
||||
The root`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -209,12 +201,10 @@ mindmap
|
||||
});
|
||||
it('adding children', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
`mindmap
|
||||
The root
|
||||
child1
|
||||
child2
|
||||
`,
|
||||
child2`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -222,13 +212,11 @@ mindmap
|
||||
});
|
||||
it('adding grand children', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
`mindmap
|
||||
The root
|
||||
child1
|
||||
child2
|
||||
child3
|
||||
`,
|
||||
child3`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -240,25 +228,21 @@ mindmap
|
||||
`mindmap
|
||||
id1[\`**Start** with
|
||||
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)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
mindmap
|
||||
` mindmap
|
||||
root
|
||||
Photograph
|
||||
Waterfall
|
||||
Landscape
|
||||
Geography
|
||||
Mountains
|
||||
Rocks
|
||||
`,
|
||||
Rocks`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -130,6 +130,76 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
c
|
||||
d
|
||||
B
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
D
|
||||
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
</pre>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
@@ -191,8 +261,145 @@ treemap
|
||||
"Item B2": 25
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
flowchart TB
|
||||
A --> n0["1"]
|
||||
A --> n1["2"]
|
||||
A --> n2["3"]
|
||||
A --> n3["4"] --> Q & R & S & T
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart TB
|
||||
A --> n0["1"]
|
||||
A --> n1["2"]
|
||||
A --> n2["3"]
|
||||
A --> n3["4"] --> Q & R & S & T
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
flowchart LR
|
||||
AB["apa@apa@"] --> B(("`apa@apa`"))
|
||||
root{mindmap} --- Origins --- Europe
|
||||
Origins --> Asia
|
||||
root --- Background --- Rich
|
||||
Background --- Poor
|
||||
subgraph apa
|
||||
Background
|
||||
Poor
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart LR
|
||||
root{mindmap} --- Origins --- Europe
|
||||
Origins --> Asia
|
||||
root --- Background --- Rich
|
||||
Background --- Poor
|
||||
|
||||
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart
|
||||
@@ -274,6 +481,44 @@ config:
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart LR
|
||||
a
|
||||
subgraph s0["APA"]
|
||||
subgraph s8["APA"]
|
||||
subgraph s1["APA"]
|
||||
D{"X"}
|
||||
E[Q]
|
||||
end
|
||||
subgraph s3["BAPA"]
|
||||
F[Q]
|
||||
I
|
||||
end
|
||||
D --> I
|
||||
D --> I
|
||||
D --> I
|
||||
|
||||
I{"X"}
|
||||
end
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart LR
|
||||
a
|
||||
D{"Use the editor"}
|
||||
|
||||
D -- Mermaid js --> I{"fa:fa-code Text"}
|
||||
D-->I
|
||||
D-->I
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
|
||||
376
cypress/platform/mindmap-layouts.html
Normal file
376
cypress/platform/mindmap-layouts.html
Normal file
@@ -0,0 +1,376 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Mermaid Quick Test Page</title>
|
||||
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
|
||||
<style>
|
||||
div.mermaid {
|
||||
font-family: 'Courier New', Courier, monospace !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<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,5 +1,6 @@
|
||||
import externalExample from './mermaid-example-diagram.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
import tidyTree from './mermaid-layout-tidy-tree.esm.mjs';
|
||||
import zenUml from './mermaid-zenuml.esm.mjs';
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
|
||||
@@ -65,6 +66,7 @@ const contentLoaded = async function () {
|
||||
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
|
||||
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.registerLayoutLoaders(tidyTree);
|
||||
mermaid.initialize(graphObj.mermaid);
|
||||
/**
|
||||
* CC-BY-4.0
|
||||
|
||||
@@ -2,223 +2,227 @@
|
||||
"durations": [
|
||||
{
|
||||
"spec": "cypress/integration/other/configuration.spec.js",
|
||||
"duration": 6162
|
||||
"duration": 5841
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
||||
"duration": 2148
|
||||
"duration": 2138
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/ghsa.spec.js",
|
||||
"duration": 3585
|
||||
"duration": 3370
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/iife.spec.js",
|
||||
"duration": 2099
|
||||
"duration": 2052
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/interaction.spec.js",
|
||||
"duration": 12119
|
||||
"duration": 12243
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/rerender.spec.js",
|
||||
"duration": 2063
|
||||
"duration": 2065
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/xss.spec.js",
|
||||
"duration": 31921
|
||||
"duration": 31288
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/appli.spec.js",
|
||||
"duration": 3385
|
||||
"duration": 3421
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
||||
"duration": 108
|
||||
"duration": 97
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/block.spec.js",
|
||||
"duration": 18063
|
||||
"duration": 18500
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/c4.spec.js",
|
||||
"duration": 5519
|
||||
"duration": 5793
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
||||
"duration": 40040
|
||||
"duration": 40966
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
||||
"duration": 38665
|
||||
"duration": 39176
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
||||
"duration": 22836
|
||||
"duration": 23468
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
||||
"duration": 37096
|
||||
"duration": 38291
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
||||
"duration": 16452
|
||||
"duration": 16949
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
||||
"duration": 10387
|
||||
"duration": 9480
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/current.spec.js",
|
||||
"duration": 2803
|
||||
"duration": 2753
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
||||
"duration": 86891
|
||||
"duration": 88028
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
||||
"duration": 15206
|
||||
"duration": 15615
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
||||
"duration": 3540
|
||||
"duration": 3706
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
||||
"duration": 41975
|
||||
"duration": 43905
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
||||
"duration": 30909
|
||||
"duration": 31217
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
|
||||
"duration": 7881
|
||||
"duration": 7531
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
||||
"duration": 24294
|
||||
"duration": 25423
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
||||
"duration": 47652
|
||||
"duration": 49664
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
||||
"duration": 32049
|
||||
"duration": 32525
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gantt.spec.js",
|
||||
"duration": 20248
|
||||
"duration": 20915
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
||||
"duration": 51202
|
||||
"duration": 53556
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
||||
"duration": 283546
|
||||
"duration": 283038
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
||||
"duration": 57257
|
||||
"duration": 59434
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/info.spec.ts",
|
||||
"duration": 3352
|
||||
"duration": 3101
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/journey.spec.js",
|
||||
"duration": 7423
|
||||
"duration": 7099
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
||||
"duration": 7804
|
||||
"duration": 7567
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/katex.spec.js",
|
||||
"duration": 3847
|
||||
"duration": 3817
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
||||
"duration": 2637
|
||||
"duration": 2624
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js",
|
||||
"duration": 4246
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
||||
"duration": 11658
|
||||
"duration": 11967
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
||||
"duration": 149500
|
||||
"duration": 151914
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
||||
"duration": 115427
|
||||
"duration": 116698
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/packet.spec.ts",
|
||||
"duration": 4801
|
||||
"duration": 4967
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/pie.spec.ts",
|
||||
"duration": 6786
|
||||
"duration": 6700
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
||||
"duration": 9422
|
||||
"duration": 8963
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/radar.spec.js",
|
||||
"duration": 5652
|
||||
"duration": 5540
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirement.spec.js",
|
||||
"duration": 2787
|
||||
"duration": 2782
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
||||
"duration": 53631
|
||||
"duration": 54797
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
||||
"duration": 7075
|
||||
"duration": 6914
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
|
||||
"duration": 20446
|
||||
"duration": 20481
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
||||
"duration": 37326
|
||||
"duration": 38490
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
||||
"duration": 29208
|
||||
"duration": 30766
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
||||
"duration": 16328
|
||||
"duration": 16705
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/theme.spec.js",
|
||||
"duration": 30541
|
||||
"duration": 30928
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
||||
"duration": 8611
|
||||
"duration": 8424
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/treemap.spec.ts",
|
||||
"duration": 11878
|
||||
"duration": 12533
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
||||
"duration": 20400
|
||||
"duration": 21197
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
||||
"duration": 3528
|
||||
"duration": 3455
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
40
docs/config/layouts.md
Normal file
40
docs/config/layouts.md
Normal file
@@ -0,0 +1,40 @@
|
||||
> **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,10 +10,6 @@
|
||||
|
||||
# mermaid
|
||||
|
||||
## Classes
|
||||
|
||||
- [UnknownDiagramError](classes/UnknownDiagramError.md)
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [DetailedError](interfaces/DetailedError.md)
|
||||
@@ -27,6 +23,7 @@
|
||||
- [RenderOptions](interfaces/RenderOptions.md)
|
||||
- [RenderResult](interfaces/RenderResult.md)
|
||||
- [RunOptions](interfaces/RunOptions.md)
|
||||
- [UnknownDiagramError](interfaces/UnknownDiagramError.md)
|
||||
|
||||
## Type Aliases
|
||||
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Class: UnknownDiagramError
|
||||
|
||||
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
|
||||
|
||||
## Extends
|
||||
|
||||
- `Error`
|
||||
|
||||
## Constructors
|
||||
|
||||
### new UnknownDiagramError()
|
||||
|
||||
> **new UnknownDiagramError**(`message`): [`UnknownDiagramError`](UnknownDiagramError.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/errors.ts:2](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L2)
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### message
|
||||
|
||||
`string`
|
||||
|
||||
#### Returns
|
||||
|
||||
[`UnknownDiagramError`](UnknownDiagramError.md)
|
||||
|
||||
#### Overrides
|
||||
|
||||
`Error.constructor`
|
||||
|
||||
## Properties
|
||||
|
||||
### cause?
|
||||
|
||||
> `optional` **cause**: `unknown`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.cause`
|
||||
|
||||
---
|
||||
|
||||
### message
|
||||
|
||||
> **message**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.message`
|
||||
|
||||
---
|
||||
|
||||
### name
|
||||
|
||||
> **name**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.name`
|
||||
|
||||
---
|
||||
|
||||
### stack?
|
||||
|
||||
> `optional` **stack**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.stack`
|
||||
|
||||
---
|
||||
|
||||
### prepareStackTrace()?
|
||||
|
||||
> `static` `optional` **prepareStackTrace**: (`err`, `stackTraces`) => `any`
|
||||
|
||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:143
|
||||
|
||||
Optional override for formatting stack traces
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### err
|
||||
|
||||
`Error`
|
||||
|
||||
##### stackTraces
|
||||
|
||||
`CallSite`\[]
|
||||
|
||||
#### Returns
|
||||
|
||||
`any`
|
||||
|
||||
#### See
|
||||
|
||||
<https://v8.dev/docs/stack-trace-api#customizing-stack-traces>
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.prepareStackTrace`
|
||||
|
||||
---
|
||||
|
||||
### stackTraceLimit
|
||||
|
||||
> `static` **stackTraceLimit**: `number`
|
||||
|
||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:145
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.stackTraceLimit`
|
||||
|
||||
## Methods
|
||||
|
||||
### captureStackTrace()
|
||||
|
||||
> `static` **captureStackTrace**(`targetObject`, `constructorOpt`?): `void`
|
||||
|
||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:136
|
||||
|
||||
Create .stack property on a target object
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### targetObject
|
||||
|
||||
`object`
|
||||
|
||||
##### constructorOpt?
|
||||
|
||||
`Function`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.captureStackTrace`
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: LayoutData
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L168)
|
||||
|
||||
## Indexable
|
||||
|
||||
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171)
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co
|
||||
|
||||
> **edges**: `Edge`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co
|
||||
|
||||
> **nodes**: `Node`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169)
|
||||
|
||||
@@ -32,7 +32,7 @@ page.
|
||||
|
||||
### detectType()
|
||||
|
||||
> **detectType**: (`text`, `config`?) => `string`
|
||||
> **detectType**: (`text`, `config?`) => `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449)
|
||||
|
||||
@@ -105,7 +105,7 @@ An array of objects with the id of the diagram.
|
||||
|
||||
### ~~init()~~
|
||||
|
||||
> **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`>
|
||||
> **init**: (`config?`, `nodes?`, `callback?`) => `Promise`<`void`>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
|
||||
|
||||
@@ -117,7 +117,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/
|
||||
|
||||
[`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
**Deprecated**, please set configuration in [initialize](Mermaid.md#initialize).
|
||||
**Deprecated**, please set configuration in [initialize](#initialize).
|
||||
|
||||
##### nodes?
|
||||
|
||||
@@ -141,13 +141,13 @@ Called once for each rendered diagram's id.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
|
||||
Use [initialize](#initialize) and [run](#run) instead.
|
||||
|
||||
Renders the mermaid diagrams
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
|
||||
Use [initialize](#initialize) and [run](#run) instead.
|
||||
|
||||
---
|
||||
|
||||
@@ -176,7 +176,7 @@ Configuration object for mermaid.
|
||||
|
||||
### ~~mermaidAPI~~
|
||||
|
||||
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
|
||||
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }; `render`: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
|
||||
|
||||
@@ -184,73 +184,81 @@ Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use [parse](Mermaid.md#parse) and [render](Mermaid.md#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
|
||||
Use [parse](#parse) and [render](#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
|
||||
|
||||
---
|
||||
|
||||
### parse()
|
||||
|
||||
> **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>
|
||||
> **parse**: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
|
||||
|
||||
#### Call Signature
|
||||
|
||||
> (`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
||||
|
||||
Parse the text and validate the syntax.
|
||||
|
||||
#### Parameters
|
||||
##### Parameters
|
||||
|
||||
##### text
|
||||
###### text
|
||||
|
||||
`string`
|
||||
|
||||
The mermaid diagram definition.
|
||||
|
||||
##### parseOptions
|
||||
###### parseOptions
|
||||
|
||||
[`ParseOptions`](ParseOptions.md) & `object`
|
||||
|
||||
Options for parsing.
|
||||
|
||||
#### Returns
|
||||
##### Returns
|
||||
|
||||
`Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
||||
|
||||
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
||||
|
||||
#### See
|
||||
##### See
|
||||
|
||||
[ParseOptions](ParseOptions.md)
|
||||
|
||||
#### Throws
|
||||
##### Throws
|
||||
|
||||
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
||||
|
||||
#### Call Signature
|
||||
|
||||
> (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>
|
||||
|
||||
Parse the text and validate the syntax.
|
||||
|
||||
#### Parameters
|
||||
##### Parameters
|
||||
|
||||
##### text
|
||||
###### text
|
||||
|
||||
`string`
|
||||
|
||||
The mermaid diagram definition.
|
||||
|
||||
##### parseOptions?
|
||||
###### parseOptions?
|
||||
|
||||
[`ParseOptions`](ParseOptions.md)
|
||||
|
||||
Options for parsing.
|
||||
|
||||
#### Returns
|
||||
##### Returns
|
||||
|
||||
`Promise`<[`ParseResult`](ParseResult.md)>
|
||||
|
||||
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
||||
|
||||
#### See
|
||||
##### See
|
||||
|
||||
[ParseOptions](ParseOptions.md)
|
||||
|
||||
#### Throws
|
||||
##### Throws
|
||||
|
||||
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
||||
|
||||
@@ -332,7 +340,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/
|
||||
|
||||
### render()
|
||||
|
||||
> **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>
|
||||
> **render**: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **suppressErrors**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
|
||||
|
||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||
The `parseError` function will not be called.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
|
||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mer
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
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:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104)
|
||||
|
||||
The config passed as YAML frontmatter or directives
|
||||
|
||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
|
||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/me
|
||||
|
||||
> `optional` **bindFunctions**: (`element`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
|
||||
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132)
|
||||
|
||||
Bind function to be called after the svg has been inserted into the DOM.
|
||||
This is necessary for adding event listeners to the elements in the svg.
|
||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
> **svg**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||
|
||||
The svg code for the rendered graph.
|
||||
|
||||
65
docs/config/setup/mermaid/interfaces/UnknownDiagramError.md
Normal file
65
docs/config/setup/mermaid/interfaces/UnknownDiagramError.md
Normal file
@@ -0,0 +1,65 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Interface: UnknownDiagramError
|
||||
|
||||
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
|
||||
|
||||
## Extends
|
||||
|
||||
- `Error`
|
||||
|
||||
## Properties
|
||||
|
||||
### cause?
|
||||
|
||||
> `optional` **cause**: `unknown`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.cause`
|
||||
|
||||
---
|
||||
|
||||
### message
|
||||
|
||||
> **message**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.message`
|
||||
|
||||
---
|
||||
|
||||
### name
|
||||
|
||||
> **name**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.name`
|
||||
|
||||
---
|
||||
|
||||
### stack?
|
||||
|
||||
> `optional` **stack**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.stack`
|
||||
@@ -10,6 +10,6 @@
|
||||
|
||||
# Type Alias: InternalHelpers
|
||||
|
||||
> **InternalHelpers**: _typeof_ `internalHelpers`
|
||||
> **InternalHelpers** = _typeof_ `internalHelpers`
|
||||
|
||||
Defined in: [packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# Type Alias: ParseErrorFunction()
|
||||
|
||||
> **ParseErrorFunction**: (`err`, `hash`?) => `void`
|
||||
> **ParseErrorFunction** = (`err`, `hash?`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
|
||||
# Type Alias: SVG
|
||||
|
||||
> **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
> **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
|
||||
# Type Alias: SVGGroup
|
||||
|
||||
> **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
> **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)
|
||||
|
||||
89
docs/config/tidy-tree.md
Normal file
89
docs/config/tidy-tree.md
Normal file
@@ -0,0 +1,89 @@
|
||||
> **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.
|
||||
@@ -326,7 +326,9 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
|
||||
|
||||
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Bang | Bang | `bang` | Bang | `bang` |
|
||||
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
|
||||
| Cloud | Cloud | `cloud` | cloud | `cloud` |
|
||||
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
|
||||
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
|
||||
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
|
||||
|
||||
@@ -314,3 +314,22 @@ You can also refer the [implementation in the live editor](https://github.com/me
|
||||
cspell:locale en,en-gb
|
||||
cspell:ignore Buzan
|
||||
--->
|
||||
|
||||
## Layouts
|
||||
|
||||
Mermaid also supports a Tidy Tree layout for mindmaps.
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
```
|
||||
|
||||
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)
|
||||
|
||||
@@ -138,7 +138,7 @@ xychart
|
||||
|
||||
## Chart Theme Variables
|
||||
|
||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
||||
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -163,6 +163,52 @@ config:
|
||||
| yAxisLineColor | Color of the y-axis line |
|
||||
| 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
|
||||
|
||||
```mermaid-example
|
||||
|
||||
@@ -17,6 +17,7 @@ export default tseslint.config(
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
ignores: [
|
||||
'**/*.d.ts',
|
||||
'**/dist/',
|
||||
'**/node_modules/',
|
||||
'.git/',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
|
||||
|
||||
> [!NOTE]
|
||||
> [!NOTE]
|
||||
> The ELK Layout engine will not be available in all providers that support mermaid by default.
|
||||
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
|
||||
|
||||
@@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts);
|
||||
- `elk.mrtree`: Multi-root tree layout
|
||||
- `elk.sporeOverlap`: Spore overlap layout
|
||||
|
||||
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively -->
|
||||
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively. -->
|
||||
|
||||
9
packages/mermaid-layout-elk/src/find-common-ancestor.d.ts
vendored
Normal file
9
packages/mermaid-layout-elk/src/find-common-ancestor.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface TreeData {
|
||||
parentById: Record<string, string>;
|
||||
childrenById: Record<string, string[]>;
|
||||
}
|
||||
export declare const findCommonAncestor: (
|
||||
id1: string,
|
||||
id2: string,
|
||||
{ parentById }: TreeData
|
||||
) => string;
|
||||
@@ -4,7 +4,8 @@ import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from '
|
||||
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
|
||||
|
||||
type Node = LayoutData['nodes'][number];
|
||||
|
||||
// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers
|
||||
const epsilon = 0.0001;
|
||||
interface LabelData {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -13,11 +14,20 @@ interface LabelData {
|
||||
}
|
||||
|
||||
interface NodeWithVertex extends Omit<Node, 'domId'> {
|
||||
children?: unknown[];
|
||||
children?: LayoutData['nodes'];
|
||||
labelData?: LabelData;
|
||||
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
function distance(p1?: Point, p2?: Point): number {
|
||||
if (!p1 || !p2) {
|
||||
return 0;
|
||||
}
|
||||
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
||||
}
|
||||
export const render = async (
|
||||
data4Layout: LayoutData,
|
||||
svg: SVG,
|
||||
@@ -51,15 +61,30 @@ export const render = async (
|
||||
|
||||
// Add the element to the DOM
|
||||
if (!node.isGroup) {
|
||||
// Create a clean node object for ELK with only the properties it expects
|
||||
const child: NodeWithVertex = {
|
||||
...node,
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
// Store the original node data for later use
|
||||
label: node.label,
|
||||
isGroup: node.isGroup,
|
||||
shape: node.shape,
|
||||
padding: node.padding,
|
||||
cssClasses: node.cssClasses,
|
||||
cssStyles: node.cssStyles,
|
||||
look: node.look,
|
||||
// Include parentId for subgraph processing
|
||||
parentId: node.parentId,
|
||||
};
|
||||
graph.children.push(child);
|
||||
nodeDb[node.id] = child;
|
||||
|
||||
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
|
||||
const boundingBox = childNodeEl.node()!.getBBox();
|
||||
// Store the domId separately for rendering, not in the ELK graph
|
||||
child.domId = childNodeEl;
|
||||
child.calcIntersect = node.calcIntersect;
|
||||
child.width = boundingBox.width;
|
||||
child.height = boundingBox.height;
|
||||
} else {
|
||||
@@ -459,302 +484,6 @@ export const render = async (
|
||||
}
|
||||
}
|
||||
|
||||
function intersectLine(
|
||||
p1: { y: number; x: number },
|
||||
p2: { y: number; x: number },
|
||||
q1: { x: any; y: any },
|
||||
q2: { x: any; y: any }
|
||||
) {
|
||||
log.debug('UIO intersectLine', p1, p2, q1, q2);
|
||||
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||
// p7 and p473.
|
||||
|
||||
// let a1, a2, b1, b2, c1, c2;
|
||||
// let r1, r2, r3, r4;
|
||||
// let denom, offset, num;
|
||||
// let x, y;
|
||||
|
||||
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||
// b1 y + c1 = 0.
|
||||
const a1 = p2.y - p1.y;
|
||||
const b1 = p1.x - p2.x;
|
||||
const c1 = p2.x * p1.y - p1.x * p2.y;
|
||||
|
||||
// Compute r3 and r4.
|
||||
const r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||
const r4 = a1 * q2.x + b1 * q2.y + c1;
|
||||
|
||||
const epsilon = 1e-6;
|
||||
|
||||
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
||||
// same side of line 1, the line segments do not intersect.
|
||||
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
||||
return /*DON'T_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
||||
const a2 = q2.y - q1.y;
|
||||
const b2 = q1.x - q2.x;
|
||||
const c2 = q2.x * q1.y - q1.x * q2.y;
|
||||
|
||||
// Compute r1 and r2
|
||||
const r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||
const r2 = a2 * p2.x + b2 * p2.y + c2;
|
||||
|
||||
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
||||
// on same side of second line segment, the line segments do
|
||||
// not intersect.
|
||||
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
|
||||
return /*DON'T_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Line segments intersect: compute intersection point.
|
||||
const denom = a1 * b2 - a2 * b1;
|
||||
if (denom === 0) {
|
||||
return /*COLLINEAR*/;
|
||||
}
|
||||
|
||||
const offset = Math.abs(denom / 2);
|
||||
|
||||
// The denom/2 is to get rounding instead of truncating. It
|
||||
// is added or subtracted to the numerator, depending upon the
|
||||
// sign of the numerator.
|
||||
let num = b1 * c2 - b2 * c1;
|
||||
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
num = a2 * c1 - a1 * c2;
|
||||
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
return { x: x, y: y };
|
||||
}
|
||||
|
||||
function sameSign(r1: number, r2: number) {
|
||||
return r1 * r2 > 0;
|
||||
}
|
||||
const diamondIntersection = (
|
||||
bounds: { x: any; y: any; width: any; height: any },
|
||||
outsidePoint: { x: number; y: number },
|
||||
insidePoint: any
|
||||
) => {
|
||||
const x1 = bounds.x;
|
||||
const y1 = bounds.y;
|
||||
|
||||
const w = bounds.width; //+ bounds.padding;
|
||||
const h = bounds.height; // + bounds.padding;
|
||||
|
||||
const polyPoints = [
|
||||
{ x: x1, y: y1 - h / 2 },
|
||||
{ x: x1 + w / 2, y: y1 },
|
||||
{ x: x1, y: y1 + h / 2 },
|
||||
{ x: x1 - w / 2, y: y1 },
|
||||
];
|
||||
log.debug(
|
||||
`APA16 diamondIntersection calc abc89:
|
||||
outsidePoint: ${JSON.stringify(outsidePoint)}
|
||||
insidePoint : ${JSON.stringify(insidePoint)}
|
||||
node-bounds : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
|
||||
JSON.stringify(polyPoints)
|
||||
);
|
||||
|
||||
const intersections = [];
|
||||
|
||||
let minX = Number.POSITIVE_INFINITY;
|
||||
let minY = Number.POSITIVE_INFINITY;
|
||||
|
||||
polyPoints.forEach(function (entry) {
|
||||
minX = Math.min(minX, entry.x);
|
||||
minY = Math.min(minY, entry.y);
|
||||
});
|
||||
|
||||
const left = x1 - w / 2 - minX;
|
||||
const top = y1 - h / 2 - minY;
|
||||
|
||||
for (let i = 0; i < polyPoints.length; i++) {
|
||||
const p1 = polyPoints[i];
|
||||
const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
|
||||
const intersect = intersectLine(
|
||||
bounds,
|
||||
outsidePoint,
|
||||
{ x: left + p1.x, y: top + p1.y },
|
||||
{ x: left + p2.x, y: top + p2.y }
|
||||
);
|
||||
|
||||
if (intersect) {
|
||||
intersections.push(intersect);
|
||||
}
|
||||
}
|
||||
|
||||
if (!intersections.length) {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
log.debug('UIO intersections', intersections);
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// More intersections, find the one nearest to edge end point
|
||||
intersections.sort(function (p, q) {
|
||||
const pdx = p.x - outsidePoint.x;
|
||||
const pdy = p.y - outsidePoint.y;
|
||||
const distp = Math.sqrt(pdx * pdx + pdy * pdy);
|
||||
|
||||
const qdx = q.x - outsidePoint.x;
|
||||
const qdy = q.y - outsidePoint.y;
|
||||
const distq = Math.sqrt(qdx * qdx + qdy * qdy);
|
||||
|
||||
return distp < distq ? -1 : distp === distq ? 0 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
return intersections[0];
|
||||
};
|
||||
|
||||
const intersection = (
|
||||
node: { x: any; y: any; width: number; height: number },
|
||||
outsidePoint: { x: number; y: number },
|
||||
insidePoint: { x: number; y: number }
|
||||
) => {
|
||||
log.debug(`intersection calc abc89:
|
||||
outsidePoint: ${JSON.stringify(outsidePoint)}
|
||||
insidePoint : ${JSON.stringify(insidePoint)}
|
||||
node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`);
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
|
||||
const dx = Math.abs(x - insidePoint.x);
|
||||
// const dy = Math.abs(y - insidePoint.y);
|
||||
const w = node.width / 2;
|
||||
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||
const h = node.height / 2;
|
||||
|
||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
|
||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||
r = (R * q) / Q;
|
||||
const res = {
|
||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
|
||||
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
|
||||
};
|
||||
|
||||
if (r === 0) {
|
||||
res.x = outsidePoint.x;
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
if (R === 0) {
|
||||
res.x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
|
||||
log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line
|
||||
|
||||
return res;
|
||||
} else {
|
||||
// Intersection on sides of rect
|
||||
if (insidePoint.x < outsidePoint.x) {
|
||||
r = outsidePoint.x - w - x;
|
||||
} else {
|
||||
// r = outsidePoint.x - w - x;
|
||||
r = x - w - outsidePoint.x;
|
||||
}
|
||||
const q = (Q * r) / R;
|
||||
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w;
|
||||
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
||||
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||
// let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
||||
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||
log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
|
||||
if (r === 0) {
|
||||
_x = outsidePoint.x;
|
||||
_y = outsidePoint.y;
|
||||
}
|
||||
if (R === 0) {
|
||||
_x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
_y = outsidePoint.y;
|
||||
}
|
||||
|
||||
return { x: _x, y: _y };
|
||||
}
|
||||
};
|
||||
const outsideNode = (
|
||||
node: { x: any; y: any; width: number; height: number },
|
||||
point: { x: number; y: number }
|
||||
) => {
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
const dx = Math.abs(point.x - x);
|
||||
const dy = Math.abs(point.y - y);
|
||||
const w = node.width / 2;
|
||||
const h = node.height / 2;
|
||||
if (dx >= w || dy >= h) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* This function will page a path and node where the last point(s) in the path is inside the node
|
||||
* and return an update path ending by the border of the node.
|
||||
*/
|
||||
const cutPathAtIntersect = (
|
||||
_points: any[],
|
||||
bounds: { x: any; y: any; width: any; height: any; padding: any },
|
||||
isDiamond: boolean
|
||||
) => {
|
||||
log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
|
||||
const points: any[] = [];
|
||||
let lastPointOutside = _points[0];
|
||||
let isInside = false;
|
||||
_points.forEach((point: any) => {
|
||||
// check if point is inside the boundary rect
|
||||
if (!outsideNode(bounds, point) && !isInside) {
|
||||
// First point inside the rect found
|
||||
// Calc the intersection coord between the point and the last point outside the rect
|
||||
let inter;
|
||||
|
||||
if (isDiamond) {
|
||||
const inter2 = diamondIntersection(bounds, lastPointOutside, point);
|
||||
const distance = Math.sqrt(
|
||||
(lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2
|
||||
);
|
||||
if (distance > 1) {
|
||||
inter = inter2;
|
||||
}
|
||||
}
|
||||
if (!inter) {
|
||||
inter = intersection(bounds, lastPointOutside, point);
|
||||
}
|
||||
|
||||
// Check case where the intersection is the same as the last point
|
||||
let pointPresent = false;
|
||||
points.forEach((p) => {
|
||||
pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
|
||||
});
|
||||
// if (!pointPresent) {
|
||||
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
|
||||
points.push(inter);
|
||||
} else {
|
||||
log.debug('abc88 no intersect', inter, points);
|
||||
}
|
||||
// points.push(inter);
|
||||
isInside = true;
|
||||
} else {
|
||||
// Outside
|
||||
log.debug('abc88 outside', point, lastPointOutside, points);
|
||||
lastPointOutside = point;
|
||||
// points.push(point);
|
||||
if (!isInside) {
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
});
|
||||
return points;
|
||||
};
|
||||
|
||||
// @ts-ignore - ELK is not typed
|
||||
const elk = new ELK();
|
||||
const element = svg.select('g');
|
||||
@@ -869,11 +598,16 @@ export const render = async (
|
||||
delete node.height;
|
||||
}
|
||||
});
|
||||
elkGraph.edges.forEach((edge: any) => {
|
||||
log.debug('APA01 processing edges, count:', elkGraph.edges.length);
|
||||
elkGraph.edges.forEach((edge: any, index: number) => {
|
||||
log.debug('APA01 processing edge', index, ':', edge);
|
||||
const source = edge.sources[0];
|
||||
const target = edge.targets[0];
|
||||
log.debug('APA01 source:', source, 'target:', target);
|
||||
log.debug('APA01 nodeDb[source]:', nodeDb[source]);
|
||||
log.debug('APA01 nodeDb[target]:', nodeDb[target]);
|
||||
|
||||
if (nodeDb[source].parentId !== nodeDb[target].parentId) {
|
||||
if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) {
|
||||
const ancestorId = findCommonAncestor(source, target, parentLookupDb);
|
||||
// an edge that breaks a subgraph has been identified, set configuration accordingly
|
||||
setIncludeChildrenPolicy(source, ancestorId);
|
||||
@@ -881,7 +615,37 @@ export const render = async (
|
||||
}
|
||||
});
|
||||
|
||||
const g = await elk.layout(elkGraph);
|
||||
log.debug('APA01 before');
|
||||
log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2));
|
||||
log.debug('APA01 elkGraph.children length:', elkGraph.children?.length);
|
||||
log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length);
|
||||
|
||||
// Validate that all edge references exist as nodes
|
||||
elkGraph.edges?.forEach((edge: any, index: number) => {
|
||||
log.debug(`APA01 validating edge ${index}:`, edge);
|
||||
if (edge.sources) {
|
||||
edge.sources.forEach((sourceId: any) => {
|
||||
const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId);
|
||||
log.debug(`APA01 source ${sourceId} exists:`, sourceExists);
|
||||
});
|
||||
}
|
||||
if (edge.targets) {
|
||||
edge.targets.forEach((targetId: any) => {
|
||||
const targetExists = elkGraph.children?.some((child: any) => child.id === targetId);
|
||||
log.debug(`APA01 target ${targetId} exists:`, targetExists);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let g;
|
||||
try {
|
||||
g = await elk.layout(elkGraph);
|
||||
log.debug('APA01 after - success');
|
||||
log.debug('APA01 layout result:', JSON.stringify(g, null, 2));
|
||||
} catch (error) {
|
||||
log.error('APA01 ELK layout error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// debugger;
|
||||
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
|
||||
@@ -969,42 +733,37 @@ export const render = async (
|
||||
startNode.innerHTML
|
||||
);
|
||||
}
|
||||
if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
|
||||
edge.points.unshift({
|
||||
x: startNode.offset.posX + startNode.width / 2,
|
||||
y: startNode.offset.posY + startNode.height / 2,
|
||||
});
|
||||
}
|
||||
if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
|
||||
edge.points.push({
|
||||
x: endNode.offset.posX + endNode.width / 2,
|
||||
y: endNode.offset.posY + endNode.height / 2,
|
||||
});
|
||||
}
|
||||
|
||||
edge.points = cutPathAtIntersect(
|
||||
edge.points.reverse(),
|
||||
{
|
||||
x: startNode.offset.posX + startNode.width / 2,
|
||||
y: startNode.offset.posY + startNode.height / 2,
|
||||
width: sw,
|
||||
height: startNode.height,
|
||||
padding: startNode.padding,
|
||||
},
|
||||
startNode.shape === 'diamond' || startNode.shape === 'diam'
|
||||
).reverse();
|
||||
if (startNode.calcIntersect) {
|
||||
const intersection = startNode.calcIntersect(
|
||||
{
|
||||
x: startNode.offset.posX + startNode.width / 2,
|
||||
y: startNode.offset.posY + startNode.height / 2,
|
||||
width: startNode.width,
|
||||
height: startNode.height,
|
||||
},
|
||||
edge.points[0]
|
||||
);
|
||||
|
||||
edge.points = cutPathAtIntersect(
|
||||
edge.points,
|
||||
{
|
||||
x: endNode.offset.posX + endNode.width / 2,
|
||||
y: endNode.offset.posY + endNode.height / 2,
|
||||
width: ew,
|
||||
height: endNode.height,
|
||||
padding: endNode.padding,
|
||||
},
|
||||
endNode.shape === 'diamond' || endNode.shape === 'diam'
|
||||
);
|
||||
if (distance(intersection, edge.points[0]) > epsilon) {
|
||||
edge.points.unshift(intersection);
|
||||
}
|
||||
}
|
||||
if (endNode.calcIntersect) {
|
||||
const intersection = endNode.calcIntersect(
|
||||
{
|
||||
x: endNode.offset.posX + endNode.width / 2,
|
||||
y: endNode.offset.posY + endNode.height / 2,
|
||||
width: endNode.width,
|
||||
height: endNode.height,
|
||||
},
|
||||
edge.points[edge.points.length - 1]
|
||||
);
|
||||
|
||||
if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) {
|
||||
edge.points.push(intersection);
|
||||
}
|
||||
}
|
||||
|
||||
const paths = insertEdge(
|
||||
edgesEl,
|
||||
@@ -1015,7 +774,6 @@ export const render = async (
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
log.info('APA12 edge points after insert', JSON.stringify(edge.points));
|
||||
|
||||
edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
|
||||
edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
||||
|
||||
59
packages/mermaid-layout-tidy-tree/README.md
Normal file
59
packages/mermaid-layout-tidy-tree/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# @mermaid-js/layout-tidy-tree
|
||||
|
||||
This package provides a bidirectional tidy tree layout engine for Mermaid based on the non-layered-tidy-tree-layout algorithm.
|
||||
|
||||
> [!NOTE]
|
||||
> The Tidy Tree Layout engine will not be available in all providers that support mermaid by default.
|
||||
> The websites will have to install the @mermaid-js/layout-tidy-tree package to use the Tidy Tree layout engine.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
```
|
||||
|
||||
### With bundlers
|
||||
|
||||
```sh
|
||||
npm install @mermaid-js/layout-tidy-tree
|
||||
```
|
||||
|
||||
```ts
|
||||
import mermaid from 'mermaid';
|
||||
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
|
||||
|
||||
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
||||
```
|
||||
|
||||
### With CDN
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
|
||||
|
||||
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Tidy Tree Layout Overview
|
||||
|
||||
tidy-tree: The bidirectional tidy tree layout
|
||||
|
||||
The bidirectional tidy tree layout algorithm creates two separate trees that grow horizontally in opposite directions from a central root node:
|
||||
Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
|
||||
Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
|
||||
|
||||
This creates a balanced, symmetric layout that is ideal for mindmaps, organizational charts, and other tree-based diagrams.
|
||||
|
||||
Layout Structure:
|
||||
[Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
|
||||
↓ ↓ ↓ ↓
|
||||
[GrandChild] [GrandChild] [GrandChild] [GrandChild]
|
||||
46
packages/mermaid-layout-tidy-tree/package.json
Normal file
46
packages/mermaid-layout-tidy-tree/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@mermaid-js/layout-tidy-tree",
|
||||
"version": "0.1.0",
|
||||
"description": "Tidy-tree layout engine for mermaid",
|
||||
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
|
||||
"types": "dist/layouts.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-layout-tidy-tree.core.mjs",
|
||||
"types": "./dist/layouts.d.ts"
|
||||
},
|
||||
"./": "./"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"tidy-tree",
|
||||
"mermaid",
|
||||
"layout"
|
||||
],
|
||||
"scripts": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"contributors": [
|
||||
"Knut Sveidqvist",
|
||||
"Sidharth Vinod"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mermaid": "^11.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
50
packages/mermaid-layout-tidy-tree/src/index.ts
Normal file
50
packages/mermaid-layout-tidy-tree/src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Bidirectional Tidy-Tree Layout Algorithm for Generic Diagrams
|
||||
*
|
||||
* This module provides a layout algorithm implementation using the
|
||||
* non-layered-tidy-tree-layout algorithm for positioning nodes and edges
|
||||
* in tree structures with a bidirectional approach.
|
||||
*
|
||||
* The algorithm creates two separate trees that grow horizontally in opposite
|
||||
* directions from a central root node:
|
||||
* - Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
|
||||
* - Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
|
||||
*
|
||||
* This creates a balanced, symmetric layout that is ideal for mindmaps,
|
||||
* organizational charts, and other tree-based diagrams.
|
||||
*
|
||||
* The algorithm follows the unified rendering pattern and can be used
|
||||
* by any diagram type that provides compatible LayoutData.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render function for the bidirectional tidy-tree layout algorithm
|
||||
*
|
||||
* This function follows the unified rendering pattern used by all layout algorithms.
|
||||
* It takes LayoutData, inserts nodes into DOM, runs the bidirectional tidy-tree layout algorithm,
|
||||
* and renders the positioned elements to the SVG.
|
||||
*
|
||||
* Features:
|
||||
* - Alternates root children between left and right trees
|
||||
* - Left tree grows horizontally to the left (rotated 90° counterclockwise)
|
||||
* - Right tree grows horizontally to the right (rotated 90° clockwise)
|
||||
* - Uses tidy-tree algorithm for optimal spacing within each tree
|
||||
* - Creates symmetric, balanced layouts
|
||||
* - Maintains proper edge connections between all nodes
|
||||
*
|
||||
* Layout Structure:
|
||||
* ```
|
||||
* [Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
|
||||
* ↓ ↓ ↓ ↓
|
||||
* [GrandChild] [GrandChild] [GrandChild] [GrandChild]
|
||||
* ```
|
||||
*
|
||||
* @param layoutData - Layout data containing nodes, edges, and configuration
|
||||
* @param svg - SVG element to render to
|
||||
* @param helpers - Internal helper functions for rendering
|
||||
* @param options - Rendering options
|
||||
*/
|
||||
export { default } from './layouts.js';
|
||||
export * from './types.js';
|
||||
export * from './layout.js';
|
||||
export { render } from './render.js';
|
||||
409
packages/mermaid-layout-tidy-tree/src/layout.test.ts
Normal file
409
packages/mermaid-layout-tidy-tree/src/layout.test.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { executeTidyTreeLayout, validateLayoutData } from './layout.js';
|
||||
import type { LayoutResult } from './types.js';
|
||||
import type { LayoutData, MermaidConfig } from 'mermaid';
|
||||
|
||||
// Mock non-layered-tidy-tree-layout
|
||||
vi.mock('non-layered-tidy-tree-layout', () => ({
|
||||
BoundingBox: vi.fn().mockImplementation(() => ({})),
|
||||
Layout: vi.fn().mockImplementation(() => ({
|
||||
layout: vi.fn().mockImplementation((treeData) => {
|
||||
const result = { ...treeData };
|
||||
|
||||
if (result.id?.toString().startsWith('virtual-root')) {
|
||||
result.x = 0;
|
||||
result.y = 0;
|
||||
} else {
|
||||
result.x = 100;
|
||||
result.y = 50;
|
||||
}
|
||||
|
||||
if (result.children) {
|
||||
result.children.forEach((child: any, index: number) => {
|
||||
child.x = 50 + index * 100;
|
||||
child.y = 100;
|
||||
|
||||
if (child.children) {
|
||||
child.children.forEach((grandchild: any, gIndex: number) => {
|
||||
grandchild.x = 25 + gIndex * 50;
|
||||
grandchild.y = 200;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
result,
|
||||
boundingBox: {
|
||||
left: 0,
|
||||
right: 200,
|
||||
top: 0,
|
||||
bottom: 250,
|
||||
},
|
||||
};
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Tidy-Tree Layout Algorithm', () => {
|
||||
let mockConfig: MermaidConfig;
|
||||
let mockLayoutData: LayoutData;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
theme: 'default',
|
||||
} as MermaidConfig;
|
||||
|
||||
mockLayoutData = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Root',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 100,
|
||||
height: 50,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child1',
|
||||
label: 'Child 1',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child2',
|
||||
label: 'Child 2',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child3',
|
||||
label: 'Child 3',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child4',
|
||||
label: 'Child 4',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'root_child1',
|
||||
start: 'root',
|
||||
end: 'child1',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_child2',
|
||||
start: 'root',
|
||||
end: 'child2',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_child3',
|
||||
start: 'root',
|
||||
end: 'child3',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_child4',
|
||||
start: 'root',
|
||||
end: 'child4',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
],
|
||||
config: mockConfig,
|
||||
direction: 'TB',
|
||||
type: 'test',
|
||||
diagramId: 'test-diagram',
|
||||
markers: [],
|
||||
};
|
||||
});
|
||||
|
||||
describe('validateLayoutData', () => {
|
||||
it('should validate correct layout data', () => {
|
||||
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for missing data', () => {
|
||||
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
|
||||
});
|
||||
|
||||
it('should throw error for missing config', () => {
|
||||
const invalidData = { ...mockLayoutData, config: null as any };
|
||||
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
|
||||
});
|
||||
|
||||
it('should throw error for invalid nodes array', () => {
|
||||
const invalidData = { ...mockLayoutData, nodes: null as any };
|
||||
expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
|
||||
});
|
||||
|
||||
it('should throw error for invalid edges array', () => {
|
||||
const invalidData = { ...mockLayoutData, edges: null as any };
|
||||
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeTidyTreeLayout function', () => {
|
||||
it('should execute layout algorithm successfully', async () => {
|
||||
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toBeDefined();
|
||||
expect(result.edges).toBeDefined();
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(Array.isArray(result.edges)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return positioned nodes with coordinates', async () => {
|
||||
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result.nodes.length).toBeGreaterThan(0);
|
||||
result.nodes.forEach((node) => {
|
||||
expect(node.x).toBeDefined();
|
||||
expect(node.y).toBeDefined();
|
||||
expect(typeof node.x).toBe('number');
|
||||
expect(typeof node.y).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return positioned edges with coordinates', async () => {
|
||||
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result.edges.length).toBeGreaterThan(0);
|
||||
result.edges.forEach((edge) => {
|
||||
expect(edge.startX).toBeDefined();
|
||||
expect(edge.startY).toBeDefined();
|
||||
expect(edge.midX).toBeDefined();
|
||||
expect(edge.midY).toBeDefined();
|
||||
expect(edge.endX).toBeDefined();
|
||||
expect(edge.endY).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty layout data gracefully', async () => {
|
||||
const emptyData: LayoutData = {
|
||||
...mockLayoutData,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
await expect(executeTidyTreeLayout(emptyData)).rejects.toThrow(
|
||||
'No nodes found in layout data'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing nodes', async () => {
|
||||
const invalidData = { ...mockLayoutData, nodes: [] };
|
||||
|
||||
await expect(executeTidyTreeLayout(invalidData)).rejects.toThrow(
|
||||
'No nodes found in layout data'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty edges (single node tree)', async () => {
|
||||
const singleNodeData = {
|
||||
...mockLayoutData,
|
||||
edges: [],
|
||||
nodes: [mockLayoutData.nodes[0]],
|
||||
};
|
||||
|
||||
const result = await executeTidyTreeLayout(singleNodeData);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.edges).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should create bidirectional dual-tree layout with alternating left/right children', async () => {
|
||||
const result = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(5);
|
||||
|
||||
const rootNode = result.nodes.find((node) => node.id === 'root');
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootNode!.x).toBe(0);
|
||||
expect(rootNode!.y).toBe(20);
|
||||
|
||||
const child1 = result.nodes.find((node) => node.id === 'child1');
|
||||
const child2 = result.nodes.find((node) => node.id === 'child2');
|
||||
const child3 = result.nodes.find((node) => node.id === 'child3');
|
||||
const child4 = result.nodes.find((node) => node.id === 'child4');
|
||||
|
||||
expect(child1).toBeDefined();
|
||||
expect(child2).toBeDefined();
|
||||
expect(child3).toBeDefined();
|
||||
expect(child4).toBeDefined();
|
||||
|
||||
expect(child1!.x).toBeLessThan(rootNode!.x);
|
||||
expect(child2!.x).toBeGreaterThan(rootNode!.x);
|
||||
expect(child3!.x).toBeLessThan(rootNode!.x);
|
||||
expect(child4!.x).toBeGreaterThan(rootNode!.x);
|
||||
|
||||
expect(child1!.x).toBeLessThan(-100);
|
||||
expect(child3!.x).toBeLessThan(-100);
|
||||
|
||||
expect(child2!.x).toBeGreaterThan(100);
|
||||
expect(child4!.x).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it('should correctly transpose coordinates to prevent high nodes from covering nodes above them', async () => {
|
||||
const testData = {
|
||||
...mockLayoutData,
|
||||
nodes: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Root',
|
||||
isGroup: false,
|
||||
shape: 'rect' as const,
|
||||
width: 100,
|
||||
height: 50,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'tall-child',
|
||||
label: 'Tall Child',
|
||||
isGroup: false,
|
||||
shape: 'rect' as const,
|
||||
width: 80,
|
||||
height: 120,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'short-child',
|
||||
label: 'Short Child',
|
||||
isGroup: false,
|
||||
shape: 'rect' as const,
|
||||
width: 80,
|
||||
height: 30,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'root_tall',
|
||||
start: 'root',
|
||||
end: 'tall-child',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_short',
|
||||
start: 'root',
|
||||
end: 'short-child',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await executeTidyTreeLayout(testData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
|
||||
const rootNode = result.nodes.find((node) => node.id === 'root');
|
||||
const tallChild = result.nodes.find((node) => node.id === 'tall-child');
|
||||
const shortChild = result.nodes.find((node) => node.id === 'short-child');
|
||||
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(tallChild).toBeDefined();
|
||||
expect(shortChild).toBeDefined();
|
||||
|
||||
expect(tallChild!.x).not.toBe(shortChild!.x);
|
||||
|
||||
expect(tallChild!.width).toBe(80);
|
||||
expect(tallChild!.height).toBe(120);
|
||||
expect(shortChild!.width).toBe(80);
|
||||
expect(shortChild!.height).toBe(30);
|
||||
|
||||
const yDifference = Math.abs(tallChild!.y - shortChild!.y);
|
||||
expect(yDifference).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
629
packages/mermaid-layout-tidy-tree/src/layout.ts
Normal file
629
packages/mermaid-layout-tidy-tree/src/layout.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
import type { LayoutData } from 'mermaid';
|
||||
import type { Bounds, Point } from 'mermaid/src/types.js';
|
||||
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
|
||||
import type {
|
||||
Edge,
|
||||
LayoutResult,
|
||||
Node,
|
||||
PositionedEdge,
|
||||
PositionedNode,
|
||||
TidyTreeNode,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Execute the tidy-tree layout algorithm on generic layout data
|
||||
*
|
||||
* This function takes layout data and uses the non-layered-tidy-tree-layout
|
||||
* algorithm to calculate optimal node positions for tree structures.
|
||||
*
|
||||
* @param data - The layout data containing nodes, edges, and configuration
|
||||
* @param config - Mermaid configuration object
|
||||
* @returns Promise resolving to layout result with positioned nodes and edges
|
||||
*/
|
||||
export function executeTidyTreeLayout(data: LayoutData): Promise<LayoutResult> {
|
||||
let intersectionShift = 50;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) {
|
||||
throw new Error('No nodes found in layout data');
|
||||
}
|
||||
|
||||
if (!data.edges || !Array.isArray(data.edges)) {
|
||||
data.edges = [];
|
||||
}
|
||||
|
||||
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
|
||||
|
||||
const gap = 20;
|
||||
const bottomPadding = 40;
|
||||
intersectionShift = 30;
|
||||
|
||||
const bb = new BoundingBox(gap, bottomPadding);
|
||||
const layout = new Layout(bb);
|
||||
|
||||
let leftResult = null;
|
||||
let rightResult = null;
|
||||
|
||||
if (leftTree) {
|
||||
const leftLayoutResult = layout.layout(leftTree);
|
||||
leftResult = leftLayoutResult.result;
|
||||
}
|
||||
|
||||
if (rightTree) {
|
||||
const rightLayoutResult = layout.layout(rightTree);
|
||||
rightResult = rightLayoutResult.result;
|
||||
}
|
||||
|
||||
const positionedNodes = combineAndPositionTrees(rootNode, leftResult, rightResult);
|
||||
const positionedEdges = calculateEdgePositions(
|
||||
data.edges,
|
||||
positionedNodes,
|
||||
intersectionShift
|
||||
);
|
||||
resolve({
|
||||
nodes: positionedNodes,
|
||||
edges: positionedEdges,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LayoutData to dual-tree format (left and right trees)
|
||||
*
|
||||
* This function builds two separate tree structures from the nodes and edges,
|
||||
* alternating children between left and right trees.
|
||||
*/
|
||||
function convertToDualTreeFormat(data: LayoutData): {
|
||||
leftTree: TidyTreeNode | null;
|
||||
rightTree: TidyTreeNode | null;
|
||||
rootNode: TidyTreeNode;
|
||||
} {
|
||||
const { nodes, edges } = data;
|
||||
|
||||
const nodeMap = new Map<string, Node>();
|
||||
nodes.forEach((node) => nodeMap.set(node.id, node));
|
||||
|
||||
const children = new Map<string, string[]>();
|
||||
const parents = new Map<string, string>();
|
||||
|
||||
edges.forEach((edge) => {
|
||||
const parentId = edge.start;
|
||||
const childId = edge.end;
|
||||
|
||||
if (parentId && childId) {
|
||||
if (!children.has(parentId)) {
|
||||
children.set(parentId, []);
|
||||
}
|
||||
children.get(parentId)!.push(childId);
|
||||
parents.set(childId, parentId);
|
||||
}
|
||||
});
|
||||
|
||||
const rootNodeData = nodes.find((node) => !parents.has(node.id));
|
||||
if (!rootNodeData && nodes.length === 0) {
|
||||
throw new Error('No nodes available to create tree');
|
||||
}
|
||||
|
||||
const actualRoot = rootNodeData ?? nodes[0];
|
||||
|
||||
const rootNode: TidyTreeNode = {
|
||||
id: actualRoot.id,
|
||||
width: actualRoot.width ?? 100,
|
||||
height: actualRoot.height ?? 50,
|
||||
_originalNode: actualRoot,
|
||||
};
|
||||
|
||||
const rootChildren = children.get(actualRoot.id) ?? [];
|
||||
const leftChildren: string[] = [];
|
||||
const rightChildren: string[] = [];
|
||||
|
||||
rootChildren.forEach((childId, index) => {
|
||||
if (index % 2 === 0) {
|
||||
leftChildren.push(childId);
|
||||
} else {
|
||||
rightChildren.push(childId);
|
||||
}
|
||||
});
|
||||
|
||||
const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null;
|
||||
|
||||
const rightTree =
|
||||
rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null;
|
||||
|
||||
return { leftTree, rightTree, rootNode };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a subtree from a list of root children
|
||||
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
|
||||
*/
|
||||
function buildSubTree(
|
||||
rootChildren: string[],
|
||||
children: Map<string, string[]>,
|
||||
nodeMap: Map<string, Node>
|
||||
): TidyTreeNode {
|
||||
const virtualRoot: TidyTreeNode = {
|
||||
id: `virtual-root-${Math.random()}`,
|
||||
width: 1,
|
||||
height: 1,
|
||||
children: rootChildren
|
||||
.map((childId) => nodeMap.get(childId))
|
||||
.filter((child): child is Node => child !== undefined)
|
||||
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
|
||||
};
|
||||
|
||||
return virtualRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively convert a node and its children to tidy-tree format
|
||||
* This version transposes width/height for horizontal tree layout
|
||||
*/
|
||||
function convertNodeToTidyTreeTransposed(
|
||||
node: Node,
|
||||
children: Map<string, string[]>,
|
||||
nodeMap: Map<string, Node>
|
||||
): TidyTreeNode {
|
||||
const childIds = children.get(node.id) ?? [];
|
||||
const childNodes = childIds
|
||||
.map((childId) => nodeMap.get(childId))
|
||||
.filter((child): child is Node => child !== undefined)
|
||||
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
width: node.height ?? 50,
|
||||
height: node.width ?? 100,
|
||||
children: childNodes.length > 0 ? childNodes : undefined,
|
||||
_originalNode: node,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Combine and position the left and right trees around the root node
|
||||
* Creates a bidirectional layout where left tree grows left and right tree grows right
|
||||
*/
|
||||
function combineAndPositionTrees(
|
||||
rootNode: TidyTreeNode,
|
||||
leftResult: TidyTreeNode | null,
|
||||
rightResult: TidyTreeNode | null
|
||||
): PositionedNode[] {
|
||||
const positionedNodes: PositionedNode[] = [];
|
||||
|
||||
const rootX = 0;
|
||||
const rootY = 0;
|
||||
|
||||
const treeSpacing = rootNode.width / 2 + 30;
|
||||
const leftTreeNodes: PositionedNode[] = [];
|
||||
const rightTreeNodes: PositionedNode[] = [];
|
||||
|
||||
if (leftResult?.children) {
|
||||
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
|
||||
}
|
||||
|
||||
if (rightResult?.children) {
|
||||
positionRightTreeBidirectional(
|
||||
rightResult.children,
|
||||
rightTreeNodes,
|
||||
rootX + treeSpacing,
|
||||
rootY
|
||||
);
|
||||
}
|
||||
|
||||
let leftTreeCenterY = 0;
|
||||
let rightTreeCenterY = 0;
|
||||
|
||||
if (leftTreeNodes.length > 0) {
|
||||
const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
|
||||
(a, b) => b - a
|
||||
);
|
||||
const firstLevelLeftX = leftTreeXPositions[0];
|
||||
const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
|
||||
|
||||
if (firstLevelLeftNodes.length > 0) {
|
||||
const leftMinY = Math.min(
|
||||
...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2)
|
||||
);
|
||||
const leftMaxY = Math.max(
|
||||
...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2)
|
||||
);
|
||||
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (rightTreeNodes.length > 0) {
|
||||
const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
|
||||
(a, b) => a - b
|
||||
);
|
||||
const firstLevelRightX = rightTreeXPositions[0];
|
||||
const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
|
||||
|
||||
if (firstLevelRightNodes.length > 0) {
|
||||
const rightMinY = Math.min(
|
||||
...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2)
|
||||
);
|
||||
const rightMaxY = Math.max(
|
||||
...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2)
|
||||
);
|
||||
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
const leftTreeOffset = -leftTreeCenterY;
|
||||
const rightTreeOffset = -rightTreeCenterY;
|
||||
|
||||
positionedNodes.push({
|
||||
id: String(rootNode.id),
|
||||
x: rootX,
|
||||
y: rootY + 20,
|
||||
section: 'root',
|
||||
width: rootNode._originalNode?.width ?? rootNode.width,
|
||||
height: rootNode._originalNode?.height ?? rootNode.height,
|
||||
originalNode: rootNode._originalNode,
|
||||
});
|
||||
|
||||
const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({
|
||||
id: node.id,
|
||||
x: node.x - (node.width ?? 0) / 2,
|
||||
y: node.y + leftTreeOffset + (node.height ?? 0) / 2,
|
||||
section: 'left' as const,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
originalNode: node.originalNode,
|
||||
}));
|
||||
|
||||
const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({
|
||||
id: node.id,
|
||||
x: node.x + (node.width ?? 0) / 2,
|
||||
y: node.y + rightTreeOffset + (node.height ?? 0) / 2,
|
||||
section: 'right' as const,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
originalNode: node.originalNode,
|
||||
}));
|
||||
|
||||
positionedNodes.push(...leftTreeNodesWithOffset);
|
||||
positionedNodes.push(...rightTreeNodesWithOffset);
|
||||
|
||||
return positionedNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position nodes from the left tree in a bidirectional layout (grows to the left)
|
||||
* Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left
|
||||
*/
|
||||
function positionLeftTreeBidirectional(
|
||||
nodes: TidyTreeNode[],
|
||||
positionedNodes: PositionedNode[],
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): void {
|
||||
nodes.forEach((node) => {
|
||||
const distanceFromRoot = node.y ?? 0;
|
||||
const verticalPosition = node.x ?? 0;
|
||||
|
||||
const originalWidth = node._originalNode?.width ?? 100;
|
||||
const originalHeight = node._originalNode?.height ?? 50;
|
||||
|
||||
const adjustedY = offsetY + verticalPosition;
|
||||
|
||||
positionedNodes.push({
|
||||
id: String(node.id),
|
||||
x: offsetX - distanceFromRoot,
|
||||
y: adjustedY,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
originalNode: node._originalNode,
|
||||
});
|
||||
|
||||
if (node.children) {
|
||||
positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Position nodes from the right tree in a bidirectional layout (grows to the right)
|
||||
* Rotates the tree 90 degrees clockwise so it grows horizontally to the right
|
||||
*/
|
||||
function positionRightTreeBidirectional(
|
||||
nodes: TidyTreeNode[],
|
||||
positionedNodes: PositionedNode[],
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): void {
|
||||
nodes.forEach((node) => {
|
||||
const distanceFromRoot = node.y ?? 0;
|
||||
const verticalPosition = node.x ?? 0;
|
||||
|
||||
const originalWidth = node._originalNode?.width ?? 100;
|
||||
const originalHeight = node._originalNode?.height ?? 50;
|
||||
|
||||
const adjustedY = offsetY + verticalPosition;
|
||||
|
||||
positionedNodes.push({
|
||||
id: String(node.id),
|
||||
x: offsetX + distanceFromRoot,
|
||||
y: adjustedY,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
originalNode: node._originalNode,
|
||||
});
|
||||
|
||||
if (node.children) {
|
||||
positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the intersection point of a line with a circle
|
||||
* @param circle - Circle coordinates and radius
|
||||
* @param lineStart - Starting point of the line
|
||||
* @param lineEnd - Ending point of the line
|
||||
* @returns The intersection point
|
||||
*/
|
||||
function computeCircleEdgeIntersection(circle: Bounds, lineStart: Point, lineEnd: Point): Point {
|
||||
const radius = Math.min(circle.width, circle.height) / 2;
|
||||
|
||||
const dx = lineEnd.x - lineStart.x;
|
||||
const dy = lineEnd.y - lineStart.y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length === 0) {
|
||||
return lineStart;
|
||||
}
|
||||
|
||||
const nx = dx / length;
|
||||
const ny = dy / length;
|
||||
|
||||
return {
|
||||
x: circle.x - nx * radius,
|
||||
y: circle.y - ny * radius,
|
||||
};
|
||||
}
|
||||
|
||||
function intersection(node: PositionedNode, outsidePoint: Point, insidePoint: Point): Point {
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
|
||||
if (!node.width || !node.height) {
|
||||
return { x: outsidePoint.x, y: outsidePoint.y };
|
||||
}
|
||||
const dx = Math.abs(x - insidePoint.x);
|
||||
const w = node?.width / 2;
|
||||
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||
const h = node.height / 2;
|
||||
|
||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
|
||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||
r = (R * q) / Q;
|
||||
const res = {
|
||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
|
||||
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
|
||||
};
|
||||
|
||||
if (r === 0) {
|
||||
res.x = outsidePoint.x;
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
if (R === 0) {
|
||||
res.x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
|
||||
return res;
|
||||
} else {
|
||||
if (insidePoint.x < outsidePoint.x) {
|
||||
r = outsidePoint.x - w - x;
|
||||
} else {
|
||||
r = x - w - outsidePoint.x;
|
||||
}
|
||||
const q = (Q * r) / R;
|
||||
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||
|
||||
if (r === 0) {
|
||||
_x = outsidePoint.x;
|
||||
_y = outsidePoint.y;
|
||||
}
|
||||
if (R === 0) {
|
||||
_x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
_y = outsidePoint.y;
|
||||
}
|
||||
|
||||
return { x: _x, y: _y };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate edge positions based on positioned nodes
|
||||
* Now includes tree membership and node dimensions for precise edge calculations
|
||||
* Edges now stop at shape boundaries instead of extending to centers
|
||||
*/
|
||||
function calculateEdgePositions(
|
||||
edges: Edge[],
|
||||
positionedNodes: PositionedNode[],
|
||||
intersectionShift: number
|
||||
): PositionedEdge[] {
|
||||
const nodeInfo = new Map<string, PositionedNode>();
|
||||
positionedNodes.forEach((node) => {
|
||||
nodeInfo.set(node.id, node);
|
||||
});
|
||||
|
||||
return edges.map((edge) => {
|
||||
const sourceNode = nodeInfo.get(edge.start ?? '');
|
||||
const targetNode = nodeInfo.get(edge.end ?? '');
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return {
|
||||
id: edge.id,
|
||||
source: edge.start ?? '',
|
||||
target: edge.end ?? '',
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
midX: 0,
|
||||
midY: 0,
|
||||
endX: 0,
|
||||
endY: 0,
|
||||
points: [{ x: 0, y: 0 }],
|
||||
sourceSection: undefined,
|
||||
targetSection: undefined,
|
||||
sourceWidth: undefined,
|
||||
sourceHeight: undefined,
|
||||
targetWidth: undefined,
|
||||
targetHeight: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const sourceCenter = { x: sourceNode.x, y: sourceNode.y };
|
||||
const targetCenter = { x: targetNode.x, y: targetNode.y };
|
||||
|
||||
const isSourceRound = ['circle', 'cloud', 'bang'].includes(
|
||||
sourceNode.originalNode?.shape ?? ''
|
||||
);
|
||||
const isTargetRound = ['circle', 'cloud', 'bang'].includes(
|
||||
targetNode.originalNode?.shape ?? ''
|
||||
);
|
||||
|
||||
let startPos = isSourceRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: sourceNode.x,
|
||||
y: sourceNode.y,
|
||||
width: sourceNode.width ?? 100,
|
||||
height: sourceNode.height ?? 100,
|
||||
},
|
||||
targetCenter,
|
||||
sourceCenter
|
||||
)
|
||||
: intersection(sourceNode, sourceCenter, targetCenter);
|
||||
|
||||
let endPos = isTargetRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: targetNode.x,
|
||||
y: targetNode.y,
|
||||
width: targetNode.width ?? 100,
|
||||
height: targetNode.height ?? 100,
|
||||
},
|
||||
sourceCenter,
|
||||
targetCenter
|
||||
)
|
||||
: intersection(targetNode, targetCenter, sourceCenter);
|
||||
|
||||
const midX = (startPos.x + endPos.x) / 2;
|
||||
const midY = (startPos.y + endPos.y) / 2;
|
||||
|
||||
const points = [startPos];
|
||||
if (sourceNode.section === 'left') {
|
||||
points.push({
|
||||
x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
|
||||
y: sourceNode.y,
|
||||
});
|
||||
} else if (sourceNode.section === 'right') {
|
||||
points.push({
|
||||
x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
|
||||
y: sourceNode.y,
|
||||
});
|
||||
}
|
||||
if (targetNode.section === 'left') {
|
||||
points.push({
|
||||
x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
|
||||
y: targetNode.y,
|
||||
});
|
||||
} else if (targetNode.section === 'right') {
|
||||
points.push({
|
||||
x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
|
||||
y: targetNode.y,
|
||||
});
|
||||
}
|
||||
|
||||
points.push(endPos);
|
||||
|
||||
const secondPoint = points.length > 1 ? points[1] : targetCenter;
|
||||
startPos = isSourceRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: sourceNode.x,
|
||||
y: sourceNode.y,
|
||||
width: sourceNode.width ?? 100,
|
||||
height: sourceNode.height ?? 100,
|
||||
},
|
||||
secondPoint,
|
||||
sourceCenter
|
||||
)
|
||||
: intersection(sourceNode, secondPoint, sourceCenter);
|
||||
points[0] = startPos;
|
||||
|
||||
const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter;
|
||||
endPos = isTargetRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: targetNode.x,
|
||||
y: targetNode.y,
|
||||
width: targetNode.width ?? 100,
|
||||
height: targetNode.height ?? 100,
|
||||
},
|
||||
secondLastPoint,
|
||||
targetCenter
|
||||
)
|
||||
: intersection(targetNode, secondLastPoint, targetCenter);
|
||||
points[points.length - 1] = endPos;
|
||||
|
||||
return {
|
||||
id: edge.id,
|
||||
source: edge.start ?? '',
|
||||
target: edge.end ?? '',
|
||||
startX: startPos.x,
|
||||
startY: startPos.y,
|
||||
midX,
|
||||
midY,
|
||||
endX: endPos.x,
|
||||
endY: endPos.y,
|
||||
points,
|
||||
sourceSection: sourceNode?.section,
|
||||
targetSection: targetNode?.section,
|
||||
sourceWidth: sourceNode?.width,
|
||||
sourceHeight: sourceNode?.height,
|
||||
targetWidth: targetNode?.width,
|
||||
targetHeight: targetNode?.height,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate layout data structure
|
||||
* @param data - The data to validate
|
||||
* @returns True if data is valid, throws error otherwise
|
||||
*/
|
||||
export function validateLayoutData(data: LayoutData): boolean {
|
||||
if (!data) {
|
||||
throw new Error('Layout data is required');
|
||||
}
|
||||
|
||||
if (!data.config) {
|
||||
throw new Error('Configuration is required in layout data');
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.nodes)) {
|
||||
throw new Error('Nodes array is required in layout data');
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.edges)) {
|
||||
throw new Error('Edges array is required in layout data');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
13
packages/mermaid-layout-tidy-tree/src/layouts.ts
Normal file
13
packages/mermaid-layout-tidy-tree/src/layouts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { LayoutLoaderDefinition } from 'mermaid';
|
||||
|
||||
const loader = async () => await import(`./render.js`);
|
||||
|
||||
const tidyTreeLayout: LayoutLoaderDefinition[] = [
|
||||
{
|
||||
name: 'tidy-tree',
|
||||
loader,
|
||||
algorithm: 'tidy-tree',
|
||||
},
|
||||
];
|
||||
|
||||
export default tidyTreeLayout;
|
||||
18
packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts
vendored
Normal file
18
packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
declare module 'non-layered-tidy-tree-layout' {
|
||||
export class BoundingBox {
|
||||
constructor(gap: number, bottomPadding: number);
|
||||
}
|
||||
|
||||
export class Layout {
|
||||
constructor(boundingBox: BoundingBox);
|
||||
layout(data: any): {
|
||||
result: any;
|
||||
boundingBox: {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
180
packages/mermaid-layout-tidy-tree/src/render.ts
Normal file
180
packages/mermaid-layout-tidy-tree/src/render.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
|
||||
import { executeTidyTreeLayout } from './layout.js';
|
||||
|
||||
interface NodeWithPosition {
|
||||
id: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
domId?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render function for bidirectional tidy-tree layout algorithm
|
||||
*
|
||||
* This follows the same pattern as ELK and dagre renderers:
|
||||
* 1. Insert nodes into DOM to get their actual dimensions
|
||||
* 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
|
||||
* 3. Position the nodes and edges based on layout results
|
||||
*
|
||||
* The bidirectional layout creates two trees that grow horizontally in opposite
|
||||
* directions from a central root node:
|
||||
* - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
|
||||
* - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
|
||||
*/
|
||||
export const render = async (
|
||||
data4Layout: LayoutData,
|
||||
svg: SVG,
|
||||
{
|
||||
insertCluster,
|
||||
insertEdge,
|
||||
insertEdgeLabel,
|
||||
insertMarkers,
|
||||
insertNode,
|
||||
log,
|
||||
positionEdgeLabel,
|
||||
}: InternalHelpers,
|
||||
{ algorithm: _algorithm }: RenderOptions
|
||||
) => {
|
||||
const nodeDb: Record<string, NodeWithPosition> = {};
|
||||
const clusterDb: Record<string, any> = {};
|
||||
|
||||
const element = svg.select('g');
|
||||
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||
|
||||
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
|
||||
const edgePaths = element.insert('g').attr('class', 'edgePaths');
|
||||
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||
const nodes = element.insert('g').attr('class', 'nodes');
|
||||
// Step 1: Insert nodes into DOM to get their actual dimensions
|
||||
log.debug('Inserting nodes into DOM for dimension calculation');
|
||||
|
||||
await Promise.all(
|
||||
data4Layout.nodes.map(async (node) => {
|
||||
if (node.isGroup) {
|
||||
const clusterNode: NodeWithPosition = {
|
||||
...node,
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
};
|
||||
clusterDb[node.id] = clusterNode;
|
||||
nodeDb[node.id] = clusterNode;
|
||||
|
||||
await insertCluster(subGraphsEl, node);
|
||||
} else {
|
||||
const nodeWithPosition: NodeWithPosition = {
|
||||
...node,
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
};
|
||||
nodeDb[node.id] = nodeWithPosition;
|
||||
|
||||
const nodeEl = await insertNode(nodes, node, {
|
||||
config: data4Layout.config,
|
||||
dir: data4Layout.direction || 'TB',
|
||||
});
|
||||
|
||||
const boundingBox = nodeEl.node()!.getBBox();
|
||||
nodeWithPosition.width = boundingBox.width;
|
||||
nodeWithPosition.height = boundingBox.height;
|
||||
nodeWithPosition.domId = nodeEl;
|
||||
|
||||
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
// Step 2: Run the bidirectional tidy-tree layout algorithm
|
||||
log.debug('Running bidirectional tidy-tree layout algorithm');
|
||||
|
||||
const updatedLayoutData = {
|
||||
...data4Layout,
|
||||
nodes: data4Layout.nodes.map((node) => {
|
||||
const nodeWithDimensions = nodeDb[node.id];
|
||||
return {
|
||||
...node,
|
||||
width: nodeWithDimensions.width ?? node.width ?? 100,
|
||||
height: nodeWithDimensions.height ?? node.height ?? 50,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
|
||||
// Step 3: Position the nodes based on bidirectional layout results
|
||||
log.debug('Positioning nodes based on bidirectional layout results');
|
||||
|
||||
layoutResult.nodes.forEach((positionedNode) => {
|
||||
const node = nodeDb[positionedNode.id];
|
||||
if (node?.domId) {
|
||||
// Position the node at the calculated coordinates from bidirectional layout
|
||||
// The layout algorithm has already calculated positions for:
|
||||
// - Root node at center (0, 0)
|
||||
// - Left tree nodes with negative x coordinates (growing left)
|
||||
// - Right tree nodes with positive x coordinates (growing right)
|
||||
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
||||
// Store the final position
|
||||
node.x = positionedNode.x;
|
||||
node.y = positionedNode.y;
|
||||
// Step 3: Position the nodes based on bidirectional layout results
|
||||
log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
|
||||
}
|
||||
});
|
||||
|
||||
log.debug('Inserting and positioning edges');
|
||||
|
||||
await Promise.all(
|
||||
data4Layout.edges.map(async (edge) => {
|
||||
await insertEdgeLabel(edgeLabels, edge);
|
||||
|
||||
const startNode = nodeDb[edge.start ?? ''];
|
||||
const endNode = nodeDb[edge.end ?? ''];
|
||||
|
||||
if (startNode && endNode) {
|
||||
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||
|
||||
if (positionedEdge) {
|
||||
log.debug('APA01 positionedEdge', positionedEdge);
|
||||
const edgeWithPath = {
|
||||
...edge,
|
||||
points: positionedEdge.points,
|
||||
};
|
||||
const paths = insertEdge(
|
||||
edgePaths,
|
||||
edgeWithPath,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
|
||||
positionEdgeLabel(edgeWithPath, paths);
|
||||
} else {
|
||||
const edgeWithPath = {
|
||||
...edge,
|
||||
points: [
|
||||
{ x: startNode.x ?? 0, y: startNode.y ?? 0 },
|
||||
{ x: endNode.x ?? 0, y: endNode.y ?? 0 },
|
||||
],
|
||||
};
|
||||
|
||||
const paths = insertEdge(
|
||||
edgePaths,
|
||||
edgeWithPath,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
positionEdgeLabel(edgeWithPath, paths);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
log.debug('Bidirectional tidy-tree rendering completed');
|
||||
};
|
||||
69
packages/mermaid-layout-tidy-tree/src/types.ts
Normal file
69
packages/mermaid-layout-tidy-tree/src/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { LayoutData } from 'mermaid';
|
||||
|
||||
export type Node = LayoutData['nodes'][number];
|
||||
export type Edge = LayoutData['edges'][number];
|
||||
|
||||
/**
|
||||
* Positioned node after layout calculation
|
||||
*/
|
||||
export interface PositionedNode {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
section?: 'root' | 'left' | 'right';
|
||||
width?: number;
|
||||
height?: number;
|
||||
originalNode?: Node;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positioned edge after layout calculation
|
||||
*/
|
||||
export interface PositionedEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
sourceSection?: 'root' | 'left' | 'right';
|
||||
targetSection?: 'root' | 'left' | 'right';
|
||||
sourceWidth?: number;
|
||||
sourceHeight?: number;
|
||||
targetWidth?: number;
|
||||
targetHeight?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of layout algorithm execution
|
||||
*/
|
||||
export interface LayoutResult {
|
||||
nodes: PositionedNode[];
|
||||
edges: PositionedEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy-tree node structure compatible with non-layered-tidy-tree-layout
|
||||
*/
|
||||
export interface TidyTreeNode {
|
||||
id: string | number;
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
children?: TidyTreeNode[];
|
||||
_originalNode?: Node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy-tree layout configuration
|
||||
*/
|
||||
export interface TidyTreeLayoutConfig {
|
||||
gap: number;
|
||||
bottomPadding: number;
|
||||
}
|
||||
10
packages/mermaid-layout-tidy-tree/tsconfig.json
Normal file
10
packages/mermaid-layout-tidy-tree/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
||||
@@ -229,7 +229,6 @@
|
||||
- [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type
|
||||
|
||||
- [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes:
|
||||
|
||||
- Updates the class diagram to the new unified way of rendering.
|
||||
- Includes a new "classBox" shape to be used in diagrams
|
||||
- Other updates such as:
|
||||
|
||||
@@ -123,8 +123,8 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"start-server-and-test": "^2.0.10",
|
||||
"type-fest": "^4.35.0",
|
||||
"typedoc": "^0.27.8",
|
||||
"typedoc-plugin-markdown": "^4.4.2",
|
||||
"typedoc": "^0.28.9",
|
||||
"typedoc-plugin-markdown": "^4.8.0",
|
||||
"typescript": "~5.7.3",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
|
||||
@@ -171,7 +171,9 @@ This Markdown should be kept.
|
||||
expect(buildShapeDoc()).toMatchInlineSnapshot(`
|
||||
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Bang | Bang | \`bang\` | Bang | \`bang\` |
|
||||
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
|
||||
| Cloud | Cloud | \`cloud\` | cloud | \`cloud\` |
|
||||
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
|
||||
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
|
||||
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
|
||||
|
||||
@@ -1075,6 +1075,10 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
|
||||
export interface MindmapDiagramConfig extends BaseDiagramConfig {
|
||||
padding?: number;
|
||||
maxNodeWidth?: number;
|
||||
/**
|
||||
* Layout algorithm to use for positioning mindmap nodes
|
||||
*/
|
||||
layoutAlgorithm?: string;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for kanban diagrams
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// tests to check that comments are removed
|
||||
|
||||
import { cleanupComments } from './comments.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
@@ -10,12 +8,12 @@ describe('comments', () => {
|
||||
%% This is a comment
|
||||
%% This is another comment
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
%% This is a comment
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -29,9 +27,9 @@ graph TD
|
||||
%%{ init: {'theme': 'space before init'}}%%
|
||||
%%{init: {'theme': 'space after ending'}}%%
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
|
||||
B-->C
|
||||
B-->C
|
||||
%% This is a comment
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
@@ -39,9 +37,9 @@ graph TD
|
||||
%%{ init: {'theme': 'space before init'}}%%
|
||||
%%{init: {'theme': 'space after ending'}}%%
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
|
||||
B-->C
|
||||
B-->C
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -50,14 +48,14 @@ graph TD
|
||||
const text = `
|
||||
%% This is a comment
|
||||
graph TD
|
||||
A-->B
|
||||
%% This is a comment
|
||||
C-->D
|
||||
A-->B
|
||||
%% This is a comment
|
||||
C-->D
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
C-->D
|
||||
A-->B
|
||||
C-->D
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -70,11 +68,11 @@ graph TD
|
||||
|
||||
%% This is a comment
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -82,12 +80,12 @@ graph TD
|
||||
it('should remove comments at end of text with no newline', () => {
|
||||
const text = `
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
%% This is a comment`;
|
||||
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import usecase from '../diagrams/useCase/useCaseDetector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -102,7 +101,6 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap,
|
||||
usecase
|
||||
treemap
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Position } from 'cytoscape';
|
||||
import type { LayoutOptions, Position } from 'cytoscape';
|
||||
import cytoscape from 'cytoscape';
|
||||
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
|
||||
import fcose from 'cytoscape-fcose';
|
||||
import { select } from 'd3';
|
||||
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
||||
@@ -41,7 +40,7 @@ registerIconPacks([
|
||||
icons: architectureIcons,
|
||||
},
|
||||
]);
|
||||
cytoscape.use(fcose);
|
||||
cytoscape.use(fcose as any);
|
||||
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
services.forEach((service) => {
|
||||
@@ -429,7 +428,7 @@ function layoutArchitecture(
|
||||
},
|
||||
alignmentConstraint,
|
||||
relativePlacementConstraint,
|
||||
} as FcoseLayoutOptions);
|
||||
} as LayoutOptions);
|
||||
|
||||
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
|
||||
layout.one('layoutstop', () => {
|
||||
|
||||
@@ -1070,6 +1070,14 @@ describe('given a class diagram with members and methods ', function () {
|
||||
|
||||
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,6 +293,7 @@ classStatement
|
||||
: classIdentifier
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($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);}
|
||||
;
|
||||
|
||||
@@ -301,8 +302,15 @@ classIdentifier
|
||||
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
|
||||
;
|
||||
|
||||
|
||||
emptyBody
|
||||
:
|
||||
| SPACE emptyBody
|
||||
| NEWLINE emptyBody
|
||||
;
|
||||
|
||||
annotationStatement
|
||||
:ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
|
||||
: ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
|
||||
;
|
||||
|
||||
members
|
||||
|
||||
297
packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
Normal file
297
packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MindmapDB } from './mindmapDb.js';
|
||||
import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js';
|
||||
import type { Edge } from '../../rendering-util/types.js';
|
||||
|
||||
// Mock the getConfig function
|
||||
vi.mock('../../diagram-api/diagramAPI.js', () => ({
|
||||
getConfig: vi.fn(() => ({
|
||||
mindmap: {
|
||||
layoutAlgorithm: 'cose-bilkent',
|
||||
padding: 10,
|
||||
maxNodeWidth: 200,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('MindmapDb getData function', () => {
|
||||
let db: MindmapDB;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new MindmapDB();
|
||||
// Clear the database before each test
|
||||
db.clear();
|
||||
});
|
||||
|
||||
describe('getData', () => {
|
||||
it('should return empty data when no mindmap is set', () => {
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toEqual([]);
|
||||
expect(result.edges).toEqual([]);
|
||||
expect(result.config).toBeDefined();
|
||||
expect(result.rootNode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return structured data for simple mindmap', () => {
|
||||
// Create a simple mindmap structure
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child1', 'Child 1', 0);
|
||||
db.addNode(1, 'child2', 'Child 2', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
expect(result.edges).toHaveLength(2);
|
||||
expect(result.config).toBeDefined();
|
||||
expect(result.rootNode).toBeDefined();
|
||||
|
||||
// Check root node
|
||||
const rootNode = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '0');
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootNode?.label).toBe('Root Node');
|
||||
expect(rootNode?.level).toBe(0);
|
||||
|
||||
// Check child nodes
|
||||
const child1 = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '1');
|
||||
expect(child1).toBeDefined();
|
||||
expect(child1?.label).toBe('Child 1');
|
||||
expect(child1?.level).toBe(1);
|
||||
|
||||
// Check edges
|
||||
expect(result.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
start: '0',
|
||||
end: '1',
|
||||
depth: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return structured data for hierarchical mindmap', () => {
|
||||
// Create a hierarchical mindmap structure
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child1', 'Child 1', 0);
|
||||
db.addNode(2, 'grandchild1', 'Grandchild 1', 0);
|
||||
db.addNode(2, 'grandchild2', 'Grandchild 2', 0);
|
||||
db.addNode(1, 'child2', 'Child 2', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(5);
|
||||
expect(result.edges).toHaveLength(4);
|
||||
|
||||
// Check that all levels are represented
|
||||
const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level);
|
||||
expect(levels).toContain(0); // root
|
||||
expect(levels).toContain(1); // children
|
||||
expect(levels).toContain(2); // grandchildren
|
||||
|
||||
// Check edge relationships
|
||||
const edgeRelations = result.edges.map(
|
||||
(e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}`
|
||||
);
|
||||
expect(edgeRelations).toContain('0->1'); // root to child1
|
||||
expect(edgeRelations).toContain('1->2'); // child1 to grandchild1
|
||||
expect(edgeRelations).toContain('1->3'); // child1 to grandchild2
|
||||
expect(edgeRelations).toContain('0->4'); // root to child2
|
||||
});
|
||||
|
||||
it('should preserve node properties in processed data', () => {
|
||||
// Add a node with specific properties
|
||||
db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle
|
||||
|
||||
// Set additional properties
|
||||
const mindmap = db.getMindmap();
|
||||
if (mindmap) {
|
||||
mindmap.width = 150;
|
||||
mindmap.height = 75;
|
||||
mindmap.padding = 15;
|
||||
mindmap.section = 1;
|
||||
mindmap.class = 'custom-class';
|
||||
mindmap.icon = 'star';
|
||||
}
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
const node = result.nodes[0] as MindmapLayoutNode;
|
||||
|
||||
expect(node.type).toBe(2);
|
||||
expect(node.width).toBe(150);
|
||||
expect(node.height).toBe(75);
|
||||
expect(node.padding).toBe(15);
|
||||
expect(node.section).toBeUndefined(); // Root node has undefined section
|
||||
expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class');
|
||||
expect(node.icon).toBe('star');
|
||||
});
|
||||
|
||||
it('should generate unique edge IDs', () => {
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child1', 'Child 1', 0);
|
||||
db.addNode(1, 'child2', 'Child 2', 0);
|
||||
db.addNode(1, 'child3', 'Child 3', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
const edgeIds = result.edges.map((e: Edge) => e.id);
|
||||
const uniqueIds = new Set(edgeIds);
|
||||
|
||||
expect(edgeIds).toHaveLength(3);
|
||||
expect(uniqueIds.size).toBe(3); // All IDs should be unique
|
||||
});
|
||||
|
||||
it('should handle nodes with missing optional properties', () => {
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
|
||||
const result = db.getData();
|
||||
const node = result.nodes[0] as MindmapLayoutNode;
|
||||
|
||||
// Should handle undefined/missing properties gracefully
|
||||
expect(node.section).toBeUndefined(); // Root node has undefined section
|
||||
expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes
|
||||
expect(node.icon).toBeUndefined();
|
||||
expect(node.x).toBeUndefined();
|
||||
expect(node.y).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should assign correct section classes based on sibling position', () => {
|
||||
// Create the example mindmap structure:
|
||||
// A
|
||||
// a0
|
||||
// aa0
|
||||
// a1
|
||||
// aaa
|
||||
// a2
|
||||
db.addNode(0, 'A', 'A', 0); // Root
|
||||
db.addNode(1, 'a0', 'a0', 0); // First child of root
|
||||
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
|
||||
db.addNode(1, 'a1', 'a1', 0); // Second child of root
|
||||
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
|
||||
db.addNode(1, 'a2', 'a2', 0); // Third child of root
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
// Find nodes by their labels
|
||||
const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
|
||||
const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode;
|
||||
const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode;
|
||||
const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode;
|
||||
const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode;
|
||||
const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode;
|
||||
|
||||
// Check section assignments
|
||||
expect(nodeA.section).toBeUndefined(); // Root has undefined section
|
||||
expect(nodeA0.section).toBe(0); // First child of root
|
||||
expect(nodeAa0.section).toBe(0); // Inherits from parent a0
|
||||
expect(nodeA1.section).toBe(1); // Second child of root
|
||||
expect(nodeAaa.section).toBe(1); // Inherits from parent a1
|
||||
expect(nodeA2.section).toBe(2); // Third child of root
|
||||
|
||||
// Check CSS classes
|
||||
expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1');
|
||||
expect(nodeA0.cssClasses).toBe('mindmap-node section-0');
|
||||
expect(nodeAa0.cssClasses).toBe('mindmap-node section-0');
|
||||
expect(nodeA1.cssClasses).toBe('mindmap-node section-1');
|
||||
expect(nodeAaa.cssClasses).toBe('mindmap-node section-1');
|
||||
expect(nodeA2.cssClasses).toBe('mindmap-node section-2');
|
||||
});
|
||||
|
||||
it('should preserve custom classes while adding section classes', () => {
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child', 'Child Node', 0);
|
||||
|
||||
// Add custom classes to nodes
|
||||
const mindmap = db.getMindmap();
|
||||
if (mindmap) {
|
||||
mindmap.class = 'custom-root-class';
|
||||
if (mindmap.children?.[0]) {
|
||||
mindmap.children[0].class = 'custom-child-class';
|
||||
}
|
||||
}
|
||||
|
||||
const result = db.getData();
|
||||
const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode;
|
||||
const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode;
|
||||
|
||||
// Should include both section classes and custom classes
|
||||
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class');
|
||||
expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class');
|
||||
});
|
||||
|
||||
it('should not create any fake root nodes', () => {
|
||||
// Create a simple mindmap
|
||||
db.addNode(0, 'A', 'A', 0);
|
||||
db.addNode(1, 'a0', 'a0', 0);
|
||||
db.addNode(1, 'a1', 'a1', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
// Check that we only have the expected nodes
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']);
|
||||
|
||||
// Check that there's no node with label "mindmap" or any other fake root
|
||||
const mindmapNode = result.nodes.find((n) => n.label === 'mindmap');
|
||||
expect(mindmapNode).toBeUndefined();
|
||||
|
||||
// Verify the root node has the correct classes
|
||||
const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
|
||||
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1');
|
||||
expect(rootNode.level).toBe(0);
|
||||
});
|
||||
|
||||
it('should assign correct section classes to edges', () => {
|
||||
// Create the example mindmap structure:
|
||||
// A
|
||||
// a0
|
||||
// aa0
|
||||
// a1
|
||||
// aaa
|
||||
// a2
|
||||
db.addNode(0, 'A', 'A', 0); // Root
|
||||
db.addNode(1, 'a0', 'a0', 0); // First child of root
|
||||
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
|
||||
db.addNode(1, 'a1', 'a1', 0); // Second child of root
|
||||
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
|
||||
db.addNode(1, 'a2', 'a2', 0); // Third child of root
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
// Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2
|
||||
expect(result.edges).toHaveLength(5);
|
||||
|
||||
// Find edges by their start and end nodes
|
||||
const edgeA_a0 = result.edges.find(
|
||||
(e) => e.start === '0' && e.end === '1'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA0_aa0 = result.edges.find(
|
||||
(e) => e.start === '1' && e.end === '2'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA_a1 = result.edges.find(
|
||||
(e) => e.start === '0' && e.end === '3'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA1_aaa = result.edges.find(
|
||||
(e) => e.start === '3' && e.end === '4'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA_a2 = result.edges.find(
|
||||
(e) => e.start === '0' && e.end === '5'
|
||||
) as MindmapLayoutEdge;
|
||||
|
||||
// Check edge classes
|
||||
expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1
|
||||
expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2
|
||||
expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1
|
||||
expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2
|
||||
expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1
|
||||
|
||||
// Check section assignments match the child nodes
|
||||
expect(edgeA_a0.section).toBe(0);
|
||||
expect(edgeA0_aa0.section).toBe(0);
|
||||
expect(edgeA_a1.section).toBe(1);
|
||||
expect(edgeA1_aaa.section).toBe(1);
|
||||
expect(edgeA_a2.section).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,26 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { v4 } from 'uuid';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { MindmapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
|
||||
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 = {
|
||||
DEFAULT: 0,
|
||||
@@ -27,7 +44,6 @@ export class MindmapDB {
|
||||
this.nodeType = nodeType;
|
||||
this.clear();
|
||||
this.getType = this.getType.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
this.getElementById = this.getElementById.bind(this);
|
||||
this.getParent = this.getParent.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
@@ -156,6 +172,223 @@ 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() {
|
||||
return log;
|
||||
}
|
||||
|
||||
@@ -1,200 +1,83 @@
|
||||
import cytoscape from 'cytoscape';
|
||||
// @ts-expect-error No types available
|
||||
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||
import { select } from 'd3';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
|
||||
import { drawNode, positionNode } from './svgDraw.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import type { FilledMindMapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
async function drawNodes(
|
||||
db: MindmapDB,
|
||||
svg: D3Element,
|
||||
mindmap: FilledMindMapNode,
|
||||
section: number,
|
||||
conf: MermaidConfig
|
||||
) {
|
||||
await drawNode(db, svg, mindmap, section, conf);
|
||||
if (mindmap.children) {
|
||||
await Promise.all(
|
||||
mindmap.children.map((child, index) =>
|
||||
drawNodes(db, svg, child, section < 0 ? index : section, conf)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'cytoscape' {
|
||||
interface EdgeSingular {
|
||||
_private: {
|
||||
bodyBounds: unknown;
|
||||
rscratch: {
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
cy.edges().map((edge, id) => {
|
||||
const data = edge.data();
|
||||
if (edge[0]._private.bodyBounds) {
|
||||
const bounds = edge[0]._private.rscratch;
|
||||
log.trace('Edge: ', id, data);
|
||||
edgesEl
|
||||
.insert('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
|
||||
)
|
||||
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
|
||||
/**
|
||||
* Update the layout data with actual node dimensions after drawing
|
||||
*/
|
||||
function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) {
|
||||
const updateNode = (node: FilledMindMapNode) => {
|
||||
// Find the corresponding node in the layout data
|
||||
const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString());
|
||||
if (layoutNode) {
|
||||
// Update with the actual dimensions calculated by drawNode
|
||||
layoutNode.width = node.width;
|
||||
layoutNode.height = node.height;
|
||||
log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: mindmap.id.toString(),
|
||||
labelText: mindmap.descr,
|
||||
height: mindmap.height,
|
||||
width: mindmap.width,
|
||||
level: level,
|
||||
nodeId: mindmap.id,
|
||||
padding: mindmap.padding,
|
||||
type: mindmap.type,
|
||||
},
|
||||
position: {
|
||||
x: mindmap.x!,
|
||||
y: mindmap.y!,
|
||||
},
|
||||
});
|
||||
if (mindmap.children) {
|
||||
mindmap.children.forEach((child) => {
|
||||
addNodes(child, cy, conf, level + 1);
|
||||
cy.add({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${mindmap.id}_${child.id}`,
|
||||
source: mindmap.id,
|
||||
target: child.id,
|
||||
depth: level,
|
||||
section: child.section,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// Recursively update children
|
||||
node.children?.forEach(updateNode);
|
||||
};
|
||||
|
||||
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
// Add temporary render element
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
const cy = cytoscape({
|
||||
container: document.getElementById('cy'), // container to render in
|
||||
style: [
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
addNodes(node, cy, conf, 0);
|
||||
|
||||
// Make cytoscape care about the dimensions of the nodes
|
||||
cy.nodes().forEach(function (n) {
|
||||
n.layoutDimensions = () => {
|
||||
const data = n.data();
|
||||
return { w: data.width, h: data.height };
|
||||
};
|
||||
});
|
||||
|
||||
cy.layout({
|
||||
name: 'cose-bilkent',
|
||||
// @ts-ignore Types for cose-bilkent are not correct?
|
||||
quality: 'proof',
|
||||
styleEnabled: false,
|
||||
animate: false,
|
||||
}).run();
|
||||
cy.ready((e) => {
|
||||
log.info('Ready', e);
|
||||
resolve(cy);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
|
||||
cy.nodes().map((node, id) => {
|
||||
const data = node.data();
|
||||
data.x = node.position().x;
|
||||
data.y = node.position().y;
|
||||
positionNode(db, data);
|
||||
const el = db.getElementById(data.nodeId);
|
||||
log.info('id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
|
||||
el.attr(
|
||||
'transform',
|
||||
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
|
||||
);
|
||||
el.attr('attr', `apa-${id})`);
|
||||
});
|
||||
updateNode(mindmapRoot);
|
||||
}
|
||||
|
||||
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||
log.debug('Rendering mindmap diagram\n' + text);
|
||||
|
||||
// Draw the nodes first to get their dimensions, then update the layout data
|
||||
const db = diagObj.db as MindmapDB;
|
||||
|
||||
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||
// into the Layout data format
|
||||
const data4Layout = db.getData();
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, data4Layout.config.securityLevel);
|
||||
|
||||
data4Layout.type = diagObj.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(data4Layout.config.layout, {
|
||||
fallback: 'cose-bilkent',
|
||||
});
|
||||
|
||||
data4Layout.diagramId = id;
|
||||
|
||||
const mm = db.getMindmap();
|
||||
if (!mm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conf = getConfig();
|
||||
conf.htmlLabels = false;
|
||||
data4Layout.nodes.forEach((node) => {
|
||||
if (node.shape === 'rounded') {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
const svg = selectSvgElement(id);
|
||||
// Use the unified rendering system
|
||||
await render(data4Layout, svg);
|
||||
|
||||
// Draw the graph and start with drawing the nodes without proper position
|
||||
// this gives us the size of the nodes and we can set the positions later
|
||||
|
||||
const edgesElem = svg.append('g');
|
||||
edgesElem.attr('class', 'mindmap-edges');
|
||||
const nodesElem = svg.append('g');
|
||||
nodesElem.attr('class', 'mindmap-nodes');
|
||||
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||
|
||||
// Next step is to layout the mindmap, giving each node a position
|
||||
|
||||
const cy = await layoutMindmap(mm, conf);
|
||||
|
||||
// After this we can draw, first the edges and the then nodes with the correct position
|
||||
drawEdges(edgesElem, cy);
|
||||
positionNodes(db, cy);
|
||||
|
||||
// Setup the view box and size of the svg element
|
||||
setupGraphViewbox(
|
||||
undefined,
|
||||
// Setup the view box and size of the svg element using config from data4Layout
|
||||
setupViewPortForSVG(
|
||||
svg,
|
||||
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||
data4Layout.config.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||
'mindmapDiagram',
|
||||
data4Layout.config.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ const getStyles: DiagramStylesProvider = (options) =>
|
||||
.section-root text {
|
||||
fill: ${options.gitBranchLabel0};
|
||||
}
|
||||
.section-root span {
|
||||
color: ${options.gitBranchLabel0};
|
||||
}
|
||||
.section-2 span {
|
||||
color: ${options.gitBranchLabel0};
|
||||
}
|
||||
.icon-container {
|
||||
height:100%;
|
||||
display: flex;
|
||||
|
||||
@@ -1368,7 +1368,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
||||
it('should handle box without description', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
box Aqua
|
||||
box aqua
|
||||
participant a as Alice
|
||||
participant b as Bob
|
||||
end
|
||||
@@ -1384,7 +1384,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
||||
const boxes = diagram.db.getBoxes();
|
||||
expect(boxes[0].name).toBeFalsy();
|
||||
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
||||
expect(boxes[0].fill).toEqual('Aqua');
|
||||
expect(boxes[0].fill).toEqual('aqua');
|
||||
});
|
||||
|
||||
it('should handle simple actor creation', async () => {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
const getStyles = (options) =>
|
||||
`
|
||||
.usecase-diagram {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: ${options.fontSize};
|
||||
}
|
||||
|
||||
/* Actor styles */
|
||||
.usecase-actor-man {
|
||||
stroke: ${options.actorBorder};
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-actor-man circle {
|
||||
fill: ${options.useCaseActorBkg};
|
||||
stroke: ${options.useCaseActorBorder};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-actor-man line {
|
||||
stroke: ${options.useCaseActorBorder};
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.usecase-actor-man text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
fill: ${options.useCaseActorTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Use case styles */
|
||||
.usecase-usecase {
|
||||
fill: ${options.useCaseUseCaseBkg};
|
||||
stroke: ${options.useCaseUseCaseBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.usecase-usecase text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseUseCaseTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* System boundary styles */
|
||||
.usecase-system-boundary {
|
||||
fill: ${options.useCaseSystemBoundaryBkg};
|
||||
stroke: ${options.useCaseSystemBoundaryBorder};
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 5,5;
|
||||
}
|
||||
|
||||
.usecase-system-boundary text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
fill: ${options.useCaseSystemBoundaryTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Arrow and relationship styles */
|
||||
.usecase-arrow {
|
||||
stroke: ${'red'};
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.usecase-arrow-label {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseArrowTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Node styles for standalone nodes */
|
||||
.usecase-node {
|
||||
fill: ${options.useCaseUseCaseBkg};
|
||||
stroke: ${options.useCaseUseCaseBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.usecase-node text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseUseCaseTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.usecase-actor-man:hover circle {
|
||||
fill: ${options.useCaseActorBkg};
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-actor-man:hover line {
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-actor-man:hover text {
|
||||
fill: ${options.useCaseArrowColor};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.usecase-usecase:hover {
|
||||
fill: ${options.useCaseSystemBoundaryBkg};
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-usecase:hover text {
|
||||
fill: ${options.useCaseArrowColor};
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
@@ -1,586 +0,0 @@
|
||||
// Simple actor type for useCase diagrams
|
||||
interface Actor {
|
||||
type: 'actor';
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Simple use case type
|
||||
interface UseCase {
|
||||
type: 'useCase';
|
||||
name: string;
|
||||
}
|
||||
|
||||
// System boundary type
|
||||
interface SystemBoundary {
|
||||
type: 'systemBoundary';
|
||||
name: string;
|
||||
useCases: UseCase[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// System boundary metadata type
|
||||
interface SystemBoundaryMetadata {
|
||||
type: 'systemBoundaryMetadata';
|
||||
name: string; // boundary name
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
// Actor-UseCase relationship type
|
||||
interface ActorUseCaseRelationship {
|
||||
type: 'actorUseCaseRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // use case name
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
// Node type
|
||||
interface Node {
|
||||
type: 'node';
|
||||
id: string; // node ID (e.g., 'a', 'b', 'c')
|
||||
label: string; // node label (e.g., 'Go through code')
|
||||
}
|
||||
|
||||
// Actor-Node relationship type
|
||||
interface ActorNodeRelationship {
|
||||
type: 'actorNodeRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // node ID
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
// Inline Actor-Node relationship type
|
||||
interface InlineActorNodeRelationship {
|
||||
type: 'inlineActorNodeRelationship';
|
||||
actor: string; // actor name
|
||||
node: Node; // node definition
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export class UseCaseDB {
|
||||
private actors: Actor[] = [];
|
||||
private systemBoundaries: SystemBoundary[] = [];
|
||||
private systemBoundaryMetadata: SystemBoundaryMetadata[] = [];
|
||||
private useCases: UseCase[] = [];
|
||||
private relationships: ActorUseCaseRelationship[] = [];
|
||||
private nodes: Node[] = [];
|
||||
private nodeRelationships: ActorNodeRelationship[] = [];
|
||||
private inlineRelationships: InlineActorNodeRelationship[] = [];
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.actors = [];
|
||||
this.systemBoundaries = [];
|
||||
this.systemBoundaryMetadata = [];
|
||||
this.useCases = [];
|
||||
this.relationships = [];
|
||||
this.nodes = [];
|
||||
this.nodeRelationships = [];
|
||||
this.inlineRelationships = [];
|
||||
}
|
||||
|
||||
addActor(actor: Actor): void {
|
||||
this.actors.push(actor);
|
||||
}
|
||||
|
||||
addSystemBoundary(boundary: SystemBoundary): void {
|
||||
this.systemBoundaries.push(boundary);
|
||||
}
|
||||
|
||||
addSystemBoundaryMetadata(metadata: SystemBoundaryMetadata): void {
|
||||
this.systemBoundaryMetadata.push(metadata);
|
||||
// Apply metadata to existing system boundary
|
||||
const boundary = this.systemBoundaries.find(b => b.name === metadata.name);
|
||||
if (boundary) {
|
||||
boundary.metadata = metadata.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
addUseCase(useCase: UseCase): void {
|
||||
this.useCases.push(useCase);
|
||||
}
|
||||
|
||||
addRelationship(relationship: ActorUseCaseRelationship): void {
|
||||
this.relationships.push(relationship);
|
||||
}
|
||||
|
||||
addNode(node: Node): void {
|
||||
this.nodes.push(node);
|
||||
}
|
||||
|
||||
addNodeRelationship(relationship: ActorNodeRelationship): void {
|
||||
this.nodeRelationships.push(relationship);
|
||||
}
|
||||
|
||||
addInlineRelationship(relationship: InlineActorNodeRelationship): void {
|
||||
this.inlineRelationships.push(relationship);
|
||||
// Also add the node and actor separately
|
||||
this.addNode(relationship.node);
|
||||
// Add actor if not already exists
|
||||
const actorExists = this.actors.some(actor => actor.name === relationship.actor);
|
||||
if (!actorExists) {
|
||||
this.addActor({
|
||||
type: 'actor',
|
||||
name: relationship.actor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getActors(): Actor[] {
|
||||
return this.actors;
|
||||
}
|
||||
|
||||
getSystemBoundaries(): SystemBoundary[] {
|
||||
return this.systemBoundaries;
|
||||
}
|
||||
|
||||
getSystemBoundaryMetadata(): SystemBoundaryMetadata[] {
|
||||
return this.systemBoundaryMetadata;
|
||||
}
|
||||
|
||||
getUseCases(): UseCase[] {
|
||||
return this.useCases;
|
||||
}
|
||||
|
||||
getRelationships(): ActorUseCaseRelationship[] {
|
||||
return this.relationships;
|
||||
}
|
||||
|
||||
getNodes(): Node[] {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
getNodeRelationships(): ActorNodeRelationship[] {
|
||||
return this.nodeRelationships;
|
||||
}
|
||||
|
||||
getInlineRelationships(): InlineActorNodeRelationship[] {
|
||||
return this.inlineRelationships;
|
||||
}
|
||||
|
||||
parse(text: string): void {
|
||||
this.clear();
|
||||
|
||||
// For now, use the simple parser with enhanced metadata support
|
||||
// TODO: Integrate ANTLR parser in the future
|
||||
|
||||
// Simple parser for usecase diagrams (fallback)
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('%'));
|
||||
|
||||
let foundUsecase = false;
|
||||
let inSystemBoundary = false;
|
||||
let currentBoundary: SystemBoundary | null = null;
|
||||
let inMetadataBlock = false;
|
||||
let currentMetadataName = '';
|
||||
let currentMetadataContent = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === 'usecase') {
|
||||
foundUsecase = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundUsecase) {
|
||||
continue
|
||||
};
|
||||
|
||||
if (line.startsWith('actor ')) {
|
||||
const actorPart = line.substring(6).trim();
|
||||
if (actorPart) {
|
||||
// Check if this is an inline actor-node relationship
|
||||
if (this.isInlineActorNodeRelationshipLine(actorPart)) {
|
||||
const relationship = this.parseInlineActorNodeRelationshipLine(actorPart);
|
||||
if (relationship) {
|
||||
this.addInlineRelationship(relationship);
|
||||
}
|
||||
} else {
|
||||
const actors = this.parseActorList(actorPart);
|
||||
actors.forEach((actor: Actor) => this.addActor(actor));
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('systemBoundary ')) {
|
||||
const boundaryPart = line.substring(15).trim();
|
||||
if (boundaryPart.endsWith(' {')) {
|
||||
// New curly brace syntax: systemBoundary Name {
|
||||
const boundaryName = boundaryPart.substring(0, boundaryPart.length - 2).trim();
|
||||
currentBoundary = {
|
||||
type: 'systemBoundary',
|
||||
name: boundaryName,
|
||||
useCases: []
|
||||
};
|
||||
inSystemBoundary = true;
|
||||
} else if (boundaryPart) {
|
||||
// Old syntax: systemBoundary Name (followed by 'end')
|
||||
currentBoundary = {
|
||||
type: 'systemBoundary',
|
||||
name: boundaryPart,
|
||||
useCases: []
|
||||
};
|
||||
inSystemBoundary = true;
|
||||
}
|
||||
} else if (line === 'end' || (line === '}' && !inMetadataBlock)) {
|
||||
if (inSystemBoundary && currentBoundary) {
|
||||
this.addSystemBoundary(currentBoundary);
|
||||
currentBoundary = null;
|
||||
inSystemBoundary = false;
|
||||
}
|
||||
} else if (inSystemBoundary && currentBoundary && line) {
|
||||
// This is a use case inside the system boundary
|
||||
const useCase: UseCase = {
|
||||
type: 'useCase',
|
||||
name: line
|
||||
};
|
||||
currentBoundary.useCases.push(useCase);
|
||||
} else if (line && !inSystemBoundary) {
|
||||
// Handle multi-line metadata blocks
|
||||
if (inMetadataBlock) {
|
||||
if (line.includes('}')) {
|
||||
// End of metadata block
|
||||
currentMetadataContent += line.replace('}', '').trim();
|
||||
const metadata = this.parseMetadataContent(currentMetadataName, currentMetadataContent);
|
||||
if (metadata) {
|
||||
this.addSystemBoundaryMetadata(metadata);
|
||||
}
|
||||
inMetadataBlock = false;
|
||||
currentMetadataName = '';
|
||||
currentMetadataContent = '';
|
||||
} else {
|
||||
// Continue collecting metadata content
|
||||
currentMetadataContent += line.trim() + ' ';
|
||||
}
|
||||
} else if (line.includes('@{')) {
|
||||
// Start of metadata block
|
||||
const match = line.match(/^(\w+)@\{(.*)$/);
|
||||
if (match) {
|
||||
currentMetadataName = match[1];
|
||||
const content = match[2].trim();
|
||||
if (content.includes('}')) {
|
||||
// Single line metadata
|
||||
const metadata = this.parseMetadataContent(currentMetadataName, content.replace('}', ''));
|
||||
if (metadata) {
|
||||
this.addSystemBoundaryMetadata(metadata);
|
||||
}
|
||||
} else {
|
||||
// Multi-line metadata
|
||||
inMetadataBlock = true;
|
||||
currentMetadataContent = content + ' ';
|
||||
}
|
||||
}
|
||||
} else if (this.isRelationshipLine(line)) {
|
||||
// Check if this is a relationship (actor --> usecase or actor --> node)
|
||||
const relationship = this.parseRelationshipLine(line);
|
||||
if (relationship) {
|
||||
if (relationship.type === 'actorUseCaseRelationship') {
|
||||
this.addRelationship(relationship);
|
||||
} else if (relationship.type === 'actorNodeRelationship') {
|
||||
this.addNodeRelationship(relationship);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a standalone use case
|
||||
const useCase: UseCase = {
|
||||
type: 'useCase',
|
||||
name: line
|
||||
};
|
||||
this.addUseCase(useCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private parseActorList(actorPart: string): Actor[] {
|
||||
// Smart split by comma that respects metadata braces
|
||||
const actorNames = this.smartSplitActors(actorPart);
|
||||
|
||||
return actorNames.map(actorName => this.parseActorWithMetadata(actorName));
|
||||
}
|
||||
|
||||
private smartSplitActors(input: string): string[] {
|
||||
const actors: string[] = [];
|
||||
let current = '';
|
||||
let braceDepth = 0;
|
||||
let inQuotes = false;
|
||||
let quoteChar = '';
|
||||
|
||||
for (const char of input) {
|
||||
|
||||
if (!inQuotes && (char === '"' || char === "'")) {
|
||||
inQuotes = true;
|
||||
quoteChar = char;
|
||||
current += char;
|
||||
} else if (inQuotes && char === quoteChar) {
|
||||
inQuotes = false;
|
||||
quoteChar = '';
|
||||
current += char;
|
||||
} else if (!inQuotes && char === '{') {
|
||||
braceDepth++;
|
||||
current += char;
|
||||
} else if (!inQuotes && char === '}') {
|
||||
braceDepth--;
|
||||
current += char;
|
||||
} else if (!inQuotes && char === ',' && braceDepth === 0) {
|
||||
// This is a real separator, not inside metadata
|
||||
if (current.trim()) {
|
||||
actors.push(current.trim());
|
||||
}
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last actor
|
||||
if (current.trim()) {
|
||||
actors.push(current.trim());
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private parseActorWithMetadata(actorPart: string): Actor {
|
||||
// Check if there's metadata (contains @{...})
|
||||
const metadataRegex = /^([^@]+)@{([^}]*)}$/;
|
||||
const metadataMatch = metadataRegex.exec(actorPart);
|
||||
|
||||
if (metadataMatch) {
|
||||
const name = metadataMatch[1].trim();
|
||||
const metadataStr = metadataMatch[2].trim();
|
||||
const metadata = this.parseMetadataString(metadataStr);
|
||||
|
||||
return {
|
||||
type: 'actor',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
} else {
|
||||
// No metadata, just return the name
|
||||
return {
|
||||
type: 'actor',
|
||||
name: actorPart
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private parseMetadataString(metadataStr: string): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
if (!metadataStr.trim()) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Split by comma and parse key-value pairs
|
||||
const pairs = metadataStr.split(',');
|
||||
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private isRelationshipLine(line: string): boolean {
|
||||
return line.includes('-->') || line.includes('->');
|
||||
}
|
||||
|
||||
private parseRelationshipLine(line: string): ActorUseCaseRelationship | ActorNodeRelationship | null {
|
||||
let arrow = '';
|
||||
let label: string | undefined;
|
||||
let parts: string[] = [];
|
||||
|
||||
// Check for labeled arrows first (--label--> or --label->)
|
||||
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
|
||||
if (labeledArrowMatch) {
|
||||
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
|
||||
arrow = labeledArrowMatch[2];
|
||||
// Extract label from arrow
|
||||
const labelMatch = arrow.match(/^--(\w+)--?>$/);
|
||||
if (labelMatch) {
|
||||
label = labelMatch[1];
|
||||
}
|
||||
} else if (line.includes('-->')) {
|
||||
arrow = '-->';
|
||||
parts = line.split('-->').map(part => part.trim());
|
||||
} else if (line.includes('->')) {
|
||||
arrow = '->';
|
||||
parts = line.split('->').map(part => part.trim());
|
||||
}
|
||||
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
// Check if target is a node definition (contains parentheses)
|
||||
if (this.isNodeDefinitionString(parts[1])) {
|
||||
const node = this.parseNodeDefinitionString(parts[1]);
|
||||
if (node) {
|
||||
this.addNode(node);
|
||||
return {
|
||||
type: 'actorNodeRelationship',
|
||||
from: parts[0],
|
||||
to: node.id,
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'actorUseCaseRelationship',
|
||||
from: parts[0],
|
||||
to: parts[1],
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isInlineActorNodeRelationshipLine(line: string): boolean {
|
||||
// Check for pattern: ActorName --> nodeId(label) or ActorName --label--> nodeId(label)
|
||||
const hasArrow = line.includes('-->') || line.includes('->') || !!line.match(/--\w+-->/);
|
||||
const hasNodeDefinition = line.includes('(') && line.includes(')');
|
||||
return hasArrow && hasNodeDefinition;
|
||||
}
|
||||
|
||||
private parseInlineActorNodeRelationshipLine(line: string): InlineActorNodeRelationship | null {
|
||||
let arrow = '';
|
||||
let label: string | undefined;
|
||||
let parts: string[] = [];
|
||||
|
||||
// Check for labeled arrows first (--label--> or --label->)
|
||||
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
|
||||
if (labeledArrowMatch) {
|
||||
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
|
||||
arrow = labeledArrowMatch[2];
|
||||
// Extract label from arrow
|
||||
const labelMatch = arrow.match(/^--(\w+)--?>$/);
|
||||
if (labelMatch) {
|
||||
label = labelMatch[1];
|
||||
}
|
||||
} else if (line.includes('-->')) {
|
||||
arrow = '-->';
|
||||
parts = line.split('-->').map(part => part.trim());
|
||||
} else if (line.includes('->')) {
|
||||
arrow = '->';
|
||||
parts = line.split('->').map(part => part.trim());
|
||||
}
|
||||
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
const node = this.parseNodeDefinitionString(parts[1]);
|
||||
if (node) {
|
||||
return {
|
||||
type: 'inlineActorNodeRelationship',
|
||||
actor: parts[0],
|
||||
node,
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isNodeDefinitionString(str: string): boolean {
|
||||
return str.includes('(') && str.includes(')');
|
||||
}
|
||||
|
||||
private parseNodeDefinitionString(str: string): Node | null {
|
||||
const match = str.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'node',
|
||||
id: match[1],
|
||||
label: match[2]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isSystemBoundaryMetadataLine(line: string): boolean {
|
||||
// Check for pattern: boundaryName@{...}
|
||||
return line.includes('@{') && line.includes('}');
|
||||
}
|
||||
|
||||
private parseSystemBoundaryMetadataLine(line: string): SystemBoundaryMetadata | null {
|
||||
// Parse pattern: boundaryName@{key: value, key2: value2}
|
||||
const match = line.match(/^(\w+)@\{(.+)\}$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = match[1];
|
||||
const metadataContent = match[2];
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse key-value pairs
|
||||
const pairs = metadataContent.split(',').map(pair => pair.trim());
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
private parseMetadataContent(name: string, content: string): SystemBoundaryMetadata | null {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse key-value pairs from content
|
||||
const pairs = content.split(',').map(pair => pair.trim()).filter(pair => pair);
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'usecase';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*usecase/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./useCaseDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { UseCaseDB } from './useCaseDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './useCaseRenderer.js';
|
||||
|
||||
// Shared database instance
|
||||
let db: UseCaseDB;
|
||||
|
||||
// Create a simple parser that integrates with our custom parser
|
||||
const parser = {
|
||||
parse: (text: string) => {
|
||||
// Use the shared database instance
|
||||
db.parse(text);
|
||||
},
|
||||
};
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
get db() {
|
||||
if (!db) {
|
||||
db = new UseCaseDB();
|
||||
}
|
||||
return db;
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
// Initialize configuration if needed
|
||||
if (!db) {
|
||||
db = new UseCaseDB();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,619 +0,0 @@
|
||||
import { select } from 'd3';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import type { UseCaseDB } from './useCaseDb.js';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
// Position interfaces
|
||||
interface NodePosition {
|
||||
name: string; // node ID (for relationship matching)
|
||||
label: string; // node label (for display)
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// Constants for actor rendering
|
||||
const ACTOR_TYPE_WIDTH = 36; // 18 * 2 from sequence diagram
|
||||
const ACTOR_MAN_FIGURE_CLASS = 'usecase-actor-man';
|
||||
const ACTOR_SPACING = 120; // Horizontal spacing between actors
|
||||
const ACTOR_HEIGHT = 80; // Height of actor figure
|
||||
const MARGIN = 50; // Margin around the diagram
|
||||
|
||||
// Simple actor interface for positioning
|
||||
interface ActorPosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// System boundary interface for positioning
|
||||
interface SystemBoundaryPosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
useCases: UseCasePosition[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Use case interface for positioning
|
||||
interface UseCasePosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a stick figure actor similar to sequence diagrams but optimized for useCase
|
||||
*/
|
||||
const drawActorTypeActor = (elem: any, actor: ActorPosition, conf: any): number => {
|
||||
const center = actor.x + actor.width / 2;
|
||||
const actorY = actor.y;
|
||||
|
||||
// Create actor group
|
||||
const actElem = elem.append('g');
|
||||
actElem.attr('class', ACTOR_MAN_FIGURE_CLASS);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
// Draw stick figure
|
||||
// Head (circle)
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', center)
|
||||
.attr('cy', actorY + 15)
|
||||
.attr('r', 10);
|
||||
|
||||
// Body (torso line)
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 25)
|
||||
.attr('x2', center)
|
||||
.attr('y2', actorY + 50)
|
||||
.style('stroke', 'black');
|
||||
|
||||
// Arms (horizontal line)
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
|
||||
.attr('y1', actorY + 35)
|
||||
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
|
||||
.style('stroke', 'black')
|
||||
.attr('y2', actorY + 35);
|
||||
|
||||
// Left leg
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 50)
|
||||
.attr('x2', center - ACTOR_TYPE_WIDTH / 2)
|
||||
.style('stroke', 'black')
|
||||
.attr('y2', actorY + 70);
|
||||
|
||||
// Right leg
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 50)
|
||||
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
|
||||
.attr('y2', actorY + 70)
|
||||
.style('stroke', 'black');
|
||||
|
||||
// Actor name text
|
||||
const textY = actorY + ACTOR_HEIGHT + 15;
|
||||
drawActorText(actor.name, actElem, actor.x, textY, actor.width, 20);
|
||||
|
||||
return ACTOR_HEIGHT; // Total height including text and metadata
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws text for actor name - simplified version of sequence diagram text drawing
|
||||
*/
|
||||
const drawActorText = (content: string, g: any, x: number, y: number, width: number, height: number): void => {
|
||||
g.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y + height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a system boundary box with use cases inside
|
||||
*/
|
||||
const drawSystemBoundary = (g: any, boundary: SystemBoundaryPosition, conf: any): void => {
|
||||
// Determine boundary type from metadata (default to 'rect')
|
||||
const boundaryType = boundary.metadata?.type || 'rect';
|
||||
|
||||
if (boundaryType === 'package') {
|
||||
// Draw package-style boundary with title box
|
||||
const titleHeight = 25;
|
||||
const titleWidth = Math.max(100, boundary.name.length * 8 + 20);
|
||||
|
||||
// Draw main boundary rectangle
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y + titleHeight)
|
||||
.attr('width', boundary.width)
|
||||
.attr('height', boundary.height - titleHeight)
|
||||
.attr('class', 'usecase-system-boundary')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw title box
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y)
|
||||
.attr('width', titleWidth)
|
||||
.attr('height', titleHeight)
|
||||
.attr('class', 'usecase-system-boundary')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw title text
|
||||
g.append('text')
|
||||
.attr('x', boundary.x + titleWidth / 2)
|
||||
.attr('y', boundary.y + titleHeight / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.style('font-size', '14px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('font-family', 'Arial, sans-serif')
|
||||
.style('fill', '#333')
|
||||
.text(boundary.name);
|
||||
} else {
|
||||
// Draw rect-style boundary (default)
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y)
|
||||
.attr('width', boundary.width)
|
||||
.attr('height', boundary.height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
|
||||
// Draw boundary title
|
||||
g.append('text')
|
||||
.attr('x', boundary.x + 10)
|
||||
.attr('y', boundary.y + 20)
|
||||
.style('font-size', '16px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('font-family', 'Arial, sans-serif')
|
||||
.style('fill', '#333')
|
||||
.text(boundary.name);
|
||||
}
|
||||
|
||||
// Draw use cases inside the boundary
|
||||
boundary.useCases.forEach((useCase) => {
|
||||
// Draw use case oval
|
||||
g.append('ellipse')
|
||||
.attr('cx', useCase.x + useCase.width / 2)
|
||||
.attr('cy', useCase.y + useCase.height / 2)
|
||||
.attr('rx', useCase.width / 2)
|
||||
.attr('ry', useCase.height / 2)
|
||||
.attr('class', 'usecase-usecase')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333');
|
||||
|
||||
// Draw use case text
|
||||
g.append('text')
|
||||
.attr('x', useCase.x + useCase.width / 2)
|
||||
.attr('y', useCase.y + useCase.height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(useCase.name);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a standalone node as an oval
|
||||
*/
|
||||
const drawNode = (g: any, nodePos: NodePosition): void => {
|
||||
const nodeGroup = g.append('g').attr('class', `node-${nodePos.name}`);
|
||||
|
||||
// Draw oval background
|
||||
nodeGroup.append('ellipse')
|
||||
.attr('cx', nodePos.x + nodePos.width / 2)
|
||||
.attr('cy', nodePos.y + nodePos.height / 2)
|
||||
.attr('rx', nodePos.width / 2)
|
||||
.attr('ry', nodePos.height / 2)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('class', 'usecase-node');
|
||||
|
||||
// Add node label
|
||||
nodeGroup.append('text')
|
||||
.attr('x', nodePos.x + nodePos.width / 2)
|
||||
.attr('y', nodePos.y + nodePos.height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.text(nodePos.label);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship between entities (actor-to-usecase or actor-to-actor)
|
||||
*/
|
||||
const drawRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], boundaryPositions: SystemBoundaryPosition[], conf: any): void => {
|
||||
// Find the source entity (always an actor)
|
||||
const fromEntity = actorPositions.find(a => a.name === relationship.from);
|
||||
if (!fromEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the target entity (could be a use case or another actor)
|
||||
let toEntity: UseCasePosition | ActorPosition | undefined;
|
||||
let isTargetUseCase = false;
|
||||
|
||||
// First check if target is a use case in system boundaries
|
||||
for (const boundary of boundaryPositions) {
|
||||
toEntity = boundary.useCases.find(uc => uc.name === relationship.to);
|
||||
if (toEntity) {
|
||||
isTargetUseCase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in boundaries, check if target is another actor
|
||||
toEntity ??= actorPositions.find(a => a.name === relationship.to);
|
||||
|
||||
if (!toEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate connection points
|
||||
const fromCenterX = fromEntity.x + fromEntity.width / 2;
|
||||
const fromCenterY = fromEntity.y + fromEntity.height / 2;
|
||||
|
||||
// For use cases, connect to the edge (left side), for actors connect to center
|
||||
const toCenterX = isTargetUseCase ? toEntity.x : toEntity.x + toEntity.width / 2;
|
||||
const toCenterY = isTargetUseCase ? toEntity.y + toEntity.height / 2 : toEntity.y + toEntity.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', fromCenterX)
|
||||
.attr('y1', fromCenterY)
|
||||
.attr('x2', toCenterX)
|
||||
.attr('y2', toCenterY)
|
||||
.attr('class', 'usecase-arrow')
|
||||
.attr('stroke', '#333')
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (fromCenterX + toCenterX) / 2;
|
||||
const midY = (fromCenterY + toCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('class', 'usecase-arrow-label')
|
||||
.attr('stroke', '#333')
|
||||
.attr('font-weight', 200)
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship between an actor and a standalone node
|
||||
*/
|
||||
const drawNodeRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
|
||||
// Find the actor position
|
||||
const actor = actorPositions.find(a => a.name === relationship.from);
|
||||
if (!actor) {return};
|
||||
|
||||
// Find the node position
|
||||
const node = nodePositions.find(n => n.name === relationship.to);
|
||||
if (!node) {return};
|
||||
|
||||
// Calculate connection points
|
||||
const actorCenterX = actor.x + actor.width / 2;
|
||||
const actorCenterY = actor.y + actor.height / 2;
|
||||
|
||||
// For nodes (which are like use cases), connect to the edge (left side)
|
||||
const nodeCenterX = node.x;
|
||||
const nodeCenterY = node.y + node.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', actorCenterX)
|
||||
.attr('y1', actorCenterY)
|
||||
.attr('x2', nodeCenterX)
|
||||
.attr('y2', nodeCenterY)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (actorCenterX + nodeCenterX) / 2;
|
||||
const midY = (actorCenterY + nodeCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-family', 'Arial, sans-serif')
|
||||
.attr('fill', '#333')
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship from an inline actor-node definition
|
||||
*/
|
||||
const drawInlineRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
|
||||
// Find the actor position
|
||||
const actor = actorPositions.find(a => a.name === relationship.actor);
|
||||
if (!actor) {return};
|
||||
|
||||
// Find the node position by node ID
|
||||
const node = nodePositions.find(n => n.name === relationship.node.id);
|
||||
if (!node) {return};
|
||||
|
||||
// Calculate connection points
|
||||
const actorCenterX = actor.x + actor.width / 2;
|
||||
const actorCenterY = actor.y + actor.height / 2;
|
||||
|
||||
// For nodes (which are like use cases), connect to the edge (left side)
|
||||
const nodeCenterX = node.x;
|
||||
const nodeCenterY = node.y + node.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', actorCenterX)
|
||||
.attr('y1', actorCenterY)
|
||||
.attr('x2', nodeCenterX)
|
||||
.attr('y2', nodeCenterY)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (actorCenterX + nodeCenterX) / 2;
|
||||
const midY = (actorCenterY + nodeCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-family', 'Arial, sans-serif')
|
||||
.attr('fill', '#333')
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main draw function for useCase diagrams
|
||||
*/
|
||||
const draw = (text: string, id: string, version: string, diagram: Diagram): void => {
|
||||
const db = diagram.db as UseCaseDB;
|
||||
|
||||
log.debug('Drawing useCase diagram', id);
|
||||
|
||||
const actors = db.getActors();
|
||||
const systemBoundaries = db.getSystemBoundaries();
|
||||
const useCases = db.getUseCases();
|
||||
const relationships = db.getRelationships();
|
||||
const nodes = db.getNodes();
|
||||
const nodeRelationships = db.getNodeRelationships();
|
||||
const inlineRelationships = db.getInlineRelationships();
|
||||
|
||||
// Create SVG container - use the same approach as other diagrams
|
||||
const svg = select(`[id="${id}"]`);
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
if (actors.length === 0 && systemBoundaries.length === 0 && useCases.length === 0 && relationships.length === 0 && nodes.length === 0 && nodeRelationships.length === 0 && inlineRelationships.length === 0) {
|
||||
// Empty diagram
|
||||
svg.attr('width', 200);
|
||||
svg.attr('height', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate layout
|
||||
let currentX = MARGIN;
|
||||
let currentY = MARGIN;
|
||||
let maxHeight = 0;
|
||||
|
||||
// Position actors
|
||||
const actorPositions: ActorPosition[] = actors.map((actor, index) => ({
|
||||
name: actor.name,
|
||||
x: currentX + index * ACTOR_SPACING,
|
||||
y: currentY,
|
||||
width: ACTOR_TYPE_WIDTH + 20, // Extra width for text
|
||||
height: ACTOR_HEIGHT,
|
||||
metadata: actor.metadata
|
||||
}));
|
||||
|
||||
if (actors.length > 0) {
|
||||
currentX += actors.length * ACTOR_SPACING;
|
||||
maxHeight = Math.max(maxHeight, ACTOR_HEIGHT + 50);
|
||||
}
|
||||
|
||||
// Position system boundaries
|
||||
const boundaryPositions: SystemBoundaryPosition[] = systemBoundaries.map((boundary, index) => {
|
||||
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
|
||||
const boundaryHeight = 150;
|
||||
|
||||
const position: SystemBoundaryPosition = {
|
||||
name: boundary.name,
|
||||
x: currentX + index * (boundaryWidth + 50),
|
||||
y: currentY,
|
||||
width: boundaryWidth,
|
||||
height: boundaryHeight,
|
||||
metadata: boundary.metadata,
|
||||
useCases: boundary.useCases.map((useCase, ucIndex) => ({
|
||||
name: useCase.name,
|
||||
x: currentX + index * (boundaryWidth + 50) + 20 + ucIndex * 100,
|
||||
y: currentY + 40,
|
||||
width: 80,
|
||||
height: 40
|
||||
}))
|
||||
};
|
||||
|
||||
return position;
|
||||
});
|
||||
|
||||
if (systemBoundaries.length > 0) {
|
||||
const totalBoundaryWidth = systemBoundaries.reduce((sum, boundary, index) => {
|
||||
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
|
||||
return sum + boundaryWidth + (index > 0 ? 50 : 0);
|
||||
}, 0);
|
||||
currentX += totalBoundaryWidth;
|
||||
maxHeight = Math.max(maxHeight, 150);
|
||||
}
|
||||
|
||||
// Position standalone nodes
|
||||
|
||||
const nodePositions: NodePosition[] = [];
|
||||
if (nodes.length > 0) {
|
||||
currentX += 50; // Add some spacing
|
||||
nodes.forEach((node, index) => {
|
||||
const nodeWidth = Math.max(100, node.label.length * 8);
|
||||
const nodeHeight = 40;
|
||||
|
||||
nodePositions.push({
|
||||
name: node.id,
|
||||
label: node.label,
|
||||
x: currentX,
|
||||
y: MARGIN + 50,
|
||||
width: nodeWidth,
|
||||
height: nodeHeight
|
||||
});
|
||||
|
||||
currentX += nodeWidth + 50;
|
||||
});
|
||||
maxHeight = Math.max(maxHeight, 90);
|
||||
}
|
||||
|
||||
// Create main group
|
||||
const g = svg.append('g').attr('class', 'usecase-diagram');
|
||||
|
||||
// Default configuration
|
||||
const conf = {
|
||||
actorFontSize: '14px',
|
||||
actorFontFamily: 'Arial, sans-serif',
|
||||
actorFontWeight: 'normal'
|
||||
};
|
||||
|
||||
// Draw all actors
|
||||
actorPositions.forEach((actorPos) => {
|
||||
const height = drawActorTypeActor(g, actorPos, conf);
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
});
|
||||
|
||||
// Draw system boundaries
|
||||
boundaryPositions.forEach((boundaryPos) => {
|
||||
drawSystemBoundary(g, boundaryPos, conf);
|
||||
});
|
||||
|
||||
// Draw standalone nodes
|
||||
nodePositions.forEach((nodePos) => {
|
||||
drawNode(g, nodePos);
|
||||
});
|
||||
|
||||
// Draw relationships (arrows)
|
||||
relationships.forEach((relationship) => {
|
||||
drawRelationship(g, relationship, actorPositions, boundaryPositions, conf);
|
||||
});
|
||||
|
||||
// Draw node relationships (arrows to standalone nodes)
|
||||
nodeRelationships.forEach((relationship) => {
|
||||
drawNodeRelationship(g, relationship, actorPositions, nodePositions, conf);
|
||||
});
|
||||
|
||||
// Draw inline relationships (from inline actor-node definitions)
|
||||
inlineRelationships.forEach((relationship) => {
|
||||
drawInlineRelationship(g, relationship, actorPositions, nodePositions, conf);
|
||||
});
|
||||
|
||||
// Calculate total dimensions
|
||||
let totalWidth = MARGIN;
|
||||
if (actors.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, actorPositions[actorPositions.length - 1].x + actorPositions[actorPositions.length - 1].width + MARGIN);
|
||||
}
|
||||
if (systemBoundaries.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, boundaryPositions[boundaryPositions.length - 1].x + boundaryPositions[boundaryPositions.length - 1].width + MARGIN);
|
||||
}
|
||||
if (nodePositions.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, nodePositions[nodePositions.length - 1].x + nodePositions[nodePositions.length - 1].width + MARGIN);
|
||||
}
|
||||
|
||||
const totalHeight = MARGIN + maxHeight + MARGIN;
|
||||
|
||||
// Set SVG dimensions
|
||||
svg.attr('width', totalWidth);
|
||||
svg.attr('height', totalHeight);
|
||||
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
|
||||
};
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
||||
@@ -203,6 +203,7 @@ function sidebarConfig() {
|
||||
{ text: 'Accessibility', link: '/config/accessibility' },
|
||||
{ text: 'Mermaid CLI', link: '/config/mermaidCLI' },
|
||||
{ text: 'FAQ', link: '/config/faq' },
|
||||
{ text: 'Layouts', link: '/config/layouts' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
|
||||
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 specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||
|
||||
24
packages/mermaid/src/docs/config/layouts.md
Normal file
24
packages/mermaid/src/docs/config/layouts.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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;
|
||||
```
|
||||
49
packages/mermaid/src/docs/config/tidy-tree.md
Normal file
49
packages/mermaid/src/docs/config/tidy-tree.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
@@ -209,3 +209,22 @@ You can also refer the [implementation in the live editor](https://github.com/me
|
||||
cspell:locale en,en-gb
|
||||
cspell:ignore Buzan
|
||||
--->
|
||||
|
||||
## Layouts
|
||||
|
||||
Mermaid also supports a Tidy Tree layout for mindmaps.
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
```
|
||||
|
||||
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)
|
||||
|
||||
@@ -126,7 +126,7 @@ xychart
|
||||
|
||||
## Chart Theme Variables
|
||||
|
||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
||||
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -151,6 +151,31 @@ config:
|
||||
| yAxisLineColor | Color of the y-axis line |
|
||||
| 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
|
||||
|
||||
```mermaid-example
|
||||
|
||||
@@ -26,6 +26,7 @@ const processFrontmatter = (code: string) => {
|
||||
}
|
||||
config.gantt.displayMode = displayMode;
|
||||
}
|
||||
|
||||
return { title, config, text };
|
||||
};
|
||||
|
||||
|
||||
148
packages/mermaid/src/rendering-util/createGraph.ts
Normal file
148
packages/mermaid/src/rendering-util/createGraph.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { insertNode } from './rendering-elements/nodes.js';
|
||||
import type { LayoutData, NonClusterNode } from './types.ts';
|
||||
import type { Selection } from 'd3';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
|
||||
// Update type:
|
||||
type D3Selection<T extends SVGElement = SVGElement> = Selection<
|
||||
T,
|
||||
unknown,
|
||||
Element | null,
|
||||
unknown
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a graph by merging the graph construction and DOM element insertion.
|
||||
*
|
||||
* This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes)
|
||||
* into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions
|
||||
* are computed using each node's bounding box.
|
||||
*
|
||||
* @param element - The D3 selection in which the SVG groups are inserted.
|
||||
* @param data4Layout - The layout data containing nodes and edges.
|
||||
* @returns A promise resolving to an object containing the Graphology graph and the inserted groups.
|
||||
*/
|
||||
export async function createGraphWithElements(
|
||||
element: D3Selection,
|
||||
data4Layout: LayoutData
|
||||
): Promise<{
|
||||
graph: graphlib.Graph;
|
||||
groups: {
|
||||
clusters: D3Selection<SVGGElement>;
|
||||
edgePaths: D3Selection<SVGGElement>;
|
||||
edgeLabels: D3Selection<SVGGElement>;
|
||||
nodes: D3Selection<SVGGElement>;
|
||||
rootGroups: D3Selection<SVGGElement>;
|
||||
};
|
||||
nodeElements: Map<string, D3Selection<SVGElement | SVGGElement>>;
|
||||
}> {
|
||||
// Create a directed, multi graph.
|
||||
const graph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
});
|
||||
const edgesToProcess = [...data4Layout.edges];
|
||||
const config = getConfig();
|
||||
// Create groups for clusters, edge paths, edge labels, and nodes.
|
||||
const clusters = element.insert('g').attr('class', 'clusters');
|
||||
const edgePaths = element.insert('g').attr('class', 'edges edgePath');
|
||||
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||
const nodesGroup = element.insert('g').attr('class', 'nodes');
|
||||
const rootGroups = element.insert('g').attr('class', 'root');
|
||||
|
||||
const nodeElements = new Map<string, D3Selection<SVGElement | SVGGElement>>();
|
||||
|
||||
// Insert nodes into the DOM and add them to the graph.
|
||||
await Promise.all(
|
||||
data4Layout.nodes.map(async (node) => {
|
||||
if (node.isGroup) {
|
||||
graph.setNode(node.id, { ...node });
|
||||
} else {
|
||||
const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir });
|
||||
const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
|
||||
nodeElements.set(node.id, childNodeEl as D3Selection<SVGElement | SVGGElement>);
|
||||
node.width = boundingBox.width;
|
||||
node.height = boundingBox.height;
|
||||
graph.setNode(node.id, { ...node });
|
||||
}
|
||||
})
|
||||
);
|
||||
// Add edges to the graph.
|
||||
for (const edge of edgesToProcess) {
|
||||
if (edge.label && edge.label?.length > 0) {
|
||||
// Create a label node for the edge
|
||||
const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`;
|
||||
const labelNode = {
|
||||
id: labelNodeId,
|
||||
label: edge.label,
|
||||
edgeStart: edge.start,
|
||||
edgeEnd: edge.end,
|
||||
shape: 'labelRect',
|
||||
width: 0, // Will be updated after insertion
|
||||
height: 0, // Will be updated after insertion
|
||||
isEdgeLabel: true,
|
||||
isDummy: true,
|
||||
isGroup: false,
|
||||
parentId: edge.parentId,
|
||||
...(edge.dir ? { dir: edge.dir } : {}),
|
||||
} as NonClusterNode;
|
||||
|
||||
// Insert the label node into the DOM
|
||||
const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: edge.dir });
|
||||
const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
|
||||
|
||||
// Update node dimensions
|
||||
labelNode.width = boundingBox.width;
|
||||
labelNode.height = boundingBox.height;
|
||||
|
||||
// Add to graph and tracking maps
|
||||
graph.setNode(labelNodeId, { ...labelNode });
|
||||
nodeElements.set(labelNodeId, labelNodeEl as D3Selection<SVGElement | SVGGElement>);
|
||||
data4Layout.nodes.push(labelNode);
|
||||
|
||||
// Create two edges to replace the original one
|
||||
const edgeToLabel = {
|
||||
...edge,
|
||||
id: `${edge.id}-to-label`,
|
||||
end: labelNodeId,
|
||||
label: undefined,
|
||||
isLabelEdge: true,
|
||||
arrowTypeEnd: 'none',
|
||||
arrowTypeStart: 'none',
|
||||
};
|
||||
const edgeFromLabel = {
|
||||
...edge,
|
||||
id: `${edge.id}-from-label`,
|
||||
start: labelNodeId,
|
||||
end: edge.end,
|
||||
label: undefined,
|
||||
isLabelEdge: true,
|
||||
arrowTypeStart: 'none',
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
};
|
||||
graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel });
|
||||
graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel });
|
||||
data4Layout.edges.push(edgeToLabel, edgeFromLabel);
|
||||
const edgeIdToRemove = edge.id;
|
||||
data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove);
|
||||
const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id);
|
||||
if (indexInOriginal !== -1) {
|
||||
data4Layout.edges.splice(indexInOriginal, 1);
|
||||
}
|
||||
} else {
|
||||
// Regular edge without label
|
||||
graph.setEdge(edge.id, edge.start, edge.end, { ...edge });
|
||||
const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id);
|
||||
if (!edgeExists) {
|
||||
data4Layout.edges.push(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
graph,
|
||||
groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups },
|
||||
nodeElements,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import cytoscape from 'cytoscape';
|
||||
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||
import { select } from 'd3';
|
||||
import { log } from '../../../logger.js';
|
||||
import type { LayoutData, Node, Edge } from '../../types.js';
|
||||
import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js';
|
||||
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
/**
|
||||
* Declare module augmentation for cytoscape edge types
|
||||
*/
|
||||
declare module 'cytoscape' {
|
||||
interface EdgeSingular {
|
||||
_private: {
|
||||
bodyBounds: unknown;
|
||||
rscratch: {
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nodes to cytoscape instance from provided node array
|
||||
* This function processes only the nodes provided in the data structure
|
||||
* @param nodes - Array of nodes to add
|
||||
* @param cy - The cytoscape instance
|
||||
*/
|
||||
export function addNodes(nodes: Node[], cy: cytoscape.Core): void {
|
||||
nodes.forEach((node) => {
|
||||
const nodeData: Record<string, unknown> = {
|
||||
id: node.id,
|
||||
labelText: node.label,
|
||||
height: node.height,
|
||||
width: node.width,
|
||||
padding: node.padding ?? 0,
|
||||
};
|
||||
|
||||
// Add any additional properties from the node
|
||||
Object.keys(node).forEach((key) => {
|
||||
if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) {
|
||||
nodeData[key] = (node as unknown as Record<string, unknown>)[key];
|
||||
}
|
||||
});
|
||||
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: nodeData,
|
||||
position: {
|
||||
x: node.x ?? 0,
|
||||
y: node.y ?? 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add edges to cytoscape instance from provided edge array
|
||||
* This function processes only the edges provided in the data structure
|
||||
* @param edges - Array of edges to add
|
||||
* @param cy - The cytoscape instance
|
||||
*/
|
||||
export function addEdges(edges: Edge[], cy: cytoscape.Core): void {
|
||||
edges.forEach((edge) => {
|
||||
const edgeData: Record<string, unknown> = {
|
||||
id: edge.id,
|
||||
source: edge.start,
|
||||
target: edge.end,
|
||||
};
|
||||
|
||||
// Add any additional properties from the edge
|
||||
Object.keys(edge).forEach((key) => {
|
||||
if (!['id', 'start', 'end'].includes(key)) {
|
||||
edgeData[key] = (edge as unknown as Record<string, unknown>)[key];
|
||||
}
|
||||
});
|
||||
|
||||
cy.add({
|
||||
group: 'edges',
|
||||
data: edgeData,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure cytoscape instance
|
||||
* @param data - Layout data containing nodes and edges
|
||||
* @returns Promise resolving to configured cytoscape instance
|
||||
*/
|
||||
export function createCytoscapeInstance(data: LayoutData): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
// Add temporary render element
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
|
||||
const cy = cytoscape({
|
||||
container: document.getElementById('cy'), // container to render in
|
||||
style: [
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
|
||||
// Add all nodes and edges to cytoscape using the generic functions
|
||||
addNodes(data.nodes, cy);
|
||||
addEdges(data.edges, cy);
|
||||
|
||||
// Make cytoscape care about the dimensions of the nodes
|
||||
cy.nodes().forEach(function (n) {
|
||||
n.layoutDimensions = () => {
|
||||
const nodeData = n.data();
|
||||
return { w: nodeData.width, h: nodeData.height };
|
||||
};
|
||||
});
|
||||
|
||||
// Configure and run the cose-bilkent layout
|
||||
const layoutConfig: CytoscapeLayoutConfig = {
|
||||
name: 'cose-bilkent',
|
||||
// @ts-ignore Types for cose-bilkent are not correct?
|
||||
quality: 'proof',
|
||||
styleEnabled: false,
|
||||
animate: false,
|
||||
};
|
||||
|
||||
cy.layout(layoutConfig).run();
|
||||
|
||||
cy.ready((e) => {
|
||||
log.info('Cytoscape ready', e);
|
||||
resolve(cy);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract positioned nodes from cytoscape instance
|
||||
* @param cy - The cytoscape instance after layout
|
||||
* @returns Array of positioned nodes
|
||||
*/
|
||||
export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] {
|
||||
return cy.nodes().map((node) => {
|
||||
const data = node.data();
|
||||
const position = node.position();
|
||||
|
||||
// Create a positioned node with all original data plus position
|
||||
const positionedNode: PositionedNode = {
|
||||
id: data.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
};
|
||||
|
||||
// Add all other properties from the original data
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (key !== 'id') {
|
||||
positionedNode[key] = data[key];
|
||||
}
|
||||
});
|
||||
|
||||
return positionedNode;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract positioned edges from cytoscape instance
|
||||
* @param cy - The cytoscape instance after layout
|
||||
* @returns Array of positioned edges
|
||||
*/
|
||||
export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] {
|
||||
return cy.edges().map((edge) => {
|
||||
const data = edge.data();
|
||||
const rscratch = edge._private.rscratch;
|
||||
|
||||
// Create a positioned edge with all original data plus position
|
||||
const positionedEdge: PositionedEdge = {
|
||||
id: data.id,
|
||||
source: data.source,
|
||||
target: data.target,
|
||||
startX: rscratch.startX,
|
||||
startY: rscratch.startY,
|
||||
midX: rscratch.midX,
|
||||
midY: rscratch.midY,
|
||||
endX: rscratch.endX,
|
||||
endY: rscratch.endY,
|
||||
};
|
||||
|
||||
// Add all other properties from the original data
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (!['id', 'source', 'target'].includes(key)) {
|
||||
positionedEdge[key] = data[key];
|
||||
}
|
||||
});
|
||||
|
||||
return positionedEdge;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { render as renderWithCoseBilkent } from './render.js';
|
||||
|
||||
/**
|
||||
* Cose-Bilkent Layout Algorithm for Generic Diagrams
|
||||
*
|
||||
* This module provides a layout algorithm implementation using Cytoscape
|
||||
* with the cose-bilkent algorithm for positioning nodes and edges.
|
||||
*
|
||||
* The algorithm follows the unified rendering pattern and can be used
|
||||
* by any diagram type that provides compatible LayoutData.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render function for the cose-bilkent layout algorithm
|
||||
*
|
||||
* This function follows the unified rendering pattern used by all layout algorithms.
|
||||
* It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
|
||||
* and renders the positioned elements to the SVG.
|
||||
*
|
||||
* @param layoutData - Layout data containing nodes, edges, and configuration
|
||||
* @param svg - SVG element to render to
|
||||
* @param helpers - Internal helper functions for rendering
|
||||
* @param options - Rendering options
|
||||
*/
|
||||
export const render = renderWithCoseBilkent;
|
||||
@@ -0,0 +1,236 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { MermaidConfig } from '../../../config.type.js';
|
||||
import { log } from '../../../logger.js';
|
||||
import type { LayoutData } from '../../types.js';
|
||||
import type { LayoutResult } from './types.js';
|
||||
import {
|
||||
createCytoscapeInstance,
|
||||
extractPositionedNodes,
|
||||
extractPositionedEdges,
|
||||
} from './cytoscape-setup.js';
|
||||
|
||||
/**
|
||||
* Execute the cose-bilkent layout algorithm on generic layout data
|
||||
*
|
||||
* This function takes layout data and uses Cytoscape with the cose-bilkent
|
||||
* algorithm to calculate optimal node positions and edge paths.
|
||||
*
|
||||
* @param data - The layout data containing nodes, edges, and configuration
|
||||
* @param config - Mermaid configuration object
|
||||
* @returns Promise resolving to layout result with positioned nodes and edges
|
||||
*/
|
||||
export async function executeCoseBilkentLayout(
|
||||
data: LayoutData,
|
||||
_config: MermaidConfig
|
||||
): Promise<LayoutResult> {
|
||||
log.debug('Starting cose-bilkent layout algorithm');
|
||||
|
||||
try {
|
||||
// Validate layout data structure
|
||||
validateLayoutData(data);
|
||||
|
||||
// Create and configure cytoscape instance
|
||||
const cy = await createCytoscapeInstance(data);
|
||||
|
||||
// Extract positioned nodes and edges after layout
|
||||
const positionedNodes = extractPositionedNodes(cy);
|
||||
const positionedEdges = extractPositionedEdges(cy);
|
||||
|
||||
log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`);
|
||||
|
||||
return {
|
||||
nodes: positionedNodes,
|
||||
edges: positionedEdges,
|
||||
};
|
||||
} catch (error) {
|
||||
log.error('Error in cose-bilkent layout algorithm:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate layout data structure
|
||||
* @param data - The data to validate
|
||||
* @returns True if data is valid, throws error otherwise
|
||||
*/
|
||||
export function validateLayoutData(data: LayoutData): boolean {
|
||||
if (!data) {
|
||||
throw new Error('Layout data is required');
|
||||
}
|
||||
|
||||
if (!data.config) {
|
||||
throw new Error('Configuration is required in layout data');
|
||||
}
|
||||
|
||||
if (!data.rootNode) {
|
||||
throw new Error('Root node is required');
|
||||
}
|
||||
|
||||
if (!data.nodes || !Array.isArray(data.nodes)) {
|
||||
throw new Error('No nodes found in layout data');
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.edges)) {
|
||||
throw new Error('Edges array is required in layout data');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
|
||||
import { executeCoseBilkentLayout } from './layout.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
|
||||
type Node = Record<string, unknown>;
|
||||
|
||||
interface NodeWithPosition extends Node {
|
||||
x?: number;
|
||||
y?: number;
|
||||
domId?: string | SVGGroup | D3Selection<SVGAElement>;
|
||||
width?: number;
|
||||
height?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render function for cose-bilkent layout algorithm
|
||||
*
|
||||
* This follows the same pattern as ELK and dagre renderers:
|
||||
* 1. Insert nodes into DOM to get their actual dimensions
|
||||
* 2. Run the layout algorithm to calculate positions
|
||||
* 3. Position the nodes and edges based on layout results
|
||||
*/
|
||||
export const render = async (
|
||||
data4Layout: LayoutData,
|
||||
svg: SVG,
|
||||
{
|
||||
insertCluster,
|
||||
insertEdge,
|
||||
insertEdgeLabel,
|
||||
insertMarkers,
|
||||
insertNode,
|
||||
log,
|
||||
positionEdgeLabel,
|
||||
}: InternalHelpers,
|
||||
{ algorithm: _algorithm }: RenderOptions
|
||||
) => {
|
||||
const nodeDb: Record<string, NodeWithPosition> = {};
|
||||
const clusterDb: Record<string, any> = {};
|
||||
|
||||
// Insert markers for edges
|
||||
const element = svg.select('g');
|
||||
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||
|
||||
// Create container groups
|
||||
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
|
||||
const edgePaths = element.insert('g').attr('class', 'edgePaths');
|
||||
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||
const nodes = element.insert('g').attr('class', 'nodes');
|
||||
|
||||
// Step 1: Insert nodes into DOM to get their actual dimensions
|
||||
log.debug('Inserting nodes into DOM for dimension calculation');
|
||||
|
||||
await Promise.all(
|
||||
data4Layout.nodes.map(async (node) => {
|
||||
if (node.isGroup) {
|
||||
// Handle subgraphs/clusters
|
||||
const clusterNode: NodeWithPosition = { ...node };
|
||||
clusterDb[node.id] = clusterNode;
|
||||
nodeDb[node.id] = clusterNode;
|
||||
|
||||
// Insert cluster to get dimensions
|
||||
await insertCluster(subGraphsEl, node);
|
||||
} else {
|
||||
// Handle regular nodes
|
||||
const nodeWithPosition: NodeWithPosition = { ...node };
|
||||
nodeDb[node.id] = nodeWithPosition;
|
||||
|
||||
// Insert node to get actual dimensions
|
||||
const nodeEl = await insertNode(nodes, node, {
|
||||
config: data4Layout.config,
|
||||
dir: data4Layout.direction || 'TB',
|
||||
});
|
||||
|
||||
// Get the actual bounding box after insertion
|
||||
const boundingBox = nodeEl.node()!.getBBox();
|
||||
nodeWithPosition.width = boundingBox.width;
|
||||
nodeWithPosition.height = boundingBox.height;
|
||||
nodeWithPosition.domId = nodeEl;
|
||||
|
||||
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Step 2: Run the cose-bilkent layout algorithm
|
||||
log.debug('Running cose-bilkent layout algorithm');
|
||||
|
||||
// Update the layout data with actual dimensions
|
||||
const updatedLayoutData = {
|
||||
...data4Layout,
|
||||
nodes: data4Layout.nodes.map((node) => {
|
||||
const nodeWithDimensions = nodeDb[node.id];
|
||||
return {
|
||||
...node,
|
||||
width: nodeWithDimensions.width,
|
||||
height: nodeWithDimensions.height,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
|
||||
|
||||
// Step 3: Position the nodes based on layout results
|
||||
log.debug('Positioning nodes based on layout results');
|
||||
|
||||
layoutResult.nodes.forEach((positionedNode) => {
|
||||
const node = nodeDb[positionedNode.id];
|
||||
if (node?.domId) {
|
||||
// Position the node at the calculated coordinates
|
||||
// The positionedNode.x/y represents the center of the node, so use directly
|
||||
(node.domId as D3Selection<SVGAElement>).attr(
|
||||
'transform',
|
||||
`translate(${positionedNode.x}, ${positionedNode.y})`
|
||||
);
|
||||
|
||||
// Store the final position
|
||||
node.x = positionedNode.x;
|
||||
node.y = positionedNode.y;
|
||||
|
||||
log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
|
||||
}
|
||||
});
|
||||
|
||||
layoutResult.edges.forEach((positionedEdge) => {
|
||||
const edge = data4Layout.edges.find((e) => e.id === positionedEdge.id);
|
||||
if (edge) {
|
||||
// Update the edge data with positioned coordinates
|
||||
edge.points = [
|
||||
{ x: positionedEdge.startX, y: positionedEdge.startY },
|
||||
{ x: positionedEdge.midX, y: positionedEdge.midY },
|
||||
{ x: positionedEdge.endX, y: positionedEdge.endY },
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// Step 4: Insert and position edges
|
||||
log.debug('Inserting and positioning edges');
|
||||
|
||||
await Promise.all(
|
||||
data4Layout.edges.map(async (edge) => {
|
||||
// Insert edge label first
|
||||
const _edgeLabel = await insertEdgeLabel(edgeLabels, edge);
|
||||
|
||||
// Get start and end nodes
|
||||
const startNode = nodeDb[edge.start ?? ''];
|
||||
const endNode = nodeDb[edge.end ?? ''];
|
||||
|
||||
if (startNode && endNode) {
|
||||
// Find the positioned edge data
|
||||
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||
|
||||
if (positionedEdge) {
|
||||
log.debug('APA01 positionedEdge', positionedEdge);
|
||||
// Create edge path with positioned coordinates
|
||||
const edgeWithPath = { ...edge };
|
||||
|
||||
// Insert the edge path
|
||||
const paths = insertEdge(
|
||||
edgePaths,
|
||||
edgeWithPath,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
|
||||
// Position the edge label
|
||||
positionEdgeLabel(edgeWithPath, paths);
|
||||
} else {
|
||||
// Fallback: create a simple straight line between nodes
|
||||
const edgeWithPath = {
|
||||
...edge,
|
||||
points: [
|
||||
{ x: startNode.x || 0, y: startNode.y || 0 },
|
||||
{ x: endNode.x || 0, y: endNode.y || 0 },
|
||||
],
|
||||
};
|
||||
|
||||
const paths = insertEdge(
|
||||
edgePaths,
|
||||
edgeWithPath,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
positionEdgeLabel(edgeWithPath, paths);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
log.debug('Cose-bilkent rendering completed');
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Positioned node after layout calculation
|
||||
*/
|
||||
export interface PositionedNode {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
[key: string]: unknown; // Allow additional properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Positioned edge after layout calculation
|
||||
*/
|
||||
export interface PositionedEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
[key: string]: unknown; // Allow additional properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of layout algorithm execution
|
||||
*/
|
||||
export interface LayoutResult {
|
||||
nodes: PositionedNode[];
|
||||
edges: PositionedEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cytoscape layout configuration
|
||||
*/
|
||||
export interface CytoscapeLayoutConfig {
|
||||
name: 'cose-bilkent';
|
||||
quality: 'proof';
|
||||
styleEnabled: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
@@ -39,6 +39,14 @@ const registerDefaultLayoutLoaders = () => {
|
||||
name: 'dagre',
|
||||
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||
},
|
||||
...(includeLargeFeatures
|
||||
? [
|
||||
{
|
||||
name: 'cose-bilkent',
|
||||
loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -438,7 +438,6 @@ const fixCorners = function (lineData) {
|
||||
}
|
||||
return newLineData;
|
||||
};
|
||||
|
||||
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
|
||||
const { handDrawnSeed } = getConfig();
|
||||
let points = edge.points;
|
||||
@@ -622,9 +621,9 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
// lineData.forEach((point) => {
|
||||
// elem
|
||||
// .append('circle')
|
||||
// .style('stroke', 'blue')
|
||||
// .style('fill', 'blue')
|
||||
// .attr('r', 3)
|
||||
// .style('stroke', 'red')
|
||||
// .style('fill', 'red')
|
||||
// .attr('r', 1)
|
||||
// .attr('cx', point.x)
|
||||
// .attr('cy', point.y);
|
||||
// });
|
||||
|
||||
@@ -2,64 +2,63 @@
|
||||
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
|
||||
*/
|
||||
function intersectLine(p1, p2, q1, q2) {
|
||||
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||
// p7 and p473.
|
||||
{
|
||||
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||
// p7 and p473.
|
||||
|
||||
var a1, a2, b1, b2, c1, c2;
|
||||
var r1, r2, r3, r4;
|
||||
var denom, offset, num;
|
||||
var x, y;
|
||||
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||
// b1 y + c1 = 0.
|
||||
const a1 = p2.y - p1.y;
|
||||
const b1 = p1.x - p2.x;
|
||||
const c1 = p2.x * p1.y - p1.x * p2.y;
|
||||
|
||||
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||
// b1 y + c1 = 0.
|
||||
a1 = p2.y - p1.y;
|
||||
b1 = p1.x - p2.x;
|
||||
c1 = p2.x * p1.y - p1.x * p2.y;
|
||||
// Compute r3 and r4.
|
||||
const r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||
const r4 = a1 * q2.x + b1 * q2.y + c1;
|
||||
|
||||
// Compute r3 and r4.
|
||||
r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||
r4 = a1 * q2.x + b1 * q2.y + c1;
|
||||
const epsilon = 1e-6;
|
||||
|
||||
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
||||
// same side of line 1, the line segments do not intersect.
|
||||
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
||||
return /*DON'T_INTERSECT*/;
|
||||
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
||||
// same side of line 1, the line segments do not intersect.
|
||||
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
||||
return /*DON'T_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
||||
const a2 = q2.y - q1.y;
|
||||
const b2 = q1.x - q2.x;
|
||||
const c2 = q2.x * q1.y - q1.x * q2.y;
|
||||
|
||||
// Compute r1 and r2
|
||||
const r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||
const r2 = a2 * p2.x + b2 * p2.y + c2;
|
||||
|
||||
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
||||
// on same side of second line segment, the line segments do
|
||||
// not intersect.
|
||||
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
|
||||
return /*DON'T_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Line segments intersect: compute intersection point.
|
||||
const denom = a1 * b2 - a2 * b1;
|
||||
if (denom === 0) {
|
||||
return /*COLLINEAR*/;
|
||||
}
|
||||
|
||||
const offset = Math.abs(denom / 2);
|
||||
|
||||
// The denom/2 is to get rounding instead of truncating. It
|
||||
// is added or subtracted to the numerator, depending upon the
|
||||
// sign of the numerator.
|
||||
let num = b1 * c2 - b2 * c1;
|
||||
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
num = a2 * c1 - a1 * c2;
|
||||
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
return { x: x, y: y };
|
||||
}
|
||||
|
||||
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
||||
a2 = q2.y - q1.y;
|
||||
b2 = q1.x - q2.x;
|
||||
c2 = q2.x * q1.y - q1.x * q2.y;
|
||||
|
||||
// Compute r1 and r2
|
||||
r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||
r2 = a2 * p2.x + b2 * p2.y + c2;
|
||||
|
||||
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
||||
// on same side of second line segment, the line segments do
|
||||
// not intersect.
|
||||
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
|
||||
return /*DON'T_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Line segments intersect: compute intersection point.
|
||||
denom = a1 * b2 - a2 * b1;
|
||||
if (denom === 0) {
|
||||
return /*COLLINEAR*/;
|
||||
}
|
||||
|
||||
offset = Math.abs(denom / 2);
|
||||
|
||||
// The denom/2 is to get rounding instead of truncating. It
|
||||
// is added or subtracted to the numerator, depending upon the
|
||||
// sign of the numerator.
|
||||
num = b1 * c2 - b2 * c1;
|
||||
x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
num = a2 * c1 - a1 * c2;
|
||||
y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
return { x: x, y: y };
|
||||
}
|
||||
|
||||
function sameSign(r1, r2) {
|
||||
|
||||
@@ -61,6 +61,10 @@ import { erBox } from './shapes/erBox.js';
|
||||
import { classBox } from './shapes/classBox.js';
|
||||
import { requirementBox } from './shapes/requirementBox.js';
|
||||
import { kanbanItem } from './shapes/kanbanItem.js';
|
||||
import { bang } from './shapes/bang.js';
|
||||
import { cloud } from './shapes/cloud.js';
|
||||
import { defaultMindmapNode } from './shapes/defaultMindmapNode.js';
|
||||
import { mindmapCircle } from './shapes/mindmapCircle.js';
|
||||
|
||||
type ShapeHandler = <T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
@@ -135,6 +139,22 @@ export const shapesDefs = [
|
||||
aliases: ['circ'],
|
||||
handler: circle,
|
||||
},
|
||||
{
|
||||
semanticName: 'Bang',
|
||||
name: 'Bang',
|
||||
shortName: 'bang',
|
||||
description: 'Bang',
|
||||
aliases: ['bang'],
|
||||
handler: bang,
|
||||
},
|
||||
{
|
||||
semanticName: 'Cloud',
|
||||
name: 'Cloud',
|
||||
shortName: 'cloud',
|
||||
description: 'cloud',
|
||||
aliases: ['cloud'],
|
||||
handler: cloud,
|
||||
},
|
||||
{
|
||||
semanticName: 'Decision',
|
||||
name: 'Diamond',
|
||||
@@ -476,6 +496,9 @@ const generateShapeMap = () => {
|
||||
// Kanban diagram
|
||||
kanbanItem,
|
||||
|
||||
//Mindmap diagram
|
||||
mindmapCircle,
|
||||
defaultMindmapNode,
|
||||
// class diagram
|
||||
classBox,
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { log } from '../../../logger.js';
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { handleUndefinedAttr } from '../../../utils.js';
|
||||
import type { Bounds, Point } from '../../../types.js';
|
||||
|
||||
export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getNodeClasses(node)
|
||||
);
|
||||
|
||||
const w = bbox.width + 10 * halfPadding;
|
||||
const h = bbox.height + 8 * halfPadding;
|
||||
const r = 0.15 * w;
|
||||
const { cssStyles } = node;
|
||||
|
||||
const minWidth = bbox.width + 20;
|
||||
const minHeight = bbox.height + 20;
|
||||
const effectiveWidth = Math.max(w, minWidth);
|
||||
const effectiveHeight = Math.max(h, minHeight);
|
||||
|
||||
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
||||
|
||||
let bangElem;
|
||||
const path = `M0 0
|
||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1}
|
||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0}
|
||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0}
|
||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${effectiveHeight * 0.1}
|
||||
|
||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.15},${effectiveHeight * 0.33}
|
||||
a${r * 0.8},${r * 0.8} 1 0,0 0,${effectiveHeight * 0.34}
|
||||
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.15},${effectiveHeight * 0.33}
|
||||
|
||||
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${effectiveHeight * 0.15}
|
||||
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0
|
||||
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0
|
||||
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${-1 * effectiveHeight * 0.15}
|
||||
|
||||
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
|
||||
a${r * 0.8},${r * 0.8} 1 0,0 0,${-1 * effectiveHeight * 0.34}
|
||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
|
||||
H0 V0 Z`;
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
const roughNode = rc.path(path, options);
|
||||
bangElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||
bangElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||
} else {
|
||||
bangElem = shapeSvg
|
||||
.insert('path', ':first-child')
|
||||
.attr('class', 'basic label-container')
|
||||
.attr('style', nodeStyles)
|
||||
.attr('d', path);
|
||||
}
|
||||
|
||||
// Translate the path (center the shape)
|
||||
bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`);
|
||||
|
||||
updateNodeBounds(node, bangElem);
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
return intersect.rect(bounds, point);
|
||||
};
|
||||
node.intersect = function (point) {
|
||||
log.info('Bang intersect', node, point);
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import { log } from '../../../logger.js';
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { log } from '../../../logger.js';
|
||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||
import { handleUndefinedAttr } from '../../../utils.js';
|
||||
import type { MindmapOptions, Node, ShapeRenderOptions } from '../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||
|
||||
export async function circle<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||
export async function circle<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node,
|
||||
options?: MindmapOptions | ShapeRenderOptions
|
||||
) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
const radius = bbox.width / 2 + halfPadding;
|
||||
const padding = options?.padding ?? halfPadding;
|
||||
const radius = bbox.width / 2 + padding;
|
||||
let circleElem;
|
||||
const { cssStyles } = node;
|
||||
|
||||
@@ -35,7 +39,10 @@ export async function circle<T extends SVGGraphicsElement>(parent: D3Selection<T
|
||||
}
|
||||
|
||||
updateNodeBounds(node, circleElem);
|
||||
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
const radius = bounds.width / 2;
|
||||
return intersect.circle(bounds, radius, point);
|
||||
};
|
||||
node.intersect = function (point) {
|
||||
log.info('Circle intersect', node, radius, point);
|
||||
return intersect.circle(node, radius, point);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import rough from 'roughjs';
|
||||
import { log } from '../../../logger.js';
|
||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||
import { handleUndefinedAttr } from '../../../utils.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||
|
||||
export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
|
||||
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getNodeClasses(node)
|
||||
);
|
||||
|
||||
const w = bbox.width + 2 * halfPadding;
|
||||
const h = bbox.height + 2 * halfPadding;
|
||||
|
||||
// Cloud radii
|
||||
const r1 = 0.15 * w;
|
||||
const r2 = 0.25 * w;
|
||||
const r3 = 0.35 * w;
|
||||
const r4 = 0.2 * w;
|
||||
|
||||
const { cssStyles } = node;
|
||||
let cloudElem;
|
||||
|
||||
// Cloud path
|
||||
const path = `M0 0
|
||||
a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1}
|
||||
a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1}
|
||||
a${r2},${r2} 1 0,1 ${w * 0.35},${w * 0.2}
|
||||
|
||||
a${r1},${r1} 1 0,1 ${w * 0.15},${h * 0.35}
|
||||
a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${h * 0.65}
|
||||
|
||||
a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15}
|
||||
a${r3},${r3} 1 0,1 ${-1 * w * 0.5},0
|
||||
a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15}
|
||||
|
||||
a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35}
|
||||
a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65}
|
||||
H0 V0 Z`;
|
||||
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
const roughNode = rc.path(path, options);
|
||||
cloudElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||
cloudElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||
} else {
|
||||
cloudElem = shapeSvg
|
||||
.insert('path', ':first-child')
|
||||
.attr('class', 'basic label-container')
|
||||
.attr('style', nodeStyles)
|
||||
.attr('d', path);
|
||||
}
|
||||
|
||||
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
||||
|
||||
// Center the shape
|
||||
cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
|
||||
|
||||
updateNodeBounds(node, cloudElem);
|
||||
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
return intersect.rect(bounds, point);
|
||||
};
|
||||
node.intersect = function (point) {
|
||||
log.info('Cloud intersect', node, point);
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import { styles2String } from './handDrawnShapeStyles.js';
|
||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||
|
||||
export async function defaultMindmapNode<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
|
||||
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getNodeClasses(node)
|
||||
);
|
||||
|
||||
const w = bbox.width + 8 * halfPadding;
|
||||
const h = bbox.height + 2 * halfPadding;
|
||||
const rd = 5;
|
||||
|
||||
const rectPath = `
|
||||
M${-w / 2} ${h / 2 - rd}
|
||||
v${-h + 2 * rd}
|
||||
q0,-${rd} ${rd},-${rd}
|
||||
h${w - 2 * rd}
|
||||
q${rd},0 ${rd},${rd}
|
||||
v${h - 2 * rd}
|
||||
q0,${rd} -${rd},${rd}
|
||||
h${-w + 2 * rd}
|
||||
q-${rd},0 -${rd},-${rd}
|
||||
Z
|
||||
`;
|
||||
|
||||
const bg = shapeSvg
|
||||
.append('path')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + node.type)
|
||||
.attr('style', nodeStyles)
|
||||
.attr('d', rectPath);
|
||||
|
||||
shapeSvg
|
||||
.append('line')
|
||||
.attr('class', 'node-line-')
|
||||
.attr('x1', -w / 2)
|
||||
.attr('y1', h / 2)
|
||||
.attr('x2', w / 2)
|
||||
.attr('y2', h / 2);
|
||||
|
||||
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
||||
shapeSvg.append(() => label.node());
|
||||
|
||||
updateNodeBounds(node, bg);
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
return intersect.rect(bounds, point);
|
||||
};
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { handleUndefinedAttr } from '../../../utils.js';
|
||||
import type { Bounds, Point } from '../../../types.js';
|
||||
|
||||
export async function drawRect<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
@@ -62,6 +63,10 @@ export async function drawRect<T extends SVGGraphicsElement>(
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
return intersect.rect(bounds, point);
|
||||
};
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { circle } from './circle.js';
|
||||
import type { Node, MindmapOptions } from '../../types.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
|
||||
export async function mindmapCircle<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const options = {
|
||||
padding: node.padding ?? 0,
|
||||
} as MindmapOptions;
|
||||
return circle(parent, node, options);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { log } from '../../../logger.js';
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import type { Node } from '../../types.js';
|
||||
@@ -6,6 +5,7 @@ import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import { insertPolygonShape } from './insertPolygonShape.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import type { Bounds, Point } from '../../../types.js';
|
||||
|
||||
export const createDecisionBoxPathD = (x: number, y: number, size: number): string => {
|
||||
return [
|
||||
@@ -61,17 +61,26 @@ export async function question<T extends SVGGraphicsElement>(parent: D3Selection
|
||||
}
|
||||
|
||||
updateNodeBounds(node, polygon);
|
||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||
const s = bounds.width;
|
||||
|
||||
// Define polygon points
|
||||
const points = [
|
||||
{ x: s / 2, y: 0 },
|
||||
{ x: s, y: -s / 2 },
|
||||
{ x: s / 2, y: -s },
|
||||
{ x: 0, y: -s / 2 },
|
||||
];
|
||||
|
||||
// Calculate the intersection point
|
||||
const res = intersect.polygon(bounds, points, point);
|
||||
|
||||
return { x: res.x - 0.5, y: res.y - 0.5 }; // Adjusted result
|
||||
};
|
||||
|
||||
node.intersect = function (point) {
|
||||
log.debug(
|
||||
'APA12 Intersect called SPLIT\npoint:',
|
||||
point,
|
||||
'\nnode:\n',
|
||||
node,
|
||||
'\nres:',
|
||||
intersect.polygon(node, points, point)
|
||||
);
|
||||
return intersect.polygon(node, points, point);
|
||||
// @ts-ignore TODO fix this (KNSV)
|
||||
return this.calcIntersect(node as Bounds, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
|
||||
@@ -98,18 +98,19 @@ export async function roundedRect<T extends SVGGraphicsElement>(
|
||||
|
||||
const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
|
||||
const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
|
||||
const radius = 5;
|
||||
const taper = 5; // Taper width for the rounded corners
|
||||
const radius = node.radius || 5;
|
||||
const taper = node.taper || 5; // Taper width for the rounded corners
|
||||
const { cssStyles } = node;
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
if (node.stroke) {
|
||||
options.stroke = node.stroke;
|
||||
}
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
const points = [
|
||||
// Top edge (left to right)
|
||||
{ x: -w / 2 + taper, y: -h / 2 }, // Top-left corner start (1)
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function squareRect<T extends SVGGraphicsElement>(parent: D3Selecti
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
classes: '',
|
||||
labelPaddingX: (node?.padding || 0) * 2,
|
||||
labelPaddingX: node.labelPaddingX ?? (node?.padding || 0) * 2,
|
||||
labelPaddingY: (node?.padding || 0) * 1,
|
||||
} as RectOptions;
|
||||
return drawRect(parent, node, options);
|
||||
|
||||
@@ -2,6 +2,7 @@ export type MarkdownWordType = 'normal' | 'strong' | 'em';
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import type { ClusterShapeID } from './rendering-elements/clusters.js';
|
||||
import type { ShapeID } from './rendering-elements/shapes.js';
|
||||
import type { Bounds, Point } from '../types.js';
|
||||
export interface MarkdownWord {
|
||||
content: string;
|
||||
type: MarkdownWordType;
|
||||
@@ -38,11 +39,12 @@ interface BaseNode {
|
||||
linkTarget?: string;
|
||||
tooltip?: string;
|
||||
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
|
||||
isGroup: boolean;
|
||||
isGroup?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
// Specific properties for State Diagram nodes TODO remove and use generic properties
|
||||
intersect?: (point: any) => any;
|
||||
calcIntersect?: (bounds: Bounds, point: Point) => any;
|
||||
|
||||
// Non-generic properties
|
||||
rx?: number; // Used for rounded corners in Rect, Ellipse, etc.Maybe it to specialized RectNode, EllipseNode, etc.
|
||||
@@ -58,6 +60,8 @@ interface BaseNode {
|
||||
borderStyle?: string;
|
||||
borderWidth?: number;
|
||||
labelTextColor?: string;
|
||||
labelPaddingX?: number;
|
||||
labelPaddingY?: number;
|
||||
|
||||
// Flowchart specific properties
|
||||
x?: number;
|
||||
@@ -72,16 +76,25 @@ interface BaseNode {
|
||||
defaultWidth?: number;
|
||||
imageAspectRatio?: number;
|
||||
constraint?: 'on' | 'off';
|
||||
children?: NodeChildren;
|
||||
nodeId?: string;
|
||||
level?: number;
|
||||
descr?: string;
|
||||
type?: number;
|
||||
radius?: number;
|
||||
taper?: number;
|
||||
stroke?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group/cluster nodes, e.g. nodes that contain other nodes.
|
||||
*/
|
||||
export type NodeChildren = Node[];
|
||||
|
||||
export interface ClusterNode extends BaseNode {
|
||||
shape?: ClusterShapeID;
|
||||
isGroup: true;
|
||||
}
|
||||
|
||||
export interface NonClusterNode extends BaseNode {
|
||||
shape?: ShapeID;
|
||||
isGroup: false;
|
||||
@@ -113,7 +126,7 @@ export interface Edge {
|
||||
start?: string;
|
||||
stroke?: string;
|
||||
text?: string;
|
||||
type: string;
|
||||
type?: string;
|
||||
// Class Diagram specific properties
|
||||
startLabelRight?: string;
|
||||
endLabelLeft?: string;
|
||||
@@ -126,6 +139,12 @@ export interface Edge {
|
||||
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
|
||||
look?: string;
|
||||
isUserDefinedId?: boolean;
|
||||
points?: Point[];
|
||||
parentId?: string;
|
||||
dir?: string;
|
||||
source?: string;
|
||||
target?: string;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export interface RectOptions {
|
||||
@@ -136,6 +155,10 @@ export interface RectOptions {
|
||||
classes: string;
|
||||
}
|
||||
|
||||
export interface MindmapOptions {
|
||||
padding: number;
|
||||
}
|
||||
|
||||
// Extending the Node interface for specific types if needed
|
||||
export type ClassDiagramNode = Node & {
|
||||
memberData: any; // Specific property for class diagram nodes
|
||||
@@ -171,6 +194,7 @@ export interface ShapeRenderOptions {
|
||||
config: MermaidConfig;
|
||||
/** Some shapes render differently if a diagram has a direction `LR` */
|
||||
dir?: Node['dir'];
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
export type KanbanNode = Node & {
|
||||
|
||||
@@ -977,6 +977,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
- useMaxWidth
|
||||
- padding
|
||||
- maxNodeWidth
|
||||
- layoutAlgorithm
|
||||
properties:
|
||||
padding:
|
||||
type: number
|
||||
@@ -984,6 +985,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
maxNodeWidth:
|
||||
type: number
|
||||
default: 200
|
||||
layoutAlgorithm:
|
||||
description: Layout algorithm to use for positioning mindmap nodes
|
||||
type: string
|
||||
default: 'cose-bilkent'
|
||||
|
||||
KanbanDiagramConfig:
|
||||
title: Kanban Diagram Config
|
||||
|
||||
@@ -48,6 +48,10 @@ export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export interface Bounds extends Point {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TextDimensionConfig {
|
||||
fontSize?: number;
|
||||
|
||||
4
packages/mermaid/src/types/cytoscape-cose-bilkent.d.ts
vendored
Normal file
4
packages/mermaid/src/types/cytoscape-cose-bilkent.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module 'cytoscape-cose-bilkent' {
|
||||
const coseBilkent: any;
|
||||
export default coseBilkent;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user