From 49f8aac6a92e4ce03b8a2b42e7614f238a9983f1 Mon Sep 17 00:00:00 2001 From: Danny Shemesh Date: Thu, 23 Apr 2020 07:23:50 +0300 Subject: [PATCH 1/5] Fixed outdated doc strings This commit fixes some outdated docstrings, replacing the description and @params where necessary. --- src/diagrams/sequence/sequenceRenderer.js | 10 +++++---- src/diagrams/sequence/svgDraw.js | 25 +++++++++++++---------- src/diagrams/state/shapes.js | 7 +++---- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index c297a6526..dee77d889 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -186,10 +186,12 @@ const _drawLongText = (text, x, y, g, width) => { }; /** - * Draws an actor in the diagram with the attaced line - * @param center - The center of the the actor - * @param pos The position if the actor in the liost of actors - * @param description The text in the box + * Draws an note in the diagram with the attaced line + * @param elem - The diagram to draw to. + * @param startx - The x axis start position. + * @param verticalPos - The y axis position. + * @param msg - The message to be drawn. + * @param forceWidth - Set this with a custom width to override the default configured width. */ const drawNote = function(elem, startx, verticalPos, msg, forceWidth) { const rect = svgDraw.getNoteRect(); diff --git a/src/diagrams/sequence/svgDraw.js b/src/diagrams/sequence/svgDraw.js index d8047e403..b30c46d36 100644 --- a/src/diagrams/sequence/svgDraw.js +++ b/src/diagrams/sequence/svgDraw.js @@ -73,9 +73,9 @@ export const drawLabel = function(elem, txtObject) { let actorCnt = -1; /** * Draws an actor in the diagram with the attaced line - * @param center - The center of the the actor - * @param pos The position if the actor in the liost of actors - * @param description The text in the box + * @param elem - The diagram we'll draw to. + * @param actor - The actor to draw. + * @param config - The sequence diagram config object. */ export const drawActor = function(elem, left, verticalPos, description, conf) { const center = left + conf.width / 2; @@ -120,10 +120,12 @@ export const anchorElement = function(elem) { return elem.append('g'); }; /** - * Draws an actor in the diagram with the attaced line - * @param elem - element to append activation rect - * @param bounds - activation box bounds - * @param verticalPos - precise y cooridnate of bottom activation box edge + * Draws an activation in the diagram + * @param elem - element to append activation rect. + * @param bounds - activation box bounds. + * @param verticalPos - precise y cooridnate of bottom activation box edge. + * @param conf - sequence diagram config object. + * @param actorActivations - number of activations on the actor. */ export const drawActivation = function(elem, bounds, verticalPos, conf, actorActivations) { const rect = getNoteRect(); @@ -137,10 +139,11 @@ export const drawActivation = function(elem, bounds, verticalPos, conf, actorAct }; /** - * Draws an actor in the diagram with the attaced line - * @param center - The center of the the actor - * @param pos The position if the actor in the list of actors - * @param description The text in the box + * Draws a loop in the diagram + * @param elem - elemenet to append the loop to. + * @param bounds - bounds of the given loop. + * @param labelText - Text within the loop. + * @param config - sequence diagram config object. */ export const drawLoop = function(elem, bounds, labelText, conf) { const g = elem.append('g'); diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index a436e23b3..3889732e2 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -333,10 +333,9 @@ const _drawLongText = (_text, x, y, g) => { }; /** - * Draws an actor in the diagram with the attaced line - * @param center - The center of the the actor - * @param pos The position if the actor in the liost of actors - * @param description The text in the box + * Draws a note to the diagram + * @param text - The text of the given note. + * @param g - The element the note is attached to. */ export const drawNote = (text, g) => { From 197d006860ed69a023439c6897def996acaf0fa3 Mon Sep 17 00:00:00 2001 From: Danny Shemesh Date: Thu, 23 Apr 2020 07:17:09 +0300 Subject: [PATCH 2/5] Extracted out the commonly used line break regex This commit extract the commonly used /br\s*\/?>/gi regex to common.js, in order to keep the code more DRY. --- src/diagrams/common/common.js | 7 +++++-- src/diagrams/flowchart/flowRenderer-v2.js | 5 +++-- src/diagrams/flowchart/flowRenderer.js | 5 +++-- src/diagrams/gantt/ganttRenderer.js | 3 ++- src/diagrams/sequence/sequenceRenderer.js | 5 +++-- src/diagrams/sequence/svgDraw.js | 6 ++++-- src/diagrams/state/shapes.js | 4 ++-- 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/diagrams/common/common.js b/src/diagrams/common/common.js index cc91948fc..975bd6176 100644 --- a/src/diagrams/common/common.js +++ b/src/diagrams/common/common.js @@ -25,8 +25,10 @@ export const sanitizeText = (text, config) => { return txt; }; +export const lineBreakRegex = //gi; + const breakToPlaceholder = s => { - return s.replace(//gi, '#br#'); + return s.replace(lineBreakRegex, '#br#'); }; const placeholderToBreak = s => { @@ -35,5 +37,6 @@ const placeholderToBreak = s => { export default { getRows, - sanitizeText + sanitizeText, + lineBreakRegex }; diff --git a/src/diagrams/flowchart/flowRenderer-v2.js b/src/diagrams/flowchart/flowRenderer-v2.js index 5e4b2ef87..e382e0174 100644 --- a/src/diagrams/flowchart/flowRenderer-v2.js +++ b/src/diagrams/flowchart/flowRenderer-v2.js @@ -8,6 +8,7 @@ import { getConfig } from '../../config'; import { render } from '../../dagre-wrapper/index.js'; import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js'; import { logger } from '../../logger'; +import common from '../common/common'; import { interpolateToCurve, getStylesFromArray } from '../../utils'; const conf = {}; @@ -61,7 +62,7 @@ export const addVertices = function(vert, g, svgId) { const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:')); - const rows = vertexText.split(//gi); + 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'); @@ -243,7 +244,7 @@ export const addEdges = function(edges, g) { edgeData.label = '' + edge.text + ''; } else { edgeData.labelType = 'text'; - edgeData.label = edge.text.replace(//gi, '\n'); + 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'; diff --git a/src/diagrams/flowchart/flowRenderer.js b/src/diagrams/flowchart/flowRenderer.js index 6f86d638d..bbb8af944 100644 --- a/src/diagrams/flowchart/flowRenderer.js +++ b/src/diagrams/flowchart/flowRenderer.js @@ -8,6 +8,7 @@ import { getConfig } from '../../config'; import dagreD3 from 'dagre-d3'; import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js'; import { logger } from '../../logger'; +import common from '../common/common'; import { interpolateToCurve, getStylesFromArray } from '../../utils'; import flowChartShapes from './flowChartShapes'; @@ -62,7 +63,7 @@ export const addVertices = function(vert, g, svgId) { const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:')); - const rows = vertexText.split(//gi); + 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'); @@ -225,7 +226,7 @@ export const addEdges = function(edges, g) { edgeData.label = '' + edge.text + ''; } else { edgeData.labelType = 'text'; - edgeData.label = edge.text.replace(//gi, '\n'); + 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'; diff --git a/src/diagrams/gantt/ganttRenderer.js b/src/diagrams/gantt/ganttRenderer.js index 825b7dce8..b12fd5b5e 100644 --- a/src/diagrams/gantt/ganttRenderer.js +++ b/src/diagrams/gantt/ganttRenderer.js @@ -1,6 +1,7 @@ import * as d3 from 'd3'; import { parser } from './parser/gantt'; +import common from '../common/common'; import ganttDb from './ganttDb'; parser.yy = ganttDb; @@ -358,7 +359,7 @@ export const draw = function(text, id) { .data(numOccurances) .enter() .append(function(d) { - const rows = d[0].split(//gi); + const rows = d[0].split(common.lineBreakRegex); const dy = -(rows.length - 1) / 2; const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index dee77d889..db122e1f0 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -3,6 +3,7 @@ import * as d3 from 'd3'; import svgDraw from './svgDraw'; import { logger } from '../../logger'; import { parser } from './parser/sequenceDiagram'; +import common from '../common/common'; import sequenceDb from './sequenceDb'; parser.yy = sequenceDb; @@ -170,7 +171,7 @@ export const bounds = { const _drawLongText = (text, x, y, g, width) => { let textHeight = 0; - const lines = text.split(//gi); + const lines = text.split(common.lineBreakRegex); for (const line of lines) { const textObj = svgDraw.getTextObj(); textObj.x = x; @@ -237,7 +238,7 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde let textElems = []; let counterBreaklines = 0; let breaklineOffset = 17; - const breaklines = msg.message.split(//gi); + const breaklines = msg.message.split(common.lineBreakRegex); for (const breakline of breaklines) { textElems.push( g diff --git a/src/diagrams/sequence/svgDraw.js b/src/diagrams/sequence/svgDraw.js index b30c46d36..6e4af6e2e 100644 --- a/src/diagrams/sequence/svgDraw.js +++ b/src/diagrams/sequence/svgDraw.js @@ -1,3 +1,5 @@ +import common from '../common/common'; + export const drawRect = function(elem, rectData) { const rectElem = elem.append('rect'); rectElem.attr('x', rectData.x); @@ -18,7 +20,7 @@ export const drawRect = function(elem, rectData) { export const drawText = function(elem, textData) { // Remove and ignore br:s - const nText = textData.text.replace(//gi, ' '); + const nText = textData.text.replace(common.lineBreakRegex, ' '); const textElem = elem.append('text'); textElem.attr('x', textData.x); @@ -324,7 +326,7 @@ const _drawTextCandidateFunc = (function() { function byTspan(content, g, x, y, width, height, textAttrs, conf) { const { actorFontSize, actorFontFamily } = conf; - const lines = content.split(//gi); + const lines = content.split(common.lineBreakRegex); for (let i = 0; i < lines.length; i++) { const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2; const text = g diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index 3889732e2..0893b40d6 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -282,7 +282,7 @@ const drawForkJoinState = (g, stateDef) => { export const drawText = function(elem, textData) { // Remove and ignore br:s - const nText = textData.text.replace(//gi, ' '); + const nText = textData.text.replace(common.lineBreakRegex, ' '); const textElem = elem.append('text'); textElem.attr('x', textData.x); @@ -310,7 +310,7 @@ const _drawLongText = (_text, x, y, g) => { let text = _text.replace(/\r\n/g, '
'); text = text.replace(/\n/g, '
'); - const lines = text.split(//gi); + const lines = text.split(common.lineBreakRegex); let tHeight = 1.25 * getConfig().state.noteMargin; for (const line of lines) { From 5f6887b3164b0aae6c185463d4b68e27b0bf98a3 Mon Sep 17 00:00:00 2001 From: Danny Shemesh Date: Thu, 23 Apr 2020 07:37:32 +0300 Subject: [PATCH 3/5] Sequence diagram fixes & improvements This commit fixes some bugs, and I believe, improves upon the current implementation. In no particular order, it adds: 1. Control over note font size, family and alignment (now defaults to center) 2. Dynamic actor resizing - actor's width will now scale if its description is bigger than the static configured width 3. Dynamic actor margins - the margin between actors will now be dynamically calculated by taking into account the width of connecting messages or notes 4. Fixed a small visual annoyance where a loop arrow would intersect with the text it loops on 5. Fixed a bug where if global config -> fontFamily wasn't defined, it would override the actorFontFamily with an undefined 6. Removed some stale / commented out code 7. Added missing config variables to the global config object in mermaidAPI.js 8. Added messageFontSize, messageFontFamily to control message (non-note) font settings 9. Memoized the actor widths in a pre-calculation that takes notes and signals lengths into account 10. Removed redundant console.log lines 11. Extracted out actor width & margin calculation to getMaxMessageWidthPerActor, and calculateActorMargins --- src/diagrams/sequence/sequenceRenderer.js | 329 ++++++++++++++++++++-- src/diagrams/sequence/svgDraw.js | 17 +- src/mermaidAPI.js | 37 ++- 3 files changed, 344 insertions(+), 39 deletions(-) diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index db122e1f0..18177516d 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -19,6 +19,13 @@ const conf = { height: 65, actorFontSize: 14, actorFontFamily: '"Open-Sans", "sans-serif"', + // Note font settings + noteFontSize: 14, + noteFontFamily: '"trebuchet ms", verdana, arial', + noteAlign: 'center', + // Message font settings + messageFontSize: 16, + messageFontFamily: '"trebuchet ms", verdana, arial', // Margin around loop boxes boxMargin: 10, boxTextMargin: 5, @@ -171,18 +178,52 @@ export const bounds = { const _drawLongText = (text, x, y, g, width) => { let textHeight = 0; + let prevTextHeight = 0; + + const alignmentToAnchor = { + left: 'start', + start: 'start', + center: 'middle', + middle: 'middle', + right: 'end', + end: 'end' + }; + const lines = text.split(common.lineBreakRegex); for (const line of lines) { const textObj = svgDraw.getTextObj(); - textObj.x = x; + const alignment = alignmentToAnchor[conf.noteAlign] || 'middle'; + + switch (alignment) { + case 'start': + textObj.x = x + conf.noteMargin; + break; + case 'middle': + textObj.x = x + width / 2; + break; + case 'end': + textObj.x = x + width - conf.noteMargin; + break; + } + textObj.y = y + textHeight; - textObj.textMargin = conf.noteMargin; textObj.dy = '1em'; textObj.text = line; textObj.class = 'noteText'; - const textElem = svgDraw.drawText(g, textObj, width); + + const textElem = svgDraw + .drawText(g, textObj) + .style('text-anchor', alignment) + .style('font-size', conf.noteFontSize) + .style('font-family', conf.noteFontFamily) + .attr('dominant-baseline', 'central') + .attr('alignment-baseline', 'central'); + textHeight += (textElem._groups || textElem)[0][0].getBBox().height; + textElem.attr('y', y + (prevTextHeight + textHeight + 2 * conf.noteMargin) / 2); + prevTextHeight = textHeight; } + return textHeight; }; @@ -204,13 +245,7 @@ const drawNote = function(elem, startx, verticalPos, msg, forceWidth) { let g = elem.append('g'); const rectElem = svgDraw.drawRect(g, rect); - const textHeight = _drawLongText( - msg.message, - startx - 4, - verticalPos + 24, - g, - rect.width - conf.noteMargin - ); + const textHeight = _drawLongText(msg.message, startx, verticalPos, g, rect.width); bounds.insert( startx, @@ -218,6 +253,7 @@ const drawNote = function(elem, startx, verticalPos, msg, forceWidth) { startx + rect.width, verticalPos + 2 * conf.noteMargin + textHeight ); + rectElem.attr('height', textHeight + 2 * conf.noteMargin); bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin); }; @@ -245,6 +281,8 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde .append('text') // text label for the x axis .attr('x', txtCenter) .attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset) + .style('font-size', conf.messageFontSize) + .style('font-family', conf.messageFontFamily) .style('text-anchor', 'middle') .attr('class', 'messageText') .text(breakline.trim()) @@ -252,7 +290,7 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde counterBreaklines++; } const offsetLineCounter = counterBreaklines - 1; - const totalOffset = offsetLineCounter * breaklineOffset; + let totalOffset = offsetLineCounter * breaklineOffset; let textWidths = textElems.map(function(textElem) { return (textElem._groups || textElem)[0][0].getBBox().width; @@ -280,6 +318,8 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde totalOffset} H ${startx}` ); } else { + totalOffset += 5; + line = g .append('path') .attr( @@ -377,18 +417,26 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde export const drawActors = function(diagram, actors, actorKeys, verticalPos) { // Draw the actors + let prevWidth = 0; + let prevMargin = 0; + for (let i = 0; i < actorKeys.length; i++) { - const key = actorKeys[i]; + const actor = actors[actorKeys[i]]; // Add some rendering data to the object - actors[key].x = i * conf.actorMargin + i * conf.width; - actors[key].y = verticalPos; - actors[key].width = conf.diagramMarginX; - actors[key].height = conf.diagramMarginY; + actor.width = actor.width || calculateActorWidth(actor); + actor.height = conf.height; + actor.margin = actor.margin || conf.actorMargin; + + actor.x = prevWidth + prevMargin; + actor.y = verticalPos; // Draw the box with the attached line - svgDraw.drawActor(diagram, actors[key].x, verticalPos, actors[key].description, conf); - bounds.insert(actors[key].x, verticalPos, actors[key].x + conf.width, conf.height); + svgDraw.drawActor(diagram, actor, conf); + bounds.insert(actor.x, verticalPos, actor.x + actor.width, conf.height); + + prevWidth += actor.width; + prevMargin += actor.margin; } // Add a margin between the actor boxes and the first arrow @@ -401,7 +449,10 @@ export const setConf = function(cnf) { keys.forEach(function(key) { conf[key] = cnf[key]; }); - conf.actorFontFamily = cnf.fontFamily; + + if (cnf.fontFamily) { + conf.actorFontFamily = conf.noteFontFamily = cnf.fontFamily; + } }; const actorActivations = function(actor) { @@ -412,18 +463,89 @@ const actorActivations = function(actor) { const actorFlowVerticaBounds = function(actor) { // handle multiple stacked activations for same actor - const actors = parser.yy.getActors(); + const actorObj = parser.yy.getActors()[actor]; const activations = actorActivations(actor); const left = activations.reduce(function(acc, activation) { return Math.min(acc, activation.startx); - }, actors[actor].x + conf.width / 2); + }, actorObj.x + actorObj.width / 2); const right = activations.reduce(function(acc, activation) { return Math.max(acc, activation.stopx); - }, actors[actor].x + conf.width / 2); + }, actorObj.x + actorObj.width / 2); return [left, right]; }; +/** + * This calculates the actor's width, taking into account both the statically configured width, + * and the actor's description. + * + * If the description text has greater length, we extend the width of the actor, so it's description + * won't overflow. + * + * @param actor - An actor object + * @return - The width for the given actor + */ +const calculateActorWidth = function(actor) { + if (!actor.description) { + return conf.width; + } + + return Math.max( + conf.width, + calculateTextWidth(actor.description, conf.actorFontSize, conf.actorFontFamily) + ); +}; + +/** + * This calculates the width of the given text, font size and family. + * + * @param text - The text to calculate the width of + * @param fontSize - The font size of the given text + * @param fontFamily - The font family (one, or more fonts) to render + */ +export const calculateTextWidth = function(text, fontSize, fontFamily) { + if (!text) { + return 0; + } + + fontSize = fontSize ? fontSize : conf.actorFontSize; + fontFamily = fontFamily ? fontFamily : conf.actorFontFamily; + + // We can't really know if the user supplied font family will render on the user agent; + // thus, we'll take the max width between the user supplied font family, and a default + // of sans-serif. + const fontFamilies = ['sans-serif', fontFamily]; + const lines = text.split(common.lineBreakRegex); + let maxWidth = 0; + + const body = d3.select('body'); + // We don'y want to leak DOM elements - if a removal operation isn't available + // for any reason, do not continue. + if (!body.remove) { + return 0; + } + + const g = body.append('svg'); + + for (let line of lines) { + for (let fontFamily of fontFamilies) { + const textObj = svgDraw.getTextObj(); + textObj.text = line; + const textElem = svgDraw + .drawText(g, textObj) + .style('font-size', fontSize) + .style('font-family', fontFamily); + + maxWidth = Math.max(maxWidth, (textElem._groups || textElem)[0][0].getBBox().width); + } + } + + g.remove(); + + // Adds some padding, so the text won't sit exactly within the actor's borders + return maxWidth + 35; +}; + /** * Draws a flowchart in the tag with id: id based on the graph definition in text. * @param text @@ -445,6 +567,10 @@ export const draw = function(text, id) { const actorKeys = parser.yy.getActorKeys(); const messages = parser.yy.getMessages(); const title = parser.yy.getTitle(); + + const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages); + calculateActorMargins(actors, maxMessageWidthPerActor); + drawActors(diagram, actors, actorKeys, 0); // The arrow head definition is attached to the svg once @@ -469,12 +595,15 @@ export const draw = function(text, id) { bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos); } - // const lastMsg - // Draw the messages/signals let sequenceIndex = 1; messages.forEach(function(msg) { let loopData; + const noteWidth = Math.max( + conf.width, + calculateTextWidth(msg.message, conf.noteFontSize, conf.noteFontFamily) + ); + switch (msg.type) { case parser.yy.LINETYPE.NOTE: bounds.bumpVerticalPos(conf.boxMargin); @@ -485,26 +614,35 @@ export const draw = function(text, id) { if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) { drawNote( diagram, - startx + (conf.width + conf.actorMargin) / 2, + startx + (actors[msg.from].width + conf.actorMargin) / 2, bounds.getVerticalPos(), - msg + msg, + noteWidth ); } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) { drawNote( diagram, - startx - (conf.width + conf.actorMargin) / 2, + startx - noteWidth + (actors[msg.from].width - conf.actorMargin) / 2, bounds.getVerticalPos(), - msg + msg, + noteWidth ); } else if (msg.to === msg.from) { // Single-actor over - drawNote(diagram, startx, bounds.getVerticalPos(), msg); + drawNote( + diagram, + startx + (actors[msg.to].width - noteWidth) / 2, + bounds.getVerticalPos(), + msg, + noteWidth + ); } else { // Multi-actor over forceWidth = Math.abs(startx - stopx) + conf.actorMargin; + drawNote( diagram, - (startx + stopx + conf.width - forceWidth) / 2, + (startx + stopx + noteWidth - forceWidth) / 2, bounds.getVerticalPos(), msg, forceWidth @@ -670,6 +808,137 @@ export const draw = function(text, id) { ); }; +/** + * Retrieves the max message width of each actor, supports signals (messages, loops) + * and notes. + * + * It will enumerate each given message, and will determine its text width, in relation + * to the actor it originates from, and destined to. + * + * @param actors - The actors map + * @param messages - A list of message objects to iterate + */ +const getMaxMessageWidthPerActor = function(actors, messages) { + const maxMessageWidthPerActor = {}; + + messages.forEach(function(msg) { + if (actors[msg.to] && actors[msg.from]) { + const actor = actors[msg.to]; + + // If this is the first actor, and the message is left of it, no need to calculate the margin + if (msg.placement == parser.yy.PLACEMENT.LEFTOF && !actor.prevActor) { + return; + } + + // If this is the last actor, and the message is right of it, no need to calculate the margin + if (msg.placement == parser.yy.PLACEMENT.RIGHTOF && !actor.nextActor) { + return; + } + + const isNote = msg.placement !== undefined; + const isMessage = !isNote; + + const fontSize = isNote ? conf.noteFontSize : conf.messageFontSize; + const fontFamily = isNote ? conf.noteFontFamily : conf.messageFontFamily; + const messageWidth = calculateTextWidth(msg.message, fontSize, fontFamily); + + /* + * The following scenarios should be supported: + * + * - There's a message (non-note) between fromActor and toActor + * - If fromActor is on the right and toActor is on the left, we should + * define the toActor's margin + * - If fromActor is on the left and toActor is on the right, we should + * define the fromActor's margin + * - There's a note, in which case fromActor == toActor + * - If the note is to the left of the actor, we should define the previous actor + * margin + * - If the note is on the actor, we should define both the previous and next actor + * margins, each being the half of the note size + * - If the note is on the right of the actor, we should define the current actor + * margin + */ + if (isMessage && msg.from == actor.nextActor) { + maxMessageWidthPerActor[msg.to] = Math.max( + maxMessageWidthPerActor[msg.to] || 0, + messageWidth + ); + } else if ( + (isMessage && msg.from == actor.prevActor) || + msg.placement == parser.yy.PLACEMENT.RIGHTOF + ) { + maxMessageWidthPerActor[msg.from] = Math.max( + maxMessageWidthPerActor[msg.from] || 0, + messageWidth + ); + } else if (msg.placement == parser.yy.PLACEMENT.LEFTOF) { + maxMessageWidthPerActor[actor.prevActor] = Math.max( + maxMessageWidthPerActor[actor.prevActor] || 0, + messageWidth + ); + } else if (msg.placement == parser.yy.PLACEMENT.OVER) { + if (actor.prevActor) { + maxMessageWidthPerActor[actor.prevActor] = Math.max( + maxMessageWidthPerActor[actor.prevActor] || 0, + messageWidth / 2 + ); + } + + if (actor.nextActor) { + maxMessageWidthPerActor[msg.from] = Math.max( + maxMessageWidthPerActor[msg.from] || 0, + messageWidth / 2 + ); + } + } + } + }); + + return maxMessageWidthPerActor; +}; + +/** + * This will calculate the optimal margin for each given actor, for a given + * actor->messageWidth map. + * + * An actor's margin is determined by the width of the actor, the width of the + * largest message that originates from it, and the configured conf.actorMargin. + * + * @param actors - The actors map to calculate margins for + * @param actorToMessageWidth - A map of actor key -> max message width it holds + */ +const calculateActorMargins = function(actors, actorToMessageWidth) { + for (let actorKey in actorToMessageWidth) { + const actor = actors[actorKey]; + + if (!actor) { + continue; + } + + const nextActor = actors[actor.nextActor]; + + // No need to space out an actor that doesn't have a next link + if (!nextActor) { + continue; + } + + actor.width = Math.max( + conf.width, + calculateTextWidth(actor.description, conf.actorFontSize, conf.actorFontFamily) + ); + + nextActor.width = Math.max( + conf.width, + calculateTextWidth(nextActor.description, conf.actorFontSize, conf.actorFontFamily) + ); + + const messageWidth = actorToMessageWidth[actorKey]; + const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2; + + actor.margin = Math.max(actorWidth, conf.actorMargin); + } +}; + export default { bounds, drawActors, diff --git a/src/diagrams/sequence/svgDraw.js b/src/diagrams/sequence/svgDraw.js index 6e4af6e2e..a4dfd3694 100644 --- a/src/diagrams/sequence/svgDraw.js +++ b/src/diagrams/sequence/svgDraw.js @@ -79,10 +79,11 @@ let actorCnt = -1; * @param actor - The actor to draw. * @param config - The sequence diagram config object. */ -export const drawActor = function(elem, left, verticalPos, description, conf) { - const center = left + conf.width / 2; +export const drawActor = function(elem, actor, conf) { + const center = actor.x + actor.width / 2; + const g = elem.append('g'); - if (verticalPos === 0) { + if (actor.y === 0) { actorCnt++; g.append('line') .attr('id', 'actor' + actorCnt) @@ -96,18 +97,18 @@ export const drawActor = function(elem, left, verticalPos, description, conf) { } const rect = getNoteRect(); - rect.x = left; - rect.y = verticalPos; + rect.x = actor.x; + rect.y = actor.y; rect.fill = '#eaeaea'; - rect.width = conf.width; - rect.height = conf.height; + rect.width = actor.width; + rect.height = actor.height; rect.class = 'actor'; rect.rx = 3; rect.ry = 3; drawRect(g, rect); _drawTextCandidateFunc(conf)( - description, + actor.description, g, rect.x, rect.y, diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 9dd4c77ab..4035c4bb3 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -270,7 +270,42 @@ const config = { * This will show the node numbers * **Default value false**. */ - showSequenceNumbers: false + showSequenceNumbers: false, + /** + * This sets the font size of the actor's description + * **Default value 14**. + */ + actorFontSize: 14, + /** + * This sets the font family of the actor's description + * **Default value "Open-Sans", "sans-serif"**. + */ + actorFontFamily: '"Open-Sans", "sans-serif"', + /** + * This sets the font size of actor-attached notes. + * **Default value 14**. + */ + noteFontSize: 14, + /** + * This sets the font family of actor-attached notes. + * **Default value "trebuchet ms", verdana, arial**. + */ + noteFontFamily: '"trebuchet ms", verdana, arial', + /** + * This sets the text alignment of actor-attached notes. + * **Default value center**. + */ + noteAlign: 'center', + /** + * This sets the font size of actor messages. + * **Default value 16**. + */ + messageFontSize: 16, + /** + * This sets the font family of actor messages. + * **Default value "trebuchet ms", verdana, arial**. + */ + messageFontFamily: '"trebuchet ms", verdana, arial' }, /** From 862f20ef20e88392e40eca839373d9e932c4cfc0 Mon Sep 17 00:00:00 2001 From: Danny Shemesh Date: Thu, 23 Apr 2020 18:31:22 +0300 Subject: [PATCH 4/5] Added cypress e2e tests for suggested improvements This commits adds e2e tests for the suggested improvements. I've went over the generated screenshots and they look good to me. --- .../rendering/sequencediagram.spec.js | 138 +++++++++++++++++- src/diagrams/sequence/sequenceDb.js | 2 +- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index cf65485c7..c5925fa6a 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -52,6 +52,138 @@ context('Sequence diagram', () => { {} ); }); + it('should render loops with a slight margin', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + loop Loopy + Bob->>Alice: Pasten + end `, + {} + ); + }); + context('font settings', () => { + it('should render different note fonts when configured', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: I'm short + note left of Alice: I should have bigger fonts + Bob->>Alice: Short as well + `, + { sequence: { noteFontSize: 18, noteFontFamily: 'Arial' } } + ); + }); + it('should render different message fonts when configured', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: I'm short + Bob->>Alice: Short as well + `, + { sequence: { messageFontSize: 18, messageFontFamily: 'Arial' } } + ); + }); + it('should render different actor fonts when configured', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: I'm short + Bob->>Alice: Short as well + `, + { sequence: { actorFontSize: 18, actorFontFamily: 'Arial' } } + ); + }); + it('should render notes aligned to the left when configured', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: I'm short + note left of Alice: I am left aligned + Bob->>Alice: Short as well + `, + { sequence: { noteAlign: 'left' } } + ); + }); + it('should render notes aligned to the right when configured', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: I'm short + note left of Alice: I am right aligned + Bob->>Alice: Short as well + `, + { sequence: { noteAlign: 'right' } } + ); + }); + }); + context('auth width scaling', () => { + it('should render long actor descriptions', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant A as Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + A->>Bob: Hola + Bob-->A: Pasten ! + `, + {} + ); + }); + it('should render long notes left of actor', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: Hola + Note left of Alice: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + it('should render long notes right of actor', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: Hola + Note right of Alice: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + it('should render long notes over actor', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: Hola + Note over Alice: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + it('should render long messages from an actor to the left to one to the right', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + it('should render long messages from an actor to the right to one to the left', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice->>Bob: I'm short + Bob->>Alice: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + }); context('background rects', () => { it('should render a single and nested rects', () => { imgSnapshotTest( @@ -162,7 +294,7 @@ context('Sequence diagram', () => { John->>Bob: How about you? Bob-->>John: Jolly good! `, - {sequence: { actorMargin: 50, showSequenceNumbers: true }} + { sequence: { actorMargin: 50, showSequenceNumbers: true } } ); }); it('should render autonumber when autonumber keyword is used', () => { @@ -179,7 +311,7 @@ context('Sequence diagram', () => { John->>Bob: How about you? Bob-->>John: Jolly good! `, - {} + {} ); }); it('should render autonumber with different line breaks', () => { @@ -192,7 +324,7 @@ context('Sequence diagram', () => { John-->>Alice: Hi Alice,
I can hear you! John-->>Alice: I feel great! `, - {} + {} ); }); }); diff --git a/src/diagrams/sequence/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js index 038073faa..4676eee28 100644 --- a/src/diagrams/sequence/sequenceDb.js +++ b/src/diagrams/sequence/sequenceDb.js @@ -17,7 +17,7 @@ export const addActor = function(id, name, description) { actors[id] = { name: name, description: description, prevActor: prevActor }; if (prevActor && actors[prevActor]) { - actors[prevActor].nextActor = actors[id]; + actors[prevActor].nextActor = id; } prevActor = id; From 38ee4f556e4e9d9d85307c1a9a73defee7ddc2cd Mon Sep 17 00:00:00 2001 From: Danny Shemesh Date: Thu, 23 Apr 2020 18:39:23 +0300 Subject: [PATCH 5/5] Added (& pretty-printed) sequence diagram docs This commit adds documentation to new config variables that were introduced as part of this PR, namely, the font settings for actors, messages and notes. I've also linted the markdown document. --- docs/sequenceDiagram.md | 147 +++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 63 deletions(-) diff --git a/docs/sequenceDiagram.md b/docs/sequenceDiagram.md index bc8262ebe..16948a9cb 100755 --- a/docs/sequenceDiagram.md +++ b/docs/sequenceDiagram.md @@ -9,6 +9,7 @@ sequenceDiagram Alice->>John: Hello John, how are you? John-->>Alice: Great! ``` + ```mermaid sequenceDiagram Alice->>John: Hello John, how are you? @@ -31,6 +32,7 @@ sequenceDiagram Alice->>John: Hello John, how are you? John-->>Alice: Great! ``` + ```mermaid sequenceDiagram participant John @@ -50,6 +52,7 @@ sequenceDiagram A->>J: Hello John, how are you? J->>A: Great! ``` + ```mermaid sequenceDiagram participant A as Alice @@ -68,14 +71,14 @@ Messages can be of two displayed either solid or with a dotted line. There are six types of arrows currently supported: -Type | Description ---- | --- --> | Solid line without arrow ---> | Dotted line without arrow -->> | Solid line with arrowhead --->> | Dotted line with arrowhead --x | Solid line with a cross at the end (async) ---x | Dotted line with a cross at the end (async) +| Type | Description | +| ---- | ------------------------------------------- | +| -> | Solid line without arrow | +| --> | Dotted line without arrow | +| ->> | Solid line with arrowhead | +| -->> | Dotted line with arrowhead | +| -x | Solid line with a cross at the end (async) | +| --x | Dotted line with a cross at the end (async) | ## Activations @@ -88,6 +91,7 @@ sequenceDiagram John-->>Alice: Great! deactivate John ``` + ```mermaid sequenceDiagram Alice->>John: Hello John, how are you? @@ -103,6 +107,7 @@ sequenceDiagram Alice->>+John: Hello John, how are you? John-->>-Alice: Great! ``` + ```mermaid sequenceDiagram Alice->>+John: Hello John, how are you? @@ -118,6 +123,7 @@ sequenceDiagram John-->>-Alice: Hi Alice, I can hear you! John-->>-Alice: I feel great! ``` + ```mermaid sequenceDiagram Alice->>+John: Hello John, how are you? @@ -138,6 +144,7 @@ sequenceDiagram participant John Note right of John: Text in note ``` + ```mermaid sequenceDiagram participant John @@ -151,6 +158,7 @@ sequenceDiagram Alice->John: Hello John, how are you? Note over Alice,John: A typical interaction ``` + ```mermaid sequenceDiagram Alice->John: Hello John, how are you? @@ -176,6 +184,7 @@ sequenceDiagram John-->Alice: Great! end ``` + ```mermaid sequenceDiagram Alice->John: Hello John, how are you? @@ -218,6 +227,7 @@ sequenceDiagram Bob->>Alice: Thanks for asking end ``` + ```mermaid sequenceDiagram Alice->>Bob: Hello Bob, how are you? @@ -281,11 +291,13 @@ sequenceDiagram It is possible to highlight flows by providing colored background rects. This is done by the notation The colors are defined using rgb and rgba syntax. + ``` rect rgb(0, 255, 0) ... content ... end ``` + ``` rect rgba(0, 0, 255, .1) ... content ... @@ -327,6 +339,7 @@ sequenceDiagram ## sequenceNumbers It is possible to get a sequence number attached to each arrow in a sequence diagram. This can be configured when adding mermaid to the website as shown below: + ```