mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-03 02:54:06 +01:00
Compare commits
10 Commits
sidv/fixRe
...
architectu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57b98c004 | ||
|
|
4bb5753635 | ||
|
|
22e1b17ac3 | ||
|
|
18f461267d | ||
|
|
4430eddb24 | ||
|
|
0a23304d1a | ||
|
|
7effdc147b | ||
|
|
6e67515f41 | ||
|
|
d5c4eff251 | ||
|
|
5324fd8dfd |
@@ -38,6 +38,11 @@ export const packageOptions = {
|
||||
packageName: 'mermaid-layout-tidy-tree',
|
||||
file: 'index.ts',
|
||||
},
|
||||
'mermaid-layout-fcose': {
|
||||
name: 'mermaid-layout-fcose',
|
||||
packageName: 'mermaid-layout-fcose',
|
||||
file: 'index.ts',
|
||||
},
|
||||
examples: {
|
||||
name: 'mermaid-examples',
|
||||
packageName: 'examples',
|
||||
|
||||
5
.changeset/deep-pumas-run.md
Normal file
5
.changeset/deep-pumas-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: Fix mindmap rendering in docs and apply tidytree layout
|
||||
5
.changeset/four-eyes-wish.md
Normal file
5
.changeset/four-eyes-wish.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Ensure edge label color is applied when using classDef with edge IDs
|
||||
5
.changeset/moody-fans-try.md
Normal file
5
.changeset/moody-fans-try.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Resolve gantt chart crash due to invalid array length
|
||||
5
.changeset/proud-colts-smell.md
Normal file
5
.changeset/proud-colts-smell.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add IDs in architecture diagrams
|
||||
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
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -26,8 +26,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
language: ['javascript', 'actions']
|
||||
# CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
|
||||
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- sidv/fixRelease
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -33,18 +32,15 @@ jobs:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
- name: Install Packages
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
npm install -g npm@11
|
||||
npm --version
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# - name: Create Release Pull Request or Publish to npm
|
||||
# id: changesets
|
||||
# uses: changesets/action@06245a4e0a36c064a573d4150030f5ec548e4fcc # v1.4.10
|
||||
# with:
|
||||
# version: pnpm changeset:version
|
||||
# publish: pnpm changeset:publish
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# NPM_CONFIG_PROVENANCE: true
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@06245a4e0a36c064a573d4150030f5ec548e4fcc # v1.4.10
|
||||
with:
|
||||
version: pnpm changeset:version
|
||||
publish: pnpm changeset:publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
@@ -833,34 +833,4 @@ describe('Gantt diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should handle seconds-only format with tickInterval (issue #5496)', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
tickInterval 1second
|
||||
dateFormat ss
|
||||
axisFormat %s
|
||||
|
||||
section Network Request
|
||||
RTT : rtt, 0, 20
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should handle dates with year typo like 202 instead of 2024 (issue #5496)', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
title Schedule
|
||||
dateFormat YYYY-MM-DD
|
||||
tickInterval 1week
|
||||
axisFormat %m-%d
|
||||
|
||||
section Vacation
|
||||
London : 2024-12-01, 7d
|
||||
London : 202-12-01, 7d
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
|
||||
|
||||
## Plans
|
||||
|
||||
- **Free** - A free plan that includes six diagrams.
|
||||
- **Free** - A free plan that includes three diagrams.
|
||||
|
||||
- **Pro** - A paid plan that includes unlimited diagrams, access to the collaboration feature, and more.
|
||||
|
||||
|
||||
36
package.json
36
package.json
@@ -63,12 +63,12 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-cypress": "^3.44.9",
|
||||
"@applitools/eyes-cypress": "^3.55.2",
|
||||
"@argos-ci/cypress": "^6.1.1",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"@cspell/eslint-plugin": "^8.19.4",
|
||||
"@cypress/code-coverage": "^3.12.49",
|
||||
"@cypress/code-coverage": "^3.14.6",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@rollup/plugin-typescript": "^12.1.4",
|
||||
"@types/cors": "^2.8.19",
|
||||
@@ -77,22 +77,22 @@
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^22.13.17",
|
||||
"@types/node": "^22.18.6",
|
||||
"@types/rollup-plugin-visualizer": "^5.0.3",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/spy": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/spy": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"ajv": "^8.17.1",
|
||||
"chokidar": "3.6.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cspell": "^9.1.5",
|
||||
"cspell": "^9.2.1",
|
||||
"cypress": "^14.5.4",
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"cypress-split": "^1.24.21",
|
||||
"esbuild": "^0.25.9",
|
||||
"cypress-split": "^1.24.23",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-cypress": "^4.3.0",
|
||||
@@ -106,10 +106,10 @@
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"express": "^5.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"globby": "^14.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^30.0.5",
|
||||
"jest": "^30.1.3",
|
||||
"jison": "^0.4.18",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^26.1.0",
|
||||
@@ -118,18 +118,18 @@
|
||||
"markdown-table": "^3.0.4",
|
||||
"nyc": "^17.1.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-jsdoc": "^1.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"start-server-and-test": "^2.0.13",
|
||||
"start-server-and-test": "^2.1.2",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.7.3",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "~5.7.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.6",
|
||||
"vite": "^7.0.7",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vitest": "^3.0.9"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"nyc": {
|
||||
"report-dir": "coverage/cypress"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"khroma": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"mermaid": "workspace:*",
|
||||
"rimraf": "^6.0.1"
|
||||
},
|
||||
|
||||
48
packages/mermaid-layout-fcose/package.json
Normal file
48
packages/mermaid-layout-fcose/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@mermaid-js/layout-fcose",
|
||||
"version": "0.1.0",
|
||||
"description": "FCoSE layout engine for architecture diagrams",
|
||||
"module": "dist/mermaid-layout-fcose.core.mjs",
|
||||
"types": "dist/layouts.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-layout-fcose.core.mjs",
|
||||
"types": "./dist/layouts.d.ts"
|
||||
},
|
||||
"./": "./"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"fcose",
|
||||
"mermaid",
|
||||
"layout",
|
||||
"architecture"
|
||||
],
|
||||
"scripts": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"contributors": [
|
||||
"Knut Sveidqvist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cytoscape": "^3.27.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"d3": "^7.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mermaid": "^11.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
22
packages/mermaid-layout-fcose/src/index.ts
Normal file
22
packages/mermaid-layout-fcose/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* FCoSE Layout Algorithm for Architecture Diagrams
|
||||
*
|
||||
* This module provides a layout algorithm implementation using the
|
||||
* cytoscape-fcose algorithm for positioning nodes and edges in architecture
|
||||
* diagrams with spatial constraints and group alignments.
|
||||
*
|
||||
* The algorithm is optimized for architecture diagrams and supports:
|
||||
* - Spatial maps for relative positioning
|
||||
* - Group alignments for organizing related services
|
||||
* - XY edges with 90-degree bends
|
||||
* - Complex edge routing
|
||||
*
|
||||
* The algorithm follows the unified rendering pattern and can be used
|
||||
* by architecture diagrams that provide compatible LayoutData with
|
||||
* architecture-specific data structures.
|
||||
*/
|
||||
|
||||
export { default } from './layouts.js';
|
||||
export * from './types.js';
|
||||
export * from './layout.js';
|
||||
export { render } from './render.js';
|
||||
637
packages/mermaid-layout-fcose/src/layout.ts
Normal file
637
packages/mermaid-layout-fcose/src/layout.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
import type { LayoutData } from 'mermaid';
|
||||
import type { LayoutOptions, Position } from 'cytoscape';
|
||||
import cytoscape from 'cytoscape';
|
||||
import fcose from 'cytoscape-fcose';
|
||||
import { select } from 'd3';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
ArchitectureDataStructures,
|
||||
ArchitectureGroupAlignments,
|
||||
ArchitectureSpatialMap,
|
||||
LayoutResult,
|
||||
PositionedNode,
|
||||
PositionedEdge,
|
||||
} from './types.js';
|
||||
import type {
|
||||
FcoseAlignmentConstraint,
|
||||
FcoseRelativePlacementConstraint,
|
||||
} from './cytoscape-fcose.d.js';
|
||||
|
||||
cytoscape.use(fcose as any);
|
||||
|
||||
/**
|
||||
* Architecture direction types
|
||||
*/
|
||||
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
||||
|
||||
const ArchitectureDirectionName = {
|
||||
L: 'left',
|
||||
R: 'right',
|
||||
T: 'top',
|
||||
B: 'bottom',
|
||||
} as const;
|
||||
|
||||
function isArchitectureDirectionY(x: ArchitectureDirection): boolean {
|
||||
return x === 'T' || x === 'B';
|
||||
}
|
||||
|
||||
function isArchitectureDirectionXY(a: ArchitectureDirection, b: ArchitectureDirection): boolean {
|
||||
const aX = a === 'L' || a === 'R';
|
||||
const bY = b === 'T' || b === 'B';
|
||||
const aY = a === 'T' || a === 'B';
|
||||
const bX = b === 'L' || b === 'R';
|
||||
return (aX && bY) || (aY && bX);
|
||||
}
|
||||
|
||||
function getOppositeArchitectureDirection(x: ArchitectureDirection): ArchitectureDirection {
|
||||
if (x === 'L' || x === 'R') {
|
||||
return x === 'L' ? 'R' : 'L';
|
||||
} else {
|
||||
return x === 'T' ? 'B' : 'T';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the fcose layout algorithm on architecture diagram data
|
||||
*
|
||||
* This function takes layout data and uses cytoscape-fcose to calculate
|
||||
* optimal node positions for architecture diagrams with spatial constraints.
|
||||
*
|
||||
* @param data - The layout data containing nodes, edges, and configuration
|
||||
* @returns Promise resolving to layout result with positioned nodes and edges
|
||||
*/
|
||||
export function executeFcoseLayout(data: LayoutData): Promise<LayoutResult> {
|
||||
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 = [];
|
||||
}
|
||||
|
||||
// Extract architecture-specific data structures if available
|
||||
const dataStructures = data.dataStructures as ArchitectureDataStructures | undefined;
|
||||
|
||||
const spatialMaps = dataStructures?.spatialMaps ?? [];
|
||||
const groupAlignments = dataStructures?.groupAlignments ?? {};
|
||||
|
||||
// Get icon size from config (default to 50)
|
||||
// Try to get from architecture config, or use a reasonable default
|
||||
const iconSize = data.config?.architecture?.iconSize || data.config?.iconSize || 50;
|
||||
|
||||
// Create a hidden container for cytoscape
|
||||
const renderEl = select('body')
|
||||
.append('div')
|
||||
.attr('id', 'cy-fcose')
|
||||
.attr('style', 'display:none');
|
||||
const cy = cytoscape({
|
||||
container: document.getElementById('cy-fcose'),
|
||||
style: [
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'straight',
|
||||
label: 'data(label)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge.segments',
|
||||
style: {
|
||||
'curve-style': 'segments',
|
||||
'segment-weights': '0',
|
||||
'segment-distances': [0.5],
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
// @ts-ignore Incorrect library types
|
||||
'compound-sizing-wrt-labels': 'include',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node[label]',
|
||||
style: {
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.node-service',
|
||||
style: {
|
||||
label: 'data(label)',
|
||||
width: 'data(width)',
|
||||
height: 'data(height)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.node-junction',
|
||||
style: {
|
||||
width: 'data(width)',
|
||||
height: 'data(height)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.node-group',
|
||||
style: {
|
||||
// @ts-ignore Incorrect library types
|
||||
padding: `${iconSize * 0.5}px`,
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'grid',
|
||||
boundingBox: {
|
||||
x1: 0,
|
||||
x2: 100,
|
||||
y1: 0,
|
||||
y2: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add nodes to cytoscape
|
||||
// First add groups, then services/junctions (to ensure parents exist before children)
|
||||
const nodeMap = new Map<string, any>();
|
||||
const groups = data.nodes.filter((n) => n.isGroup);
|
||||
const services = data.nodes.filter((n) => !n.isGroup);
|
||||
|
||||
// Add groups first
|
||||
groups.forEach((node) => {
|
||||
const cyNode = cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id,
|
||||
label: node.label || '',
|
||||
parent: node.parentId,
|
||||
type: 'group',
|
||||
},
|
||||
classes: 'node-group',
|
||||
});
|
||||
nodeMap.set(node.id, { node: cyNode, originalNode: node });
|
||||
});
|
||||
|
||||
// Then add services and junctions
|
||||
services.forEach((node) => {
|
||||
const nodeType = (node as any).type === 'junction' ? 'junction' : 'service';
|
||||
const cyNode = cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id,
|
||||
label: node.label || '',
|
||||
parent: node.parentId,
|
||||
width: node.width || iconSize,
|
||||
height: node.height || iconSize,
|
||||
type: nodeType,
|
||||
},
|
||||
classes: nodeType === 'junction' ? 'node-junction' : 'node-service',
|
||||
});
|
||||
nodeMap.set(node.id, { node: cyNode, originalNode: node });
|
||||
});
|
||||
|
||||
// Add edges to cytoscape
|
||||
const edgeMap = new Map<string, any>();
|
||||
data.edges.forEach((edge) => {
|
||||
const edgeData: any = {
|
||||
id: edge.id,
|
||||
source: edge.start || edge.source,
|
||||
target: edge.end || edge.target,
|
||||
label: edge.label || '',
|
||||
};
|
||||
|
||||
// Preserve architecture-specific edge data
|
||||
if ((edge as any).lhsDir) {
|
||||
edgeData.sourceDir = (edge as any).lhsDir;
|
||||
edgeData.targetDir = (edge as any).rhsDir;
|
||||
}
|
||||
|
||||
const edgeType =
|
||||
(edge as any).lhsDir && (edge as any).rhsDir
|
||||
? isArchitectureDirectionXY((edge as any).lhsDir, (edge as any).rhsDir)
|
||||
? 'segments'
|
||||
: 'straight'
|
||||
: 'straight';
|
||||
|
||||
const cyEdge = cy.add({
|
||||
group: 'edges',
|
||||
data: edgeData,
|
||||
classes: edgeType,
|
||||
});
|
||||
edgeMap.set(edge.id, { edge: cyEdge, originalEdge: edge });
|
||||
});
|
||||
|
||||
// Make cytoscape care about the dimensions of the nodes
|
||||
cy.nodes().forEach(function (n) {
|
||||
n.layoutDimensions = () => {
|
||||
const nodeData = n.data();
|
||||
return { w: nodeData.width || iconSize, h: nodeData.height || iconSize };
|
||||
};
|
||||
});
|
||||
|
||||
// Get alignment constraints
|
||||
const alignmentConstraint = getAlignments(data.nodes, spatialMaps, groupAlignments);
|
||||
|
||||
// Get relative placement constraints
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, data.nodes, iconSize);
|
||||
|
||||
// Run fcose layout
|
||||
const layout = cy.layout({
|
||||
name: 'fcose',
|
||||
quality: 'proof',
|
||||
styleEnabled: false,
|
||||
animate: false,
|
||||
nodeDimensionsIncludeLabels: false,
|
||||
idealEdgeLength(edge: any) {
|
||||
const [nodeA, nodeB] = edge.connectedNodes();
|
||||
const parentA = nodeA.data('parent');
|
||||
const parentB = nodeB.data('parent');
|
||||
const elasticity = parentA === parentB ? 1.5 * iconSize : 0.5 * iconSize;
|
||||
return elasticity;
|
||||
},
|
||||
edgeElasticity(edge: any) {
|
||||
const [nodeA, nodeB] = edge.connectedNodes();
|
||||
const parentA = nodeA.data('parent');
|
||||
const parentB = nodeB.data('parent');
|
||||
const elasticity = parentA === parentB ? 0.45 : 0.001;
|
||||
return elasticity;
|
||||
},
|
||||
alignmentConstraint,
|
||||
relativePlacementConstraint,
|
||||
} as LayoutOptions);
|
||||
|
||||
// Handle XY edges (edges with bends)
|
||||
layout.one('layoutstop', () => {
|
||||
function getSegmentWeights(
|
||||
source: Position,
|
||||
target: Position,
|
||||
pointX: number,
|
||||
pointY: number
|
||||
) {
|
||||
const { x: sX, y: sY } = source;
|
||||
const { x: tX, y: tY } = target;
|
||||
|
||||
let D =
|
||||
(pointY - sY + ((sX - pointX) * (sY - tY)) / (sX - tX)) /
|
||||
Math.sqrt(1 + Math.pow((sY - tY) / (sX - tX), 2));
|
||||
let W = Math.sqrt(Math.pow(pointY - sY, 2) + Math.pow(pointX - sX, 2) - Math.pow(D, 2));
|
||||
|
||||
const distAB = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2));
|
||||
W = W / distAB;
|
||||
|
||||
let delta1 = (tX - sX) * (pointY - sY) - (tY - sY) * (pointX - sX);
|
||||
delta1 = delta1 >= 0 ? 1 : -1;
|
||||
|
||||
let delta2 = (tX - sX) * (pointX - sX) + (tY - sY) * (pointY - sY);
|
||||
delta2 = delta2 >= 0 ? 1 : -1;
|
||||
|
||||
D = Math.abs(D) * delta1;
|
||||
W = W * delta2;
|
||||
|
||||
return { distances: D, weights: W };
|
||||
}
|
||||
|
||||
cy.startBatch();
|
||||
cy.edges().forEach((cyEdge) => {
|
||||
// Check if edge has data method and data exists
|
||||
if (
|
||||
cyEdge &&
|
||||
typeof cyEdge.data === 'function' &&
|
||||
typeof cyEdge.source === 'function' &&
|
||||
typeof cyEdge.target === 'function'
|
||||
) {
|
||||
try {
|
||||
const edgeData = cyEdge.data();
|
||||
if (edgeData?.sourceDir && edgeData?.targetDir) {
|
||||
const sourceNode = cyEdge.source();
|
||||
const targetNode = cyEdge.target();
|
||||
if (
|
||||
sourceNode &&
|
||||
targetNode &&
|
||||
typeof sourceNode.position === 'function' &&
|
||||
typeof targetNode.position === 'function'
|
||||
) {
|
||||
const { x: sX, y: sY } = sourceNode.position();
|
||||
const { x: tX, y: tY } = targetNode.position();
|
||||
if (
|
||||
sX !== tX &&
|
||||
sY !== tY &&
|
||||
!isNaN(sX) &&
|
||||
!isNaN(sY) &&
|
||||
!isNaN(tX) &&
|
||||
!isNaN(tY)
|
||||
) {
|
||||
const sourceDir = edgeData.sourceDir as ArchitectureDirection;
|
||||
if (
|
||||
typeof cyEdge.sourceEndpoint === 'function' &&
|
||||
typeof cyEdge.targetEndpoint === 'function'
|
||||
) {
|
||||
const sEP = cyEdge.sourceEndpoint();
|
||||
const tEP = cyEdge.targetEndpoint();
|
||||
if (
|
||||
sEP &&
|
||||
tEP &&
|
||||
typeof sEP.x === 'number' &&
|
||||
typeof sEP.y === 'number' &&
|
||||
typeof tEP.x === 'number' &&
|
||||
typeof tEP.y === 'number'
|
||||
) {
|
||||
const [pointX, pointY] = isArchitectureDirectionY(sourceDir)
|
||||
? [sEP.x, tEP.y]
|
||||
: [tEP.x, sEP.y];
|
||||
const { weights, distances } = getSegmentWeights(sEP, tEP, pointX, pointY);
|
||||
if (typeof cyEdge.style === 'function') {
|
||||
cyEdge.style('segment-distances', distances);
|
||||
cyEdge.style('segment-weights', weights);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// skip edges that can't be processed
|
||||
void error;
|
||||
}
|
||||
}
|
||||
});
|
||||
cy.endBatch();
|
||||
layout.run();
|
||||
});
|
||||
|
||||
layout.run();
|
||||
|
||||
cy.ready(() => {
|
||||
// Extract positioned nodes
|
||||
const positionedNodes: PositionedNode[] = [];
|
||||
cy.nodes().forEach((cyNode) => {
|
||||
const nodeData = nodeMap.get(cyNode.id());
|
||||
if (!nodeData) {
|
||||
return;
|
||||
}
|
||||
const nodeType = typeof cyNode.data === 'function' ? cyNode.data('type') : undefined;
|
||||
const isGroup = nodeType === 'group';
|
||||
|
||||
let pos: { x: number; y: number } | null = null;
|
||||
|
||||
if (isGroup && typeof cyNode.boundingBox === 'function') {
|
||||
const bbox = cyNode.boundingBox();
|
||||
if (
|
||||
bbox &&
|
||||
typeof bbox.x1 === 'number' &&
|
||||
typeof bbox.y1 === 'number' &&
|
||||
!isNaN(bbox.x1) &&
|
||||
!isNaN(bbox.y1)
|
||||
) {
|
||||
pos = { x: bbox.x1, y: bbox.y1 };
|
||||
if (
|
||||
typeof bbox.w === 'number' &&
|
||||
typeof bbox.h === 'number' &&
|
||||
!isNaN(bbox.w) &&
|
||||
!isNaN(bbox.h)
|
||||
) {
|
||||
positionedNodes.push({
|
||||
id: cyNode.id(),
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: bbox.w,
|
||||
height: bbox.h,
|
||||
originalNode: nodeData.originalNode,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pos && typeof cyNode.position === 'function') {
|
||||
const nodePos = cyNode.position();
|
||||
if (
|
||||
nodePos &&
|
||||
typeof nodePos.x === 'number' &&
|
||||
typeof nodePos.y === 'number' &&
|
||||
!isNaN(nodePos.x) &&
|
||||
!isNaN(nodePos.y)
|
||||
) {
|
||||
pos = { x: nodePos.x, y: nodePos.y };
|
||||
}
|
||||
}
|
||||
|
||||
if (pos) {
|
||||
positionedNodes.push({
|
||||
id: cyNode.id(),
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
width: typeof cyNode.data === 'function' ? cyNode.data('width') : undefined,
|
||||
height: typeof cyNode.data === 'function' ? cyNode.data('height') : undefined,
|
||||
originalNode: nodeData.originalNode,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Extract positioned edges
|
||||
const positionedEdges: PositionedEdge[] = [];
|
||||
cy.edges().forEach((cyEdge) => {
|
||||
if (
|
||||
cyEdge &&
|
||||
typeof cyEdge.id === 'function' &&
|
||||
typeof cyEdge.source === 'function' &&
|
||||
typeof cyEdge.target === 'function'
|
||||
) {
|
||||
const edgeData = edgeMap.get(cyEdge.id());
|
||||
if (edgeData) {
|
||||
const sourceNode = cyEdge.source();
|
||||
const targetNode = cyEdge.target();
|
||||
if (
|
||||
sourceNode &&
|
||||
targetNode &&
|
||||
typeof sourceNode.position === 'function' &&
|
||||
typeof targetNode.position === 'function'
|
||||
) {
|
||||
const sourcePos = sourceNode.position();
|
||||
const targetPos = targetNode.position();
|
||||
if (
|
||||
sourcePos &&
|
||||
targetPos &&
|
||||
typeof sourcePos.x === 'number' &&
|
||||
typeof sourcePos.y === 'number' &&
|
||||
typeof targetPos.x === 'number' &&
|
||||
typeof targetPos.y === 'number'
|
||||
) {
|
||||
positionedEdges.push({
|
||||
id: cyEdge.id(),
|
||||
source: sourceNode.id(),
|
||||
target: targetNode.id(),
|
||||
points: [
|
||||
{ x: sourcePos.x, y: sourcePos.y },
|
||||
{ x: targetPos.x, y: targetPos.y },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up
|
||||
renderEl.remove();
|
||||
|
||||
resolve({
|
||||
nodes: positionedNodes,
|
||||
edges: positionedEdges,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alignment constraints for fcose
|
||||
*/
|
||||
function getAlignments(
|
||||
nodes: LayoutData['nodes'],
|
||||
spatialMaps: ArchitectureSpatialMap[],
|
||||
groupAlignments: ArchitectureGroupAlignments
|
||||
): FcoseAlignmentConstraint {
|
||||
const flattenAlignments = (
|
||||
alignmentObj: Record<number, Record<string, string[]>>,
|
||||
alignmentDir: ArchitectureAlignment
|
||||
): Record<string, string[]> => {
|
||||
return Object.entries(alignmentObj).reduce(
|
||||
(prev, [dir, alignments]) => {
|
||||
let cnt = 0;
|
||||
const arr = Object.entries(alignments);
|
||||
if (arr.length === 1) {
|
||||
prev[dir] = arr[0][1];
|
||||
return prev;
|
||||
}
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
const [aGroupId, aNodeIds] = arr[i];
|
||||
const [bGroupId, bNodeIds] = arr[j];
|
||||
const alignment = groupAlignments[aGroupId]?.[bGroupId];
|
||||
|
||||
if (alignment === alignmentDir) {
|
||||
prev[dir] ??= [];
|
||||
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
|
||||
} else if (aGroupId === 'default' || bGroupId === 'default') {
|
||||
prev[dir] ??= [];
|
||||
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
|
||||
} else {
|
||||
const keyA = `${dir}-${cnt++}`;
|
||||
prev[keyA] = aNodeIds;
|
||||
const keyB = `${dir}-${cnt++}`;
|
||||
prev[keyB] = bNodeIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
{} as Record<string, string[]>
|
||||
);
|
||||
};
|
||||
|
||||
// Create a node lookup by group
|
||||
const nodeGroupMap = new Map<string, string>();
|
||||
nodes.forEach((node) => {
|
||||
nodeGroupMap.set(node.id, node.parentId || 'default');
|
||||
});
|
||||
|
||||
const alignments = spatialMaps.map((spatialMap) => {
|
||||
const horizontalAlignments: Record<number, Record<string, string[]>> = {};
|
||||
const verticalAlignments: Record<number, Record<string, string[]>> = {};
|
||||
|
||||
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
|
||||
const nodeGroup = nodeGroupMap.get(id) ?? 'default';
|
||||
|
||||
horizontalAlignments[y] ??= {};
|
||||
horizontalAlignments[y][nodeGroup] ??= [];
|
||||
horizontalAlignments[y][nodeGroup].push(id);
|
||||
|
||||
verticalAlignments[x] ??= {};
|
||||
verticalAlignments[x][nodeGroup] ??= [];
|
||||
verticalAlignments[x][nodeGroup].push(id);
|
||||
});
|
||||
|
||||
return {
|
||||
horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter(
|
||||
(arr) => arr.length > 1
|
||||
),
|
||||
vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter(
|
||||
(arr) => arr.length > 1
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const [horizontal, vertical] = alignments.reduce(
|
||||
([prevHoriz, prevVert], { horiz, vert }) => {
|
||||
return [
|
||||
[...prevHoriz, ...horiz],
|
||||
[...prevVert, ...vert],
|
||||
];
|
||||
},
|
||||
[[] as string[][], [] as string[][]]
|
||||
);
|
||||
|
||||
return {
|
||||
horizontal,
|
||||
vertical,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative placement constraints for fcose
|
||||
*/
|
||||
function getRelativeConstraints(
|
||||
spatialMaps: ArchitectureSpatialMap[],
|
||||
nodes: LayoutData['nodes'],
|
||||
iconSize: number
|
||||
): FcoseRelativePlacementConstraint[] {
|
||||
const relativeConstraints: FcoseRelativePlacementConstraint[] = [];
|
||||
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
|
||||
const strToPos = (pos: string) => pos.split(',').map((p) => parseInt(p));
|
||||
|
||||
spatialMaps.forEach((spatialMap) => {
|
||||
const invSpatialMap = Object.fromEntries(
|
||||
Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id])
|
||||
);
|
||||
|
||||
const queue = [posToStr([0, 0])];
|
||||
const visited: Record<string, number> = {};
|
||||
const directions: Record<ArchitectureDirection, number[]> = {
|
||||
L: [-1, 0],
|
||||
R: [1, 0],
|
||||
T: [0, 1],
|
||||
B: [0, -1],
|
||||
};
|
||||
|
||||
while (queue.length > 0) {
|
||||
const curr = queue.shift();
|
||||
if (curr) {
|
||||
visited[curr] = 1;
|
||||
const currId = invSpatialMap[curr];
|
||||
if (currId) {
|
||||
const currPos = strToPos(curr);
|
||||
Object.entries(directions).forEach(([dir, shift]) => {
|
||||
const newPos = posToStr([currPos[0] + shift[0], currPos[1] + shift[1]]);
|
||||
const newId = invSpatialMap[newPos];
|
||||
if (newId && !visited[newPos]) {
|
||||
queue.push(newPos);
|
||||
relativeConstraints.push({
|
||||
[ArchitectureDirectionName[dir as ArchitectureDirection]]: newId,
|
||||
[ArchitectureDirectionName[
|
||||
getOppositeArchitectureDirection(dir as ArchitectureDirection)
|
||||
]]: currId,
|
||||
gap: 1.5 * iconSize,
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return relativeConstraints;
|
||||
}
|
||||
13
packages/mermaid-layout-fcose/src/layouts.ts
Normal file
13
packages/mermaid-layout-fcose/src/layouts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { LayoutLoaderDefinition } from 'mermaid';
|
||||
|
||||
const loader = async () => await import(`./render.js`);
|
||||
|
||||
const fcoseLayout: LayoutLoaderDefinition[] = [
|
||||
{
|
||||
name: 'architecture-fcose',
|
||||
loader,
|
||||
algorithm: 'architecture-fcose',
|
||||
},
|
||||
];
|
||||
|
||||
export default fcoseLayout;
|
||||
201
packages/mermaid-layout-fcose/src/render.ts
Normal file
201
packages/mermaid-layout-fcose/src/render.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
|
||||
import { executeFcoseLayout } from './layout.js';
|
||||
|
||||
interface NodeWithPosition {
|
||||
id: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
domId?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render function for fcose layout algorithm
|
||||
|
||||
* The fcose layout is optimized for architecture diagrams with spatial constraints
|
||||
* and group alignments.
|
||||
*/
|
||||
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');
|
||||
|
||||
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;
|
||||
|
||||
const clusterResult = await insertCluster(subGraphsEl, node);
|
||||
if (clusterResult?.cluster) {
|
||||
clusterNode.domId = clusterResult.cluster;
|
||||
}
|
||||
} 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}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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 executeFcoseLayout(updatedLayoutData);
|
||||
|
||||
log.debug('Positioning nodes based on fcose layout results');
|
||||
|
||||
layoutResult.nodes.forEach((positionedNode) => {
|
||||
const node = nodeDb[positionedNode.id];
|
||||
if (
|
||||
node &&
|
||||
positionedNode.x !== undefined &&
|
||||
positionedNode.y !== undefined &&
|
||||
!isNaN(positionedNode.x) &&
|
||||
!isNaN(positionedNode.y)
|
||||
) {
|
||||
node.x = positionedNode.x;
|
||||
node.y = positionedNode.y;
|
||||
|
||||
if (node.domId) {
|
||||
if (
|
||||
node.isGroup &&
|
||||
positionedNode.width !== undefined &&
|
||||
positionedNode.height !== undefined
|
||||
) {
|
||||
const rect = node.domId.select('rect');
|
||||
if (!rect.empty()) {
|
||||
rect.attr('width', positionedNode.width);
|
||||
rect.attr('height', positionedNode.height);
|
||||
}
|
||||
}
|
||||
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
||||
log.debug(
|
||||
`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})${node.isGroup ? ` with size ${positionedNode.width}x${positionedNode.height}` : ''}`
|
||||
);
|
||||
} else if (node.isGroup) {
|
||||
const clusterElement = subGraphsEl.select(`#${node.id}`);
|
||||
if (clusterElement && !clusterElement.empty()) {
|
||||
if (positionedNode.width !== undefined && positionedNode.height !== undefined) {
|
||||
const rect = clusterElement.select('rect');
|
||||
if (!rect.empty()) {
|
||||
rect.attr('width', positionedNode.width);
|
||||
rect.attr('height', positionedNode.height);
|
||||
}
|
||||
}
|
||||
clusterElement.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
||||
log.debug(
|
||||
`Positioned group ${node.id} at (${positionedNode.x}, ${positionedNode.y})${positionedNode.width !== undefined ? ` with size ${positionedNode.width}x${positionedNode.height}` : ''}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (node && (isNaN(positionedNode.x) || isNaN(positionedNode.y))) {
|
||||
log.warn(
|
||||
`Node ${node.id} has invalid position: x=${positionedNode.x}, y=${positionedNode.y}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
53
packages/mermaid-layout-fcose/src/types.ts
Normal file
53
packages/mermaid-layout-fcose/src/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
width?: number;
|
||||
height?: number;
|
||||
originalNode?: Node;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positioned edge after layout calculation
|
||||
*/
|
||||
export interface PositionedEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
points: { x: number; y: number }[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of layout algorithm execution
|
||||
*/
|
||||
export interface LayoutResult {
|
||||
nodes: PositionedNode[];
|
||||
edges: PositionedEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Architecture-specific data structures for fcose layout
|
||||
*/
|
||||
export type ArchitectureSpatialMap = Record<string, number[]>;
|
||||
|
||||
export type ArchitectureAlignment = 'vertical' | 'horizontal' | 'bend';
|
||||
|
||||
export type ArchitectureGroupAlignments = Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
>;
|
||||
|
||||
export interface ArchitectureDataStructures {
|
||||
spatialMaps: ArchitectureSpatialMap[];
|
||||
groupAlignments: ArchitectureGroupAlignments;
|
||||
}
|
||||
11
packages/mermaid-layout-fcose/tsconfig.json
Normal file
11
packages/mermaid-layout-fcose/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.35.2"
|
||||
"@zenuml/core": "^3.41.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
||||
@@ -1,36 +1,5 @@
|
||||
# mermaid
|
||||
|
||||
## 11.12.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7200](https://github.com/mermaid-js/mermaid/pull/7200) [`de7ed10`](https://github.com/mermaid-js/mermaid/commit/de7ed1033996d702e3983dcf8114f33faea89577) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - fix: validate dates and tick interval to prevent UI freeze/crash in gantt diagramtype
|
||||
|
||||
## 11.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7107](https://github.com/mermaid-js/mermaid/pull/7107) [`cbf8946`](https://github.com/mermaid-js/mermaid/commit/cbf89462acecac7a06f19843e8d48cb137df0753) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - fix: Updated the dependency dagre-d3-es to 7.0.13 to fix GHSA-cc8p-78qf-8p7q
|
||||
|
||||
## 11.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6921](https://github.com/mermaid-js/mermaid/pull/6921) [`764b315`](https://github.com/mermaid-js/mermaid/commit/764b315dc16d0359add7c6b8e3ef7592e9bdc09c) Thanks [@quilicicf](https://github.com/quilicicf)! - feat: Add IDs in architecture diagrams
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6950](https://github.com/mermaid-js/mermaid/pull/6950) [`a957908`](https://github.com/mermaid-js/mermaid/commit/a9579083bfba367a4f4678547ec37ed7b61b9f5b) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - chore: Fix mindmap rendering in docs and apply tidytree layout
|
||||
|
||||
- [#6826](https://github.com/mermaid-js/mermaid/pull/6826) [`1d36810`](https://github.com/mermaid-js/mermaid/commit/1d3681053b9168354e48e5763023b6305cd1ca72) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Ensure edge label color is applied when using classDef with edge IDs
|
||||
|
||||
- [#6945](https://github.com/mermaid-js/mermaid/pull/6945) [`d318f1a`](https://github.com/mermaid-js/mermaid/commit/d318f1a13cd7429334a29c70e449074ec1cb9f68) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Resolve gantt chart crash due to invalid array length
|
||||
|
||||
- [#6918](https://github.com/mermaid-js/mermaid/pull/6918) [`cfe9238`](https://github.com/mermaid-js/mermaid/commit/cfe9238882cbe95416db1feea3112456a71b6aaf) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - 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
|
||||
|
||||
## 11.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "11.12.2",
|
||||
"version": "11.11.0",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
@@ -68,21 +68,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@iconify/utils": "^3.0.1",
|
||||
"@iconify/utils": "^3.0.2",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape": "^3.33.1",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.13",
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.18",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^16.2.1",
|
||||
"marked": "^16.3.0",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.6",
|
||||
"ts-dedent": "^2.2.0",
|
||||
@@ -105,9 +105,9 @@
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
"canvas": "^3.1.2",
|
||||
"canvas": "^3.2.0",
|
||||
"chokidar": "3.6.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"csstree-validator": "^4.0.1",
|
||||
"globby": "^14.1.0",
|
||||
"jison": "^0.4.18",
|
||||
@@ -116,14 +116,14 @@
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"micromatch": "^4.0.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier": "^3.6.2",
|
||||
"remark": "^15.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"start-server-and-test": "^2.0.13",
|
||||
"type-fest": "^4.35.0",
|
||||
"typedoc": "^0.28.12",
|
||||
"start-server-and-test": "^2.1.2",
|
||||
"type-fest": "^4.41.0",
|
||||
"typedoc": "^0.28.13",
|
||||
"typedoc-plugin-markdown": "^4.8.1",
|
||||
"typescript": "~5.7.3",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import { cleanAndMerge, getEdgeId } from '../../utils.js';
|
||||
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
|
||||
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
@@ -351,15 +353,147 @@ export class ArchitectureDB implements DiagramDB {
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setAccDescription = setAccDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
|
||||
* @param field - the config field to access
|
||||
* @returns
|
||||
*/
|
||||
// export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
// field: T
|
||||
// ): Required<ArchitectureDiagramConfig>[T] {
|
||||
// return db.getConfig()[field];
|
||||
// }
|
||||
/**
|
||||
* Converts architecture diagram data to LayoutData format for unified rendering
|
||||
*/
|
||||
public getData(): LayoutData {
|
||||
const config = commonGetConfig();
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const groups = this.getGroups();
|
||||
for (const group of groups) {
|
||||
const padding = this.getConfigField('padding');
|
||||
const fontSize = this.getConfigField('fontSize');
|
||||
|
||||
const groupWidth = 200;
|
||||
let groupHeight = 150;
|
||||
|
||||
if (group.title || group.icon) {
|
||||
groupHeight += fontSize + padding;
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: group.id,
|
||||
label: group.title,
|
||||
parentId: group.in,
|
||||
isGroup: true,
|
||||
shape: 'rect',
|
||||
icon: group.icon ? `mermaid-architecture:${group.icon}` : undefined,
|
||||
width: groupWidth,
|
||||
height: groupHeight,
|
||||
padding: padding,
|
||||
cssClasses: 'architecture-group',
|
||||
cssCompiledStyles: [
|
||||
'stroke: #cccccc',
|
||||
'stroke-width: 2px',
|
||||
'stroke-dasharray: 8,8',
|
||||
'fill: transparent',
|
||||
],
|
||||
labelStyle: '',
|
||||
look: config.look || 'classic',
|
||||
rx: 5,
|
||||
ry: 5,
|
||||
});
|
||||
}
|
||||
|
||||
const services = this.getServices();
|
||||
for (const service of services) {
|
||||
const iconSize = this.getConfigField('iconSize');
|
||||
let nodeWidth = iconSize;
|
||||
let nodeHeight = iconSize;
|
||||
|
||||
if (service.title) {
|
||||
nodeHeight += iconSize * 0.3;
|
||||
nodeWidth = Math.max(nodeWidth, iconSize * 1.5);
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: service.id,
|
||||
label: service.title,
|
||||
parentId: service.in,
|
||||
isGroup: false,
|
||||
shape: service.icon || (service as any).iconText ? 'icon' : 'squareRect',
|
||||
icon: service.icon ? `mermaid-architecture:${service.icon}` : 'mermaid-architecture:blank',
|
||||
width: service.width || nodeWidth,
|
||||
height: service.height || nodeHeight,
|
||||
cssClasses: 'architecture-service',
|
||||
look: config.look,
|
||||
padding: this.getConfigField('padding') / 4,
|
||||
description: (service as any).iconText ? [(service as any).iconText] : undefined,
|
||||
assetWidth: iconSize,
|
||||
assetHeight: iconSize,
|
||||
});
|
||||
}
|
||||
|
||||
const junctions = this.getJunctions();
|
||||
for (const junction of junctions) {
|
||||
nodes.push({
|
||||
id: junction.id,
|
||||
parentId: junction.in,
|
||||
isGroup: false,
|
||||
shape: 'squareRect',
|
||||
width: 2,
|
||||
height: 2,
|
||||
cssClasses: 'architecture-junction',
|
||||
look: config.look,
|
||||
type: 'junction' as any,
|
||||
padding: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const architectureEdges = this.getEdges();
|
||||
let edgeCounter = 0;
|
||||
for (const edge of architectureEdges) {
|
||||
const edgeData = {
|
||||
id: getEdgeId(edge.lhsId, edge.rhsId, { counter: edgeCounter, prefix: 'L' }),
|
||||
start: edge.lhsId,
|
||||
end: edge.rhsId,
|
||||
source: edge.lhsId,
|
||||
target: edge.rhsId,
|
||||
label: edge.title || '',
|
||||
labelpos: 'c',
|
||||
type: 'normal',
|
||||
minlen: 2,
|
||||
weight: 1,
|
||||
classes: 'edge-thickness-normal edge-pattern-solid architecture-edge',
|
||||
look: config.look || 'classic',
|
||||
curve: 'linear',
|
||||
arrowTypeStart: edge.lhsInto ? 'point' : 'none',
|
||||
arrowTypeEnd: edge.rhsInto ? 'point' : 'none',
|
||||
arrowheadStyle: 'fill: #333',
|
||||
thickness: 'normal',
|
||||
pattern: 'solid',
|
||||
style: ['stroke: #333333', 'stroke-width: 3px', 'fill: none'],
|
||||
cssCompiledStyles: [],
|
||||
labelStyle: [],
|
||||
lhsDir: edge.lhsDir,
|
||||
rhsDir: edge.rhsDir,
|
||||
lhsInto: edge.lhsInto,
|
||||
rhsInto: edge.rhsInto,
|
||||
lhsGroup: edge.lhsGroup,
|
||||
rhsGroup: edge.rhsGroup,
|
||||
} as Edge & {
|
||||
lhsDir: any;
|
||||
rhsDir: any;
|
||||
lhsInto?: boolean;
|
||||
rhsInto?: boolean;
|
||||
lhsGroup?: boolean;
|
||||
rhsGroup?: boolean;
|
||||
};
|
||||
|
||||
edges.push(edgeData);
|
||||
edgeCounter++;
|
||||
}
|
||||
|
||||
const result = {
|
||||
nodes,
|
||||
edges,
|
||||
config,
|
||||
dataStructures: this.getDataStructures(),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
import { ArchitectureDB } from './architectureDb.js';
|
||||
import styles from './architectureStyles.js';
|
||||
import { renderer } from './architectureRenderer.js';
|
||||
import { renderer } from './architectureRenderer-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.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 utils from '../../utils.js';
|
||||
|
||||
import { registerIconPacks } from '../../rendering-util/icons.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
|
||||
export const getClasses = function (
|
||||
_text: string,
|
||||
_diagramObj: any
|
||||
): Map<string, DiagramStyleClassDef> {
|
||||
return new Map();
|
||||
};
|
||||
|
||||
export const draw = async function (_text: string, id: string, _version: string, diag: any) {
|
||||
registerIconPacks([
|
||||
{
|
||||
name: architectureIcons.prefix,
|
||||
icons: architectureIcons,
|
||||
},
|
||||
]);
|
||||
const { securityLevel, architecture: conf, layout } = getConfig();
|
||||
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
const layoutToUse = layout || 'architecture-fcose';
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layoutToUse, { fallback: 'dagre' });
|
||||
|
||||
data4Layout.nodeSpacing = 100;
|
||||
data4Layout.rankSpacing = 100;
|
||||
data4Layout.markers = ['point'];
|
||||
data4Layout.diagramId = id;
|
||||
|
||||
log.debug('Architecture layout data:', data4Layout);
|
||||
await render(data4Layout, svg);
|
||||
|
||||
const padding = conf?.padding ?? 8;
|
||||
utils.insertTitle(svg, 'architectureTitleText', 0, diag.db.getDiagramTitle());
|
||||
|
||||
setupViewPortForSVG(svg, padding, 'architecture', conf?.useMaxWidth ?? true);
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
||||
@@ -2,6 +2,7 @@ import type { DiagramDBBase } from '../../diagram-api/types.js';
|
||||
import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import type cytoscape from 'cytoscape';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
|
||||
/*=======================================*\
|
||||
| Architecture Diagram Types |
|
||||
@@ -256,7 +257,8 @@ export interface ArchitectureDB extends DiagramDBBase<ArchitectureDiagramConfig>
|
||||
getEdges: () => ArchitectureEdge[];
|
||||
setElementForId: (id: string, element: D3Element) => void;
|
||||
getElementById: (id: string) => D3Element;
|
||||
getDataStructures: () => ArchitectureDataStructures;
|
||||
getData: () => LayoutData;
|
||||
getDirection: () => string;
|
||||
}
|
||||
|
||||
export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
|
||||
|
||||
@@ -268,15 +268,7 @@ const fixTaskDates = function (startTime, endTime, dateFormat, excludes, include
|
||||
|
||||
const getStartDate = function (prevTime, dateFormat, str) {
|
||||
str = str.trim();
|
||||
|
||||
// Helper function to check if format is a timestamp format (x or X)
|
||||
const isTimestampFormat = (format) => {
|
||||
const trimmedFormat = format.trim();
|
||||
return trimmedFormat === 'x' || trimmedFormat === 'X';
|
||||
};
|
||||
|
||||
// Handle timestamp formats (x, X) with numeric strings
|
||||
if (isTimestampFormat(dateFormat) && /^\d+$/.test(str)) {
|
||||
if ((dateFormat.trim() === 'x' || dateFormat.trim() === 'X') && /^\d+$/.test(str)) {
|
||||
return new Date(Number(str));
|
||||
}
|
||||
// Test for after
|
||||
@@ -301,15 +293,13 @@ const getStartDate = function (prevTime, dateFormat, str) {
|
||||
return today;
|
||||
}
|
||||
|
||||
// Check for actual date set using dayjs strict parsing
|
||||
// Check for actual date set
|
||||
let mDate = dayjs(str, dateFormat.trim(), true);
|
||||
if (mDate.isValid()) {
|
||||
return mDate.toDate();
|
||||
} else {
|
||||
log.debug('Invalid date:' + str);
|
||||
log.debug('With date format:' + dateFormat.trim());
|
||||
|
||||
// Timestamp formats can fall back to new Date()
|
||||
const d = new Date(str);
|
||||
if (
|
||||
d === undefined ||
|
||||
|
||||
@@ -505,27 +505,4 @@ describe('when using the ganttDb', function () {
|
||||
ganttDb.addTask('test1', 'id1,202304,1d');
|
||||
expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304');
|
||||
});
|
||||
|
||||
it('should handle seconds-only format with valid numeric values (issue #5496)', function () {
|
||||
ganttDb.setDateFormat('ss');
|
||||
ganttDb.addSection('Network Request');
|
||||
ganttDb.addTask('RTT', 'rtt, 0, 20');
|
||||
const tasks = ganttDb.getTasks();
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].task).toBe('RTT');
|
||||
expect(tasks[0].id).toBe('rtt');
|
||||
});
|
||||
|
||||
it('should handle dates with year typo like 202 instead of 2024 (issue #5496)', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.addSection('Vacation');
|
||||
ganttDb.addTask('London Trip 1', '2024-12-01, 7d');
|
||||
ganttDb.addTask('London Trip 2', '202-12-01, 7d');
|
||||
const tasks = ganttDb.getTasks();
|
||||
expect(tasks).toHaveLength(2);
|
||||
// First task should be in year 2024
|
||||
expect(tasks[0].startTime.getFullYear()).toBe(2024);
|
||||
// Second task will be parsed as year 202 (fallback to new Date())
|
||||
expect(tasks[1].startTime.getFullYear()).toBe(202);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import dayjs from 'dayjs';
|
||||
import dayjsDuration from 'dayjs/plugin/duration.js';
|
||||
import { log } from '../../logger.js';
|
||||
import {
|
||||
select,
|
||||
@@ -29,8 +28,6 @@ import common from '../common/common.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
|
||||
dayjs.extend(dayjsDuration);
|
||||
|
||||
export const setConf = function () {
|
||||
log.debug('Something is calling, setConf, remove the call');
|
||||
};
|
||||
@@ -81,7 +78,6 @@ const getMaxIntersections = (tasks, orderOffset) => {
|
||||
};
|
||||
|
||||
let w;
|
||||
const MAX_TICK_COUNT = 10000;
|
||||
export const draw = function (text, id, version, diagObj) {
|
||||
const conf = getConfig().gantt;
|
||||
|
||||
@@ -606,27 +602,6 @@ export const draw = function (text, id, version, diagObj) {
|
||||
.attr('class', 'exclude-range');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the estimated number of ticks based on the time domain and tick interval.
|
||||
* Returns the estimated number of ticks as a number.
|
||||
* @param {Date} minTime - The minimum time in the domain
|
||||
* @param {Date} maxTime - The maximum time in the domain
|
||||
* @param {number} every - The interval count (e.g., 1 for "1second")
|
||||
* @param {string} interval - The interval unit (e.g., "second", "day")
|
||||
* @returns {number} The estimated number of ticks
|
||||
*/
|
||||
function getEstimatedTickCount(minTime, maxTime, every, interval) {
|
||||
if (every <= 0 || minTime > maxTime) {
|
||||
return Infinity;
|
||||
}
|
||||
const timeDiffMs = maxTime - minTime;
|
||||
const intervalMs = dayjs.duration({ [interval ?? 'day']: every }).asMilliseconds();
|
||||
if (intervalMs <= 0) {
|
||||
return Infinity;
|
||||
}
|
||||
return Math.ceil(timeDiffMs / intervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param theSidePad
|
||||
* @param theTopPad
|
||||
@@ -655,54 +630,32 @@ export const draw = function (text, id, version, diagObj) {
|
||||
);
|
||||
|
||||
if (resultTickInterval !== null) {
|
||||
const every = parseInt(resultTickInterval[1], 10);
|
||||
if (isNaN(every) || every <= 0) {
|
||||
log.warn(
|
||||
`Invalid tick interval value: "${resultTickInterval[1]}". Skipping custom tick interval.`
|
||||
);
|
||||
// Skip applying custom ticks
|
||||
} else {
|
||||
const interval = resultTickInterval[2];
|
||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||
const every = resultTickInterval[1];
|
||||
const interval = resultTickInterval[2];
|
||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||
|
||||
// Get the time domain to check tick count
|
||||
const domain = timeScale.domain();
|
||||
const minTime = domain[0];
|
||||
const maxTime = domain[1];
|
||||
const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval);
|
||||
|
||||
if (estimatedTicks > MAX_TICK_COUNT) {
|
||||
log.warn(
|
||||
`The tick interval "${every}${interval}" would generate ${estimatedTicks} ticks, ` +
|
||||
`which exceeds the maximum allowed (${MAX_TICK_COUNT}). ` +
|
||||
`This may indicate an invalid date or time range. Skipping custom tick interval.`
|
||||
);
|
||||
// D3 will use its default automatic tick generation
|
||||
} else {
|
||||
switch (interval) {
|
||||
case 'millisecond':
|
||||
bottomXAxis.ticks(timeMillisecond.every(every));
|
||||
break;
|
||||
case 'second':
|
||||
bottomXAxis.ticks(timeSecond.every(every));
|
||||
break;
|
||||
case 'minute':
|
||||
bottomXAxis.ticks(timeMinute.every(every));
|
||||
break;
|
||||
case 'hour':
|
||||
bottomXAxis.ticks(timeHour.every(every));
|
||||
break;
|
||||
case 'day':
|
||||
bottomXAxis.ticks(timeDay.every(every));
|
||||
break;
|
||||
case 'week':
|
||||
bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every));
|
||||
break;
|
||||
case 'month':
|
||||
bottomXAxis.ticks(timeMonth.every(every));
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (interval) {
|
||||
case 'millisecond':
|
||||
bottomXAxis.ticks(timeMillisecond.every(every));
|
||||
break;
|
||||
case 'second':
|
||||
bottomXAxis.ticks(timeSecond.every(every));
|
||||
break;
|
||||
case 'minute':
|
||||
bottomXAxis.ticks(timeMinute.every(every));
|
||||
break;
|
||||
case 'hour':
|
||||
bottomXAxis.ticks(timeHour.every(every));
|
||||
break;
|
||||
case 'day':
|
||||
bottomXAxis.ticks(timeDay.every(every));
|
||||
break;
|
||||
case 'week':
|
||||
bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every));
|
||||
break;
|
||||
case 'month':
|
||||
bottomXAxis.ticks(timeMonth.every(every));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,48 +677,32 @@ export const draw = function (text, id, version, diagObj) {
|
||||
.tickFormat(timeFormat(axisFormat));
|
||||
|
||||
if (resultTickInterval !== null) {
|
||||
const every = parseInt(resultTickInterval[1], 10);
|
||||
if (isNaN(every) || every <= 0) {
|
||||
log.warn(
|
||||
`Invalid tick interval value: "${resultTickInterval[1]}". Skipping custom tick interval.`
|
||||
);
|
||||
// Skip applying custom ticks
|
||||
} else {
|
||||
const interval = resultTickInterval[2];
|
||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||
const every = resultTickInterval[1];
|
||||
const interval = resultTickInterval[2];
|
||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||
|
||||
// Get the time domain to check tick count
|
||||
const domain = timeScale.domain();
|
||||
const minTime = domain[0];
|
||||
const maxTime = domain[1];
|
||||
const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval);
|
||||
|
||||
// Only apply custom ticks if the count is reasonable
|
||||
if (estimatedTicks <= MAX_TICK_COUNT) {
|
||||
switch (interval) {
|
||||
case 'millisecond':
|
||||
topXAxis.ticks(timeMillisecond.every(every));
|
||||
break;
|
||||
case 'second':
|
||||
topXAxis.ticks(timeSecond.every(every));
|
||||
break;
|
||||
case 'minute':
|
||||
topXAxis.ticks(timeMinute.every(every));
|
||||
break;
|
||||
case 'hour':
|
||||
topXAxis.ticks(timeHour.every(every));
|
||||
break;
|
||||
case 'day':
|
||||
topXAxis.ticks(timeDay.every(every));
|
||||
break;
|
||||
case 'week':
|
||||
topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every));
|
||||
break;
|
||||
case 'month':
|
||||
topXAxis.ticks(timeMonth.every(every));
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (interval) {
|
||||
case 'millisecond':
|
||||
topXAxis.ticks(timeMillisecond.every(every));
|
||||
break;
|
||||
case 'second':
|
||||
topXAxis.ticks(timeSecond.every(every));
|
||||
break;
|
||||
case 'minute':
|
||||
topXAxis.ticks(timeMinute.every(every));
|
||||
break;
|
||||
case 'hour':
|
||||
topXAxis.ticks(timeHour.every(every));
|
||||
break;
|
||||
case 'day':
|
||||
topXAxis.ticks(timeDay.every(every));
|
||||
break;
|
||||
case 'week':
|
||||
topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every));
|
||||
break;
|
||||
case 'month':
|
||||
topXAxis.ticks(timeMonth.every(every));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
|
||||
|
||||
## Plans
|
||||
|
||||
- **Free** - A free plan that includes six diagrams.
|
||||
- **Free** - A free plan that includes three diagrams.
|
||||
|
||||
- **Pro** - A paid plan that includes unlimited diagrams, access to the collaboration feature, and more.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jiti": "^2.4.2",
|
||||
"mermaid": "workspace:^",
|
||||
@@ -25,17 +25,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/carbon": "^1.2.13",
|
||||
"@unocss/reset": "^66.0.0",
|
||||
"@unocss/reset": "^66.5.1",
|
||||
"@vite-pwa/vitepress": "^1.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"https-localhost": "^4.7.1",
|
||||
"pathe": "^2.0.3",
|
||||
"unocss": "^66.4.2",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vitepress": "1.6.3",
|
||||
"unocss": "^66.5.1",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^7.0.7",
|
||||
"vite-plugin-pwa": "^1.0.3",
|
||||
"vitepress": "1.6.4",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import intersectRect from '../rendering-elements/intersect/intersect-rect.js';
|
||||
import createLabel from './createLabel.js';
|
||||
import { createRoundedRectPathD } from './shapes/roundedRectPath.ts';
|
||||
import { styles2String, userNodeOverrides } from './shapes/handDrawnShapeStyles.js';
|
||||
import { getIconSVG } from '../icons.js';
|
||||
|
||||
const rect = async (parent, node) => {
|
||||
log.info('Creating subgraph rect for ', node.id, node);
|
||||
@@ -30,6 +31,21 @@ const rect = async (parent, node) => {
|
||||
// Create the label and insert it after the rect
|
||||
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');
|
||||
|
||||
let iconWidth = 0;
|
||||
if (node.icon) {
|
||||
const iconSize = node.padding;
|
||||
const iconEl = labelEl.append('g').attr('class', 'cluster-label-icon');
|
||||
iconEl.html(
|
||||
`<g>${await getIconSVG(node.icon, {
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
fallbackPrefix: '',
|
||||
})}</g>`
|
||||
);
|
||||
const iconBBox = iconEl.node().getBBox();
|
||||
iconWidth = iconBBox.width;
|
||||
}
|
||||
|
||||
const text = await createText(labelEl, node.label, {
|
||||
style: node.labelStyle,
|
||||
useHtmlLabels,
|
||||
@@ -47,8 +63,9 @@ const rect = async (parent, node) => {
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
|
||||
if (node.width <= bbox.width + node.padding) {
|
||||
const labelWidth = iconWidth > 0 ? iconWidth + bbox.width : bbox.width;
|
||||
const width = node.width <= labelWidth + node.padding ? labelWidth + node.padding : node.width;
|
||||
if (node.width <= labelWidth + node.padding) {
|
||||
node.diff = (width - node.width) / 2 - node.padding;
|
||||
} else {
|
||||
node.diff = -node.padding;
|
||||
@@ -93,11 +110,20 @@ const rect = async (parent, node) => {
|
||||
.attr('height', height);
|
||||
}
|
||||
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
labelEl.attr(
|
||||
'transform',
|
||||
// This puts the label on top of the box instead of inside it
|
||||
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||
);
|
||||
|
||||
if (node.icon) {
|
||||
const textX = iconWidth > 0 ? iconWidth + node.padding / 4 : node.padding / 4;
|
||||
const textY = node.padding / 4;
|
||||
select(text).attr('transform', `translate(${textX}, ${textY})`);
|
||||
|
||||
const labelX = x + node.padding / 4;
|
||||
const labelY = y + node.padding / 4;
|
||||
labelEl.attr('transform', `translate(${labelX}, ${labelY})`);
|
||||
} else {
|
||||
const labelX = node.x - labelWidth / 2;
|
||||
const labelY = node.y - node.height / 2 + subGraphTitleTopMargin;
|
||||
labelEl.attr('transform', `translate(${labelX}, ${labelY})`);
|
||||
}
|
||||
|
||||
if (labelStyles) {
|
||||
const span = labelEl.select('span');
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
# @mermaid-js/parser
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7051](https://github.com/mermaid-js/mermaid/pull/7051) [`63df702`](https://github.com/mermaid-js/mermaid/commit/63df7021462e8dc1f2aaecb9c5febbbbde4c38e3) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - Add validation for negative values in pie charts:
|
||||
|
||||
Prevents crashes during parsing by validating values post-parsing.
|
||||
|
||||
Provides clearer, user-friendly error messages for invalid negative inputs.
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/parser",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.2",
|
||||
"description": "MermaidJS parser",
|
||||
"author": "Yokozuna59",
|
||||
"contributors": [
|
||||
|
||||
@@ -1,36 +1,5 @@
|
||||
# mermaid
|
||||
|
||||
## 11.12.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7200](https://github.com/mermaid-js/mermaid/pull/7200) [`de7ed10`](https://github.com/mermaid-js/mermaid/commit/de7ed1033996d702e3983dcf8114f33faea89577) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - fix: validate dates and tick interval to prevent UI freeze/crash in gantt diagramtype
|
||||
|
||||
## 11.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7107](https://github.com/mermaid-js/mermaid/pull/7107) [`cbf8946`](https://github.com/mermaid-js/mermaid/commit/cbf89462acecac7a06f19843e8d48cb137df0753) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - fix: Updated the dependency dagre-d3-es to 7.0.13 to fix GHSA-cc8p-78qf-8p7q
|
||||
|
||||
## 11.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6921](https://github.com/mermaid-js/mermaid/pull/6921) [`764b315`](https://github.com/mermaid-js/mermaid/commit/764b315dc16d0359add7c6b8e3ef7592e9bdc09c) Thanks [@quilicicf](https://github.com/quilicicf)! - feat: Add IDs in architecture diagrams
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6950](https://github.com/mermaid-js/mermaid/pull/6950) [`a957908`](https://github.com/mermaid-js/mermaid/commit/a9579083bfba367a4f4678547ec37ed7b61b9f5b) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - chore: Fix mindmap rendering in docs and apply tidytree layout
|
||||
|
||||
- [#6826](https://github.com/mermaid-js/mermaid/pull/6826) [`1d36810`](https://github.com/mermaid-js/mermaid/commit/1d3681053b9168354e48e5763023b6305cd1ca72) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Ensure edge label color is applied when using classDef with edge IDs
|
||||
|
||||
- [#6945](https://github.com/mermaid-js/mermaid/pull/6945) [`d318f1a`](https://github.com/mermaid-js/mermaid/commit/d318f1a13cd7429334a29c70e449074ec1cb9f68) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Resolve gantt chart crash due to invalid array length
|
||||
|
||||
- [#6918](https://github.com/mermaid-js/mermaid/pull/6918) [`cfe9238`](https://github.com/mermaid-js/mermaid/commit/cfe9238882cbe95416db1feea3112456a71b6aaf) Thanks [@shubhamparikh2704](https://github.com/shubhamparikh2704)! - 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
|
||||
|
||||
## 11.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/tiny",
|
||||
"version": "11.12.2",
|
||||
"version": "11.11.0",
|
||||
"description": "Tiny version of mermaid",
|
||||
"type": "commonjs",
|
||||
"main": "./dist/mermaid.tiny.js",
|
||||
|
||||
1365
pnpm-lock.yaml
generated
1365
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user