Compare commits

..

10 Commits

Author SHA1 Message Date
darshanr0107
a57b98c004 fix: address lock file issues
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-18 20:46:06 +05:30
darshanr0107
4bb5753635 fix: render group icons in unified renderer
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-18 15:32:29 +05:30
darshanr0107
22e1b17ac3 fix: group positioning
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-17 15:01:55 +05:30
darshanr0107
18f461267d fix: update pnpm-lock file
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-14 17:54:56 +05:30
darshanr0107
4430eddb24 feat(WIP): architecture-fcose layout
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-14 17:01:44 +05:30
darshanr0107
0a23304d1a fix: ensure architecture diagram uses unified rendering
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-29 15:13:19 +05:30
Shubham P
7effdc147b Merge pull request #6997 from mermaid-js/ci/enable-codeql-for-github-actions
ci(codeql): enable CodeQL for GitHub Actions
2025-09-25 08:47:48 +00:00
Alois Klink
6e67515f41 ci(codeql): enable CodeQL for GitHub Actions
Support for scanning GitHub Actions was added in 2024-12-17, see
https://github.blog/changelog/2024-12-17-find-and-fix-actions-workflows-vulnerabilities-with-codeql-public-preview/
2025-09-25 17:17:12 +09:00
Shubham P
d5c4eff251 Merge pull request #6972 from mermaid-js/renovate/patch-all-patch
fix(deps): update all patch dependencies (patch)
2025-09-22 13:49:30 +00:00
renovate[bot]
5324fd8dfd fix(deps): update all patch dependencies 2025-09-22 13:35:45 +00:00
37 changed files with 2128 additions and 927 deletions

View File

@@ -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',

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Fix mindmap rendering in docs and apply tidytree layout

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Ensure edge label color is applied when using classDef with edge IDs

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Resolve gantt chart crash due to invalid array length

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add IDs in architecture diagrams

View 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

View File

@@ -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:

View File

@@ -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

View File

@@ -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
`,
{}
);
});
});

View File

@@ -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.

View File

@@ -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"

View File

@@ -42,7 +42,7 @@
"khroma": "^2.1.0"
},
"devDependencies": {
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"mermaid": "workspace:*",
"rimraf": "^6.0.1"
},

View 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"
]
}

View 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';

View 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;
}

View 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;

View 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);
}
}
})
);
};

View 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;
}

View 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"]
}

View File

@@ -33,7 +33,7 @@
],
"license": "MIT",
"dependencies": {
"@zenuml/core": "^3.35.2"
"@zenuml/core": "^3.41.4"
},
"devDependencies": {
"mermaid": "workspace:^"

View File

@@ -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

View File

@@ -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",

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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>;

View File

@@ -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 ||

View File

@@ -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);
});
});

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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');

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/parser",
"version": "0.6.3",
"version": "0.6.2",
"description": "MermaidJS parser",
"author": "Yokozuna59",
"contributors": [

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff