diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html
index 5c8939d29..ef130569b 100644
--- a/cypress/platform/knsv2.html
+++ b/cypress/platform/knsv2.html
@@ -97,14 +97,15 @@ swimlane TB
%% swimlane 1 - A E
%% swimlane 2 - B
%% swimlane 3 - C D
-lane First
+%% lane First
A
-end
+%% end
A --> B(I am B, the wide one) --> C
C --> D & F
D --> E
A --> E
+ E --> B
B@{ shape: diam}
diff --git a/cypress/platform/shape-tester.html b/cypress/platform/shape-tester.html
index 21ab9ad85..ef6339a9f 100644
--- a/cypress/platform/shape-tester.html
+++ b/cypress/platform/shape-tester.html
@@ -56,9 +56,9 @@
logLevel: 1,
});
- let shape = 'card';
- // let simplified = true;
- let simplified = false;
+ let shape = 'circle';
+ let simplified = true;
+ // let simplified = false;
let algorithm = 'elk';
// let algorithm = 'dagre';
let code = `---
@@ -86,7 +86,7 @@ config:
layout: ${algorithm}
---
flowchart LR
-A["Abrakadabra"] --> C["C"] & C & C & C & C
+A["Abrakadabra"] --> C["I am the circle"] & C & C & C & C
%% A["Abrakadabra"] --> C
A@{ shape: ${shape}}
C@{ shape: ${shape}}
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/detector.spec.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/detector.spec.ts
new file mode 100644
index 000000000..fb5541da7
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/detector.spec.ts
@@ -0,0 +1,55 @@
+import plugin from './detector.js';
+import { describe, it } from 'vitest';
+
+const { detector } = plugin;
+
+describe('swimlane detector', () => {
+ it('should fail for dagre-d3', () => {
+ expect(
+ detector('swimlane', {
+ flowchart: {
+ defaultRenderer: 'dagre-d3',
+ },
+ })
+ ).toBe(false);
+ });
+ it('should fail for dagre-wrapper', () => {
+ expect(
+ detector('flowchart', {
+ flowchart: {
+ defaultRenderer: 'dagre-wrapper',
+ },
+ })
+ ).toBe(false);
+ });
+ it('should succeed for elk', () => {
+ expect(
+ detector('flowchart', {
+ flowchart: {
+ defaultRenderer: 'elk',
+ },
+ })
+ ).toBe(true);
+ expect(
+ detector('graph', {
+ flowchart: {
+ defaultRenderer: 'elk',
+ },
+ })
+ ).toBe(true);
+ });
+
+ it('should detect swimlane', () => {
+ expect(detector('swimlane')).toBe(true);
+ });
+
+ it('should not detect class with defaultRenderer set to elk', () => {
+ expect(
+ detector('class', {
+ flowchart: {
+ defaultRenderer: 'elk',
+ },
+ })
+ ).toBe(false);
+ });
+});
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/detector.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/detector.ts
new file mode 100644
index 000000000..19e7292f3
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/detector.ts
@@ -0,0 +1,29 @@
+
+import type {
+ ExternalDiagramDefinition,
+ DiagramDetector,
+ DiagramLoader,
+} from '../../../diagram-api/types.js';
+const id = 'swimlane';
+
+
+const detector: DiagramDetector = (txt, config): boolean => {
+ if (txt.match(/^\s*swimlane/)) {
+ return true;
+ }
+ return false;
+};
+
+
+const loader: DiagramLoader = async () => {
+ const { diagram } = await import('./swimlane-definition.js');
+ return { id, diagram };
+};
+
+const plugin: ExternalDiagramDefinition = {
+ id,
+ detector,
+ loader,
+};
+
+export default plugin;
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/render-utils.spec.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/render-utils.spec.ts
new file mode 100644
index 000000000..d048b07a3
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/render-utils.spec.ts
@@ -0,0 +1,40 @@
+import { findCommonAncestor, TreeData } from './render-utils.js';
+describe('when rendering a flowchart using elk ', () => {
+ let lookupDb: TreeData;
+ beforeEach(() => {
+ lookupDb = {
+ parentById: {
+ B4: 'inner',
+ B5: 'inner',
+ C4: 'inner2',
+ C5: 'inner2',
+ B2: 'Ugge',
+ B3: 'Ugge',
+ inner: 'Ugge',
+ inner2: 'Ugge',
+ B6: 'outer',
+ },
+ childrenById: {
+ inner: ['B4', 'B5'],
+ inner2: ['C4', 'C5'],
+ Ugge: ['B2', 'B3', 'inner', 'inner2'],
+ outer: ['B6'],
+ },
+ };
+ });
+ it('to find parent of siblings in a subgraph', () => {
+ expect(findCommonAncestor('B4', 'B5', lookupDb)).toBe('inner');
+ });
+ it('to find an uncle', () => {
+ expect(findCommonAncestor('B4', 'B2', lookupDb)).toBe('Ugge');
+ });
+ it('to find a cousin', () => {
+ expect(findCommonAncestor('B4', 'C4', lookupDb)).toBe('Ugge');
+ });
+ it('to find a grandparent', () => {
+ expect(findCommonAncestor('B4', 'B6', lookupDb)).toBe('root');
+ });
+ it('to find ancestor of siblings in the root', () => {
+ expect(findCommonAncestor('B1', 'outer', lookupDb)).toBe('root');
+ });
+});
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/render-utils.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/render-utils.ts
new file mode 100644
index 000000000..ebdc01cf7
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/render-utils.ts
@@ -0,0 +1,25 @@
+export interface TreeData {
+ parentById: Record;
+ childrenById: Record;
+}
+
+export const findCommonAncestor = (id1: string, id2: string, treeData: TreeData) => {
+ const { parentById } = treeData;
+ const visited = new Set();
+ let currentId = id1;
+ while (currentId) {
+ visited.add(currentId);
+ if (currentId === id2) {
+ return currentId;
+ }
+ currentId = parentById[currentId];
+ }
+ currentId = id2;
+ while (currentId) {
+ if (visited.has(currentId)) {
+ return currentId;
+ }
+ currentId = parentById[currentId];
+ }
+ return 'root';
+};
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/setup-graph.js b/packages/mermaid/src/diagrams/swimlane/lost-and-found/setup-graph.js
new file mode 100644
index 000000000..f19453249
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/setup-graph.js
@@ -0,0 +1,395 @@
+import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
+import { select, curveLinear, selectAll } from 'd3';
+import { getConfig } from '../../../config.js';
+import utils from '../../../utils.js';
+
+import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
+import { log } from '../../../logger.js';
+import common, { evaluate } from '../../common/common.js';
+import { interpolateToCurve, getStylesFromArray } from '../../../utils.js';
+
+const conf = {};
+export const setConf = function (cnf) {
+ const keys = Object.keys(cnf);
+ for (const key of keys) {
+ conf[key] = cnf[key];
+ }
+};
+
+/**
+ * Add edges to graph based on parsed graph definition
+ *
+ * @param {object} edges The edges to add to the graph
+ * @param {object} g The graph object
+ * @param diagObj
+ */
+export const addEdges = function (edges, g, diagObj,svg) {
+ log.info('abc78 edges = ', edges);
+ let cnt = 0;
+ let linkIdCnt = {};
+
+ let defaultStyle;
+ let defaultLabelStyle;
+
+ if (edges.defaultStyle !== undefined) {
+ const defaultStyles = getStylesFromArray(edges.defaultStyle);
+ defaultStyle = defaultStyles.style;
+ defaultLabelStyle = defaultStyles.labelStyle;
+ }
+
+ edges.forEach(function (edge) {
+ cnt++;
+
+ // Identify Link
+ var linkIdBase = 'L-' + edge.start + '-' + edge.end;
+ // count the links from+to the same node to give unique id
+ if (linkIdCnt[linkIdBase] === undefined) {
+ linkIdCnt[linkIdBase] = 0;
+ log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
+ } else {
+ linkIdCnt[linkIdBase]++;
+ log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
+ }
+ let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase];
+ log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
+ var linkNameStart = 'LS-' + edge.start;
+ var linkNameEnd = 'LE-' + edge.end;
+
+ const edgeData = { style: '', labelStyle: '' };
+ edgeData.minlen = edge.length || 1;
+ //edgeData.id = 'id' + cnt;
+
+ // Set link type for rendering
+ if (edge.type === 'arrow_open') {
+ edgeData.arrowhead = 'none';
+ } else {
+ edgeData.arrowhead = 'normal';
+ }
+
+ // Check of arrow types, placed here in order not to break old rendering
+ edgeData.arrowTypeStart = 'arrow_open';
+ edgeData.arrowTypeEnd = 'arrow_open';
+
+ /* eslint-disable no-fallthrough */
+ switch (edge.type) {
+ case 'double_arrow_cross':
+ edgeData.arrowTypeStart = 'arrow_cross';
+ case 'arrow_cross':
+ edgeData.arrowTypeEnd = 'arrow_cross';
+ break;
+ case 'double_arrow_point':
+ edgeData.arrowTypeStart = 'arrow_point';
+ case 'arrow_point':
+ edgeData.arrowTypeEnd = 'arrow_point';
+ break;
+ case 'double_arrow_circle':
+ edgeData.arrowTypeStart = 'arrow_circle';
+ case 'arrow_circle':
+ edgeData.arrowTypeEnd = 'arrow_circle';
+ break;
+ }
+
+ let style = '';
+ let labelStyle = '';
+
+ switch (edge.stroke) {
+ case 'normal':
+ style = 'fill:none;';
+ if (defaultStyle !== undefined) {
+ style = defaultStyle;
+ }
+ if (defaultLabelStyle !== undefined) {
+ labelStyle = defaultLabelStyle;
+ }
+ edgeData.thickness = 'normal';
+ edgeData.pattern = 'solid';
+ break;
+ case 'dotted':
+ edgeData.thickness = 'normal';
+ edgeData.pattern = 'dotted';
+ edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
+ break;
+ case 'thick':
+ edgeData.thickness = 'thick';
+ edgeData.pattern = 'solid';
+ edgeData.style = 'stroke-width: 3.5px;fill:none;';
+ break;
+ case 'invisible':
+ edgeData.thickness = 'invisible';
+ edgeData.pattern = 'solid';
+ edgeData.style = 'stroke-width: 0;fill:none;';
+ break;
+ }
+ if (edge.style !== undefined) {
+ const styles = getStylesFromArray(edge.style);
+ style = styles.style;
+ labelStyle = styles.labelStyle;
+ }
+
+ edgeData.style = edgeData.style += style;
+ edgeData.labelStyle = edgeData.labelStyle += labelStyle;
+
+ if (edge.interpolate !== undefined) {
+ edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
+ } else if (edges.defaultInterpolate !== undefined) {
+ edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
+ } else {
+ edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
+ }
+
+ if (edge.text === undefined) {
+ if (edge.style !== undefined) {
+ edgeData.arrowheadStyle = 'fill: #333';
+ }
+ } else {
+ edgeData.arrowheadStyle = 'fill: #333';
+ edgeData.labelpos = 'c';
+ }
+
+ edgeData.labelType = edge.labelType;
+ edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
+
+ if (edge.style === undefined) {
+ edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
+ }
+
+ edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
+
+ edgeData.id = linkId;
+ edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
+
+ // Add the edge to the graph
+ g.setEdge(edge.start, edge.end, edgeData, cnt);
+ });
+};
+
+/**
+ * Function that adds the vertices found during parsing to the graph to be rendered.
+ *
+ * @param vert Object containing the vertices.
+ * @param g The graph that is to be drawn.
+ * @param svgId
+ * @param root
+ * @param doc
+ * @param diagObj
+ */
+export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
+ const svg = root.select(`[id="${svgId}"]`);
+ const keys = Object.keys(vert);
+
+ // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
+ keys.forEach(function (id) {
+ const vertex = vert[id];
+
+ /**
+ * Variable for storing the classes for the vertex
+ *
+ * @type {string}
+ */
+ let classStr = 'default';
+ if (vertex.classes.length > 0) {
+ classStr = vertex.classes.join(' ');
+ }
+ classStr = classStr + ' flowchart-label';
+ const styles = getStylesFromArray(vertex.styles);
+
+ // Use vertex id as text in the box if no text is provided by the graph definition
+ let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
+
+ // We create a SVG label, either by delegating to addHtmlLabel or manually
+ let vertexNode;
+ log.info('vertex', vertex, vertex.labelType);
+ if (vertex.labelType === 'markdown') {
+ log.info('vertex', vertex, vertex.labelType);
+ } else {
+ if (evaluate(getConfig().flowchart.htmlLabels) && svg.html) {
+ // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
+ const node = {
+ label: vertexText.replace(
+ /fa[blrs]?:fa-[\w-]+/g,
+ (s) => ``
+ ),
+ };
+ vertexNode = addHtmlLabel(svg, node).node();
+ vertexNode.parentNode.removeChild(vertexNode);
+ } else {
+ const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
+ svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
+
+ const rows = vertexText.split(common.lineBreakRegex);
+
+ for (const row of rows) {
+ const tspan = doc.createElementNS('http://www.w3.org/2000/svg', 'tspan');
+ tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
+ tspan.setAttribute('dy', '1em');
+ tspan.setAttribute('x', '1');
+ tspan.textContent = row;
+ svgLabel.appendChild(tspan);
+ }
+ vertexNode = svgLabel;
+ }
+ }
+
+ let radious = 0;
+ let _shape = '';
+ // Set the shape based parameters
+ switch (vertex.type) {
+ case 'round':
+ radious = 5;
+ _shape = 'rect';
+ break;
+ case 'square':
+ _shape = 'rect';
+ break;
+ case 'diamond':
+ _shape = 'question';
+ break;
+ case 'hexagon':
+ _shape = 'hexagon';
+ break;
+ case 'odd':
+ _shape = 'rect_left_inv_arrow';
+ break;
+ case 'lean_right':
+ _shape = 'lean_right';
+ break;
+ case 'lean_left':
+ _shape = 'lean_left';
+ break;
+ case 'trapezoid':
+ _shape = 'trapezoid';
+ break;
+ case 'inv_trapezoid':
+ _shape = 'inv_trapezoid';
+ break;
+ case 'odd_right':
+ _shape = 'rect_left_inv_arrow';
+ break;
+ case 'circle':
+ _shape = 'circle';
+ break;
+ case 'ellipse':
+ _shape = 'ellipse';
+ break;
+ case 'stadium':
+ _shape = 'stadium';
+ break;
+ case 'subroutine':
+ _shape = 'subroutine';
+ break;
+ case 'cylinder':
+ _shape = 'cylinder';
+ break;
+ case 'group':
+ _shape = 'rect';
+ break;
+ case 'doublecircle':
+ _shape = 'doublecircle';
+ break;
+ default:
+ _shape = 'rect';
+ }
+ // Add the node
+ g.setNode(vertex.id, {
+ labelStyle: styles.labelStyle,
+ shape: _shape,
+ labelText: vertexText,
+ labelType: vertex.labelType,
+ rx: radious,
+ ry: radious,
+ class: classStr,
+ style: styles.style,
+ id: vertex.id,
+ link: vertex.link,
+ linkTarget: vertex.linkTarget,
+ tooltip: diagObj.db.getTooltip(vertex.id) || '',
+ domId: diagObj.db.lookUpDomId(vertex.id),
+ haveCallback: vertex.haveCallback,
+ width: vertex.type === 'group' ? 500 : undefined,
+ dir: vertex.dir,
+ type: vertex.type,
+ props: vertex.props,
+ padding: getConfig().flowchart.padding,
+ });
+
+ log.info('setNode', {
+ labelStyle: styles.labelStyle,
+ labelType: vertex.labelType,
+ shape: _shape,
+ labelText: vertexText,
+ rx: radious,
+ ry: radious,
+ class: classStr,
+ style: styles.style,
+ id: vertex.id,
+ domId: diagObj.db.lookUpDomId(vertex.id),
+ width: vertex.type === 'group' ? 500 : undefined,
+ type: vertex.type,
+ dir: vertex.dir,
+ props: vertex.props,
+ padding: getConfig().flowchart.padding,
+ });
+ });
+};
+
+/**
+ *
+ * @param diagObj
+ * @param id
+ * @param root
+ * @param doc
+ */
+function setupGraph(diagObj, id, root, doc) {
+ const { securityLevel, flowchart: conf } = getConfig();
+ const nodeSpacing = conf.nodeSpacing || 50;
+ const rankSpacing = conf.rankSpacing || 50;
+
+ // Fetch the default direction, use TD if none was found
+ let dir = diagObj.db.getDirection();
+ if (dir === undefined) {
+ dir = 'TD';
+ }
+
+ // Create the input mermaid.graph
+ const g = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ })
+ .setGraph({
+ rankdir: dir,
+ nodesep: nodeSpacing,
+ ranksep: rankSpacing,
+ marginx: 0,
+ marginy: 0,
+ })
+ .setDefaultEdgeLabel(function () {
+ return {};
+ });
+
+ let subG;
+ const subGraphs = diagObj.db.getSubGraphs();
+
+ // Fetch the vertices/nodes and edges/links from the parsed graph definition
+ const vert = diagObj.db.getVertices();
+
+ const edges = diagObj.db.getEdges();
+
+ log.info('Edges', edges);
+ let i = 0;
+ // for (i = subGraphs.length - 1; i >= 0; i--) {
+ // // for (let i = 0; i < subGraphs.length; i++) {
+ // subG = subGraphs[i];
+
+ // selectAll('cluster').append('text');
+
+ // for (let j = 0; j < subG.nodes.length; j++) {
+ // log.info('Setting up subgraphs', subG.nodes[j], subG.id);
+ // g.setParent(subG.nodes[j], subG.id);
+ // }
+ // }
+ addVertices(vert, g, id, root, doc, diagObj);
+ addEdges(edges, g, diagObj);
+ return g;
+}
+
+export default setupGraph;
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/styles.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/styles.ts
new file mode 100644
index 000000000..60659df45
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/styles.ts
@@ -0,0 +1,143 @@
+/** Returns the styles given options */
+export interface FlowChartStyleOptions {
+ arrowheadColor: string;
+ border2: string;
+ clusterBkg: string;
+ clusterBorder: string;
+ edgeLabelBackground: string;
+ fontFamily: string;
+ lineColor: string;
+ mainBkg: string;
+ nodeBorder: string;
+ nodeTextColor: string;
+ tertiaryColor: string;
+ textColor: string;
+ titleColor: string;
+ [key: string]: string;
+}
+
+const genSections = (options: FlowChartStyleOptions) => {
+ let sections = '';
+
+ for (let i = 0; i < 5; i++) {
+ sections += `
+ .subgraph-lvl-${i} {
+ fill: ${options[`surface${i}`]};
+ stroke: ${options[`surfacePeer${i}`]};
+ }
+ `;
+ }
+ return sections;
+};
+
+const getStyles = (options: FlowChartStyleOptions) =>
+ `.label {
+ font-family: ${options.fontFamily};
+ color: ${options.nodeTextColor || options.textColor};
+ }
+ .cluster-label text {
+ fill: ${options.titleColor};
+ }
+ .cluster-label span {
+ color: ${options.titleColor};
+ }
+
+ .label text,span {
+ fill: ${options.nodeTextColor || options.textColor};
+ color: ${options.nodeTextColor || options.textColor};
+ }
+
+ .node rect,
+ .node circle,
+ .node ellipse,
+ .node polygon,
+ .node path {
+ fill: ${options.mainBkg};
+ stroke: ${options.nodeBorder};
+ stroke-width: 1px;
+ }
+
+ .node .label {
+ text-align: center;
+ }
+ .node.clickable {
+ cursor: pointer;
+ }
+
+ .arrowheadPath {
+ fill: ${options.arrowheadColor};
+ }
+
+ .edgePath .path {
+ stroke: ${options.lineColor};
+ stroke-width: 2.0px;
+ }
+
+ .flowchart-link {
+ stroke: ${options.lineColor};
+ fill: none;
+ }
+
+ .edgeLabel {
+ background-color: ${options.edgeLabelBackground};
+ rect {
+ opacity: 0.85;
+ background-color: ${options.edgeLabelBackground};
+ fill: ${options.edgeLabelBackground};
+ }
+ text-align: center;
+ }
+
+ .cluster rect {
+ fill: ${options.clusterBkg};
+ stroke: ${options.clusterBorder};
+ stroke-width: 1px;
+ }
+
+ .cluster text {
+ fill: ${options.titleColor};
+ }
+
+ .cluster span {
+ color: ${options.titleColor};
+ }
+ /* .cluster div {
+ color: ${options.titleColor};
+ } */
+
+ div.mermaidTooltip {
+ position: absolute;
+ text-align: center;
+ max-width: 200px;
+ padding: 2px;
+ font-family: ${options.fontFamily};
+ font-size: 12px;
+ background: ${options.tertiaryColor};
+ border: 1px solid ${options.border2};
+ border-radius: 2px;
+ pointer-events: none;
+ z-index: 100;
+ }
+
+ .flowchartTitleText {
+ text-anchor: middle;
+ font-size: 18px;
+ fill: ${options.textColor};
+ }
+ .subgraph {
+ stroke-width:2;
+ rx:3;
+ }
+ // .subgraph-lvl-1 {
+ // fill:#ccc;
+ // // stroke:black;
+ // }
+
+ .flowchart-label text {
+ text-anchor: middle;
+ }
+
+ ${genSections(options)}
+`;
+
+export default getStyles;
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-definition.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-definition.ts
new file mode 100644
index 000000000..6e35c1253
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-definition.ts
@@ -0,0 +1,13 @@
+// @ts-ignore: JISON typing missing
+import parser from '../parser/flow.jison';
+
+import * as db from '../flowDb.js';
+import renderer from './swimlaneRenderer.js';
+import styles from './styles.js';
+
+export const diagram = {
+ db,
+ renderer,
+ parser,
+ styles,
+};
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-layout.js b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-layout.js
new file mode 100644
index 000000000..074d9e6da
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-layout.js
@@ -0,0 +1,220 @@
+import { log } from '../../../logger.js';
+import flowDb from '../flowDb.js';
+
+export const getSubgraphLookupTable = function (diagObj) {
+ const subGraphs = diagObj.db.getSubGraphs();
+ const subgraphDb = {};
+ log.info('Subgraphs - ', subGraphs);
+ for (let i = subGraphs.length - 1; i >= 0; i--) {
+ const subG = subGraphs[i];
+ log.info('Subgraph - ', subG);
+ for (let j = 0; j < subG.nodes.length; j++) {
+ log.info('Setting up subgraphs', subG.nodes[j], subG.id);
+ subgraphDb[flowDb.lookUpId(subG.nodes[j])] = subG.id;
+ }
+ }
+ return subgraphDb;
+};
+
+/**
+ *
+ * @param graph
+ * @param subgraphLookupTable
+ */
+export function assignRanks(graph, subgraphLookupTable) {
+ let visited = new Set();
+ const lock = new Map();
+ const ranks = new Map();
+ let cnt = 0;
+ let changesDetected = true;
+
+ /**
+ *
+ * @param nodeId
+ * @param currentRank
+ */
+ function dfs(nodeId, currentRank) {
+ if (visited.has(nodeId)) {
+ return;
+ }
+
+ visited.add(nodeId);
+ const existingRank = ranks.get(nodeId) || 0;
+
+ // console.log('APA444 DFS Base case for', nodeId, 'to', Math.max(existingRank, currentRank));
+ if (lock.get(nodeId) !== 1) {
+ ranks.set(nodeId, Math.max(existingRank, currentRank));
+ } else {
+ console.log(
+ 'APA444 ',
+ nodeId,
+ 'was locked to ',
+ existingRank,
+ 'so not changing it',
+ ranks.get(nodeId)
+ );
+ }
+
+ const currentRankAdjusted = ranks.get(nodeId) || currentRank;
+ graph.successors(nodeId).forEach((targetId) => {
+ if (subgraphLookupTable[targetId] !== subgraphLookupTable[nodeId]) {
+ dfs(targetId, currentRankAdjusted);
+ } else {
+ // In same line, easy increase
+ dfs(targetId, currentRankAdjusted + 1);
+ }
+ });
+ }
+
+ /**
+ *
+ */
+ function adjustSuccessors() {
+ console.log('APA444 Adjusting successors');
+ graph.nodes().forEach((nodeId) => {
+ console.log('APA444 Going through nodes', nodeId);
+ // if (graph.predecessors(nodeId).length === 0) {
+ console.log('APA444 has no predecessors', nodeId);
+ graph.successors(nodeId).forEach((successorNodeId) => {
+ console.log('APA444 has checking successor', successorNodeId);
+ if (subgraphLookupTable[successorNodeId] !== subgraphLookupTable[nodeId]) {
+ const newRank = ranks.get(successorNodeId);
+ ranks.set(nodeId, newRank);
+ console.log('APA444 POST-process case for', nodeId, 'to', newRank);
+ lock.set(nodeId, 1);
+ changesDetected = true;
+ // setRankFromTopNodes();
+
+ // Adjust ranks of successors in the same subgraph
+ graph.successors(nodeId).forEach((sameSubGraphSuccessorNodeId) => {
+ if (subgraphLookupTable[sameSubGraphSuccessorNodeId] === subgraphLookupTable[nodeId]) {
+ console.log(
+ 'APA444 Adjusting rank of',
+ sameSubGraphSuccessorNodeId,
+ 'to',
+ newRank + 1
+ );
+ ranks.set(sameSubGraphSuccessorNodeId, newRank + 1);
+ lock.set(sameSubGraphSuccessorNodeId, 1);
+ changesDetected = true;
+ // dfs(sameSubGraphSuccessorNodeId, newRank + 1);
+ // setRankFromTopNodes();
+ }
+ });
+ } else {
+ console.log('APA444 Node', nodeId, ' and ', successorNodeId, ' is in the same lane');
+ }
+ });
+ // }
+ });
+ }
+
+ /**
+ *
+ */
+ function setRankFromTopNodes() {
+ visited = new Set();
+ graph.nodes().forEach((nodeId) => {
+ if (graph.predecessors(nodeId).length === 0) {
+ dfs(nodeId, 0);
+ }
+ });
+ adjustSuccessors();
+ }
+
+ while (changesDetected && cnt < 10) {
+ setRankFromTopNodes();
+ cnt++;
+ }
+ // Post-process the ranks
+
+ return ranks;
+}
+
+/**
+ *
+ * @param graph
+ * @param subgraphLĂ–ookupTable
+ * @param ranks
+ * @param subgraphLookupTable
+ */
+export function assignAffinities(graph, ranks, subgraphLookupTable) {
+ const affinities = new Map();
+ const swimlaneRankAffinities = new Map();
+ const swimlaneMaxAffinity = new Map();
+
+ graph.nodes().forEach((nodeId) => {
+ const swimlane = subgraphLookupTable[nodeId];
+ const rank = ranks.get(nodeId);
+ const key = swimlane + ':' + rank;
+ let currentAffinity = swimlaneRankAffinities.get(key);
+ if (currentAffinity === undefined) {
+ currentAffinity = -1;
+ }
+ const newAffinity = currentAffinity + 1;
+ swimlaneRankAffinities.set(key, newAffinity);
+ affinities.set(nodeId, newAffinity);
+ let currentMaxAffinity = swimlaneMaxAffinity.get(swimlane);
+ if (currentMaxAffinity === undefined) {
+ swimlaneMaxAffinity.set(swimlane, 0);
+ currentMaxAffinity = 0;
+ }
+ if (newAffinity > currentMaxAffinity) {
+ swimlaneMaxAffinity.set(swimlane, newAffinity);
+ }
+ });
+
+ // console.log('APA444 affinities', swimlaneRankAffinities);
+
+ return { affinities, swimlaneMaxAffinity };
+ //return affinities;
+}
+
+/**
+ *
+ * @param graph
+ * @param diagObj
+ */
+export function swimlaneLayout(graph, diagObj) {
+ const subgraphLookupTable = getSubgraphLookupTable(diagObj);
+ const ranks = assignRanks(graph, subgraphLookupTable);
+
+ const { affinities, swimlaneMaxAffinity } = assignAffinities(graph, ranks, subgraphLookupTable);
+ // const affinities = assignAffinities(graph, ranks, subgraphLookupTable);
+
+ const subGraphs = diagObj.db.getSubGraphs();
+ const lanes = [];
+ const laneDb = {};
+ let xPos = 0;
+ for (const subG of subGraphs) {
+ const maxAffinity = swimlaneMaxAffinity.get(subG.id);
+ const lane = {
+ title: subG.title,
+ x: xPos,
+ width: 200 + maxAffinity * 150,
+ };
+ xPos += lane.width;
+ lanes.push(lane);
+ laneDb[subG.id] = lane;
+ }
+
+ const rankWidth = [];
+ // Basic layout, calculate the node positions based on rank
+ graph.nodes().forEach((nodeId) => {
+ const rank = ranks.get(nodeId);
+
+ if (!rankWidth[rank]) {
+ const laneId = subgraphLookupTable[nodeId];
+ const lane = laneDb[laneId];
+ const n = graph.node(nodeId);
+ console.log('Node', nodeId, n);
+ const affinity = affinities.get(nodeId);
+
+ console.log('APA444', nodeId, 'rank', rank, 'affinity', affinity);
+ graph.setNode(nodeId, { y: rank * 200 + 50, x: lane.x + 150 * affinity + 100 });
+ // lane.width = Math.max(lane.width, lane.x + 150*affinity + lane.width / 4);
+ }
+ });
+
+ return { graph, lanes };
+}
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-layout.spec.ts b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-layout.spec.ts
new file mode 100644
index 000000000..96420ee11
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlane-layout.spec.ts
@@ -0,0 +1,129 @@
+import flowDb from '../flowDb.js';
+import { cleanupComments } from '../../../diagram-api/comments.js';
+import setupGraph from './setup-graph.js';
+import { swimlaneLayout, assignRanks, getSubgraphLookupTable } from './swimlane-layout.js';
+import { getDiagramFromText } from '../../../Diagram.js';
+import { addDiagrams } from '../../../diagram-api/diagram-orchestration.ts';
+import jsdom from 'jsdom';
+
+const { JSDOM } = jsdom;
+
+addDiagrams();
+describe('When doing a assigning ranks specific for swim lanes ', () => {
+ let root;
+ let doc;
+ beforeEach(function () {
+ const dom = new JSDOM(`My First JSDOM!
`);
+ root = select(dom.window.document.getElementById('swimmer'));
+ root.html = () => {
+ ' return hello
';
+ };
+
+ doc = dom.window.document;
+ });
+ describe('Layout: ', () => {
+ // it('should rank the nodes:', async () => {
+ // const diagram = await getDiagramFromText(`swimlane LR
+ // subgraph "\`one\`"
+ // start --> cat --> rat
+ // end`);
+ // const g = setupGraph(diagram, 'swimmer', root, doc);
+ // const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ // const ranks = assignRanks(g, subgraphLookupTable);
+ // expect(ranks.get('start')).toEqual(0);
+ // expect(ranks.get('cat')).toEqual(1);
+ // expect(ranks.get('rat')).toEqual(2);
+ // });
+
+ it('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat
+ end
+ subgraph "\`two\`"
+ monkey --> dog --> done
+ end
+ cat --> monkey`);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const ranks = assignRanks(g, subgraphLookupTable);
+ expect(ranks.get('start')).toEqual(0);
+ expect(ranks.get('cat')).toEqual(1);
+ expect(ranks.get('rat')).toEqual(2);
+ expect(ranks.get('monkey')).toEqual(1);
+ expect(ranks.get('dog')).toEqual(2);
+ expect(ranks.get('done')).toEqual(3);
+ });
+ });
+ describe('Layout: ', () => {
+ it('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat
+ end`);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const { graph, lanes } = swimlaneLayout(g, diagram);
+ expect(lanes.length).toBe(1);
+ const start = graph.node('start');
+ const cat = graph.node('cat');
+ const rat = graph.node('rat');
+ expect(start.y).toBe(50);
+ expect(cat.y).toBe(250);
+ expect(rat.y).toBe(450);
+ expect(rat.x).toBe(100);
+ });
+
+ it('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat
+ end
+ subgraph "\`two\`"
+ monkey --> dog --> done
+ end
+ cat --> monkey`);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const { graph, lanes } = swimlaneLayout(g, diagram);
+ expect(lanes.length).toBe(2);
+ const start = graph.node('start');
+ const cat = graph.node('cat');
+ const rat = graph.node('rat');
+ const monkey = graph.node('monkey');
+ const dog = graph.node('dog');
+ const done = graph.node('done');
+
+ expect(start.y).toBe(50);
+ expect(cat.y).toBe(250);
+ expect(rat.y).toBe(450);
+ expect(rat.x).toBe(100);
+ expect(monkey.y).toBe(250);
+ expect(dog.y).toBe(450);
+ expect(done.y).toBe(650);
+ expect(monkey.x).toBe(300);
+ });
+ it.only('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat & hat
+ end
+ `);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const { graph, lanes } = swimlaneLayout(g, diagram);
+ expect(lanes.length).toBe(1);
+ const start = graph.node('start');
+ const cat = graph.node('cat');
+ const rat = graph.node('rat');
+ const hat = graph.node('rat');
+
+ expect(start.y).toBe(50);
+ expect(cat.y).toBe(250);
+ expect(rat.y).toBe(450);
+ expect(rat.x).toBe(300);
+ expect(hat.y).toBe(450);
+ expect(hat.x).toBe(100);
+ });
+ });
+});
diff --git a/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlaneRenderer.js b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlaneRenderer.js
new file mode 100644
index 000000000..6b1c9b2a5
--- /dev/null
+++ b/packages/mermaid/src/diagrams/swimlane/lost-and-found/swimlaneRenderer.js
@@ -0,0 +1,596 @@
+import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
+import { select, curveLinear, selectAll } from 'd3';
+import { swimlaneLayout } from './swimlane-layout.js';
+import insertMarkers from '../../../dagre-wrapper/markers.js';
+import { insertNode } from '../../../dagre-wrapper/nodes.js';
+import flowDb from '../flowDb.js';
+import { getConfig } from '../../../config.js';
+import {getStylesFromArray} from '../../../utils.js';
+import setupGraph, { addEdges, addVertices } from './setup-graph.js';
+import { render } from '../../../dagre-wrapper/index.js';
+import { log } from '../../../logger.js';
+import { setupGraphViewbox } from '../../../setupGraphViewbox.js';
+import common, { evaluate } from '../../common/common.js';
+import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
+import { insertEdge,positionEdgeLabel } from '../../../dagre-wrapper/edges.js';
+import {
+ clear as clearGraphlib,
+ clusterDb,
+ adjustClustersAndEdges,
+ findNonClusterChild,
+ sortNodesByHierarchy,
+} from '../../../dagre-wrapper/mermaid-graphlib.js';
+
+
+const conf = {};
+export const setConf = function (cnf) {
+ const keys = Object.keys(cnf);
+ for (const key of keys) {
+ conf[key] = cnf[key];
+ }
+};
+
+/**
+ *
+ * @param element
+ * @param graph
+ * @param layout
+ * @param vert
+ * @param elem
+ * @param g
+ * @param id
+ * @param conf
+ */
+async function swimlaneRender(layout,vert, elem,g, id, conf) {
+
+ let renderedNodes = [];
+ // draw nodes from layout.graph to element
+ const nodes = layout.graph.nodes();
+
+ // lanes are the swimlanes
+ const lanes = layout.lanes;
+
+
+
+ const nodesElements = elem.insert('g').attr('class', 'nodes');
+ // for each node, draw a rect, with a child text inside as label
+ for (const node of nodes) {
+ const nodeFromLayout = layout.graph.node(node);
+ const vertex = vert[node];
+ //Initialise the node
+ /**
+ * Variable for storing the classes for the vertex
+ *
+ * @type {string}
+ */
+ let classStr = 'default';
+ if (vertex.classes.length > 0) {
+ classStr = vertex.classes.join(' ');
+ }
+ classStr = classStr + ' swimlane-label';
+ const styles = getStylesFromArray(vertex.styles);
+
+ // Use vertex id as text in the box if no text is provided by the graph definition
+ let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
+
+ // We create a SVG label, either by delegating to addHtmlLabel or manually
+ let vertexNode;
+ log.info('vertex', vertex, vertex.labelType);
+ if (vertex.labelType === 'markdown') {
+ log.info('vertex', vertex, vertex.labelType);
+ } else {
+ if (evaluate(getConfig().flowchart.htmlLabels)) {
+ // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
+ const node = {
+ label: vertexText.replace(
+ /fa[blrs]?:fa-[\w-]+/g,
+ (s) => ``
+ ),
+ };
+ vertexNode = addHtmlLabel(elem, node).node();
+ vertexNode.parentNode.removeChild(vertexNode);
+ } else {
+ const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
+ svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
+
+ const rows = vertexText.split(common.lineBreakRegex);
+
+ for (const row of rows) {
+ const tspan = doc.createElementNS('http://www.w3.org/2000/svg', 'tspan');
+ tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
+ tspan.setAttribute('dy', '1em');
+ tspan.setAttribute('x', '1');
+ tspan.textContent = row;
+ svgLabel.appendChild(tspan);
+ }
+ vertexNode = svgLabel;
+ }
+ }
+
+ let radious = 0;
+ let _shape = '';
+ // Set the shape based parameters
+ switch (vertex.type) {
+ case 'round':
+ radious = 5;
+ _shape = 'rect';
+ break;
+ case 'square':
+ _shape = 'rect';
+ break;
+ case 'diamond':
+ _shape = 'question';
+ break;
+ case 'hexagon':
+ _shape = 'hexagon';
+ break;
+ case 'odd':
+ _shape = 'rect_left_inv_arrow';
+ break;
+ case 'lean_right':
+ _shape = 'lean_right';
+ break;
+ case 'lean_left':
+ _shape = 'lean_left';
+ break;
+ case 'trapezoid':
+ _shape = 'trapezoid';
+ break;
+ case 'inv_trapezoid':
+ _shape = 'inv_trapezoid';
+ break;
+ case 'odd_right':
+ _shape = 'rect_left_inv_arrow';
+ break;
+ case 'circle':
+ _shape = 'circle';
+ break;
+ case 'ellipse':
+ _shape = 'ellipse';
+ break;
+ case 'stadium':
+ _shape = 'stadium';
+ break;
+ case 'subroutine':
+ _shape = 'subroutine';
+ break;
+ case 'cylinder':
+ _shape = 'cylinder';
+ break;
+ case 'group':
+ _shape = 'rect';
+ break;
+ case 'doublecircle':
+ _shape = 'doublecircle';
+ break;
+ default:
+ _shape = 'rect';
+ }
+ // Add the node
+ let nodeObj ={
+ labelStyle: styles.labelStyle,
+ shape: _shape,
+ labelText: vertexText,
+ labelType: vertex.labelType,
+ rx: radious,
+ ry: radious,
+ class: classStr,
+ style: styles.style,
+ id: vertex.id,
+ link: vertex.link,
+ linkTarget: vertex.linkTarget,
+ // tooltip: diagObj.db.getTooltip(vertex.id) || '',
+ // domId: diagObj.db.lookUpDomId(vertex.id),
+ haveCallback: vertex.haveCallback,
+ width: vertex.type === 'group' ? 500 : undefined,
+ dir: vertex.dir,
+ type: vertex.type,
+ props: vertex.props,
+ padding: getConfig().flowchart.padding,
+ x: nodeFromLayout.x,
+ y: nodeFromLayout.y,
+ };
+
+ let boundingBox;
+ let nodeEl;
+
+ // Add the element to the DOM
+
+ nodeEl = await insertNode(nodesElements, nodeObj, vertex.dir);
+ boundingBox = nodeEl.node().getBBox();
+ nodeEl.attr('transform', `translate(${nodeObj.x}, ${nodeObj.y / 2})`);
+
+ // add to rendered nodes
+ renderedNodes.push({id: vertex.id, nodeObj: nodeObj, boundingBox: boundingBox});
+
+ }
+
+
+ return renderedNodes;
+}
+
+/**
+ * Returns the all the styles from classDef statements in the graph definition.
+ *
+ * @param text
+ * @param diagObj
+ * @returns {object} ClassDef styles
+ */
+// export const getClasses = function (text, diagObj) {
+// log.info('Extracting classes');
+// diagObj.db.clear();
+// try {
+// // Parse the graph definition
+// diagObj.parse(text);
+// return diagObj.db.getClasses();
+// } catch (e) {
+// return;
+// }
+// };
+
+/**
+ * Returns the all the styles from classDef statements in the graph definition.
+ *
+ * @param text
+ * @param diagObj
+ * @returns {Record} ClassDef styles
+ */
+export const getClasses = function (text, diagObj) {
+ log.info('Extracting classes');
+ return diagObj.db.getClasses();
+};
+
+/**
+ * Draws a flowchart in the tag with id: id based on the graph definition in text.
+ *
+ * @param text
+ * @param id
+ */
+
+export const draw = async function (text, id, _version, diagObj) {
+ log.info('Drawing flowchart');
+ diagObj.db.clear();
+ flowDb.setGen('gen-2');
+ // Parse the graph definition
+ diagObj.parser.parse(text);
+
+ const { securityLevel, flowchart: conf } = getConfig();
+
+ // Handle root and document for when rendering in sandbox mode
+ let sandboxElement;
+ if (securityLevel === 'sandbox') {
+ sandboxElement = select('#i' + id);
+ }
+ const root =
+ securityLevel === 'sandbox'
+ ? select(sandboxElement.nodes()[0].contentDocument.body)
+ : select('body');
+ const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
+
+// create g as a graphlib graph using setupGraph from setup-graph.js
+ const g = setupGraph(diagObj, id, root, doc);
+
+
+
+ let subG;
+ const subGraphs = diagObj.db.getSubGraphs();
+ log.info('Subgraphs - ', subGraphs);
+ for (let i = subGraphs.length - 1; i >= 0; i--) {
+ subG = subGraphs[i];
+ log.info('Subgraph - ', subG);
+ diagObj.db.addVertex(
+ subG.id,
+ { text: subG.title, type: subG.labelType },
+ 'group',
+ undefined,
+ subG.classes,
+ subG.dir
+ );
+ }
+
+ // Fetch the vertices/nodes and edges/links from the parsed graph definition
+ const vert = diagObj.db.getVertices();
+
+ const edges = diagObj.db.getEdges();
+
+ log.info('Edges', edges);
+
+ const svg = root.select('#' + id);
+
+ svg.append('g');
+
+ // Run the renderer. This is what draws the final graph.
+ // const element = root.select('#' + id + ' g');
+console.log('diagObj',diagObj);
+ console.log('subGraphs', diagObj.db.getSubGraphs());
+ const layout = swimlaneLayout(g, diagObj);
+ console.log('custom layout',layout);
+
+
+ // insert markers
+ // Define the supported markers for the diagram
+ const markers = ['point', 'circle', 'cross'];
+ insertMarkers(svg, markers, 'flowchart', id);
+ // draw lanes as vertical lines
+ const lanesElements = svg.insert('g').attr('class', 'lanes');
+
+
+ let laneCount = 0;
+
+ for (const lane of layout.lanes) {
+
+ laneCount++;
+
+ //draw lane header as rectangle with lane title centered in it
+ const laneHeader = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+
+ // Set attributes for the rectangle
+ laneHeader.setAttribute("x",lane.x); // x-coordinate of the top-left corner
+ laneHeader.setAttribute("y", -50); // y-coordinate of the top-left corner
+ laneHeader.setAttribute("width", lane.width); // width of the rectangle
+ laneHeader.setAttribute("height", "50"); // height of the rectangle
+ if(laneCount % 2 == 0){
+ //set light blue color for even lanes
+ laneHeader.setAttribute("fill", "blue"); // fill color of the rectangle
+ }else{
+ //set white color odd lanes
+ laneHeader.setAttribute("fill", "grey"); // fill color of the rectangle
+ }
+
+ laneHeader.setAttribute("stroke", "black"); // color of the stroke/border
+ laneHeader.setAttribute("stroke-width", "2"); // width of the stroke/border
+
+ // Append the rectangle to the SVG element
+ lanesElements.node().appendChild(laneHeader);
+
+ //draw lane title
+ const laneTitle = document.createElementNS("http://www.w3.org/2000/svg", "text");
+
+ // Set attributes for the rectangle
+ laneTitle.setAttribute("x",lane.x + lane.width/2); // x-coordinate of the top-left corner
+ laneTitle.setAttribute("y", -50 + 50/2); // y-coordinate of the top-left corner
+ laneTitle.setAttribute("width", lane.width); // width of the rectangle
+ laneTitle.setAttribute("height", "50"); // height of the rectangle
+ laneTitle.setAttribute("fill", "white"); // fill color of the rectangle
+ laneTitle.setAttribute("stroke-width", "1"); // width of the stroke/border
+ laneTitle.setAttribute("text-anchor", "middle"); // width of the stroke/border
+ laneTitle.setAttribute("alignment-baseline", "middle"); // width of the stroke/border
+ laneTitle.setAttribute("font-size", "20"); // width of the stroke/border
+ laneTitle.textContent = lane.title;
+
+ // Append the rectangle to the SVG element
+ lanesElements.node().appendChild(laneTitle);
+
+ //draw lane
+
+ // Create a element
+ const rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+
+ // Set attributes for the rectangle
+ rectangle.setAttribute("x",lane.x); // x-coordinate of the top-left corner
+ rectangle.setAttribute("y", 0); // y-coordinate of the top-left corner
+ rectangle.setAttribute("width", lane.width); // width of the rectangle
+ rectangle.setAttribute("height", "500"); // height of the rectangle
+
+ if(laneCount % 2 == 0){
+ //set light blue color for even lanes
+ rectangle.setAttribute("fill", "lightblue"); // fill color of the rectangle
+ }else{
+ //set white color odd lanes
+ rectangle.setAttribute("fill", "#ffffff"); // fill color of the rectangle
+ }
+
+ rectangle.setAttribute("stroke", "black"); // color of the stroke/border
+ rectangle.setAttribute("stroke-width", "2"); // width of the stroke/border
+
+ // Append the rectangle to the SVG element
+ lanesElements.node().appendChild(rectangle);
+ }
+
+ // append lanesElements to elem
+ svg.node().appendChild(lanesElements.node());
+
+ // add lane headers
+ const laneHeaders = svg.insert('g').attr('class', 'laneHeaders');
+
+ let drawnEdges =[];
+
+ //get edge markers
+
+
+
+
+
+
+ let renderedNodes = await swimlaneRender(layout,vert, svg,g,id, conf);
+let renderedEdgePaths= [];
+ addEdges(edges, g, diagObj,svg);
+
+ g.edges().forEach(function (e) {
+ const edge = g.edge(e);
+ log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
+ const edgePaths = svg.insert('g').attr('class', 'edgePaths');
+
+
+
+ //get start node x, y coordinates
+
+ let sourceNode = {x:layout.graph.node(e.v).x, y:layout.graph.node(e.v).y/2, id: e.v};
+ //get end node x, y coordinates=
+ const targetNode = {x:layout.graph.node(e.w).x, y:layout.graph.node(e.w).y/2, id: e.w};
+
+
+ //create edge points based on start and end node
+ edge.points = getEdgePoints(sourceNode, targetNode, drawnEdges, renderedNodes,renderedEdgePaths);
+
+
+ // add to drawn edges
+ drawnEdges.push(edge);
+
+ const paths = insertEdge(edgePaths, e, edge, clusterDb, 'flowchart', g);
+ //positionEdgeLabel(edge, paths);
+ });
+
+
+
+
+ // utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
+
+ setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth);
+};
+
+// function to find edge path points based on start and end node
+/**
+ *
+ * @param startNode
+ * @param endNode
+ * @param drawnEdges
+ * @param renderedNodes
+ */
+function getEdgePoints(startNode, endNode, drawnEdges, renderedNodes) {
+
+ let potentialEdgePaths = [];
+
+ for(let i=1;i<=3;i++){
+ const points = [];
+
+ // add start point
+ points.push({ x: startNode.x, y: startNode.y })
+
+ // Point in the middle, if both nodes do not have same x or y
+ if (startNode.x !== endNode.x && startNode.y !== endNode.y && i!=1) {
+
+ if(i==2){
+ points.push({ x: startNode.x, y: endNode.y });
+ }else{
+ points.push({ x: endNode.x, y: startNode.y });
+ }
+ }
+ // add end point
+ points.push({ x: endNode.x, y: endNode.y });
+
+
+ //print points
+ console.log('points before intersection', points);
+
+ // get start and end node objects from array of rendered nodes
+ const startNodeObj = renderedNodes.find(node => node.id === startNode.id);
+ const endNodeObj = renderedNodes.find(node => node.id === endNode.id);
+
+ console.log(" intersection startNodeObj", startNodeObj);
+ console.log(" intersection endNodeObj", endNodeObj);
+ startNodeObj.nodeObj.x = startNode.x;
+ startNodeObj.nodeObj.y = startNode.y;
+ // the first point should be the intersection of the start node and the edge
+ let startInsection = startNodeObj.nodeObj.intersect(points[1]);
+ points[0] = startInsection;
+
+ //log intersection
+ console.log('start intersection', startInsection);
+
+ endNodeObj.nodeObj.x = endNode.x;
+ endNodeObj.nodeObj.y = endNode.y;
+ // the last point should be the intersection of the end node and the edge
+ let endInsection = endNodeObj.nodeObj.intersect(points[points.length - 2]);
+ points[points.length - 1] = endInsection;
+
+ //log intersection
+ console.log('end intersection', endInsection);
+
+ //push points to potential edge paths
+ potentialEdgePaths.push({points: points});
+ }
+
+ // Create a new list of renderedNodes without the start and end node
+ const filteredRenderedNodes = renderedNodes.filter(node => node.id !== startNode.id && node.id !== endNode.id);
+
+ //Rank the potential edge path
+ const rankedEdgePaths = rankEdgePaths(potentialEdgePaths, filteredRenderedNodes);
+ if(startNode.id==='sheep' && endNode.id === 'dog'){
+ console.log('sheep--> dog rankedEdgePaths', rankedEdgePaths);
+ }
+
+ return rankedEdgePaths[0].edgePath.points;
+
+}
+
+// Function to check if a point is inside a nodes bounding box
+/**
+ *
+ * @param point
+ * @param nodes
+ */
+function isPointInsideNode(point, nodes) {
+ let isInside = false;
+ for (const node of nodes) {
+ if (
+ point.x >= node.nodeObj.x &&
+ point.x <= node.nodeObj.x + node.boundingBox.width &&
+ point.y >= node.nodeObj.y &&
+ point.y <= node.nodeObj.y + node.boundingBox.height
+ ) {
+ isInside = true;
+ }
+ }
+ return isInside;
+}
+
+// Ranks edgePaths (points) based on the number of intersections with nodes
+/**
+ *
+ * @param edgePaths
+ * @param nodes
+ */
+function rankEdgePaths(edgePaths, nodes) {
+ let rankedEdgePaths = [];
+ for (const edgePath of edgePaths) {
+ let rank = 10 + edgePath.points.length;
+ for (const point of edgePath.points) {
+ if (isPointInsideNode(point, nodes)) {
+ // remove edge path
+
+ }
+ }
+ rankedEdgePaths.push({ rank: rank, edgePath: edgePath });
+ }
+
+ //sort on the basis of rank, highest rank first
+ rankedEdgePaths.sort((a, b) => (a.rank < b.rank ? 1 : -1));
+ return rankedEdgePaths;
+}
+
+
+/**
+ * Function to find if edge path is intersecting with any other edge path
+ * @param edgePath
+ * @param renderedEdgePaths
+ * @returns {boolean}
+ */
+function isEdgePathIntersecting(edgePath, renderedEdgePaths) {
+ let isIntersecting = false;
+ for (const renderedEdgePath of renderedEdgePaths) {
+ // check if line drawn from start point of edge path to start point of rendered edge path is intersecting with any other edge path
+
+ if (
+ common.isLineIntersecting(
+ edgePath.points[0],
+ renderedEdgePath.points[0],
+ edgePath.points[1],
+ renderedEdgePath.points[1]
+ )
+ ) {
+ isIntersecting = true;
+ }
+ }
+ return isIntersecting;
+}
+
+
+
+export default {
+ setConf,
+ addVertices,
+ addEdges,
+ getClasses,
+ draw,
+};
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/swimlane/swimlane-layout.js b/packages/mermaid/src/rendering-util/layout-algorithms/swimlane/swimlane-layout.js
new file mode 100644
index 000000000..074d9e6da
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/swimlane/swimlane-layout.js
@@ -0,0 +1,220 @@
+import { log } from '../../../logger.js';
+import flowDb from '../flowDb.js';
+
+export const getSubgraphLookupTable = function (diagObj) {
+ const subGraphs = diagObj.db.getSubGraphs();
+ const subgraphDb = {};
+ log.info('Subgraphs - ', subGraphs);
+ for (let i = subGraphs.length - 1; i >= 0; i--) {
+ const subG = subGraphs[i];
+ log.info('Subgraph - ', subG);
+ for (let j = 0; j < subG.nodes.length; j++) {
+ log.info('Setting up subgraphs', subG.nodes[j], subG.id);
+ subgraphDb[flowDb.lookUpId(subG.nodes[j])] = subG.id;
+ }
+ }
+ return subgraphDb;
+};
+
+/**
+ *
+ * @param graph
+ * @param subgraphLookupTable
+ */
+export function assignRanks(graph, subgraphLookupTable) {
+ let visited = new Set();
+ const lock = new Map();
+ const ranks = new Map();
+ let cnt = 0;
+ let changesDetected = true;
+
+ /**
+ *
+ * @param nodeId
+ * @param currentRank
+ */
+ function dfs(nodeId, currentRank) {
+ if (visited.has(nodeId)) {
+ return;
+ }
+
+ visited.add(nodeId);
+ const existingRank = ranks.get(nodeId) || 0;
+
+ // console.log('APA444 DFS Base case for', nodeId, 'to', Math.max(existingRank, currentRank));
+ if (lock.get(nodeId) !== 1) {
+ ranks.set(nodeId, Math.max(existingRank, currentRank));
+ } else {
+ console.log(
+ 'APA444 ',
+ nodeId,
+ 'was locked to ',
+ existingRank,
+ 'so not changing it',
+ ranks.get(nodeId)
+ );
+ }
+
+ const currentRankAdjusted = ranks.get(nodeId) || currentRank;
+ graph.successors(nodeId).forEach((targetId) => {
+ if (subgraphLookupTable[targetId] !== subgraphLookupTable[nodeId]) {
+ dfs(targetId, currentRankAdjusted);
+ } else {
+ // In same line, easy increase
+ dfs(targetId, currentRankAdjusted + 1);
+ }
+ });
+ }
+
+ /**
+ *
+ */
+ function adjustSuccessors() {
+ console.log('APA444 Adjusting successors');
+ graph.nodes().forEach((nodeId) => {
+ console.log('APA444 Going through nodes', nodeId);
+ // if (graph.predecessors(nodeId).length === 0) {
+ console.log('APA444 has no predecessors', nodeId);
+ graph.successors(nodeId).forEach((successorNodeId) => {
+ console.log('APA444 has checking successor', successorNodeId);
+ if (subgraphLookupTable[successorNodeId] !== subgraphLookupTable[nodeId]) {
+ const newRank = ranks.get(successorNodeId);
+ ranks.set(nodeId, newRank);
+ console.log('APA444 POST-process case for', nodeId, 'to', newRank);
+ lock.set(nodeId, 1);
+ changesDetected = true;
+ // setRankFromTopNodes();
+
+ // Adjust ranks of successors in the same subgraph
+ graph.successors(nodeId).forEach((sameSubGraphSuccessorNodeId) => {
+ if (subgraphLookupTable[sameSubGraphSuccessorNodeId] === subgraphLookupTable[nodeId]) {
+ console.log(
+ 'APA444 Adjusting rank of',
+ sameSubGraphSuccessorNodeId,
+ 'to',
+ newRank + 1
+ );
+ ranks.set(sameSubGraphSuccessorNodeId, newRank + 1);
+ lock.set(sameSubGraphSuccessorNodeId, 1);
+ changesDetected = true;
+ // dfs(sameSubGraphSuccessorNodeId, newRank + 1);
+ // setRankFromTopNodes();
+ }
+ });
+ } else {
+ console.log('APA444 Node', nodeId, ' and ', successorNodeId, ' is in the same lane');
+ }
+ });
+ // }
+ });
+ }
+
+ /**
+ *
+ */
+ function setRankFromTopNodes() {
+ visited = new Set();
+ graph.nodes().forEach((nodeId) => {
+ if (graph.predecessors(nodeId).length === 0) {
+ dfs(nodeId, 0);
+ }
+ });
+ adjustSuccessors();
+ }
+
+ while (changesDetected && cnt < 10) {
+ setRankFromTopNodes();
+ cnt++;
+ }
+ // Post-process the ranks
+
+ return ranks;
+}
+
+/**
+ *
+ * @param graph
+ * @param subgraphLĂ–ookupTable
+ * @param ranks
+ * @param subgraphLookupTable
+ */
+export function assignAffinities(graph, ranks, subgraphLookupTable) {
+ const affinities = new Map();
+ const swimlaneRankAffinities = new Map();
+ const swimlaneMaxAffinity = new Map();
+
+ graph.nodes().forEach((nodeId) => {
+ const swimlane = subgraphLookupTable[nodeId];
+ const rank = ranks.get(nodeId);
+ const key = swimlane + ':' + rank;
+ let currentAffinity = swimlaneRankAffinities.get(key);
+ if (currentAffinity === undefined) {
+ currentAffinity = -1;
+ }
+ const newAffinity = currentAffinity + 1;
+ swimlaneRankAffinities.set(key, newAffinity);
+ affinities.set(nodeId, newAffinity);
+ let currentMaxAffinity = swimlaneMaxAffinity.get(swimlane);
+ if (currentMaxAffinity === undefined) {
+ swimlaneMaxAffinity.set(swimlane, 0);
+ currentMaxAffinity = 0;
+ }
+ if (newAffinity > currentMaxAffinity) {
+ swimlaneMaxAffinity.set(swimlane, newAffinity);
+ }
+ });
+
+ // console.log('APA444 affinities', swimlaneRankAffinities);
+
+ return { affinities, swimlaneMaxAffinity };
+ //return affinities;
+}
+
+/**
+ *
+ * @param graph
+ * @param diagObj
+ */
+export function swimlaneLayout(graph, diagObj) {
+ const subgraphLookupTable = getSubgraphLookupTable(diagObj);
+ const ranks = assignRanks(graph, subgraphLookupTable);
+
+ const { affinities, swimlaneMaxAffinity } = assignAffinities(graph, ranks, subgraphLookupTable);
+ // const affinities = assignAffinities(graph, ranks, subgraphLookupTable);
+
+ const subGraphs = diagObj.db.getSubGraphs();
+ const lanes = [];
+ const laneDb = {};
+ let xPos = 0;
+ for (const subG of subGraphs) {
+ const maxAffinity = swimlaneMaxAffinity.get(subG.id);
+ const lane = {
+ title: subG.title,
+ x: xPos,
+ width: 200 + maxAffinity * 150,
+ };
+ xPos += lane.width;
+ lanes.push(lane);
+ laneDb[subG.id] = lane;
+ }
+
+ const rankWidth = [];
+ // Basic layout, calculate the node positions based on rank
+ graph.nodes().forEach((nodeId) => {
+ const rank = ranks.get(nodeId);
+
+ if (!rankWidth[rank]) {
+ const laneId = subgraphLookupTable[nodeId];
+ const lane = laneDb[laneId];
+ const n = graph.node(nodeId);
+ console.log('Node', nodeId, n);
+ const affinity = affinities.get(nodeId);
+
+ console.log('APA444', nodeId, 'rank', rank, 'affinity', affinity);
+ graph.setNode(nodeId, { y: rank * 200 + 50, x: lane.x + 150 * affinity + 100 });
+ // lane.width = Math.max(lane.width, lane.x + 150*affinity + lane.width / 4);
+ }
+ });
+
+ return { graph, lanes };
+}
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/swimlane/swimlane-layout.spec.ts b/packages/mermaid/src/rendering-util/layout-algorithms/swimlane/swimlane-layout.spec.ts
new file mode 100644
index 000000000..96420ee11
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/swimlane/swimlane-layout.spec.ts
@@ -0,0 +1,129 @@
+import flowDb from '../flowDb.js';
+import { cleanupComments } from '../../../diagram-api/comments.js';
+import setupGraph from './setup-graph.js';
+import { swimlaneLayout, assignRanks, getSubgraphLookupTable } from './swimlane-layout.js';
+import { getDiagramFromText } from '../../../Diagram.js';
+import { addDiagrams } from '../../../diagram-api/diagram-orchestration.ts';
+import jsdom from 'jsdom';
+
+const { JSDOM } = jsdom;
+
+addDiagrams();
+describe('When doing a assigning ranks specific for swim lanes ', () => {
+ let root;
+ let doc;
+ beforeEach(function () {
+ const dom = new JSDOM(`My First JSDOM!
`);
+ root = select(dom.window.document.getElementById('swimmer'));
+ root.html = () => {
+ ' return hello
';
+ };
+
+ doc = dom.window.document;
+ });
+ describe('Layout: ', () => {
+ // it('should rank the nodes:', async () => {
+ // const diagram = await getDiagramFromText(`swimlane LR
+ // subgraph "\`one\`"
+ // start --> cat --> rat
+ // end`);
+ // const g = setupGraph(diagram, 'swimmer', root, doc);
+ // const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ // const ranks = assignRanks(g, subgraphLookupTable);
+ // expect(ranks.get('start')).toEqual(0);
+ // expect(ranks.get('cat')).toEqual(1);
+ // expect(ranks.get('rat')).toEqual(2);
+ // });
+
+ it('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat
+ end
+ subgraph "\`two\`"
+ monkey --> dog --> done
+ end
+ cat --> monkey`);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const ranks = assignRanks(g, subgraphLookupTable);
+ expect(ranks.get('start')).toEqual(0);
+ expect(ranks.get('cat')).toEqual(1);
+ expect(ranks.get('rat')).toEqual(2);
+ expect(ranks.get('monkey')).toEqual(1);
+ expect(ranks.get('dog')).toEqual(2);
+ expect(ranks.get('done')).toEqual(3);
+ });
+ });
+ describe('Layout: ', () => {
+ it('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat
+ end`);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const { graph, lanes } = swimlaneLayout(g, diagram);
+ expect(lanes.length).toBe(1);
+ const start = graph.node('start');
+ const cat = graph.node('cat');
+ const rat = graph.node('rat');
+ expect(start.y).toBe(50);
+ expect(cat.y).toBe(250);
+ expect(rat.y).toBe(450);
+ expect(rat.x).toBe(100);
+ });
+
+ it('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat
+ end
+ subgraph "\`two\`"
+ monkey --> dog --> done
+ end
+ cat --> monkey`);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const { graph, lanes } = swimlaneLayout(g, diagram);
+ expect(lanes.length).toBe(2);
+ const start = graph.node('start');
+ const cat = graph.node('cat');
+ const rat = graph.node('rat');
+ const monkey = graph.node('monkey');
+ const dog = graph.node('dog');
+ const done = graph.node('done');
+
+ expect(start.y).toBe(50);
+ expect(cat.y).toBe(250);
+ expect(rat.y).toBe(450);
+ expect(rat.x).toBe(100);
+ expect(monkey.y).toBe(250);
+ expect(dog.y).toBe(450);
+ expect(done.y).toBe(650);
+ expect(monkey.x).toBe(300);
+ });
+ it.only('should rank the nodes:', async () => {
+ const diagram = await getDiagramFromText(`swimlane LR
+ subgraph "\`one\`"
+ start --> cat --> rat & hat
+ end
+ `);
+ const g = setupGraph(diagram, 'swimmer', root, doc);
+ const subgraphLookupTable = getSubgraphLookupTable(diagram);
+ const { graph, lanes } = swimlaneLayout(g, diagram);
+ expect(lanes.length).toBe(1);
+ const start = graph.node('start');
+ const cat = graph.node('cat');
+ const rat = graph.node('rat');
+ const hat = graph.node('rat');
+
+ expect(start.y).toBe(50);
+ expect(cat.y).toBe(250);
+ expect(rat.y).toBe(450);
+ expect(rat.x).toBe(300);
+ expect(hat.y).toBe(450);
+ expect(hat.x).toBe(100);
+ });
+ });
+});