diff --git a/cypress/platform/class.html b/cypress/platform/class.html new file mode 100644 index 000000000..6fc4290e6 --- /dev/null +++ b/cypress/platform/class.html @@ -0,0 +1,66 @@ + + + + + + + + +

info below

+
+ classDiagram-v2 + class BankAccount{ + +String owner + +BigDecimal balance + +deposit(amount) bool + +withdrawl(amount) int + } + classA --|> classB : Inheritance + classC --* classD : Composition + classE --o classF : Aggregation + classG --> classH : Association + classI -- classJ : Link(Solid) + classK ..> classL : Dependency + classM ..|> classN : Realization + classO .. classP : Link(Dashed) + classA : +attr1 + classA : attr2 + classA : method1() + <<interface>> classB + classB : method2() int +
+ + + + diff --git a/src/dagre-wrapper/GraphObjects.md b/src/dagre-wrapper/GraphObjects.md index 91bd3eee1..30365e1c8 100644 --- a/src/dagre-wrapper/GraphObjects.md +++ b/src/dagre-wrapper/GraphObjects.md @@ -84,6 +84,7 @@ This is set by the renderer of the diagram and insert the data that the wrapper | id | id of the shape | | type | if set to group then this node indicates *a cluster*. | | padding | Padding. Passed from the render as this might differ between different diagrams. Maybe obsolete. | +| data | Non-generic data specific to the shape. | # edge diff --git a/src/dagre-wrapper/edges.js b/src/dagre-wrapper/edges.js index ce11fdc56..121f79b0a 100644 --- a/src/dagre-wrapper/edges.js +++ b/src/dagre-wrapper/edges.js @@ -297,35 +297,60 @@ export const insertEdge = function(elem, e, edge, clusterDb, diagramType, graph) url = url.replace(/\(/g, '\\('); url = url.replace(/\)/g, '\\)'); } - logger.info('arrowType', edge.arrowType); - switch (edge.arrowType) { + logger.info('arrowTypeStart', edge.arrowTypeStart); + logger.info('arrowTypeEnd', edge.arrowTypeEnd); + + switch (edge.arrowTypeStart) { + case 'arrow_cross': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')'); + break; + case 'arrow_point': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')'); + break; + case 'arrow_barb': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')'); + break; + case 'arrow_circle': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')'); + break; + case 'aggregation': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')'); + break; + case 'extension': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')'); + break; + case 'composition': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')'); + break; + case 'dependency': + svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')'); + break; + default: + } + switch (edge.arrowTypeEnd) { case 'arrow_cross': svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')'); break; - case 'double_arrow_cross': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')'); - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')'); - break; case 'arrow_point': svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')'); break; - case 'double_arrow_point': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')'); - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')'); - break; case 'arrow_barb': svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')'); break; - case 'double_arrow_barb': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barnEnd' + ')'); - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')'); - break; case 'arrow_circle': svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')'); break; - case 'double_arrow_circle': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')'); - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')'); + case 'aggregation': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')'); + break; + case 'extension': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')'); + break; + case 'composition': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')'); + break; + case 'dependency': + svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')'); break; default: } diff --git a/src/dagre-wrapper/index.js b/src/dagre-wrapper/index.js index a9d537cca..712fee30f 100644 --- a/src/dagre-wrapper/index.js +++ b/src/dagre-wrapper/index.js @@ -20,12 +20,12 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { const elem = _elem.insert('g').attr('class', 'root'); // eslint-disable-line if (!graph.nodes()) { - log.trace('No nodes found for', graph); + log.info('No nodes found for', graph); } else { - log.trace('Recursive render', graph.nodes()); + log.info('Recursive render', graph.nodes()); } if (graph.edges().length > 0) { - log.trace('Recursive edges', graph.edge(graph.edges()[0])); + log.info('Recursive edges', graph.edge(graph.edges()[0])); } const clusters = elem.insert('g').attr('class', 'clusters'); // eslint-disable-line const edgePaths = elem.insert('g').attr('class', 'edgePaths'); @@ -39,14 +39,14 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { if (typeof parentCluster !== 'undefined') { const data = JSON.parse(JSON.stringify(parentCluster.clusterData)); // data.clusterPositioning = true; - log.trace('Setting data for cluster', data); + log.info('Setting data for cluster', data); graph.setNode(parentCluster.id, data); graph.setParent(v, parentCluster.id, data); } - log.trace('(Insert) Node ' + v + ': ' + JSON.stringify(graph.node(v))); + log.info('(Insert) Node ' + v + ': ' + JSON.stringify(graph.node(v))); if (node && node.clusterNode) { // const children = graph.children(v); - log.trace('Cluster identified', v, node, graph.node(v)); + log.info('Cluster identified', v, node, graph.node(v)); const newEl = recursiveRender(nodes, node.graph, diagramtype, graph.node(v)); updateNodeBounds(node, newEl); setNodeElem(newEl, node); @@ -56,12 +56,12 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { if (graph.children(v).length > 0) { // This is a cluster but not to be rendered recusively // Render as before - log.trace('Cluster - the non recursive path', v, node.id, node, graph); - log.trace(findNonClusterChild(node.id, graph)); + log.info('Cluster - the non recursive path', v, node.id, node, graph); + log.info(findNonClusterChild(node.id, graph)); clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node }; // insertCluster(clusters, graph.node(v)); } else { - log.trace('Node - the non recursive path', v, node.id, node); + log.info('Node - the non recursive path', v, node.id, node); insertNode(nodes, graph.node(v), dir); } } @@ -73,11 +73,11 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { // TODO: pick optimal child in the cluster to us as link anchor graph.edges().forEach(function(e) { const edge = graph.edge(e.v, e.w, e.name); - log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - log.trace('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e))); + log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); + log.info('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e))); // Check if link is either from or to a cluster - log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]); + log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]); insertEdgeLabel(edgeLabels, edge); }); @@ -89,11 +89,11 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { log.info('#############################################'); log.info(graph); dagre.layout(graph); - log.trace('Graph after layout:', graphlib.json.write(graph)); + log.info('Graph after layout:', graphlib.json.write(graph)); // Move the nodes to the correct place graph.nodes().forEach(function(v) { const node = graph.node(v); - log.trace('Position ' + v + ': ' + JSON.stringify(graph.node(v))); + log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); log.info( 'Position ' + v + ': (' + node.x, ',' + node.y, diff --git a/src/dagre-wrapper/markers.js b/src/dagre-wrapper/markers.js index 84a65dd68..9d879c08b 100644 --- a/src/dagre-wrapper/markers.js +++ b/src/dagre-wrapper/markers.js @@ -29,7 +29,7 @@ const extension = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-extensionEnd ' + type) + .attr('id', type + '-extensionEnd') .attr('class', 'marker extension ' + type) .attr('refX', 19) .attr('refY', 7) @@ -45,7 +45,7 @@ const composition = (elem, type) => { .append('defs') .append('marker') .attr('id', type + '-compositionStart') - .attr('class', 'marker extension ' + type) + .attr('class', 'marker composition ' + type) .attr('refX', 0) .attr('refY', 7) .attr('markerWidth', 190) @@ -58,7 +58,7 @@ const composition = (elem, type) => { .append('defs') .append('marker') .attr('id', type + '-compositionEnd') - .attr('class', 'marker extension ' + type) + .attr('class', 'marker composition ' + type) .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) @@ -72,7 +72,7 @@ const aggregation = (elem, type) => { .append('defs') .append('marker') .attr('id', type + '-aggregationStart') - .attr('class', 'marker extension ' + type) + .attr('class', 'marker aggregation ' + type) .attr('refX', 0) .attr('refY', 7) .attr('markerWidth', 190) @@ -85,7 +85,7 @@ const aggregation = (elem, type) => { .append('defs') .append('marker') .attr('id', type + '-aggregationEnd') - .attr('class', 'marker ' + type) + .attr('class', 'marker aggregation ' + type) .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) @@ -99,7 +99,7 @@ const dependency = (elem, type) => { .append('defs') .append('marker') .attr('id', type + '-dependencyStart') - .attr('class', 'marker extension ' + type) + .attr('class', 'marker dependency ' + type) .attr('refX', 0) .attr('refY', 7) .attr('markerWidth', 190) @@ -112,7 +112,7 @@ const dependency = (elem, type) => { .append('defs') .append('marker') .attr('id', type + '-dependencyEnd') - .attr('class', 'marker ' + type) + .attr('class', 'marker dependency ' + type) .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) diff --git a/src/dagre-wrapper/nodes.js b/src/dagre-wrapper/nodes.js index 624dc5272..a434ed4ab 100644 --- a/src/dagre-wrapper/nodes.js +++ b/src/dagre-wrapper/nodes.js @@ -1,8 +1,8 @@ -import intersect from './intersect/index.js'; import { select } from 'd3'; import { logger } from '../logger'; // eslint-disable-line import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util'; import { getConfig } from '../config'; +import intersect from './intersect/index.js'; import createLabel from './createLabel'; import note from './shapes/note'; @@ -539,6 +539,219 @@ const end = (parent, node) => { return shapeSvg; }; +const class_box = (parent, node) => { + const halfPadding = node.padding / 2; + const rowPadding = 4; + const lineHeight = 8; + + let classes; + if (!node.classes) { + classes = 'node default'; + } else { + classes = 'node ' + node.classes; + } + // Add outer g element + const shapeSvg = parent + .insert('g') + .attr('class', classes) + .attr('id', node.id); + + // Create the title label and insert it after the rect + const rect = shapeSvg.insert('rect', ':first-child'); + const topLine = shapeSvg.insert('line'); + const bottomLine = shapeSvg.insert('line'); + let maxWidth = 0; + let maxHeight = rowPadding; + + const labelContainer = shapeSvg.insert('g').attr('class', 'label'); + let verticalPos = 0; + const hasInterface = node.classData.annotations && node.classData.annotations[0]; + + // 1. Create the labels + const interfaceLabel = labelContainer + .node() + .appendChild(createLabel(node.classData.annotations[0], node.labelStyle, true, true)); + const interfaceBBox = interfaceLabel.getBBox(); + if (node.classData.annotations[0]) { + maxHeight += interfaceBBox.height + rowPadding; + maxWidth += interfaceBBox.width; + } + + const classTitleLabel = labelContainer + .node() + .appendChild(createLabel(node.labelText, node.labelStyle, true, true)); + const classTitleBBox = classTitleLabel.getBBox(); + maxHeight += classTitleBBox.height + rowPadding; + if (classTitleBBox.width > maxWidth) { + maxWidth = classTitleBBox.width; + } + const classAttributes = []; + node.classData.members.forEach(str => { + const lbl = labelContainer.node().appendChild(createLabel(str, node.labelStyle, true, true)); + const bbox = lbl.getBBox(); + if (bbox.width > maxWidth) { + maxWidth = bbox.width; + } + maxHeight += bbox.height + rowPadding; + classAttributes.push(lbl); + }); + + const classMethods = []; + node.classData.methods.forEach(str => { + const lbl = labelContainer.node().appendChild(createLabel(str, node.labelStyle, true, true)); + const bbox = lbl.getBBox(); + if (bbox.width > maxWidth) { + maxWidth = bbox.width; + } + maxHeight += bbox.height + rowPadding; + + classMethods.push(lbl); + }); + + maxHeight += lineHeight; + + // 2. Position the labels + + // position the interface label + if (hasInterface) { + select(interfaceLabel).attr( + 'transform', + 'translate( ' + + -(maxWidth + node.padding - interfaceBBox.width / 2) / 2 + + ', ' + + (-1 * maxHeight) / 2 + + ')' + ); + verticalPos = interfaceBBox.height + rowPadding; + } + // Positin the class title label + let diffX = (maxWidth - classTitleBBox.width) / 2; + select(classTitleLabel).attr( + 'transform', + 'translate( ' + + ((-1 * maxWidth) / 2 + diffX) + + ', ' + + ((-1 * maxHeight) / 2 + verticalPos) + + ')' + ); + verticalPos += classTitleBBox.height + rowPadding; + + topLine + .attr('class', 'divider') + .attr('x1', -maxWidth / 2 - halfPadding) + .attr('x2', maxWidth / 2 + halfPadding) + .attr('y1', -maxHeight / 2 - halfPadding + lineHeight + verticalPos) + .attr('y2', -maxHeight / 2 - halfPadding + lineHeight + verticalPos); + + verticalPos += lineHeight; + + classAttributes.forEach(lbl => { + select(lbl).attr( + 'transform', + 'translate( ' + + -maxWidth / 2 + + ', ' + + ((-1 * maxHeight) / 2 + verticalPos + lineHeight / 2) + + ')' + ); + verticalPos += classTitleBBox.height + rowPadding; + }); + + bottomLine + .attr('class', 'divider') + .attr('x1', -maxWidth / 2 - halfPadding) + .attr('x2', maxWidth / 2 + halfPadding) + .attr('y1', -maxHeight / 2 - halfPadding + lineHeight + verticalPos) + .attr('y2', -maxHeight / 2 - halfPadding + lineHeight + verticalPos); + + verticalPos += lineHeight; + + classMethods.forEach(lbl => { + select(lbl).attr( + 'transform', + 'translate( ' + -maxWidth / 2 + ', ' + ((-1 * maxHeight) / 2 + verticalPos) + ')' + ); + verticalPos += classTitleBBox.height + rowPadding; + }); + // + let bbox; + if (getConfig().flowchart.htmlLabels) { + const div = interfaceLabel.children[0]; + const dv = select(interfaceLabel); + bbox = div.getBoundingClientRect(); + dv.attr('width', bbox.width); + dv.attr('height', bbox.height); + } + // bbox = labelContainer.getBBox(); + + // logger.info('Text 2', text2); + // const textRows = text2.slice(1, text2.length); + // let titleBox = text.getBBox(); + // const descr = label + // .node() + // .appendChild(createLabel(textRows.join('
'), node.labelStyle, true, true)); + + // if (getConfig().flowchart.htmlLabels) { + // const div = descr.children[0]; + // const dv = select(descr); + // bbox = div.getBoundingClientRect(); + // dv.attr('width', bbox.width); + // dv.attr('height', bbox.height); + // } + // // bbox = label.getBBox(); + // // logger.info(descr); + // select(descr).attr( + // 'transform', + // 'translate( ' + + // // (titleBox.width - bbox.width) / 2 + + // (bbox.width > titleBox.width ? 0 : (titleBox.width - bbox.width) / 2) + + // ', ' + + // (titleBox.height + halfPadding + 5) + + // ')' + // ); + // select(text).attr( + // 'transform', + // 'translate( ' + + // // (titleBox.width - bbox.width) / 2 + + // (bbox.width < titleBox.width ? 0 : -(titleBox.width - bbox.width) / 2) + + // ', ' + + // 0 + + // ')' + // ); + // // Get the size of the label + + // // Bounding box for title and text + // bbox = label.node().getBBox(); + + // // Center the label + // label.attr( + // 'transform', + // 'translate(' + -bbox.width / 2 + ', ' + (-bbox.height / 2 - halfPadding + 3) + ')' + // ); + + rect + .attr('class', 'outer title-state') + .attr('x', -maxWidth / 2 - halfPadding) + .attr('y', -(maxHeight / 2) - halfPadding) + .attr('width', maxWidth + node.padding) + .attr('height', maxHeight + node.padding); + + // innerLine + // .attr('class', 'divider') + // .attr('x1', -bbox.width / 2 - halfPadding) + // .attr('x2', bbox.width / 2 + halfPadding) + // .attr('y1', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding) + // .attr('y2', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding); + + updateNodeBounds(node, rect); + + node.intersect = function(point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +}; + const shapes = { question, rect, @@ -558,7 +771,8 @@ const shapes = { note, subroutine, fork: forkJoin, - join: forkJoin + join: forkJoin, + class_box }; let nodeElems = {}; diff --git a/src/diagrams/class/classRenderer-v2.js b/src/diagrams/class/classRenderer-v2.js new file mode 100644 index 000000000..aa37de74c --- /dev/null +++ b/src/diagrams/class/classRenderer-v2.js @@ -0,0 +1,523 @@ +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 } 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 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; + + if (conf.useMaxWidth) { + diagram.attr('width', '100%'); + diagram.attr('style', `max-width: ${width}px;`); + } else { + diagram.attr('height', height); + diagram.attr('width', width); + } + + // 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}"]`); + + // 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})` + ); + + if (conf.useMaxWidth) { + svg.attr('width', '100%'); + svg.attr('style', `max-width: ${width}px;`); + } else { + svg.attr('height', height); + svg.attr('width', width); + } + + 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; +} diff --git a/src/diagrams/class/parser/classDiagram.jison b/src/diagrams/class/parser/classDiagram.jison index bd8cf6f59..f53de9c0f 100644 --- a/src/diagrams/class/parser/classDiagram.jison +++ b/src/diagrams/class/parser/classDiagram.jison @@ -12,6 +12,7 @@ \%\%[^\n]*\n* /* do nothing */ \n+ return 'NEWLINE'; \s+ /* skip whitespace */ +"classDiagram-v2" return 'CLASS_DIAGRAM'; "classDiagram" return 'CLASS_DIAGRAM'; [\{] { this.begin("struct"); /*console.log('Starting struct');*/return 'STRUCT_START';} <> return "EOF_IN_STRUCT"; diff --git a/src/diagrams/class/styles.js b/src/diagrams/class/styles.js index bf237241e..781f3cd9d 100644 --- a/src/diagrams/class/styles.js +++ b/src/diagrams/class/styles.js @@ -10,6 +10,21 @@ const getStyles = options => font-weight: bolder; } } +.node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${options.mainBkg}; + stroke: ${options.nodeBorder}; + stroke-width: 1px; + } + + +.divider { + stroke: ${options.nodeBorder}; + stroke: 1; +} g.clickable { cursor: pointer; @@ -47,15 +62,51 @@ g.classGroup line { stroke-dasharray: 3; } -#compositionStart, #compositionEnd, #dependencyStart, #dependencyEnd, #extensionStart, #extensionEnd { - fill: ${options.lineColor}; - stroke: ${options.lineColor}; +#compositionStart, .composition { + fill: ${options.lineColor} !important; + stroke: ${options.lineColor} !important; stroke-width: 1; } -#aggregationStart, #aggregationEnd { - fill: ${options.nodeBkg}; - stroke: ${options.lineColor}; +#compositionEnd, .composition { + fill: ${options.lineColor} !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; +} + +#dependencyStart, .dependency { + fill: ${options.lineColor} !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; +} + +#dependencyStart, .dependency { + fill: ${options.lineColor} !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; +} + +#extensionStart, .extension { + fill: ${options.lineColor} !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; +} + +#extensionEnd, .extension { + fill: ${options.lineColor} !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; +} + +#aggregationStart, .aggregation { + fill: ${options.nodeBkg} !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; +} + +#aggregationEnd, .aggregation { + fill: ${options.nodeBkg} !important; + stroke: ${options.lineColor} !important; stroke-width: 1; } `; diff --git a/src/diagrams/flowchart/flowRenderer-v2.js b/src/diagrams/flowchart/flowRenderer-v2.js index b03991ec9..1bd2db9de 100644 --- a/src/diagrams/flowchart/flowRenderer-v2.js +++ b/src/diagrams/flowchart/flowRenderer-v2.js @@ -197,8 +197,34 @@ export const addEdges = function(edges, g) { edgeData.arrowhead = 'normal'; } - logger.info(edgeData, edge); - edgeData.arrowType = edge.type; + // 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; + } + + // logger.info('apa', edgeData, edge); + // edgeData.arrowTypeStart = edge.arrowTypeStart; + // edgeData.arrowTypeStart = edge.arrowTypeStart; + // edgeData.arrowType = edgeData.arrowTypeEnd; + // logger.info('apa', edgeData, edge); let style = ''; let labelStyle = ''; diff --git a/src/diagrams/state/stateRenderer-v2.js b/src/diagrams/state/stateRenderer-v2.js index 9d806d6a1..073654bc8 100644 --- a/src/diagrams/state/stateRenderer-v2.js +++ b/src/diagrams/state/stateRenderer-v2.js @@ -179,7 +179,7 @@ const setupDoc = (g, parent, doc, altFlag) => { const edgeData = { id: 'edge' + cnt, arrowhead: 'normal', - arrowType: 'arrow_barb', + arrowTypeEnd: 'arrow_barb', style: 'fill:none', labelStyle: '', label: item.description, diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 1b2e91da2..0f37186a5 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -27,6 +27,7 @@ import ganttRenderer from './diagrams/gantt/ganttRenderer'; import ganttParser from './diagrams/gantt/parser/gantt'; import ganttDb from './diagrams/gantt/ganttDb'; import classRenderer from './diagrams/class/classRenderer'; +import classRendererV2 from './diagrams/class/classRenderer-v2'; import classParser from './diagrams/class/parser/classDiagram'; import classDb from './diagrams/class/classDb'; import stateRenderer from './diagrams/state/stateRenderer'; @@ -94,6 +95,10 @@ function parse(text) { parser = classParser; parser.parser.yy = classDb; break; + case 'classDiagram': + parser = classParser; + parser.parser.yy = classDb; + break; case 'state': parser = stateParser; parser.parser.yy = stateDb; @@ -342,6 +347,11 @@ const render = function(id, _txt, cb, container) { classRenderer.setConf(cnf.class); classRenderer.draw(txt, id); break; + case 'classDiagram': + cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; + classRendererV2.setConf(cnf.class); + classRendererV2.draw(txt, id); + break; case 'state': cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; stateRenderer.setConf(cnf.state); diff --git a/src/styles.js b/src/styles.js index 810760bdb..06769dde6 100644 --- a/src/styles.js +++ b/src/styles.js @@ -14,7 +14,8 @@ const themes = { 'flowchart-v2': flowchart, sequence, gantt, - class: classDiagram, + classDiagram, + 'classDiagram-v2': classDiagram, stateDiagram, state: stateDiagram, git, diff --git a/src/themes/class.scss b/src/themes/class.scss index db9a3606b..d29e8fbf3 100644 --- a/src/themes/class.scss +++ b/src/themes/class.scss @@ -10,6 +10,11 @@ g.classGroup text { } } +.divider { + stroke: $nodeBorder; + stroke-width: 1; +} + g.clickable { cursor: pointer; } @@ -47,45 +52,45 @@ g.classGroup line { } @mixin composition { - fill: $nodeBorder; - stroke: $nodeBorder; + fill: $nodeBorder !important ; + stroke: $nodeBorder !important ; stroke-width: 1; } -#compositionStart { +#compositionStart, .composition { @include composition; } -#compositionEnd { +#compositionEnd, .composition { @include composition; } @mixin aggregation { - fill: $nodeBkg; - stroke: $nodeBorder; + fill: $nodeBkg !important ; + stroke: $nodeBorder !important ; stroke-width: 1; } -#aggregationStart { +#aggregationStart, .aggregation { @include aggregation; } -#aggregationEnd { +#aggregationEnd, .aggregation { @include aggregation; } -#dependencyStart { +#dependencyStart, .dependency { @include composition; } -#dependencyEnd { +#dependencyEnd, .dependency { @include composition; } -#extensionStart { +#extensionStart , .extension{ @include composition; } -#extensionEnd { +#extensionEnd, .extension { @include composition; } diff --git a/src/utils.js b/src/utils.js index 42342d2c0..8fe374b00 100644 --- a/src/utils.js +++ b/src/utils.js @@ -184,10 +184,13 @@ export const detectType = function(text) { if (text.match(/^\s*gantt/)) { return 'gantt'; } - + if (text.match(/^\s*classDiagram-v2/)) { + return 'classDiagram'; + } if (text.match(/^\s*classDiagram/)) { return 'class'; } + if (text.match(/^\s*stateDiagram-v2/)) { return 'stateDiagram'; }