diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index 7374dff53..34af8da15 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -125,4 +125,32 @@ describe('Flowchart v2', () => { expect(svg).to.not.have.attr('style'); }); }); + it('50: handle nested subgraphs in reverse order', () => { + imgSnapshotTest( + `flowchart LR + a -->b + subgraph A + B + end + subgraph B + b + end + `, + {htmlLabels: true, flowchart: {htmlLabels: true}, securityLevel: 'loose'} + ); + }); + it('51: handle nested subgraphs in reverse order', () => { + imgSnapshotTest( + `flowchart LR + a -->b + subgraph A + B + end + subgraph B + b + end + `, + {htmlLabels: true, flowchart: {htmlLabels: true}, securityLevel: 'loose'} + ); + }); }); diff --git a/cypress/platform/current.html b/cypress/platform/current.html index 6297d39b6..348c2e986 100644 --- a/cypress/platform/current.html +++ b/cypress/platform/current.html @@ -75,19 +75,48 @@ stateDiagram-v2
+%% this does not produce the desired result +flowchart TB + subgraph container_Beta + process_C-->Process_D + end + subgraph container_Alpha + process_A-->process_B + process_B-->|via_AWSBatch|container_Beta + process_A-->|messages|process_C + end + +
+
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ff0000'}}}%% -classDiagram - Animal <|-- Duck +flowchart TB + b-->B + a-->c + subgraph O + A + end + subgraph B + c + end + subgraph A + a + b + B + end
-graph TD - A(Start) --> B[/Another/] - A[/Another/] --> C[End] - subgraph section - B - C - end +flowchart TB + subgraph O + A + end + subgraph A + b-->B + a-->c + end + subgraph B + c + end
sequenceDiagram diff --git a/cypress/platform/current2.html b/cypress/platform/current2.html new file mode 100644 index 000000000..9718d54f0 --- /dev/null +++ b/cypress/platform/current2.html @@ -0,0 +1,129 @@ + + + + + + + + + +

info below

+
+
+flowchart BT + subgraph two + b1 + end + subgraph three + c1-->c2 + end + c1 --apa apa apa--> b1 + two --> c2 +
+
+sequenceDiagram + Alice->>Bob:Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though +
+
+ %%{init: {'securityLevel': 'loose'}}%% + graph TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{{Let me think...
Do I want something for work,
something to spend every free second with,
or something to get around?}} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car] + click A "index.html#link-clicked" "link test" + click B callback "click test" + classDef someclass fill:#f96; + class A someclass; + class C someclass; +
+
+ + flowchart BT + subgraph a + b1 -- ok --> b2 + end + a -- sert --> c + c --> d + b1 --> d + a --asd123 --> d +
+
+stateDiagram-v2 + state A { + B1 --> B2: ok + } + A --> C: sert + C --> D + B1 --> D + A --> D: asd123 +
+
+
+ %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ff0000'}}}%% +flowchart LR + a -->b + subgraph A + B + end + subgraph B + b + end + +
+
+flowchart TB + subgraph A + b-->B + a-->c + end + subgraph B + c + end + +
+
+sequenceDiagram +Alice->Bob: Hello Bob, how are you? +Note over Alice,Bob: Looks +Note over Bob,Alice: Looks back +
+ + + + + diff --git a/src/dagre-wrapper/index.js b/src/dagre-wrapper/index.js index 712fee30f..1cdda0265 100644 --- a/src/dagre-wrapper/index.js +++ b/src/dagre-wrapper/index.js @@ -6,7 +6,8 @@ import { clear as clearGraphlib, clusterDb, adjustClustersAndEdges, - findNonClusterChild + findNonClusterChild, + sortNodesByHierarchy } from './mermaid-graphlib'; import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes'; import { insertCluster, clear as clearClusters } from './clusters'; @@ -14,7 +15,7 @@ import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } f import { logger as log } from '../logger'; const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { - log.info('Graph in recursive render:', graphlib.json.write(graph), parentCluster); + log.info('Graph in recursive render: XXX', graphlib.json.write(graph), parentCluster); const dir = graph.graph().rankdir; log.warn('Dir in recursive render - dir:', dir); @@ -22,7 +23,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { if (!graph.nodes()) { log.info('No nodes found for', graph); } else { - log.info('Recursive render', graph.nodes()); + log.info('Recursive render XXX', graph.nodes()); } if (graph.edges().length > 0) { log.info('Recursive edges', graph.edge(graph.edges()[0])); @@ -39,11 +40,14 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { if (typeof parentCluster !== 'undefined') { const data = JSON.parse(JSON.stringify(parentCluster.clusterData)); // data.clusterPositioning = true; - log.info('Setting data for cluster', data); + log.info('Setting data for cluster XXX (', v, ') ', data, parentCluster); graph.setNode(parentCluster.id, data); - graph.setParent(v, parentCluster.id, data); + if (!graph.parent(v)) { + log.warn('Setting parent', v, parentCluster.id); + graph.setParent(v, parentCluster.id, data); + } } - log.info('(Insert) Node ' + v + ': ' + JSON.stringify(graph.node(v))); + log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v))); if (node && node.clusterNode) { // const children = graph.children(v); log.info('Cluster identified', v, node, graph.node(v)); @@ -56,7 +60,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { if (graph.children(v).length > 0) { // This is a cluster but not to be rendered recusively // Render as before - log.info('Cluster - the non recursive path', v, node.id, node, graph); + log.info('Cluster - the non recursive path XXX', v, node.id, node, graph); log.info(findNonClusterChild(node.id, graph)); clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node }; // insertCluster(clusters, graph.node(v)); @@ -91,7 +95,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { dagre.layout(graph); log.info('Graph after layout:', graphlib.json.write(graph)); // Move the nodes to the correct place - graph.nodes().forEach(function(v) { + sortNodesByHierarchy(graph).forEach(function(v) { const node = graph.node(v); log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); log.info( @@ -138,10 +142,10 @@ export const render = (elem, graph, markers, diagramtype, id) => { clearClusters(); clearGraphlib(); - log.warn('Graph before:', graphlib.json.write(graph)); + log.warn('Graph at first:', graphlib.json.write(graph)); adjustClustersAndEdges(graph); log.warn('Graph after:', graphlib.json.write(graph)); - log.warn('Graph ever after:', graph.graph()); + // log.warn('Graph ever after:', graphlib.json.write(graph.node('A').graph)); recursiveRender(elem, graph, diagramtype); }; diff --git a/src/dagre-wrapper/mermaid-graphlib.js b/src/dagre-wrapper/mermaid-graphlib.js index a9b7956c7..dfa187958 100644 --- a/src/dagre-wrapper/mermaid-graphlib.js +++ b/src/dagre-wrapper/mermaid-graphlib.js @@ -52,7 +52,7 @@ const edgeInCluster = (edge, clusterId) => { }; const copy = (clusterId, graph, newGraph, rootId) => { - log.info( + log.warn( 'Copying children of ', clusterId, 'root', @@ -68,7 +68,7 @@ const copy = (clusterId, graph, newGraph, rootId) => { nodes.push(clusterId); } - log.debug('Copying (nodes) clusterId', clusterId, 'nodes', nodes); + log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes); nodes.forEach(node => { if (graph.children(node).length > 0) { @@ -77,8 +77,8 @@ const copy = (clusterId, graph, newGraph, rootId) => { const data = graph.node(node); log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId); newGraph.setNode(node, data); - log.debug('Setting parent', node, graph.parent(node)); if (rootId !== graph.parent(node)) { + log.warn('Setting parent', node, graph.parent(node)); newGraph.setParent(node, graph.parent(node)); } @@ -245,40 +245,51 @@ export const adjustClustersAndEdges = (graph, depth) => { // d1 xor d2 - if either d1 is true and d2 is false or the other way around if (d1 ^ d2) { - log.debug('Edge: ', edge, ' leaves cluster ', id); - log.debug('Decendants of ', id, ': ', decendants[id]); + log.warn('Edge: ', edge, ' leaves cluster ', id); + log.warn('Decendants of XXX ', id, ': ', decendants[id]); clusterDb[id].externalConnections = true; } } }); + } else { + log.debug('Not a cluster ', id, decendants); } }); - extractor(graph, 0); - // For clusters with incoming and/or outgoing edges translate those edges to a real node // in the cluster inorder to fake the edge graph.edges().forEach(function(e) { const edge = graph.edge(e); - log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); + log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); + log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); let v = e.v; let w = e.w; // Check if link is either from or to a cluster - log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]); + log.warn( + 'Fix XXX', + clusterDb, + 'ids:', + e.v, + e.w, + 'Translateing: ', + clusterDb[e.v], + ' --- ', + clusterDb[e.w] + ); if (clusterDb[e.v] || clusterDb[e.w]) { - log.warn('Fixing and trixing - removing', e.v, e.w, e.name); + log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name); v = getAnchorId(e.v); w = getAnchorId(e.w); graph.removeEdge(e.v, e.w, e.name); if (v !== e.v) edge.fromCluster = e.v; if (w !== e.w) edge.toCluster = e.w; - log.warn('Replacing with', v, w, e.name); + log.warn('Fix Replacing with XXX', v, w, e.name); graph.setEdge(v, w, edge, e.name); } }); log.warn('Adjusted Graph', graphlib.json.write(graph)); + extractor(graph, 0); log.trace(clusterDb); @@ -291,7 +302,7 @@ export const adjustClustersAndEdges = (graph, depth) => { }; export const extractor = (graph, depth) => { - log.debug('extractor - ', depth, graphlib.json.write(graph), graph.children('D')); + log.warn('extractor - ', depth, graphlib.json.write(graph), graph.children('D')); if (depth > 10) { log.error('Bailing out'); return; @@ -340,7 +351,7 @@ export const extractor = (graph, depth) => { graph.children(node) && graph.children(node).length > 0 ) { - log.debug( + log.warn( 'Cluster without external connections, without a parent and with children', node, depth @@ -364,7 +375,7 @@ export const extractor = (graph, depth) => { return {}; }); - log.debug('Old graph before copy', graphlib.json.write(graph)); + log.warn('Old graph before copy', graphlib.json.write(graph)); copy(node, graph, clusterGraph, node); graph.setNode(node, { clusterNode: true, @@ -373,10 +384,10 @@ export const extractor = (graph, depth) => { labelText: clusterDb[node].labelText, graph: clusterGraph }); - log.debug('New graph after copy', graphlib.json.write(clusterGraph)); + log.warn('New graph after copy node: (', node, ')', graphlib.json.write(clusterGraph)); log.debug('Old graph after copy', graphlib.json.write(graph)); } else { - log.debug( + log.warn( 'Cluster ** ', node, ' **not meeting the criteria !externalConnections:', @@ -393,13 +404,27 @@ export const extractor = (graph, depth) => { } nodes = graph.nodes(); - log.debug('New list of nodes', nodes); + log.warn('New list of nodes', nodes); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const data = graph.node(node); - log.debug(' Now next leveƶ', node, data); + log.warn(' Now next level', node, data); if (data.clusterNode) { extractor(data.graph, depth + 1); } } }; + +const sorter = (graph, nodes) => { + if (nodes.length === 0) return []; + let result = Object.assign(nodes); + nodes.forEach(node => { + const children = graph.children(node); + const sorted = sorter(graph, children); + result = result.concat(sorted); + }); + + return result; +}; + +export const sortNodesByHierarchy = graph => sorter(graph, graph.children()); diff --git a/src/dagre-wrapper/mermaid-graphlib.spec.js b/src/dagre-wrapper/mermaid-graphlib.spec.js index 6dcda35db..e6a9506e8 100644 --- a/src/dagre-wrapper/mermaid-graphlib.spec.js +++ b/src/dagre-wrapper/mermaid-graphlib.spec.js @@ -1,6 +1,6 @@ import graphlib from 'graphlib'; import dagre from 'dagre'; -import { validate, adjustClustersAndEdges, extractDecendants } from './mermaid-graphlib'; +import { validate, adjustClustersAndEdges, extractDecendants, sortNodesByHierarchy } from './mermaid-graphlib'; import { setLogLevel, logger } from '../logger'; describe('Graphlib decorations', () => { @@ -373,6 +373,31 @@ describe('Graphlib decorations', () => { }); }); + it('adjustClustersAndEdges should handle nesting GLB77', function () { + /* +flowchart TB + subgraph A + b-->B + a-->c + end + subgraph B + c + end + */ + + const exportedGraph = JSON.parse('{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"class":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","classes":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","classes":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}'); + const gr = graphlib.json.read(exportedGraph) + + logger.info('Graph before', graphlib.json.write(gr)) + adjustClustersAndEdges(gr); + const aGraph = gr.node('A').graph; + const bGraph = aGraph.node('B').graph; + logger.info('Graph after', graphlib.json.write(aGraph)); + // logger.trace('Graph after', graphlib.json.write(g)) + expect(aGraph.parent('c')).toBe('B'); + expect(aGraph.parent('B')).toBe(undefined); + }); + }); describe('extractDecendants', function () { let g; @@ -426,3 +451,57 @@ describe('extractDecendants', function () { expect(d3).toEqual(['c']); }); }); +describe('sortNodesByHierarchy', function () { + let g; + beforeEach(function () { + setLogLevel(1); + g = new graphlib.Graph({ + multigraph: true, + compound: true + }); + g.setGraph({ + rankdir: 'TB', + nodesep: 10, + ranksep: 10, + marginx: 8, + marginy: 8 + }); + g.setDefaultEdgeLabel(function () { + return {}; + }); + }); + it('it should sort proper en nodes are in reverse order', function () { + /* + a -->b + subgraph B + b + end + subgraph A + B + end + */ + g.setNode('a', { data: 1 }); + g.setNode('b', { data: 2 }); + g.setParent('b', 'B'); + g.setParent('B', 'A'); + g.setEdge('a', 'b', '1'); + expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']); + }); + it('it should sort proper en nodes are in correct order', function () { + /* + a -->b + subgraph B + b + end + subgraph A + B + end + */ + g.setNode('a', { data: 1 }); + g.setParent('B', 'A'); + g.setParent('b', 'B'); + g.setNode('b', { data: 2 }); + g.setEdge('a', 'b', '1'); + expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']); + }); +}); diff --git a/src/diagrams/flowchart/flowDb.js b/src/diagrams/flowchart/flowDb.js index a18e23c43..6be1f2c18 100644 --- a/src/diagrams/flowchart/flowDb.js +++ b/src/diagrams/flowchart/flowDb.js @@ -421,6 +421,22 @@ export const addSubGraph = function(_id, list, _title) { title = common.sanitizeText(title, config); subCount = subCount + 1; const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] }; + + /** + * Deletes an id from all subgraphs + */ + const del = _id => { + subGraphs.forEach(sg => { + const pos = sg.nodes.indexOf(_id); + if (pos >= 0) { + console.log(sg.nodes, pos, _id); + sg.nodes.splice(pos, 1); + } + }); + }; + // Removes the members of this subgraph from any other subgraphs, a node only belong to one subgraph + subGraph.nodes.forEach(_id => del(_id)); + console.log(subGraph.nodes); subGraphs.push(subGraph); subGraphLookup[id] = subGraph; return id; diff --git a/src/diagrams/flowchart/flowRenderer-v2.js b/src/diagrams/flowchart/flowRenderer-v2.js index d89ef118b..6d0d48359 100644 --- a/src/diagrams/flowchart/flowRenderer-v2.js +++ b/src/diagrams/flowchart/flowRenderer-v2.js @@ -382,11 +382,13 @@ export const draw = function(text, id) { logger.info(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++) { + logger.info('Setting up subgraphs', subG.nodes[j], subG.id); g.setParent(subG.nodes[j], subG.id); } } diff --git a/src/diagrams/flowchart/parser/subgraph.spec.js b/src/diagrams/flowchart/parser/subgraph.spec.js index e2e412b40..d83d62d22 100644 --- a/src/diagrams/flowchart/parser/subgraph.spec.js +++ b/src/diagrams/flowchart/parser/subgraph.spec.js @@ -1,5 +1,6 @@ import flowDb from '../flowDb'; import flow from './flow'; +import filter from 'lodash/filter'; import { setConfig } from '../../../config'; setConfig({ @@ -238,4 +239,75 @@ describe('when parsing subgraphs', function() { expect(edges[0].type).toBe('arrow_point'); }); + it('should handle nested subgraphs 1', function() { + const res = flow.parser.parse(`flowchart TB + subgraph A + b-->B + a-->c + end + subgraph B + c + end`); + + const subgraphs = flow.parser.yy.getSubGraphs(); + expect(subgraphs.length).toBe(2); + + const subgraphA = filter(subgraphs,o => o.id === 'A')[0]; + const subgraphB = filter(subgraphs,o => o.id === 'B')[0]; + + expect(subgraphB.nodes[0]).toBe('c'); + expect(subgraphA.nodes).toContain('B'); + expect(subgraphA.nodes).toContain('b'); + expect(subgraphA.nodes).toContain('a'); + expect(subgraphA.nodes).not.toContain('c'); + }); + it('should handle nested subgraphs 2', function() { + const res = flow.parser.parse(`flowchart TB + b-->B + a-->c + subgraph B + c + end + subgraph A + a + b + B + end`); + + const subgraphs = flow.parser.yy.getSubGraphs(); + expect(subgraphs.length).toBe(2); + + const subgraphA = filter(subgraphs,o => o.id === 'A')[0]; + const subgraphB = filter(subgraphs,o => o.id === 'B')[0]; + + expect(subgraphB.nodes[0]).toBe('c'); + expect(subgraphA.nodes).toContain('B'); + expect(subgraphA.nodes).toContain('b'); + expect(subgraphA.nodes).toContain('a'); + expect(subgraphA.nodes).not.toContain('c'); + }); + it('should handle nested subgraphs 3', function() { + console.log('#3'); + const res = flow.parser.parse(`flowchart TB + subgraph B + c + end + a-->c + subgraph A + b-->B + a + end`); + + const subgraphs = flow.parser.yy.getSubGraphs(); + expect(subgraphs.length).toBe(2); + + const subgraphA = filter(subgraphs,o => o.id === 'A')[0]; + const subgraphB = filter(subgraphs,o => o.id === 'B')[0]; + console.log(subgraphB.nodes) + expect(subgraphB.nodes[0]).toBe('c'); + expect(subgraphA.nodes).toContain('B'); + expect(subgraphA.nodes).toContain('b'); + expect(subgraphA.nodes).toContain('a'); + expect(subgraphA.nodes).not.toContain('c'); + }); });