mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
Merge pull request #2237 from RonaldZielaznicki/2070_more_columns_for_entity_relationship_diagrams
2070: Update ER Diagram to have keys and comments.
This commit is contained in:
commit
b6ba4b2fd8
@ -186,4 +186,15 @@ describe('Entity Relationship Diagram', () => {
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render entities with keys and comments', () => {
|
||||
renderGraph(
|
||||
`
|
||||
erDiagram
|
||||
BOOK { string title PK "comment"}
|
||||
`,
|
||||
{ logLevel : 1 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -191,6 +191,28 @@ erDiagram
|
||||
|
||||
The `type` and `name` values must begin with an alphabetic character and may contain digits, hyphens or underscores. Other than that, there are no restrictions, and there is no implicit set of valid data types.
|
||||
|
||||
#### Attribute Keys and Comments
|
||||
|
||||
Attributes may also have a `key` or comment defined. Keys can be "PK" or "FK", for Primary Key or Foreign Key. And a `comment` is defined by quotes at the end of an attribute. Comments themselves cannot have quote characters in them.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
CAR {
|
||||
string allowedDriver FK 'The license of the allowed driver'
|
||||
string registrationNumber
|
||||
string make
|
||||
string model
|
||||
}
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
PERSON {
|
||||
string driversLicense PK 'The license #'
|
||||
string firstName
|
||||
string lastName
|
||||
int age
|
||||
}
|
||||
```
|
||||
|
||||
### Other Things
|
||||
|
||||
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
|
||||
|
@ -35,13 +35,20 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
const attrFontSize = conf.fontSize * 0.85;
|
||||
const labelBBox = entityTextNode.node().getBBox();
|
||||
const attributeNodes = []; // Intermediate storage for attribute nodes created so that we can do a second pass
|
||||
let hasKeyType = false;
|
||||
let hasComment = false;
|
||||
let maxWidth = 0;
|
||||
let maxTypeWidth = 0;
|
||||
let maxNameWidth = 0;
|
||||
let maxKeyWidth = 0;
|
||||
let maxCommentWidth = 0;
|
||||
let cumulativeHeight = labelBBox.height + heightPadding * 2;
|
||||
let attrNum = 1;
|
||||
|
||||
attributes.forEach((item) => {
|
||||
const attrPrefix = `${entityTextNode.node().id}-attr-${attrNum}`;
|
||||
let nodeWidth = 0;
|
||||
let nodeHeight = 0;
|
||||
|
||||
// Add a text node for the attribute type
|
||||
const typeNode = groupNode
|
||||
@ -73,16 +80,70 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
)
|
||||
.text(item.attributeName);
|
||||
|
||||
// Keep a reference to the nodes so that we can iterate through them later
|
||||
attributeNodes.push({ tn: typeNode, nn: nameNode });
|
||||
const attributeNode = {};
|
||||
attributeNode.tn = typeNode;
|
||||
attributeNode.nn = nameNode;
|
||||
|
||||
const typeBBox = typeNode.node().getBBox();
|
||||
const nameBBox = nameNode.node().getBBox();
|
||||
|
||||
maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width);
|
||||
maxNameWidth = Math.max(maxNameWidth, nameBBox.width);
|
||||
nodeWidth += typeBBox.width;
|
||||
nodeWidth += nameBBox.width;
|
||||
|
||||
cumulativeHeight += Math.max(typeBBox.height, nameBBox.height) + heightPadding * 2;
|
||||
nodeHeight = Math.max(typeBBox.height, nameBBox.height);
|
||||
|
||||
if (hasKeyType || item.attributeKeyType !== undefined) {
|
||||
const keyTypeNode = groupNode
|
||||
.append('text')
|
||||
.attr('class', 'er entityLabel')
|
||||
.attr('id', `${attrPrefix}-name`)
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'left')
|
||||
.attr(
|
||||
'style',
|
||||
'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px'
|
||||
)
|
||||
.text(item.attributeKeyType || '');
|
||||
|
||||
attributeNode.kn = keyTypeNode;
|
||||
const keyTypeBBox = keyTypeNode.node().getBBox();
|
||||
nodeWidth += keyTypeBBox.width;
|
||||
maxKeyWidth = Math.max(maxKeyWidth, nodeWidth);
|
||||
nodeHeight = Math.max(nodeHeight, keyTypeBBox.height);
|
||||
hasKeyType = true;
|
||||
}
|
||||
|
||||
if (hasComment || item.attributeComment !== undefined) {
|
||||
const commentNode = groupNode
|
||||
.append('text')
|
||||
.attr('class', 'er entityLabel')
|
||||
.attr('id', `${attrPrefix}-name`)
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'left')
|
||||
.attr(
|
||||
'style',
|
||||
'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px'
|
||||
)
|
||||
.text(item.attributeComment || '');
|
||||
|
||||
attributeNode.cn = commentNode;
|
||||
const commentNodeBBox = commentNode.node().getBBox();
|
||||
nodeWidth += commentNodeBBox.width;
|
||||
maxCommentWidth = Math.max(nodeWidth, nameBBox.width);
|
||||
nodeHeight = Math.max(nodeHeight, commentNodeBBox.height);
|
||||
hasComment = true;
|
||||
}
|
||||
|
||||
attributeNode.height = nodeHeight;
|
||||
// Keep a reference to the nodes so that we can iterate through them later
|
||||
attributeNodes.push(attributeNode);
|
||||
maxWidth = Math.max(maxWidth, nodeWidth);
|
||||
cumulativeHeight += nodeHeight + heightPadding * 2;
|
||||
attrNum += 1;
|
||||
});
|
||||
|
||||
@ -90,10 +151,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
const bBox = {
|
||||
width: Math.max(
|
||||
conf.minEntityWidth,
|
||||
Math.max(
|
||||
labelBBox.width + conf.entityPadding * 2,
|
||||
maxTypeWidth + maxNameWidth + widthPadding * 4
|
||||
)
|
||||
Math.max(labelBBox.width + conf.entityPadding * 2, maxWidth + widthPadding * 4)
|
||||
),
|
||||
height:
|
||||
attributes.length > 0
|
||||
@ -102,7 +160,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
};
|
||||
|
||||
// There might be some spare width for padding out attributes if the entity name is very long
|
||||
const spareWidth = Math.max(0, bBox.width - (maxTypeWidth + maxNameWidth) - widthPadding * 4);
|
||||
const spareWidth = Math.max(0, bBox.width - maxWidth - widthPadding * 4);
|
||||
|
||||
if (attributes.length > 0) {
|
||||
// Position the entity label near the top of the entity bounding box
|
||||
@ -115,51 +173,85 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
let heightOffset = labelBBox.height + heightPadding * 2; // Start at the bottom of the entity label
|
||||
let attribStyle = 'attributeBoxOdd'; // We will flip the style on alternate rows to achieve a banded effect
|
||||
|
||||
attributeNodes.forEach((nodePair) => {
|
||||
attributeNodes.forEach((attributeNode) => {
|
||||
// Calculate the alignment y co-ordinate for the type/name of the attribute
|
||||
const alignY =
|
||||
heightOffset +
|
||||
heightPadding +
|
||||
Math.max(nodePair.tn.node().getBBox().height, nodePair.nn.node().getBBox().height) / 2;
|
||||
const alignY = heightOffset + heightPadding + attributeNode.height / 2;
|
||||
|
||||
// Position the type of the attribute
|
||||
nodePair.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')');
|
||||
attributeNode.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')');
|
||||
|
||||
// Insert a rectangle for the type
|
||||
const typeRect = groupNode
|
||||
.insert('rect', '#' + nodePair.tn.node().id)
|
||||
.insert('rect', '#' + attributeNode.tn.node().id)
|
||||
.attr('class', `er ${attribStyle}`)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', '100%')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', 0)
|
||||
.attr('y', heightOffset)
|
||||
.attr('width', maxTypeWidth + widthPadding * 2 + spareWidth / 2)
|
||||
.attr('height', nodePair.tn.node().getBBox().height + heightPadding * 2);
|
||||
.attr('width', maxTypeWidth * 2 + spareWidth / 2)
|
||||
.attr('height', attributeNode.tn.node().getBBox().height + heightPadding * 2);
|
||||
|
||||
// Position the name of the attribute
|
||||
nodePair.nn.attr(
|
||||
attributeNode.nn.attr(
|
||||
'transform',
|
||||
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')'
|
||||
);
|
||||
|
||||
// Insert a rectangle for the name
|
||||
groupNode
|
||||
.insert('rect', '#' + nodePair.nn.node().id)
|
||||
.insert('rect', '#' + attributeNode.nn.node().id)
|
||||
.attr('class', `er ${attribStyle}`)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', '100%')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`)
|
||||
//.attr('x', maxTypeWidth + (widthPadding * 2))
|
||||
.attr('y', heightOffset)
|
||||
.attr('width', maxNameWidth + widthPadding * 2 + spareWidth / 2)
|
||||
.attr('height', nodePair.nn.node().getBBox().height + heightPadding * 2);
|
||||
.attr('height', attributeNode.nn.node().getBBox().height + heightPadding * 2);
|
||||
|
||||
if (hasKeyType) {
|
||||
// Position the name of the attribute
|
||||
attributeNode.kn.attr(
|
||||
'transform',
|
||||
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')'
|
||||
);
|
||||
|
||||
// Insert a rectangle for the name
|
||||
groupNode
|
||||
.insert('rect', '#' + attributeNode.kn.node().id)
|
||||
.attr('class', `er ${attribStyle}`)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', '100%')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`)
|
||||
.attr('y', heightOffset)
|
||||
.attr('width', maxKeyWidth + widthPadding * 2 + spareWidth / 2)
|
||||
.attr('height', attributeNode.kn.node().getBBox().height + heightPadding * 2);
|
||||
}
|
||||
|
||||
if (hasComment) {
|
||||
// Position the name of the attribute
|
||||
attributeNode.cn.attr(
|
||||
'transform',
|
||||
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')'
|
||||
);
|
||||
|
||||
// Insert a rectangle for the name
|
||||
groupNode
|
||||
.insert('rect', '#' + attributeNode.cn.node().id)
|
||||
.attr('class', `er ${attribStyle}`)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', '100%')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`)
|
||||
.attr('y', heightOffset)
|
||||
.attr('width', maxCommentWidth + widthPadding * 2 + spareWidth / 2)
|
||||
.attr('height', attributeNode.cn.node().getBBox().height + heightPadding * 2);
|
||||
}
|
||||
|
||||
// Increment the height offset to move to the next row
|
||||
heightOffset +=
|
||||
Math.max(nodePair.tn.node().getBBox().height, nodePair.nn.node().getBBox().height) +
|
||||
heightPadding * 2;
|
||||
heightOffset += attributeNode.height + heightPadding * 2;
|
||||
|
||||
// Flip the attribute style for row banding
|
||||
attribStyle = attribStyle == 'attributeBoxOdd' ? 'attributeBoxEven' : 'attributeBoxOdd';
|
||||
|
@ -18,7 +18,9 @@
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
"{" { this.begin("block"); return 'BLOCK_START'; }
|
||||
<block>\s+ /* skip whitespace in block */
|
||||
<block>[A-Za-z][A-Za-z0-9\-_]* { return 'ATTRIBUTE_WORD'; }
|
||||
<block>(?:PK)|(?:FK) return 'ATTRIBUTE_KEY'
|
||||
<block>[A-Za-z][A-Za-z0-9\-_]* return 'ATTRIBUTE_WORD'
|
||||
<block>\"[^"]*\" return 'COMMENT';
|
||||
<block>[\n]+ /* nothing */
|
||||
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
|
||||
<block>. return yytext[0];
|
||||
@ -95,6 +97,9 @@ attributes
|
||||
|
||||
attribute
|
||||
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
|
||||
| attributeType attributeName attributeKeyType { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3 }; }
|
||||
| attributeType attributeName COMMENT { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
|
||||
| attributeType attributeName attributeKeyType COMMENT { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3, attributeComment: $4 }; }
|
||||
;
|
||||
|
||||
attributeType
|
||||
@ -105,6 +110,10 @@ attributeName
|
||||
: ATTRIBUTE_WORD { $$=$1; }
|
||||
;
|
||||
|
||||
attributeKeyType
|
||||
: ATTRIBUTE_KEY { $$=$1; }
|
||||
;
|
||||
|
||||
relSpec
|
||||
: cardinality relType cardinality
|
||||
{
|
||||
|
@ -42,6 +42,36 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined with a key', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string title PK';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined with a comment', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute = `string title "comment"`;
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined with a key and a comment', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute = `string title PK "comment"`;
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow an entity with multiple attributes to be defined', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'string title';
|
||||
|
Loading…
x
Reference in New Issue
Block a user