import { select } from 'd3'; import dagre from 'dagre'; import graphlib from 'graphlib'; import { logger } from '../../logger'; import classDb, { lookUpDomId } from './classDb'; import { parser } from './parser/classDiagram'; import svgDraw from './svgDraw'; import { getConfig } from '../../config'; import { render } from '../../dagre-wrapper/index.js'; // import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js'; import { curveLinear } from 'd3'; import { interpolateToCurve, getStylesFromArray, configureSvgSize } from '../../utils'; import common from '../common/common'; parser.yy = classDb; let idCache = {}; const padding = 20; const conf = { dividerMargin: 10, padding: 5, textHeight: 10 }; /** * 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. */ export const addClasses = function(classes, g) { // const svg = select(`[id="${svgId}"]`); const keys = Object.keys(classes); logger.info('keys:', keys); logger.info(classes); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition keys.forEach(function(id) { const vertex = classes[id]; /** * Variable for storing the classes for the vertex * @type {string} */ let classStr = 'default'; // if (vertex.classes.length > 0) { // classStr = vertex.classes.join(' '); // } const styles = { labelStyle: '' }; //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; // if (getConfig().flowchart.htmlLabels) { // const node = { // label: vertexText.replace( // /fa[lrsb]?:fa-[\w-]+/g, // s => `` // ) // }; // vertexNode = addHtmlLabel(svg, node).node(); // vertexNode.parentNode.removeChild(vertexNode); // } else { // const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); // svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:')); // const rows = vertexText.split(common.lineBreakRegex); // for (let j = 0; j < rows.length; j++) { // const tspan = document.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 = rows[j]; // svgLabel.appendChild(tspan); // } // vertexNode = svgLabel; // } let radious = 0; let _shape = ''; // Set the shape based parameters switch (vertex.type) { case 'class': _shape = 'class_box'; break; default: _shape = 'class_box'; } // Add the node g.setNode(vertex.id, { labelStyle: styles.labelStyle, shape: _shape, labelText: vertexText, classData: vertex, rx: radious, ry: radious, class: classStr, style: styles.style, id: vertex.id, width: vertex.type === 'group' ? 500 : undefined, type: vertex.type, padding: getConfig().flowchart.padding }); logger.info('setNode', { labelStyle: styles.labelStyle, shape: _shape, labelText: vertexText, rx: radious, ry: radious, class: classStr, style: styles.style, id: vertex.id, width: vertex.type === 'group' ? 500 : undefined, type: vertex.type, padding: getConfig().flowchart.padding }); }); }; /** * Add edges to graph based on parsed graph defninition * @param {Object} edges The edges to add to the graph * @param {Object} g The graph object */ export const addRelations = function(relations, g) { let cnt = 0; let defaultStyle; let defaultLabelStyle; // if (typeof relations.defaultStyle !== 'undefined') { // const defaultStyles = getStylesFromArray(relations.defaultStyle); // defaultStyle = defaultStyles.style; // defaultLabelStyle = defaultStyles.labelStyle; // } relations.forEach(function(edge) { cnt++; const edgeData = {}; //Set relationship style and line type edgeData.classes = 'relation'; edgeData.pattern = edge.relation.lineType == 1 ? 'dashed' : 'solid'; edgeData.id = 'id' + cnt; // Set link type for rendering if (edge.type === 'arrow_open') { edgeData.arrowhead = 'none'; } else { edgeData.arrowhead = 'normal'; } logger.info(edgeData, edge); //Set edge extra labels //edgeData.startLabelLeft = edge.relationTitle1; edgeData.startLabelRight = edge.relationTitle1 === 'none' ? '' : edge.relationTitle1; edgeData.endLabelLeft = edge.relationTitle2 === 'none' ? '' : edge.relationTitle2; //edgeData.endLabelRight = edge.relationTitle2; //Set relation arrow types edgeData.arrowTypeStart = getArrowMarker(edge.relation.type1); edgeData.arrowTypeEnd = getArrowMarker(edge.relation.type2); let style = ''; let labelStyle = ''; if (typeof edge.style !== 'undefined') { const styles = getStylesFromArray(edge.style); style = styles.style; labelStyle = styles.labelStyle; } else { style = 'fill:none'; if (typeof defaultStyle !== 'undefined') { style = defaultStyle; } if (typeof defaultLabelStyle !== 'undefined') { labelStyle = defaultLabelStyle; } } edgeData.style = style; edgeData.labelStyle = labelStyle; if (typeof edge.interpolate !== 'undefined') { edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); } else if (typeof relations.defaultInterpolate !== 'undefined') { edgeData.curve = interpolateToCurve(relations.defaultInterpolate, curveLinear); } else { edgeData.curve = interpolateToCurve(conf.curve, curveLinear); } edge.text = edge.title; if (typeof edge.text === 'undefined') { if (typeof edge.style !== 'undefined') { edgeData.arrowheadStyle = 'fill: #333'; } } else { edgeData.arrowheadStyle = 'fill: #333'; edgeData.labelpos = 'c'; if (getConfig().flowchart.htmlLabels && false) { // eslint-disable-line edgeData.labelType = 'html'; edgeData.label = '' + edge.text + ''; } else { edgeData.labelType = 'text'; edgeData.label = edge.text.replace(common.lineBreakRegex, '\n'); if (typeof edge.style === 'undefined') { edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none'; } edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); } } // Add the edge to the graph g.setEdge(edge.id1, edge.id2, edgeData, cnt); }); }; // Todo optimize const getGraphId = function(label) { const keys = Object.keys(idCache); for (let i = 0; i < keys.length; i++) { if (idCache[keys[i]].label === label) { return keys[i]; } } return undefined; }; export const setConf = function(cnf) { const keys = Object.keys(cnf); keys.forEach(function(key) { conf[key] = cnf[key]; }); }; /** * Draws a flowchart in the tag with id: id based on the graph definition in text. * @param text * @param id */ export const drawOld = function(text, id) { idCache = {}; parser.yy.clear(); parser.parse(text); logger.info('Rendering diagram ' + text); // Fetch the default direction, use TD if none was found const diagram = select(`[id='${id}']`); // insertMarkers(diagram); // Layout graph, Create a new directed graph const g = new graphlib.Graph({ multigraph: true }); // Set an object for the graph label g.setGraph({ isMultiGraph: true }); // Default to assigning a new object as a label for each new edge. g.setDefaultEdgeLabel(function() { return {}; }); const classes = classDb.getClasses(); logger.info('classes:'); logger.info(classes); const keys = Object.keys(classes); for (let i = 0; i < keys.length; i++) { const classDef = classes[keys[i]]; const node = svgDraw.drawClass(diagram, classDef, conf); idCache[node.id] = node; // Add nodes to the graph. The first argument is the node id. The second is // metadata about the node. In this case we're going to add labels to each of // our nodes. g.setNode(node.id, node); logger.info('Org height: ' + node.height); } const relations = classDb.getRelations(); logger.info('relations:', relations); relations.forEach(function(relation) { logger.info( 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation) ); g.setEdge( getGraphId(relation.id1), getGraphId(relation.id2), { relation: relation }, relation.title || 'DEFAULT' ); }); dagre.layout(g); g.nodes().forEach(function(v) { if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v))); select('#' + lookUpDomId(v)).attr( 'transform', 'translate(' + (g.node(v).x - g.node(v).width / 2) + ',' + (g.node(v).y - g.node(v).height / 2) + ' )' ); } }); g.edges().forEach(function(e) { if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') { logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))); svgDraw.drawEdge(diagram, g.edge(e), g.edge(e).relation, conf); } }); const svgBounds = diagram.node().getBBox(); const width = svgBounds.width + padding * 2; const height = svgBounds.height + padding * 2; configureSvgSize(diagram, height, width, conf.useMaxWidth); // Ensure the viewBox includes the whole svgBounds area with extra space for padding const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`; logger.debug(`viewBox ${vBox}`); diagram.attr('viewBox', vBox); }; export const draw = function(text, id) { logger.info('Drawing class'); classDb.clear(); // const parser = classDb.parser; // parser.yy = classDb; // Parse the graph definition // try { parser.parse(text); // } catch (err) { // logger.debug('Parsing failed'); // } // Fetch the default direction, use TD if none was found let dir = 'TD'; const conf = getConfig().flowchart; logger.info('config:', conf); const nodeSpacing = conf.nodeSpacing || 50; const rankSpacing = conf.rankSpacing || 50; // Create the input mermaid.graph const g = new graphlib.Graph({ multigraph: true, compound: true }) .setGraph({ rankdir: dir, nodesep: nodeSpacing, ranksep: rankSpacing, marginx: 8, marginy: 8 }) .setDefaultEdgeLabel(function() { return {}; }); // let subG; // const subGraphs = flowDb.getSubGraphs(); // logger.info('Subgraphs - ', subGraphs); // for (let i = subGraphs.length - 1; i >= 0; i--) { // subG = subGraphs[i]; // logger.info('Subgraph - ', subG); // flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes); // } // Fetch the verices/nodes and edges/links from the parsed graph definition const classes = classDb.getClasses(); const relations = classDb.getRelations(); logger.info(relations); // let i = 0; // for (i = subGraphs.length - 1; i >= 0; i--) { // subG = subGraphs[i]; // selectAll('cluster').append('text'); // for (let j = 0; j < subG.nodes.length; j++) { // g.setParent(subG.nodes[j], subG.id); // } // } addClasses(classes, g, id); addRelations(relations, g); // Add custom shapes // flowChartShapes.addToRenderV2(addShape); // Set up an SVG group so that we can translate the final graph. const svg = select(`[id="${id}"]`); svg.attr('xmlns:xlink', 'http://www.w3.org/1999/xlink'); // Run the renderer. This is what draws the final graph. const element = select('#' + id + ' g'); render(element, g, ['aggregation', 'extension', 'composition', 'dependency'], 'classDiagram', id); // element.selectAll('g.node').attr('title', function() { // return flowDb.getTooltip(this.id); // }); const padding = 8; const svgBounds = svg.node().getBBox(); const width = svgBounds.width + padding * 2; const height = svgBounds.height + padding * 2; logger.debug( `new ViewBox 0 0 ${width} ${height}`, `translate(${padding - g._label.marginx}, ${padding - g._label.marginy})` ); configureSvgSize(svg, height, width, conf.useMaxWidth); svg.attr('viewBox', `0 0 ${width} ${height}`); svg .select('g') .attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`); // Index nodes // flowDb.indexNodes('subGraph' + i); // Add label rects for non html labels if (!conf.htmlLabels) { const labels = document.querySelectorAll('[id="' + id + '"] .edgeLabel .label'); for (let k = 0; k < labels.length; k++) { const label = labels[k]; // Get dimensions of label const dim = label.getBBox(); const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('rx', 0); rect.setAttribute('ry', 0); rect.setAttribute('width', dim.width); rect.setAttribute('height', dim.height); rect.setAttribute('style', 'fill:#e8e8e8;'); label.insertBefore(rect, label.firstChild); } } // If node has a link, wrap it in an anchor SVG object. // const keys = Object.keys(classes); // keys.forEach(function(key) { // const vertex = classes[key]; // if (vertex.link) { // const node = select('#' + id + ' [id="' + key + '"]'); // if (node) { // const link = document.createElementNS('http://www.w3.org/2000/svg', 'a'); // link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' ')); // link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link); // link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener'); // const linkNode = node.insert(function() { // return link; // }, ':first-child'); // const shape = node.select('.label-container'); // if (shape) { // linkNode.append(function() { // return shape.node(); // }); // } // const label = node.select('.label'); // if (label) { // linkNode.append(function() { // return label.node(); // }); // } // } // } // }); }; export default { setConf, draw }; function getArrowMarker(type) { let marker; switch (type) { case 0: marker = 'aggregation'; break; case 1: marker = 'extension'; break; case 2: marker = 'composition'; break; case 3: marker = 'dependency'; break; default: marker = 'none'; } return marker; }