From 8e5e212c496aaf750fdff919e6724e6ed58e190f Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 24 Jul 2022 11:05:54 +0200 Subject: [PATCH] Layout algorithm in place --- cypress/platform/knsv.html | 51 ++++++- package.json | 4 +- src/diagram-api/detectType.js | 1 - src/diagrams/mindmap/mindmap.spec.js | 16 +- src/diagrams/mindmap/mindmapDb.js | 17 ++- src/diagrams/mindmap/mindmapRenderer.js | 188 ++++++++++++++++++++++-- src/diagrams/mindmap/svgDraw.js | 16 +- yarn.lock | 10 ++ 8 files changed, 271 insertions(+), 32 deletions(-) diff --git a/cypress/platform/knsv.html b/cypress/platform/knsv.html index fc68f0259..99a94e949 100644 --- a/cypress/platform/knsv.html +++ b/cypress/platform/knsv.html @@ -22,6 +22,7 @@ } .mermaid svg { /* font-size: 18px !important; */ + border: 1px solid red; } @@ -41,7 +42,7 @@ journey Go downstairs: 5: Me Sit down: 5: Mee -
+
mindmap root[ The root where the things @@ -50,6 +51,52 @@ mindmap pen! ] Child1 + child2[ + Child2
+ The second
+ The second
+ The second
+ The second
+ The second
+ The second
+ The second
+ ] + Other + Child3 + GrandChild1 + sc1 + sc2 + sc3 + GrandChild2 +
+
+mindmap + root[ + The root where the things + happen! + ] + Child2 + GrandChild1 + GrandChild2 + Child3 + GrandChild3 + GrandChild4 + Child4 + GrandChild5 + GrandChild6 + Child1 + GrandChild1 + sc1 + sc2 + sc3 + GrandChild2 + Child5 + GrandChild7 + sc1 + sc2 + sc3 + GrandChild7 +
pie @@ -62,7 +109,7 @@ mindmap "Magnesium" : 10.01 "Iron" : 5
-
+
gitGraph TB commit commit diff --git a/package.json b/package.json index b49cb0ee2..779491a20 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,11 @@ "dagre": "^0.8.5", "dagre-d3": "^0.6.4", "dompurify": "2.3.8", + "fast-clone": "^1.5.13", "graphlib": "^2.1.8", "khroma": "^2.0.0", "moment-mini": "^2.24.0", + "non-layered-tidy-tree-layout": "^2.0.2", "stylis": "^4.0.10" }, "devDependencies": { @@ -112,7 +114,7 @@ "webpack-merge": "^5.8.0", "webpack-node-externals": "^3.0.0" }, - "resolutions": { + "resolutions": { "d3": "^7.0.0" }, "files": [ diff --git a/src/diagram-api/detectType.js b/src/diagram-api/detectType.js index 0bcde1e17..07021da3a 100644 --- a/src/diagram-api/detectType.js +++ b/src/diagram-api/detectType.js @@ -83,7 +83,6 @@ const detectType = function (text, cnf) { if (cnf && cnf.flowchart && cnf.flowchart.defaultRenderer === 'dagre-wrapper') return 'flowchart-v2'; const k = Object.keys(detectors); - console.log('here', k); for (let i = 0; i < k.length; i++) { const key = k[i]; console.log('Detecting type for', key); diff --git a/src/diagrams/mindmap/mindmap.spec.js b/src/diagrams/mindmap/mindmap.spec.js index a29aaec8f..55fc570d9 100644 --- a/src/diagrams/mindmap/mindmap.spec.js +++ b/src/diagrams/mindmap/mindmap.spec.js @@ -85,7 +85,7 @@ describe('when parsing a mindmap ', function () { mindmap.parse(str); const mm = mindmap.yy.getMindmap(); - expect(mm.id).toEqual('root'); + expect(mm.nodeId).toEqual('root'); expect(mm.descr).toEqual('The root'); expect(mm.type).toEqual(mindmap.yy.nodeType.RECT); }); @@ -100,7 +100,7 @@ describe('when parsing a mindmap ', function () { expect(mm.children.length).toEqual(1); const child = mm.children[0]; expect(child.descr).toEqual('child1'); - expect(child.id).toEqual('theId'); + expect(child.nodeId).toEqual('theId'); expect(child.type).toEqual(mindmap.yy.nodeType.ROUNDED_RECT); }); it('should handle an id and type for a node definition', function () { @@ -114,7 +114,7 @@ root expect(mm.children.length).toEqual(1); const child = mm.children[0]; expect(child.descr).toEqual('child1'); - expect(child.id).toEqual('theId'); + expect(child.nodeId).toEqual('theId'); expect(child.type).toEqual(mindmap.yy.nodeType.ROUNDED_RECT); }); it('mutiple types (circle)', function () { @@ -139,7 +139,7 @@ root((the root)) mindmap.parse(str); const mm = mindmap.yy.getMindmap(); - expect(mm.id).toEqual('root'); + expect(mm.nodeId).toEqual('root'); expect(mm.descr).toEqual('The root'); expect(mm.type).toEqual(mindmap.yy.nodeType.RECT); expect(mm.icon).toEqual('bomb'); @@ -153,7 +153,7 @@ root((the root)) mindmap.parse(str); const mm = mindmap.yy.getMindmap(); - expect(mm.id).toEqual('root'); + expect(mm.nodeId).toEqual('root'); expect(mm.descr).toEqual('The root'); expect(mm.type).toEqual(mindmap.yy.nodeType.RECT); expect(mm.class).toEqual('m-4 p-8'); @@ -168,7 +168,7 @@ root((the root)) mindmap.parse(str); const mm = mindmap.yy.getMindmap(); - expect(mm.id).toEqual('root'); + expect(mm.nodeId).toEqual('root'); expect(mm.descr).toEqual('The root'); expect(mm.type).toEqual(mindmap.yy.nodeType.RECT); expect(mm.class).toEqual('m-4 p-8'); @@ -182,7 +182,7 @@ root((the root)) `; mindmap.parse(str); const mm = mindmap.yy.getMindmap(); - expect(mm.id).toEqual('root'); + expect(mm.nodeId).toEqual('root'); expect(mm.descr).toEqual('String containing []'); }); it('should be possible to use node syntax in the descriptions in children', function () { @@ -192,7 +192,7 @@ root((the root)) `; mindmap.parse(str); const mm = mindmap.yy.getMindmap(); - expect(mm.id).toEqual('root'); + expect(mm.nodeId).toEqual('root'); expect(mm.descr).toEqual('String containing []'); expect(mm.children.length).toEqual(1); expect(mm.children[0].descr).toEqual('String containing ()'); diff --git a/src/diagrams/mindmap/mindmapDb.js b/src/diagrams/mindmap/mindmapDb.js index 29a982cea..64fdbb0f9 100644 --- a/src/diagrams/mindmap/mindmapDb.js +++ b/src/diagrams/mindmap/mindmapDb.js @@ -5,9 +5,12 @@ var message = ''; var info = false; const root = {}; let nodes = []; - +let cnt = 0; +let elements = {}; export const clear = () => { nodes = []; + cnt = 0; + elements = {}; }; const getParent = function (level) { @@ -26,7 +29,8 @@ export const getMindmap = () => { }; export const addNode = (level, id, descr, type) => { const node = { - id: sanitizeText(id), + id: cnt++, + nodeId: sanitizeText(id), level, descr: sanitizeText(descr), type, @@ -79,6 +83,11 @@ export const getTypeFromStart = (str) => { return nodeType.DEFAULT; } }; + +export const setElementForId = (id, element) => { + elements[id] = element; +}; + export const decorateNode = (decoration) => { console.log('decorateNode', decoration); const node = nodes[nodes.length - 1]; @@ -96,5 +105,9 @@ export default { nodeType, getTypeFromStart, decorateNode, + setElementForId, + getElementById: (id) => elements[id], + // getNodeById: (id) => nodes.find((node) => node.id === id), + getNodeById: (id) => nodes[id], // parseError }; diff --git a/src/diagrams/mindmap/mindmapRenderer.js b/src/diagrams/mindmap/mindmapRenderer.js index 5cc8549df..f0e4ec304 100644 --- a/src/diagrams/mindmap/mindmapRenderer.js +++ b/src/diagrams/mindmap/mindmapRenderer.js @@ -2,7 +2,9 @@ import { select } from 'd3'; import { log, getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI'; import svgDraw from './svgDraw'; - +import { BoundingBox, Layout, Tree } from 'non-layered-tidy-tree-layout'; +import clone from 'fast-clone'; +import db from './mindmapDb'; /** * @param {any} svg The svg element to draw the diagram onto * @param {object} mindmap The maindmap data and hierarchy @@ -17,17 +19,181 @@ function drawNodes(svg, mindmap, conf) { } } +/** @param {any} svg The svg element to draw the diagram onto */ function drawEdges() {} + +/** + * @param mindmap + * @param callback + */ +function eachNode(mindmap, callback) { + callback(mindmap); + if (mindmap.children) { + mindmap.children.forEach((child) => { + eachNode(child, callback); + }); + } +} +/** @param {object} mindmap */ +function transpose(mindmap) { + console.log('transpose', mindmap); + eachNode(mindmap, (node) => { + // node.y = node.y - (node.y - bb.top) * 2 - node.height; + const orgWidth = node.width; + const orgX = node.x; + node.width = node.height; + node.height = orgWidth; + node.x = node.y; + node.y = orgX; + }); + return mindmap; +} +/** @param {object} mindmap */ +function bottomToUp(mindmap) { + console.log('bottomToUp', mindmap); + eachNode(mindmap.result, (node) => { + // node.y = node.y - (node.y - bb.top) * 2 - node.height; + node.y = node.y - (node.y - 0) * 2 - node.height; + }); + return mindmap; +} +/** @param {object} mindmap The mindmap hierarchy */ +function rightToLeft(mindmap) { + console.log('bottomToUp', mindmap); + eachNode(mindmap.result, (node) => { + // node.y = node.y - (node.y - bb.top) * 2 - node.height; + node.x = node.x - (node.x - 0) * 2 - node.width; + }); + return mindmap; +} + +/** + * @param mindmap + * @param dir + * @param conf + */ +function layout(mindmap, dir, conf) { + const bb = new BoundingBox(40, 40); + + const layout = new Layout(bb); + switch (dir) { + case 'TB': + return layout.layout(mindmap); + case 'BT': + return bottomToUp(layout.layout(mindmap)); + case 'RL': { + transpose(mindmap); + let newRes = layout.layout(mindmap); + transpose(newRes.result); + return rightToLeft(newRes); + } + case 'LR': { + transpose(mindmap); + let newRes = layout.layout(mindmap); + transpose(newRes.result); + return newRes; + } + default: + } +} +const dirFromIndex = (index) => { + const dirNum = index % 4; + switch (dirNum) { + case 0: + return 'LR'; + case 1: + return 'RL'; + case 2: + return 'TB'; + case 3: + return 'BT'; + default: + return 'TB'; + } +}; + +const mergeTrees = (node, trees) => { + node.x = trees[0].result.x; + node.y = trees[0].result.y; + trees.forEach((tree) => { + tree.result.children.forEach((child) => { + const dx = node.x - tree.result.x; + const dy = node.y - tree.result.y; + eachNode(child, (childNode) => { + const orgNode = db.getNodeById(childNode.id); + if (orgNode) { + orgNode.x = childNode.x + dx; + orgNode.y = childNode.y + dy; + } + }); + }); + }); + return node; +}; + /** * @param node * @param isRoot + * @param parent + * @param conf */ -function layoutMindmap(node, isRoot) {} +function layoutMindmap(node, conf) { + // BoundingBox(gap, bottomPadding) + // const bb = new BoundingBox(10, 10); + // const layout = new Layout(bb); + // // const layout = new HorizontalLayout(bb); + + const trees = []; + // node.children.forEach((child, index) => { + // const tree = clone(node); + // tree.children = [tree.children[index]]; + // trees.push(layout(tree, dirFromIndex(index), conf)); + // }); + + let cnt = 0; + // For each direction, create a new tree with the same root, and add a ubset of the children to it. + for (let i = 0; i < 4; i++) { + // Calculate the number of the children of the root node that will be used in this direction + const numChildren = + Math.floor(node.children.length / 4) + (node.children.length % 4 > i ? 1 : 0); + // Copy the original root node + const tree = clone(node); + // Setup the new copy with the children to be rendered in this direction + tree.children = []; + for (let j = 0; j < numChildren; j++) { + tree.children.push(node.children[cnt]); + cnt++; + } + if (tree.children.length > 0) { + trees.push(layout(tree, dirFromIndex(i), conf)); + } + } + + // Merge the trees into a single tree + const result = mergeTrees(node, trees); + + // return layout(node, 'BT', conf); + // const res = layout(node, 'BT', conf); + // res.result.children = []; + // trees.forEach((tree) => { + // res.result.children.push(tree.result); + // }); + console.log('Trees', trees); + return node; +} /** * @param node * @param isRoot + * @param conf */ -function positionNodes(node, isRoot) {} +function positionNodes(node, conf) { + svgDraw.positionNode(node, conf); + if (node.children) { + node.children.forEach((child) => { + positionNodes(child, conf); + }); + } +} /** * Draws a an info picture in the tag with id: id based on the graph definition in text. @@ -63,16 +229,6 @@ export const draw = (text, id, version, diagObj) => { const g = svg.append('g'); const mm = diagObj.db.getMindmap(); - // mm.x = 0; - // mm.y = 0; - // svgDraw.drawNode(g, mm, getConfig()); - // mm.children.forEach((child) => { - // child.x = 200; - // child.y = 200; - // child.width = 200; - // svgDraw.drawNode(g, child, getConfig()); - // }); - // Draw the graph and start with drawing the nodes without proper position // this gives us the size of the nodes and we can set the positions later @@ -82,12 +238,14 @@ export const draw = (text, id, version, diagObj) => { // Next step is to layout the mindmap, giving each node a position - // layoutMindmap(mm, conf); + console.log('Before', mm); + const positionedMindmap = layoutMindmap(mm, conf); + console.log(positionedMindmap); // After this we can draw, first the edges and the then nodes with the correct position // drawEdges(svg, mm, conf); - // positionNodes(svg, mm, conf); + positionNodes(positionedMindmap, conf); // Setup the view box and size of the svg element setupGraphViewbox(undefined, svg, conf.mindmap.diagramPadding, conf.mindmap.useMaxWidth); diff --git a/src/diagrams/mindmap/svgDraw.js b/src/diagrams/mindmap/svgDraw.js index edb4d2f72..1853327e9 100644 --- a/src/diagrams/mindmap/svgDraw.js +++ b/src/diagrams/mindmap/svgDraw.js @@ -1,5 +1,7 @@ const lineBreakRegex = //gi; import { select } from 'd3'; +import db from './mindmapDb'; + /** * @param {string} text The text to be wrapped * @param {number} width The max width of the text @@ -88,13 +90,21 @@ export const drawNode = function (elem, node, conf) { .call(wrap, node.width); const bbox = txt.node().getBBox(); node.height = bbox.height + conf.fontSize * 1.1 * 0.5; - r.attr('height', node.height).attr('y', (-1 * node.height) / 2); + r.attr('height', node.height); // .attr('y', (-1 * node.height) / 2); - txt.attr('transform', 'translate( 0,' + (-1 * node.height) / 2 + ')'); // Position the node to its coordinate if (node.x || node.y) { nodeElem.attr('transform', 'translate(' + node.x + ',' + node.y + ')'); } + db.setElementForId(node.id, nodeElem); return node.height; }; -export default { drawNode }; +export const positionNode = function (node, conf) { + const nodeElem = db.getElementById(node.id); + + const x = node.x || 0; + const y = node.y || 0; + // Position the node to its coordinate + nodeElem.attr('transform', 'translate(' + x + ',' + y + ')'); +}; +export default { drawNode, positionNode }; diff --git a/yarn.lock b/yarn.lock index 853d09418..f9c96a3ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5574,6 +5574,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== +fast-clone@^1.5.13: + version "1.5.13" + resolved "https://registry.yarnpkg.com/fast-clone/-/fast-clone-1.5.13.tgz#7fe17542ae1c872e71bf80d177d00c11f51c2ea7" + integrity sha512-0ez7coyFBQFjZtId+RJqJ+EQs61w9xARfqjqK0AD9vIUkSxWD4HvPt80+5evebZ1tTnv1GYKrPTipx7kOW5ipA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -8663,6 +8668,11 @@ nomnom@1.5.2: chalk "~0.4.0" underscore "~1.6.0" +non-layered-tidy-tree-layout@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" + integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"