From fbac4c61bb64c290b98e531fdd34b4d65e95a2b6 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 5 Feb 2025 19:27:32 -0500 Subject: [PATCH 01/23] diagram adjusts with legend width changes Co-authored-by: Pranav Mishra --- .../src/diagrams/user-journey/journeyRenderer.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 13eb31a02..0860c4753 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -13,10 +13,12 @@ export const setConf = function (cnf) { }; const actors = {}; +let maxWidth = 0; /** @param diagram - The diagram to draw to. */ function drawActorLegend(diagram) { const conf = getConfig().journey; + maxWidth = 0; // Draw the actors let yPos = 60; Object.keys(actors).forEach((person) => { @@ -39,14 +41,19 @@ function drawActorLegend(diagram) { text: person, textMargin: conf.boxTextMargin | 5, }; - svgDraw.drawText(diagram, labelData); + const textElement = svgDraw.drawText(diagram, labelData); + const bbox = textElement.node().getBBox(); + const textLength = bbox.width; + if (textLength > maxWidth) { + maxWidth = textLength; + } yPos += 20; }); } // TODO: Cleanup? const conf = getConfig().journey; -const LEFT_MARGIN = conf.leftMargin; +let LEFT_MARGIN = 0; export const draw = function (text, id, version, diagObj) { const conf = getConfig().journey; @@ -84,6 +91,7 @@ export const draw = function (text, id, version, diagObj) { }); drawActorLegend(diagram); + LEFT_MARGIN = conf.leftMargin + maxWidth - 80; bounds.insert(0, 0, LEFT_MARGIN, Object.keys(actors).length * 50); drawTasks(diagram, tasks, 0); From 5366e8b69251493d107dda743bb330536a738929 Mon Sep 17 00:00:00 2001 From: pranavm2109 Date: Wed, 12 Feb 2025 15:19:37 -0500 Subject: [PATCH 02/23] added first draft of cypress visual tests Co-authored-by: Shahir Ahmed --- cypress/integration/rendering/journey.spec.js | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index d8bef6d1b..8e37c8669 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -47,7 +47,7 @@ section Checkout from website const style = svg.attr('style'); expect(style).to.match(/^max-width: [\d.]+px;$/); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); - expect(maxWidthValue).to.eq(700); + //expect(maxWidthValue).to.eq(700); }); }); @@ -63,4 +63,82 @@ section Checkout from website { journey: { useMaxWidth: false } } ); }); + + it('should maintain consistent distance between widest legend label and diagram', () => { + renderGraph( + `journey + title Web hook life cycle + section Darkoob + Make preBuilt:5: Darkoob user + register slug : 5: Darkoob userf + Map slug to a Prebuilt Job:5: Darkoob user + section External Service + set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty + listen to the events : 5 : External Service + call darkoob endpoint : 5 : External Service + section Darkoob + check for inputs : 5 : DarkoobAPI + run the prebuilt job : 5 : DarkoobAPI + `, + { journey: { useMaxWidth: true } } + ); + + let rightEdgeXInitial, leftEdgeXInitial, rightEdgeXFinal, leftEdgeXFinal, initialDifference, finalDifference; + + cy.contains('tspan', 'admin Exjjjnjjjj qwerty') + .invoke('getBBox') + .then((bbox) => { + rightEdgeXInitial = bbox.x + bbox.width; + cy.log(`Right edge x-coordinate: ${rightEdgeXInitial}`); + }); + + cy.contains('div.label', 'Make preBuilt') + .invoke('getBoundingClientRect') + .then((rect) => { + leftEdgeXInitial = rect.left; + cy.log(`Left edge x-coordinate: ${leftEdgeXInitial}`); + initialDifference = leftEdgeXInitial - rightEdgeXInitial; + cy.log(`Initial Difference: ${initialDifference}`); + }); + + // renderGraph( + // `journey + // title Web hook life cycle + // section Darkoob + // Make preBuilt:5: Darkoob user + // register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained + // Map slug to a Prebuilt Job:5: Darkoob user + // section External Service + // set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty + // listen to the events : 5 : External Service + // call darkoob endpoint : 5 : External Service + // section Darkoob + // check for inputs : 5 : DarkoobAPI + // run the prebuilt job : 5 : DarkoobAPI + // `, + // { journey: { useMaxWidth: true } } + // ); + + // cy.contains('tspan', 'Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained') + // .invoke('getBBox') + // .then((bbox) => { + // rightEdgeXFinal = bbox.x + bbox.width; + // cy.log(`Right edge x-coordinate final: ${rightEdgeXFinal}`); + // }); + + // cy.contains('div.label', 'Make preBuilt') + // .invoke('getBoundingClientRect') + // .then((rect) => { + // leftEdgeXFinal = rect.left; + // cy.log(`Left edge x-coordinate final: ${leftEdgeXFinal}`); + // finalDifference = leftEdgeXFinal - rightEdgeXFinal; + // cy.log(`Final Difference: ${finalDifference}`); + // }); + + // expect(initialDifference).toEqual(finalDifference); + + + }); + + }); From db4ea020ba7e9e57493b00b2236189f7fb7e36d9 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 12 Feb 2025 18:39:03 -0500 Subject: [PATCH 03/23] testing Co-authored-by: Pranav Mishra --- cypress/integration/rendering/journey.spec.js | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 8e37c8669..a89e613e1 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -83,50 +83,53 @@ section Checkout from website { journey: { useMaxWidth: true } } ); - let rightEdgeXInitial, leftEdgeXInitial, rightEdgeXFinal, leftEdgeXFinal, initialDifference, finalDifference; + let rightEdgeXInitial, + leftEdgeXInitial, + rightEdgeXFinal, + leftEdgeXFinal, + initialDifference, + finalDifference; - cy.contains('tspan', 'admin Exjjjnjjjj qwerty') - .invoke('getBBox') - .then((bbox) => { - rightEdgeXInitial = bbox.x + bbox.width; - cy.log(`Right edge x-coordinate: ${rightEdgeXInitial}`); - }); + cy.contains('tspan', 'admin Exjjjnjjjj qwerty').then((textBox) => { + const bbox = textBox[0].getBBox(); + const rightEdge = bbox.x + bbox.width; + console.warn(rightEdge); + }); - cy.contains('div.label', 'Make preBuilt') - .invoke('getBoundingClientRect') - .then((rect) => { - leftEdgeXInitial = rect.left; - cy.log(`Left edge x-coordinate: ${leftEdgeXInitial}`); - initialDifference = leftEdgeXInitial - rightEdgeXInitial; - cy.log(`Initial Difference: ${initialDifference}`); - }); + cy.get(':nth-child(14) > switch > foreignobject').then((rect) => { + console.warn(rect); + //const leftEdgeXInitial = rect.left; + // cy.log(`Left edge x-coordinate: ${leftEdgeXInitial}`); + // initialDifference = leftEdgeXInitial - rightEdgeXInitial; + // cy.log(`Initial Difference: ${initialDifference}`); + }); // renderGraph( // `journey // title Web hook life cycle - // section Darkoob - // Make preBuilt:5: Darkoob user + // section Darkoob + // Make preBuilt:5: Darkoob user // register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained // Map slug to a Prebuilt Job:5: Darkoob user // section External Service // set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty - // listen to the events : 5 : External Service - // call darkoob endpoint : 5 : External Service - // section Darkoob + // listen to the events : 5 : External Service + // call darkoob endpoint : 5 : External Service + // section Darkoob // check for inputs : 5 : DarkoobAPI - // run the prebuilt job : 5 : DarkoobAPI + // run the prebuilt job : 5 : DarkoobAPI // `, // { journey: { useMaxWidth: true } } // ); - // cy.contains('tspan', 'Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained') + // cy.contains('tspan', 'Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained') // .invoke('getBBox') // .then((bbox) => { // rightEdgeXFinal = bbox.x + bbox.width; // cy.log(`Right edge x-coordinate final: ${rightEdgeXFinal}`); // }); - // cy.contains('div.label', 'Make preBuilt') + // cy.contains('div.label', 'Make preBuilt') // .invoke('getBoundingClientRect') // .then((rect) => { // leftEdgeXFinal = rect.left; @@ -136,9 +139,5 @@ section Checkout from website // }); // expect(initialDifference).toEqual(finalDifference); - - }); - - }); From d618b8398ee857ab848c6c2f38924b902f2cc884 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 12 Feb 2025 19:54:56 -0500 Subject: [PATCH 04/23] adds test for journey Co-authored-by: Pranav Mishra --- cypress/integration/rendering/journey.spec.js | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index a89e613e1..0cbacc594 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -64,7 +64,7 @@ section Checkout from website ); }); - it('should maintain consistent distance between widest legend label and diagram', () => { + it('should maintain sufficient space between legend labels and diagram elements', () => { renderGraph( `journey title Web hook life cycle @@ -83,61 +83,64 @@ section Checkout from website { journey: { useMaxWidth: true } } ); - let rightEdgeXInitial, - leftEdgeXInitial, - rightEdgeXFinal, - leftEdgeXFinal, - initialDifference, - finalDifference; + let LabelEndX, diagramStartX; + // Get right edge of the legend cy.contains('tspan', 'admin Exjjjnjjjj qwerty').then((textBox) => { const bbox = textBox[0].getBBox(); - const rightEdge = bbox.x + bbox.width; - console.warn(rightEdge); + LabelEndX = bbox.x + bbox.width; }); - cy.get(':nth-child(14) > switch > foreignobject').then((rect) => { - console.warn(rect); - //const leftEdgeXInitial = rect.left; - // cy.log(`Left edge x-coordinate: ${leftEdgeXInitial}`); - // initialDifference = leftEdgeXInitial - rightEdgeXInitial; - // cy.log(`Initial Difference: ${initialDifference}`); + // Get left edge of the diagram + cy.contains('foreignobject', 'Make preBuilt').then((rect) => { + diagramStartX = parseFloat(rect.attr('x')); }); - // renderGraph( - // `journey - // title Web hook life cycle - // section Darkoob - // Make preBuilt:5: Darkoob user - // register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained - // Map slug to a Prebuilt Job:5: Darkoob user - // section External Service - // set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty - // listen to the events : 5 : External Service - // call darkoob endpoint : 5 : External Service - // section Darkoob - // check for inputs : 5 : DarkoobAPI - // run the prebuilt job : 5 : DarkoobAPI - // `, - // { journey: { useMaxWidth: true } } - // ); + // Assert right edge of the diagram is greater than or equal to the right edge of the label + cy.then(() => { + expect(diagramStartX).to.be.gte(LabelEndX); + }); + }); - // cy.contains('tspan', 'Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained') - // .invoke('getBBox') - // .then((bbox) => { - // rightEdgeXFinal = bbox.x + bbox.width; - // cy.log(`Right edge x-coordinate final: ${rightEdgeXFinal}`); - // }); + it('should maintain sufficient space between legend and diagram when legend labels are longer', () => { + cy.then(() => { + renderGraph( + `journey + title Web hook life cycle + section Darkoob + Make preBuilt:5: Darkoob user + register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained + Map slug to a Prebuilt Job:5: Darkoob user + section External Service + set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty + listen to the events : 5 : External Service + call darkoob endpoint : 5 : External Service + section Darkoob + check for inputs : 5 : DarkoobAPI + run the prebuilt job : 5 : DarkoobAPI + `, + { journey: { useMaxWidth: true } } + ); + }); - // cy.contains('div.label', 'Make preBuilt') - // .invoke('getBoundingClientRect') - // .then((rect) => { - // leftEdgeXFinal = rect.left; - // cy.log(`Left edge x-coordinate final: ${leftEdgeXFinal}`); - // finalDifference = leftEdgeXFinal - rightEdgeXFinal; - // cy.log(`Final Difference: ${finalDifference}`); - // }); + let LabelEndX, diagramStartX; - // expect(initialDifference).toEqual(finalDifference); + // Get right edge of the legend + cy.contains('tspan', 'Darkoob userf deliberately increasing the size of this label').then( + (textBox) => { + const bbox = textBox[0].getBBox(); + LabelEndX = bbox.x + bbox.width; + } + ); + + // Get left edge of the diagram + cy.contains('foreignobject', 'Make preBuilt').then((rect) => { + diagramStartX = parseFloat(rect.attr('x')); + }); + + // Assert right edge of the diagram is greater than or equal to the right edge of the label + cy.then(() => { + expect(diagramStartX).to.be.gte(LabelEndX); + }); }); }); From 5f7c68def7eb8dc9c5e706d9d810c273dcb66584 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Thu, 13 Feb 2025 01:46:27 -0500 Subject: [PATCH 05/23] refactor: standardize variable naming and improve legend width calculations Co-authored-by: pranavm2109 --- cypress/integration/rendering/journey.spec.js | 2 +- .../diagrams/user-journey/journeyRenderer.ts | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 0cbacc594..a30b65b84 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -47,7 +47,7 @@ section Checkout from website const style = svg.attr('style'); expect(style).to.match(/^max-width: [\d.]+px;$/); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); - //expect(maxWidthValue).to.eq(700); + expect(maxWidthValue).to.eq(700); }); }); diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 0860c4753..a9dee0546 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -43,8 +43,7 @@ function drawActorLegend(diagram) { }; const textElement = svgDraw.drawText(diagram, labelData); - const bbox = textElement.node().getBBox(); - const textLength = bbox.width; + const textLength = textElement.node().getBBox().width; if (textLength > maxWidth) { maxWidth = textLength; } @@ -53,7 +52,7 @@ function drawActorLegend(diagram) { } // TODO: Cleanup? const conf = getConfig().journey; -let LEFT_MARGIN = 0; +let leftMargin = 0; export const draw = function (text, id, version, diagObj) { const conf = getConfig().journey; @@ -91,8 +90,8 @@ export const draw = function (text, id, version, diagObj) { }); drawActorLegend(diagram); - LEFT_MARGIN = conf.leftMargin + maxWidth - 80; - bounds.insert(0, 0, LEFT_MARGIN, Object.keys(actors).length * 50); + leftMargin = conf.leftMargin + maxWidth - 22.328125; + bounds.insert(0, 0, leftMargin, Object.keys(actors).length * 50); drawTasks(diagram, tasks, 0); const box = bounds.getBounds(); @@ -100,23 +99,23 @@ export const draw = function (text, id, version, diagObj) { diagram .append('text') .text(title) - .attr('x', LEFT_MARGIN) + .attr('x', leftMargin) .attr('font-size', '4ex') .attr('font-weight', 'bold') .attr('y', 25); } const height = box.stopy - box.starty + 2 * conf.diagramMarginY; - const width = LEFT_MARGIN + box.stopx + 2 * conf.diagramMarginX; + const width = leftMargin + box.stopx + 2 * conf.diagramMarginX; configureSvgSize(diagram, height, width, conf.useMaxWidth); // Draw activity line diagram .append('line') - .attr('x1', LEFT_MARGIN) + .attr('x1', leftMargin) .attr('y1', conf.height * 4) // One section head + one task + margins - .attr('x2', width - LEFT_MARGIN - 4) // Subtract stroke width so arrow point is retained + .attr('x2', width - leftMargin - 4) // Subtract stroke width so arrow point is retained .attr('y2', conf.height * 4) .attr('stroke-width', 4) .attr('stroke', 'black') @@ -242,7 +241,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) { } const section = { - x: i * conf.taskMargin + i * conf.width + LEFT_MARGIN, + x: i * conf.taskMargin + i * conf.width + leftMargin, y: 50, text: task.section, fill, @@ -266,7 +265,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) { }, {}); // Add some rendering data to the object - task.x = i * conf.taskMargin + i * conf.width + LEFT_MARGIN; + task.x = i * conf.taskMargin + i * conf.width + leftMargin; task.y = taskPos; task.width = conf.diagramMarginX; task.height = conf.diagramMarginY; From a318ea3692f19dba3aa5b20aa2fed2d57bbc75b2 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Fri, 21 Feb 2025 16:15:24 -0500 Subject: [PATCH 06/23] removes cy.then and magic value Co-authored-by: Pranav Mishra --- cypress/integration/rendering/journey.spec.js | 10 ++++------ .../src/diagrams/user-journey/journeyRenderer.ts | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index a30b65b84..a7e280ab2 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -103,9 +103,8 @@ section Checkout from website }); it('should maintain sufficient space between legend and diagram when legend labels are longer', () => { - cy.then(() => { - renderGraph( - `journey + renderGraph( + `journey title Web hook life cycle section Darkoob Make preBuilt:5: Darkoob user @@ -119,9 +118,8 @@ section Checkout from website check for inputs : 5 : DarkoobAPI run the prebuilt job : 5 : DarkoobAPI `, - { journey: { useMaxWidth: true } } - ); - }); + { journey: { useMaxWidth: true } } + ); let LabelEndX, diagramStartX; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index a9dee0546..06c6cdeea 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -44,7 +44,7 @@ function drawActorLegend(diagram) { const textElement = svgDraw.drawText(diagram, labelData); const textLength = textElement.node().getBBox().width; - if (textLength > maxWidth) { + if (textLength > maxWidth && textLength > conf?.leftMargin) { maxWidth = textLength; } yPos += 20; @@ -90,7 +90,7 @@ export const draw = function (text, id, version, diagObj) { }); drawActorLegend(diagram); - leftMargin = conf.leftMargin + maxWidth - 22.328125; + leftMargin = conf.leftMargin + maxWidth; bounds.insert(0, 0, leftMargin, Object.keys(actors).length * 50); drawTasks(diagram, tasks, 0); From 50816a7f98a86a4d6ebf03abd35809a09a624b9d Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Fri, 21 Feb 2025 17:35:32 -0500 Subject: [PATCH 07/23] remove magic value Co-authored-by: Pranav Mishra --- packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 06c6cdeea..2e27cc8b5 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -44,7 +44,7 @@ function drawActorLegend(diagram) { const textElement = svgDraw.drawText(diagram, labelData); const textLength = textElement.node().getBBox().width; - if (textLength > maxWidth && textLength > conf?.leftMargin) { + if (textLength > maxWidth && textLength > conf?.leftMargin - textLength) { maxWidth = textLength; } yPos += 20; From d47e4724cbfeef8d818eaf3ad79b14bc471a4839 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Tue, 25 Feb 2025 15:21:08 -0500 Subject: [PATCH 08/23] wraps long text into new line Co-authored-by: Pranav Mishra --- .../diagrams/user-journey/journeyRenderer.ts | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 2e27cc8b5..9158a8c82 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -19,11 +19,14 @@ let maxWidth = 0; function drawActorLegend(diagram) { const conf = getConfig().journey; maxWidth = 0; - // Draw the actors let yPos = 60; + + const getRemInPx = (rem) => { + return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); + }; + Object.keys(actors).forEach((person) => { const colour = actors[person].color; - const circleData = { cx: 20, cy: yPos, @@ -34,20 +37,56 @@ function drawActorLegend(diagram) { }; svgDraw.drawCircle(diagram, circleData); - const labelData = { - x: 40, - y: yPos + 7, - fill: '#666', - text: person, - textMargin: conf.boxTextMargin | 5, - }; + // Create temporary text element to measure width + const tempText = diagram.append('text').attr('visibility', 'hidden').text(person); + const textWidth = tempText.node().getBBox().width; + tempText.remove(); - const textElement = svgDraw.drawText(diagram, labelData); - const textLength = textElement.node().getBBox().width; - if (textLength > maxWidth && textLength > conf?.leftMargin - textLength) { - maxWidth = textLength; + const maxLineLength = getRemInPx(15); + let lines = []; + + if (textWidth > maxLineLength) { + // Break the text into chunks regardless of word boundaries + let currentText = ''; + const measureText = diagram.append('text').attr('visibility', 'hidden'); + + for (const element of person) { + currentText += element; + measureText.text(currentText); + const currentWidth = measureText.node().getBBox().width; + + if (currentWidth > maxLineLength && currentText.length > 1) { + // Add hyphen only if we're breaking within a word + lines.push(currentText.slice(0, -1) + '-'); + currentText = element; + } + } + if (currentText) { + lines.push(currentText); + } + measureText.remove(); + } else { + lines = [person]; } - yPos += 20; + + // Draw the text lines + lines.forEach((line, index) => { + const labelData = { + x: 40, + y: yPos + 7 + index * 20, + fill: '#666', + text: line, + textMargin: conf.boxTextMargin | 5, + }; + const textElement = svgDraw.drawText(diagram, labelData); + const lineWidth = textElement.node().getBBox().width; + + if (lineWidth > maxWidth && lineWidth > conf?.leftMargin - lineWidth) { + maxWidth = lineWidth; + } + }); + + yPos += Math.max(20, lines.length * 20); }); } // TODO: Cleanup? From ad6248147c1823f7eeba2ecaf92c25bf2abc7419 Mon Sep 17 00:00:00 2001 From: pranavm2109 Date: Tue, 25 Feb 2025 15:29:29 -0500 Subject: [PATCH 09/23] revised current cypress test Co-authored-by: Shahir Ahmed --- cypress/integration/rendering/journey.spec.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index a7e280ab2..41af44ae6 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -124,12 +124,10 @@ section Checkout from website let LabelEndX, diagramStartX; // Get right edge of the legend - cy.contains('tspan', 'Darkoob userf deliberately increasing the size of this label').then( - (textBox) => { - const bbox = textBox[0].getBBox(); - LabelEndX = bbox.x + bbox.width; - } - ); + cy.contains('tspan', 'Darkoob userf').then((textBox) => { + const bbox = textBox[0].getBBox(); + LabelEndX = bbox.x + bbox.width; + }); // Get left edge of the diagram cy.contains('foreignobject', 'Make preBuilt').then((rect) => { From edbf125c83f09c57a6039e08ecb0acf91b4db59a Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Tue, 25 Feb 2025 15:43:00 -0500 Subject: [PATCH 10/23] wraps long text into new line Co-authored-by: Pranav Mishra --- .../mermaid/src/diagrams/user-journey/journeyRenderer.ts | 2 +- packages/mermaid/src/schemas/config.schema.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 9158a8c82..d0bb8e0c1 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -41,7 +41,7 @@ function drawActorLegend(diagram) { const tempText = diagram.append('text').attr('visibility', 'hidden').text(person); const textWidth = tempText.node().getBBox().width; tempText.remove(); - + let configObject = getConfig().journey; const maxLineLength = getRemInPx(15); let lines = []; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index e1014e889..e94c3d69c 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -1485,6 +1485,12 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) type: integer default: 150 minimum: 0 + maxLabelWidth: + description: Maximum width of actor labels + type: integer + default: 360 + minimum: 240 + maximum: 480 width: description: Width of actor boxes type: integer From 5510f18d333725b17bd94b461c5ee0dc46d84d63 Mon Sep 17 00:00:00 2001 From: pranavm2109 Date: Tue, 25 Feb 2025 15:50:10 -0500 Subject: [PATCH 11/23] removed function to get rem in px since config schema now has maxLabelWidth Co-authored-by: Shahir Ahmed --- .../mermaid/src/diagrams/user-journey/journeyRenderer.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index d0bb8e0c1..8e221f6b4 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -21,10 +21,6 @@ function drawActorLegend(diagram) { maxWidth = 0; let yPos = 60; - const getRemInPx = (rem) => { - return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); - }; - Object.keys(actors).forEach((person) => { const colour = actors[person].color; const circleData = { @@ -41,8 +37,8 @@ function drawActorLegend(diagram) { const tempText = diagram.append('text').attr('visibility', 'hidden').text(person); const textWidth = tempText.node().getBBox().width; tempText.remove(); - let configObject = getConfig().journey; - const maxLineLength = getRemInPx(15); + const journeyConfigObject = getConfig().journey; + const maxLineLength = journeyConfigObject.maxLabelWidth; let lines = []; if (textWidth > maxLineLength) { From 3724d112554c645faee6b4576dfbef0d78a546e2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:55:26 +0000 Subject: [PATCH 12/23] [autofix.ci] apply automated fixes --- packages/mermaid/src/config.type.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 86281cd52..ff547eeec 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -546,6 +546,10 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig { * Margin between actors */ leftMargin?: number; + /** + * Maximum width of actor labels + */ + maxLabelWidth?: number; /** * Width of actor boxes */ From aaf15fccc14c0282fd61b35fb13eb69d8936d89e Mon Sep 17 00:00:00 2001 From: pranavm2109 Date: Tue, 25 Feb 2025 16:13:14 -0500 Subject: [PATCH 13/23] added first draft of test to see if label text is being wrapped in different lines Co-authored-by: Shahir Ahmed --- cypress/integration/rendering/journey.spec.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 41af44ae6..0cd1945c7 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -139,4 +139,39 @@ section Checkout from website expect(diagramStartX).to.be.gte(LabelEndX); }); }); + + it('should check the width and number of lines of the Darkoob user text element', () => { + renderGraph( + `journey + title Web hook life cycle + section Darkoob + Make preBuilt:5: Darkoob user + register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained + Map slug to a Prebuilt Job:5: Darkoob user + section External Service + set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty + listen to the events : 5 : External Service + call darkoob endpoint : 5 : External Service + section Darkoob + check for inputs : 5 : DarkoobAPI + run the prebuilt job : 5 : DarkoobAPI + `, + { journey: { useMaxWidth: true } } + ); + + cy.contains('tspan', 'Darkoob user').then((textBox) => { + const bbox = textBox[0].getBBox(); + const textWidth = bbox.width; + + expect(textWidth).to.equal(320); + }); + + cy.contains('tspan', 'Darkoob user') + .parent() + .then((textElement) => { + const numLines = textElement.find('tspan').length; + + expect(numLines).to.equal(3); + }); + }); }); From 7c7fd4bc5efc2738a1647d93377870ab2fbe006b Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Tue, 25 Feb 2025 18:40:27 -0500 Subject: [PATCH 14/23] adds test for line wrapping Co-authored-by: Pranav Mishra --- cypress/integration/rendering/journey.spec.js | 153 +++++++++++------- .../diagrams/user-journey/journeyRenderer.ts | 15 +- 2 files changed, 103 insertions(+), 65 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 0cd1945c7..ec7788439 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -64,44 +64,6 @@ section Checkout from website ); }); - it('should maintain sufficient space between legend labels and diagram elements', () => { - renderGraph( - `journey - title Web hook life cycle - section Darkoob - Make preBuilt:5: Darkoob user - register slug : 5: Darkoob userf - Map slug to a Prebuilt Job:5: Darkoob user - section External Service - set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty - listen to the events : 5 : External Service - call darkoob endpoint : 5 : External Service - section Darkoob - check for inputs : 5 : DarkoobAPI - run the prebuilt job : 5 : DarkoobAPI - `, - { journey: { useMaxWidth: true } } - ); - - let LabelEndX, diagramStartX; - - // Get right edge of the legend - cy.contains('tspan', 'admin Exjjjnjjjj qwerty').then((textBox) => { - const bbox = textBox[0].getBBox(); - LabelEndX = bbox.x + bbox.width; - }); - - // Get left edge of the diagram - cy.contains('foreignobject', 'Make preBuilt').then((rect) => { - diagramStartX = parseFloat(rect.attr('x')); - }); - - // Assert right edge of the diagram is greater than or equal to the right edge of the label - cy.then(() => { - expect(diagramStartX).to.be.gte(LabelEndX); - }); - }); - it('should maintain sufficient space between legend and diagram when legend labels are longer', () => { renderGraph( `journey @@ -140,38 +102,105 @@ section Checkout from website }); }); - it('should check the width and number of lines of the Darkoob user text element', () => { + it('should wrap a single long word with hyphenation', () => { renderGraph( - `journey - title Web hook life cycle - section Darkoob - Make preBuilt:5: Darkoob user - register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained - Map slug to a Prebuilt Job:5: Darkoob user - section External Service - set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty - listen to the events : 5 : External Service - call darkoob endpoint : 5 : External Service - section Darkoob - check for inputs : 5 : DarkoobAPI - run the prebuilt job : 5 : DarkoobAPI + ` + --- + config: + journey: + maxLabelWidth: 100 + --- + journey + title Long Word Test + section Test + VeryLongWord: 5: Supercalifragilisticexpialidocious `, { journey: { useMaxWidth: true } } ); - cy.contains('tspan', 'Darkoob user').then((textBox) => { - const bbox = textBox[0].getBBox(); - const textWidth = bbox.width; - - expect(textWidth).to.equal(320); + // Check that at least one line ends with a hyphen, indicating a mid-word break. + cy.get('tspan').then((tspans) => { + const hasHyphen = [...tspans].some((t) => t.textContent.trim().endsWith('-')); + return expect(hasHyphen).to.be.true; }); + }); - cy.contains('tspan', 'Darkoob user') - .parent() - .then((textElement) => { - const numLines = textElement.find('tspan').length; + it('should wrap text on whitespace without adding hyphens', () => { + renderGraph( + ` + --- + config: + journey: + maxLabelWidth: 200 + --- + journey + title Whitespace Test + section Test + TextWithSpaces: 5: Gustavo Fring is played by Giancarlo Esposito. + `, + { journey: { useMaxWidth: true } } + ); - expect(numLines).to.equal(3); + // Verify that none of the text spans end with a hyphen. + cy.get('tspan').each(($el) => { + const text = $el.text(); + expect(text.trim()).not.to.match(/-$/); + }); + }); + + it('should wrap long labels into multiple lines, keep them under max width, and maintain margins', () => { + renderGraph( + ` + --- + config: + journey: + maxLabelWidth: 320 + --- + journey + title User Journey Example + section Onboarding + Sign Up: 5: Sam Sam Sam Sam Sam Sam Sam Sam Sam Sam Sam Sam ... + section Engagement + Browse Features: 3 + Use Core Functionality: 4 + `, + { journey: { useMaxWidth: true } } + ); + + let diagramStartX; + + // Get the diagram's left edge + cy.contains('foreignobject', 'Sign Up') + .then(($diagram) => { + diagramStartX = parseFloat($diagram.attr('x')); + }) + .then(() => { + // Get all legend lines that include "Sam" + cy.get('text.legend') + .filter((i, el) => el.textContent.includes('Sam')) + .then(($lines) => { + // Check that there are two lines + expect($lines.length).to.be.equal(2); + + // Check that for all but the last line it nearly fills the max width + $lines.each((index, el) => { + const bbox = el.getBBox(); + if (index < $lines.length - 1) { + expect(bbox.width).to.be.closeTo(320, 5); + } else { + // Last line may be shorter + expect(bbox.width).to.be.lte(320); + } + }); + + // check margin between diagram and legend is maintained + const longestBBox = $lines.get(0).getBBox(); // longest Line's bbox + if (diagramStartX && longestBBox) { + const legendRightEdge = longestBBox.x + longestBBox.width; + const margin = diagramStartX - legendRightEdge; + expect(margin).to.be.closeTo(100, 2); // expect margin to be around 100 + } + }); }); }); }); diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 8e221f6b4..821f023a0 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -52,9 +52,18 @@ function drawActorLegend(diagram) { const currentWidth = measureText.node().getBBox().width; if (currentWidth > maxLineLength && currentText.length > 1) { - // Add hyphen only if we're breaking within a word - lines.push(currentText.slice(0, -1) + '-'); - currentText = element; + let lineToPush = currentText.slice(0, -1); + + // If the line ends with a space, trim it and do not add a hyphen. + if (lineToPush.endsWith(' ')) { + lineToPush = lineToPush.trimEnd(); + } else { + lineToPush = lineToPush + '-'; + } + lines.push(lineToPush); + + // If the breaking character is a space, start fresh; otherwise, start with the character. + currentText = element === ' ' ? '' : element; } } if (currentText) { From 55e1dd0eadeac1b55020d4af85aae6c6b3461b8c Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Tue, 18 Mar 2025 15:32:04 -0400 Subject: [PATCH 15/23] implemented knuth-plass line-breaking algorithm Co-authored-by: pranavm2109 --- .../diagrams/user-journey/journeyRenderer.ts | 85 +++++++++---------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 821f023a0..aecbe9d2e 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -18,7 +18,7 @@ let maxWidth = 0; /** @param diagram - The diagram to draw to. */ function drawActorLegend(diagram) { const conf = getConfig().journey; - maxWidth = 0; + maxWidth = conf.maxLabelWidth; // Ensures we don't exceed this width let yPos = 60; Object.keys(actors).forEach((person) => { @@ -33,67 +33,62 @@ function drawActorLegend(diagram) { }; svgDraw.drawCircle(diagram, circleData); - // Create temporary text element to measure width - const tempText = diagram.append('text').attr('visibility', 'hidden').text(person); - const textWidth = tempText.node().getBBox().width; - tempText.remove(); - const journeyConfigObject = getConfig().journey; - const maxLineLength = journeyConfigObject.maxLabelWidth; - let lines = []; + const words = person.split(' '); // Split text into words + const lines = []; + let currentLine = ''; - if (textWidth > maxLineLength) { - // Break the text into chunks regardless of word boundaries - let currentText = ''; - const measureText = diagram.append('text').attr('visibility', 'hidden'); + const measureText = diagram.append('text').attr('visibility', 'hidden'); - for (const element of person) { - currentText += element; - measureText.text(currentText); - const currentWidth = measureText.node().getBBox().width; + words.forEach((word, _index) => { + const testLine = currentLine ? `${currentLine} ${word}` : word; + measureText.text(testLine); + const textWidth = measureText.node().getBBox().width; - if (currentWidth > maxLineLength && currentText.length > 1) { - let lineToPush = currentText.slice(0, -1); - - // If the line ends with a space, trim it and do not add a hyphen. - if (lineToPush.endsWith(' ')) { - lineToPush = lineToPush.trimEnd(); - } else { - lineToPush = lineToPush + '-'; - } - lines.push(lineToPush); - - // If the breaking character is a space, start fresh; otherwise, start with the character. - currentText = element === ' ' ? '' : element; + if (textWidth > maxWidth) { + if (currentLine) { + lines.push(currentLine); // Push previous line before adding a new word } - } - if (currentText) { - lines.push(currentText); - } - measureText.remove(); - } else { - lines = [person]; - } + currentLine = word; - // Draw the text lines + // If a single word is too long, break it + if (measureText.node().getBBox().width > maxWidth) { + let brokenWord = ''; + for (const char of word) { + brokenWord += char; + measureText.text(brokenWord + '-'); + if (measureText.node().getBBox().width > maxWidth) { + lines.push(brokenWord.slice(0, -1) + '-'); // Break word with a hyphen + brokenWord = char; + } + } + currentLine = brokenWord; + } + } else { + currentLine = testLine; + } + }); + + if (currentLine) { + lines.push(currentLine); + } + measureText.remove(); + + // Draw the text lines within the fixed width lines.forEach((line, index) => { const labelData = { x: 40, y: yPos + 7 + index * 20, fill: '#666', text: line, - textMargin: conf.boxTextMargin | 5, + textMargin: conf.boxTextMargin || 5, }; - const textElement = svgDraw.drawText(diagram, labelData); - const lineWidth = textElement.node().getBBox().width; - - if (lineWidth > maxWidth && lineWidth > conf?.leftMargin - lineWidth) { - maxWidth = lineWidth; - } + svgDraw.drawText(diagram, labelData); }); yPos += Math.max(20, lines.length * 20); }); } + // TODO: Cleanup? const conf = getConfig().journey; let leftMargin = 0; From b2ab34ca2bb0062c6fbf5e60c1e66446e2111d9a Mon Sep 17 00:00:00 2001 From: pranavm2109 Date: Tue, 18 Mar 2025 15:36:50 -0400 Subject: [PATCH 16/23] fixed linting issue Co-authored-by: Shahir Ahmed --- packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index aecbe9d2e..28d750e1c 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -80,7 +80,7 @@ function drawActorLegend(diagram) { y: yPos + 7 + index * 20, fill: '#666', text: line, - textMargin: conf.boxTextMargin || 5, + textMargin: conf.boxTextMargin ?? 5, }; svgDraw.drawText(diagram, labelData); }); From 20927a1c8ed1a6b7c33d5e72c988b990552acd01 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Tue, 18 Mar 2025 15:39:58 -0400 Subject: [PATCH 17/23] remove max and min attributes for maxLabelWidth Co-authored-by: pranavm2109 --- packages/mermaid/src/schemas/config.schema.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 5c9eb5a89..ce2cc8a34 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -1497,8 +1497,6 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) description: Maximum width of actor labels type: integer default: 360 - minimum: 240 - maximum: 480 width: description: Width of actor boxes type: integer From 044a3d968673c8ff616f8b838cc35fe76bfb28b4 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 19 Mar 2025 20:34:44 -0400 Subject: [PATCH 18/23] Set legend width baseline (conf.leftMargin) and expand dynamically Co-authored-by: pranavm2109 --- cypress/integration/rendering/journey.spec.js | 10 +- .../diagrams/user-journey/journeyRenderer.ts | 108 +++++++++++------- 2 files changed, 73 insertions(+), 45 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index ec7788439..2e0b0dc40 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -136,7 +136,7 @@ section Checkout from website journey title Whitespace Test section Test - TextWithSpaces: 5: Gustavo Fring is played by Giancarlo Esposito. + TextWithSpaces: 5: Gustavo Fring is played by Giancarlo Esposito and is a character in Breaking Bad. `, { journey: { useMaxWidth: true } } ); @@ -159,7 +159,9 @@ section Checkout from website journey title User Journey Example section Onboarding - Sign Up: 5: Sam Sam Sam Sam Sam Sam Sam Sam Sam Sam Sam Sam ... + Sign Up: 5: This is a long label that will be split into multiple lines to test the wrapping functionality + Browse Features: 3: This is another long label that will be split into multiple lines to test the wrapping functionality + Use Core Functionality: 4: This is yet another long label that will be split into multiple lines to test the wrapping functionality section Engagement Browse Features: 3 Use Core Functionality: 4 @@ -177,10 +179,10 @@ section Checkout from website .then(() => { // Get all legend lines that include "Sam" cy.get('text.legend') - .filter((i, el) => el.textContent.includes('Sam')) + .filter((i, el) => el.textContent.includes('long')) .then(($lines) => { // Check that there are two lines - expect($lines.length).to.be.equal(2); + expect($lines.length).to.be.equal(9); // Check that for all but the last line it nearly fills the max width $lines.each((index, el) => { diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 28d750e1c..bb3386bfc 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -18,7 +18,8 @@ let maxWidth = 0; /** @param diagram - The diagram to draw to. */ function drawActorLegend(diagram) { const conf = getConfig().journey; - maxWidth = conf.maxLabelWidth; // Ensures we don't exceed this width + const maxLabelWidth = conf.maxLabelWidth; + maxWidth = 0; let yPos = 60; Object.keys(actors).forEach((person) => { @@ -33,47 +34,63 @@ function drawActorLegend(diagram) { }; svgDraw.drawCircle(diagram, circleData); - const words = person.split(' '); // Split text into words - const lines = []; - let currentLine = ''; - - const measureText = diagram.append('text').attr('visibility', 'hidden'); - - words.forEach((word, _index) => { - const testLine = currentLine ? `${currentLine} ${word}` : word; - measureText.text(testLine); - const textWidth = measureText.node().getBBox().width; - - if (textWidth > maxWidth) { - if (currentLine) { - lines.push(currentLine); // Push previous line before adding a new word - } - currentLine = word; - - // If a single word is too long, break it - if (measureText.node().getBBox().width > maxWidth) { - let brokenWord = ''; - for (const char of word) { - brokenWord += char; - measureText.text(brokenWord + '-'); - if (measureText.node().getBBox().width > maxWidth) { - lines.push(brokenWord.slice(0, -1) + '-'); // Break word with a hyphen - brokenWord = char; - } - } - currentLine = brokenWord; - } - } else { - currentLine = testLine; - } - }); - - if (currentLine) { - lines.push(currentLine); - } + // First, measure the full text width without wrapping. + let measureText = diagram.append('text').attr('visibility', 'hidden').text(person); + const fullTextWidth = measureText.node().getBBox().width; measureText.remove(); - // Draw the text lines within the fixed width + let lines = []; + + // If the text is naturally within the max width, use it as a single line. + if (fullTextWidth <= maxLabelWidth) { + lines = [person]; + } else { + // Otherwise, wrap the text using the knuth-plass algorithm. + const words = person.split(' '); // Split the text into words. + let currentLine = ''; + measureText = diagram.append('text').attr('visibility', 'hidden'); + + words.forEach((word) => { + // check the width of the line with the new word. + const testLine = currentLine ? `${currentLine} ${word}` : word; + measureText.text(testLine); + const textWidth = measureText.node().getBBox().width; + + if (textWidth > maxLabelWidth) { + // If adding the new word exceeds max width, push the current line. + if (currentLine) { + lines.push(currentLine); + } + currentLine = word; // Start a new line with the current word. + + // If the word itself is too long, break it with a hyphen. + measureText.text(word); + if (measureText.node().getBBox().width > maxLabelWidth) { + let brokenWord = ''; + for (const char of word) { + brokenWord += char; + measureText.text(brokenWord + '-'); + if (measureText.node().getBBox().width > maxLabelWidth) { + // Push the broken part with a hyphen. + lines.push(brokenWord.slice(0, -1) + '-'); + brokenWord = char; + } + } + currentLine = brokenWord; + } + } else { + // If the line with the new word fits, add the new word to the current line. + currentLine = testLine; + } + }); + + // Push the last line. + if (currentLine) { + lines.push(currentLine); + } + measureText.remove(); // Remove the text element used for measuring. + } + lines.forEach((line, index) => { const labelData = { x: 40, @@ -82,7 +99,16 @@ function drawActorLegend(diagram) { text: line, textMargin: conf.boxTextMargin ?? 5, }; - svgDraw.drawText(diagram, labelData); + + // Draw the text and measure the width. + const textElement = svgDraw.drawText(diagram, labelData); + const lineWidth = textElement.node().getBBox().width; + + // Use conf.leftMargin as the initial spacing baseline, + // but expand maxWidth if the line is wider. + if (lineWidth > maxWidth && lineWidth > conf.leftMargin - lineWidth) { + maxWidth = lineWidth; + } }); yPos += Math.max(20, lines.length * 20); From ea3925455686083b5ee2b0bd1d3043e64632a5de Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 19 Mar 2025 20:41:43 -0400 Subject: [PATCH 19/23] ensure maxLabelWidth is a number Co-authored-by: pranavm2109 --- packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index bb3386bfc..75a441d31 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -22,6 +22,10 @@ function drawActorLegend(diagram) { maxWidth = 0; let yPos = 60; + if (isNaN(maxLabelWidth)) { + throw new Error('maxLabelWidth must be a number'); + } + Object.keys(actors).forEach((person) => { const colour = actors[person].color; const circleData = { From 573b6d9ba7fee75811d2af7e420c357b1e58147f Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 19 Mar 2025 20:58:17 -0400 Subject: [PATCH 20/23] remove redundant test Co-authored-by: pranavm2109 --- packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 75a441d31..bb3386bfc 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -22,10 +22,6 @@ function drawActorLegend(diagram) { maxWidth = 0; let yPos = 60; - if (isNaN(maxLabelWidth)) { - throw new Error('maxLabelWidth must be a number'); - } - Object.keys(actors).forEach((person) => { const colour = actors[person].color; const circleData = { From b322392f971ce2c7b4ed0ccc351362244c74554d Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 19 Mar 2025 21:18:45 -0400 Subject: [PATCH 21/23] refactor tests Co-authored-by: pranavm2109 --- cypress/integration/rendering/journey.spec.js | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 2e0b0dc40..4c4d60f65 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -177,32 +177,24 @@ section Checkout from website diagramStartX = parseFloat($diagram.attr('x')); }) .then(() => { - // Get all legend lines that include "Sam" - cy.get('text.legend') - .filter((i, el) => el.textContent.includes('long')) - .then(($lines) => { - // Check that there are two lines - expect($lines.length).to.be.equal(9); + cy.get('text.legend').then(($lines) => { + // Check that there are two lines + expect($lines.length).to.be.equal(9); - // Check that for all but the last line it nearly fills the max width - $lines.each((index, el) => { - const bbox = el.getBBox(); - if (index < $lines.length - 1) { - expect(bbox.width).to.be.closeTo(320, 5); - } else { - // Last line may be shorter - expect(bbox.width).to.be.lte(320); - } - }); - - // check margin between diagram and legend is maintained - const longestBBox = $lines.get(0).getBBox(); // longest Line's bbox - if (diagramStartX && longestBBox) { - const legendRightEdge = longestBBox.x + longestBBox.width; - const margin = diagramStartX - legendRightEdge; - expect(margin).to.be.closeTo(100, 2); // expect margin to be around 100 - } + // Check that all lines are under the max width + $lines.each((index, el) => { + const bbox = el.getBBox(); + expect(bbox.width).to.be.lte(320); }); + + // check baseline margin between diagram and legend is maintained + const longestBBox = $lines.get(7).getBBox(); // longest Line's bbox + if (diagramStartX && longestBBox) { + const legendRightEdge = longestBBox.x + longestBBox.width; + const margin = diagramStartX - legendRightEdge; + expect(margin).to.be.closeTo(150, 2); + } + }); }); }); }); From 2d583b186d357a5605a9030ed660ebdc97e481df Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 19 Mar 2025 22:51:02 -0400 Subject: [PATCH 22/23] fix some pixel issues Co-authored-by: pranavm2109 --- cypress/integration/rendering/journey.spec.js | 13 ++++--------- .../src/diagrams/user-journey/journeyRenderer.ts | 10 +++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 4c4d60f65..b03e48a38 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -169,7 +169,7 @@ section Checkout from website { journey: { useMaxWidth: true } } ); - let diagramStartX; + let diagramStartX, maxLineWidth; // Get the diagram's left edge cy.contains('foreignobject', 'Sign Up') @@ -178,22 +178,17 @@ section Checkout from website }) .then(() => { cy.get('text.legend').then(($lines) => { - // Check that there are two lines + // Check that there are multiple lines expect($lines.length).to.be.equal(9); // Check that all lines are under the max width $lines.each((index, el) => { const bbox = el.getBBox(); expect(bbox.width).to.be.lte(320); + maxLineWidth = Math.max(maxLineWidth || 0, bbox.width); }); - // check baseline margin between diagram and legend is maintained - const longestBBox = $lines.get(7).getBBox(); // longest Line's bbox - if (diagramStartX && longestBBox) { - const legendRightEdge = longestBBox.x + longestBBox.width; - const margin = diagramStartX - legendRightEdge; - expect(margin).to.be.closeTo(150, 2); - } + expect(diagramStartX - 150).to.be.closeTo(maxLineWidth, 2); }); }); }); diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index bb3386bfc..64e21a10e 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -36,7 +36,7 @@ function drawActorLegend(diagram) { // First, measure the full text width without wrapping. let measureText = diagram.append('text').attr('visibility', 'hidden').text(person); - const fullTextWidth = measureText.node().getBBox().width; + const fullTextWidth = measureText.node().getBoundingClientRect().width; measureText.remove(); let lines = []; @@ -54,7 +54,7 @@ function drawActorLegend(diagram) { // check the width of the line with the new word. const testLine = currentLine ? `${currentLine} ${word}` : word; measureText.text(testLine); - const textWidth = measureText.node().getBBox().width; + const textWidth = measureText.node().getBoundingClientRect().width; if (textWidth > maxLabelWidth) { // If adding the new word exceeds max width, push the current line. @@ -65,12 +65,12 @@ function drawActorLegend(diagram) { // If the word itself is too long, break it with a hyphen. measureText.text(word); - if (measureText.node().getBBox().width > maxLabelWidth) { + if (measureText.node().getBoundingClientRect().width > maxLabelWidth) { let brokenWord = ''; for (const char of word) { brokenWord += char; measureText.text(brokenWord + '-'); - if (measureText.node().getBBox().width > maxLabelWidth) { + if (measureText.node().getBoundingClientRect().width > maxLabelWidth) { // Push the broken part with a hyphen. lines.push(brokenWord.slice(0, -1) + '-'); brokenWord = char; @@ -102,7 +102,7 @@ function drawActorLegend(diagram) { // Draw the text and measure the width. const textElement = svgDraw.drawText(diagram, labelData); - const lineWidth = textElement.node().getBBox().width; + const lineWidth = textElement.node().getBoundingClientRect().width; // Use conf.leftMargin as the initial spacing baseline, // but expand maxWidth if the line is wider. From f7e31a978b137f807a1a08c88360bcd081256f10 Mon Sep 17 00:00:00 2001 From: Shahir Ahmed Date: Wed, 19 Mar 2025 23:44:21 -0400 Subject: [PATCH 23/23] update docs Co-authored-by: pranavm2109 --- cypress/integration/rendering/journey.spec.js | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index b03e48a38..2d6c14c9d 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -64,6 +64,35 @@ section Checkout from website ); }); + it('should initialize with a left margin of 150px for user journeys', () => { + renderGraph( + ` + --- + config: + journey: + maxLabelWidth: 320 + --- + journey + title User Journey Example + section Onboarding + Sign Up: 5: + Browse Features: 3: + Use Core Functionality: 4: + section Engagement + Browse Features: 3 + Use Core Functionality: 4 + `, + { journey: { useMaxWidth: true } } + ); + + let diagramStartX; + + cy.contains('foreignobject', 'Sign Up').then(($diagram) => { + diagramStartX = parseFloat($diagram.attr('x')); + expect(diagramStartX).to.be.closeTo(150, 2); + }); + }); + it('should maintain sufficient space between legend and diagram when legend labels are longer', () => { renderGraph( `journey @@ -118,7 +147,7 @@ section Checkout from website { journey: { useMaxWidth: true } } ); - // Check that at least one line ends with a hyphen, indicating a mid-word break. + // Verify that the line ends with a hyphen, indicating proper hyphenation for words exceeding maxLabelWidth. cy.get('tspan').then((tspans) => { const hasHyphen = [...tspans].some((t) => t.textContent.trim().endsWith('-')); return expect(hasHyphen).to.be.true; @@ -171,7 +200,7 @@ section Checkout from website let diagramStartX, maxLineWidth; - // Get the diagram's left edge + // Get the diagram's left edge x-coordinate cy.contains('foreignobject', 'Sign Up') .then(($diagram) => { diagramStartX = parseFloat($diagram.attr('x')); @@ -181,14 +210,17 @@ section Checkout from website // Check that there are multiple lines expect($lines.length).to.be.equal(9); - // Check that all lines are under the max width + // Check that all lines are under the maxLabelWidth $lines.each((index, el) => { const bbox = el.getBBox(); expect(bbox.width).to.be.lte(320); maxLineWidth = Math.max(maxLineWidth || 0, bbox.width); }); - expect(diagramStartX - 150).to.be.closeTo(maxLineWidth, 2); + /** The expected margin between the diagram and the legend is 150px, as defined by + * conf.leftMargin in user-journey-config.js + */ + expect(diagramStartX - maxLineWidth).to.be.closeTo(150, 2); }); }); });