From f05f07e44f722222615e878d759b74c18af65ac7 Mon Sep 17 00:00:00 2001 From: Dima Kurilo Date: Mon, 10 Oct 2022 20:53:09 -0400 Subject: [PATCH 1/4] add the way to add notes to class diagram --- README.md | 2 + .../mermaid/src/diagrams/class/classDb.js | 17 ++++ .../src/diagrams/class/classDiagram.spec.js | 10 ++ .../src/diagrams/class/classRenderer-v2.js | 95 +++++++++++++++++++ .../src/diagrams/class/classRenderer.js | 32 ++++++- .../diagrams/class/parser/classDiagram.jison | 10 ++ packages/mermaid/src/diagrams/class/styles.js | 4 + .../mermaid/src/diagrams/class/svgDraw.js | 75 ++++++++++++++- packages/mermaid/src/utils.ts | 7 +- 9 files changed, 241 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b30d8d438..e363c7447 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ Class01 <|-- AveryLongClass : Cool Class09 --> C2 : Where am I? Class09 --* C3 Class09 --|> Class07 +note "I love this diagram!\nDo you love it?" Class07 : equals() Class07 : Object[] elementData Class01 : size() @@ -172,6 +173,7 @@ class Class10 { int id size() } +note for Class10 "Cool class\nI said it's very cool class!" ``` ### State diagram [docs - live editor] diff --git a/packages/mermaid/src/diagrams/class/classDb.js b/packages/mermaid/src/diagrams/class/classDb.js index 223bfe067..fd970b902 100644 --- a/packages/mermaid/src/diagrams/class/classDb.js +++ b/packages/mermaid/src/diagrams/class/classDb.js @@ -16,6 +16,7 @@ const MERMAID_DOM_ID_PREFIX = 'classid-'; let relations = []; let classes = {}; +let notes = []; let classCounter = 0; let funs = []; @@ -82,6 +83,7 @@ export const lookUpDomId = function (id) { export const clear = function () { relations = []; classes = {}; + notes = []; funs = []; funs.push(setupToolTips); commonClear(); @@ -98,6 +100,10 @@ export const getRelations = function () { return relations; }; +export const getNotes = function () { + return notes; +}; + export const addRelation = function (relation) { log.debug('Adding relation: ' + JSON.stringify(relation)); addClass(relation.id1); @@ -168,6 +174,15 @@ export const addMembers = function (className, members) { } }; +export const addNote = function (text, className) { + const note = { + id: `note${notes.length}`, + class: className, + text: text, + }; + notes.push(note); +}; + export const cleanupLabel = function (label) { if (label.substring(0, 1) === ':') { return common.sanitizeText(label.substr(1).trim(), configApi.getConfig()); @@ -369,7 +384,9 @@ export default { clear, getClass, getClasses, + getNotes, addAnnotation, + addNote, getRelations, addRelation, getDirection, diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.js b/packages/mermaid/src/diagrams/class/classDiagram.spec.js index 3f47701e6..04a8e9bf3 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.js +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.js @@ -529,6 +529,16 @@ foo() parser.parse(str); }); + + it('should handle "note for"', function () { + const str = 'classDiagram\n' + 'Class11 <|.. Class12\n' + 'note for Class11 "test"\n'; + parser.parse(str); + }); + + it('should handle "note"', function () { + const str = 'classDiagram\n' + 'note "test"\n'; + parser.parse(str); + }); }); describe('when fetching data from a classDiagram graph it', function () { diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.js b/packages/mermaid/src/diagrams/class/classRenderer-v2.js index 20722e6d0..ab556c4ae 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.js +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.js @@ -133,6 +133,99 @@ export const addClasses = function (classes, g, _id, diagObj) { }); }; +/** + * Function that adds the additional vertices (notes) found during parsing to the graph to be rendered. + * + * @param {{text: string; class: string; placement: number}[]} notes + * Object containing the additional vertices (notes). + * @param {SVGGElement} g The graph that is to be drawn. + * @param {number} startEdgeId starting index for note edge + * @param classes + */ +export const addNotes = function (notes, g, startEdgeId, classes) { + log.info(notes); + + // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition + notes.forEach(function (note, i) { + const vertex = note; + + /** + * Variable for storing the classes for the vertex + * + * @type {string} + */ + let cssNoteStr = ''; + + const styles = { labelStyle: '', style: '' }; + + // Use vertex id as text in the box if no text is provided by the graph definition + let vertexText = vertex.text; + + let radious = 0; + let _shape = 'note'; + // Add the node + g.setNode(vertex.id, { + labelStyle: styles.labelStyle, + shape: _shape, + labelText: sanitizeText(vertexText), + noteData: vertex, + rx: radious, + ry: radious, + class: cssNoteStr, + style: styles.style, + id: vertex.id, + domId: vertex.id, + tooltip: '', + type: 'note', + padding: getConfig().flowchart.padding, + }); + + log.info('setNode', { + labelStyle: styles.labelStyle, + shape: _shape, + labelText: vertexText, + rx: radious, + ry: radious, + style: styles.style, + id: vertex.id, + type: 'note', + padding: getConfig().flowchart.padding, + }); + + if (!vertex.class || !(vertex.class in classes)) { + return; + } + const edgeId = startEdgeId + i; + const edgeData = {}; + //Set relationship style and line type + edgeData.classes = 'relation'; + edgeData.pattern = 'dotted'; + + edgeData.id = `edgeNote${edgeId}`; + // Set link type for rendering + edgeData.arrowhead = 'none'; + + log.info(`Note edge: ${JSON.stringify(edgeData)}, ${JSON.stringify(vertex)}`); + //Set edge extra labels + edgeData.startLabelRight = ''; + edgeData.endLabelLeft = ''; + + //Set relation arrow types + edgeData.arrowTypeStart = 'none'; + edgeData.arrowTypeEnd = 'none'; + let style = 'fill:none'; + let labelStyle = ''; + + edgeData.style = style; + edgeData.labelStyle = labelStyle; + + edgeData.curve = interpolateToCurve(conf.curve, curveLinear); + + // Add the edge to the graph + g.setEdge(vertex.id, vertex.class, edgeData, edgeId); + }); +}; + /** * Add edges to graph based on parsed graph definition * @@ -304,10 +397,12 @@ export const draw = function (text, id, _version, diagObj) { // Fetch the vertices/nodes and edges/links from the parsed graph definition const classes = diagObj.db.getClasses(); const relations = diagObj.db.getRelations(); + const notes = diagObj.db.getNotes(); log.info(relations); addClasses(classes, g, id, diagObj); addRelations(relations, g); + addNotes(notes, g, relations.length + 1, classes); // Add custom shapes // flowChartShapes.addToRenderV2(addShape); diff --git a/packages/mermaid/src/diagrams/class/classRenderer.js b/packages/mermaid/src/diagrams/class/classRenderer.js index c1236afea..14fdc5efe 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer.js +++ b/packages/mermaid/src/diagrams/class/classRenderer.js @@ -208,12 +208,42 @@ export const draw = function (text, id, _version, diagObj) { ); }); + const notes = diagObj.db.getNotes(); + notes.forEach(function (note) { + log.debug(`Adding note: ${JSON.stringify(note)}`); + const node = svgDraw.drawNote(diagram, note, conf, diagObj); + 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); + if (note.class && note.class in classes) { + g.setEdge( + note.id, + getGraphId(note.class), + { + relation: { + id1: note.id, + id2: note.class, + relation: { + type1: 'none', + type2: 'none', + lineType: 10, + }, + }, + }, + 'DEFAULT' + ); + } + }); + dagre.layout(g); g.nodes().forEach(function (v) { if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { log.debug('Node ' + v + ': ' + JSON.stringify(g.node(v))); root - .select('#' + diagObj.db.lookUpDomId(v)) + .select('#' + (diagObj.db.lookUpDomId(v) || v)) .attr( 'transform', 'translate(' + diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index ba0e69fba..157e3d7d8 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -56,6 +56,8 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili "callback" return 'CALLBACK'; "link" return 'LINK'; "click" return 'CLICK'; +"note for" return 'NOTE_FOR'; +"note" return 'NOTE'; "<<" return 'ANNOTATION_START'; ">>" return 'ANNOTATION_END'; [~] this.begin("generic"); @@ -263,6 +265,7 @@ statement | annotationStatement | clickStatement | cssClassStatement + | noteStatement | directive | direction | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } @@ -300,6 +303,11 @@ relationStatement | className STR relation STR className { $$ = {id1:$1, id2:$5, relation:$3, relationTitle1:$2, relationTitle2:$4} } ; +noteStatement + : NOTE_FOR className noteText { yy.addNote($3, $2); } + | NOTE noteText { yy.addNote($2); } + ; + relation : relationType lineType relationType { $$={type1:$1,type2:$3,lineType:$2}; } | lineType relationType { $$={type1:'none',type2:$2,lineType:$1}; } @@ -351,4 +359,6 @@ alphaNumToken : UNICODE_TEXT | NUM | ALPHA; classLiteralName : BQUOTE_STR; +noteText : STR; + %% diff --git a/packages/mermaid/src/diagrams/class/styles.js b/packages/mermaid/src/diagrams/class/styles.js index 9e7665c58..bb5580492 100644 --- a/packages/mermaid/src/diagrams/class/styles.js +++ b/packages/mermaid/src/diagrams/class/styles.js @@ -80,6 +80,10 @@ g.classGroup line { stroke-dasharray: 3; } +.dotted-line{ + stroke-dasharray: 1 2; +} + #compositionStart, .composition { fill: ${options.lineColor} !important; stroke: ${options.lineColor} !important; diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js index 9a4dc761e..bcb0d2c3d 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.js @@ -9,13 +9,13 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) { switch (type) { case diagObj.db.relationType.AGGREGATION: return 'aggregation'; - case diagObj.db.EXTENSION: + case diagObj.db.relationType.EXTENSION: return 'extension'; - case diagObj.db.COMPOSITION: + case diagObj.db.relationType.COMPOSITION: return 'composition'; - case diagObj.db.DEPENDENCY: + case diagObj.db.relationType.DEPENDENCY: return 'dependency'; - case diagObj.db.LOLLIPOP: + case diagObj.db.relationType.LOLLIPOP: return 'lollipop'; } }; @@ -55,6 +55,9 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) { if (relation.relation.lineType == 1) { svgPath.attr('class', 'relation dashed-line'); } + if (relation.relation.lineType == 10) { + svgPath.attr('class', 'relation dotted-line'); + } if (relation.relation.type1 !== 'none') { svgPath.attr( 'marker-start', @@ -284,6 +287,69 @@ export const drawClass = function (elem, classDef, conf, diagObj) { return classInfo; }; +/** + * Renders a note diagram + * + * @param {SVGSVGElement} elem The element to draw it into + * @param {{id: string; text: string; class: string;}} note + * @param conf + * @param diagObj + * @todo Add more information in the JSDOC here + */ +export const drawNote = function (elem, note, conf, diagObj) { + log.debug('Rendering note ', note, conf); + + const id = note.id; + const noteInfo = { + id: id, + text: note.text, + width: 0, + height: 0, + }; + + // add class group + const g = elem.append('g').attr('id', id).attr('class', 'classGroup'); + + // add text + let text = g + .append('text') + .attr('y', conf.textHeight + conf.padding) + .attr('x', 0); + + const lines = JSON.parse(`"${note.text}"`).split('\n'); + + lines.forEach(function (line) { + log.debug(`Adding line: ${line}`); + text.append('tspan').text(line).attr('class', 'title').attr('dy', conf.textHeight); + }); + + const noteBox = g.node().getBBox(); + + const rect = g + .insert('rect', ':first-child') + .attr('x', 0) + .attr('y', 0) + .attr('width', noteBox.width + 2 * conf.padding) + .attr( + 'height', + noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin + ); + + const rectWidth = rect.node().getBBox().width; + + // Center title + // We subtract the width of each text element from the class box width and divide it by 2 + text.node().childNodes.forEach(function (x) { + x.setAttribute('x', (rectWidth - x.getBBox().width) / 2); + }); + + noteInfo.width = rectWidth; + noteInfo.height = + noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin; + + return noteInfo; +}; + export const parseMember = function (text) { const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+) *(\*|\$)?$/; const methodRegEx = /^([+|\-|~|#])?(\w+) *\( *(.*)\) *(\*|\$)? *(\w*[~|[\]]*\s*\w*~?)$/; @@ -435,5 +501,6 @@ const parseClassifier = function (classifier) { export default { drawClass, drawEdge, + drawNote, parseMember, }; diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 395e6fe2a..a62c4d3ad 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -306,15 +306,10 @@ const calcLabelPosition = (points) => { const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => { let prevPoint; - log.info('our points', points); + log.info(`our points ${JSON.stringify(points)}`); if (points[0] !== initialPosition) { points = points.reverse(); } - points.forEach((point) => { - totalDistance += distance(point, prevPoint); - prevPoint = point; - }); - // Traverse only 25 total distance along points to find cardinality point const distanceToCardinalityPoint = 25; From d41efa413c65e0db3d9b39d37ec5157dd7f83565 Mon Sep 17 00:00:00 2001 From: Dima Kurilo Date: Mon, 17 Oct 2022 09:58:04 -0400 Subject: [PATCH 2/4] add more docs --- docs/classDiagram.md | 8 ++++++++ packages/mermaid/src/docs/classDiagram.md | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/docs/classDiagram.md b/docs/classDiagram.md index 60dc6c390..f89b4b002 100644 --- a/docs/classDiagram.md +++ b/docs/classDiagram.md @@ -11,7 +11,9 @@ Mermaid can render class diagrams. ```mermaid-example classDiagram + note "From Duck till Zebra" Animal <|-- Duck + note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging" Animal <|-- Fish Animal <|-- Zebra Animal : +int age @@ -35,7 +37,9 @@ classDiagram ```mermaid classDiagram + note "From Duck till Zebra" Animal <|-- Duck + note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging" Animal <|-- Fish Animal <|-- Zebra Animal : +int age @@ -549,6 +553,10 @@ You would define these actions on a separate line after all classes have been de - (_optional_) tooltip is a string to be displayed when hovering over element (note: The styles of the tooltip are set by the class .mermaidTooltip.) - note: callback function will be called with the nodeId as parameter. +## Notes + +It is possible to add notes on digram using `note "line1\nline2"` or note for class using `note for class "line1\nline2"` + ### Examples _URL Link:_ diff --git a/packages/mermaid/src/docs/classDiagram.md b/packages/mermaid/src/docs/classDiagram.md index 362e90bc6..3ca564e55 100644 --- a/packages/mermaid/src/docs/classDiagram.md +++ b/packages/mermaid/src/docs/classDiagram.md @@ -9,7 +9,9 @@ Mermaid can render class diagrams. ```mermaid-example classDiagram + note "From Duck till Zebra" Animal <|-- Duck + note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging" Animal <|-- Fish Animal <|-- Zebra Animal : +int age @@ -375,6 +377,10 @@ click className href "url" "tooltip" - (_optional_) tooltip is a string to be displayed when hovering over element (note: The styles of the tooltip are set by the class .mermaidTooltip.) - note: callback function will be called with the nodeId as parameter. +## Notes + +It is possible to add notes on digram using `note "line1\nline2"` or note for class using `note for class "line1\nline2"` + ### Examples _URL Link:_ From cead1f36f477aef9a2f003e3671e90b2d11df199 Mon Sep 17 00:00:00 2001 From: Dima Kurilo Date: Mon, 17 Oct 2022 12:13:22 -0400 Subject: [PATCH 3/4] add basic render (cypress test for notes --- .../integration/rendering/classDiagram.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index 8cf410d05..16601652d 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -407,4 +407,21 @@ describe('Class diagram', () => { // // expect(svg).to.not.have.attr('style'); // }); // }); + + it('19: should render a simple class diagram with notes', () => { + imgSnapshotTest( + ` + classDiagram + note "I love this diagram!\nDo you love it?" + class Class10 { + <> + int id + size() + } + note for Class10 "Cool class\nI said it's very cool class!" + `, + { logLevel: 1 } + ); + cy.get('svg'); + }); }); From 75e11b1fdeae1e4aa62fdcd4a6ebc752c3418e73 Mon Sep 17 00:00:00 2001 From: Dima Kurilo Date: Mon, 17 Oct 2022 12:33:23 -0400 Subject: [PATCH 4/4] add basic render (cypress) test for classDiagram-v2 too --- .../rendering/classDiagram-v2.spec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index d285a9237..e36693a65 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -478,4 +478,22 @@ describe('Class diagram V2', () => { ); cy.get('svg'); }); + + it('18: should render a simple class diagram with notes', () => { + imgSnapshotTest( + ` + classDiagram-v2 + note "I love this diagram!\nDo you love it?" + class Class10 { + <> + int id + size() + } + note for Class10 "Cool class\nI said it's very cool class!" + + `, + { logLevel: 1, flowchart: { htmlLabels: false } } + ); + cy.get('svg'); + }); });