Compare commits

..

5 Commits

Author SHA1 Message Date
darshanr0107
1388787ddc fix: use compositional syntax for aggregation relationships
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-12-10 18:09:41 +05:30
darshanr0107
74a582b1b2 chore: update docs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-12-10 16:29:40 +05:30
darshanr0107
4171891fa4 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into aggregation-support-in-ER-diagram 2025-12-10 16:22:37 +05:30
darshanr0107
d32f6afd35 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-01 17:28:33 +05:30
darshanr0107
5a167835cc feat: provide support for aggregation in ER diagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-01 17:07:13 +05:30
15 changed files with 687 additions and 14 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Support consecutive LaTeX in node text

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add support for aggregation relationships in ER diagram

View File

@@ -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 }
);
});
});
});

View File

@@ -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' }
);
});
});

View File

@@ -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({

View File

@@ -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:

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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);
});
});
});

View File

@@ -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;

View File

@@ -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: