#815 Using elk for flowchart layouts

This commit is contained in:
Knut Sveidqvist
2022-12-07 09:37:27 +01:00
parent 4863d0d29d
commit ebf76e3d1f
3 changed files with 268 additions and 77 deletions

View File

@@ -39,6 +39,7 @@
"gitgraph", "gitgraph",
"globby", "globby",
"graphlib", "graphlib",
"graphviz",
"grav", "grav",
"greywolf", "greywolf",
"inkdrop", "inkdrop",
@@ -66,6 +67,7 @@
"plantuml", "plantuml",
"playfair", "playfair",
"podlite", "podlite",
"radious",
"ranksep", "ranksep",
"redmine", "redmine",
"sandboxed", "sandboxed",

View File

@@ -56,11 +56,77 @@
<body> <body>
<div>Security check</div> <div>Security check</div>
<pre id="diagram" class="mermaid"> <pre id="diagram" class="mermaid">
cyto TD
%% 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
</pre
>
<pre id="diagram" class="mermaid2">
cyto LR cyto LR
inside1 --> inside2 & inside3 & inside4 & inside5 & inside6 subgraph TOP
a(letter a<br />a) ---> b(letter b)--> c(letter c) --> d -->e(letter e<br />e) --> a direction LR
b <--> d(letter b<br />d) subgraph B1
</pre> direction RL
i1 -->f1
end
subgraph B2
direction BT
i2 -->f2
end
end
B1 --> B2
F --> f1
</pre
>
inside1 --> inside2 & inside3 & inside4 & inside5 & inside6 a(letter a<br />a) ---> b(letter
b)--> c(letter c) --> d -->e(letter e<br />e) --> a b <--> d(letter b<br />d)
<pre id="diagram" class="mermaid2"> <pre id="diagram" class="mermaid2">
mindmap mindmap
root root

View File

@@ -37,7 +37,7 @@ const nodeDb = {};
// * @param doc // * @param doc
// * @param diagObj // * @param diagObj
// */ // */
export const addVertices = function (vert, svgId, root, doc, diagObj, parentLookUpDb, graph) { export const addVertices = function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) {
const svg = root.select(`[id="${svgId}"]`); const svg = root.select(`[id="${svgId}"]`);
const nodes = svg.insert('g').attr('class', 'nodes'); const nodes = svg.insert('g').attr('class', 'nodes');
const keys = Object.keys(vert); const keys = Object.keys(vert);
@@ -170,55 +170,60 @@ export const addVertices = function (vert, svgId, root, doc, diagObj, parentLook
props: vertex.props, props: vertex.props,
padding: getConfig().flowchart.padding, padding: getConfig().flowchart.padding,
}; };
const nodeEl = insertNode(nodes, node, vertex.dir); let boundingBox;
const boundingBox = nodeEl.node().getBBox(); let nodeEl;
if (node.type !== 'group') {
nodeEl = insertNode(nodes, node, vertex.dir);
boundingBox = nodeEl.node().getBBox();
}
const data = { const data = {
id: vertex.id, id: vertex.id,
labelStyle: styles.labelStyle, // labelStyle: styles.labelStyle,
shape: _shape, // shape: _shape,
labelText: vertexText, // labelText: vertexText,
rx: radious, labels: [{ text: vertexText }, { text: vertexText }],
ry: radious, // rx: radious,
class: classStr, // ry: radious,
style: styles.style, // class: classStr,
link: vertex.link, // style: styles.style,
linkTarget: vertex.linkTarget, // link: vertex.link,
tooltip: diagObj.db.getTooltip(vertex.id) || '', // linkTarget: vertex.linkTarget,
// tooltip: diagObj.db.getTooltip(vertex.id) || '',
domId: diagObj.db.lookUpDomId(vertex.id), domId: diagObj.db.lookUpDomId(vertex.id),
haveCallback: vertex.haveCallback, // haveCallback: vertex.haveCallback,
width: boundingBox.width, width: boundingBox?.width,
height: boundingBox.height, height: boundingBox?.height,
dir: vertex.dir, // dir: vertex.dir,
type: vertex.type, type: vertex.type,
props: vertex.props, // props: vertex.props,
padding: getConfig().flowchart.padding, // padding: getConfig().flowchart.padding,
boundingBox, // boundingBox,
el: nodeEl, el: nodeEl,
parent: parentLookUpDb.parentById[vertex.id], parent: parentLookupDb.parentById[vertex.id],
}; };
// if (!Object.keys(parentLookUpDb.childrenById).includes(vertex.id)) { // if (!Object.keys(parentLookupDb.childrenById).includes(vertex.id)) {
graph.children.push({ // graph.children.push({
...data, // ...data,
}); // });
// } // }
nodeDb[node.id] = data; nodeDb[node.id] = data;
log.trace('setNode', { // log.trace('setNode', {
labelStyle: styles.labelStyle, // labelStyle: styles.labelStyle,
shape: _shape, // shape: _shape,
labelText: vertexText, // labelText: vertexText,
rx: radious, // rx: radious,
ry: radious, // ry: radious,
class: classStr, // class: classStr,
style: styles.style, // style: styles.style,
id: vertex.id, // id: vertex.id,
domId: diagObj.db.lookUpDomId(vertex.id), // domId: diagObj.db.lookUpDomId(vertex.id),
width: vertex.type === 'group' ? 500 : undefined, // width: vertex.type === 'group' ? 500 : undefined,
type: vertex.type, // type: vertex.type,
dir: vertex.dir, // dir: vertex.dir,
props: vertex.props, // props: vertex.props,
padding: getConfig().flowchart.padding, // padding: getConfig().flowchart.padding,
parent: parentLookUpDb.parentById[vertex.id], // parent: parentLookupDb.parentById[vertex.id],
}); // });
}); });
return graph; return graph;
}; };
@@ -368,7 +373,7 @@ export const addEdges = function (edges, diagObj, graph) {
sources: [edge.start], sources: [edge.start],
targets: [edge.end], targets: [edge.end],
edgeData, edgeData,
targetPort: 'PortSide.NORTH', // targetPort: 'PortSide.NORTH',
// id: cnt, // id: cnt,
}); });
}); });
@@ -470,40 +475,69 @@ export const getClasses = function (text, diagObj) {
}; };
const addSubGraphs = function (db) { const addSubGraphs = function (db) {
const parentLookUpDb = { parentById: {}, childrenById: {} }; const parentLookupDb = { parentById: {}, childrenById: {} };
const subgraphs = db.getSubGraphs(); const subgraphs = db.getSubGraphs();
log.info('Subgraphs - ', subgraphs); log.info('Subgraphs - ', subgraphs);
subgraphs.forEach(function (subgraph) { subgraphs.forEach(function (subgraph) {
subgraph.nodes.forEach(function (node) { subgraph.nodes.forEach(function (node) {
parentLookUpDb.parentById[node] = subgraph.id; parentLookupDb.parentById[node] = subgraph.id;
if (parentLookUpDb.childrenById[subgraph.id] === undefined) { if (parentLookupDb.childrenById[subgraph.id] === undefined) {
parentLookUpDb.childrenById[subgraph.id] = []; parentLookupDb.childrenById[subgraph.id] = [];
} }
parentLookUpDb.childrenById[subgraph.id].push(node); parentLookupDb.childrenById[subgraph.id].push(node);
}); });
}); });
subgraphs.forEach(function (subgraph) { subgraphs.forEach(function (subgraph) {
const data = { id: subgraph.id }; const data = { id: subgraph.id };
if (parentLookUpDb.parentById[subgraph.id] !== undefined) { if (parentLookupDb.parentById[subgraph.id] !== undefined) {
data.parent = parentLookUpDb.parentById[subgraph.id]; data.parent = parentLookupDb.parentById[subgraph.id];
} }
// cy.add({ // cy.add({
// group: 'nodes', // group: 'nodes',
// data, // data,
// }); // });
}); });
return parentLookUpDb; return parentLookupDb;
};
/* Reverse engineered with trial and error */
const calcOffset = function (src, dest, sourceId, targetId) {
if (src === dest) {
return src;
}
return 0;
}; };
const insertEdge = function (edgesEl, edge, edgeData, diagObj) { const insertEdge = function (edgesEl, edge, edgeData, diagObj) {
const srcOffset = nodeDb[edge.sources[0]].offset;
const targetOffset = nodeDb[edge.targets[0]].offset;
const offset = {
x: calcOffset(
srcOffset.x,
targetOffset.x,
nodeDb[edge.sources[0]].id,
nodeDb[edge.targets[0]].id
),
y: calcOffset(
srcOffset.y,
targetOffset.y,
nodeDb[edge.sources[0]].id,
nodeDb[edge.targets[0]].id
),
};
// console.log('srcOffset', srcOffset.x, targetOffset.x, srcOffset.y, targetOffset.y);
const src = edge.sections[0].startPoint; const src = edge.sections[0].startPoint;
const dest = edge.sections[0].endPoint; const dest = edge.sections[0].endPoint;
const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : [];
// const dest = edge.target().position(); // const dest = edge.target().position();
// const dest = edge.targetEndpoint(); // const dest = edge.targetEndpoint();
const segPoints = segments.map((segment) => [segment.x, segment.y]); const segPoints = segments.map((segment) => [segment.x + offset.x, segment.y + offset.y]);
const points = [[src.x, src.y], ...segPoints, [dest.x, dest.y]]; const points = [
[src.x + offset.x, src.y + offset.y],
...segPoints,
[dest.x + offset.x, dest.y + offset.y],
];
// console.log('Edge ctrl points:', edge.segmentPoints(), 'Bounds:', bounds, edge.source(), points); // console.log('Edge ctrl points:', edge.segmentPoints(), 'Bounds:', bounds, edge.source(), points);
// console.log('Edge ctrl points:', points); // console.log('Edge ctrl points:', points);
// const curve = line().curve(curveCardinal); // const curve = line().curve(curveCardinal);
@@ -538,6 +572,28 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj) {
// .attr('cy', dest.y); // .attr('cy', dest.y);
}; };
/**
*
* @param {*} graph
* @param nodeArray
* @param parentLookupDb
*/
const insertChildren = (nodeArray, parentLookupDb) => {
nodeArray.forEach((node) => {
if (!node.children) {
node.children = [];
}
const childIds = parentLookupDb.childrenById[node.id];
// console.log('UGH', node.id, childIds);
if (childIds) {
childIds.forEach((childId) => {
node.children.push(nodeDb[childId]);
});
}
insertChildren(node.children, parentLookupDb);
});
};
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a flowchart in the tag with id: id based on the graph definition in text.
* *
@@ -558,16 +614,18 @@ export const draw = function (text, id, _version, diagObj) {
let graph = { let graph = {
id: 'root', id: 'root',
layoutOptions: { layoutOptions: {
'elk.algorithm': 'layered', 'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
'elk.direction': 'DOWN', // 'elk.algorithm': 'layered',
'elk.port.side': 'SOUTH', // 'elk.direction': 'DOWN',
// 'elk.port.side': 'SOUTH',
// 'nodePlacement.strategy': 'SIMPLE', // 'nodePlacement.strategy': 'SIMPLE',
'org.eclipse.elk.graphviz.concentrate': true, // 'org.eclipse.elk.spacing.labelLabel': 120,
// 'org.eclipse.elk.graphviz.concentrate': true,
// 'org.eclipse.elk.spacing.nodeNode': 120, // 'org.eclipse.elk.spacing.nodeNode': 120,
// 'org.eclipse.elk.spacing.edgeEdge': 120, // 'org.eclipse.elk.spacing.edgeEdge': 120,
// 'org.eclipse.elk.spacing.edgeNode': 120, // 'org.eclipse.elk.spacing.edgeNode': 120,
// 'org.eclipse.elk.spacing.nodeEdge': 120, // 'org.eclipse.elk.spacing.nodeEdge': 120,
'org.eclipse.elk.spacing.componentComponent': 120, // 'org.eclipse.elk.spacing.componentComponent': 120,
}, },
children: [], children: [],
edges: [], edges: [],
@@ -606,29 +664,59 @@ export const draw = function (text, id, _version, diagObj) {
log.info('Subgraph - ', subG); log.info('Subgraph - ', subG);
diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes, subG.dir); diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes, subG.dir);
} }
const subGraphsEl = svg.insert('g').attr('class', 'subgraphs');
const parentLookUpDb = addSubGraphs(diagObj.db); const parentLookupDb = addSubGraphs(diagObj.db);
graph = addVertices(vert, id, root, doc, diagObj, parentLookUpDb, graph); graph = addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph);
const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); const edgesEl = svg.insert('g').attr('class', 'edges edgePath');
const edges = diagObj.db.getEdges(); const edges = diagObj.db.getEdges();
graph = addEdges(edges, diagObj, graph); graph = addEdges(edges, diagObj, graph);
elk.layout(graph).then(function (g) { // Iterate through all nodes and add the top level nodes to the graph
g.children.forEach(function (node) { const nodes = Object.keys(nodeDb);
const data = nodeDb[node.id]; nodes.forEach((nodeId) => {
if (data) { const node = nodeDb[nodeId];
data.el.attr( if (!node.parent) {
'transform', graph.children.push(node);
`translate(${node.x + node.width / 2}, ${node.y + node.height / 2})` }
); if (parentLookupDb.childrenById[nodeId] !== undefined) {
// document // console.log('UGH node', node);
// .querySelector(`[id="${data.domId}"]`) delete node.x;
// .setAttribute('transform', `translate(${node.position().x}, ${node.position().y})`); delete node.y;
log.info('Id = ', data.domId, svg.select(`[id="${data.domId}"]`), data.el.node()); delete node.width;
delete node.height;
} }
}); });
insertChildren(graph.children, parentLookupDb);
// console.log('Graph (UGH)- ', JSON.parse(JSON.stringify(graph)), JSON.stringify(graph));
// const graph2 = {
// id: 'root',
// layoutOptions: { 'elk.algorithm': 'layered' },
// children: [
// {
// id: 'N1',
// width: 30,
// height: 30,
// padding: 12,
// children: [
// { id: 'n1', width: 30, height: 30 },
// { id: 'n2', width: 30, height: 30 },
// { id: 'n3', width: 30, height: 30 },
// ],
// },
// ],
// edges: [
// { id: 'e1', sources: ['n1'], targets: ['n2'] },
// { id: 'e2', sources: ['n1'], targets: ['n3'] },
// ],
// };
elk.layout(graph).then(function (g) {
// elk.layout(graph2).then(function (g) {
// console.log('Layout (UGH)- ', g);
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj);
g.edges.map((edge, id) => { g.edges.map((edge, id) => {
// console.log('Edge (UGH)- ', edge);
insertEdge(edgesEl, edge, edge.edgeData, diagObj); insertEdge(edgesEl, edge, edge.edgeData, diagObj);
}); });
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth); setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
@@ -639,6 +727,41 @@ export const draw = function (text, id, _version, diagObj) {
}); });
}; };
const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj) => {
nodeArray.forEach(function (node) {
if (node) {
if (node.type === 'group') {
subgraphsEl
.insert('rect')
.attr('class', 'subgraph node')
.attr('style', 'fill:#ccc;stroke:black;stroke-width:1')
.attr('x', node.x + relX)
.attr('y', node.y + relY)
.attr('width', node.width)
.attr('height', node.height);
log.info('Id (UGH)= ', node.type, node.domId, svg.select(`[id="${node.domId}"]`));
} else {
log.info('Id (UGH)= ', node.id);
node.el.attr(
'transform',
`translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})`
);
}
nodeDb[node.id].offset = {
posX: node.x + relX,
posY: node.y + relY,
x: relX,
y: relY,
};
}
});
nodeArray.forEach(function (node) {
if (node && node.type === 'group') {
drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, diagObj);
}
});
};
export default { export default {
// setConf, // setConf,
// addVertices, // addVertices,