From 34dbb6ba5e643f1cfbffad5f4bcf951379577838 Mon Sep 17 00:00:00 2001 From: ashishj Date: Wed, 3 May 2023 18:15:28 +0200 Subject: [PATCH 1/2] #2028 Initial example --- cypress/platform/knsv2.html | 322 +----- .../src/diagram-api/diagram-orchestration.ts | 2 + .../src/diagrams/flowchart/parser/flow.jison | 1 + .../flowchart/swimlane/detector.spec.ts | 55 + .../diagrams/flowchart/swimlane/detector.ts | 29 + .../flowchart/swimlane/render-utils.spec.ts | 40 + .../flowchart/swimlane/render-utils.ts | 25 + .../src/diagrams/flowchart/swimlane/styles.ts | 143 +++ .../flowchart/swimlane/swimlane-definition.ts | 13 + .../flowchart/swimlane/swimlaneRenderer.js | 968 ++++++++++++++++++ 10 files changed, 1278 insertions(+), 320 deletions(-) create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/detector.spec.ts create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/detector.ts create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/render-utils.spec.ts create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/render-utils.ts create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/styles.ts create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/swimlane-definition.ts create mode 100644 packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 1b1ccd685..188e1188b 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -57,333 +57,15 @@ +
-stateDiagram-v2
-    [*] --> Still
-    Still --> [*]
-    Still --> Moving
-    Moving --> Still
-    Moving --> Crash
-    Crash --> [*]    
-
-flowchart RL
+swimlane RL
     subgraph "`one`"
       a1 -- l1 --> a2
       a1 -- l2 --> a2
     end
     
-
-flowchart RL
-    subgraph "`one`"
-      a1 -- l1 --> a2
-      a1 -- l2 --> a2
-    end
-    
-
-flowchart
-id["`A root with a long text that wraps to keep the node size in check. A root with a long text that wraps to keep the node size in check`"]
-
-flowchart LR
-    A[A text that needs to be wrapped wraps to another line]
-    B[A text that needs to be
wrapped wraps to another line] - C["`A text that needs to be wrapped to another line`"]
-
-flowchart LR
-    C["`A text
-        that needs
-        to be wrapped
-        in another
-        way`"]
-  
-
-      classDiagram-v2
-        note "I love this diagram!\nDo you love it?"
-    
-
-    stateDiagram-v2
-    State1: The state with a note with minus - and plus + in it
-    note left of State1
-      Important information! You can write
-      notes with . and  in them.
-    end note    
-
-mindmap
-root
-  Child3(A node with an icon and with a long text that wraps to keep the node size in check)
-
-
-      %%{init: {"theme": "forest"} }%%
-mindmap
-    id1[**Start2**
end] - id2[**Start2**
end] - %% Another comment - id3[**Start2**
end] %% Comment - id4[**Start2**
end
the very end] -
-
-mindmap
-    id1["`**Start2**
-    second line 😎 with long text that is wrapping to the next line`"]
-      id2["`Child **with bold** text`"]
-      id3["`Children of which some
-      is using *italic type of* text`"]
-      id4[Child]
-      id5["`Child
-      Row
-      and another
-      `"]
-    
-
-mindmap
-    id1("`**Root**`"]
-      id2["`A formatted text... with **bold** and *italics*`"]
-      id3[Regular labels works as usual]
-      id4["`Emojis and unicode works too: 🤓
-      शान्तिः سلام  和平 `"]
 
-    
-
-%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
-flowchart TB
-  %% I could not figure out how to use double quotes in labels in Mermaid
-  subgraph ibm[IBM Espresso CPU]
-    core0[IBM PowerPC Broadway Core 0]
-    core1[IBM PowerPC Broadway Core 1]
-    core2[IBM PowerPC Broadway Core 2]
-
-    rom[16 KB ROM]
-
-    core0 --- core2
-
-    rom --> core2
-  end
-
-  subgraph amd["`**AMD** Latte GPU`"]
-    mem[Memory & I/O Bridge]
-    dram[DRAM Controller]
-    edram[32 MB EDRAM MEM1]
-    rom[512 B SEEPROM]
-
-    sata[SATA IF]
-    exi[EXI]
-
-    subgraph gx[GX]
-      sram[3 MB 1T-SRAM]
-    end
-
-    radeon[AMD Radeon R7xx GX2]
-
-    mem --- gx
-    mem --- radeon
-
-    rom --- mem
-
-    mem --- sata
-    mem --- exi
-
-    dram --- sata
-    dram --- exi
-  end
-
-  ddr3[2 GB DDR3 RAM MEM2]
-
-  mem --- ddr3
-  dram --- ddr3
-  edram --- ddr3
-
-  core1 --- mem
-
-  exi --- rtc
-  rtc{{rtc}}
-
-
-%%{init: {"flowchart": {"defaultRenderer": "elk", "htmlLabels": false}} }%%
-flowchart TB
-  %% I could not figure out how to use double quotes in labels in Mermaid
-  subgraph ibm[IBM Espresso CPU]
-    core0[IBM PowerPC Broadway Core 0]
-    core1[IBM PowerPC Broadway Core 1]
-    core2[IBM PowerPC Broadway Core 2]
-
-    rom[16 KB ROM]
-
-    core0 --- core2
-
-    rom --> core2
-  end
-
-  subgraph amd["`**AMD** Latte GPU`"]
-    mem[Memory & I/O Bridge]
-    dram[DRAM Controller]
-    edram[32 MB EDRAM MEM1]
-    rom[512 B SEEPROM]
-
-    sata[SATA IF]
-    exi[EXI]
-
-    subgraph gx[GX]
-      sram[3 MB 1T-SRAM]
-    end
-
-    radeon[AMD Radeon R7xx GX2]
-
-    mem --- gx
-    mem --- radeon
-
-    rom --- mem
-
-    mem --- sata
-    mem --- exi
-
-    dram --- sata
-    dram --- exi
-  end
-
-  ddr3[2 GB DDR3 RAM MEM2]
-
-  mem --- ddr3
-  dram --- ddr3
-  edram --- ddr3
-
-  core1 --- mem
-
-  exi --- rtc
-  rtc{{rtc}}
-
- -
-
-flowchart TB
-  %% I could not figure out how to use double quotes in labels in Mermaid
-  subgraph ibm[IBM Espresso CPU]
-    core0[IBM PowerPC Broadway Core 0]
-    core1[IBM PowerPC Broadway Core 1]
-    core2[IBM PowerPC Broadway Core 2]
-
-    rom[16 KB ROM]
-
-    core0 --- core2
-
-    rom --> core2
-  end
-
-  subgraph amd[AMD Latte GPU]
-    mem[Memory & I/O Bridge]
-    dram[DRAM Controller]
-    edram[32 MB EDRAM MEM1]
-    rom[512 B SEEPROM]
-
-    sata[SATA IF]
-    exi[EXI]
-
-    subgraph gx[GX]
-      sram[3 MB 1T-SRAM]
-    end
-
-    radeon[AMD Radeon R7xx GX2]
-
-    mem --- gx
-    mem --- radeon
-
-    rom --- mem
-
-    mem --- sata
-    mem --- exi
-
-    dram --- sata
-    dram --- exi
-  end
-
-  ddr3[2 GB DDR3 RAM MEM2]
-
-  mem --- ddr3
-  dram --- ddr3
-  edram --- ddr3
-
-  core1 --- mem
-
-  exi --- rtc
-  rtc{{rtc}}
-
-
-   -
-      flowchart LR
-  B1 --be be--x B2
-  B1 --bo bo--o B3
-  subgraph Ugge
-      B2
-      B3
-      subgraph inner
-          B4
-          B5
-      end
-      subgraph inner2
-        subgraph deeper
-          C4
-          C5
-        end
-        C6
-      end
-
-      B4 --> C4
-
-      B3 -- X --> B4
-      B2 --> inner
-
-      C4 --> C5
-  end
-
-  subgraph outer
-      B6
-  end
-  B6 --> B5
-  
-
-sequenceDiagram
-    Customer->>+Stripe: Makes a payment request
-    Stripe->>+Bank: Forwards the payment request to the bank
-    Bank->>+Customer: Asks for authorization
-    Customer->>+Bank: Provides authorization
-    Bank->>+Stripe: Sends a response with payment details
-    Stripe->>+Merchant: Sends a notification of payment receipt
-    Merchant->>+Stripe: Confirms the payment
-    Stripe->>+Customer: Sends a confirmation of payment
-    Customer->>+Merchant: Receives goods or services
-        
-
-mindmap
-  root((mindmap))
-    Origins
-      Long history
-      ::icon(fa fa-book)
-      Popularisation
-        British popular psychology author Tony Buzan
-    Research
-      On effectiveness
and features - On Automatic creation - Uses - Creative techniques - Strategic planning - Argument mapping - Tools - Pen and paper - Mermaid -
-
-
-  example-diagram
-    
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 0d4e6159d..81840d318 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -14,6 +14,7 @@ import state from '../diagrams/state/stateDetector.js'; import stateV2 from '../diagrams/state/stateDetector-V2.js'; import journey from '../diagrams/user-journey/journeyDetector.js'; import errorDiagram from '../diagrams/error/errorDiagram.js'; +import swimlane from '../diagrams/flowchart/swimlane/detector.js'; import flowchartElk from '../diagrams/flowchart/elk/detector.js'; import timeline from '../diagrams/timeline/detector.js'; import mindmap from '../diagrams/mindmap/detector.js'; @@ -69,6 +70,7 @@ export const addDiagrams = () => { pie, requirement, sequence, + swimlane, flowchartElk, flowchartV2, flowchart, diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison index 51427118f..b5dac9178 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison @@ -84,6 +84,7 @@ that id. [\s\n] this.popState(); [^\s\n]* return 'CLICK'; +"swimlane" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} "flowchart-elk" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} "graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} "flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} diff --git a/packages/mermaid/src/diagrams/flowchart/swimlane/detector.spec.ts b/packages/mermaid/src/diagrams/flowchart/swimlane/detector.spec.ts new file mode 100644 index 000000000..fb5541da7 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/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/flowchart/swimlane/detector.ts b/packages/mermaid/src/diagrams/flowchart/swimlane/detector.ts new file mode 100644 index 000000000..e72101f88 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/detector.ts @@ -0,0 +1,29 @@ +import type { MermaidConfig } from '../../../config.type.js'; +import type { ExternalDiagramDefinition, DiagramDetector } from '../../../diagram-api/types.js'; + +const id = 'swimlane'; + +const detector: DiagramDetector = (txt: string, config?: MermaidConfig): boolean => { + + if ( + + txt.match(/^\s*swimlane/)) { + console.log("swimlane detector true"); + return true; + } + console.log("swimlane detector false"); + return false; +}; + +const loader = 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/flowchart/swimlane/render-utils.spec.ts b/packages/mermaid/src/diagrams/flowchart/swimlane/render-utils.spec.ts new file mode 100644 index 000000000..d048b07a3 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/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/flowchart/swimlane/render-utils.ts b/packages/mermaid/src/diagrams/flowchart/swimlane/render-utils.ts new file mode 100644 index 000000000..ebdc01cf7 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/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/flowchart/swimlane/styles.ts b/packages/mermaid/src/diagrams/flowchart/swimlane/styles.ts new file mode 100644 index 000000000..60659df45 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/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/flowchart/swimlane/swimlane-definition.ts b/packages/mermaid/src/diagrams/flowchart/swimlane/swimlane-definition.ts new file mode 100644 index 000000000..6e35c1253 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/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/flowchart/swimlane/swimlaneRenderer.js b/packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js new file mode 100644 index 000000000..6a90c46f5 --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js @@ -0,0 +1,968 @@ +import { select, line, curveLinear } from 'd3'; +import { insertNode } from '../../../dagre-wrapper/nodes.js'; +import insertMarkers from '../../../dagre-wrapper/markers.js'; +import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js'; +import { findCommonAncestor } from './render-utils.js'; +import { labelHelper } from '../../../dagre-wrapper/shapes/util.js'; +import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; +import { getConfig } from '../../../config.js'; +import { log } from '../../../logger.js'; +import { setupGraphViewbox } from '../../../setupGraphViewbox.js'; +import common, { evaluate } from '../../common/common.js'; +import { interpolateToCurve, getStylesFromArray } from '../../../utils.js'; +import ELK from 'elkjs/lib/elk.bundled.js'; +const elk = new ELK(); + +let portPos = {}; + +const conf = {}; +export const setConf = function (cnf) { + const keys = Object.keys(cnf); + for (const key of keys) { + conf[key] = cnf[key]; + } +}; + +let nodeDb = {}; + +// /** +// * 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 = async function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) { + const svg = root.select(`[id="${svgId}"]`); + const nodes = svg.insert('g').attr('class', 'nodes'); + const keys = Object.keys(vert); + + // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition + await Promise.all( + keys.map(async 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; + const labelData = { width: 0, height: 0 }; + + const ports = [ + { + id: vertex.id + '-west', + layoutOptions: { + 'port.side': 'WEST', + }, + }, + { + id: vertex.id + '-east', + layoutOptions: { + 'port.side': 'EAST', + }, + }, + { + id: vertex.id + '-south', + layoutOptions: { + 'port.side': 'SOUTH', + }, + }, + { + id: vertex.id + '-north', + layoutOptions: { + 'port.side': 'NORTH', + }, + }, + ]; + + let radious = 0; + let _shape = ''; + let layoutOptions = {}; + // Set the shape based parameters + switch (vertex.type) { + case 'round': + radious = 5; + _shape = 'rect'; + break; + case 'square': + _shape = 'rect'; + break; + case 'diamond': + _shape = 'question'; + layoutOptions = { + portConstraints: 'FIXED_SIDE', + }; + 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 + const node = { + 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, + }; + let boundingBox; + let nodeEl; + + // Add the element to the DOM + if (node.type !== 'group') { + nodeEl = await insertNode(nodes, node, vertex.dir); + boundingBox = nodeEl.node().getBBox(); + } 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; + // const bbox = vertexNode.getBBox(); + const { shapeSvg, bbox } = await labelHelper(nodes, node, undefined, true); + labelData.width = bbox.width; + labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; + labelData.height = bbox.height; + labelData.labelNode = shapeSvg.node(); + node.labelData = labelData; + } + // const { shapeSvg, bbox } = await labelHelper(svg, node, undefined, true); + + const data = { + id: vertex.id, + ports: vertex.type === 'diamond' ? ports : [], + // labelStyle: styles.labelStyle, + // shape: _shape, + layoutOptions, + labelText: vertexText, + labelData, + // labels: [{ text: vertexText }], + // rx: radius, + // ry: radius, + // class: classStr, + // style: styles.style, + // link: vertex.link, + // linkTarget: vertex.linkTarget, + // tooltip: diagObj.db.getTooltip(vertex.id) || '', + domId: diagObj.db.lookUpDomId(vertex.id), + // haveCallback: vertex.haveCallback, + width: boundingBox?.width, + height: boundingBox?.height, + // dir: vertex.dir, + type: vertex.type, + // props: vertex.props, + // padding: getConfig().flowchart.padding, + // boundingBox, + el: nodeEl, + parent: parentLookupDb.parentById[vertex.id], + }; + // if (!Object.keys(parentLookupDb.childrenById).includes(vertex.id)) { + // graph.children.push({ + // ...data, + // }); + // } + nodeDb[node.id] = data; + // log.trace('setNode', { + // labelStyle: styles.labelStyle, + // shape: _shape, + // labelText: vertexText, + // rx: radius, + // ry: radius, + // 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, + // parent: parentLookupDb.parentById[vertex.id], + // }); + }) + ); + return graph; +}; + +const getNextPosition = (position, edgeDirection, graphDirection) => { + const portPos = { + TB: { + in: { + north: 'north', + }, + out: { + south: 'west', + west: 'east', + east: 'south', + }, + }, + LR: { + in: { + west: 'west', + }, + out: { + east: 'south', + south: 'north', + north: 'east', + }, + }, + RL: { + in: { + east: 'east', + }, + out: { + west: 'north', + north: 'south', + south: 'west', + }, + }, + BT: { + in: { + south: 'south', + }, + out: { + north: 'east', + east: 'west', + west: 'north', + }, + }, + }; + portPos.TD = portPos.TB; + log.info('abc88', graphDirection, edgeDirection, position); + return portPos[graphDirection][edgeDirection][position]; + // return 'south'; +}; + +const getNextPort = (node, edgeDirection, graphDirection) => { + log.info('getNextPort abc88', { node, edgeDirection, graphDirection }); + if (!portPos[node]) { + switch (graphDirection) { + case 'TB': + case 'TD': + portPos[node] = { + inPosition: 'north', + outPosition: 'south', + }; + break; + case 'BT': + portPos[node] = { + inPosition: 'south', + outPosition: 'north', + }; + break; + case 'RL': + portPos[node] = { + inPosition: 'east', + outPosition: 'west', + }; + break; + case 'LR': + portPos[node] = { + inPosition: 'west', + outPosition: 'east', + }; + break; + } + } + const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition; + + if (edgeDirection === 'in') { + portPos[node].inPosition = getNextPosition( + portPos[node].inPosition, + edgeDirection, + graphDirection + ); + } else { + portPos[node].outPosition = getNextPosition( + portPos[node].outPosition, + edgeDirection, + graphDirection + ); + } + return result; +}; + +const getEdgeStartEndPoint = (edge, dir) => { + let source = edge.start; + let target = edge.end; + + // Save the original source and target + const sourceId = source; + const targetId = target; + + const startNode = nodeDb[source]; + const endNode = nodeDb[target]; + + if (!startNode || !endNode) { + return { source, target }; + } + + if (startNode.type === 'diamond') { + source = `${source}-${getNextPort(source, 'out', dir)}`; + } + + if (endNode.type === 'diamond') { + target = `${target}-${getNextPort(target, 'in', dir)}`; + } + + // Add the edge to the graph + return { source, target, sourceId, targetId }; +}; + +/** + * 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 cy + * @param diagObj + * @param graph + * @param svg + */ +export const addEdges = function (edges, diagObj, graph, svg) { + log.info('abc78 edges = ', edges); + const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); + let linkIdCnt = {}; + let dir = diagObj.db.getDirection(); + let defaultStyle; + let defaultLabelStyle; + + if (edges.defaultStyle !== undefined) { + const defaultStyles = getStylesFromArray(edges.defaultStyle); + defaultStyle = defaultStyles.style; + defaultLabelStyle = defaultStyles.labelStyle; + } + + edges.forEach(function (edge) { + // Identify Link + const 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]); + const linkNameStart = 'LS-' + edge.start; + const 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; + } + 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; + + const labelEl = insertEdgeLabel(labelsEl, edgeData); + + // calculate start and end points of the edge, note that the source and target + // can be modified for shapes that have ports + const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); + log.debug('abc78 source and target', source, target); + // Add the edge to the graph + graph.edges.push({ + id: 'e' + edge.start + edge.end, + sources: [source], + targets: [target], + sourceId, + targetId, + labelEl: labelEl, + labels: [ + { + width: edgeData.width, + height: edgeData.height, + orgWidth: edgeData.width, + orgHeight: edgeData.height, + text: edgeData.label, + layoutOptions: { + 'edgeLabels.inline': 'true', + 'edgeLabels.placement': 'CENTER', + }, + }, + ], + edgeData, + }); + }); + return graph; +}; + +// TODO: break out and share with dagre wrapper. The current code in dagre wrapper also adds +// adds the line to the graph, but we don't need that here. This is why we cant use the dagre +// wrapper directly for this +/** + * Add the markers to the edge depending on the type of arrow is + * @param svgPath + * @param edgeData + * @param diagramType + * @param arrowMarkerAbsolute + */ +const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute) { + let url = ''; + // Check configuration for absolute path + if (arrowMarkerAbsolute) { + url = + window.location.protocol + + '//' + + window.location.host + + window.location.pathname + + window.location.search; + url = url.replace(/\(/g, '\\('); + url = url.replace(/\)/g, '\\)'); + } + + // look in edge data and decide which marker to use + switch (edgeData.arrowTypeStart) { + case 'arrow_cross': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')'); + break; + case 'arrow_point': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')'); + break; + case 'arrow_barb': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')'); + break; + case 'arrow_circle': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')'); + break; + case 'aggregation': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')'); + break; + case 'extension': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')'); + break; + case 'composition': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')'); + break; + case 'dependency': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')'); + break; + case 'lollipop': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')'); + break; + default: + } + switch (edgeData.arrowTypeEnd) { + case 'arrow_cross': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')'); + break; + case 'arrow_point': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')'); + break; + case 'arrow_barb': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')'); + break; + case 'arrow_circle': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')'); + break; + case 'aggregation': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')'); + break; + case 'extension': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')'); + break; + case 'composition': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')'); + break; + case 'dependency': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')'); + break; + case 'lollipop': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')'); + break; + default: + } +}; + +/** + * 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('ver-2'); + try { + // Parse the graph definition + diagObj.parse(text); + return diagObj.db.getClasses(); + } catch (e) { + return {}; + } +}; + +const addSubGraphs = function (db) { + const parentLookupDb = { parentById: {}, childrenById: {} }; + const subgraphs = db.getSubGraphs(); + log.info('Subgraphs - ', subgraphs); + subgraphs.forEach(function (subgraph) { + subgraph.nodes.forEach(function (node) { + parentLookupDb.parentById[node] = subgraph.id; + if (parentLookupDb.childrenById[subgraph.id] === undefined) { + parentLookupDb.childrenById[subgraph.id] = []; + } + parentLookupDb.childrenById[subgraph.id].push(node); + }); + }); + + subgraphs.forEach(function (subgraph) { + const data = { id: subgraph.id }; + if (parentLookupDb.parentById[subgraph.id] !== undefined) { + data.parent = parentLookupDb.parentById[subgraph.id]; + } + }); + return parentLookupDb; +}; + +const calcOffset = function (src, dest, parentLookupDb) { + const ancestor = findCommonAncestor(src, dest, parentLookupDb); + if (ancestor === undefined || ancestor === 'root') { + return { x: 0, y: 0 }; + } + + const ancestorOffset = nodeDb[ancestor].offset; + return { x: ancestorOffset.posX, y: ancestorOffset.posY }; +}; + +const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) { + const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb); + + const src = edge.sections[0].startPoint; + const dest = edge.sections[0].endPoint; + const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; + + const segPoints = segments.map((segment) => [segment.x + offset.x, segment.y + offset.y]); + const points = [ + [src.x + offset.x, src.y + offset.y], + ...segPoints, + [dest.x + offset.x, dest.y + offset.y], + ]; + + // const curve = line().curve(curveBasis); + const curve = line().curve(curveLinear); + const edgePath = edgesEl + .insert('path') + .attr('d', curve(points)) + .attr('class', 'path') + .attr('fill', 'none'); + const edgeG = edgesEl.insert('g').attr('class', 'edgeLabel'); + const edgeWithLabel = select(edgeG.node().appendChild(edge.labelEl)); + const box = edgeWithLabel.node().firstChild.getBoundingClientRect(); + edgeWithLabel.attr('width', box.width); + edgeWithLabel.attr('height', box.height); + + edgeG.attr( + 'transform', + `translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})` + ); + addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute); +}; + +/** + * Recursive function that iterates over an array of nodes and inserts the children of each node. + * It also recursively populates the inserts the children of the children and so on. + * @param {*} graph + * @param nodeArray + * @param parentLookupDb + */ +const insertChildren = (nodeArray, parentLookupDb) => { + nodeArray.forEach((node) => { + // Check if we have reached the end of the tree + if (!node.children) { + node.children = []; + } + // Check if the node has children + const childIds = parentLookupDb.childrenById[node.id]; + // If the node has children, add them to the node + if (childIds) { + childIds.forEach((childId) => { + node.children.push(nodeDb[childId]); + }); + } + // Recursive call + insertChildren(node.children, parentLookupDb); + }); +}; + +/** + * 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) { + // Add temporary render element + diagObj.db.clear(); + nodeDb = {}; + portPos = {}; + diagObj.db.setGen('gen-2'); + // Parse the graph definition + diagObj.parser.parse(text); + + const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy'); + let graph = { + id: 'root', + layoutOptions: { + 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', + 'org.eclipse.elk.padding': '[top=100, left=100, bottom=110, right=110]', + 'elk.layered.spacing.edgeNodeBetweenLayers': '30', + // 'elk.layered.mergeEdges': 'true', + 'elk.direction': 'DOWN', + // 'elk.ports.sameLayerEdges': true, + // 'nodePlacement.strategy': 'SIMPLE', + }, + children: [], + edges: [], + }; + log.info('Drawing flowchart using v3 renderer', elk); + + // Set the direction, + // Fetch the default direction, use TD if none was found + let dir = diagObj.db.getDirection(); + switch (dir) { + case 'BT': + graph.layoutOptions['elk.direction'] = 'UP'; + break; + case 'TB': + graph.layoutOptions['elk.direction'] = 'DOWN'; + break; + case 'LR': + graph.layoutOptions['elk.direction'] = 'RIGHT'; + break; + case 'RL': + graph.layoutOptions['elk.direction'] = 'LEFT'; + break; + } + const { securityLevel, flowchart: conf } = getConfig(); + + // Find the root dom node to ne used in rendering + // 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; + + const svg = root.select(`[id="${id}"]`); + + // Define the supported markers for the diagram + const markers = ['point', 'circle', 'cross']; + + // Add the marker definitions to the svg as marker tags + insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute); + + // Fetch the vertices/nodes and edges/links from the parsed graph definition + const vert = diagObj.db.getVertices(); + + // Setup nodes from the subgraphs with type group, these will be used + // as nodes with children in the subgraph + let subG; + const subGraphs = diagObj.db.getSubGraphs(); + log.info('Subgraphs - ', subGraphs); + for (let i = subGraphs.length - 1; i >= 0; i--) { + subG = subGraphs[i]; + diagObj.db.addVertex( + subG.id, + { text: subG.title, type: subG.labelType }, + 'group', + undefined, + subG.classes, + subG.dir + ); + } + + // debugger; + // Add an element in the svg to be used to hold the subgraphs container + // elements + const subGraphsEl = svg.insert('g').attr('class', 'subgraphs'); + + // Create the lookup db for the subgraphs and their children to used when creating + // the tree structured graph + const parentLookupDb = addSubGraphs(diagObj.db); + + // Add the nodes to the graph, this will entail creating the actual nodes + // in order to get the size of the node. You can't get the size of a node + // that is not in the dom so we need to add it to the dom, get the size + // we will position the nodes when we get the layout from elkjs + graph = await addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph); + + // Time for the edges, we start with adding an element in the node to hold the edges + const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); + // Fetch the edges form the parsed graph definition + const edges = diagObj.db.getEdges(); + + // Add the edges to the graph, this will entail creating the actual edges + graph = addEdges(edges, diagObj, graph, svg); + + // Iterate through all nodes and add the top level nodes to the graph + const nodes = Object.keys(nodeDb); + nodes.forEach((nodeId) => { + const node = nodeDb[nodeId]; + if (!node.parent) { + graph.children.push(node); + } + // Subgraph + if (parentLookupDb.childrenById[nodeId] !== undefined) { + node.labels = [ + { + text: node.labelText, + layoutOptions: { + 'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]', + }, + width: node.labelData.width, + height: node.labelData.height, + // width: 100, + // height: 100, + }, + ]; + delete node.x; + delete node.y; + delete node.width; + delete node.height; + } + }); + + insertChildren(graph.children, parentLookupDb); + log.info('after layout', JSON.stringify(graph, null, 2)); + const g = await elk.layout(graph); + drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0); + log.info('after layout', g); + g.edges?.map((edge) => { + insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb); + }); + setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth); + // Remove element after layout + renderEl.remove(); +}; + +const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => { + nodeArray.forEach(function (node) { + if (node) { + nodeDb[node.id].offset = { + posX: node.x + relX, + posY: node.y + relY, + x: relX, + y: relY, + depth, + width: node.width, + height: node.height, + }; + if (node.type === 'group') { + const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); + subgraphEl + .insert('rect') + .attr('class', 'subgraph subgraph-lvl-' + (depth % 5) + ' node') + .attr('x', node.x + relX) + .attr('y', node.y + relY) + .attr('width', node.width) + .attr('height', node.height); + const label = subgraphEl.insert('g').attr('class', 'label'); + const labelCentering = getConfig().flowchart.htmlLabels ? node.labelData.width / 2 : 0; + label.attr( + 'transform', + `translate(${node.labels[0].x + relX + node.x + labelCentering}, ${ + node.labels[0].y + relY + node.y + 3 + })` + ); + label.node().appendChild(node.labelData.labelNode); + + log.info('Id (UGH)= ', node.type, node.labels); + } else { + log.info('Id (UGH)= ', node.id); + node.el.attr( + 'transform', + `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` + ); + } + } + }); + nodeArray.forEach(function (node) { + if (node && node.type === 'group') { + drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, diagObj, depth + 1); + } + }); +}; + +export default { + getClasses, + draw, +}; From 9f77aa2e943efb00ada0e4e0203be25197fad1d8 Mon Sep 17 00:00:00 2001 From: ashishj Date: Wed, 3 May 2023 20:00:39 +0200 Subject: [PATCH 2/2] #2028 iter2 --- cypress/platform/knsv2.html | 17 +- packages/mermaid/src/dagre-wrapper/index.js | 1 + .../flowchart/swimlane/swimlaneRenderer.js | 1040 +++++------------ 3 files changed, 322 insertions(+), 736 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 188e1188b..7f9928fd0 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -59,14 +59,21 @@
-swimlane RL
+flowchart LR
     subgraph "`one`"
-      a1 -- l1 --> a2
-      a1 -- l2 --> a2
-    end
+      start -- l1 --> cat --> rat
+      end
+    subgraph "`two`"
+      monkey -- l2 --> dog --> done2
+      end
+    subgraph "`three`"
+      cow -- l3 --> done3
+      end
     
- + monkey + dog --> cat + monkey --> cow --> diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js index 590242b02..048f17985 100644 --- a/packages/mermaid/src/dagre-wrapper/index.js +++ b/packages/mermaid/src/dagre-wrapper/index.js @@ -148,6 +148,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => { return { elem, diff }; }; + export const render = async (elem, graph, markers, diagramtype, id) => { insertMarkers(elem, markers, diagramtype, id); clearNodes(); diff --git a/packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js b/packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js index 6a90c46f5..6d96c4730 100644 --- a/packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js +++ b/packages/mermaid/src/diagrams/flowchart/swimlane/swimlaneRenderer.js @@ -1,19 +1,16 @@ -import { select, line, curveLinear } from 'd3'; -import { insertNode } from '../../../dagre-wrapper/nodes.js'; -import insertMarkers from '../../../dagre-wrapper/markers.js'; -import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js'; -import { findCommonAncestor } from './render-utils.js'; -import { labelHelper } from '../../../dagre-wrapper/shapes/util.js'; -import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; -import { getConfig } from '../../../config.js'; -import { log } from '../../../logger.js'; -import { setupGraphViewbox } from '../../../setupGraphViewbox.js'; -import common, { evaluate } from '../../common/common.js'; -import { interpolateToCurve, getStylesFromArray } from '../../../utils.js'; -import ELK from 'elkjs/lib/elk.bundled.js'; -const elk = new ELK(); +import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; +import { select, curveLinear, selectAll } from 'd3'; -let portPos = {}; +import flowDb from './flowDb.js'; +import { getConfig } from '../../config.js'; +import utils from '../../utils.js'; + +import { render } from '../../dagre-wrapper/index.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'; +import { setupGraphViewbox } from '../../setupGraphViewbox.js'; const conf = {}; export const setConf = function (cnf) { @@ -23,369 +20,190 @@ export const setConf = function (cnf) { } }; -let nodeDb = {}; + /** + * + * @param graph + */ + function swimlaneLayout(graph) { + return graph; +} + /** + * + * @param element + * @param graph + */ + function swimlaneRender(element,graph) { + return graph; +} -// /** -// * 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 = async function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) { + +/** + * 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 nodes = svg.insert('g').attr('class', 'nodes'); const keys = Object.keys(vert); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - await Promise.all( - keys.map(async function (id) { - const vertex = vert[id]; + 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); + /** + * 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; + // 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; - const labelData = { width: 0, height: 0 }; - - const ports = [ - { - id: vertex.id + '-west', - layoutOptions: { - 'port.side': 'WEST', - }, - }, - { - id: vertex.id + '-east', - layoutOptions: { - 'port.side': 'EAST', - }, - }, - { - id: vertex.id + '-south', - layoutOptions: { - 'port.side': 'SOUTH', - }, - }, - { - id: vertex.id + '-north', - layoutOptions: { - 'port.side': 'NORTH', - }, - }, - ]; - - let radious = 0; - let _shape = ''; - let layoutOptions = {}; - // Set the shape based parameters - switch (vertex.type) { - case 'round': - radious = 5; - _shape = 'rect'; - break; - case 'square': - _shape = 'rect'; - break; - case 'diamond': - _shape = 'question'; - layoutOptions = { - portConstraints: 'FIXED_SIDE', - }; - 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 - const node = { - 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, - }; - let boundingBox; - let nodeEl; - - // Add the element to the DOM - if (node.type !== 'group') { - nodeEl = await insertNode(nodes, node, vertex.dir); - boundingBox = nodeEl.node().getBBox(); + // 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(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; - // const bbox = vertexNode.getBBox(); - const { shapeSvg, bbox } = await labelHelper(nodes, node, undefined, true); - labelData.width = bbox.width; - labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; - labelData.height = bbox.height; - labelData.labelNode = shapeSvg.node(); - node.labelData = labelData; + 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; } - // const { shapeSvg, bbox } = await labelHelper(svg, node, undefined, true); - - const data = { - id: vertex.id, - ports: vertex.type === 'diamond' ? ports : [], - // labelStyle: styles.labelStyle, - // shape: _shape, - layoutOptions, - labelText: vertexText, - labelData, - // labels: [{ text: vertexText }], - // rx: radius, - // ry: radius, - // class: classStr, - // style: styles.style, - // link: vertex.link, - // linkTarget: vertex.linkTarget, - // tooltip: diagObj.db.getTooltip(vertex.id) || '', - domId: diagObj.db.lookUpDomId(vertex.id), - // haveCallback: vertex.haveCallback, - width: boundingBox?.width, - height: boundingBox?.height, - // dir: vertex.dir, - type: vertex.type, - // props: vertex.props, - // padding: getConfig().flowchart.padding, - // boundingBox, - el: nodeEl, - parent: parentLookupDb.parentById[vertex.id], - }; - // if (!Object.keys(parentLookupDb.childrenById).includes(vertex.id)) { - // graph.children.push({ - // ...data, - // }); - // } - nodeDb[node.id] = data; - // log.trace('setNode', { - // labelStyle: styles.labelStyle, - // shape: _shape, - // labelText: vertexText, - // rx: radius, - // ry: radius, - // 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, - // parent: parentLookupDb.parentById[vertex.id], - // }); - }) - ); - return graph; -}; - -const getNextPosition = (position, edgeDirection, graphDirection) => { - const portPos = { - TB: { - in: { - north: 'north', - }, - out: { - south: 'west', - west: 'east', - east: 'south', - }, - }, - LR: { - in: { - west: 'west', - }, - out: { - east: 'south', - south: 'north', - north: 'east', - }, - }, - RL: { - in: { - east: 'east', - }, - out: { - west: 'north', - north: 'south', - south: 'west', - }, - }, - BT: { - in: { - south: 'south', - }, - out: { - north: 'east', - east: 'west', - west: 'north', - }, - }, - }; - portPos.TD = portPos.TB; - log.info('abc88', graphDirection, edgeDirection, position); - return portPos[graphDirection][edgeDirection][position]; - // return 'south'; -}; - -const getNextPort = (node, edgeDirection, graphDirection) => { - log.info('getNextPort abc88', { node, edgeDirection, graphDirection }); - if (!portPos[node]) { - switch (graphDirection) { - case 'TB': - case 'TD': - portPos[node] = { - inPosition: 'north', - outPosition: 'south', - }; - break; - case 'BT': - portPos[node] = { - inPosition: 'south', - outPosition: 'north', - }; - break; - case 'RL': - portPos[node] = { - inPosition: 'east', - outPosition: 'west', - }; - break; - case 'LR': - portPos[node] = { - inPosition: 'west', - outPosition: 'east', - }; - break; } - } - const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition; - if (edgeDirection === 'in') { - portPos[node].inPosition = getNextPosition( - portPos[node].inPosition, - edgeDirection, - graphDirection - ); - } else { - portPos[node].outPosition = getNextPosition( - portPos[node].outPosition, - edgeDirection, - graphDirection - ); - } - return result; -}; + 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, + }); -const getEdgeStartEndPoint = (edge, dir) => { - let source = edge.start; - let target = edge.end; - - // Save the original source and target - const sourceId = source; - const targetId = target; - - const startNode = nodeDb[source]; - const endNode = nodeDb[target]; - - if (!startNode || !endNode) { - return { source, target }; - } - - if (startNode.type === 'diamond') { - source = `${source}-${getNextPort(source, 'out', dir)}`; - } - - if (endNode.type === 'diamond') { - target = `${target}-${getNextPort(target, 'in', dir)}`; - } - - // Add the edge to the graph - return { source, target, sourceId, targetId }; + 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, + }); + }); }; /** @@ -393,16 +211,13 @@ const getEdgeStartEndPoint = (edge, dir) => { * * @param {object} edges The edges to add to the graph * @param {object} g The graph object - * @param cy * @param diagObj - * @param graph - * @param svg */ -export const addEdges = function (edges, diagObj, graph, svg) { +export const addEdges = function (edges, g, diagObj) { log.info('abc78 edges = ', edges); - const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); + let cnt = 0; let linkIdCnt = {}; - let dir = diagObj.db.getDirection(); + let defaultStyle; let defaultLabelStyle; @@ -413,8 +228,10 @@ export const addEdges = function (edges, diagObj, graph, svg) { } edges.forEach(function (edge) { + cnt++; + // Identify Link - const linkIdBase = 'L-' + edge.start + '-' + edge.end; + 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; @@ -425,8 +242,8 @@ export const addEdges = function (edges, diagObj, graph, svg) { } let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase]; log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); - const linkNameStart = 'LS-' + edge.start; - const linkNameEnd = 'LE-' + edge.end; + var linkNameStart = 'LS-' + edge.start; + var linkNameEnd = 'LE-' + edge.end; const edgeData = { style: '', labelStyle: '' }; edgeData.minlen = edge.length || 1; @@ -487,6 +304,11 @@ export const addEdges = function (edges, diagObj, graph, svg) { 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); @@ -526,124 +348,9 @@ export const addEdges = function (edges, diagObj, graph, svg) { edgeData.id = linkId; edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; - const labelEl = insertEdgeLabel(labelsEl, edgeData); - - // calculate start and end points of the edge, note that the source and target - // can be modified for shapes that have ports - const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); - log.debug('abc78 source and target', source, target); // Add the edge to the graph - graph.edges.push({ - id: 'e' + edge.start + edge.end, - sources: [source], - targets: [target], - sourceId, - targetId, - labelEl: labelEl, - labels: [ - { - width: edgeData.width, - height: edgeData.height, - orgWidth: edgeData.width, - orgHeight: edgeData.height, - text: edgeData.label, - layoutOptions: { - 'edgeLabels.inline': 'true', - 'edgeLabels.placement': 'CENTER', - }, - }, - ], - edgeData, - }); + g.setEdge(edge.start, edge.end, edgeData, cnt); }); - return graph; -}; - -// TODO: break out and share with dagre wrapper. The current code in dagre wrapper also adds -// adds the line to the graph, but we don't need that here. This is why we cant use the dagre -// wrapper directly for this -/** - * Add the markers to the edge depending on the type of arrow is - * @param svgPath - * @param edgeData - * @param diagramType - * @param arrowMarkerAbsolute - */ -const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute) { - let url = ''; - // Check configuration for absolute path - if (arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - - // look in edge data and decide which marker to use - switch (edgeData.arrowTypeStart) { - case 'arrow_cross': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')'); - break; - case 'arrow_point': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')'); - break; - case 'arrow_barb': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')'); - break; - case 'arrow_circle': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')'); - break; - case 'aggregation': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')'); - break; - case 'extension': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')'); - break; - case 'composition': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')'); - break; - case 'dependency': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')'); - break; - case 'lollipop': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')'); - break; - default: - } - switch (edgeData.arrowTypeEnd) { - case 'arrow_cross': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')'); - break; - case 'arrow_point': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')'); - break; - case 'arrow_barb': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')'); - break; - case 'arrow_circle': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')'); - break; - case 'aggregation': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')'); - break; - case 'extension': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')'); - break; - case 'composition': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')'); - break; - case 'dependency': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')'); - break; - case 'lollipop': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')'); - break; - default: - } }; /** @@ -655,109 +362,16 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb */ export const getClasses = function (text, diagObj) { log.info('Extracting classes'); - diagObj.db.clear('ver-2'); + diagObj.db.clear(); try { // Parse the graph definition diagObj.parse(text); return diagObj.db.getClasses(); } catch (e) { - return {}; + return; } }; -const addSubGraphs = function (db) { - const parentLookupDb = { parentById: {}, childrenById: {} }; - const subgraphs = db.getSubGraphs(); - log.info('Subgraphs - ', subgraphs); - subgraphs.forEach(function (subgraph) { - subgraph.nodes.forEach(function (node) { - parentLookupDb.parentById[node] = subgraph.id; - if (parentLookupDb.childrenById[subgraph.id] === undefined) { - parentLookupDb.childrenById[subgraph.id] = []; - } - parentLookupDb.childrenById[subgraph.id].push(node); - }); - }); - - subgraphs.forEach(function (subgraph) { - const data = { id: subgraph.id }; - if (parentLookupDb.parentById[subgraph.id] !== undefined) { - data.parent = parentLookupDb.parentById[subgraph.id]; - } - }); - return parentLookupDb; -}; - -const calcOffset = function (src, dest, parentLookupDb) { - const ancestor = findCommonAncestor(src, dest, parentLookupDb); - if (ancestor === undefined || ancestor === 'root') { - return { x: 0, y: 0 }; - } - - const ancestorOffset = nodeDb[ancestor].offset; - return { x: ancestorOffset.posX, y: ancestorOffset.posY }; -}; - -const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) { - const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb); - - const src = edge.sections[0].startPoint; - const dest = edge.sections[0].endPoint; - const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - - const segPoints = segments.map((segment) => [segment.x + offset.x, segment.y + offset.y]); - const points = [ - [src.x + offset.x, src.y + offset.y], - ...segPoints, - [dest.x + offset.x, dest.y + offset.y], - ]; - - // const curve = line().curve(curveBasis); - const curve = line().curve(curveLinear); - const edgePath = edgesEl - .insert('path') - .attr('d', curve(points)) - .attr('class', 'path') - .attr('fill', 'none'); - const edgeG = edgesEl.insert('g').attr('class', 'edgeLabel'); - const edgeWithLabel = select(edgeG.node().appendChild(edge.labelEl)); - const box = edgeWithLabel.node().firstChild.getBoundingClientRect(); - edgeWithLabel.attr('width', box.width); - edgeWithLabel.attr('height', box.height); - - edgeG.attr( - 'transform', - `translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})` - ); - addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute); -}; - -/** - * Recursive function that iterates over an array of nodes and inserts the children of each node. - * It also recursively populates the inserts the children of the children and so on. - * @param {*} graph - * @param nodeArray - * @param parentLookupDb - */ -const insertChildren = (nodeArray, parentLookupDb) => { - nodeArray.forEach((node) => { - // Check if we have reached the end of the tree - if (!node.children) { - node.children = []; - } - // Check if the node has children - const childIds = parentLookupDb.childrenById[node.id]; - // If the node has children, add them to the node - if (childIds) { - childIds.forEach((childId) => { - node.children.push(nodeDb[childId]); - }); - } - // Recursive call - insertChildren(node.children, parentLookupDb); - }); -}; - /** * Draws a flowchart in the tag with id: id based on the graph definition in text. * @@ -766,51 +380,22 @@ const insertChildren = (nodeArray, parentLookupDb) => { */ export const draw = async function (text, id, _version, diagObj) { - // Add temporary render element + log.info('Drawing flowchart'); diagObj.db.clear(); - nodeDb = {}; - portPos = {}; - diagObj.db.setGen('gen-2'); + flowDb.setGen('gen-2'); // Parse the graph definition diagObj.parser.parse(text); - const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy'); - let graph = { - id: 'root', - layoutOptions: { - 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', - 'org.eclipse.elk.padding': '[top=100, left=100, bottom=110, right=110]', - 'elk.layered.spacing.edgeNodeBetweenLayers': '30', - // 'elk.layered.mergeEdges': 'true', - 'elk.direction': 'DOWN', - // 'elk.ports.sameLayerEdges': true, - // 'nodePlacement.strategy': 'SIMPLE', - }, - children: [], - edges: [], - }; - log.info('Drawing flowchart using v3 renderer', elk); - - // Set the direction, // Fetch the default direction, use TD if none was found let dir = diagObj.db.getDirection(); - switch (dir) { - case 'BT': - graph.layoutOptions['elk.direction'] = 'UP'; - break; - case 'TB': - graph.layoutOptions['elk.direction'] = 'DOWN'; - break; - case 'LR': - graph.layoutOptions['elk.direction'] = 'RIGHT'; - break; - case 'RL': - graph.layoutOptions['elk.direction'] = 'LEFT'; - break; + if (dir === undefined) { + dir = 'TD'; } - const { securityLevel, flowchart: conf } = getConfig(); - // Find the root dom node to ne used in rendering + const { securityLevel, flowchart: conf } = getConfig(); + const nodeSpacing = conf.nodeSpacing || 50; + const rankSpacing = conf.rankSpacing || 50; + // Handle root and document for when rendering in sandbox mode let sandboxElement; if (securityLevel === 'sandbox') { @@ -822,24 +407,28 @@ export const draw = async function (text, id, _version, diagObj) { : select('body'); const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - const svg = root.select(`[id="${id}"]`); + // 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 {}; + }); - // Define the supported markers for the diagram - const markers = ['point', 'circle', 'cross']; - - // Add the marker definitions to the svg as marker tags - insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute); - - // Fetch the vertices/nodes and edges/links from the parsed graph definition - const vert = diagObj.db.getVertices(); - - // Setup nodes from the subgraphs with type group, these will be used - // as nodes with children in the subgraph 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 }, @@ -850,119 +439,108 @@ export const draw = async function (text, id, _version, diagObj) { ); } - // debugger; - // Add an element in the svg to be used to hold the subgraphs container - // elements - const subGraphsEl = svg.insert('g').attr('class', 'subgraphs'); + // Fetch the vertices/nodes and edges/links from the parsed graph definition + const vert = diagObj.db.getVertices(); - // Create the lookup db for the subgraphs and their children to used when creating - // the tree structured graph - const parentLookupDb = addSubGraphs(diagObj.db); - - // Add the nodes to the graph, this will entail creating the actual nodes - // in order to get the size of the node. You can't get the size of a node - // that is not in the dom so we need to add it to the dom, get the size - // we will position the nodes when we get the layout from elkjs - graph = await addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph); - - // Time for the edges, we start with adding an element in the node to hold the edges - const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); - // Fetch the edges form the parsed graph definition const edges = diagObj.db.getEdges(); - // Add the edges to the graph, this will entail creating the actual edges - graph = addEdges(edges, diagObj, graph, svg); + 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]; - // Iterate through all nodes and add the top level nodes to the graph - const nodes = Object.keys(nodeDb); - nodes.forEach((nodeId) => { - const node = nodeDb[nodeId]; - if (!node.parent) { - graph.children.push(node); + 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); } - // Subgraph - if (parentLookupDb.childrenById[nodeId] !== undefined) { - node.labels = [ - { - text: node.labelText, - layoutOptions: { - 'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]', - }, - width: node.labelData.width, - height: node.labelData.height, - // width: 100, - // height: 100, - }, - ]; - delete node.x; - delete node.y; - delete node.width; - delete node.height; + } + addVertices(vert, g, id, root, doc, diagObj); + addEdges(edges, g, diagObj); + + // Add custom shapes + // flowChartShapes.addToRenderV2(addShape); + + // Set up an SVG group so that we can translate the final graph. + const svg = root.select(`[id="${id}"]`); + + // Run the renderer. This is what draws the final graph. + const element = root.select('#' + id + ' g'); + + swimlaneLayout(g, conf); + swimlaneRender(element,g, conf); + await render(element, g, ['point', 'circle', 'cross'], 'flowchart', id); + + utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle()); + + setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth); + + // Index nodes + diagObj.db.indexNodes('subGraph' + i); + + // Add label rects for non html labels + if (!conf.htmlLabels) { + const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label'); + for (const label of labels) { + // Get dimensions of label + const dim = label.getBBox(); + + const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('rx', 0); + rect.setAttribute('ry', 0); + rect.setAttribute('width', dim.width); + rect.setAttribute('height', dim.height); + + label.insertBefore(rect, label.firstChild); } - }); + } - insertChildren(graph.children, parentLookupDb); - log.info('after layout', JSON.stringify(graph, null, 2)); - const g = await elk.layout(graph); - drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0); - log.info('after layout', g); - g.edges?.map((edge) => { - insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb); - }); - setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth); - // Remove element after layout - renderEl.remove(); -}; + // If node has a link, wrap it in an anchor SVG object. + const keys = Object.keys(vert); + keys.forEach(function (key) { + const vertex = vert[key]; -const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => { - nodeArray.forEach(function (node) { - if (node) { - nodeDb[node.id].offset = { - posX: node.x + relX, - posY: node.y + relY, - x: relX, - y: relY, - depth, - width: node.width, - height: node.height, - }; - if (node.type === 'group') { - const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); - subgraphEl - .insert('rect') - .attr('class', 'subgraph subgraph-lvl-' + (depth % 5) + ' node') - .attr('x', node.x + relX) - .attr('y', node.y + relY) - .attr('width', node.width) - .attr('height', node.height); - const label = subgraphEl.insert('g').attr('class', 'label'); - const labelCentering = getConfig().flowchart.htmlLabels ? node.labelData.width / 2 : 0; - label.attr( - 'transform', - `translate(${node.labels[0].x + relX + node.x + labelCentering}, ${ - node.labels[0].y + relY + node.y + 3 - })` - ); - label.node().appendChild(node.labelData.labelNode); + if (vertex.link) { + const node = select('#' + id + ' [id="' + key + '"]'); + if (node) { + const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a'); + link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' ')); + link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link); + link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener'); + if (securityLevel === 'sandbox') { + link.setAttributeNS('http://www.w3.org/2000/svg', 'target', '_top'); + } else if (vertex.linkTarget) { + link.setAttributeNS('http://www.w3.org/2000/svg', 'target', vertex.linkTarget); + } - log.info('Id (UGH)= ', node.type, node.labels); - } else { - log.info('Id (UGH)= ', node.id); - node.el.attr( - 'transform', - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); + const linkNode = node.insert(function () { + return link; + }, ':first-child'); + + const shape = node.select('.label-container'); + if (shape) { + linkNode.append(function () { + return shape.node(); + }); + } + + const label = node.select('.label'); + if (label) { + linkNode.append(function () { + return label.node(); + }); + } } } }); - nodeArray.forEach(function (node) { - if (node && node.type === 'group') { - drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, diagObj, depth + 1); - } - }); }; export default { + setConf, + addVertices, + addEdges, getClasses, draw, };