Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-moles-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': minor
---

feat: implement neo look styling for ER diagrams
148 changes: 148 additions & 0 deletions cypress/integration/rendering/erDiagram-neo-look.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { imgSnapshotTest } from '../../helpers/util.ts';

const looks = ['neo'] as const;
const themes = ['neo', 'neo-dark', 'redux', 'redux-dark'] as const;

// ER diagram relationship types
const relationshipTypes = [
{ cardA: '||', relType: '--', cardB: '||', name: 'one-to-one-identifying' },
{ cardA: '||', relType: '--', cardB: 'o{', name: 'one-to-many-identifying' },
{ cardA: '}o', relType: '--', cardB: 'o{', name: 'many-to-many-identifying' },
{ cardA: '||', relType: '..', cardB: 'o|', name: 'one-to-zero-or-one-non-identifying' },
{ cardA: '}|', relType: '..', cardB: 'o{', name: 'one-or-more-to-many-non-identifying' },
] as const;

looks.forEach((look) => {
themes.forEach((theme) => {
describe(`Test ER diagrams in ${look} look and ${theme} theme`, () => {
it('should render a simple ER diagram with basic relationships', () => {
const erCode = `erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER ||--|{ ADDRESS : "has"
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with all relationship types', () => {
let erCode = `erDiagram\n`;
relationshipTypes.forEach((rel, index) => {
const entityA = `ENTITY_A${index}`;
const entityB = `ENTITY_B${index}`;
erCode += ` ${entityA} ${rel.cardA}${rel.relType}${rel.cardB} ${entityB} : "${rel.name}"\n`;
});
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with entities and attributes', () => {
const erCode = `erDiagram
CUSTOMER {
string name
string custNumber
string sector
}
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with keys and comments', () => {
const erCode = `erDiagram
AUTHOR {
string name PK "Primary identifier"
string email UK "Unique email"
}
BOOK {
string isbn PK "Book identifier"
string title "Book title"
string author FK "Author reference"
float price "Book price"
}
AUTHOR ||--|{ BOOK : writes
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with entity aliases', () => {
const erCode = `erDiagram
p[Person] {
varchar(64) firstName
varchar(64) lastName
}
c["Customer Account"] {
varchar(128) email
}
o[Order] {
int orderNumber
}
p ||--o| c : has
c ||--o{ o : places
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render complex ER diagram with multiple relationships', () => {
const erCode = `erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with recursive relationships', () => {
const erCode = `erDiagram
EMPLOYEE {
int id PK
string name
int managerId FK
}
EMPLOYEE ||--o{ EMPLOYEE : manages
DEPARTMENT ||--|{ EMPLOYEE : employs
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with standalone entities', () => {
const erCode = `erDiagram
ACTIVE_ENTITY
ISOLATED_ENTITY {
string id PK
string data
}
CONNECTED_A ||--|| CONNECTED_B : relates
`;
imgSnapshotTest(erCode, { look, theme });
});

it('should render ER diagram with various attribute types', () => {
const erCode = `erDiagram
PRODUCT {
int id PK
string name
string[] tags
varchar(255) description
type~T~ genericType
float price
}
`;
imgSnapshotTest(erCode, { look, theme });
});
});
});
});
2 changes: 2 additions & 0 deletions packages/mermaid/src/diagrams/er/erDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,12 @@ export class ErDB implements DiagramDB {
const edges: Edge[] = [];
const config = getConfig();

let colorIndex = 0;
for (const entityKey of this.entities.keys()) {
const entityNode = this.entities.get(entityKey);
if (entityNode) {
entityNode.cssCompiledStyles = this.getCompiledStyles(entityNode.cssClasses!.split(' '));
entityNode.colorIndex = colorIndex++;
nodes.push(entityNode as unknown as Node);
}
}
Expand Down
13 changes: 12 additions & 1 deletion packages/mermaid/src/diagrams/er/erRenderer-unified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@ export const draw = async function (text: string, id: string, _version: string,
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
data4Layout.direction = diag.db.getDirection();
const { config } = data4Layout;
const { look } = config;

data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
if (look === 'neo') {
data4Layout.markers = [
'only_one_neo',
'zero_or_one_neo',
'one_or_more_neo',
'zero_or_more_neo',
];
} else {
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
}
data4Layout.diagramId = id;
await render(data4Layout, svg);
// Elk layout algorithm displays markers above nodes, so move edges to top so they are "painted" over by the nodes.
Expand Down
1 change: 1 addition & 0 deletions packages/mermaid/src/diagrams/er/erTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface EntityNode {
cssStyles?: string[];
cssCompiledStyles?: string[];
labelType?: string;
colorIndex?: number;
}

export interface Attribute {
Expand Down
60 changes: 45 additions & 15 deletions packages/mermaid/src/diagrams/er/styles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as khroma from 'khroma';
import type { FlowChartStyleOptions } from '../flowchart/styles.js';
import type { DiagramStylesProvider } from '../../diagram-api/types.js';

const fade = (color: string, opacity: number) => {
// @ts-ignore TODO: incorrect types from khroma
Expand All @@ -13,8 +13,34 @@ const fade = (color: string, opacity: number) => {
return khroma.rgba(r, g, b, opacity);
};

const getStyles = (options: FlowChartStyleOptions) =>
`
const genColor: DiagramStylesProvider = (options) => {
const { theme, look, bkgColorArray, borderColorArray } = options;
if (theme !== 'redux-color') {
return '';
}
let sections = '';

for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
sections += `

[data-look="${look}"][data-color-id="color-${i}"].node path {
stroke: ${borderColorArray[i]};
fill: ${bkgColorArray[i]};
}

[data-look="${look}"][data-color-id="color-${i}"].node rect {
stroke: ${borderColorArray[i]};
fill: ${bkgColorArray[i]};
}
`;
}
return sections;
};

const getStyles: DiagramStylesProvider = (options) => {
const { look, theme, erEdgeLabelBackground, strokeWidth } = options;
return `
${genColor(options)}
.entityBox {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
Expand All @@ -30,7 +56,17 @@ const getStyles = (options: FlowChartStyleOptions) =>
}

.labelBkg {
background-color: ${fade(options.tertiaryColor, 0.5)};
background-color: ${theme === 'redux-color' && erEdgeLabelBackground ? erEdgeLabelBackground : fade(options.tertiaryColor, 0.5)};
}

.edgeLabel {
background-color: ${theme === 'redux-color' && erEdgeLabelBackground ? erEdgeLabelBackground : options.edgeLabelBackground};
}
.edgeLabel .label rect {
fill: ${theme === 'redux-color' && erEdgeLabelBackground ? erEdgeLabelBackground : options.edgeLabelBackground};
}
.edgeLabel .label text {
fill: ${options.textColor};
}

.edgeLabel .label {
Expand All @@ -54,12 +90,12 @@ const getStyles = (options: FlowChartStyleOptions) =>
{
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: 1px;
stroke-width: ${look === 'neo' ? strokeWidth : '1px'};
}

.relationshipLine {
stroke: ${options.lineColor};
stroke-width: 1;
stroke-width: ${look === 'neo' ? strokeWidth : '1px'};
fill: none;
}

Expand All @@ -68,16 +104,10 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke: ${options.lineColor} !important;
stroke-width: 1;
}

.edgeLabel {
background-color: ${options.edgeLabelBackground};
}
.edgeLabel .label rect {
fill: ${options.edgeLabelBackground};
}
.edgeLabel .label text {
fill: ${options.textColor};
[data-look=neo].labelBkg {
background-color: ${fade(options.tertiaryColor, 0.5)};
}
`;
};

export default getStyles;
7 changes: 6 additions & 1 deletion packages/mermaid/src/mermaidAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ export const createUserStyles = (
svgId: string
): string => {
const userCSSstyles = createCssStyles(config, classDefs);
const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables, svgId);
const allStyles = getStyles(
graphType,
userCSSstyles,
{ ...config.themeVariables, look: config.look, theme: config.theme },
svgId
);

// Now turn all of the styles into a (compiled) string that starts with the id
// use the stylis library to compile the css, turn the results into a valid CSS string (serialize(...., stringify))
Expand Down
Loading
Loading