mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-14 08:20:41 +01:00
Compare commits
5 Commits
fix/latex-
...
aggregatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1388787ddc | ||
|
|
74a582b1b2 | ||
|
|
4171891fa4 | ||
|
|
d32f6afd35 | ||
|
|
5a167835cc |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Support consecutive LaTeX in node text
|
||||
5
.changeset/tough-clocks-flow.md
Normal file
5
.changeset/tough-clocks-flow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add support for aggregation relationships in ER diagram
|
||||
@@ -457,4 +457,117 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Aggregation Relationships', () => {
|
||||
it('should render basic aggregation relationships', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
DEPARTMENT ||<>--|| EMPLOYEE : contains
|
||||
PROJECT o{<>..o{ TASK : manages
|
||||
TEAM ||<>--|| MEMBER : consists_of
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with entity attributes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
DEPARTMENT ||<>--o{ EMPLOYEE : contains
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
string location
|
||||
}
|
||||
EMPLOYEE {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with quoted labels', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
|
||||
COLLEGE ||<>--o{ DEPARTMENT : "contains"
|
||||
DEPARTMENT ||<>--o{ FACULTY : "employs"
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render mixed relationship types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ ORDER_ITEM : contains
|
||||
PRODUCT ||<>--o{ ORDER_ITEM : "aggregated in"
|
||||
WAREHOUSE o{<>..o{ PRODUCT : "stores"
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with entity aliases', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
d[DEPARTMENT]
|
||||
e[EMPLOYEE]
|
||||
p[PROJECT]
|
||||
t[TASK]
|
||||
|
||||
d ||<>--|| e : contains
|
||||
p o{<>..o{ t : manages
|
||||
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render complex aggregation scenarios', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
COMPANY ||<>--o{ DEPARTMENT : owns
|
||||
DEPARTMENT ||<>--o{ EMPLOYEE : contains
|
||||
EMPLOYEE o{<>--o{ PROJECT : works_on
|
||||
PROJECT ||<>--o{ TASK : consists_of
|
||||
TASK ||<>--o{ SUBTASK : includes
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with different cardinalities', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
COMPANY ||<>--o{ DEPARTMENT : has
|
||||
MANAGER o|<>..o| TEAM : leads
|
||||
PRODUCT |{<>--|{ CATEGORY : belongs_to
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render aggregation with zero-or-one relationships', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
PERSON o|<>--o| PASSPORT : owns
|
||||
EMPLOYEE o|<>..o| PARKING_SPOT : assigned
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,12 +33,4 @@ describe('Katex', () => {
|
||||
// { fontFamily: 'courier' }
|
||||
// );
|
||||
// });
|
||||
it('4: should render consecutive LaTeX equations separated by text', () => {
|
||||
imgSnapshotTest(
|
||||
`graph TD
|
||||
A["From $$x(t)$$"] --> B{"$$\\tilde{x}(t)$$"};
|
||||
C["From $$x(t)$$ to $$y(t)$$"] --> D{"$$\\tilde{x}(t)$$"};`,
|
||||
{ fontFamily: 'courier' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
158
demos/er.html
158
demos/er.html
@@ -169,6 +169,164 @@
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<!-- Aggregation Examples -->
|
||||
<h2>Aggregation Examples</h2>
|
||||
|
||||
<h3>Basic Aggregation (Solid Line)</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
string location
|
||||
}
|
||||
EMPLOYEE {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Dashed Aggregation</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
PROJECT <>.. TASK : manages
|
||||
PROJECT {
|
||||
int id PK
|
||||
string name
|
||||
string description
|
||||
}
|
||||
TASK {
|
||||
int id PK
|
||||
string title
|
||||
int project_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Aggregation with Different Cardinalities</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
COMPANY <> DEPARTMENT : owns
|
||||
DEPARTMENT <> EMPLOYEE : contains
|
||||
EMPLOYEE <> PROJECT : works_on
|
||||
PROJECT <> TASK : consists_of
|
||||
|
||||
COMPANY {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
int company_id FK
|
||||
}
|
||||
EMPLOYEE {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
PROJECT {
|
||||
int id PK
|
||||
string name
|
||||
int employee_id FK
|
||||
}
|
||||
TASK {
|
||||
int id PK
|
||||
string title
|
||||
int project_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Two-way Aggregation</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
TEAM <> MEMBER : consists_of
|
||||
TEAM {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
MEMBER {
|
||||
int id PK
|
||||
string name
|
||||
int team_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Complex Aggregation with Labels</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
UNIVERSITY <> COLLEGE : "has multiple"
|
||||
COLLEGE <> DEPARTMENT : "contains"
|
||||
DEPARTMENT <> FACULTY : "employs"
|
||||
FACULTY <> STUDENT : "teaches"
|
||||
|
||||
UNIVERSITY {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
COLLEGE {
|
||||
int id PK
|
||||
string name
|
||||
int university_id FK
|
||||
}
|
||||
DEPARTMENT {
|
||||
int id PK
|
||||
string name
|
||||
int college_id FK
|
||||
}
|
||||
FACULTY {
|
||||
int id PK
|
||||
string name
|
||||
int department_id FK
|
||||
}
|
||||
STUDENT {
|
||||
int id PK
|
||||
string name
|
||||
int faculty_id FK
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h3>Mixed Relationship Types</h3>
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ ORDER_ITEM : contains
|
||||
PRODUCT <> ORDER_ITEM : "aggregated in"
|
||||
WAREHOUSE <>.. PRODUCT : "stores"
|
||||
|
||||
CUSTOMER {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
ORDER {
|
||||
int id PK
|
||||
int customer_id FK
|
||||
date order_date
|
||||
}
|
||||
ORDER_ITEM {
|
||||
int id PK
|
||||
int order_id FK
|
||||
int product_id FK
|
||||
int quantity
|
||||
}
|
||||
PRODUCT {
|
||||
int id PK
|
||||
string name
|
||||
int warehouse_id FK
|
||||
}
|
||||
WAREHOUSE {
|
||||
int id PK
|
||||
string name
|
||||
}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
mermaid.initialize({
|
||||
|
||||
@@ -209,6 +209,72 @@ erDiagram
|
||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
### Aggregation (v\<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
|
||||
|
||||
Aggregation syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type:
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```
|
||||
<first-entity> <cardinalityA><>--<cardinalityB> <second-entity> : <relationship-label>
|
||||
<first-entity> <cardinalityA><>..<cardinalityB> <second-entity> : <relationship-label>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<>` is the aggregation marker
|
||||
- `--` represents a solid line (identifying relationship)
|
||||
- `..` represents a dashed line (non-identifying relationship)
|
||||
- Cardinality markers can be: `||` (only one), `o|` (zero or one), `o{` (zero or more), `|{` (one or more)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
DEPARTMENT ||<>--o{ EMPLOYEE : contains
|
||||
PROJECT o{<>..o{ TASK : manages
|
||||
TEAM ||<>--|| MEMBER : consists_of
|
||||
COMPANY ||<>--o{ DEPARTMENT : owns
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
DEPARTMENT ||<>--o{ EMPLOYEE : contains
|
||||
PROJECT o{<>..o{ TASK : manages
|
||||
TEAM ||<>--|| MEMBER : consists_of
|
||||
COMPANY ||<>--o{ DEPARTMENT : owns
|
||||
```
|
||||
|
||||
In these examples:
|
||||
|
||||
- `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation)
|
||||
- `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation)
|
||||
- `TEAM ||<>--|| MEMBER` shows that one team consists of one member (solid aggregation)
|
||||
- `COMPANY ||<>--o{ DEPARTMENT` shows that one company owns zero or more departments (solid aggregation)
|
||||
|
||||
**Aggregation vs Association**
|
||||
|
||||
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently. The aggregation marker must be combined with cardinalities and line type (e.g., `||<>--o{`)
|
||||
- **Association** (`||--`, `}o--`): General relationship between entities with cardinality markers directly connected to line type
|
||||
|
||||
**Additional Examples:**
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
|
||||
MANAGER o|<>..o| TEAM : leads
|
||||
PERSON o|<>--o| PASSPORT : owns
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
|
||||
MANAGER o|<>..o| TEAM : leads
|
||||
PERSON o|<>--o| PASSPORT : owns
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
||||
|
||||
@@ -293,7 +293,7 @@ const processSet = (input: string): string => {
|
||||
// Firefox versions between [4,71] (0.47%) and Safari versions between [5,13.4] (0.17%) don't have this interface implemented but MathML is supported
|
||||
export const isMathMLSupported = () => window.MathMLElement !== undefined;
|
||||
|
||||
export const katexRegex = /\$\$(.*?)\$\$/g;
|
||||
export const katexRegex = /\$\$(.*)\$\$/g;
|
||||
|
||||
/**
|
||||
* Whether or not a text has KaTeX delimiters
|
||||
|
||||
@@ -2,6 +2,7 @@ import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { Edge, Node } from '../../rendering-util/types.js';
|
||||
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
|
||||
import { AggregationType } from './erTypes.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@@ -33,6 +34,11 @@ export class ErDB implements DiagramDB {
|
||||
IDENTIFYING: 'IDENTIFYING',
|
||||
};
|
||||
|
||||
private Aggregation = {
|
||||
AGGREGATION: AggregationType.AGGREGATION,
|
||||
AGGREGATION_DASHED: AggregationType.AGGREGATION_DASHED,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
this.addEntity = this.addEntity.bind(this);
|
||||
@@ -131,6 +137,31 @@ export class ErDB implements DiagramDB {
|
||||
return this.relationships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate aggregation relationship
|
||||
* @param rSpec - The relationship specification to validate
|
||||
* @returns boolean indicating if the aggregation relationship is valid
|
||||
*/
|
||||
public validateAggregationRelationship(rSpec: RelSpec): boolean {
|
||||
const isAggregation =
|
||||
rSpec.relType === this.Aggregation.AGGREGATION ||
|
||||
rSpec.relType === this.Aggregation.AGGREGATION_DASHED;
|
||||
|
||||
if (!isAggregation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const validCardinalities = [
|
||||
this.Cardinality.ZERO_OR_ONE,
|
||||
this.Cardinality.ZERO_OR_MORE,
|
||||
this.Cardinality.ONE_OR_MORE,
|
||||
this.Cardinality.ONLY_ONE,
|
||||
this.Cardinality.MD_PARENT,
|
||||
];
|
||||
|
||||
return validCardinalities.includes(rSpec.cardA) && validCardinalities.includes(rSpec.cardB);
|
||||
}
|
||||
|
||||
public getDirection() {
|
||||
return this.direction;
|
||||
}
|
||||
@@ -248,4 +279,17 @@ export class ErDB implements DiagramDB {
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getConfig = () => getConfig().er;
|
||||
|
||||
// Getter methods for aggregation constants
|
||||
public get AggregationConstants() {
|
||||
return this.Aggregation;
|
||||
}
|
||||
|
||||
public get CardinalityConstants() {
|
||||
return this.Cardinality;
|
||||
}
|
||||
|
||||
public get IdentificationConstants() {
|
||||
return this.Identification;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ const ERMarkers = {
|
||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
|
||||
MD_PARENT_END: 'MD_PARENT_END',
|
||||
MD_PARENT_START: 'MD_PARENT_START',
|
||||
AGGREGATION_START: 'AGGREGATION_START',
|
||||
AGGREGATION_END: 'AGGREGATION_END',
|
||||
AGGREGATION_DASHED_START: 'AGGREGATION_DASHED_START',
|
||||
AGGREGATION_DASHED_END: 'AGGREGATION_DASHED_END',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,6 +184,66 @@ const insertMarkers = function (elem, conf) {
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||
|
||||
// Aggregation markers (hollow diamond)
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_END)
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
|
||||
|
||||
// Dashed aggregation markers
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_DASHED_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke-dasharray', '3,3')
|
||||
.attr('d', 'M18,9 L9,0 L0,9 L9,18 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.AGGREGATION_DASHED_END)
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke-dasharray', '3,3')
|
||||
.attr('d', 'M2,9 L11,0 L20,9 L11,18 Z');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -448,6 +448,11 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
svgPath.attr('stroke-dasharray', '8,8');
|
||||
}
|
||||
|
||||
// Handle aggregation relationship styling
|
||||
if (rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED) {
|
||||
svgPath.attr('stroke-dasharray', '8,8');
|
||||
}
|
||||
|
||||
// TODO: Understand this better
|
||||
let url = '';
|
||||
if (conf.arrowMarkerAbsolute) {
|
||||
@@ -503,6 +508,15 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle aggregation markers
|
||||
if (
|
||||
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION ||
|
||||
rel.relSpec.relType === diagObj.db.Aggregation.AGGREGATION_DASHED
|
||||
) {
|
||||
// Add aggregation marker at the start (entity B side)
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.AGGREGATION_START + ')');
|
||||
}
|
||||
|
||||
// Now label the relationship
|
||||
|
||||
// Find the half-way point
|
||||
|
||||
@@ -35,3 +35,15 @@ export interface EntityClass {
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
|
||||
// Aggregation relationship types
|
||||
export const AggregationType = {
|
||||
AGGREGATION: 'AGGREGATION',
|
||||
AGGREGATION_DASHED: 'AGGREGATION_DASHED',
|
||||
} as const;
|
||||
|
||||
// Line types for aggregation
|
||||
export const AggregationLineType = {
|
||||
SOLID: 'SOLID',
|
||||
DASHED: 'DASHED',
|
||||
} as const;
|
||||
|
||||
@@ -75,6 +75,7 @@ o\| return 'ZERO_OR_ONE';
|
||||
o\{ return 'ZERO_OR_MORE';
|
||||
\|\{ return 'ONE_OR_MORE';
|
||||
u(?=[\.\-\|]) return 'MD_PARENT';
|
||||
"<>" return 'AGGREGATION';
|
||||
\.\. return 'NON_IDENTIFYING';
|
||||
\-\- return 'IDENTIFYING';
|
||||
"to" return 'IDENTIFYING';
|
||||
@@ -172,6 +173,36 @@ statement
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||
| entityName aggregationRelSpec entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $9, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
yy.setClass([$5], $7);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList aggregationRelSpec entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $7, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
}
|
||||
| entityName aggregationRelSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $7, $3, $2);
|
||||
yy.setClass([$3], $5);
|
||||
}
|
||||
|
||||
|
||||
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
@@ -280,6 +311,17 @@ relSpec
|
||||
}
|
||||
;
|
||||
|
||||
aggregationRelSpec
|
||||
: cardinality 'AGGREGATION' 'IDENTIFYING' cardinality
|
||||
{
|
||||
$$ = { cardA: $1, relType: yy.Aggregation.AGGREGATION, cardB: $4 };
|
||||
}
|
||||
| cardinality 'AGGREGATION' 'NON_IDENTIFYING' cardinality
|
||||
{
|
||||
$$ = { cardA: $1, relType: yy.Aggregation.AGGREGATION_DASHED, cardB: $4 };
|
||||
}
|
||||
;
|
||||
|
||||
cardinality
|
||||
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
||||
|
||||
@@ -1087,4 +1087,95 @@ describe('when parsing ER diagram it...', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation relationships', function () {
|
||||
it('should parse basic aggregation syntax with solid line', function () {
|
||||
erDiagram.parser.parse('erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].roleA).toBe('contains');
|
||||
});
|
||||
|
||||
it('should parse dashed aggregation syntax', function () {
|
||||
erDiagram.parser.parse('erDiagram\nPROJECT o{<>..o{ TASK : manages');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].roleA).toBe('manages');
|
||||
});
|
||||
|
||||
it('should parse aggregation with quoted labels', function () {
|
||||
erDiagram.parser.parse('erDiagram\nUNIVERSITY ||<>--|| COLLEGE : "has multiple"');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[0].roleA).toBe('has multiple');
|
||||
});
|
||||
|
||||
it('should parse multiple aggregation relationships', function () {
|
||||
erDiagram.parser.parse(
|
||||
'erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains\nPROJECT o{<>..o{ TASK : manages'
|
||||
);
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(4);
|
||||
expect(rels.length).toBe(2);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||
});
|
||||
|
||||
it('should parse aggregation with entity aliases', function () {
|
||||
erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd ||<>--|| e : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(erDb.getEntity('d').alias).toBe('DEPARTMENT');
|
||||
expect(erDb.getEntity('e').alias).toBe('EMPLOYEE');
|
||||
});
|
||||
|
||||
it('should validate aggregation relationships', function () {
|
||||
erDiagram.parser.parse('erDiagram\nDEPARTMENT ||<>--|| EMPLOYEE : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed relationship types', function () {
|
||||
erDiagram.parser.parse(
|
||||
'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT ||<>--|| ORDER_ITEM : "aggregated in"'
|
||||
);
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(4);
|
||||
expect(rels.length).toBe(2);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
});
|
||||
|
||||
it('should parse aggregation with different cardinalities', function () {
|
||||
erDiagram.parser.parse('erDiagram\nCOMPANY ||<>--o{ DEPARTMENT : has');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should parse aggregation with zero-or-one cardinality', function () {
|
||||
erDiagram.parser.parse('erDiagram\nMANAGER o|<>..o| TEAM : leads');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(erDb.getEntities().size).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,6 +68,32 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.aggregation {
|
||||
stroke: ${options.lineColor};
|
||||
stroke-width: 1;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.aggregation-dashed {
|
||||
stroke: ${options.lineColor};
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 8,8;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.aggregation-marker {
|
||||
fill: white !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.aggregation-marker-dashed {
|
||||
fill: white !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 3,3;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
||||
@@ -151,6 +151,57 @@ erDiagram
|
||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
### Aggregation (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints.
|
||||
|
||||
Aggregation syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type:
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```
|
||||
<first-entity> <cardinalityA><>--<cardinalityB> <second-entity> : <relationship-label>
|
||||
<first-entity> <cardinalityA><>..<cardinalityB> <second-entity> : <relationship-label>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<>` is the aggregation marker
|
||||
- `--` represents a solid line (identifying relationship)
|
||||
- `..` represents a dashed line (non-identifying relationship)
|
||||
- Cardinality markers can be: `||` (only one), `o|` (zero or one), `o{` (zero or more), `|{` (one or more)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
DEPARTMENT ||<>--o{ EMPLOYEE : contains
|
||||
PROJECT o{<>..o{ TASK : manages
|
||||
TEAM ||<>--|| MEMBER : consists_of
|
||||
COMPANY ||<>--o{ DEPARTMENT : owns
|
||||
```
|
||||
|
||||
In these examples:
|
||||
|
||||
- `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation)
|
||||
- `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation)
|
||||
- `TEAM ||<>--|| MEMBER` shows that one team consists of one member (solid aggregation)
|
||||
- `COMPANY ||<>--o{ DEPARTMENT` shows that one company owns zero or more departments (solid aggregation)
|
||||
|
||||
**Aggregation vs Association**
|
||||
|
||||
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently. The aggregation marker must be combined with cardinalities and line type (e.g., `||<>--o{`)
|
||||
- **Association** (`||--`, `}o--`): General relationship between entities with cardinality markers directly connected to line type
|
||||
|
||||
**Additional Examples:**
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
|
||||
MANAGER o|<>..o| TEAM : leads
|
||||
PERSON o|<>--o| PASSPORT : owns
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. The attributes are rendered inside the entity boxes. For example:
|
||||
|
||||
Reference in New Issue
Block a user