mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-16 22:09:57 +02:00
Support entity attributes
This commit is contained in:
@@ -27,13 +27,26 @@ export const parseDirective = function(statement, context, type) {
|
|||||||
|
|
||||||
const addEntity = function(name) {
|
const addEntity = function(name) {
|
||||||
if (typeof entities[name] === 'undefined') {
|
if (typeof entities[name] === 'undefined') {
|
||||||
entities[name] = name;
|
entities[name] = { attributes: [] };
|
||||||
logger.debug('Added new entity :', name);
|
logger.info('Added new entity :', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return entities[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEntities = () => entities;
|
const getEntities = () => entities;
|
||||||
|
|
||||||
|
const addAttributes = function(entityName, attribs) {
|
||||||
|
let entity = addEntity(entityName); // May do nothing (if entity has already been added)
|
||||||
|
|
||||||
|
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
|
||||||
|
let i;
|
||||||
|
for (i = attribs.length - 1; i >= 0; i--) {
|
||||||
|
entity.attributes.push(attribs[i]);
|
||||||
|
logger.debug('Added attribute ', attribs[i].attributeName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a relationship
|
* Add a relationship
|
||||||
* @param entA The first entity in the relationship
|
* @param entA The first entity in the relationship
|
||||||
@@ -76,6 +89,7 @@ export default {
|
|||||||
parseDirective,
|
parseDirective,
|
||||||
getConfig: () => configApi.getConfig().er,
|
getConfig: () => configApi.getConfig().er,
|
||||||
addEntity,
|
addEntity,
|
||||||
|
addAttributes,
|
||||||
getEntities,
|
getEntities,
|
||||||
addRelationship,
|
addRelationship,
|
||||||
getRelationships,
|
getRelationships,
|
||||||
|
@@ -22,6 +22,154 @@ export const setConf = function(cnf) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw attributes for an entity
|
||||||
|
* @param groupNode the svg group node for the entity
|
||||||
|
* @param entityTextNode the svg node for the entity label text
|
||||||
|
* @param attributes an array of attributes defined for the entity (each attribute has a type and a name)
|
||||||
|
* @return the bounding box of the entity, after attributes have been added
|
||||||
|
*/
|
||||||
|
const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||||
|
const heightPadding = conf.entityPadding / 3; // Padding internal to attribute boxes
|
||||||
|
const widthPadding = conf.entityPadding / 3; // Ditto
|
||||||
|
const attrFontSize = conf.fontSize * 0.8;
|
||||||
|
const labelBBox = entityTextNode.node().getBBox();
|
||||||
|
const attributeNodes = []; // Intermediate storage for attribute nodes created so that we can do a second pass
|
||||||
|
let maxTypeWidth = 0;
|
||||||
|
let maxNameWidth = 0;
|
||||||
|
let cumulativeHeight = labelBBox.height + heightPadding * 2;
|
||||||
|
let attrNum = 1;
|
||||||
|
|
||||||
|
attributes.forEach(item => {
|
||||||
|
const attrPrefix = `${entityTextNode.node().id}-attr-${attrNum}`;
|
||||||
|
|
||||||
|
// Add a text node for the attribute type
|
||||||
|
const typeNode = groupNode
|
||||||
|
.append('text')
|
||||||
|
.attr('class', 'er entityLabel')
|
||||||
|
.attr('id', `${attrPrefix}-type`)
|
||||||
|
.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.attributeType);
|
||||||
|
|
||||||
|
// Add a text node for the attribute name
|
||||||
|
const nameNode = 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.attributeName);
|
||||||
|
|
||||||
|
// Keep a reference to the nodes so that we can iterate through them later
|
||||||
|
attributeNodes.push({ tn: typeNode, nn: nameNode });
|
||||||
|
|
||||||
|
const typeBBox = typeNode.node().getBBox();
|
||||||
|
const nameBBox = nameNode.node().getBBox();
|
||||||
|
|
||||||
|
maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width);
|
||||||
|
maxNameWidth = Math.max(maxNameWidth, nameBBox.width);
|
||||||
|
|
||||||
|
cumulativeHeight += Math.max(typeBBox.height, nameBBox.height) + heightPadding * 2;
|
||||||
|
attrNum += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate the new bounding box of the overall entity, now that attributes have been added
|
||||||
|
const bBox = {
|
||||||
|
width: Math.max(
|
||||||
|
conf.minEntityWidth,
|
||||||
|
Math.max(labelBBox.width + widthPadding * 2, maxTypeWidth + maxNameWidth + widthPadding * 4)
|
||||||
|
),
|
||||||
|
height:
|
||||||
|
attributes.length > 0 ? cumulativeHeight : Math.max(conf.minEntityHeight, cumulativeHeight)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
if (attributes.length > 0) {
|
||||||
|
// Position the entity label near the top of the entity bounding box
|
||||||
|
entityTextNode.attr(
|
||||||
|
'transform',
|
||||||
|
'translate(' + bBox.width / 2 + ',' + (heightPadding + labelBBox.height / 2) + ')'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add rectangular boxes for the attribute types/names
|
||||||
|
let heightOffset = labelBBox.height + heightPadding * 2; // Start at the bottom of the entity label
|
||||||
|
let attribStyle = 'attributeBox1'; // We will flip the style on alternate rows to achieve a banded effect
|
||||||
|
|
||||||
|
attributeNodes.forEach(nodePair => {
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Position the type of the attribute
|
||||||
|
nodePair.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')');
|
||||||
|
|
||||||
|
// Insert a rectangle for the type
|
||||||
|
const typeRect = groupNode
|
||||||
|
.insert('rect', '#' + nodePair.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);
|
||||||
|
|
||||||
|
// Position the name of the attribute
|
||||||
|
nodePair.nn.attr(
|
||||||
|
'transform',
|
||||||
|
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert a rectangle for the name
|
||||||
|
groupNode
|
||||||
|
.insert('rect', '#' + nodePair.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);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Flip the attribute style for row banding
|
||||||
|
attribStyle = attribStyle == 'attributeBox1' ? 'attributeBox2' : 'attributeBox1';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Ensure the entity box is a decent size without any attributes
|
||||||
|
bBox.height = Math.max(conf.minEntityHeight, cumulativeHeight);
|
||||||
|
|
||||||
|
// Position the entity label in the middle of the box
|
||||||
|
entityTextNode.attr('transform', 'translate(' + bBox.width / 2 + ',' + bBox.height / 2 + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bBox;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use D3 to construct the svg elements for the entities
|
* Use D3 to construct the svg elements for the entities
|
||||||
* @param svgNode the svg node that contains the diagram
|
* @param svgNode the svg node that contains the diagram
|
||||||
@@ -56,13 +204,11 @@ const drawEntities = function(svgNode, entities, graph) {
|
|||||||
)
|
)
|
||||||
.text(id);
|
.text(id);
|
||||||
|
|
||||||
// Calculate the width and height of the entity
|
const { width: entityWidth, height: entityHeight } = drawAttributes(
|
||||||
const textBBox = textNode.node().getBBox();
|
groupNode,
|
||||||
const entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2);
|
textNode,
|
||||||
const entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2);
|
entities[id].attributes
|
||||||
|
);
|
||||||
// Make sure the text gets centred relative to the entity box
|
|
||||||
textNode.attr('transform', 'translate(' + entityWidth / 2 + ',' + entityHeight / 2 + ')');
|
|
||||||
|
|
||||||
// Draw the rectangle - insert it before the text so that the text is not obscured
|
// Draw the rectangle - insert it before the text so that the text is not obscured
|
||||||
const rectNode = groupNode
|
const rectNode = groupNode
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
%lex
|
%lex
|
||||||
|
|
||||||
%options case-insensitive
|
%options case-insensitive
|
||||||
%x open_directive type_directive arg_directive
|
%x open_directive type_directive arg_directive block
|
||||||
|
|
||||||
%%
|
%%
|
||||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
[\s]+ return 'SPACE';
|
[\s]+ return 'SPACE';
|
||||||
\"[^"]*\" return 'WORD';
|
\"[^"]*\" return 'WORD';
|
||||||
"erDiagram" return 'ER_DIAGRAM';
|
"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>[\n]+ /* nothing */
|
||||||
|
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
|
||||||
|
<block>. return yytext[0];
|
||||||
\|o return 'ZERO_OR_ONE';
|
\|o return 'ZERO_OR_ONE';
|
||||||
\}o return 'ZERO_OR_MORE';
|
\}o return 'ZERO_OR_MORE';
|
||||||
\}\| return 'ONE_OR_MORE';
|
\}\| return 'ONE_OR_MORE';
|
||||||
@@ -67,12 +73,37 @@ statement
|
|||||||
yy.addRelationship($1, $5, $3, $2);
|
yy.addRelationship($1, $5, $3, $2);
|
||||||
/*console.log($1 + $2 + $3 + ':' + $5);*/
|
/*console.log($1 + $2 + $3 + ':' + $5);*/
|
||||||
}
|
}
|
||||||
|
| entityName BLOCK_START attributes BLOCK_STOP
|
||||||
|
{
|
||||||
|
/* console.log('detected block'); */
|
||||||
|
yy.addEntity($1);
|
||||||
|
yy.addAttributes($1, $3);
|
||||||
|
/* console.log('handled block'); */
|
||||||
|
}
|
||||||
|
| entityName { yy.addEntity($1); }
|
||||||
;
|
;
|
||||||
|
|
||||||
entityName
|
entityName
|
||||||
: 'ALPHANUM' { $$ = $1; /*console.log('Entity: ' + $1);*/ }
|
: 'ALPHANUM' { $$ = $1; /*console.log('Entity: ' + $1);*/ }
|
||||||
;
|
;
|
||||||
|
|
||||||
|
attributes
|
||||||
|
: attribute { $$ = [$1]; }
|
||||||
|
| attribute attributes { $2.push($1); $$=$2; }
|
||||||
|
;
|
||||||
|
|
||||||
|
attribute
|
||||||
|
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
|
||||||
|
;
|
||||||
|
|
||||||
|
attributeType
|
||||||
|
: ATTRIBUTE_WORD { $$=$1; }
|
||||||
|
;
|
||||||
|
|
||||||
|
attributeName
|
||||||
|
: ATTRIBUTE_WORD { $$=$1; }
|
||||||
|
;
|
||||||
|
|
||||||
relSpec
|
relSpec
|
||||||
: cardinality relType cardinality
|
: cardinality relType cardinality
|
||||||
{
|
{
|
||||||
|
@@ -5,6 +5,16 @@ const getStyles = options =>
|
|||||||
stroke: ${options.nodeBorder};
|
stroke: ${options.nodeBorder};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attributeBox1 {
|
||||||
|
fill: #ffffff;
|
||||||
|
stroke: ${options.nodeBorder};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributeBox2 {
|
||||||
|
fill: #f2f2f2;
|
||||||
|
stroke: ${options.nodeBorder};
|
||||||
|
}
|
||||||
|
|
||||||
.relationshipLabelBox {
|
.relationshipLabelBox {
|
||||||
fill: ${options.tertiaryColor};
|
fill: ${options.tertiaryColor};
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
Reference in New Issue
Block a user