Using cose-bilkent layout algorithm for mindmaps

This commit is contained in:
Knut Sveidqvist
2022-10-03 10:05:12 +02:00
parent 383cbcf3a0
commit 5079fa93b4
9 changed files with 247 additions and 164 deletions

View File

@@ -4,9 +4,14 @@ import { log, getConfig, setupGraphViewbox } from './mermaidUtils';
import svgDraw from './svgDraw';
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
import cytoscape from 'cytoscape';
import coseBilkent from 'cytoscape-cose-bilkent';
import fcose from 'cytoscape-fcose';
import clone from 'fast-clone';
import * as db from './mindmapDb';
cytoscape.use(fcose);
cytoscape.use(coseBilkent);
/**
* @param {any} svg The svg element to draw the diagram onto
* @param {object} mindmap The maindmap data and hierarchy
@@ -28,17 +33,47 @@ function drawNodes(svg, mindmap, section, conf) {
* @param parent
* @param depth
* @param section
* @param edgesEl
* @param cy
* @param conf
*/
function drawEdges(edgesElem, mindmap, parent, depth, section, conf) {
if (parent) {
svgDraw.drawEdge(edgesElem, mindmap, parent, depth, section, conf);
}
if (mindmap.children) {
mindmap.children.forEach((child, index) => {
drawEdges(edgesElem, child, mindmap, depth + 1, section < 0 ? index : section, conf);
});
}
// edgesElem, cy, conf
function drawEdges(edgesEl, cy, conf) {
cy.edges().map((edge, id) => {
const data = edge.data();
if (edge[0]._private.bodyBounds) {
const bounds = edge[0]._private.rscratch;
log.info(
id,
// 'x:',
// edge.controlPoints(),
// 'y:',
// edge[0]._private.rscratch
// 'w:',
// edge.boundingbox().w,
// 'h:',
// edge.boundingbox().h,
// edge.midPoint()
data
);
// data.el.attr('transform', `translate(${node.position().x}, ${node.position().y})`);
// edgesEl
// .insert('line')
// .attr('x1', bounds.startX)
// .attr('y1', bounds.startY)
// .attr('x2', bounds.endX)
// .attr('y2', bounds.endY)
// .attr('class', 'path');
edgesEl
.insert('path')
// Todo use regular line function
.attr(
'd',
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
)
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
}
});
}
/**
@@ -53,159 +88,159 @@ function eachNode(mindmap, callback) {
});
}
}
/** @param {object} mindmap */
function transpose(mindmap) {
eachNode(mindmap, (node) => {
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) {
log.debug('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) {
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 {any} svg The svg element to draw the diagram onto
* @param {object} mindmap The maindmap data and hierarchy
* @param section
* @param cy
* @param {object} conf The configuration object
* @param level
*/
function layout(mindmap, dir) {
const bb = new BoundingBox(30, 60);
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 + 2) % 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;
}
function addNodes(mindmap, cy, conf, level) {
const node = cy.add({
group: 'nodes',
data: {
id: mindmap.id,
labelText: mindmap.descr,
height: mindmap.height,
width: mindmap.width,
level: level,
nodeId: mindmap.id,
padding: mindmap.padding,
type: mindmap.type,
},
position: {
x: mindmap.x,
y: mindmap.y,
},
});
if (mindmap.children) {
mindmap.children.forEach((child, index) => {
addNodes(child, cy, conf, level + 1);
const edge = cy.add({
group: 'edges',
data: {
id: `${mindmap.id}_${child.id}`,
source: mindmap.id,
target: child.id,
depth: level,
section: child.section,
},
});
});
});
return node;
};
}
}
/**
* @param node
* @param conf
* @param cy
*/
function layoutMindmap(node, conf) {
// BoundingBox(gap, bottomPadding)
// const bb = new BoundingBox(10, 10);
// const layout = new Layout(bb);
// // const layout = new HorizontalLayout(bb);
if (node.children.length === 0) {
return node;
}
const trees = [];
// node.children.forEach((child, index) => {
// const tree = clone(node);
// tree.children = [tree.children[index]];
// trees.push(layout(tree, dirFromIndex(index), conf));
// });
return new Promise((resolve) => {
// BoundingBox(gap, bottomPadding)
// const bb = new BoundingBox(10, 10);
// const layout = new Layout(bb);
// // const layout = new HorizontalLayout(bb);
if (node.children.length === 0) {
return node;
}
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));
}
}
// Let each node know the direct of its tree for when we draw the branches.
trees.forEach((tree, index) => {
tree.result.direction = dirFromIndex(index);
eachNode(tree.result, (node) => {
node.direction = dirFromIndex(index);
const cy = cytoscape({
// styleEnabled: false,
// animate: false,
// ready: function () {
// log.info('Ready', this);
// },
container: document.getElementById('cy'), // container to render in
style: [
// the stylesheet for the graph
{
selector: 'node',
style: {
'background-color': '#666',
label: 'data(labelText)',
},
},
{
selector: 'edge',
style: {
width: 3,
'line-color': '#ccc',
'target-arrow-color': '#ccc',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
label: 'data(id)',
},
},
],
});
addNodes(node, cy, conf, 0);
// Make cytoscape care about the dimensisions of the nodes
cy.nodes().forEach(function (n) {
n.layoutDimensions = () => {
const data = n.data();
// console.log(
// 'id',
// data.id,
// ' node',
// data.nodeId,
// ' layoutDimensions',
// data.width,
// 'x',
// data.height
// );
return { w: data.width, h: data.height };
};
});
// // Merge the trees into a single tree
// mergeTrees(node, trees);
cy.layout({
// name: 'grid',
// name: 'circle',
// name: 'cose',
// name: 'fcose',
name: 'cose-bilkent',
quality: 'proof',
// randomize: false,
// seed: 2,
// name: 'breadthfirst',
// headless: true,
styleEnabled: false,
animate: false,
}).run();
cy.ready((e) => {
log.info('Ready', e);
resolve({ positionedMindmap: node, cy });
});
});
// Merge the trees into a single tree
mergeTrees(node, trees);
return node;
}
/**
* @param node
* @param cy
* @param positionedMindmap
* @param conf
*/
function positionNodes(node, conf) {
svgDraw.positionNode(node, conf);
if (node.children) {
node.children.forEach((child) => {
positionNodes(child, conf);
});
}
function positionNodes(cy, conf) {
cy.nodes().map((node, id) => {
const data = node.data();
data.x = node.position().x;
data.y = node.position().y;
svgDraw.positionNode(data);
const el = db.getElementById(data.nodeId);
log.info('Id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
el.attr(
'transform',
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
);
el.attr('attr', `apa-${id})`);
});
}
/**
@@ -217,7 +252,7 @@ function positionNodes(node, conf) {
* @param diagObj
*/
export const draw = (text, id, version, diagObj) => {
export const draw = async (text, id, version, diagObj) => {
const conf = getConfig();
// This is done only for throwing the error if the text is not valid.
@@ -255,11 +290,11 @@ export const draw = (text, id, version, diagObj) => {
// Next step is to layout the mindmap, giving each node a position
const positionedMindmap = layoutMindmap(mm, conf);
const { positionedMindmap, cy } = await layoutMindmap(mm, conf);
// After this we can draw, first the edges and the then nodes with the correct position
drawEdges(edgesElem, positionedMindmap, null, 0, -1, conf);
positionNodes(positionedMindmap, conf);
// // After this we can draw, first the edges and the then nodes with the correct position
drawEdges(edgesElem, cy, conf);
positionNodes(cy, conf);
// Setup the view box and size of the svg element
setupGraphViewbox(undefined, svg, conf.mindmap.padding, conf.mindmap.useMaxWidth);