Files
mermaid/src/diagrams/sequence/sequenceRenderer.js
2020-02-16 15:26:08 +01:00

660 lines
19 KiB
JavaScript

import * as d3 from 'd3';
import svgDraw from './svgDraw';
import { logger } from '../../logger';
import { parser } from './parser/sequenceDiagram';
import sequenceDb from './sequenceDb';
parser.yy = sequenceDb;
const conf = {
diagramMarginX: 50,
diagramMarginY: 30,
// Margin between actors
actorMargin: 50,
// Width of actor boxes
width: 150,
// Height of actor boxes
height: 65,
actorFontSize: 14,
actorFontFamily: '"Open-Sans", "sans-serif"',
// Margin around loop boxes
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
// Space between messages
messageMargin: 35,
// mirror actors under diagram
mirrorActors: false,
// Depending on css styling this might need adjustment
// Prolongs the edge of the diagram downwards
bottomMarginAdj: 1,
// width of activation box
activationWidth: 10,
// text placement as: tspan | fo | old only text as before
textPlacement: 'tspan',
showSequenceNumbers: false
};
export const bounds = {
data: {
startx: undefined,
stopx: undefined,
starty: undefined,
stopy: undefined
},
verticalPos: 0,
sequenceItems: [],
activations: [],
init: function() {
this.sequenceItems = [];
this.activations = [];
this.data = {
startx: undefined,
stopx: undefined,
starty: undefined,
stopy: undefined
};
this.verticalPos = 0;
},
updateVal: function(obj, key, val, fun) {
if (typeof obj[key] === 'undefined') {
obj[key] = val;
} else {
obj[key] = fun(val, obj[key]);
}
},
updateBounds: function(startx, starty, stopx, stopy) {
const _self = this;
let cnt = 0;
function updateFn(type) {
return function updateItemBounds(item) {
cnt++;
// The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems
const n = _self.sequenceItems.length - cnt + 1;
_self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min);
_self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max);
_self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min);
_self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max);
if (!(type === 'activation')) {
_self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min);
_self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max);
_self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min);
_self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max);
}
};
}
this.sequenceItems.forEach(updateFn());
this.activations.forEach(updateFn('activation'));
},
insert: function(startx, starty, stopx, stopy) {
const _startx = Math.min(startx, stopx);
const _stopx = Math.max(startx, stopx);
const _starty = Math.min(starty, stopy);
const _stopy = Math.max(starty, stopy);
this.updateVal(bounds.data, 'startx', _startx, Math.min);
this.updateVal(bounds.data, 'starty', _starty, Math.min);
this.updateVal(bounds.data, 'stopx', _stopx, Math.max);
this.updateVal(bounds.data, 'stopy', _stopy, Math.max);
this.updateBounds(_startx, _starty, _stopx, _stopy);
},
newActivation: function(message, diagram) {
const actorRect = parser.yy.getActors()[message.from.actor];
const stackedSize = actorActivations(message.from.actor).length;
const x = actorRect.x + conf.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
this.activations.push({
startx: x,
starty: this.verticalPos + 2,
stopx: x + conf.activationWidth,
stopy: undefined,
actor: message.from.actor,
anchored: svgDraw.anchorElement(diagram)
});
},
endActivation: function(message) {
// find most recent activation for given actor
const lastActorActivationIdx = this.activations
.map(function(activation) {
return activation.actor;
})
.lastIndexOf(message.from.actor);
const activation = this.activations.splice(lastActorActivationIdx, 1)[0];
return activation;
},
newLoop: function(title, fill) {
this.sequenceItems.push({
startx: undefined,
starty: this.verticalPos,
stopx: undefined,
stopy: undefined,
title: title,
fill: fill
});
},
endLoop: function() {
const loop = this.sequenceItems.pop();
return loop;
},
addSectionToLoop: function(message) {
const loop = this.sequenceItems.pop();
loop.sections = loop.sections || [];
loop.sectionTitles = loop.sectionTitles || [];
loop.sections.push(bounds.getVerticalPos());
loop.sectionTitles.push(message);
this.sequenceItems.push(loop);
},
bumpVerticalPos: function(bump) {
this.verticalPos = this.verticalPos + bump;
this.data.stopy = this.verticalPos;
},
getVerticalPos: function() {
return this.verticalPos;
},
getBounds: function() {
return this.data;
}
};
const _drawLongText = (text, x, y, g, width) => {
let textHeight = 0;
const lines = text.split(/<br\s*\/?>/gi);
for (const line of lines) {
const textObj = svgDraw.getTextObj();
textObj.x = x;
textObj.y = y + textHeight;
textObj.textMargin = conf.noteMargin;
textObj.dy = '1em';
textObj.text = line;
textObj.class = 'noteText';
const textElem = svgDraw.drawText(g, textObj, width);
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
}
return textHeight;
};
/**
* 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
*/
const drawNote = function(elem, startx, verticalPos, msg, forceWidth) {
const rect = svgDraw.getNoteRect();
rect.x = startx;
rect.y = verticalPos;
rect.width = forceWidth || conf.width;
rect.class = 'note';
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
);
bounds.insert(
startx,
verticalPos,
startx + rect.width,
verticalPos + 2 * conf.noteMargin + textHeight
);
rectElem.attr('height', textHeight + 2 * conf.noteMargin);
bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin);
};
/**
* Draws a message
* @param elem
* @param startx
* @param stopx
* @param verticalPos
* @param txtCenter
* @param msg
*/
const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) {
const g = elem.append('g');
const txtCenter = startx + (stopx - startx) / 2;
let textElem;
let counterBreaklines = 0;
let breaklineOffset = 17;
const breaklines = msg.message.split(/<br\s*\/?>/gi);
for (const breakline of breaklines) {
textElem = g
.append('text') // text label for the x axis
.attr('x', txtCenter)
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
.style('text-anchor', 'middle')
.attr('class', 'messageText')
.text(breakline.trim());
counterBreaklines++;
}
const offsetLineCounter = counterBreaklines - 1;
const totalOffset = offsetLineCounter * breaklineOffset;
let textWidth = (textElem._groups || textElem)[0][0].getBBox().width;
let line;
if (startx === stopx) {
if (conf.rightAngles) {
line = g
.append('path')
.attr(
'd',
`M ${startx},${verticalPos + totalOffset} H ${startx + conf.width / 2} V ${verticalPos +
25 +
totalOffset} H ${startx}`
);
} else {
line = g
.append('path')
.attr(
'd',
'M ' +
startx +
',' +
(verticalPos + totalOffset) +
' C ' +
(startx + 60) +
',' +
(verticalPos - 10 + totalOffset) +
' ' +
(startx + 60) +
',' +
(verticalPos + 30 + totalOffset) +
' ' +
startx +
',' +
(verticalPos + 20 + totalOffset)
);
}
bounds.bumpVerticalPos(30 + totalOffset);
const dx = Math.max(textWidth / 2, 100);
bounds.insert(
startx - dx,
bounds.getVerticalPos() - 10 + totalOffset,
stopx + dx,
bounds.getVerticalPos() + totalOffset
);
} else {
line = g.append('line');
line.attr('x1', startx);
line.attr('y1', verticalPos);
line.attr('x2', stopx);
line.attr('y2', verticalPos);
bounds.insert(
startx,
bounds.getVerticalPos() - 10 + totalOffset,
stopx,
bounds.getVerticalPos() + totalOffset
);
}
// Make an SVG Container
// Draw the line
if (
msg.type === parser.yy.LINETYPE.DOTTED ||
msg.type === parser.yy.LINETYPE.DOTTED_CROSS ||
msg.type === parser.yy.LINETYPE.DOTTED_OPEN
) {
line.style('stroke-dasharray', '3, 3');
line.attr('class', 'messageLine1');
} else {
line.attr('class', 'messageLine0');
}
let url = '';
if (conf.arrowMarkerAbsolute) {
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
}
line.attr('stroke-width', 2);
line.attr('stroke', 'black');
line.style('fill', 'none'); // remove any fill colour
if (msg.type === parser.yy.LINETYPE.SOLID || msg.type === parser.yy.LINETYPE.DOTTED) {
line.attr('marker-end', 'url(' + url + '#arrowhead)');
}
if (msg.type === parser.yy.LINETYPE.SOLID_CROSS || msg.type === parser.yy.LINETYPE.DOTTED_CROSS) {
line.attr('marker-end', 'url(' + url + '#crosshead)');
}
// add node number
if (sequenceDb.showSequenceNumbers() || conf.showSequenceNumbers) {
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
g.append('text')
.attr('x', startx)
.attr('y', verticalPos + 4)
.attr('font-family', 'sans-serif')
.attr('font-size', '12px')
.attr('text-anchor', 'middle')
.attr('textLength', '16px')
.attr('class', 'sequenceNumber')
.text(sequenceIndex);
}
};
export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
// Draw the actors
for (let i = 0; i < actorKeys.length; i++) {
const key = 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;
// 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);
}
// Add a margin between the actor boxes and the first arrow
bounds.bumpVerticalPos(conf.height);
};
export const setConf = function(cnf) {
const keys = Object.keys(cnf);
keys.forEach(function(key) {
conf[key] = cnf[key];
});
conf.actorFontFamily = cnf.fontFamily;
};
const actorActivations = function(actor) {
return bounds.activations.filter(function(activation) {
return activation.actor === actor;
});
};
const actorFlowVerticaBounds = function(actor) {
// handle multiple stacked activations for same actor
const actors = parser.yy.getActors();
const activations = actorActivations(actor);
const left = activations.reduce(function(acc, activation) {
return Math.min(acc, activation.startx);
}, actors[actor].x + conf.width / 2);
const right = activations.reduce(function(acc, activation) {
return Math.max(acc, activation.stopx);
}, actors[actor].x + conf.width / 2);
return [left, right];
};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text
* @param id
*/
export const draw = function(text, id) {
parser.yy.clear();
parser.parse(text + '\n');
bounds.init();
const diagram = d3.select(`[id="${id}"]`);
let startx;
let stopx;
let forceWidth;
// Fetch data from the parsing
const actors = parser.yy.getActors();
const actorKeys = parser.yy.getActorKeys();
const messages = parser.yy.getMessages();
const title = parser.yy.getTitle();
drawActors(diagram, actors, actorKeys, 0);
// The arrow head definition is attached to the svg once
svgDraw.insertArrowHead(diagram);
svgDraw.insertArrowCrossHead(diagram);
svgDraw.insertSequenceNumber(diagram);
function activeEnd(msg, verticalPos) {
const activationData = bounds.endActivation(msg);
if (activationData.starty + 18 > verticalPos) {
activationData.starty = verticalPos - 6;
verticalPos += 12;
}
svgDraw.drawActivation(
diagram,
activationData,
verticalPos,
conf,
actorActivations(msg.from.actor).length
);
bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos);
}
// const lastMsg
// Draw the messages/signals
let sequenceIndex = 1;
messages.forEach(function(msg) {
let loopData;
switch (msg.type) {
case parser.yy.LINETYPE.NOTE:
bounds.bumpVerticalPos(conf.boxMargin);
startx = actors[msg.from].x;
stopx = actors[msg.to].x;
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
drawNote(
diagram,
startx + (conf.width + conf.actorMargin) / 2,
bounds.getVerticalPos(),
msg
);
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
drawNote(
diagram,
startx - (conf.width + conf.actorMargin) / 2,
bounds.getVerticalPos(),
msg
);
} else if (msg.to === msg.from) {
// Single-actor over
drawNote(diagram, startx, bounds.getVerticalPos(), msg);
} else {
// Multi-actor over
forceWidth = Math.abs(startx - stopx) + conf.actorMargin;
drawNote(
diagram,
(startx + stopx + conf.width - forceWidth) / 2,
bounds.getVerticalPos(),
msg,
forceWidth
);
}
break;
case parser.yy.LINETYPE.ACTIVE_START:
bounds.newActivation(msg, diagram);
break;
case parser.yy.LINETYPE.ACTIVE_END:
activeEnd(msg, bounds.getVerticalPos());
break;
case parser.yy.LINETYPE.LOOP_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break;
case parser.yy.LINETYPE.LOOP_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'loop', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.RECT_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(undefined, msg.message);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.RECT_END: {
const rectData = bounds.endLoop();
svgDraw.drawBackgroundRect(diagram, rectData);
bounds.bumpVerticalPos(conf.boxMargin);
break;
}
case parser.yy.LINETYPE.OPT_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break;
case parser.yy.LINETYPE.OPT_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'opt', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.ALT_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break;
case parser.yy.LINETYPE.ALT_ELSE:
bounds.bumpVerticalPos(conf.boxMargin);
loopData = bounds.addSectionToLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.ALT_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'alt', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.PAR_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break;
case parser.yy.LINETYPE.PAR_AND:
bounds.bumpVerticalPos(conf.boxMargin);
loopData = bounds.addSectionToLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.PAR_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'par', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
default:
try {
// lastMsg = msg
bounds.bumpVerticalPos(conf.messageMargin);
const fromBounds = actorFlowVerticaBounds(msg.from);
const toBounds = actorFlowVerticaBounds(msg.to);
const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
startx = fromBounds[fromIdx];
stopx = toBounds[toIdx];
const verticalPos = bounds.getVerticalPos();
drawMessage(diagram, startx, stopx, verticalPos, msg, sequenceIndex);
const allBounds = fromBounds.concat(toBounds);
bounds.insert(
Math.min.apply(null, allBounds),
verticalPos,
Math.max.apply(null, allBounds),
verticalPos
);
} catch (e) {
logger.error('error while drawing message', e);
}
}
// Increment sequence counter if msg.type is a line (and not another event like activation or note, etc)
if (
[
parser.yy.LINETYPE.SOLID_OPEN,
parser.yy.LINETYPE.DOTTED_OPEN,
parser.yy.LINETYPE.SOLID,
parser.yy.LINETYPE.DOTTED,
parser.yy.LINETYPE.SOLID_CROSS,
parser.yy.LINETYPE.DOTTED_CROSS
].includes(msg.type)
) {
sequenceIndex++;
}
});
if (conf.mirrorActors) {
// Draw actors below diagram
bounds.bumpVerticalPos(conf.boxMargin * 2);
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos());
}
const box = bounds.getBounds();
// Adjust line height of actor lines now that the height of the diagram is known
logger.debug('For line height fix Querying: #' + id + ' .actor-line');
const actorLines = d3.selectAll('#' + id + ' .actor-line');
actorLines.attr('y2', box.stopy);
let height = box.stopy - box.starty + 2 * conf.diagramMarginY;
if (conf.mirrorActors) {
height = height - conf.boxMargin + conf.bottomMarginAdj;
}
const width = box.stopx - box.startx + 2 * conf.diagramMarginX;
if (title) {
diagram
.append('text')
.text(title)
.attr('x', (box.stopx - box.startx) / 2 - 2 * conf.diagramMarginX)
.attr('y', -25);
}
if (conf.useMaxWidth) {
diagram.attr('height', '100%');
diagram.attr('width', '100%');
diagram.attr('style', 'max-width:' + width + 'px;');
} else {
diagram.attr('height', height);
diagram.attr('width', width);
}
const extraVertForTitle = title ? 40 : 0;
diagram.attr(
'viewBox',
box.startx -
conf.diagramMarginX +
' -' +
(conf.diagramMarginY + extraVertForTitle) +
' ' +
width +
' ' +
(height + extraVertForTitle)
);
};
export default {
bounds,
drawActors,
setConf,
draw
};