mermaid/src/diagrams/class/classRenderer.js

571 lines
15 KiB
JavaScript
Raw Normal View History

import * as d3 from 'd3';
2019-10-21 14:34:04 +05:30
import dagre from 'dagre';
import graphlib from 'graphlib';
import { logger } from '../../logger';
import classDb, { lookUpDomId } from './classDb';
import utils from '../../utils';
import { parser } from './parser/classDiagram';
2017-09-10 19:41:34 +08:00
parser.yy = classDb;
2017-04-11 22:14:25 +08:00
let idCache = {};
2017-04-11 22:14:25 +08:00
2017-09-14 20:12:54 +08:00
const conf = {
2017-04-11 22:14:25 +08:00
dividerMargin: 10,
padding: 5,
2017-04-22 22:25:07 +08:00
textHeight: 10
};
2015-10-30 10:47:25 +01:00
// Todo optimize
const getGraphId = function(label) {
const keys = Object.keys(idCache);
2015-11-15 15:06:24 +01:00
2017-09-14 20:12:54 +08:00
for (let i = 0; i < keys.length; i++) {
2017-04-11 22:14:25 +08:00
if (idCache[keys[i]].label === label) {
return keys[i];
2015-10-30 10:47:25 +01:00
}
2017-04-11 22:14:25 +08:00
}
2015-11-15 15:06:24 +01:00
return undefined;
};
2015-10-30 10:47:25 +01:00
/**
* Setup arrow head and define the marker. The result is appended to the svg.
*/
const insertMarkers = function(elem) {
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'extensionStart')
.attr('class', 'extension')
.attr('refX', 0)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 1,7 L18,13 V 1 Z');
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'extensionEnd')
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'compositionStart')
.attr('class', 'extension')
.attr('refX', 0)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'compositionEnd')
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'aggregationStart')
.attr('class', 'extension')
.attr('refX', 0)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'aggregationEnd')
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'dependencyStart')
.attr('class', 'extension')
.attr('refX', 0)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
2019-02-12 10:27:05 +02:00
elem
2019-06-14 01:22:46 +03:00
.append('defs')
.append('marker')
.attr('id', 'dependencyEnd')
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
};
2019-06-14 01:22:46 +03:00
let edgeCount = 0;
const drawEdge = function(elem, path, relation) {
const getRelationType = function(type) {
2017-04-11 22:14:25 +08:00
switch (type) {
2017-09-10 21:23:04 +08:00
case classDb.relationType.AGGREGATION:
return 'aggregation';
2017-09-10 21:23:04 +08:00
case classDb.relationType.EXTENSION:
return 'extension';
2017-09-10 21:23:04 +08:00
case classDb.relationType.COMPOSITION:
return 'composition';
2017-09-10 21:23:04 +08:00
case classDb.relationType.DEPENDENCY:
return 'dependency';
2017-04-11 22:14:25 +08:00
}
};
2019-02-12 10:27:05 +02:00
path.points = path.points.filter(p => !Number.isNaN(p.y));
2017-04-11 22:14:25 +08:00
2017-04-16 23:48:36 +08:00
// The data for our line
const lineData = path.points;
2017-04-11 22:14:25 +08:00
2017-04-16 23:48:36 +08:00
// This is the accessor function we talked about above
2019-02-12 10:27:05 +02:00
const lineFunction = d3
.line()
.x(function(d) {
return d.x;
2017-04-16 23:48:36 +08:00
})
.y(function(d) {
return d.y;
2017-04-16 23:48:36 +08:00
})
.curve(d3.curveBasis);
2019-02-12 10:27:05 +02:00
const svgPath = elem
2019-06-14 01:22:46 +03:00
.append('path')
.attr('d', lineFunction(lineData))
.attr('id', 'edge' + edgeCount)
.attr('class', 'relation');
let url = '';
2017-04-11 22:14:25 +08:00
if (conf.arrowMarkerAbsolute) {
2019-02-12 10:27:05 +02:00
url =
window.location.protocol +
2019-06-14 01:22:46 +03:00
'//' +
2019-02-12 10:27:05 +02:00
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
2017-04-11 22:14:25 +08:00
}
if (relation.relation.lineType == 1) {
svgPath.attr('class', 'relation dashed-line');
}
2019-06-14 01:22:46 +03:00
if (relation.relation.type1 !== 'none') {
2019-02-12 10:27:05 +02:00
svgPath.attr(
2019-06-14 01:22:46 +03:00
'marker-start',
'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')'
);
2017-04-11 22:14:25 +08:00
}
2019-06-14 01:22:46 +03:00
if (relation.relation.type2 !== 'none') {
2019-02-12 10:27:05 +02:00
svgPath.attr(
2019-06-14 01:22:46 +03:00
'marker-end',
'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')'
);
2017-04-11 22:14:25 +08:00
}
let x, y;
const l = path.points.length;
// Calculate Label position
let labalPosition = utils.calcLabelPosition(path.points);
x = labalPosition.x;
y = labalPosition.y;
2019-10-27 15:24:56 +01:00
let p1_card_x, p1_card_y;
// p1_card_padd_x = conf.padding * 2,
// p1_card_padd_y = conf.padding;
let p2_card_x, p2_card_y;
// p2_card_padd_x = conf.padding * 2,
// p2_card_padd_y = -conf.padding / 2;
2019-02-12 10:27:05 +02:00
if (l % 2 !== 0 && l > 1) {
let cardinality_1_point = utils.calcCardinalityPosition(
relation.relation.type1 !== 'none',
path.points,
path.points[0]
);
let cardinality_2_point = utils.calcCardinalityPosition(
relation.relation.type2 !== 'none',
path.points,
path.points[l - 1]
);
logger.debug('cardinality_1_point ' + JSON.stringify(cardinality_1_point));
logger.debug('cardinality_2_point ' + JSON.stringify(cardinality_2_point));
p1_card_x = cardinality_1_point.x;
p1_card_y = cardinality_1_point.y;
p2_card_x = cardinality_2_point.x;
p2_card_y = cardinality_2_point.y;
2017-04-11 22:14:25 +08:00
}
2019-06-14 01:22:46 +03:00
if (typeof relation.title !== 'undefined') {
const g = elem.append('g').attr('class', 'classLabel');
2019-02-12 10:27:05 +02:00
const label = g
2019-06-14 01:22:46 +03:00
.append('text')
.attr('class', 'label')
.attr('x', x)
.attr('y', y)
.attr('fill', 'red')
.attr('text-anchor', 'middle')
.text(relation.title);
2019-06-14 01:22:46 +03:00
window.label = label;
const bounds = label.node().getBBox();
2019-06-14 01:22:46 +03:00
g.insert('rect', ':first-child')
.attr('class', 'box')
.attr('x', bounds.x - conf.padding / 2)
.attr('y', bounds.y - conf.padding / 2)
.attr('width', bounds.width + conf.padding)
.attr('height', bounds.height + conf.padding);
2017-04-11 22:14:25 +08:00
}
logger.info('Rendering relation ' + JSON.stringify(relation));
if (typeof relation.relationTitle1 !== 'undefined' && relation.relationTitle1 !== 'none') {
const g = elem.append('g').attr('class', 'cardinality');
2019-10-27 15:24:56 +01:00
g.append('text')
.attr('class', 'type1')
.attr('x', p1_card_x)
.attr('y', p1_card_y)
.attr('fill', 'black')
.attr('font-size', '6')
.text(relation.relationTitle1);
}
if (typeof relation.relationTitle2 !== 'undefined' && relation.relationTitle2 !== 'none') {
const g = elem.append('g').attr('class', 'cardinality');
2019-10-27 15:24:56 +01:00
g.append('text')
.attr('class', 'type2')
.attr('x', p2_card_x)
.attr('y', p2_card_y)
.attr('fill', 'black')
.attr('font-size', '6')
.text(relation.relationTitle2);
}
edgeCount++;
};
const drawClass = function(elem, classDef) {
logger.info('Rendering class ' + classDef);
let cssClassStr = 'classGroup ';
if (classDef.cssClasses.length > 0) {
cssClassStr = cssClassStr + classDef.cssClasses.join(' ');
}
const addTspan = function(textEl, txt, isFirst) {
let isMethod = txt.indexOf(')') > 1;
let displayText = txt;
let cssStyle = '';
if (isMethod) {
let method = buildDisplayTextForMethod(txt);
displayText = method.displayText;
cssStyle = method.cssStyle;
}
2019-02-12 10:27:05 +02:00
const tSpan = textEl
2019-06-14 01:22:46 +03:00
.append('tspan')
.attr('x', conf.padding)
.text(displayText);
if (cssStyle !== '') {
tSpan.attr('style', cssStyle);
}
2017-04-11 22:14:25 +08:00
if (!isFirst) {
tSpan.attr('dy', conf.textHeight);
2017-04-11 22:14:25 +08:00
}
};
2015-10-30 10:47:25 +01:00
const id = classDef.id;
const buildDisplayTextForMethod = function(txt) {
let regEx = /(\+|-|~|#)?(\w+)\((\w+\[?\]?)?\s?(\w+)?\)([*|$])?\s?(\w+\[?\]?)?/;
let cssStyle = '';
let displayText = txt;
let methodName = txt;
let classifier = '';
let parsedText = txt.match(regEx);
if (parsedText) {
let visibility = parsedText[1] ? parsedText[1] : '';
methodName = parsedText[2] ? parsedText[2] : '';
let parameterType = parsedText[3] ? parsedText[3] : '';
let parameterName = parsedText[4] ? parsedText[4] : '';
classifier = parsedText[5] ? parsedText[5] : '';
let returnType = parsedText[6] ? ' : ' + parsedText[6] : '';
displayText =
visibility + methodName + '(' + parameterType + ' ' + parameterName + ')' + returnType;
}
switch (classifier) {
case '*':
cssStyle = 'font-style:italic;';
break;
case '$':
cssStyle = 'text-decoration:underline;';
break;
}
let method = {
methodname: methodName,
displayText: displayText,
cssStyle: cssStyle
};
return method;
2020-01-06 12:26:36 -08:00
};
2017-09-14 20:12:54 +08:00
const classInfo = {
2017-04-11 22:14:25 +08:00
id: id,
label: classDef.id,
width: 0,
height: 0
};
2019-02-12 10:27:05 +02:00
2019-10-05 12:38:29 +02:00
// add class group
2019-02-12 10:27:05 +02:00
const g = elem
2019-06-14 01:22:46 +03:00
.append('g')
.attr('id', lookUpDomId(id))
.attr('class', cssClassStr);
2019-10-05 12:38:29 +02:00
// add title
let title;
if (classDef.link) {
title = g
2020-01-02 07:59:36 -08:00
.append('svg:a')
.attr('xlink:href', classDef.link)
.attr('target', '_blank')
.append('text')
.attr('y', conf.textHeight + conf.padding)
.attr('x', 0);
2020-01-02 07:59:36 -08:00
} else {
title = g
.append('text')
.attr('y', conf.textHeight + conf.padding)
.attr('x', 0);
}
2019-10-04 21:09:49 +02:00
// add annotations
let isFirst = true;
classDef.annotations.forEach(function(member) {
const titleText2 = title.append('tspan').text('«' + member + '»');
if (!isFirst) titleText2.attr('dy', conf.textHeight);
isFirst = false;
});
let classTitleString = classDef.id;
if (classDef.type !== undefined && classDef.type !== '') {
classTitleString += '<' + classDef.type + '>';
}
2019-10-04 21:49:02 +02:00
const classTitle = title
2019-10-04 21:09:49 +02:00
.append('tspan')
.text(classTitleString)
2019-10-04 21:49:02 +02:00
.attr('class', 'title');
2020-01-02 07:59:36 -08:00
2019-10-05 12:38:29 +02:00
// If class has annotations the title needs to have an offset of the text height
2019-10-04 21:49:02 +02:00
if (!isFirst) classTitle.attr('dy', conf.textHeight);
2019-02-12 10:27:05 +02:00
const titleHeight = title.node().getBBox().height;
2019-02-12 10:27:05 +02:00
const membersLine = g
2019-06-14 01:22:46 +03:00
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
2019-02-12 10:27:05 +02:00
const members = g
2019-06-14 01:22:46 +03:00
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
2019-02-12 10:27:05 +02:00
2019-10-04 21:09:49 +02:00
isFirst = true;
classDef.members.forEach(function(member) {
addTspan(members, member, isFirst);
isFirst = false;
});
2019-02-12 10:27:05 +02:00
const membersBox = members.node().getBBox();
2019-02-12 10:27:05 +02:00
const methodsLine = g
2019-06-14 01:22:46 +03:00
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
2019-02-12 10:27:05 +02:00
const methods = g
2019-06-14 01:22:46 +03:00
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
2019-06-14 01:22:46 +03:00
.attr('fill', 'white')
.attr('class', 'classText');
2019-02-12 10:27:05 +02:00
isFirst = true;
2019-02-12 10:27:05 +02:00
classDef.methods.forEach(function(method) {
addTspan(methods, method, isFirst);
isFirst = false;
});
2019-02-12 10:27:05 +02:00
const classBox = g.node().getBBox();
2019-10-05 12:38:29 +02:00
const rect = g
.insert('rect', ':first-child')
2019-06-14 01:22:46 +03:00
.attr('x', 0)
.attr('y', 0)
.attr('width', classBox.width + 2 * conf.padding)
.attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin);
2019-02-12 10:27:05 +02:00
2019-10-05 12:38:29 +02:00
const rectWidth = rect.node().getBBox().width;
2019-10-04 21:09:49 +02:00
// Center title
2019-10-05 12:38:29 +02:00
// We subtract the width of each text element from the class box width and divide it by 2
2019-10-04 21:09:49 +02:00
title.node().childNodes.forEach(function(x) {
2019-10-05 12:38:29 +02:00
x.setAttribute('x', (rectWidth - x.getBBox().width) / 2);
2019-10-04 21:09:49 +02:00
});
if (classDef.tooltip) {
2020-01-02 11:27:56 -08:00
title.insert('title').text(classDef.tooltip);
}
2019-10-05 12:38:29 +02:00
membersLine.attr('x2', rectWidth);
methodsLine.attr('x2', rectWidth);
2019-02-12 10:27:05 +02:00
2019-10-05 12:38:29 +02:00
classInfo.width = rectWidth;
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
2019-02-12 10:27:05 +02:00
idCache[id] = classInfo;
return classInfo;
};
2019-02-12 10:27:05 +02:00
export const setConf = function(cnf) {
const keys = Object.keys(cnf);
2019-02-12 10:27:05 +02:00
keys.forEach(function(key) {
conf[key] = cnf[key];
});
};
/**
* 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) {
idCache = {};
parser.yy.clear();
parser.parse(text);
logger.info('Rendering diagram ' + text);
2015-10-30 10:47:25 +01:00
2017-04-16 23:48:36 +08:00
/// / Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id='${id}']`);
insertMarkers(diagram);
2017-04-16 23:48:36 +08:00
// Layout graph, Create a new directed graph
2018-03-06 16:04:40 +08:00
const g = new graphlib.Graph({
2017-04-11 22:14:25 +08:00
multigraph: true
});
2015-10-30 10:47:25 +01:00
2017-04-16 23:48:36 +08:00
// Set an object for the graph label
2017-04-11 22:14:25 +08:00
g.setGraph({
isMultiGraph: true
});
2015-10-30 10:47:25 +01:00
2017-04-16 23:48:36 +08:00
// Default to assigning a new object as a label for each new edge.
g.setDefaultEdgeLabel(function() {
return {};
});
2017-04-11 22:14:25 +08:00
const classes = classDb.getClasses();
const keys = Object.keys(classes);
2017-09-14 20:12:54 +08:00
for (let i = 0; i < keys.length; i++) {
const classDef = classes[keys[i]];
const node = drawClass(diagram, classDef);
2017-04-16 23:48:36 +08:00
// Add nodes to the graph. The first argument is the node id. The second is
// metadata about the node. In this case we're going to add labels to each of
// our nodes.
g.setNode(node.id, node);
logger.info('Org height: ' + node.height);
2017-04-11 22:14:25 +08:00
}
const relations = classDb.getRelations();
relations.forEach(function(relation) {
2019-02-12 10:27:05 +02:00
logger.info(
'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation)
);
2019-02-12 10:27:05 +02:00
g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), {
relation: relation
});
});
dagre.layout(g);
g.nodes().forEach(function(v) {
2019-06-14 01:22:46 +03:00
if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') {
logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v)));
d3.select('#' + lookUpDomId(v)).attr(
2019-06-14 01:22:46 +03:00
'transform',
'translate(' +
2019-02-12 10:27:05 +02:00
(g.node(v).x - g.node(v).width / 2) +
2019-06-14 01:22:46 +03:00
',' +
2019-02-12 10:27:05 +02:00
(g.node(v).y - g.node(v).height / 2) +
2019-06-14 01:22:46 +03:00
' )'
);
2019-02-12 10:27:05 +02:00
}
});
g.edges().forEach(function(e) {
2019-06-14 01:22:46 +03:00
if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') {
logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e)));
drawEdge(diagram, g.edge(e), g.edge(e).relation);
2017-04-11 22:14:25 +08:00
}
});
2015-10-30 10:47:25 +01:00
diagram.attr('height', '100%');
diagram.attr('width', `${g.graph().width * 1.5 + 20}`);
diagram.attr('viewBox', '-10 -10 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20));
};
2017-09-10 22:03:10 +08:00
export default {
setConf,
draw
};