Merge pull request #6274 from Shahir-47/bug/5955_adjust-diagram-position-with-legend-width

fix: Prevent Legend Labels from Overlapping Diagram Elements in Journey Diagrams
This commit is contained in:
Sidharth Vinod
2025-04-08 17:17:06 +00:00
committed by GitHub
4 changed files with 260 additions and 19 deletions

View File

@@ -559,6 +559,10 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
* Margin between actors
*/
leftMargin?: number;
/**
* Maximum width of actor labels
*/
maxLabelWidth?: number;
/**
* Width of actor boxes
*/

View File

@@ -13,15 +13,17 @@ export const setConf = function (cnf) {
};
const actors = {};
let maxWidth = 0;
/** @param diagram - The diagram to draw to. */
function drawActorLegend(diagram) {
const conf = getConfig().journey;
// Draw the actors
const maxLabelWidth = conf.maxLabelWidth;
maxWidth = 0;
let yPos = 60;
Object.keys(actors).forEach((person) => {
const colour = actors[person].color;
const circleData = {
cx: 20,
cy: yPos,
@@ -32,21 +34,90 @@ function drawActorLegend(diagram) {
};
svgDraw.drawCircle(diagram, circleData);
const labelData = {
x: 40,
y: yPos + 7,
fill: '#666',
text: person,
textMargin: conf.boxTextMargin | 5,
};
svgDraw.drawText(diagram, labelData);
// First, measure the full text width without wrapping.
let measureText = diagram.append('text').attr('visibility', 'hidden').text(person);
const fullTextWidth = measureText.node().getBoundingClientRect().width;
measureText.remove();
yPos += 20;
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().getBoundingClientRect().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().getBoundingClientRect().width > maxLabelWidth) {
let brokenWord = '';
for (const char of word) {
brokenWord += char;
measureText.text(brokenWord + '-');
if (measureText.node().getBoundingClientRect().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,
y: yPos + 7 + index * 20,
fill: '#666',
text: line,
textMargin: conf.boxTextMargin ?? 5,
};
// Draw the text and measure the width.
const textElement = svgDraw.drawText(diagram, labelData);
const lineWidth = textElement.node().getBoundingClientRect().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);
});
}
// TODO: Cleanup?
const conf = getConfig().journey;
const LEFT_MARGIN = conf.leftMargin;
let leftMargin = 0;
export const draw = function (text, id, version, diagObj) {
const conf = getConfig().journey;
@@ -84,7 +155,8 @@ export const draw = function (text, id, version, diagObj) {
});
drawActorLegend(diagram);
bounds.insert(0, 0, LEFT_MARGIN, Object.keys(actors).length * 50);
leftMargin = conf.leftMargin + maxWidth;
bounds.insert(0, 0, leftMargin, Object.keys(actors).length * 50);
drawTasks(diagram, tasks, 0);
const box = bounds.getBounds();
@@ -92,23 +164,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')
@@ -234,7 +306,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,
@@ -258,7 +330,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;

View File

@@ -1496,6 +1496,10 @@ $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
width:
description: Width of actor boxes
type: integer