mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-28 16:44:12 +01:00
User journey handler
This commit is contained in:
429
src/diagrams/user-journey/svgDraw.js
Normal file
429
src/diagrams/user-journey/svgDraw.js
Normal file
@@ -0,0 +1,429 @@
|
||||
import * as d3 from 'd3';
|
||||
|
||||
export const drawRect = function(elem, rectData) {
|
||||
const rectElem = elem.append('rect');
|
||||
rectElem.attr('x', rectData.x);
|
||||
rectElem.attr('y', rectData.y);
|
||||
rectElem.attr('fill', rectData.fill);
|
||||
rectElem.attr('stroke', rectData.stroke);
|
||||
rectElem.attr('width', rectData.width);
|
||||
rectElem.attr('height', rectData.height);
|
||||
rectElem.attr('rx', rectData.rx);
|
||||
rectElem.attr('ry', rectData.ry);
|
||||
|
||||
if (typeof rectData.class !== 'undefined') {
|
||||
rectElem.attr('class', rectData.class);
|
||||
}
|
||||
|
||||
return rectElem;
|
||||
};
|
||||
|
||||
export const drawFace = function(element, faceData) {
|
||||
const radius = 15;
|
||||
const circleElement = element
|
||||
.append('circle')
|
||||
.attr('cx', faceData.cx)
|
||||
.attr('cy', faceData.cy)
|
||||
.attr('fill', '#FFF8DC')
|
||||
.attr('stroke', '#999')
|
||||
.attr('r', radius)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('overflow', 'visible');
|
||||
|
||||
const face = element.append('g');
|
||||
|
||||
//left eye
|
||||
face
|
||||
.append('circle')
|
||||
.attr('cx', faceData.cx - radius / 3)
|
||||
.attr('cy', faceData.cy - radius / 3)
|
||||
.attr('r', 1.5)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('fill', '#666')
|
||||
.attr('stroke', '#666');
|
||||
|
||||
//right eye
|
||||
face
|
||||
.append('circle')
|
||||
.attr('cx', faceData.cx + radius / 3)
|
||||
.attr('cy', faceData.cy - radius / 3)
|
||||
.attr('r', 1.5)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('fill', '#666')
|
||||
.attr('stroke', '#666');
|
||||
|
||||
function smile(face) {
|
||||
const arc = d3
|
||||
.arc()
|
||||
.startAngle(Math.PI / 2)
|
||||
.endAngle(3 * (Math.PI / 2))
|
||||
.innerRadius(radius / 2)
|
||||
.outerRadius(radius / 2.2);
|
||||
//mouth
|
||||
face
|
||||
.append('path')
|
||||
.attr('d', arc)
|
||||
.attr('transform', 'translate(' + faceData.cx + ',' + (faceData.cy + 2) + ')');
|
||||
}
|
||||
|
||||
function sad(face) {
|
||||
const arc = d3
|
||||
.arc()
|
||||
.startAngle((3 * Math.PI) / 2)
|
||||
.endAngle(5 * (Math.PI / 2))
|
||||
.innerRadius(radius / 2)
|
||||
.outerRadius(radius / 2.2);
|
||||
//mouth
|
||||
face
|
||||
.append('path')
|
||||
.attr('d', arc)
|
||||
.attr('transform', 'translate(' + faceData.cx + ',' + (faceData.cy + 7) + ')');
|
||||
}
|
||||
|
||||
function ambivalent(face) {
|
||||
face
|
||||
.append('line')
|
||||
.attr('stroke', 2)
|
||||
.attr('x1', faceData.cx - 5)
|
||||
.attr('y1', faceData.cy + 7)
|
||||
.attr('x2', faceData.cx + 5)
|
||||
.attr('y2', faceData.cy + 7)
|
||||
.attr('class', 'task-line')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('stroke', '#666');
|
||||
}
|
||||
|
||||
if (faceData.score > 3) {
|
||||
smile(face);
|
||||
} else if (faceData.score < 3) {
|
||||
sad(face);
|
||||
} else {
|
||||
ambivalent(face);
|
||||
}
|
||||
|
||||
return circleElement;
|
||||
};
|
||||
|
||||
export const drawCircle = function(element, circleData) {
|
||||
const circleElement = element.append('circle');
|
||||
circleElement.attr('cx', circleData.cx);
|
||||
circleElement.attr('cy', circleData.cy);
|
||||
circleElement.attr('fill', circleData.fill);
|
||||
circleElement.attr('stroke', circleData.stroke);
|
||||
circleElement.attr('r', circleData.r);
|
||||
|
||||
if (typeof circleElement.class !== 'undefined') {
|
||||
circleElement.attr('class', circleElement.class);
|
||||
}
|
||||
|
||||
if (typeof circleData.title !== 'undefined') {
|
||||
circleElement.append('title').text(circleData.title);
|
||||
}
|
||||
|
||||
return circleElement;
|
||||
};
|
||||
|
||||
export const drawText = function(elem, textData) {
|
||||
// Remove and ignore br:s
|
||||
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
|
||||
|
||||
const textElem = elem.append('text');
|
||||
textElem.attr('x', textData.x);
|
||||
textElem.attr('y', textData.y);
|
||||
textElem.attr('fill', textData.fill);
|
||||
textElem.style('text-anchor', textData.anchor);
|
||||
|
||||
if (typeof textData.class !== 'undefined') {
|
||||
textElem.attr('class', textData.class);
|
||||
}
|
||||
|
||||
const span = textElem.append('tspan');
|
||||
span.attr('x', textData.x + textData.textMargin * 2);
|
||||
span.text(nText);
|
||||
|
||||
return textElem;
|
||||
};
|
||||
|
||||
export const drawLabel = function(elem, txtObject) {
|
||||
function genPoints(x, y, width, height, cut) {
|
||||
return (
|
||||
x +
|
||||
',' +
|
||||
y +
|
||||
' ' +
|
||||
(x + width) +
|
||||
',' +
|
||||
y +
|
||||
' ' +
|
||||
(x + width) +
|
||||
',' +
|
||||
(y + height - cut) +
|
||||
' ' +
|
||||
(x + width - cut * 1.2) +
|
||||
',' +
|
||||
(y + height) +
|
||||
' ' +
|
||||
x +
|
||||
',' +
|
||||
(y + height)
|
||||
);
|
||||
}
|
||||
const polygon = elem.append('polygon');
|
||||
polygon.attr('points', genPoints(txtObject.x, txtObject.y, 50, 20, 7));
|
||||
polygon.attr('class', 'labelBox');
|
||||
|
||||
txtObject.y = txtObject.y + txtObject.labelMargin;
|
||||
txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin;
|
||||
drawText(elem, txtObject);
|
||||
};
|
||||
|
||||
export const drawSection = function(elem, section, conf) {
|
||||
const g = elem.append('g');
|
||||
|
||||
const rect = getNoteRect();
|
||||
rect.x = section.x;
|
||||
rect.y = section.y;
|
||||
rect.fill = section.fill;
|
||||
rect.width = conf.width;
|
||||
rect.height = conf.height;
|
||||
rect.class = 'journey-section';
|
||||
rect.rx = 3;
|
||||
rect.ry = 3;
|
||||
drawRect(g, rect);
|
||||
|
||||
_drawTextCandidateFunc(conf)(
|
||||
section.text,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: 'journey-section' },
|
||||
conf,
|
||||
section.colour
|
||||
);
|
||||
};
|
||||
|
||||
let taskCount = -1;
|
||||
/**
|
||||
* Draws an actor in the diagram with the attaced line
|
||||
* @param elem The HTML element
|
||||
* @param task The task to render
|
||||
* @param conf The global configuration
|
||||
*/
|
||||
export const drawTask = function(elem, task, conf) {
|
||||
const center = task.x + conf.width / 2;
|
||||
const g = elem.append('g');
|
||||
taskCount++;
|
||||
const maxHeight = 300 + 5 * 30;
|
||||
g.append('line')
|
||||
.attr('id', 'task' + taskCount)
|
||||
.attr('x1', center)
|
||||
.attr('y1', task.y)
|
||||
.attr('x2', center)
|
||||
.attr('y2', maxHeight)
|
||||
.attr('class', 'task-line')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('stroke-dasharray', '4 2')
|
||||
.attr('stroke', '#666');
|
||||
|
||||
drawFace(g, {
|
||||
cx: center,
|
||||
cy: 300 + (5 - task.score) * 30,
|
||||
score: task.score
|
||||
});
|
||||
|
||||
const rect = getNoteRect();
|
||||
rect.x = task.x;
|
||||
rect.y = task.y;
|
||||
rect.fill = task.fill;
|
||||
rect.width = conf.width;
|
||||
rect.height = conf.height;
|
||||
rect.class = 'task';
|
||||
rect.rx = 3;
|
||||
rect.ry = 3;
|
||||
drawRect(g, rect);
|
||||
|
||||
let xPos = task.x + 14;
|
||||
task.people.forEach(person => {
|
||||
const colour = task.actors[person];
|
||||
|
||||
const circle = {
|
||||
cx: xPos,
|
||||
cy: task.y,
|
||||
r: 7,
|
||||
fill: colour,
|
||||
stroke: '#000',
|
||||
title: person
|
||||
};
|
||||
|
||||
drawCircle(g, circle);
|
||||
xPos += 10;
|
||||
});
|
||||
|
||||
_drawTextCandidateFunc(conf)(
|
||||
task.task,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: 'task' },
|
||||
conf,
|
||||
task.colour
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a background rectangle
|
||||
* @param elem The html element
|
||||
* @param bounds The bounds of the drawing
|
||||
*/
|
||||
export const drawBackgroundRect = function(elem, bounds) {
|
||||
const rectElem = drawRect(elem, {
|
||||
x: bounds.startx,
|
||||
y: bounds.starty,
|
||||
width: bounds.stopx - bounds.startx,
|
||||
height: bounds.stopy - bounds.starty,
|
||||
fill: bounds.fill,
|
||||
class: 'rect'
|
||||
});
|
||||
rectElem.lower();
|
||||
};
|
||||
|
||||
export const getTextObj = function() {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: undefined,
|
||||
'text-anchor': 'start',
|
||||
width: 100,
|
||||
height: 100,
|
||||
textMargin: 0,
|
||||
rx: 0,
|
||||
ry: 0
|
||||
};
|
||||
};
|
||||
|
||||
export const getNoteRect = function() {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
anchor: 'start',
|
||||
height: 100,
|
||||
rx: 0,
|
||||
ry: 0
|
||||
};
|
||||
};
|
||||
|
||||
const _drawTextCandidateFunc = (function() {
|
||||
function byText(content, g, x, y, width, height, textAttrs, colour) {
|
||||
const text = g
|
||||
.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y + height / 2 + 5)
|
||||
.style('font-color', colour)
|
||||
.style('text-anchor', 'middle')
|
||||
.text(content);
|
||||
_setTextAttrs(text, textAttrs);
|
||||
}
|
||||
|
||||
function byTspan(content, g, x, y, width, height, textAttrs, conf, colour) {
|
||||
const { taskFontSize, taskFontFamily } = conf;
|
||||
|
||||
const lines = content.split(/<br\s*\/?>/gi);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const dy = i * taskFontSize - (taskFontSize * (lines.length - 1)) / 2;
|
||||
const text = g
|
||||
.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y)
|
||||
.attr('fill', colour)
|
||||
.style('text-anchor', 'middle')
|
||||
.style('font-size', taskFontSize)
|
||||
.style('font-family', taskFontFamily);
|
||||
text
|
||||
.append('tspan')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('dy', dy)
|
||||
.text(lines[i]);
|
||||
|
||||
text
|
||||
.attr('y', y + height / 2.0)
|
||||
.attr('dominant-baseline', 'central')
|
||||
.attr('alignment-baseline', 'central');
|
||||
|
||||
_setTextAttrs(text, textAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
function byFo(content, g, x, y, width, height, textAttrs, conf, colour) {
|
||||
const body = g.append('switch');
|
||||
const f = body
|
||||
.append('foreignObject')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('position', 'fixed');
|
||||
|
||||
const text = f
|
||||
.append('div')
|
||||
.style('display', 'table')
|
||||
.style('height', '100%')
|
||||
.style('width', '100%');
|
||||
|
||||
text
|
||||
.append('div')
|
||||
.style('display', 'table-cell')
|
||||
.style('text-align', 'center')
|
||||
.style('vertical-align', 'middle')
|
||||
.style('color', colour)
|
||||
.text(content);
|
||||
|
||||
byTspan(content, body, x, y, width, height, textAttrs, conf);
|
||||
_setTextAttrs(text, textAttrs);
|
||||
}
|
||||
|
||||
function _setTextAttrs(toText, fromTextAttrsDict) {
|
||||
for (const key in fromTextAttrsDict) {
|
||||
if (key in fromTextAttrsDict) {
|
||||
// eslint-disable-line
|
||||
// noinspection JSUnfilteredForInLoop
|
||||
toText.attr(key, fromTextAttrsDict[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function(conf) {
|
||||
return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan;
|
||||
};
|
||||
})();
|
||||
|
||||
const initGraphics = function(graphics) {
|
||||
graphics
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('refX', 5)
|
||||
.attr('refY', 2)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 4)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 0,0 V 4 L6,2 Z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
export default {
|
||||
drawRect,
|
||||
drawCircle,
|
||||
drawSection,
|
||||
drawText,
|
||||
drawLabel,
|
||||
drawTask,
|
||||
drawBackgroundRect,
|
||||
getTextObj,
|
||||
getNoteRect,
|
||||
initGraphics
|
||||
};
|
||||
Reference in New Issue
Block a user