mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-10 02:49:40 +02:00

* develop: (99 commits) Fix spelling Fix community integrations Fix docs docs: Fix config Update all minor dependencies Amend docs to document gitgraph parallel commits Fix lint Use Yarn Add COREPACK_ENABLE_STRICT Added link to Blazorade Mermaid to the community integrations page. Bump node version Add lcov to cspell Correcting path to docker-entrypoint.sh Update recommended Vitest extension Replace mermaid-js.github.io links Replace links to docs with links to webhelp Link to contributing page on webhelp Changes to timeline.md 1. Added colons to all 'NOTES' for consistency. Changes to timeline.md 1. Updates the Wikipedia citation to include a link. 2. Removed periods from documentation sections to be consistent (some had periods, some didn't) 3. Added a space to a coding example for spacing consistency. Replace version number placeholder Fix link to Contributors section in README ...
905 lines
27 KiB
JavaScript
905 lines
27 KiB
JavaScript
import { select, line, curveLinear } from 'd3';
|
|
import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js';
|
|
import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js';
|
|
import { insertEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js';
|
|
import { findCommonAncestor } from './render-utils.js';
|
|
import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js';
|
|
import { getConfig } from '../../mermaid/src/config.js';
|
|
import { log } from '../../mermaid/src/logger.js';
|
|
import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js';
|
|
import common from '../../mermaid/src/diagrams/common/common.js';
|
|
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js';
|
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js';
|
|
import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.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 radius = 0;
|
|
let _shape = '';
|
|
let layoutOptions = {};
|
|
// Set the shape based parameters
|
|
switch (vertex.type) {
|
|
case 'round':
|
|
radius = 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: radius,
|
|
ry: radius,
|
|
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;
|
|
return portPos[graphDirection][edgeDirection][position];
|
|
// return 'south';
|
|
};
|
|
|
|
const getNextPort = (node, edgeDirection, graphDirection) => {
|
|
log.info('getNextPort', { 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 can't 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
|
|
* @param id
|
|
*/
|
|
const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) {
|
|
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
|
|
addEdgeMarkers(svgPath, edgeData, url, id, diagramType);
|
|
};
|
|
|
|
/**
|
|
* Returns the all the styles from classDef statements in the graph definition.
|
|
*
|
|
* @param text
|
|
* @param diagObj
|
|
* @returns {Record<string, import('../../mermaid/src/diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
|
*/
|
|
export const getClasses = function (text, diagObj) {
|
|
log.info('Extracting classes');
|
|
return diagObj.db.getClasses();
|
|
};
|
|
|
|
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, id) {
|
|
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 { x, y } = getLineFunctionsWithOffset(edge.edgeData);
|
|
const curve = line().x(x).y(y).curve(curveLinear);
|
|
const edgePath = edgesEl
|
|
.insert('path')
|
|
.attr('d', curve(points))
|
|
.attr('class', 'path ' + edgeData.classes)
|
|
.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, id);
|
|
};
|
|
|
|
/**
|
|
* 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, id);
|
|
|
|
// 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, id);
|
|
});
|
|
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,
|
|
};
|