Merge branch 'develop' into fix/timeline-event-colons

This commit is contained in:
Jesse Wright
2025-04-15 12:03:58 +01:00
committed by GitHub
135 changed files with 8781 additions and 2196 deletions

View File

@@ -27,6 +27,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'block',
'packet',
'architecture',
'radar',
] as const;
/**

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Fixes for consistent edge id creation & handling edge cases for animate edge feature

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Fix for issue #6195 - allowing @ signs inside node labels

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: revert state db to resolve getData returning empty nodes and edges

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Convert StateDB into TypeScript

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Remove incorrect `style="undefined;"` attributes in some Mermaid diagrams

View File

@@ -1,8 +0,0 @@
---
'mermaid': minor
---
Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
`mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Added versioning to StateDB and updated tests and diagrams to use it.

View File

@@ -0,0 +1,7 @@
---
'@mermaid-js/mermaid-zenuml': patch
---
chore: bump minimum ZenUML version to 3.23.28
commit: 9d06d8f31e7f12af9e9e092214f907f2dc93ad75

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
Adding support for animation of flowchart edges

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add support for styling Journey Diagram title (color, font-family, and font-size)

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart

View File

@@ -0,0 +1,6 @@
---
'mermaid': patch
'@mermaid-js/parser': patch
---
Refactor grammar so that title don't break Architecture Diagrams

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages.

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Dynamically Render Data Labels Within Bar Charts

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config.

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram

View File

@@ -0,0 +1,7 @@
---
'@mermaid-js/mermaid-zenuml': patch
---
fix(zenuml): limit `peerDependencies` to Mermaid v10 and v11
commit: 0ad44c12feead9d20c6a870a49327ada58d6e657

View File

@@ -34,6 +34,19 @@ const buildPackage = async (entryName: keyof typeof packageOptions) => {
{ ...iifeOptions, minify: true, metafile: shouldVisualize }
);
}
if (entryName === 'mermaid-zenuml') {
const iifeOptions: MermaidBuildOptions = {
...commonOptions,
format: 'iife',
globalName: 'mermaid-zenuml',
};
buildConfigs.push(
// mermaid-zenuml.js
{ ...iifeOptions },
// mermaid-zenuml.min.js
{ ...iifeOptions, minify: true, metafile: shouldVisualize }
);
}
const results = await Promise.all(buildConfigs.map((option) => build(getBuildConfig(option))));

View File

@@ -58,6 +58,7 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
format,
minify,
options: { name, file, packageName },
globalName = 'mermaid',
} = options;
const external: string[] = ['require', 'fs', 'path'];
const outFileName = getFileName(name, options);
@@ -68,6 +69,7 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
},
metafile,
minify,
globalName,
logLevel: 'info',
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
define: {
@@ -89,11 +91,12 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
if (format === 'iife') {
output.format = 'iife';
output.splitting = false;
output.globalName = '__esbuild_esm_mermaid';
const originalGlobalName = output.globalName ?? 'mermaid';
output.globalName = `__esbuild_esm_mermaid_nm[${JSON.stringify(originalGlobalName)}]`;
// Workaround for removing the .default access in esbuild IIFE.
// https://github.com/mermaid-js/mermaid/pull/4109#discussion_r1292317396
output.footer = {
js: 'globalThis.mermaid = globalThis.__esbuild_esm_mermaid.default;',
js: `globalThis[${JSON.stringify(originalGlobalName)}] = globalThis.${output.globalName}.default;`,
};
output.outExtension = { '.js': '.js' };
} else {

4
.github/lychee.toml vendored
View File

@@ -50,7 +50,9 @@ exclude = [
"https://docs.swimm.io",
# Timeout
"https://huehive.co"
"https://huehive.co",
"https://foswiki.org",
"https://www.gnu.org",
]
# Exclude all private IPs from checking.

View File

@@ -11,6 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
jobs:
timings:
@@ -29,6 +30,7 @@ jobs:
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
with:
runTests: false
- name: Cypress run
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
id: cypress
@@ -44,15 +46,25 @@ jobs:
SPLIT: 1
SPLIT_INDEX: 0
SPLIT_FILE: 'cypress/timings.json'
- name: Commit changes
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
- name: Compare timings
id: compare
run: |
OUTPUT=$(pnpm tsx scripts/compare-timings.ts)
echo "$OUTPUT" >> $GITHUB_STEP_SUMMARY
echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
uses: peter-evans/create-pull-request@a7b20e1da215b3ef3ccddb48ff65120256ed6226
with:
add: 'cypress/timings.json'
author_name: 'github-actions[bot]'
author_email: '41898282+github-actions[bot]@users.noreply.github.com'
message: 'chore: update E2E timings'
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
branch: release-promotion
add-paths: |
cypress/timings.json
commit-message: 'chore: update E2E timings'
branch: update-timings
title: Update E2E Timings
body: ${{ steps.compare.outputs.output }}
delete-branch: true
sign-commits: true

View File

@@ -1,8 +1,8 @@
import eyesPlugin from '@applitools/eyes-cypress';
import { registerArgosTask } from '@argos-ci/cypress/task';
import coverage from '@cypress/code-coverage/task';
import coverage from '@cypress/code-coverage/task.js';
import { defineConfig } from 'cypress';
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin';
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin.js';
import cypressSplit from 'cypress-split';
export default eyesPlugin(

View File

@@ -19,6 +19,25 @@ describe.skip('architecture diagram', () => {
`
);
});
it('should render a simple architecture diagram with titleAndAccessabilities', () => {
imgSnapshotTest(
`architecture-beta
title Simple Architecture Diagram
accTitle: Accessibility Title
accDescr: Accessibility Description
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
`
);
});
it('should render an architecture diagram with groups within groups', () => {
imgSnapshotTest(
`architecture-beta
@@ -172,7 +191,7 @@ describe.skip('architecture diagram', () => {
);
});
it('should render an architecture diagram with a resonable height', () => {
it('should render an architecture diagram with a reasonable height', () => {
imgSnapshotTest(
`architecture-beta
group federated(cloud)[Federated Environment]

View File

@@ -0,0 +1,652 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
const testOptions = [
{ description: '', options: { logLevel: 1 } },
{ description: 'ELK: ', options: { logLevel: 1, layout: 'elk' } },
{ description: 'HD: ', options: { logLevel: 1, look: 'handDrawn' } },
];
describe('Entity Relationship Diagram Unified', () => {
testOptions.forEach(({ description, options }) => {
it(`${description}should render a simple ER diagram`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
options
);
});
it(`${description}should render a simple ER diagram without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render an ER diagram with a recursive relationship`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||..o{ CUSTOMER : refers
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
options
);
});
it(`${description}should render an ER diagram with multiple relationships between the same two entities`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--|{ ADDRESS : "invoiced at"
CUSTOMER ||--|{ ADDRESS : "receives goods at"
`,
options
);
});
it(`${description}should render a cyclical ER diagram`, () => {
imgSnapshotTest(
`
erDiagram
A ||--|{ B : likes
B ||--|{ C : likes
C ||--|{ A : likes
`,
options
);
});
it(`${description}should render a not-so-simple ER diagram`, () => {
imgSnapshotTest(
`
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"
`,
options
);
});
it(`${description}should render a not-so-simple ER diagram without htmlLabels`, () => {
imgSnapshotTest(
`
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"
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render multiple ER diagrams`, () => {
imgSnapshotTest(
[
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
],
options
);
});
it(`${description}should render an ER diagram with blank or empty labels`, () => {
imgSnapshotTest(
`
erDiagram
BOOK }|..|{ AUTHOR : ""
BOOK }|..|{ GENRE : " "
AUTHOR }|..|{ GENRE : " "
`,
options
);
});
it(`${description}should render entities that have no relationships`, () => {
renderGraph(
`
erDiagram
DEAD_PARROT
HERMIT
RECLUSE
SOCIALITE }o--o{ SOCIALITE : "interacts with"
RECLUSE }o--o{ SOCIALITE : avoids
`,
options
);
});
it(`${description}should render entities with and without attributes`, () => {
renderGraph(
`
erDiagram
BOOK { string title }
AUTHOR }|..|{ BOOK : writes
BOOK { float price }
`,
options
);
});
it(`${description}should render entities with generic and array attributes`, () => {
renderGraph(
`
erDiagram
BOOK {
string title
string[] authors
type~T~ type
}
`,
options
);
});
it(`${description}should render entities with generic and array attributes without htmlLabels`, () => {
renderGraph(
`
erDiagram
BOOK {
string title
string[] authors
type~T~ type
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with length in attributes type`, () => {
renderGraph(
`
erDiagram
CLUSTER {
varchar(99) name
string(255) description
}
`,
options
);
});
it(`${description}should render entities with length in attributes type without htmlLabels`, () => {
renderGraph(
`
erDiagram
CLUSTER {
varchar(99) name
string(255) description
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities and attributes with big and small entity names`, () => {
renderGraph(
`
erDiagram
PRIVATE_FINANCIAL_INSTITUTION {
string name
int turnover
}
PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
EMPLOYEE { bool officer_of_firm }
`,
options
);
});
it(`${description}should render entities and attributes with big and small entity names without htmlLabels`, () => {
renderGraph(
`
erDiagram
PRIVATE_FINANCIAL_INSTITUTION {
string name
int turnover
}
PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
EMPLOYEE { bool officer_of_firm }
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with attributes that begin with asterisk`, () => {
imgSnapshotTest(
`
erDiagram
BOOK {
int *id
string name
varchar(99) summary
}
BOOK }o..o{ STORE : soldBy
STORE {
int *id
string name
varchar(50) address
}
`,
options
);
});
it(`${description}should render entities with attributes that begin with asterisk without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
BOOK {
int *id
string name
varchar(99) summary
}
BOOK }o..o{ STORE : soldBy
STORE {
int *id
string name
varchar(50) address
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with keys`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
float price
string author FK
string title PK
}
`,
options
);
});
it(`${description}should render entities with keys without htmlLabels`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
float price
string author FK
string title PK
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with comments`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string author
string title "author comment"
float price "price comment"
}
`,
options
);
});
it(`${description}should render entities with comments without htmlLabels`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string author
string title "author comment"
float price "price comment"
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with keys and comments`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string description
float price "price comment"
string title PK "title comment"
string author FK
}
`,
options
);
});
it(`${description}should render entities with keys and comments without htmlLabels`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string description
float price "price comment"
string title PK "title comment"
string author FK
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with aliases`, () => {
renderGraph(
`
erDiagram
T1 one or zero to one or more T2 : test
T2 one or many optionally to zero or one T3 : test
T3 zero or more to zero or many T4 : test
T4 many(0) to many(1) T5 : test
T5 many optionally to one T6 : test
T6 only one optionally to only one T1 : test
T4 0+ to 1+ T6 : test
T1 1 to 1 T3 : test
`,
options
);
});
it(`${description}should render a simple ER diagram with a title`, () => {
imgSnapshotTest(
`---
title: simple ER diagram
---
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
options
);
});
it(`${description}should render entities with entity name aliases`, () => {
imgSnapshotTest(
`
erDiagram
p[Person] {
varchar(64) firstName
varchar(64) lastName
}
c["Customer Account"] {
varchar(128) email
}
p ||--o| c : has
`,
options
);
});
it(`${description}should render relationship labels with line breaks`, () => {
imgSnapshotTest(
`
erDiagram
p[Person] {
string firstName
string lastName
}
a["Customer Account"] {
string email
}
b["Customer Account Secondary"] {
string email
}
c["Customer Account Tertiary"] {
string email
}
d["Customer Account Nth"] {
string email
}
p ||--o| a : "has<br />one"
p ||--o| b : "has<br />one<br />two"
p ||--o| c : "has<br />one<br/>two<br />three"
p ||--o| d : "has<br />one<br />two<br/>three<br />...<br/>Nth"
`,
options
);
});
it(`${description}should render an ER diagram with unicode text`, () => {
imgSnapshotTest(
`
erDiagram
_**testẽζØ😀㌕ぼ**_ {
*__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
*string(99)~T~~~~~~* firstName "Only __99__ <br>characters are a<br>llowed dsfsdfsdfsdfs"
string last*Name*
string __phone__ UK
int _age_
}
`,
options
);
});
it(`${description}should render an ER diagram with unicode text without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
_**testẽζØ😀㌕ぼ**_ {
*__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
*string(99)~T~~~~~~* firstName "Only __99__ <br>characters are a<br>llowed dsfsdfsdfsdfs"
string last*Name*
string __phone__ UK
int _age_
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render an ER diagram with relationships with unicode text`, () => {
imgSnapshotTest(
`
erDiagram
person[😀] {
string *first*Name
string _**last**Name_
}
a["*Customer Account*"] {
**string** ema*i*l
}
person ||--o| a : __hẽ😀__
`,
options
);
});
it(`${description}should render an ER diagram with relationships with unicode text without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
person[😀] {
string *first*Name
string _**last**Name_
}
a["*Customer Account*"] {
**string** ema*i*l
}
person ||--o| a : __hẽ😀__
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render an ER diagram with TB direction`, () => {
imgSnapshotTest(
`
erDiagram
direction TB
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render an ER diagram with BT direction`, () => {
imgSnapshotTest(
`
erDiagram
direction BT
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render an ER diagram with LR direction`, () => {
imgSnapshotTest(
`
erDiagram
direction LR
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render an ER diagram with RL direction`, () => {
imgSnapshotTest(
`
erDiagram
direction RL
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render entities with styles applied from style statement`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]
style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
`,
options
);
});
it(`${description}should render entities with styles applied from style statement without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]
style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with styles applied from class statement`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]:::blue
classDef bold font-size:24px, font-weight: bold
classDef blue stroke:lightblue, color: #0000FF
class c,p bold
`,
options
);
});
it(`${description}should render entities with styles applied from class statement without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]:::blue
classDef bold font-size:24px, font-weight: bold
classDef blue stroke:lightblue, color: #0000FF
class c,p bold
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with styles applied from the default class and other styles`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]:::blue
classDef blue stroke:lightblue, color: #0000FF
classDef default fill:pink
style c color:green
`,
{ ...options }
);
});
});
});

View File

@@ -109,8 +109,8 @@ describe('Entity Relationship Diagram', () => {
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
// use within because the absolute value can be slightly different depending on the environment ±5%
expect(maxWidthValue).to.be.within(140 * 0.95, 140 * 1.05);
// use within because the absolute value can be slightly different depending on the environment ±6%
expect(maxWidthValue).to.be.within(140 * 0.96, 140 * 1.06);
});
});
@@ -125,8 +125,8 @@ describe('Entity Relationship Diagram', () => {
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
// use within because the absolute value can be slightly different depending on the environment ±5%
expect(width).to.be.within(140 * 0.95, 140 * 1.05);
// use within because the absolute value can be slightly different depending on the environment ±6%
expect(width).to.be.within(140 * 0.96, 140 * 1.06);
// expect(svg).to.have.attr('height', '465');
expect(svg).to.not.have.attr('style');
});

View File

@@ -917,4 +917,21 @@ graph TD
}
);
});
it('#6369: edge color should affect arrow head', () => {
imgSnapshotTest(
`
flowchart LR
A --> B
A --> C
C --> D
linkStyle 0 stroke:#D50000
linkStyle 2 stroke:#D50000
`,
{
flowchart: { htmlLabels: true },
securityLevel: 'loose',
}
);
});
});

View File

@@ -63,4 +63,199 @@ section Checkout from website
{ journey: { useMaxWidth: false } }
);
});
it('should initialize with a left margin of 150px for user journeys', () => {
renderGraph(
`
---
config:
journey:
maxLabelWidth: 320
---
journey
title User Journey Example
section Onboarding
Sign Up: 5:
Browse Features: 3:
Use Core Functionality: 4:
section Engagement
Browse Features: 3
Use Core Functionality: 4
`,
{ journey: { useMaxWidth: true } }
);
let diagramStartX;
cy.contains('foreignobject', 'Sign Up').then(($diagram) => {
diagramStartX = parseFloat($diagram.attr('x'));
expect(diagramStartX).to.be.closeTo(150, 2);
});
});
it('should maintain sufficient space between legend and diagram when legend labels are longer', () => {
renderGraph(
`journey
title Web hook life cycle
section Darkoob
Make preBuilt:5: Darkoob user
register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained
Map slug to a Prebuilt Job:5: Darkoob user
section External Service
set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty
listen to the events : 5 : External Service
call darkoob endpoint : 5 : External Service
section Darkoob
check for inputs : 5 : DarkoobAPI
run the prebuilt job : 5 : DarkoobAPI
`,
{ journey: { useMaxWidth: true } }
);
let LabelEndX, diagramStartX;
// Get right edge of the legend
cy.contains('tspan', 'Darkoob userf').then((textBox) => {
const bbox = textBox[0].getBBox();
LabelEndX = bbox.x + bbox.width;
});
// Get left edge of the diagram
cy.contains('foreignobject', 'Make preBuilt').then((rect) => {
diagramStartX = parseFloat(rect.attr('x'));
});
// Assert right edge of the diagram is greater than or equal to the right edge of the label
cy.then(() => {
expect(diagramStartX).to.be.gte(LabelEndX);
});
});
it('should wrap a single long word with hyphenation', () => {
renderGraph(
`
---
config:
journey:
maxLabelWidth: 100
---
journey
title Long Word Test
section Test
VeryLongWord: 5: Supercalifragilisticexpialidocious
`,
{ journey: { useMaxWidth: true } }
);
// Verify that the line ends with a hyphen, indicating proper hyphenation for words exceeding maxLabelWidth.
cy.get('tspan').then((tspans) => {
const hasHyphen = [...tspans].some((t) => t.textContent.trim().endsWith('-'));
return expect(hasHyphen).to.be.true;
});
});
it('should wrap text on whitespace without adding hyphens', () => {
renderGraph(
`
---
config:
journey:
maxLabelWidth: 200
---
journey
title Whitespace Test
section Test
TextWithSpaces: 5: Gustavo Fring is played by Giancarlo Esposito and is a character in Breaking Bad.
`,
{ journey: { useMaxWidth: true } }
);
// Verify that none of the text spans end with a hyphen.
cy.get('tspan').each(($el) => {
const text = $el.text();
expect(text.trim()).not.to.match(/-$/);
});
});
it('should wrap long labels into multiple lines, keep them under max width, and maintain margins', () => {
renderGraph(
`
---
config:
journey:
maxLabelWidth: 320
---
journey
title User Journey Example
section Onboarding
Sign Up: 5: This is a long label that will be split into multiple lines to test the wrapping functionality
Browse Features: 3: This is another long label that will be split into multiple lines to test the wrapping functionality
Use Core Functionality: 4: This is yet another long label that will be split into multiple lines to test the wrapping functionality
section Engagement
Browse Features: 3
Use Core Functionality: 4
`,
{ journey: { useMaxWidth: true } }
);
let diagramStartX, maxLineWidth;
// Get the diagram's left edge x-coordinate
cy.contains('foreignobject', 'Sign Up')
.then(($diagram) => {
diagramStartX = parseFloat($diagram.attr('x'));
})
.then(() => {
cy.get('text.legend').then(($lines) => {
// Check that there are multiple lines
expect($lines.length).to.be.equal(9);
// Check that all lines are under the maxLabelWidth
$lines.each((index, el) => {
const bbox = el.getBBox();
expect(bbox.width).to.be.lte(320);
maxLineWidth = Math.max(maxLineWidth || 0, bbox.width);
});
/** The expected margin between the diagram and the legend is 150px, as defined by
* conf.leftMargin in user-journey-config.js
*/
expect(diagramStartX - maxLineWidth).to.be.closeTo(150, 2);
});
});
});
it('should correctly render the user journey diagram title with the specified styling', () => {
renderGraph(
`---
config:
journey:
titleColor: "#2900A5"
titleFontFamily: "Times New Roman"
titleFontSize: "5rem"
---
journey
title User Journey Example
section Onboarding
Sign Up: 5: John, Shahir
Complete Profile: 4: John
section Engagement
Browse Features: 3: John
Use Core Functionality: 4: John
section Retention
Revisit Application: 5: John
Invite Friends: 3: John
size: 2rem
`
);
cy.get('text').contains('User Journey Example').as('title');
cy.get('@title').then(($title) => {
expect($title).to.have.attr('fill', '#2900A5');
expect($title).to.have.attr('font-family', 'Times New Roman');
expect($title).to.have.attr('font-size', '5rem');
});
});
});

View File

@@ -0,0 +1,79 @@
import { imgSnapshotTest } from '../../helpers/util';
describe('radar structure', () => {
it('should render a simple radar diagram', () => {
imgSnapshotTest(
`radar-beta
title Best Radar Ever
axis A, B, C
curve c1{1, 2, 3}
`
);
});
it('should render a radar diagram with multiple curves', () => {
imgSnapshotTest(
`radar-beta
title Best Radar Ever
axis A, B, C
curve c1{1, 2, 3}
curve c2{2, 3, 1}
`
);
});
it('should render a complex radar diagram', () => {
imgSnapshotTest(
`radar-beta
title My favorite ninjas
axis Agility, Speed, Strength
axis Stam["Stamina"] , Intel["Intelligence"]
curve Ninja1["Naruto Uzumaki"]{
Agility 2, Speed 2,
Strength 3, Stam 5,
Intel 0
}
curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
curve Ninja3 {3, 2, 1, 5, 4}
showLegend true
ticks 3
max 8
min 0
graticule polygon
`
);
cy.get('svg').should((svg) => {
expect(svg).to.have.length(1);
});
});
it('should render radar diagram with config override', () => {
imgSnapshotTest(
`radar-beta
title Best Radar Ever
axis A,B,C
curve mycurve{1,2,3}`,
{ radar: { marginTop: 100, axisScaleFactor: 0.5 } }
);
});
it('should parse radar diagram with theme override', () => {
imgSnapshotTest(
`radar-beta
axis A,B,C
curve mycurve{1,2,3}`,
{ theme: 'base', themeVariables: { fontSize: 80, cScale0: '#FF0000' } }
);
});
it('should handle radar diagram with radar style override', () => {
imgSnapshotTest(
`radar-beta
axis A,B,C
curve mycurve{1,2,3}`,
{ theme: 'base', themeVariables: { radar: { axisColor: '#FF0000' } } }
);
});
});

View File

@@ -179,6 +179,7 @@ describe('XY Chart', () => {
axisLineWidth: 5
chartOrientation: horizontal
plotReservedSpacePercent: 60
showDataLabel: true
---
xychart-beta
title "Sales Revenue"
@@ -315,4 +316,516 @@ describe('XY Chart', () => {
);
cy.get('svg');
});
it('should render vertical bar chart with labels', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
`,
{}
);
});
it('should render horizontal bar chart with labels', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
`,
{}
);
});
it('should render vertical bar chart without labels by default', () => {
imgSnapshotTest(
`
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
`,
{}
);
});
it('should render horizontal bar chart without labels by default', () => {
imgSnapshotTest(
`
---
config:
xyChart:
chartOrientation: horizontal
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
`,
{}
);
});
it('should render multiple bar plots vertically with labels correctly', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Multiple Bar Plots"
x-axis Categories [A, B, C]
y-axis "Values" 0 --> 100
bar [10, 50, 90]
`,
{}
);
});
it('should render multiple bar plots horizontally with labels correctly', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Multiple Bar Plots"
x-axis Categories [A, B, C]
y-axis "Values" 0 --> 100
bar [10, 50, 90]
`,
{}
);
});
it('should render a single bar with label for a vertical xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Single Bar Chart"
x-axis Categories [A]
y-axis "Value" 0 --> 100
bar [75]
`,
{}
);
});
it('should render a single bar with label for a horizontal xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Single Bar Chart"
x-axis Categories [A]
y-axis "Value" 0 --> 100
bar [75]
`,
{}
);
});
it('should render negative and decimal values with correct labels for vertical xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Decimal and Negative Values"
x-axis Categories [A, B, C]
y-axis -10 --> 10
bar [ -2.5, 0.75, 5.1 ]
`,
{}
);
});
it('should render negative and decimal values with correct labels for horizontal xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Decimal and Negative Values"
x-axis Categories [A, B, C]
y-axis -10 --> 10
bar [ -2.5, 0.75, 5.1 ]
`,
{}
);
});
it('should render data labels within each bar in the vertical xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan,b,c]
y-axis "Revenue (in $)" 4000 --> 12000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 3000, 2000, 500, 2000, 3000, 11000, 5000, 6000]
`,
{}
);
cy.get('g.bar-plot-0').within(() => {
cy.get('rect').each(($rect, index) => {
// Extract bar properties
const barProps = {
x: parseFloat($rect.attr('x')),
y: parseFloat($rect.attr('y')),
width: parseFloat($rect.attr('width')),
height: parseFloat($rect.attr('height')),
};
// Get the text element corresponding to this bar by index.
cy.get('text')
.eq(index)
.then(($text) => {
const bbox = $text[0].getBBox();
const textProps = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
// Verify that the text label is positioned within the boundaries of the bar.
expect(textProps.x).to.be.greaterThan(barProps.x);
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
// Check horizontal alignment (within tolerance)
expect(textProps.x + textProps.width / 2).to.be.closeTo(
barProps.x + barProps.width / 2,
5
);
expect(textProps.y).to.be.greaterThan(barProps.y);
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
});
});
});
});
it('should render data labels within each bar in the horizontal xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan,b,c]
y-axis "Revenue (in $)" 4000 --> 12000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 3000, 2000, 500, 2000, 3000, 11000, 5000, 6000]
`,
{}
);
cy.get('g.bar-plot-0').within(() => {
cy.get('rect').each(($rect, index) => {
// Extract bar properties
const barProps = {
x: parseFloat($rect.attr('x')),
y: parseFloat($rect.attr('y')),
width: parseFloat($rect.attr('width')),
height: parseFloat($rect.attr('height')),
};
// Get the text element corresponding to this bar by index.
cy.get('text')
.eq(index)
.then(($text) => {
const bbox = $text[0].getBBox();
const textProps = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
// Verify that the text label is positioned within the boundaries of the bar.
expect(textProps.x).to.be.greaterThan(barProps.x);
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
expect(textProps.y).to.be.greaterThan(barProps.y);
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
expect(textProps.y + textProps.height / 2).to.be.closeTo(
barProps.y + barProps.height / 2,
5
);
});
});
});
});
it('should render data labels within each bar in the vertical xy-chart with a lot of bars of different sizes', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
y-axis "Revenue (in $)" 4000 --> 12000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 8000, 10000, 5000, 7600, 4999,11000 ,5000,6000]
`,
{}
);
cy.get('g.bar-plot-0').within(() => {
cy.get('rect').each(($rect, index) => {
// Extract bar properties
const barProps = {
x: parseFloat($rect.attr('x')),
y: parseFloat($rect.attr('y')),
width: parseFloat($rect.attr('width')),
height: parseFloat($rect.attr('height')),
};
// Get the text element corresponding to this bar by index.
cy.get('text')
.eq(index)
.then(($text) => {
const bbox = $text[0].getBBox();
const textProps = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
// Verify that the text label is positioned within the boundaries of the bar.
expect(textProps.x).to.be.greaterThan(barProps.x);
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
// Check horizontal alignment (within tolerance)
expect(textProps.x + textProps.width / 2).to.be.closeTo(
barProps.x + barProps.width / 2,
5
);
expect(textProps.y).to.be.greaterThan(barProps.y);
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
});
});
});
});
it('should render data labels within each bar in the horizontal xy-chart with a lot of bars of different sizes', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
y-axis "Revenue (in $)" 4000 --> 12000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 8000, 10000, 5000, 7600, 4999,11000 ,5000,6000]
`,
{}
);
cy.get('g.bar-plot-0').within(() => {
cy.get('rect').each(($rect, index) => {
// Extract bar properties
const barProps = {
x: parseFloat($rect.attr('x')),
y: parseFloat($rect.attr('y')),
width: parseFloat($rect.attr('width')),
height: parseFloat($rect.attr('height')),
};
// Get the text element corresponding to this bar by index.
cy.get('text')
.eq(index)
.then(($text) => {
const bbox = $text[0].getBBox();
const textProps = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
// Verify that the text label is positioned within the boundaries of the bar.
expect(textProps.x).to.be.greaterThan(barProps.x);
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
expect(textProps.y).to.be.greaterThan(barProps.y);
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
expect(textProps.y + textProps.height / 2).to.be.closeTo(
barProps.y + barProps.height / 2,
5
);
});
});
});
});
it('should render data labels correctly for a bar in the vertical xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan]
y-axis "Revenue (in $)" 3000 --> 12000
bar [4000]
`,
{}
);
cy.get('g.bar-plot-0').within(() => {
cy.get('rect').each(($rect, index) => {
// Extract bar properties
const barProps = {
x: parseFloat($rect.attr('x')),
y: parseFloat($rect.attr('y')),
width: parseFloat($rect.attr('width')),
height: parseFloat($rect.attr('height')),
};
// Get the text element corresponding to this bar by index.
cy.get('text')
.eq(index)
.then(($text) => {
const bbox = $text[0].getBBox();
const textProps = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
// Verify that the text label is positioned within the boundaries of the bar.
expect(textProps.x).to.be.greaterThan(barProps.x);
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
// Check horizontal alignment (within tolerance)
expect(textProps.x + textProps.width / 2).to.be.closeTo(
barProps.x + barProps.width / 2,
5
);
expect(textProps.y).to.be.greaterThan(barProps.y);
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
});
});
});
});
it('should render data labels correctly for a bar in the horizontal xy-chart', () => {
imgSnapshotTest(
`
---
config:
xyChart:
showDataLabel: true
chartOrientation: horizontal
---
xychart-beta
title "Sales Revenue"
x-axis Months [jan]
y-axis "Revenue (in $)" 3000 --> 12000
bar [4000]
`,
{}
);
cy.get('g.bar-plot-0').within(() => {
cy.get('rect').each(($rect, index) => {
// Extract bar properties
const barProps = {
x: parseFloat($rect.attr('x')),
y: parseFloat($rect.attr('y')),
width: parseFloat($rect.attr('width')),
height: parseFloat($rect.attr('height')),
};
// Get the text element corresponding to this bar by index.
cy.get('text')
.eq(index)
.then(($text) => {
const bbox = $text[0].getBBox();
const textProps = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};
// Verify that the text label is positioned within the boundaries of the bar.
expect(textProps.x).to.be.greaterThan(barProps.x);
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
expect(textProps.y).to.be.greaterThan(barProps.y);
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
expect(textProps.y + textProps.height / 2).to.be.closeTo(
barProps.y + barProps.height / 2,
5
);
});
});
});
});
});

337
cypress/platform/yari2.html Normal file
View File

@@ -0,0 +1,337 @@
<html>
<body>
<h1 class="header">Nodes</h1>
<div class="node-showcase">
<div class="test">
<h2>Basic ErNode</h2>
<pre class="mermaid">
---
config:
htmlLabels: false
look: handDrawn
theme: forest
---
erDiagram
_**hiØ**_[*test*] {
*__List~List~int~~sdfds__* __driversLicense__ PK "***The l😀icense #***"
*string(99)~T~~~~~~* firstName "Only 99 <br>characters are a<br>llowed dsfsdfsdfsdfs"
~str ing~ lastName
string phone UK
int age
}
style PERSON color:red, stroke:blue,fill:#f9f
classDef test,test2 stroke:red
class PERSON test,test2
</pre>
</div>
<div class="test">
<h2>Basic ErNode</h2>
<pre class="mermaid">
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
CAR:::someclass
PERSON:::anotherclass,someclass
classDef someclass fill:#f96
classDef anotherclass color:blue
</pre>
</div>
</div>
<h1 class="header">Diagram Testing</h1>
<div class="diagram-showcase">
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
htmlLabels: false
layout: elk
look: handDrawn
theme: forest
---
erDiagram
"hi" }o..o{ ORDER : places
style hi fill:lightblue
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
htmlLabels: false
look: handDrawn
layout: elk
---
erDiagram
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
htmlLabels: true
look: handDrawn
theme: forest
---
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
CAR {
test test PK "comment"
string make
string model
string[] parts
}
PERSON ||--o{ NAMED-DRIVER : is
PERSON ||--o{ CAR : is
PERSON {
string driversLicense PK "The license #"
string(99) firstName "Only 99 characters are allowed"
string lastName
string phone UK
int age
}
NAMED-DRIVER {
string carRegistrationNumber PK, FK
string driverLicence PK, FK
}
MANUFACTURER only one to zero or more CAR : makes
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
title: simple ER diagram
config:
theme: forest
---
erDiagram
direction TB
p[Pers😀on] {
string firstName
string lastName
}
a["Customer Account"] {
string email
}
p ||--o| a : has
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
layout: elk
---
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"
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
layout: elk
---
erDiagram
rental{
~timestamp with time zone~ rental_date "NN"
~integer~ inventory_id "NN"
~integer~ customer_id "NN"
~timestamp with time zone~ return_date
~integer~ staff_id "NN"
~integer~ rental_id "NN"
~timestamp with time zone~ last_update "NN"
}
film_actor{
~integer~ actor_id "NN"
~integer~ film_id "NN"
~timestamp with time zone~ last_update "NN"
}
film{
~text~ title "NN"
~text~ description
~public.year~ release_year
~integer~ language_id "NN"
~integer~ original_language_id
~smallint~ length
~text[]~ special_features
~tsvector~ fulltext "NN"
~integer~ film_id "NN"
~smallint~ rental_duration "NN"
~numeric(4,2)~ rental_rate "NN"
~numeric(5,2)~ replacement_cost "NN"
~public.mpaa_rating~ rating
~timestamp with time zone~ last_update "NN"
}
customer{
~integer~ store_id "NN"
~text~ first_name "NN"
~text~ last_name "NN"
~text~ email
~integer~ address_id "NN"
~integer~ active
~integer~ customer_id "NN"
~boolean~ activebool "NN"
~date~ create_date "NN"
~timestamp with time zone~ last_update
}
film_category{
~integer~ film_id "NN"
~integer~ category_id "NN"
~timestamp with time zone~ last_update "NN"
}
actor{
~text~ first_name "NN"
~text~ last_name "NN"
~integer~ actor_id "NN"
~timestamp with time zone~ last_update "NN"
}
store{
~integer~ manager_staff_id "NN"
~integer~ address_id "NN"
~integer~ store_id "NN"
~timestamp with time zone~ last_update "NN"
}
city{
~text~ city "NN"
~integer~ country_id "NN"
~integer~ city_id "NN"
~timestamp with time zone~ last_update "NN"
}
language{
~character(20)~ name "NN"
~integer~ language_id "NN"
~timestamp with time zone~ last_update "NN"
}
payment{
~integer~ customer_id "NN"
~integer~ staff_id "NN"
~integer~ rental_id "NN"
~numeric(5,2)~ amount "NN"
~timestamp with time zone~ payment_date "NN"
~integer~ payment_id "NN"
}
category{
~text~ name "NN"
~integer~ category_id "NN"
~timestamp with time zone~ last_update "NN"
}
inventory{
~integer~ film_id "NN"
~integer~ store_id "NN"
~integer~ inventory_id "NN"
~timestamp with time zone~ last_update "NN"
}
address{
~text~ address "NN"
~text~ address2
~text~ district "NN"
~integer~ city_id "NN"
~text~ postal_code
~text~ phone "NN"
~integer~ address_id "NN"
~timestamp with time zone~ last_update "NN"
}
staff{
~text~ first_name "NN"
~text~ last_name "NN"
~integer~ address_id "NN"
~text~ email
~integer~ store_id "NN"
~text~ username "NN"
~text~ password
~bytea~ picture
~integer~ staff_id "NN"
~boolean~ active "NN"
~timestamp with time zone~ last_update "NN"
}
country{
~text~ country "NN"
~integer~ country_id "NN"
~timestamp with time zone~ last_update "NN"
}
film_actor }|..|| film : film_actor_film_id_fkey
film_actor }|..|| actor : film_actor_actor_id_fkey
address }|..|| city : address_city_id_fkey
city }|..|| country : city_country_id_fkey
customer }|..|| store : customer_store_id_fkey
customer }|..|| address : customer_address_id_fkey
film }|..|| language : film_original_language_id_fkey
film }|..|| language : film_language_id_fkey
film_category }|..|| film : film_category_film_id_fkey
film_category }|..|| category : film_category_category_id_fkey
inventory }|..|| store : inventory_store_id_fkey
</pre>
</div>
</div>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
mermaid.initialize();
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
<style>
.header {
text-decoration: underline;
text-align: center;
}
.node-showcase {
display: grid;
grid-template-columns: 1fr 1fr;
}
.test {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.test > h2 {
margin: 0;
text-align: center;
}
.test > p {
margin-top: -6px;
color: gray;
}
.diagram-showcase {
display: grid;
grid-template-columns: 1fr;
}
</style>
</html>

View File

@@ -2,151 +2,211 @@
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
"duration": 4989
"duration": 5475
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 1382
"duration": 2037
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3178
"duration": 3207
},
{
"spec": "cypress/integration/other/iife.spec.js",
"duration": 1372
"duration": 1915
},
{
"spec": "cypress/integration/other/interaction.spec.js",
"duration": 8998
"duration": 10952
},
{
"spec": "cypress/integration/other/rerender.spec.js",
"duration": 1249
"duration": 1872
},
{
"spec": "cypress/integration/other/xss.spec.js",
"duration": 25664
"duration": 26686
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
"duration": 1928
"duration": 2629
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 2330
"duration": 104
},
{
"spec": "cypress/integration/rendering/block.spec.js",
"duration": 11156
"duration": 14765
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
"duration": 3418
"duration": 4913
},
{
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 36667
},
{
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 33813
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 14866
"duration": 20441
},
{
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 32504
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 9894
"duration": 13772
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 5778
"duration": 7978
},
{
"spec": "cypress/integration/rendering/current.spec.js",
"duration": 1690
"duration": 2101
},
{
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 76556
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 9144
"duration": 12756
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 1951
"duration": 2766
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 2196
"duration": 35641
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 21029
"duration": 26915
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 16087
"duration": 21171
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 27465
"duration": 37844
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 20035
"duration": 26254
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 11366
"duration": 15149
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 34025
"duration": 45049
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 185902
"duration": 250225
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 41631
"duration": 51531
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
"duration": 1736
"duration": 2455
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
"duration": 2247
"duration": 3181
},
{
"spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 6298
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
"duration": 2144
"duration": 3065
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 1646
"duration": 2521
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 6406
"duration": 9341
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 107219
"duration": 132809
},
{
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 101299
},
{
"spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 3481
},
{
"spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 4878
},
{
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 7416
},
{
"spec": "cypress/integration/rendering/radar.spec.js",
"duration": 4554
},
{
"spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2068
},
{
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 47583
},
{
"spec": "cypress/integration/rendering/sankey.spec.ts",
"duration": 5792
},
{
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 33035
},
{
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 22716
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 15834
"duration": 13868
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
"duration": 33240
"duration": 26376
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 7122
"duration": 5872
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 11127
"duration": 9469
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 2391
"duration": 2742
}
]
}

View File

@@ -91,6 +91,9 @@
<li>
<h2><a href="./architecture.html">Architecture</a></h2>
</li>
<li>
<h2><a href="./radar.html">Radar</a></h2>
</li>
</ul>
</body>
</html>

157
demos/radar.html Normal file
View File

@@ -0,0 +1,157 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<h1>Radar diagram demo</h1>
<div class="diagrams">
<pre class="mermaid">
radar-beta
title My favorite ninjas
axis Agility, Speed, Strength
axis Stam["Stamina"] , Intel["Intelligence"]
curve Ninja1["Naruto"]{
Agility 2, Speed 2,
Strength 3, Stam 5,
Intel 0
}
curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
showLegend true
ticks 3
max 8
min 0
graticule circle
</pre
>
<pre class="mermaid">
---
config:
radar:
axisScaleFactor: 0.25
axisLabelFactor: 0.95
---
radar-beta
title DevOps Radar
axis f["Feature Velocity"], s["Stability"]
axis r["Resilience"], e["Efficiency"]
axis c["Cost"], d["DevSecOps"]
curve app1["App1"]{
f 5, s 4.5, r 3.8, d 4.2, e 4.5, c 3.5
}
curve app2["App2"]{4, 3, 4, 3, 3, 4}, app3["App3"]{3, 2, 4, 3, 2, 3}
curve app4["App4"]{2, 1, 3.2, 2.5, 1, 2}
showLegend true
ticks 3
max 5
graticule polygon
</pre
>
<pre class="mermaid">
%%{init: {'theme': 'forest'} }%%
radar-beta
title Forest theme
axis Agility, Speed, Strength
axis Stam["Stamina"] , Intel["Intelligence"]
curve Ninja1["Naruto"]{
Agility 2, Speed 2,
Strength 3, Stam 5,
Intel 0
}
curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
</pre
>
<pre class="mermaid" style="background-color: black">
%%{init: {'theme': 'dark'} }%%
radar-beta
title Dark theme
axis Agility, Speed, Strength
axis Stam["Stamina"] , Intel["Intelligence"]
curve Ninja1["Naruto"]{
Agility 2, Speed 2,
Strength 3, Stam 5,
Intel 0
}
curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
</pre
>
<pre class="mermaid">
%%{init: {'theme': 'base', 'themeVariables': {'cScale0': '#ff0000', 'cScale1': '#00ff00', 'cScale2': '#0000ff'}} }%%
radar-beta
title Custom colors
axis Agility, Speed, Strength
axis Stam["Stamina"] , Intel["Intelligence"]
curve Ninja1["Naruto"]{
Agility 2, Speed 2,
Strength 3, Stam 5,
Intel 0
}
curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
</pre
>
<pre class="mermaid">
---
config:
radar:
axisScaleFactor: 0.25
curveTension: 0.1
theme: base
themeVariables:
cScale0: "#FF0000"
cScale1: "#00FF00"
cScale2: "#0000FF"
radar:
curveOpacity: 0
---
radar-beta
title Custom colors, axisScaleFactor, curveTension, opacity
axis A, B, C, D, E
curve c1{1,2,3,4,5}
curve c2{5,4,3,2,1}
curve c3{3,3,3,3,3}
</pre>
</div>
<script type="module">
import mermaid from '/mermaid.esm.mjs';
mermaid.initialize({
logLevel: 3,
securityLevel: 'loose',
});
</script>
<style>
.diagrams {
display: flex;
flex-wrap: wrap;
}
pre {
width: 45vw;
padding: 2em;
}
</style>
</body>
</html>

View File

@@ -239,6 +239,22 @@ Code is the heart of every software project. We strive to make it better. Who if
The core of Mermaid is located under `packages/mermaid/src`.
### Building Mermaid Locally
**Host**
```bash
pnpm run build
```
**Docker**
```bash
./run build
```
This will build the Mermaid library and the documentation site.
### Running Mermaid Locally
**Host**

View File

@@ -12,4 +12,4 @@
> `const` **configKeys**: `Set`<`string`>
Defined in: [packages/mermaid/src/defaultConfig.ts:270](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L270)
Defined in: [packages/mermaid/src/defaultConfig.ts:274](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L274)

View File

@@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string.
> `optional` **dompurifyConfig**: `Config`
Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
---
@@ -167,7 +167,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
> `optional` **fontSize**: `number`
Defined in: [packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205)
---
@@ -280,7 +280,7 @@ Defines which main look to use for the diagram.
> `optional` **markdownAutoWrap**: `boolean`
Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205)
Defined in: [packages/mermaid/src/config.type.ts:206](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L206)
---
@@ -336,6 +336,14 @@ Defined in: [packages/mermaid/src/config.type.ts:191](https://github.com/mermaid
---
### radar?
> `optional` **radar**: `RadarDiagramConfig`
Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
---
### requirement?
> `optional` **requirement**: `RequirementDiagramConfig`
@@ -404,7 +412,7 @@ Defined in: [packages/mermaid/src/config.type.ts:188](https://github.com/mermaid
> `optional` **suppressErrorRendering**: `boolean`
Defined in: [packages/mermaid/src/config.type.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L211)
Defined in: [packages/mermaid/src/config.type.ts:212](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L212)
Suppresses inserting 'Syntax error' diagram in the DOM.
This is useful when you want to control how to handle syntax errors in your application.
@@ -450,7 +458,7 @@ Defined in: [packages/mermaid/src/config.type.ts:186](https://github.com/mermaid
> `optional` **wrap**: `boolean`
Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
Defined in: [packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
---

View File

@@ -40,6 +40,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Mermaid Charts & Diagrams for Jira](https://marketplace.atlassian.com/apps/1224537/)
- [Mermaid for Jira Cloud - Draw UML diagrams easily](https://marketplace.atlassian.com/apps/1223053/mermaid-for-jira-cloud-draw-uml-diagrams-easily?hosting=cloud&tab=overview)
- [CloudScript.io Mermaid Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
- [Mermaid plus for Confluence](https://marketplace.atlassian.com/apps/1236814/mermaid-plus-for-confluence?hosting=cloud&tab=overview)
- [Azure Devops](https://learn.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#add-mermaid-diagrams-to-a-wiki-page) ✅
- [Deepdwn](https://billiam.itch.io/deepdwn) ✅
- [Doctave](https://www.doctave.com/) ✅
@@ -267,7 +268,5 @@ Communication tools and platforms
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
- [Reveal CK](https://github.com/jedcn/reveal-ck)
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
- [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic)
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
<!--- cspell:ignore Blazorade HueHive --->

View File

@@ -92,7 +92,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
Where:
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens.
- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space").
- `relationship` describes the way that both entities inter-relate. See below.
- `second-entity` is the name of the other entity.
- `relationship-label` describes the relationship from the perspective of the first entity.
@@ -107,6 +107,34 @@ This statement can be read as _a property contains one or more rooms, and a room
Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory.
#### Unicode text
Entity names, relationships, and attributes all support unicode text.
```mermaid-example
erDiagram
"This ❤ Unicode"
```
```mermaid
erDiagram
"This ❤ Unicode"
```
#### Markdown formatting
Markdown formatting and text is also supported.
```mermaid-example
erDiagram
"This **is** _Markdown_"
```
```mermaid
erDiagram
"This **is** _Markdown_"
```
### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components:
@@ -145,6 +173,11 @@ Cardinality is a property that describes how many elements of another entity can
Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line:
| Value | Alias for |
| :---: | :---------------: |
| -- | _identifying_ |
| .. | _non-identifying_ |
**Aliases**
| Value | Alias for |
@@ -155,13 +188,25 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and
```mermaid-example
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
PERSON ||--o{ NAMED-DRIVER : is
PERSON }o..o{ NAMED-DRIVER : is
```
```mermaid
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
PERSON ||--o{ NAMED-DRIVER : is
PERSON }o..o{ NAMED-DRIVER : is
```
```mermaid-example
erDiagram
CAR 1 to zero or more NAMED-DRIVER : allows
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
```mermaid
erDiagram
CAR 1 to zero or more NAMED-DRIVER : allows
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
### Attributes
@@ -202,9 +247,9 @@ erDiagram
The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types.
### Entity Name Aliases (v10.5.0+)
### Entity Name Aliases
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name.
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names.
```mermaid-example
erDiagram
@@ -232,7 +277,7 @@ erDiagram
#### Attribute Keys and Comments
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
```mermaid-example
erDiagram
@@ -282,35 +327,344 @@ erDiagram
MANUFACTURER only one to zero or more CAR : makes
```
### Other Things
### Direction
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
- (v11.1.0+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
The direction statement declares the direction of the diagram.
## Styling
This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`).
### Config options
```mermaid-example
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
For simple color customization:
```mermaid
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
| Name | Used as |
| :------- | :------------------------------------------------------------------- |
| `fill` | Background color of an entity or attribute |
| `stroke` | Border color of an entity or attribute, line color of a relationship |
This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`).
### Classes used
```mermaid-example
erDiagram
direction LR
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
The following CSS class selectors are available for richer styling:
```mermaid
erDiagram
direction LR
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
| Selector | Description |
| :------------------------- | :---------------------------------------------------- |
| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows |
| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows |
| `.er.entityBox` | The box representing an entity |
| `.er.entityLabel` | The label for an entity |
| `.er.relationshipLabel` | The label for a relationship |
| `.er.relationshipLabelBox` | The box surrounding a relationship label |
| `.er.relationshipLine` | The line representing a relationship between entities |
Possible diagram orientations are:
- TB - Top to bottom
- BT - Bottom to top
- RL - Right to left
- LR - Left to right
### Styling a node
It is possible to apply specific styles such as a thicker border or a different background color to a node.
```mermaid-example
erDiagram
id1||--||id2 : label
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
```
```mermaid
erDiagram
id1||--||id2 : label
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
```
It is also possible to attach styles to a list of nodes in one statement:
```
style nodeId1,nodeId2 styleList
```
#### Classes
More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that
should have a different look.
A class definition looks like the example below:
```
classDef className fill:#f9f,stroke:#333,stroke-width:4px
```
It is also possible to define multiple classes in one statement:
```
classDef firstClassName,secondClassName font-size:12pt
```
Attachment of a class to a node is done as per below:
```
class nodeId1 className
```
It is also possible to attach a class to a list of nodes in one statement:
```
class nodeId1,nodeId2 className
```
Multiple classes can be attached at the same time as well:
```
class nodeId1,nodeId2 className1,className2
```
A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below:
```mermaid-example
erDiagram
direction TB
CAR:::someclass {
string registrationNumber
string make
string model
}
PERSON:::someclass {
string firstName
string lastName
int age
}
HOUSE:::someclass
classDef someclass fill:#f96
```
```mermaid
erDiagram
direction TB
CAR:::someclass {
string registrationNumber
string make
string model
}
PERSON:::someclass {
string firstName
string lastName
int age
}
HOUSE:::someclass
classDef someclass fill:#f96
```
This form can be used when declaring relationships between entities:
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
```mermaid
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
Similar to the class statement, the shorthand syntax can also apply multiple classes at once:
```
nodeId:::className1,className2
```
### Default class
If a class is named default it will be assigned to all classes without specific class definitions.
```
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
```
> **Note:** Custom styles from style or other class statements take priority and will overwrite the default styles. (e.g. The `default` class gives nodes a background color of pink but the `blue` class will give that node a background color of blue if applied.)
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef default fill:#f9f,stroke-width:4px
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
```mermaid
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef default fill:#f9f,stroke-width:4px
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
## Configuration
### Layout
The layout of the diagram is handled by [`render()`](../config/setup/mermaid/interfaces/Mermaid.md#render). The default layout is dagre.
For larger or more-complex diagrams, you can alternatively apply the ELK (Eclipse Layout Kernel) layout using your YAML frontmatter's `config`. For more information, see [Customizing ELK Layout](../intro/syntax-reference.md#customizing-elk-layout).
```yaml
---
config:
layout: elk
---
```
Your Mermaid code should be similar to the following:
```mermaid-example
---
title: Order example
config:
layout: elk
---
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
```mermaid
---
title: Order example
config:
layout: elk
---
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
> **Note**
> Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration.
<!--- cspell:locale en,en-gb --->

View File

@@ -1193,12 +1193,12 @@ To give an edge an ID, prepend the edge syntax with the ID followed by an `@` ch
```mermaid-example
flowchart LR
A e1@> B
A e1@--> B
```
```mermaid
flowchart LR
A e1@> B
A e1@--> B
```
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
@@ -1229,13 +1229,13 @@ In the initial version, two animation speeds are supported: `fast` and `slow`. S
```mermaid-example
flowchart LR
A e1@> B
A e1@--> B
e1@{ animation: fast }
```
```mermaid
flowchart LR
A e1@> B
A e1@--> B
e1@{ animation: fast }
```
@@ -1247,14 +1247,14 @@ You can also animate edges by assigning a class to them and then defining animat
```mermaid-example
flowchart LR
A e1@> B
A e1@--> B
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
```
```mermaid
flowchart LR
A e1@> B
A e1@--> B
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
```

253
docs/syntax/radar.md Normal file
View File

@@ -0,0 +1,253 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/radar.md](../../packages/mermaid/src/docs/syntax/radar.md).
# Radar Diagram (v11.6.0+)
## Introduction
A radar diagram is a simple way to plot low-dimensional data in a circular format.
It is also known as a **radar chart**, **spider chart**, **star chart**, **cobweb chart**, **polar chart**, or **Kiviat diagram**.
## Usage
This diagram type is particularly useful for developers, data scientists, and engineers who require a clear and concise way to represent data in a circular format.
It is commonly used to graphically summarize and compare the performance of multiple entities across multiple dimensions.
## Syntax
```md
radar-beta
axis A, B, C, D, E
curve c1{1,2,3,4,5}
curve c2{5,4,3,2,1}
... More Fields ...
```
## Examples
```mermaid-example
---
title: "Grades"
---
radar-beta
axis m["Math"], s["Science"], e["English"]
axis h["History"], g["Geography"], a["Art"]
curve a["Alice"]{85, 90, 80, 70, 75, 90}
curve b["Bob"]{70, 75, 85, 80, 90, 85}
max 100
min 0
```
```mermaid
---
title: "Grades"
---
radar-beta
axis m["Math"], s["Science"], e["English"]
axis h["History"], g["Geography"], a["Art"]
curve a["Alice"]{85, 90, 80, 70, 75, 90}
curve b["Bob"]{70, 75, 85, 80, 90, 85}
max 100
min 0
```
```mermaid-example
radar-beta
title Restaurant Comparison
axis food["Food Quality"], service["Service"], price["Price"]
axis ambiance["Ambiance"],
curve a["Restaurant A"]{4, 3, 2, 4}
curve b["Restaurant B"]{3, 4, 3, 3}
curve c["Restaurant C"]{2, 3, 4, 2}
curve d["Restaurant D"]{2, 2, 4, 3}
graticule polygon
max 5
```
```mermaid
radar-beta
title Restaurant Comparison
axis food["Food Quality"], service["Service"], price["Price"]
axis ambiance["Ambiance"],
curve a["Restaurant A"]{4, 3, 2, 4}
curve b["Restaurant B"]{3, 4, 3, 3}
curve c["Restaurant C"]{2, 3, 4, 2}
curve d["Restaurant D"]{2, 2, 4, 3}
graticule polygon
max 5
```
## Details of Syntax
### Title
`title`: The title is an optional field that allows to render a title at the top of the radar diagram.
```
radar-beta
title Title of the Radar Diagram
...
```
### Axis
`axis`: The axis keyword is used to define the axes of the radar diagram.
Each axis is represented by an ID and an optional label.
Multiple axes can be defined in a single line.
```
radar-beta
axis id1["Label1"]
axis id2["Label2"], id3["Label3"]
...
```
### Curve
`curve`: The curve keyword is used to define the data points for a curve in the radar diagram.
Each curve is represented by an ID, an optional label, and a list of values.
Values can be defined by a list of numbers or a list of key-value pairs. If key-value pairs are used, the key represents the axis ID and the value represents the data point. Else, the data points are assumed to be in the order of the axes defined.
Multiple curves can be defined in a single line.
```
radar-beta
axis axis1, axis2, axis3
curve id1["Label1"]{1, 2, 3}
curve id2["Label2"]{4, 5, 6}, id3{7, 8, 9}
curve id4{ axis3: 30, axis1: 20, axis2: 10 }
...
```
### Options
- `showLegend`: The showLegend keyword is used to show or hide the legend in the radar diagram. The legend is shown by default.
- `max`: The maximum value for the radar diagram. This is used to scale the radar diagram. If not provided, the maximum value is calculated from the data points.
- `min`: The minimum value for the radar diagram. This is used to scale the radar diagram. If not provided, the minimum value is `0`.
- `graticule`: The graticule keyword is used to define the type of graticule to be rendered in the radar diagram. The graticule can be `circle` or `polygon`. If not provided, the default graticule is `circle`.
- `ticks`: The ticks keyword is used to define the number of ticks on the graticule. It is the number of concentric circles or polygons drawn to indicate the scale of the radar diagram. If not provided, the default number of ticks is `5`.
```
radar-beta
...
showLegend true
max 100
min 0
graticule circle
ticks 5
...
```
## Configuration
Please refer to the [configuration](/config/schema-docs/config-defs-radar-diagram-config.html) guide for details.
| Parameter | Description | Default Value |
| --------------- | ---------------------------------------- | ------------- |
| width | Width of the radar diagram | `600` |
| height | Height of the radar diagram | `600` |
| marginTop | Top margin of the radar diagram | `50` |
| marginBottom | Bottom margin of the radar diagram | `50` |
| marginLeft | Left margin of the radar diagram | `50` |
| marginRight | Right margin of the radar diagram | `50` |
| axisScaleFactor | Scale factor for the axis | `1` |
| axisLabelFactor | Factor to adjust the axis label position | `1.05` |
| curveTension | Tension for the rounded curves | `0.17` |
## Theme Variables
### Global Theme Variables
> **Note**
> The default values for these variables depend on the theme used. To override the default values, set the desired values in the themeVariables section of the configuration:
> %%{init: {"themeVariables": {"cScale0": "#FF0000", "cScale1": "#00FF00"}} }%%
Radar charts support the color scales `cScale${i}` where `i` is a number from `0` to the theme's maximum number of colors in its color scale. Usually, the maximum number of colors is `12`.
| Property | Description |
| ---------- | ------------------------------ |
| fontSize | Font size of the title |
| titleColor | Color of the title |
| cScale${i} | Color scale for the i-th curve |
### Radar Style Options
> **Note**
> Specific variables for radar resides inside the `radar` key. To set the radar style options, use this syntax.
> %%{init: {"themeVariables": {"radar": {"axisColor": "#FF0000"}} } }%%
| Property | Description | Default Value |
| -------------------- | ---------------------------- | ------------- |
| axisColor | Color of the axis lines | `black` |
| axisStrokeWidth | Width of the axis lines | `1` |
| axisLabelFontSize | Font size of the axis labels | `12px` |
| curveOpacity | Opacity of the curves | `0.7` |
| curveStrokeWidth | Width of the curves | `2` |
| graticuleColor | Color of the graticule | `black` |
| graticuleOpacity | Opacity of the graticule | `0.5` |
| graticuleStrokeWidth | Width of the graticule | `1` |
| legendBoxSize | Size of the legend box | `10` |
| legendFontSize | Font size of the legend | `14px` |
## Example on config and theme
```mermaid-example
---
config:
radar:
axisScaleFactor: 0.25
curveTension: 0.1
theme: base
themeVariables:
cScale0: "#FF0000"
cScale1: "#00FF00"
cScale2: "#0000FF"
radar:
curveOpacity: 0
---
radar-beta
axis A, B, C, D, E
curve c1{1,2,3,4,5}
curve c2{5,4,3,2,1}
curve c3{3,3,3,3,3}
```
```mermaid
---
config:
radar:
axisScaleFactor: 0.25
curveTension: 0.1
theme: base
themeVariables:
cScale0: "#FF0000"
cScale1: "#00FF00"
cScale2: "#0000FF"
radar:
curveOpacity: 0
---
radar-beta
axis A, B, C, D, E
curve c1{1,2,3,4,5}
curve c2{5,4,3,2,1}
curve c3{3,3,3,3,3}
```
<!--- cspell:ignore Kiviat --->

View File

@@ -108,7 +108,7 @@ xychart-beta
## Chart Configurations
| Parameter | Description | Default value |
| ------------------------ | ---------------------------------------------- | :-----------: |
| ------------------------ | ------------------------------------------------------------- | :-----------: |
| width | Width of the chart | 700 |
| height | Height of the chart | 500 |
| titlePadding | Top and Bottom padding of the title | 10 |
@@ -118,6 +118,7 @@ xychart-beta
| yAxis | yAxis configuration | AxisConfig |
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
| showDataLabel | Should show the value corresponding to the bar within the bar | false |
### AxisConfig
@@ -163,6 +164,7 @@ config:
xyChart:
width: 900
height: 600
showDataLabel: true
themeVariables:
xyChart:
titleColor: "#ff0000"
@@ -181,6 +183,7 @@ config:
xyChart:
width: 900
height: 600
showDataLabel: true
themeVariables:
xyChart:
titleColor: "#ff0000"

View File

@@ -64,12 +64,12 @@
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.44.9",
"@argos-ci/cypress": "^3.2.0",
"@argos-ci/cypress": "^4.0.3",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.27.12",
"@cspell/eslint-plugin": "^8.8.4",
"@cspell/eslint-plugin": "^8.18.1",
"@cypress/code-coverage": "^3.12.49",
"@eslint/js": "^9.4.0",
"@eslint/js": "^9.24.0",
"@rollup/plugin-typescript": "^12.1.2",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
@@ -93,19 +93,19 @@
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.14",
"esbuild": "^0.25.0",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-cypress": "^4.1.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-cypress": "^4.2.1",
"eslint-plugin-html": "^8.1.2",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsdoc": "^50.0.1",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-json": "^4.0.1",
"eslint-plugin-lodash": "^8.0.0",
"eslint-plugin-markdown": "^5.1.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^57.0.0",
"express": "^4.19.2",
"eslint-plugin-unicorn": "^58.0.0",
"express": "^5.1.0",
"globals": "^16.0.0",
"globby": "^14.0.2",
"husky": "^9.1.7",
@@ -126,7 +126,7 @@
"tslib": "^2.8.1",
"tsx": "^4.7.3",
"typescript": "~5.7.3",
"typescript-eslint": "^8.24.1",
"typescript-eslint": "^8.29.1",
"vite": "^6.1.1",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6"

View File

@@ -1,5 +1,68 @@
# mermaid
## 11.6.0
### Minor Changes
- [#6408](https://github.com/mermaid-js/mermaid/pull/6408) [`ad65313`](https://github.com/mermaid-js/mermaid/commit/ad653138e16765d095613a6e5de86dc5e52ac8f0) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix: restore curve type configuration functionality for flowcharts. This fixes the issue where curve type settings were not being applied when configured through any of the following methods:
- Config
- Init directive (%%{ init: { 'flowchart': { 'curve': '...' } } }%%)
- LinkStyle command (linkStyle default interpolate ...)
- [#6381](https://github.com/mermaid-js/mermaid/pull/6381) [`95d73bc`](https://github.com/mermaid-js/mermaid/commit/95d73bc3f064dbf261a06483f94a7ef4d0bb52eb) Thanks [@thomascizeron](https://github.com/thomascizeron)! - Add Radar Chart
### Patch Changes
- [#2](https://github.com/calvinvette/mermaid/pull/2) [`16d9b63`](https://github.com/mermaid-js/mermaid/commit/16d9b6345749ab5f24d5b8433efc3635d4913863) Thanks [@calvinvette](https://github.com/calvinvette)! - - [#6388](https://github.com/mermaid-js/mermaid/pull/6386)
Thanks [@bollwyvl](https://github.com/bollwyvl) - Fix requirement diagram containment arrow
- Updated dependencies [[`95d73bc`](https://github.com/mermaid-js/mermaid/commit/95d73bc3f064dbf261a06483f94a7ef4d0bb52eb)]:
- @mermaid-js/parser@0.4.0
## 11.5.0
### Minor Changes
- [#6187](https://github.com/mermaid-js/mermaid/pull/6187) [`7809b5a`](https://github.com/mermaid-js/mermaid/commit/7809b5a93fae127f45727071f5ff14325222c518) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line
- [#6136](https://github.com/mermaid-js/mermaid/pull/6136) [`ec0d9c3`](https://github.com/mermaid-js/mermaid/commit/ec0d9c389aa6018043187654044c1e0b5aa4f600) Thanks [@knsv](https://github.com/knsv)! - Adding support for animation of flowchart edges
- [#6373](https://github.com/mermaid-js/mermaid/pull/6373) [`05bdf0e`](https://github.com/mermaid-js/mermaid/commit/05bdf0e20e2629fe77513218fbd4e28e65f75882) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Upgrade Requirement and ER diagram to use the common renderer flow
- Added support for directions
- Added support for hand drawn look
- [#6371](https://github.com/mermaid-js/mermaid/pull/6371) [`4d25cab`](https://github.com/mermaid-js/mermaid/commit/4d25caba8e65df078966a283e7e0ae1200bef595) Thanks [@knsv](https://github.com/knsv)! - The arrowhead color should match the color of the edge. Creates a unique clone of the arrow marker with the appropriate color.
### Patch Changes
- [#6064](https://github.com/mermaid-js/mermaid/pull/6064) [`2a91849`](https://github.com/mermaid-js/mermaid/commit/2a91849a38641e97ed6b20cb60aa4506d1b63177) Thanks [@NicolasNewman](https://github.com/NicolasNewman)! - fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments
- [#6198](https://github.com/mermaid-js/mermaid/pull/6198) [`963efa6`](https://github.com/mermaid-js/mermaid/commit/963efa64c794466dcd0f06bad6de6ba554d05a54) Thanks [@ferozmht](https://github.com/ferozmht)! - Fixes for consistent edge id creation & handling edge cases for animate edge feature
- [#6196](https://github.com/mermaid-js/mermaid/pull/6196) [`127bac1`](https://github.com/mermaid-js/mermaid/commit/127bac1147034d8a8588cc8f7870abe92ebc945e) Thanks [@knsv](https://github.com/knsv)! - Fix for issue #6195 - allowing @ signs inside node labels
- [#6212](https://github.com/mermaid-js/mermaid/pull/6212) [`90bbf90`](https://github.com/mermaid-js/mermaid/commit/90bbf90a83bf5da53fc8030cf1370bc8238fa4aa) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram
- [#6218](https://github.com/mermaid-js/mermaid/pull/6218) [`232e60c`](https://github.com/mermaid-js/mermaid/commit/232e60c8cbaea804e6d98aa90f90d1ce76730e17) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: revert state db to resolve getData returning empty nodes and edges
- [#6250](https://github.com/mermaid-js/mermaid/pull/6250) [`9cad3c7`](https://github.com/mermaid-js/mermaid/commit/9cad3c7aea3bbbc61495b23225ccff76d312783f) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - `mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams
- [#6293](https://github.com/mermaid-js/mermaid/pull/6293) [`cfd84e5`](https://github.com/mermaid-js/mermaid/commit/cfd84e54d502f4d36a35b50478121558cfbef2c4) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - Added versioning to StateDB and updated tests and diagrams to use it.
- [#6161](https://github.com/mermaid-js/mermaid/pull/6161) [`6cc31b7`](https://github.com/mermaid-js/mermaid/commit/6cc31b74530baa6d0f527346ab1395b0896bb3c2) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart
- [#6272](https://github.com/mermaid-js/mermaid/pull/6272) [`ffa7804`](https://github.com/mermaid-js/mermaid/commit/ffa7804af0701b3d044d6794e36bd9132d6c7e8d) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages.
- [#6205](https://github.com/mermaid-js/mermaid/pull/6205) [`32a68d4`](https://github.com/mermaid-js/mermaid/commit/32a68d489ed83a5b79f516d6b2fb3a7505c5eb24) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config.
- [#6295](https://github.com/mermaid-js/mermaid/pull/6295) [`da6361f`](https://github.com/mermaid-js/mermaid/commit/da6361f6527918b4b6a9c07cc9558cf2e2c709d2) Thanks [@omkarht](https://github.com/omkarht)! - fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction
- [#6185](https://github.com/mermaid-js/mermaid/pull/6185) [`3e32332`](https://github.com/mermaid-js/mermaid/commit/3e32332814c659e7ed1bb73d4a26ed4e61b77d59) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - `mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram
## 11.4.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.4.1",
"version": "11.6.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -78,7 +78,7 @@
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.11",
"dayjs": "^1.11.13",
"dompurify": "^3.2.4",
"dompurify": "^3.2.5",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",

View File

@@ -199,6 +199,7 @@ export interface MermaidConfig {
sankey?: SankeyDiagramConfig;
packet?: PacketDiagramConfig;
block?: BlockDiagramConfig;
radar?: RadarDiagramConfig;
dompurifyConfig?: DOMPurifyConfiguration;
wrap?: boolean;
fontSize?: number;
@@ -261,7 +262,19 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
* Defines how mermaid renders curves for flowcharts.
*
*/
curve?: 'basis' | 'linear' | 'cardinal';
curve?:
| 'basis'
| 'bumpX'
| 'bumpY'
| 'cardinal'
| 'catmullRom'
| 'linear'
| 'monotoneX'
| 'monotoneY'
| 'natural'
| 'step'
| 'stepAfter'
| 'stepBefore';
/**
* Represents the padding between the labels and the shape
*
@@ -546,6 +559,10 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
* Margin between actors
*/
leftMargin?: number;
/**
* Maximum width of actor labels
*/
maxLabelWidth?: number;
/**
* Width of actor boxes
*/
@@ -604,6 +621,18 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
actorColours?: string[];
sectionFills?: string[];
sectionColours?: string[];
/**
* Color of the title text in Journey Diagrams
*/
titleColor?: string;
/**
* Font family to be used for the title text in Journey Diagrams
*/
titleFontFamily?: string;
/**
* Font size to be used for the title text in Journey Diagrams
*/
titleFontSize?: string;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
@@ -795,6 +824,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
*
*/
entityPadding?: number;
nodeSpacing?: number;
rankSpacing?: number;
/**
* Stroke color of box edges and lines.
*/
@@ -920,6 +951,10 @@ export interface XYChartConfig extends BaseDiagramConfig {
* Top and bottom space from the chart title
*/
titlePadding?: number;
/**
* Should show the value corresponding to the bar within the bar
*/
showDataLabel?: boolean;
/**
* Should show the chart title
*/
@@ -1524,6 +1559,50 @@ export interface PacketDiagramConfig extends BaseDiagramConfig {
export interface BlockDiagramConfig extends BaseDiagramConfig {
padding?: number;
}
/**
* The object containing configurations specific for radar diagrams.
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "RadarDiagramConfig".
*/
export interface RadarDiagramConfig extends BaseDiagramConfig {
/**
* The size of the radar diagram.
*/
width?: number;
/**
* The size of the radar diagram.
*/
height?: number;
/**
* The margin from the top of the radar diagram.
*/
marginTop?: number;
/**
* The margin from the right of the radar diagram.
*/
marginRight?: number;
/**
* The margin from the bottom of the radar diagram.
*/
marginBottom?: number;
/**
* The margin from the left of the radar diagram.
*/
marginLeft?: number;
/**
* The scale factor of the axis.
*/
axisScaleFactor?: number;
/**
* The scale factor of the axis label.
*/
axisLabelFactor?: number;
/**
* The tension factor for the Catmull-Rom spline conversion to cubic Bézier curves.
*/
curveTension?: number;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "FontConfig".

View File

@@ -255,8 +255,12 @@ const config: RequiredDeep<MermaidConfig> = {
packet: {
...defaultConfigJson.packet,
},
radar: {
...defaultConfigJson.radar,
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyify = (obj: any, prefix = ''): string[] =>
Object.keys(obj).reduce((res: string[], el): string[] => {
if (Array.isArray(obj[el])) {

View File

@@ -22,6 +22,7 @@ import mindmap from '../diagrams/mindmap/detector.js';
import kanban from '../diagrams/kanban/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { packet } from '../diagrams/packet/detector.js';
import { radar } from '../diagrams/radar/detector.js';
import block from '../diagrams/block/blockDetector.js';
import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
@@ -94,6 +95,7 @@ export const addDiagrams = () => {
packet,
xychart,
block,
architecture
architecture,
radar
);
};

View File

@@ -0,0 +1,70 @@
import { it, describe, expect } from 'vitest';
import { db } from './architectureDb.js';
import { parser } from './architectureParser.js';
const {
clear,
getDiagramTitle,
getAccTitle,
getAccDescription,
getServices,
getGroups,
getEdges,
getJunctions,
} = db;
describe('architecture diagrams', () => {
beforeEach(() => {
clear();
});
describe('architecture diagram definitions', () => {
it('should handle the architecture keyword', async () => {
const str = `architecture-beta`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should handle an simple radar definition', async () => {
const str = `architecture-beta
service db
`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
});
describe('should handle TitleAndAccessibilities', () => {
it('should handle title on the first line', async () => {
const str = `architecture-beta title Simple Architecture Diagram`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
});
it('should handle title on another line', async () => {
const str = `architecture-beta
title Simple Architecture Diagram
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
});
it('should handle accessibility title and description', async () => {
const str = `architecture-beta
accTitle: Accessibility Title
accDescr: Accessibility Description
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccTitle()).toBe('Accessibility Title');
expect(getAccDescription()).toBe('Accessibility Description');
});
it('should handle multiline accessibility description', async () => {
const str = `architecture-beta
accDescr {
Accessibility Description
}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccDescription()).toBe('Accessibility Description');
});
});
});

View File

@@ -1,6 +1,6 @@
import type { ArchitectureDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { getConfig as commonGetConfig } from '../../config.js';
import type { D3Element } from '../../types.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import {
@@ -33,6 +33,7 @@ import {
isArchitectureService,
shiftPositionByArchitectureDirectionPair,
} from './architectureTypes.js';
import { cleanAndMerge } from '../../utils.js';
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
DEFAULT_CONFIG.architecture;
@@ -316,6 +317,14 @@ const setElementForId = (id: string, element: D3Element) => {
};
const getElementById = (id: string) => state.records.elements[id];
const getConfig = (): Required<ArchitectureDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture,
});
return config;
};
export const db: ArchitectureDB = {
clear,
setDiagramTitle,
@@ -324,6 +333,7 @@ export const db: ArchitectureDB = {
getAccTitle,
setAccDescription,
getAccDescription,
getConfig,
addService,
getServices,
@@ -348,9 +358,5 @@ export const db: ArchitectureDB = {
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
field: T
): Required<ArchitectureDiagramConfig>[T] {
const arch = getConfig().architecture;
if (arch?.[field]) {
return arch[field] as Required<ArchitectureDiagramConfig>[T];
}
return DEFAULT_ARCHITECTURE_CONFIG[field];
return getConfig()[field];
}

View File

@@ -500,6 +500,8 @@ function layoutArchitecture(
}
export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) => {
// TODO: Add title support for architecture diagrams
const db = diagObj.db as ArchitectureDB;
const services = db.getServices();

View File

@@ -1,4 +1,4 @@
import type { DiagramDB } from '../../diagram-api/types.js';
import type { DiagramDBBase } from '../../diagram-api/types.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js';
import type { D3Element } from '../../types.js';
import type cytoscape from 'cytoscape';
@@ -242,7 +242,7 @@ export interface ArchitectureEdge<DT = ArchitectureDirection> {
title?: string;
}
export interface ArchitectureDB extends DiagramDB {
export interface ArchitectureDB extends DiagramDBBase<ArchitectureDiagramConfig> {
clear: () => void;
addService: (service: Omit<ArchitectureService, 'edges'>) => void;
getServices: () => ArchitectureService[];

View File

@@ -1,103 +0,0 @@
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
let entities = new Map();
let relationships = [];
const Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
};
const Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
/**
* Add entity
* @param {string} name - The name of the entity
* @param {string | undefined} alias - The alias of the entity
*/
const addEntity = function (name, alias = undefined) {
if (!entities.has(name)) {
entities.set(name, { attributes: [], alias });
log.info('Added new entity :', name);
} else if (!entities.get(name).alias && alias) {
entities.get(name).alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return entities.get(name);
};
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]);
log.debug('Added attribute ', attribs[i].attributeName);
}
};
/**
* Add a relationship
*
* @param entA The first entity in the relationship
* @param rolA The role played by the first entity in relation to the second
* @param entB The second entity in the relationship
* @param rSpec The details of the relationship between the two entities
*/
const addRelationship = function (entA, rolA, entB, rSpec) {
let rel = {
entityA: entA,
roleA: rolA,
entityB: entB,
relSpec: rSpec,
};
relationships.push(rel);
log.debug('Added new relationship :', rel);
};
const getRelationships = () => relationships;
const clear = function () {
entities = new Map();
relationships = [];
commonClear();
};
export default {
Cardinality,
Identification,
getConfig: () => getConfig().er,
addEntity,
addAttributes,
getEntities,
addRelationship,
getRelationships,
clear,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
setDiagramTitle,
getDiagramTitle,
};

View File

@@ -0,0 +1,251 @@
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 {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { getEdgeId } from '../../utils.js';
import type { DiagramDB } from '../../diagram-api/types.js';
export class ErDB implements DiagramDB {
private entities = new Map<string, EntityNode>();
private relationships: Relationship[] = [];
private classes = new Map<string, EntityClass>();
private direction = 'TB';
private Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
};
private Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
constructor() {
this.clear();
this.addEntity = this.addEntity.bind(this);
this.addAttributes = this.addAttributes.bind(this);
this.addRelationship = this.addRelationship.bind(this);
this.setDirection = this.setDirection.bind(this);
this.addCssStyles = this.addCssStyles.bind(this);
this.addClass = this.addClass.bind(this);
this.setClass = this.setClass.bind(this);
this.setAccTitle = this.setAccTitle.bind(this);
this.setAccDescription = this.setAccDescription.bind(this);
}
/**
* Add entity
* @param name - The name of the entity
* @param alias - The alias of the entity
*/
public addEntity(name: string, alias = ''): EntityNode {
if (!this.entities.has(name)) {
this.entities.set(name, {
id: `entity-${name}-${this.entities.size}`,
label: name,
attributes: [],
alias,
shape: 'erBox',
look: getConfig().look ?? 'default',
cssClasses: 'default',
cssStyles: [],
});
log.info('Added new entity :', name);
} else if (!this.entities.get(name)?.alias && alias) {
this.entities.get(name)!.alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return this.entities.get(name)!;
}
public getEntity(name: string) {
return this.entities.get(name);
}
public getEntities() {
return this.entities;
}
public getClasses() {
return this.classes;
}
public addAttributes(entityName: string, attribs: Attribute[]) {
const entity = this.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--) {
if (!attribs[i].keys) {
attribs[i].keys = [];
}
if (!attribs[i].comment) {
attribs[i].comment = '';
}
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].name);
}
}
/**
* Add a relationship
*
* @param entA - The first entity in the relationship
* @param rolA - The role played by the first entity in relation to the second
* @param entB - The second entity in the relationship
* @param rSpec - The details of the relationship between the two entities
*/
public addRelationship(entA: string, rolA: string, entB: string, rSpec: RelSpec) {
const entityA = this.entities.get(entA);
const entityB = this.entities.get(entB);
if (!entityA || !entityB) {
return;
}
const rel = {
entityA: entityA.id,
roleA: rolA,
entityB: entityB.id,
relSpec: rSpec,
};
this.relationships.push(rel);
log.debug('Added new relationship :', rel);
}
public getRelationships() {
return this.relationships;
}
public getDirection() {
return this.direction;
}
public setDirection(dir: string) {
this.direction = dir;
}
private getCompiledStyles(classDefs: string[]) {
let compiledStyles: string[] = [];
for (const customClass of classDefs) {
const cssClass = this.classes.get(customClass);
if (cssClass?.styles) {
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
}
if (cssClass?.textStyles) {
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
}
}
return compiledStyles;
}
public addCssStyles(ids: string[], styles: string[]) {
for (const id of ids) {
const entity = this.entities.get(id);
if (!styles || !entity) {
return;
}
for (const style of styles) {
entity.cssStyles!.push(style);
}
}
}
public addClass(ids: string[], style: string[]) {
ids.forEach((id) => {
let classNode = this.classes.get(id);
if (classNode === undefined) {
classNode = { id, styles: [], textStyles: [] };
this.classes.set(id, classNode);
}
if (style) {
style.forEach(function (s) {
if (/color/.exec(s)) {
const newStyle = s.replace('fill', 'bgFill');
classNode.textStyles.push(newStyle);
}
classNode.styles.push(s);
});
}
});
}
public setClass(ids: string[], classNames: string[]) {
for (const id of ids) {
const entity = this.entities.get(id);
if (entity) {
for (const className of classNames) {
entity.cssClasses += ' ' + className;
}
}
}
}
public clear() {
this.entities = new Map();
this.classes = new Map();
this.relationships = [];
commonClear();
}
public getData() {
const nodes: Node[] = [];
const edges: Edge[] = [];
const config = getConfig();
for (const entityKey of this.entities.keys()) {
const entityNode = this.entities.get(entityKey);
if (entityNode) {
entityNode.cssCompiledStyles = this.getCompiledStyles(entityNode.cssClasses!.split(' '));
nodes.push(entityNode as unknown as Node);
}
}
let count = 0;
for (const relationship of this.relationships) {
const edge: Edge = {
id: getEdgeId(relationship.entityA, relationship.entityB, {
prefix: 'id',
counter: count++,
}),
type: 'normal',
curve: 'basis',
start: relationship.entityA,
end: relationship.entityB,
label: relationship.roleA,
labelpos: 'c',
thickness: 'normal',
classes: 'relationshipLine',
arrowTypeStart: relationship.relSpec.cardB.toLowerCase(),
arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(),
pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed',
look: config.look,
};
edges.push(edge);
}
return { nodes, edges, other: {}, config, direction: 'TB' };
}
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setAccDescription = setAccDescription;
public getAccDescription = getAccDescription;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getConfig = () => getConfig().er;
}

View File

@@ -1,12 +1,14 @@
// @ts-ignore: TODO: Fix ts errors
import erParser from './parser/erDiagram.jison';
import erDb from './erDb.js';
import erRenderer from './erRenderer.js';
import { ErDB } from './erDb.js';
import * as renderer from './erRenderer-unified.js';
import erStyles from './styles.js';
export const diagram = {
parser: erParser,
db: erDb,
renderer: erRenderer,
get db() {
return new ErDB();
},
renderer,
styles: erStyles,
};

View File

@@ -0,0 +1,66 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import type { LayoutData } from '../../rendering-util/types.js';
import utils from '../../utils.js';
import { select } from 'd3';
export const draw = async function (text: string, id: string, _version: string, diag: any) {
log.info('REF0:');
log.info('Drawing er diagram (unified)', id);
const { securityLevel, er: conf, layout } = getConfig();
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
// into the Layout data format
const data4Layout = diag.db.getData() as LayoutData;
// Create the root SVG - the element is the div containing the SVG element
const svg = getDiagramElement(id, securityLevel);
data4Layout.type = diag.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
// Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing?
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
data4Layout.direction = diag.db.getDirection();
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.
if (data4Layout.layoutAlgorithm === 'elk') {
svg.select('.edges').lower();
}
// Sets the background nodes to the same position as their original counterparts.
// Background nodes are created when the look is handDrawn so the ER diagram markers do not show underneath.
const backgroundNodes = svg.selectAll('[id*="-background"]');
// eslint-disable-next-line unicorn/prefer-spread
if (Array.from(backgroundNodes).length > 0) {
backgroundNodes.each(function (this: SVGElement) {
const backgroundNode = select(this);
const backgroundId = backgroundNode.attr('id');
const nonBackgroundId = backgroundId.replace('-background', '');
const nonBackgroundNode = svg.select(`#${CSS.escape(nonBackgroundId)}`);
if (!nonBackgroundNode.empty()) {
const transform = nonBackgroundNode.attr('transform');
backgroundNode.attr('transform', transform);
}
});
}
const padding = 8;
utils.insertTitle(
svg,
'erDiagramTitleText',
conf?.titleTopMargin ?? 25,
diag.db.getDiagramTitle()
);
setupViewPortForSVG(svg, padding, 'erDiagram', conf?.useMaxWidth ?? true);
};

View File

@@ -0,0 +1,37 @@
export interface EntityNode {
id: string;
label: string;
attributes: Attribute[];
alias: string;
shape: string;
look?: string;
cssClasses?: string;
cssStyles?: string[];
cssCompiledStyles?: string[];
}
export interface Attribute {
type: string;
name: string;
keys: ('PK' | 'FK' | 'UK')[];
comment: string;
}
export interface Relationship {
entityA: string;
roleA: string;
entityB: string;
relSpec: RelSpec;
}
export interface RelSpec {
cardA: string;
cardB: string;
relType: string;
}
export interface EntityClass {
id: string;
styles: string[];
textStyles: string[];
}

View File

@@ -5,6 +5,7 @@
%x acc_title
%x acc_descr
%x acc_descr_multiline
%x style
%%
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@@ -14,6 +15,10 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
[\n]+ return 'NEWLINE';
\s+ /* skip whitespace */
[\s]+ return 'SPACE';
@@ -21,11 +26,15 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
\"[^"]*\" return 'WORD';
"erDiagram" return 'ER_DIAGRAM';
"{" { this.begin("block"); return 'BLOCK_START'; }
<block>"," return 'COMMA';
\# return 'BRKT';
"#" return 'BRKT';
"," return 'COMMA';
":::" return 'STYLE_SEPARATOR';
":" return 'COLON';
<block>\s+ /* skip whitespace in block */
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
<block>[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
<block>([^\s]*)[~].*[~]([^\s]*) return 'ATTRIBUTE_WORD';
<block>([\*A-Za-z_\u00C0-\uFFFF][A-Za-z0-9\-\_\[\]\(\)\u00C0-\uFFFF\*]*) return 'ATTRIBUTE_WORD';
<block>\"[^"]*\" return 'COMMENT';
<block>[\n]+ /* nothing */
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
@@ -33,6 +42,14 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
"[" return 'SQS';
"]" return 'SQE';
"style" { this.begin("style"); return 'STYLE'; }
<style>[\n]+ { this.popState(); return 'NEWLINE'; }
<style>\s+ /* skip whitespace in block */
<style>":" return 'COLON';
<style>"," return 'COMMA';
<style>"#" return 'BRKT';
"classDef" { this.begin("style"); return 'CLASSDEF'; }
"class" return 'CLASS';
"one or zero" return 'ZERO_OR_ONE';
"one or more" return 'ONE_OR_MORE';
"one or many" return 'ONE_OR_MORE';
@@ -61,7 +78,10 @@ o\{ return 'ZERO_OR_MORE';
"optionally to" return 'NON_IDENTIFYING';
\.\- return 'NON_IDENTIFYING';
\-\. return 'NON_IDENTIFYING';
[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM';
<style>([^\x00-\x7F]|\w|\-|\*)+ return 'STYLE_TEXT';
<style>';' return 'SEMI';
([^\x00-\x7F]|\w|\-|\*)+ return 'UNICODE_TEXT';
[0-9] return 'NUM';
. return yytext[0];
<<EOF>> return 'EOF';
@@ -88,35 +108,126 @@ line
statement
: entityName relSpec entityName ':' role
: entityName relSpec entityName COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, $2);
}
| entityName STYLE_SEPARATOR idList relSpec 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 relSpec entityName COLON role
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $7, $5, $4);
yy.setClass([$1], $3);
}
| entityName relSpec entityName STYLE_SEPARATOR idList COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $7, $3, $2);
yy.setClass([$3], $5);
}
| entityName BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1);
yy.addAttributes($1, $3);
}
| entityName STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1);
yy.addAttributes($1, $5);
yy.setClass([$1], $3);
}
| entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); }
| entityName STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1); yy.setClass([$1], $3); }
| entityName { yy.addEntity($1); }
| entityName STYLE_SEPARATOR idList { yy.addEntity($1); yy.setClass([$1], $3); }
| entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1, $3);
yy.addAttributes($1, $6);
}
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1, $3);
yy.addAttributes($1, $8);
yy.setClass([$1], $6);
}
| entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); }
| 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); }
| 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($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
| direction
| classDefStatement
| classStatement
| styleStatement
;
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
classDefStatement
: CLASSDEF idList stylesOpt separator {$$ = $CLASSDEF;yy.addClass($idList,$stylesOpt);}
;
idList
: UNICODE_TEXT { $$ = [$UNICODE_TEXT]; }
| STYLE_TEXT { $$ = [$STYLE_TEXT]; }
| idList COMMA UNICODE_TEXT = { $$ = $idList.concat([$UNICODE_TEXT]); }
| idList COMMA STYLE_TEXT = { $$ = $idList.concat([$STYLE_TEXT]); }
;
classStatement
: CLASS idList idList {$$ = $CLASS;yy.setClass($2, $3);}
;
styleStatement
: STYLE idList stylesOpt separator {;$$ = $STYLE;yy.addCssStyles($2,$stylesOpt);}
;
stylesOpt
: style { $$ = [$style] }
| stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;}
;
style
: styleComponent
| style styleComponent { $$ = $style + $styleComponent; }
;
separator
: SEMI
| NEWLINE
| EOF
;
styleComponent: STYLE_TEXT | NUM | COLON | BRKT;
entityName
: 'ALPHANUM' { $$ = $1; }
| 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
: 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
| 'UNICODE_TEXT' { $$ = $1; }
;
attributes
@@ -125,10 +236,10 @@ attributes
;
attribute
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
| attributeType attributeName attributeKeyTypeList { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3 }; }
| attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3, attributeComment: $4 }; }
: attributeType attributeName { $$ = { type: $1, name: $2 }; }
| attributeType attributeName attributeKeyTypeList { $$ = { type: $1, name: $2, keys: $3 }; }
| attributeType attributeName attributeComment { $$ = { type: $1, name: $2, comment: $3 }; }
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { type: $1, name: $2, keys: $3, comment: $4 }; }
;
@@ -142,7 +253,7 @@ attributeName
attributeKeyTypeList
: attributeKeyType { $$ = [$1]; }
| attributeKeyTypeList COMMA attributeKeyType { $1.push($3); $$ = $1; }
| attributeKeyTypeList ',' attributeKeyType { $1.push($3); $$ = $1; }
;
attributeKeyType
@@ -177,7 +288,7 @@ relType
role
: 'WORD' { $$ = $1.replace(/"/g, ''); }
| 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
| 'ALPHANUM' { $$ = $1; }
| 'UNICODE_TEXT' { $$ = $1; }
;
%%

View File

@@ -1,5 +1,5 @@
import { setConfig } from '../../../config.js';
import erDb from '../erDb.js';
import { ErDB } from '../erDb.js';
import erDiagram from './erDiagram.jison'; // jison file
setConfig({
@@ -7,6 +7,7 @@ setConfig({
});
describe('when parsing ER diagram it...', function () {
const erDb = new ErDB();
beforeEach(function () {
erDiagram.parser.yy = erDb;
erDiagram.parser.yy.clear();
@@ -143,32 +144,32 @@ describe('when parsing ER diagram it...', function () {
expect(entities.get(entity).alias).toBe(alias);
});
it('can have an alias even if the relationship is defined before class', function () {
it('can have an alias even if the relationship is defined before buzz', function () {
const firstEntity = 'foo';
const secondEntity = 'bar';
const alias = 'batman';
erDiagram.parser.parse(
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n`
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nbuzz ${firstEntity}["${alias}"]\n`
);
const entities = erDb.getEntities();
expect(entities.has(firstEntity)).toBe(true);
expect(entities.has(secondEntity)).toBe(true);
expect(entities.get(firstEntity).alias).toBe(alias);
expect(entities.get(secondEntity).alias).toBeUndefined();
expect(entities.get(secondEntity).alias).toBe('');
});
it('can have an alias even if the relationship is defined after class', function () {
it('can have an alias even if the relationship is defined after buzz', function () {
const firstEntity = 'foo';
const secondEntity = 'bar';
const alias = 'batman';
erDiagram.parser.parse(
`erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
`erDiagram\nbuzz ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
);
const entities = erDb.getEntities();
expect(entities.has(firstEntity)).toBe(true);
expect(entities.has(secondEntity)).toBe(true);
expect(entities.get(firstEntity).alias).toBe(alias);
expect(entities.get(secondEntity).alias).toBeUndefined();
expect(entities.get(secondEntity).alias).toBe('');
});
it('can start with an underscore', function () {
@@ -193,9 +194,9 @@ describe('when parsing ER diagram it...', function () {
expect(entities.size).toBe(1);
expect(entities.get(entity).attributes.length).toBe(3);
expect(entities.get(entity).attributes[0].attributeName).toBe('myBookTitle');
expect(entities.get(entity).attributes[1].attributeName).toBe('MYBOOKSUBTITLE_1');
expect(entities.get(entity).attributes[2].attributeName).toBe('author-ref[name](1)');
expect(entities.get(entity).attributes[0].name).toBe('myBookTitle');
expect(entities.get(entity).attributes[1].name).toBe('MYBOOKSUBTITLE_1');
expect(entities.get(entity).attributes[2].name).toBe('author-ref[name](1)');
});
it('should allow asterisk at the start of attribute name', function () {
@@ -258,7 +259,7 @@ describe('when parsing ER diagram it...', function () {
const entities = erDb.getEntities();
expect(entities.size).toBe(1);
expect(entities.get(entity).attributes.length).toBe(1);
expect(entities.get(entity).attributes[0].attributeComment).toBe('comment');
expect(entities.get(entity).attributes[0].comment).toBe('comment');
});
it('should allow an entity with a single attribute to be defined with a key and a comment', function () {
@@ -297,14 +298,14 @@ describe('when parsing ER diagram it...', function () {
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n${attribute4}\n${attribute5}\n}`
);
const entities = erDb.getEntities();
expect(entities.get(entity).attributes[0].attributeKeyTypeList).toEqual(['PK', 'FK']);
expect(entities.get(entity).attributes[0].attributeComment).toBe('comment1');
expect(entities.get(entity).attributes[1].attributeKeyTypeList).toEqual(['PK', 'UK', 'FK']);
expect(entities.get(entity).attributes[2].attributeKeyTypeList).toEqual(['PK', 'UK']);
expect(entities.get(entity).attributes[2].attributeComment).toBe('comment3');
expect(entities.get(entity).attributes[3].attributeKeyTypeList).toBeUndefined();
expect(entities.get(entity).attributes[4].attributeKeyTypeList).toBeUndefined();
expect(entities.get(entity).attributes[4].attributeComment).toBe('comment5');
expect(entities.get(entity).attributes[0].keys).toEqual(['PK', 'FK']);
expect(entities.get(entity).attributes[0].comment).toBe('comment1');
expect(entities.get(entity).attributes[1].keys).toEqual(['PK', 'UK', 'FK']);
expect(entities.get(entity).attributes[2].keys).toEqual(['PK', 'UK']);
expect(entities.get(entity).attributes[2].comment).toBe('comment3');
expect(entities.get(entity).attributes[3].keys).toEqual([]);
expect(entities.get(entity).attributes[4].keys).toEqual([]);
expect(entities.get(entity).attributes[4].comment).toBe('comment5');
});
it('should allow an entity with attribute that has a generic type', function () {
@@ -341,8 +342,8 @@ describe('when parsing ER diagram it...', function () {
const entities = erDb.getEntities();
expect(entities.size).toBe(1);
expect(entities.get(entity).attributes.length).toBe(2);
expect(entities.get(entity).attributes[0].attributeType).toBe('character(10)');
expect(entities.get(entity).attributes[1].attributeType).toBe('varchar(5)');
expect(entities.get(entity).attributes[0].type).toBe('character(10)');
expect(entities.get(entity).attributes[1].type).toBe('varchar(5)');
});
it('should allow an entity with multiple attributes to be defined', function () {
@@ -764,6 +765,203 @@ describe('when parsing ER diagram it...', function () {
}).toThrowError();
});
it('should be possible to apply a style to an entity', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(`erDiagram
${entityName}
style ${entityName} color:red
`);
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red']);
});
it('should be possible to apply multiple styles to an entity at the same time', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram
${entityName}
style ${entityName} color:red,stroke:blue,fill:#f9f
`
);
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'stroke:blue', 'fill:#f9f']);
});
it('should be possible to apply multiple separately defined styles', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram
${entityName}
style ${entityName} color:red
style ${entityName} fill:#f9f
`
);
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'fill:#f9f']);
});
it('should be possible to assign a class to an entity', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(`erDiagram\n${entityName}\nclass ${entityName} myClass`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign multiple classes to an entity at the same time', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram\n${entityName}\nclass ${entityName} firstClass, secondClass, thirdClass`
);
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass thirdClass');
});
it('should be possible to assign multiple separately defined classes to an entity', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram\n${entityName}\nclass ${entityName} firstClass\nclass ${entityName} secondClass`
);
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass');
});
it('should be possible to configure the default class and have it apply to each entity', function () {
const firstEntity = 'ENTITY1';
const secondEntity = 'ENTITY2';
erDiagram.parser.parse(
`erDiagram
${firstEntity}
${secondEntity}
classDef default fill:#f9f
`
);
const expectedOutput = new Map([
[
'default',
{
id: 'default',
styles: ['fill:#f9f'],
textStyles: [],
},
],
]);
expect(erDb.getEntity(firstEntity).cssClasses).toBe('default');
expect(erDb.getEntity(secondEntity).cssClasses).toBe('default');
expect(erDb.getClasses()).toEqual(expectedOutput);
});
it('should be possible to define a class with styles', function () {
const className = 'myClass';
const styles = 'fill:#f9f, stroke: red, color: pink';
erDiagram.parser.parse(
`erDiagram
classDef ${className} ${styles}
`
);
const expectedOutput = new Map([
[
className,
{
id: className,
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
textStyles: ['color:pink'],
},
],
]);
expect(erDb.getClasses()).toEqual(expectedOutput);
});
it('should be possible to define multiple class with styles at the same time', function () {
const firstClass = 'firstClass';
const secondClass = 'secondClass';
const styles = 'fill:#f9f, stroke: red, color: pink';
erDiagram.parser.parse(
`erDiagram
classDef ${firstClass},${secondClass} ${styles}
`
);
const expectedOutput = new Map([
[
firstClass,
{
id: firstClass,
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
textStyles: ['color:pink'],
},
],
[
secondClass,
{
id: secondClass,
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
textStyles: ['color:pink'],
},
],
]);
expect(erDb.getClasses()).toEqual(expectedOutput);
});
it('should be possible to assign a class using the shorthand syntax just by itself', function () {
const entityName = 'CUSTOMER';
const className = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign a class using the shorthand syntax with empty block', function () {
const entityName = 'CUSTOMER';
const className = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign a class using the shorthand syntax with block of attributes', function () {
const entityName = 'CUSTOMER';
const className = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {\nstring name\n}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign multiple classes using the shorthand syntax', function () {
const entityName = 'CUSTOMER';
const firstClass = 'firstClass';
const secondClass = 'secondClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${firstClass},${secondClass}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass');
});
it('should be possible to assign classes using the shorthand syntax after defining an alias', function () {
const entityName = 'c';
const entityAlias = 'CUSTOMER';
const myClass = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}[${entityAlias}]:::${myClass}`);
expect(erDb.getEntity(entityName).alias).toBe(entityAlias);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign classes using the shorthand syntax while defining a relationship', function () {
const entityName = 'CUSTOMER';
const otherEntity = 'PERSON';
const myClass = 'myClass';
erDiagram.parser.parse(
`erDiagram\n${entityName}:::${myClass} ||--o{ ${otherEntity}:::${myClass} : allows`
);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
expect(erDb.getEntity(otherEntity).cssClasses).toBe('default myClass');
});
describe('relationship labels', function () {
it('should allow an empty quoted label', function () {
erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER : ""');

View File

@@ -1,49 +0,0 @@
const getStyles = (options) =>
`
.entityBox {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
}
.attributeBoxOdd {
fill: ${options.attributeBackgroundColorOdd};
stroke: ${options.nodeBorder};
}
.attributeBoxEven {
fill: ${options.attributeBackgroundColorEven};
stroke: ${options.nodeBorder};
}
.relationshipLabelBox {
fill: ${options.tertiaryColor};
opacity: 0.7;
background-color: ${options.tertiaryColor};
rect {
opacity: 0.5;
}
}
.relationshipLine {
stroke: ${options.lineColor};
}
.entityTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${options.textColor};
}
#MD_PARENT_START {
fill: #f5f5f5 !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
#MD_PARENT_END {
fill: #f5f5f5 !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
`;
export default getStyles;

View File

@@ -0,0 +1,73 @@
import * as khroma from 'khroma';
import type { FlowChartStyleOptions } from '../flowchart/styles.js';
const fade = (color: string, opacity: number) => {
// @ts-ignore TODO: incorrect types from khroma
const channel = khroma.channel;
const r = channel(color, 'r');
const g = channel(color, 'g');
const b = channel(color, 'b');
// @ts-ignore incorrect types from khroma
return khroma.rgba(r, g, b, opacity);
};
const getStyles = (options: FlowChartStyleOptions) =>
`
.entityBox {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
}
.relationshipLabelBox {
fill: ${options.tertiaryColor};
opacity: 0.7;
background-color: ${options.tertiaryColor};
rect {
opacity: 0.5;
}
}
.labelBkg {
background-color: ${fade(options.tertiaryColor, 0.5)};
}
.edgeLabel .label {
fill: ${options.nodeBorder};
font-size: 14px;
}
.label {
font-family: ${options.fontFamily};
color: ${options.nodeTextColor || options.textColor};
}
.edge-pattern-dashed {
stroke-dasharray: 8,8;
}
.node rect,
.node circle,
.node ellipse,
.node polygon
{
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.relationshipLine {
stroke: ${options.lineColor};
stroke-width: 1;
fill: none;
}
.marker {
fill: none !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
`;
export default getStyles;

View File

@@ -98,3 +98,31 @@ describe('flow db class', () => {
}
});
});
describe('flow db getData', () => {
let flowDb: FlowDB;
beforeEach(() => {
flowDb = new FlowDB();
});
it('should use defaultInterpolate for edges without specific interpolate', () => {
flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addLink(['A'], ['B'], {});
flowDb.updateLinkInterpolate(['default'], 'stepBefore');
const { edges } = flowDb.getData();
expect(edges[0].curve).toBe('stepBefore');
});
it('should prioritize edge-specific interpolate over defaultInterpolate', () => {
flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addLink(['A'], ['B'], {});
flowDb.updateLinkInterpolate(['default'], 'stepBefore');
flowDb.updateLinkInterpolate([0], 'basis');
const { edges } = flowDb.getData();
expect(edges[0].curve).toBe('basis');
});
});

View File

@@ -252,6 +252,7 @@ export class FlowDB implements DiagramDB {
labelType: 'text',
classes: [],
isUserDefinedId: false,
interpolate: this.edges.defaultInterpolate,
};
log.info('abc78 Got edge...', edge);
const linkTextObj = type.text;
@@ -1124,6 +1125,7 @@ You have to call mermaid.initialize.`
look: config.look,
animate: rawEdge.animate,
animation: rawEdge.animation,
curve: rawEdge.interpolate || this.edges.defaultInterpolate || config.flowchart?.curve,
};
edges.push(edge);

View File

@@ -0,0 +1,128 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { RadarDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import { cleanAndMerge } from '../../utils.js';
import {
clear as commonClear,
getAccDescription,
getAccTitle,
getDiagramTitle,
setAccDescription,
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type {
Axis,
Curve,
Option,
Entry,
} from '../../../../parser/dist/src/language/generated/ast.js';
import type { RadarAxis, RadarCurve, RadarOptions, RadarDB, RadarData } from './types.js';
const defaultOptions: RadarOptions = {
showLegend: true,
ticks: 5,
max: null,
min: 0,
graticule: 'circle',
};
const defaultRadarData: RadarData = {
axes: [],
curves: [],
options: defaultOptions,
};
let data: RadarData = structuredClone(defaultRadarData);
const DEFAULT_RADAR_CONFIG: Required<RadarDiagramConfig> = DEFAULT_CONFIG.radar;
const getConfig = (): Required<RadarDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_RADAR_CONFIG,
...commonGetConfig().radar,
});
return config;
};
const getAxes = (): RadarAxis[] => data.axes;
const getCurves = (): RadarCurve[] => data.curves;
const getOptions = (): RadarOptions => data.options;
const setAxes = (axes: Axis[]) => {
data.axes = axes.map((axis) => {
return {
name: axis.name,
label: axis.label ?? axis.name,
};
});
};
const setCurves = (curves: Curve[]) => {
data.curves = curves.map((curve) => {
return {
name: curve.name,
label: curve.label ?? curve.name,
entries: computeCurveEntries(curve.entries),
};
});
};
const computeCurveEntries = (entries: Entry[]): number[] => {
// If entries have axis reference, we must order them according to the axes
if (entries[0].axis == undefined) {
return entries.map((entry) => entry.value);
}
const axes = getAxes();
if (axes.length === 0) {
throw new Error('Axes must be populated before curves for reference entries');
}
return axes.map((axis) => {
const entry = entries.find((entry) => entry.axis?.$refText === axis.name);
if (entry === undefined) {
throw new Error('Missing entry for axis ' + axis.label);
}
return entry.value;
});
};
const setOptions = (options: Option[]) => {
// Create a map from option names to option objects for quick lookup
const optionMap = options.reduce(
(acc, option) => {
acc[option.name] = option;
return acc;
},
{} as Record<string, Option>
);
data.options = {
showLegend: (optionMap.showLegend?.value as boolean) ?? defaultOptions.showLegend,
ticks: (optionMap.ticks?.value as number) ?? defaultOptions.ticks,
max: (optionMap.max?.value as number) ?? defaultOptions.max,
min: (optionMap.min?.value as number) ?? defaultOptions.min,
graticule: (optionMap.graticule?.value as 'circle' | 'polygon') ?? defaultOptions.graticule,
};
};
const clear = () => {
commonClear();
data = structuredClone(defaultRadarData);
};
export const db: RadarDB = {
getAxes,
getCurves,
getOptions,
setAxes,
setCurves,
setOptions,
getConfig,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
};

View File

@@ -0,0 +1,22 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'radar';
const detector: DiagramDetector = (txt) => {
return /^\s*radar-beta/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./diagram.js');
return { id, diagram };
};
export const radar: ExternalDiagramDefinition = {
id,
detector,
loader,
};

View File

@@ -0,0 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { db } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import { styles } from './styles.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
styles,
};

View File

@@ -0,0 +1,23 @@
import type { Radar } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import { db } from './db.js';
const populate = (ast: Radar) => {
populateCommonDb(ast, db);
const { axes, curves, options } = ast;
// Here we can add specific logic between the AST and the DB
db.setAxes(axes);
db.setCurves(curves);
db.setOptions(options);
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Radar = await parse('radar', input);
log.debug(ast);
populate(ast);
},
};

View File

@@ -0,0 +1,256 @@
import { it, describe, expect } from 'vitest';
import { db } from './db.js';
import { parser } from './parser.js';
import { relativeRadius, closedRoundCurve } from './renderer.js';
import { Diagram } from '../../Diagram.js';
import mermaidAPI from '../../mermaidAPI.js';
const {
clear,
getDiagramTitle,
getAccTitle,
getAccDescription,
getAxes,
getCurves,
getOptions,
getConfig,
} = db;
describe('radar diagrams', () => {
beforeEach(() => {
clear();
});
it('should handle a simple radar definition', async () => {
const str = `radar-beta
axis A,B,C
curve mycurve{1,2,3}`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should handle diagram with data and title', async () => {
const str = `radar-beta
title Radar diagram
accTitle: Radar accTitle
accDescr: Radar accDescription
axis A["Axis A"], B["Axis B"] ,C["Axis C"]
curve mycurve["My Curve"]{1,2,3}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toMatchInlineSnapshot('"Radar diagram"');
expect(getAccTitle()).toMatchInlineSnapshot('"Radar accTitle"');
expect(getAccDescription()).toMatchInlineSnapshot('"Radar accDescription"');
expect(getAxes()).toMatchInlineSnapshot(`
[
{
"label": "Axis A",
"name": "A",
},
{
"label": "Axis B",
"name": "B",
},
{
"label": "Axis C",
"name": "C",
},
]
`);
expect(getCurves()).toMatchInlineSnapshot(`
[
{
"entries": [
1,
2,
3,
],
"label": "My Curve",
"name": "mycurve",
},
]
`);
expect(getOptions()).toMatchInlineSnapshot(`
{
"graticule": "circle",
"max": null,
"min": 0,
"showLegend": true,
"ticks": 5,
}
`);
});
it('should handle a radar diagram with options', async () => {
const str = `radar-beta
ticks 10
showLegend false
graticule polygon
min 1
max 10
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getOptions()).toMatchInlineSnapshot(`
{
"graticule": "polygon",
"max": 10,
"min": 1,
"showLegend": false,
"ticks": 10,
}
`);
});
it('should handle curve with detailed data in any order', async () => {
const str = `radar-beta
axis A,B,C
curve mycurve{ C: 3, A: 1, B: 2 }`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getCurves()).toMatchInlineSnapshot(`
[
{
"entries": [
1,
2,
3,
],
"label": "mycurve",
"name": "mycurve",
},
]
`);
});
it('should handle radar diagram with comments', async () => {
const str = `radar-beta
%% This is a comment
axis A,B,C
%% This is another comment
curve mycurve{1,2,3}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should handle radar diagram with config override', async () => {
const str = `
%%{init: {'radar': {'marginTop': 80, 'axisLabelFactor': 1.25}}}%%
radar-beta
axis A,B,C
curve mycurve{1,2,3}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should parse radar diagram with theme override', async () => {
const str = `
%%{init: { "theme": "base", "themeVariables": {'fontSize': 80, 'cScale0': '#123456' }}}%%
radar-beta:
axis A,B,C
curve mycurve{1,2,3}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should handle radar diagram with radar style override', async () => {
const str = `
%%{init: { "theme": "base", "themeVariables": {'fontSize': 10, 'radar': { 'axisColor': '#FF0000' }}}}%%
radar-beta
axis A,B,C
curve mycurve{1,2,3}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
});
describe('renderer', () => {
describe('relativeRadius', () => {
it('should calculate relative radius', () => {
expect(relativeRadius(5, 0, 10, 100)).toBe(50);
});
it('should handle min value', () => {
expect(relativeRadius(0, 0, 10, 100)).toBe(0);
});
it('should handle max value', () => {
expect(relativeRadius(10, 0, 10, 100)).toBe(100);
});
it('should clip values below min', () => {
expect(relativeRadius(-5, 0, 10, 100)).toBe(0);
});
it('should clip values above max', () => {
expect(relativeRadius(15, 0, 10, 100)).toBe(100);
});
it('should handle negative min', () => {
expect(relativeRadius(5, -10, 10, 100)).toBe(75);
});
});
describe('closedRoundCurve', () => {
it('should construct a polygon if tension is 0', () => {
const points = [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 0, y: 100 },
];
const tension = 0;
const path = closedRoundCurve(points, tension);
expect(path).toMatchInlineSnapshot(
`"M0,0 C0,0 100,0 100,0 C100,0 100,100 100,100 C100,100 0,100 0,100 C0,100 0,0 0,0 Z"`
);
});
it('should construct a simple round curve', () => {
const points = [
{ x: 0, y: 0 },
{ x: 100, y: 100 },
];
const tension = 0.5;
const path = closedRoundCurve(points, tension);
expect(path).toMatchInlineSnapshot(`"M0,0 C0,0 100,100 100,100 C100,100 0,0 0,0 Z"`);
});
it('should construct a closed round curve', () => {
const points = [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 0, y: 100 },
];
const tension = 0.5;
const path = closedRoundCurve(points, tension);
expect(path).toMatchInlineSnapshot(
`"M0,0 C50,-50 50,-50 100,0 C150,50 150,50 100,100 C50,150 50,150 0,100 C-50,50 -50,50 0,0 Z"`
);
});
});
describe('draw', () => {
it('should draw a simple radar diagram', async () => {
const str = `radar-beta
axis A,B,C
curve mycurve{1,2,3}`;
await mermaidAPI.parse(str);
const diagram = await Diagram.fromText(str);
await diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
});
it('should draw a complex radar diagram', async () => {
const str = `radar-beta
title Radar diagram
accTitle: Radar accTitle
accDescr: Radar accDescription
axis A["Axis A"], B["Axis B"] ,C["Axis C"]
curve mycurve["My Curve"]{1,2,3}
curve mycurve2["My Curve 2"]{ C: 1, A: 2, B: 3 }
graticule polygon
`;
await mermaidAPI.parse(str);
const diagram = await Diagram.fromText(str);
await diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
});
});
});
});

View File

@@ -0,0 +1,232 @@
import type { Diagram } from '../../Diagram.js';
import type { RadarDiagramConfig } from '../../config.type.js';
import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import type { RadarDB, RadarAxis, RadarCurve } from './types.js';
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
const db = diagram.db as RadarDB;
const axes = db.getAxes();
const curves = db.getCurves();
const options = db.getOptions();
const config = db.getConfig();
const title = db.getDiagramTitle();
const svg: SVG = selectSvgElement(id);
// 🖼️ Draw the main frame
const g = drawFrame(svg, config);
// The maximum value for the radar chart is the 'max' option if it exists,
// otherwise it is the maximum value of the curves
const maxValue: number =
options.max ?? Math.max(...curves.map((curve) => Math.max(...curve.entries)));
const minValue: number = options.min;
const radius = Math.min(config.width, config.height) / 2;
// 🕸️ Draw graticule
drawGraticule(g, axes, radius, options.ticks, options.graticule);
// 🪓 Draw the axes
drawAxes(g, axes, radius, config);
// 📊 Draw the curves
drawCurves(g, axes, curves, minValue, maxValue, options.graticule, config);
// 🏷 Draw Legend
drawLegend(g, curves, options.showLegend, config);
// 🏷 Draw Title
g.append('text')
.attr('class', 'radarTitle')
.text(title)
.attr('x', 0)
.attr('y', -config.height / 2 - config.marginTop);
};
// Returns a g element to center the radar chart
// it is of type SVGElement
const drawFrame = (svg: SVG, config: Required<RadarDiagramConfig>): SVGGroup => {
const totalWidth = config.width + config.marginLeft + config.marginRight;
const totalHeight = config.height + config.marginTop + config.marginBottom;
const center = {
x: config.marginLeft + config.width / 2,
y: config.marginTop + config.height / 2,
};
// Initialize the SVG
svg
.attr('viewbox', `0 0 ${totalWidth} ${totalHeight}`)
.attr('width', totalWidth)
.attr('height', totalHeight);
// g element to center the radar chart
return svg.append('g').attr('transform', `translate(${center.x}, ${center.y})`);
};
const drawGraticule = (
g: SVGGroup,
axes: RadarAxis[],
radius: number,
ticks: number,
graticule: string
) => {
if (graticule === 'circle') {
// Draw a circle for each tick
for (let i = 0; i < ticks; i++) {
const r = (radius * (i + 1)) / ticks;
g.append('circle').attr('r', r).attr('class', 'radarGraticule');
}
} else if (graticule === 'polygon') {
// Draw a polygon
const numAxes = axes.length;
for (let i = 0; i < ticks; i++) {
const r = (radius * (i + 1)) / ticks;
const points = axes
.map((_, j) => {
const angle = (2 * j * Math.PI) / numAxes - Math.PI / 2;
const x = r * Math.cos(angle);
const y = r * Math.sin(angle);
return `${x},${y}`;
})
.join(' ');
g.append('polygon').attr('points', points).attr('class', 'radarGraticule');
}
}
};
const drawAxes = (
g: SVGGroup,
axes: RadarAxis[],
radius: number,
config: Required<RadarDiagramConfig>
) => {
const numAxes = axes.length;
for (let i = 0; i < numAxes; i++) {
const label = axes[i].label;
const angle = (2 * i * Math.PI) / numAxes - Math.PI / 2;
g.append('line')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', radius * config.axisScaleFactor * Math.cos(angle))
.attr('y2', radius * config.axisScaleFactor * Math.sin(angle))
.attr('class', 'radarAxisLine');
g.append('text')
.text(label)
.attr('x', radius * config.axisLabelFactor * Math.cos(angle))
.attr('y', radius * config.axisLabelFactor * Math.sin(angle))
.attr('class', 'radarAxisLabel');
}
};
function drawCurves(
g: SVGGroup,
axes: RadarAxis[],
curves: RadarCurve[],
minValue: number,
maxValue: number,
graticule: string,
config: Required<RadarDiagramConfig>
) {
const numAxes = axes.length;
const radius = Math.min(config.width, config.height) / 2;
curves.forEach((curve, index) => {
if (curve.entries.length !== numAxes) {
// Skip curves that do not have an entry for each axis.
return;
}
// Compute points for the curve.
const points = curve.entries.map((entry, i) => {
const angle = (2 * Math.PI * i) / numAxes - Math.PI / 2;
const r = relativeRadius(entry, minValue, maxValue, radius);
const x = r * Math.cos(angle);
const y = r * Math.sin(angle);
return { x, y };
});
if (graticule === 'circle') {
// Draw a closed curve through the points.
g.append('path')
.attr('d', closedRoundCurve(points, config.curveTension))
.attr('class', `radarCurve-${index}`);
} else if (graticule === 'polygon') {
// Draw a polygon for each curve.
g.append('polygon')
.attr('points', points.map((p) => `${p.x},${p.y}`).join(' '))
.attr('class', `radarCurve-${index}`);
}
});
}
export function relativeRadius(
value: number,
minValue: number,
maxValue: number,
radius: number
): number {
const clippedValue = Math.min(Math.max(value, minValue), maxValue);
return (radius * (clippedValue - minValue)) / (maxValue - minValue);
}
export function closedRoundCurve(points: { x: number; y: number }[], tension: number): string {
// Catmull-Rom spline helper function
const numPoints = points.length;
let d = `M${points[0].x},${points[0].y}`;
// For each segment from point i to point (i+1) mod n, compute control points.
for (let i = 0; i < numPoints; i++) {
const p0 = points[(i - 1 + numPoints) % numPoints];
const p1 = points[i];
const p2 = points[(i + 1) % numPoints];
const p3 = points[(i + 2) % numPoints];
// Calculate the control points for the cubic Bezier segment
const cp1 = {
x: p1.x + (p2.x - p0.x) * tension,
y: p1.y + (p2.y - p0.y) * tension,
};
const cp2 = {
x: p2.x - (p3.x - p1.x) * tension,
y: p2.y - (p3.y - p1.y) * tension,
};
d += ` C${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${p2.x},${p2.y}`;
}
return `${d} Z`;
}
function drawLegend(
g: SVGGroup,
curves: RadarCurve[],
showLegend: boolean,
config: Required<RadarDiagramConfig>
) {
if (!showLegend) {
return;
}
// Create a legend group and position it in the top-right corner of the chart.
const legendX = ((config.width / 2 + config.marginRight) * 3) / 4;
const legendY = (-(config.height / 2 + config.marginTop) * 3) / 4;
const lineHeight = 20;
curves.forEach((curve, index) => {
const itemGroup = g
.append('g')
.attr('transform', `translate(${legendX}, ${legendY + index * lineHeight})`);
// Draw a square marker for this curve.
itemGroup
.append('rect')
.attr('width', 12)
.attr('height', 12)
.attr('class', `radarLegendBox-${index}`);
// Draw the label text next to the marker.
itemGroup
.append('text')
.attr('x', 16)
.attr('y', 0)
.attr('class', 'radarLegendText')
.text(curve.label);
});
}
export const renderer: DiagramRenderer = { draw };

View File

@@ -0,0 +1,77 @@
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import { cleanAndMerge } from '../../utils.js';
import type { RadarStyleOptions } from './types.js';
import { getThemeVariables } from '../../themes/theme-default.js';
import { getConfig as getConfigAPI } from '../../config.js';
const genIndexStyles = (
themeVariables: ReturnType<typeof getThemeVariables>,
radarOptions: RadarStyleOptions
) => {
let sections = '';
for (let i = 0; i < themeVariables.THEME_COLOR_LIMIT; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const indexColor = (themeVariables as any)[`cScale${i}`];
sections += `
.radarCurve-${i} {
color: ${indexColor};
fill: ${indexColor};
fill-opacity: ${radarOptions.curveOpacity};
stroke: ${indexColor};
stroke-width: ${radarOptions.curveStrokeWidth};
}
.radarLegendBox-${i} {
fill: ${indexColor};
fill-opacity: ${radarOptions.curveOpacity};
stroke: ${indexColor};
}
`;
}
return sections;
};
export const buildRadarStyleOptions = (radar?: RadarStyleOptions) => {
const defaultThemeVariables = getThemeVariables();
const currentConfig = getConfigAPI();
const themeVariables = cleanAndMerge(defaultThemeVariables, currentConfig.themeVariables);
const radarOptions: RadarStyleOptions = cleanAndMerge(themeVariables.radar, radar);
return { themeVariables, radarOptions };
};
export const styles: DiagramStylesProvider = ({ radar }: { radar?: RadarStyleOptions } = {}) => {
const { themeVariables, radarOptions } = buildRadarStyleOptions(radar);
return `
.radarTitle {
font-size: ${themeVariables.fontSize};
color: ${themeVariables.titleColor};
dominant-baseline: hanging;
text-anchor: middle;
}
.radarAxisLine {
stroke: ${radarOptions.axisColor};
stroke-width: ${radarOptions.axisStrokeWidth};
}
.radarAxisLabel {
dominant-baseline: middle;
text-anchor: middle;
font-size: ${radarOptions.axisLabelFontSize}px;
color: ${radarOptions.axisColor};
}
.radarGraticule {
fill: ${radarOptions.graticuleColor};
fill-opacity: ${radarOptions.graticuleOpacity};
stroke: ${radarOptions.graticuleColor};
stroke-width: ${radarOptions.graticuleStrokeWidth};
}
.radarLegendText {
text-anchor: start;
font-size: ${radarOptions.legendFontSize}px;
dominant-baseline: hanging;
}
${genIndexStyles(themeVariables, radarOptions)}
`;
};
export default styles;

View File

@@ -0,0 +1,47 @@
import type { Axis, Curve, Option } from '../../../../parser/dist/src/language/generated/ast.js';
import type { RadarDiagramConfig } from '../../config.type.js';
import type { DiagramDBBase } from '../../diagram-api/types.js';
export interface RadarAxis {
name: string;
label: string;
}
export interface RadarCurve {
name: string;
entries: number[];
label: string;
}
export interface RadarOptions {
showLegend: boolean;
ticks: number;
max: number | null;
min: number;
graticule: 'circle' | 'polygon';
}
export interface RadarDB extends DiagramDBBase<RadarDiagramConfig> {
getAxes: () => RadarAxis[];
getCurves: () => RadarCurve[];
getOptions: () => RadarOptions;
setAxes: (axes: Axis[]) => void;
setCurves: (curves: Curve[]) => void;
setOptions: (options: Option[]) => void;
}
export interface RadarStyleOptions {
axisColor?: string;
axisStrokeWidth?: number;
axisLabelFontSize?: number;
curveOpacity?: number;
curveStrokeWidth?: number;
graticuleColor?: string;
graticuleOpacity?: number;
graticuleStrokeWidth?: number;
legendBoxSize?: number;
legendFontSize?: number;
}
export interface RadarData {
axes: RadarAxis[];
curves: RadarCurve[];
options: RadarOptions;
}

View File

@@ -328,7 +328,8 @@ export class RequirementDB implements DiagramDB {
thickness: 'normal',
type: 'normal',
pattern: isContains ? 'normal' : 'dashed',
arrowTypeEnd: isContains ? 'requirement_contains' : 'requirement_arrow',
arrowTypeStart: isContains ? 'requirement_contains' : '',
arrowTypeEnd: isContains ? '' : 'requirement_arrow',
look: config.look,
};

View File

@@ -1679,7 +1679,6 @@ Alice->Bob: Hello Bob, how are you?`;
const diagram = await Diagram.fromText(str);
await diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
const { bounds, models } = diagram.renderer.bounds.getBounds();
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);

View File

@@ -1,3 +1,4 @@
import type { MermaidConfig } from '../../config.type.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import common from '../common/common.js';
@@ -33,9 +34,10 @@ import {
STMT_RELATION,
STMT_STATE,
} from './stateCommon.js';
import type { Edge, NodeData, StateStmt, Stmt, StyleClass } from './stateDb.js';
// List of nodes created from the parsed diagram statement items
let nodeDb = new Map();
const nodeDb = new Map<string, NodeData>();
let graphItemCount = 0; // used to construct ids, etc.
@@ -43,18 +45,27 @@ let graphItemCount = 0; // used to construct ids, etc.
* Create a standard string for the dom ID of an item.
* If a type is given, insert that before the counter, preceded by the type spacer
*
* @param itemId
* @param counter
* @param {string | null} type
* @param typeSpacer
* @returns {string}
*/
export function stateDomId(itemId = '', counter = 0, type = '', typeSpacer = DOMID_TYPE_SPACER) {
export function stateDomId(
itemId = '',
counter = 0,
type: string | null = '',
typeSpacer = DOMID_TYPE_SPACER
) {
const typeStr = type !== null && type.length > 0 ? `${typeSpacer}${type}` : '';
return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`;
}
const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, look, classes) => {
const setupDoc = (
parentParsedItem: StateStmt | undefined,
doc: Stmt[],
diagramStates: Map<string, StateStmt>,
nodes: NodeData[],
edges: Edge[],
altFlag: boolean,
look: MermaidConfig['look'],
classes: Map<string, StyleClass>
) => {
// graphItemCount = 0;
log.trace('items', doc);
doc.forEach((item) => {
@@ -95,7 +106,7 @@ const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, l
arrowTypeEnd: 'arrow_barb',
style: G_EDGE_STYLE,
labelStyle: '',
label: common.sanitizeText(item.description, getConfig()),
label: common.sanitizeText(item.description ?? '', getConfig()),
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
labelpos: G_EDGE_LABELPOS,
labelType: G_EDGE_LABELTYPE,
@@ -115,11 +126,10 @@ const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, l
* Get the direction from the statement items.
* Look through all of the documents (docs) in the parsedItems
* Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
* @param {object[]} parsedItem - the parsed statement item to look through
* @param [defaultDir] - the direction to use if none is found
* @returns {string}
* @param parsedItem - the parsed statement item to look through
* @param defaultDir - the direction to use if none is found
*/
const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
const getDir = (parsedItem: { doc?: Stmt[] }, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
let dir = defaultDir;
if (parsedItem.doc) {
for (const parsedItemDoc of parsedItem.doc) {
@@ -131,7 +141,11 @@ const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
return dir;
};
function insertOrUpdateNode(nodes, nodeData, classes) {
function insertOrUpdateNode(
nodes: NodeData[],
nodeData: NodeData,
classes: Map<string, StyleClass>
) {
if (!nodeData.id || nodeData.id === '</join></fork>' || nodeData.id === '</choice>') {
return;
}
@@ -143,9 +157,9 @@ function insertOrUpdateNode(nodes, nodeData, classes) {
}
nodeData.cssClasses.split(' ').forEach((cssClass) => {
if (classes.get(cssClass)) {
const classDef = classes.get(cssClass);
nodeData.cssCompiledStyles = [...nodeData.cssCompiledStyles, ...classDef.styles];
if (classDef) {
nodeData.cssCompiledStyles = [...(nodeData.cssCompiledStyles ?? []), ...classDef.styles];
}
});
}
@@ -162,31 +176,30 @@ function insertOrUpdateNode(nodes, nodeData, classes) {
* If there aren't any or if dbInfoItem isn't defined, return an empty string.
* Else create 1 string from the list of classes found
*
* @param {undefined | null | object} dbInfoItem
* @returns {string}
*/
function getClassesFromDbInfo(dbInfoItem) {
function getClassesFromDbInfo(dbInfoItem?: StateStmt): string {
return dbInfoItem?.classes?.join(' ') ?? '';
}
function getStylesFromDbInfo(dbInfoItem) {
function getStylesFromDbInfo(dbInfoItem?: StateStmt): string[] {
return dbInfoItem?.styles ?? [];
}
export const dataFetcher = (
parent,
parsedItem,
diagramStates,
nodes,
edges,
altFlag,
look,
classes
parent: StateStmt | undefined,
parsedItem: StateStmt,
diagramStates: Map<string, StateStmt>,
nodes: NodeData[],
edges: Edge[],
altFlag: boolean,
look: MermaidConfig['look'],
classes: Map<string, StyleClass>
) => {
const itemId = parsedItem.id;
const dbState = diagramStates.get(itemId);
const classStr = getClassesFromDbInfo(dbState);
const style = getStylesFromDbInfo(dbState);
const config = getConfig();
log.info('dataFetcher parsedItem', parsedItem, dbState, style);
@@ -207,13 +220,13 @@ export const dataFetcher = (
nodeDb.set(itemId, {
id: itemId,
shape,
description: common.sanitizeText(itemId, getConfig()),
description: common.sanitizeText(itemId, config),
cssClasses: `${classStr} ${CSS_DIAGRAM_STATE}`,
cssStyles: style,
});
}
const newNode = nodeDb.get(itemId);
const newNode = nodeDb.get(itemId)!;
// Save data for description and group so that for instance a statement without description overwrites
// one with description @todo TODO What does this mean? If important, add a test for it
@@ -225,7 +238,7 @@ export const dataFetcher = (
newNode.shape = SHAPE_STATE_WITH_DESC;
newNode.description.push(parsedItem.description);
} else {
if (newNode.description?.length > 0) {
if (newNode.description?.length && newNode.description.length > 0) {
// if there is a description already transform it to an array
newNode.shape = SHAPE_STATE_WITH_DESC;
if (newNode.description === itemId) {
@@ -239,7 +252,7 @@ export const dataFetcher = (
newNode.description = parsedItem.description;
}
}
newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig());
newNode.description = common.sanitizeTextOrArray(newNode.description, config);
}
// If there's only 1 description entry, just use a regular state shape
@@ -262,7 +275,7 @@ export const dataFetcher = (
}
// This is what will be added to the graph
const nodeData = {
const nodeData: NodeData = {
labelStyle: '',
shape: newNode.shape,
label: newNode.description,
@@ -294,19 +307,19 @@ export const dataFetcher = (
if (parsedItem.note) {
// Todo: set random id
const noteData = {
const noteData: NodeData = {
labelStyle: '',
shape: SHAPE_NOTE,
label: parsedItem.note.text,
cssClasses: CSS_DIAGRAM_NOTE,
// useHtmlLabels: false,
cssStyles: [],
cssCompilesStyles: [],
cssCompiledStyles: [],
id: itemId + NOTE_ID + '-' + graphItemCount,
domId: stateDomId(itemId, graphItemCount, NOTE),
type: newNode.type,
isGroup: newNode.type === 'group',
padding: getConfig().flowchart.padding,
padding: config.flowchart?.padding,
look,
position: parsedItem.note.position,
};

View File

@@ -1,16 +0,0 @@
const idCache = {};
export const set = (key, val) => {
idCache[key] = val;
};
export const get = (k) => idCache[k];
export const keys = () => Object.keys(idCache);
export const size = () => keys().length;
export default {
get,
set,
keys,
size,
};

View File

@@ -1,5 +1,4 @@
import { line, curveBasis } from 'd3';
import idCache from './id-cache.js';
import { StateDB } from './stateDb.js';
import utils from '../../utils.js';
import common from '../common/common.js';
@@ -405,8 +404,6 @@ export const drawState = function (elem, stateDef) {
stateInfo.width = stateBox.width + 2 * getConfig().state.padding;
stateInfo.height = stateBox.height + 2 * getConfig().state.padding;
idCache.set(id, stateInfo);
// stateCnt++;
return stateInfo;
};

View File

@@ -13,6 +13,10 @@ export const STMT_DIRECTION = 'dir';
// parsed statement type for a state
export const STMT_STATE = 'state';
// parsed statement type for a root
export const STMT_ROOT = 'root';
// parsed statement type for a relation
export const STMT_RELATION = 'relation';
// parsed statement type for a classDef

View File

@@ -1,706 +0,0 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { generateId } from '../../utils.js';
import common from '../common/common.js';
import {
clear as commonClear,
getAccDescription,
getAccTitle,
getDiagramTitle,
setAccDescription,
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js';
import { getDir } from './stateRenderer-v3-unified.js';
import {
DEFAULT_DIAGRAM_DIRECTION,
DEFAULT_STATE_TYPE,
DIVIDER_TYPE,
STMT_APPLYCLASS,
STMT_CLASSDEF,
STMT_DIRECTION,
STMT_RELATION,
STMT_STATE,
STMT_STYLEDEF,
} from './stateCommon.js';
const START_NODE = '[*]';
const START_TYPE = 'start';
const END_NODE = START_NODE;
const END_TYPE = 'end';
const COLOR_KEYWORD = 'color';
const FILL_KEYWORD = 'fill';
const BG_FILL = 'bgFill';
const STYLECLASS_SEP = ',';
/**
* Returns a new list of classes.
* In the future, this can be replaced with a class common to all diagrams.
* ClassDef information = { id: id, styles: [], textStyles: [] }
*
* @returns {Map<string, any>}
*/
function newClassesList() {
return new Map();
}
const newDoc = () => {
return {
/** @type {{ id1: string, id2: string, relationTitle: string }[]} */
relations: [],
states: new Map(),
documents: {},
};
};
const clone = (o) => JSON.parse(JSON.stringify(o));
export class StateDB {
/**
* @param {1 | 2} version - v1 renderer or v2 renderer.
*/
constructor(version) {
this.clear();
this.version = version;
// Needed for JISON since it only supports direct properties
this.setRootDoc = this.setRootDoc.bind(this);
this.getDividerId = this.getDividerId.bind(this);
this.setDirection = this.setDirection.bind(this);
this.trimColon = this.trimColon.bind(this);
}
/**
* @private
* @type {1 | 2}
*/
version;
/**
* @private
* @type {Array}
*/
nodes = [];
/**
* @private
* @type {Array}
*/
edges = [];
/**
* @private
* @type {Array}
*/
rootDoc = [];
/**
* @private
* @type {Map<string, any>}
*/
classes = newClassesList(); // style classes defined by a classDef
/**
* @private
* @type {Object}
*/
documents = {
root: newDoc(),
};
/**
* @private
* @type {Object}
*/
currentDocument = this.documents.root;
/**
* @private
* @type {number}
*/
startEndCount = 0;
/**
* @private
* @type {number}
*/
dividerCnt = 0;
static relationType = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3,
};
setRootDoc(o) {
log.info('Setting root doc', o);
// rootDoc = { id: 'root', doc: o };
this.rootDoc = o;
if (this.version === 1) {
this.extract(o);
} else {
this.extract(this.getRootDocV2());
}
}
getRootDoc() {
return this.rootDoc;
}
/**
* @private
* @param {Object} parent
* @param {Object} node
* @param {boolean} first
*/
docTranslator(parent, node, first) {
if (node.stmt === STMT_RELATION) {
this.docTranslator(parent, node.state1, true);
this.docTranslator(parent, node.state2, false);
} else {
if (node.stmt === STMT_STATE) {
if (node.id === '[*]') {
node.id = first ? parent.id + '_start' : parent.id + '_end';
node.start = first;
} else {
// This is just a plain state, not a start or end
node.id = node.id.trim();
}
}
if (node.doc) {
const doc = [];
// Check for concurrency
let currentDoc = [];
let i;
for (i = 0; i < node.doc.length; i++) {
if (node.doc[i].type === DIVIDER_TYPE) {
const newNode = clone(node.doc[i]);
newNode.doc = clone(currentDoc);
doc.push(newNode);
currentDoc = [];
} else {
currentDoc.push(node.doc[i]);
}
}
// If any divider was encountered
if (doc.length > 0 && currentDoc.length > 0) {
const newNode = {
stmt: STMT_STATE,
id: generateId(),
type: 'divider',
doc: clone(currentDoc),
};
doc.push(clone(newNode));
node.doc = doc;
}
node.doc.forEach((docNode) => this.docTranslator(node, docNode, true));
}
}
}
/**
* @private
*/
getRootDocV2() {
this.docTranslator({ id: 'root' }, { id: 'root', doc: this.rootDoc }, true);
return { id: 'root', doc: this.rootDoc };
// Here
}
/**
* Convert all of the statements (stmts) that were parsed into states and relationships.
* This is done because a state diagram may have nested sections,
* where each section is a 'document' and has its own set of statements.
* Ex: the section within a fork has its own statements, and incoming and outgoing statements
* refer to the fork as a whole (document).
* See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement.
* This will push the statement into the list of statements for the current document.
* @private
* @param _doc
*/
extract(_doc) {
// const res = { states: [], relations: [] };
let doc;
if (_doc.doc) {
doc = _doc.doc;
} else {
doc = _doc;
}
// let doc = root.doc;
// if (!doc) {
// doc = root;
// }
log.info(doc);
this.clear(true);
log.info('Extract initial document:', doc);
doc.forEach((item) => {
log.warn('Statement', item.stmt);
switch (item.stmt) {
case STMT_STATE:
this.addState(
item.id.trim(),
item.type,
item.doc,
item.description,
item.note,
item.classes,
item.styles,
item.textStyles
);
break;
case STMT_RELATION:
this.addRelation(item.state1, item.state2, item.description);
break;
case STMT_CLASSDEF:
this.addStyleClass(item.id.trim(), item.classes);
break;
case STMT_STYLEDEF:
{
const ids = item.id.trim().split(',');
const styles = item.styleClass.split(',');
ids.forEach((id) => {
let foundState = this.getState(id);
if (foundState === undefined) {
const trimmedId = id.trim();
this.addState(trimmedId);
foundState = this.getState(trimmedId);
}
foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim());
});
}
break;
case STMT_APPLYCLASS:
this.setCssClass(item.id.trim(), item.styleClass);
break;
}
});
const diagramStates = this.getStates();
const config = getConfig();
const look = config.look;
resetDataFetching();
dataFetcher(
undefined,
this.getRootDocV2(),
diagramStates,
this.nodes,
this.edges,
true,
look,
this.classes
);
this.nodes.forEach((node) => {
if (Array.isArray(node.label)) {
// add the rest as description
node.description = node.label.slice(1);
if (node.isGroup && node.description.length > 0) {
throw new Error(
'Group nodes can only have label. Remove the additional description for node [' +
node.id +
']'
);
}
// add first description as label
node.label = node.label[0];
}
});
}
/**
* Function called by parser when a node definition has been found.
*
* @param {null | string} id
* @param {null | string} type
* @param {null | string} doc
* @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings
* @param {null | string} note
* @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class.
* @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style.
* @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style.
*/
addState(
id,
type = DEFAULT_STATE_TYPE,
doc = null,
descr = null,
note = null,
classes = null,
styles = null,
textStyles = null
) {
const trimmedId = id?.trim();
// add the state if needed
if (!this.currentDocument.states.has(trimmedId)) {
log.info('Adding state ', trimmedId, descr);
this.currentDocument.states.set(trimmedId, {
id: trimmedId,
descriptions: [],
type,
doc,
note,
classes: [],
styles: [],
textStyles: [],
});
} else {
if (!this.currentDocument.states.get(trimmedId).doc) {
this.currentDocument.states.get(trimmedId).doc = doc;
}
if (!this.currentDocument.states.get(trimmedId).type) {
this.currentDocument.states.get(trimmedId).type = type;
}
}
if (descr) {
log.info('Setting state description', trimmedId, descr);
if (typeof descr === 'string') {
this.addDescription(trimmedId, descr.trim());
}
if (typeof descr === 'object') {
descr.forEach((des) => this.addDescription(trimmedId, des.trim()));
}
}
if (note) {
const doc2 = this.currentDocument.states.get(trimmedId);
doc2.note = note;
doc2.note.text = common.sanitizeText(doc2.note.text, getConfig());
}
if (classes) {
log.info('Setting state classes', trimmedId, classes);
const classesList = typeof classes === 'string' ? [classes] : classes;
classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim()));
}
if (styles) {
log.info('Setting state styles', trimmedId, styles);
const stylesList = typeof styles === 'string' ? [styles] : styles;
stylesList.forEach((style) => this.setStyle(trimmedId, style.trim()));
}
if (textStyles) {
log.info('Setting state styles', trimmedId, styles);
const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles;
textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim()));
}
}
clear(saveCommon) {
this.nodes = [];
this.edges = [];
this.documents = {
root: newDoc(),
};
this.currentDocument = this.documents.root;
// number of start and end nodes; used to construct ids
this.startEndCount = 0;
this.classes = newClassesList();
if (!saveCommon) {
commonClear();
}
}
getState(id) {
return this.currentDocument.states.get(id);
}
getStates() {
return this.currentDocument.states;
}
logDocuments() {
log.info('Documents = ', this.documents);
}
getRelations() {
return this.currentDocument.relations;
}
/**
* If the id is a start node ( [*] ), then return a new id constructed from
* the start node name and the current start node count.
* else return the given id
*
* @param {string} id
* @returns {string} - the id (original or constructed)
* @private
*/
startIdIfNeeded(id = '') {
let fixedId = id;
if (id === START_NODE) {
this.startEndCount++;
fixedId = `${START_TYPE}${this.startEndCount}`;
}
return fixedId;
}
/**
* If the id is a start node ( [*] ), then return the start type ('start')
* else return the given type
*
* @param {string} id
* @param {string} type
* @returns {string} - the type that should be used
* @private
*/
startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
return id === START_NODE ? START_TYPE : type;
}
/**
* If the id is an end node ( [*] ), then return a new id constructed from
* the end node name and the current start_end node count.
* else return the given id
*
* @param {string} id
* @returns {string} - the id (original or constructed)
* @private
*/
endIdIfNeeded(id = '') {
let fixedId = id;
if (id === END_NODE) {
this.startEndCount++;
fixedId = `${END_TYPE}${this.startEndCount}`;
}
return fixedId;
}
/**
* If the id is an end node ( [*] ), then return the end type
* else return the given type
*
* @param {string} id
* @param {string} type
* @returns {string} - the type that should be used
* @private
*/
endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
return id === END_NODE ? END_TYPE : type;
}
/**
*
* @param item1
* @param item2
* @param relationTitle
*/
addRelationObjs(item1, item2, relationTitle) {
let id1 = this.startIdIfNeeded(item1.id.trim());
let type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type);
let id2 = this.startIdIfNeeded(item2.id.trim());
let type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type);
this.addState(
id1,
type1,
item1.doc,
item1.description,
item1.note,
item1.classes,
item1.styles,
item1.textStyles
);
this.addState(
id2,
type2,
item2.doc,
item2.description,
item2.note,
item2.classes,
item2.styles,
item2.textStyles
);
this.currentDocument.relations.push({
id1,
id2,
relationTitle: common.sanitizeText(relationTitle, getConfig()),
});
}
/**
* Add a relation between two items. The items may be full objects or just the string id of a state.
*
* @param {string | object} item1
* @param {string | object} item2
* @param {string} title
*/
addRelation(item1, item2, title) {
if (typeof item1 === 'object') {
this.addRelationObjs(item1, item2, title);
} else {
const id1 = this.startIdIfNeeded(item1.trim());
const type1 = this.startTypeIfNeeded(item1);
const id2 = this.endIdIfNeeded(item2.trim());
const type2 = this.endTypeIfNeeded(item2);
this.addState(id1, type1);
this.addState(id2, type2);
this.currentDocument.relations.push({
id1,
id2,
title: common.sanitizeText(title, getConfig()),
});
}
}
addDescription(id, descr) {
const theState = this.currentDocument.states.get(id);
const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr;
theState.descriptions.push(common.sanitizeText(_descr, getConfig()));
}
cleanupLabel(label) {
if (label.substring(0, 1) === ':') {
return label.substr(2).trim();
} else {
return label.trim();
}
}
getDividerId() {
this.dividerCnt++;
return 'divider-id-' + this.dividerCnt;
}
/**
* Called when the parser comes across a (style) class definition
* @example classDef my-style fill:#f96;
*
* @param {string} id - the id of this (style) class
* @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma)
*/
addStyleClass(id, styleAttributes = '') {
// create a new style class object with this id
if (!this.classes.has(id)) {
this.classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef
}
const foundClass = this.classes.get(id);
if (styleAttributes !== undefined && styleAttributes !== null) {
styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => {
// remove any trailing ;
const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim();
// replace some style keywords
if (RegExp(COLOR_KEYWORD).exec(attrib)) {
const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL);
const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD);
foundClass.textStyles.push(newStyle2);
}
foundClass.styles.push(fixedAttrib);
});
}
}
/**
* Return all of the style classes
* @returns {{} | any | classes}
*/
getClasses() {
return this.classes;
}
/**
* Add a (style) class or css class to a state with the given id.
* If the state isn't already in the list of known states, add it.
* Might be called by parser when a style class or CSS class should be applied to a state
*
* @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to
* @param {string} cssClassName CSS class name
*/
setCssClass(itemIds, cssClassName) {
itemIds.split(',').forEach((id) => {
let foundState = this.getState(id);
if (foundState === undefined) {
const trimmedId = id.trim();
this.addState(trimmedId);
foundState = this.getState(trimmedId);
}
foundState.classes.push(cssClassName);
});
}
/**
* Add a style to a state with the given id.
* @example style stateId fill:#f9f,stroke:#333,stroke-width:4px
* where 'style' is the keyword
* stateId is the id of a state
* the rest of the string is the styleText (all of the attributes to be applied to the state)
*
* @param itemId The id of item to apply the style to
* @param styleText - the text of the attributes for the style
*/
setStyle(itemId, styleText) {
const item = this.getState(itemId);
if (item !== undefined) {
item.styles.push(styleText);
}
}
/**
* Add a text style to a state with the given id
*
* @param itemId The id of item to apply the css class to
* @param cssClassName CSS class name
*/
setTextStyle(itemId, cssClassName) {
const item = this.getState(itemId);
if (item !== undefined) {
item.textStyles.push(cssClassName);
}
}
/**
* Finds the direction statement in the root document.
* @private
* @returns {{ value: string } | undefined} - the direction statement if present
*/
getDirectionStatement() {
return this.rootDoc.find((doc) => doc.stmt === STMT_DIRECTION);
}
getDirection() {
return this.getDirectionStatement()?.value ?? DEFAULT_DIAGRAM_DIRECTION;
}
setDirection(dir) {
const doc = this.getDirectionStatement();
if (doc) {
doc.value = dir;
} else {
this.rootDoc.unshift({ stmt: STMT_DIRECTION, value: dir });
}
}
trimColon(str) {
return str && str[0] === ':' ? str.substr(1).trim() : str.trim();
}
getData() {
const config = getConfig();
return {
nodes: this.nodes,
edges: this.edges,
other: {},
config,
direction: getDir(this.getRootDocV2()),
};
}
getConfig() {
return getConfig().state;
}
getAccTitle = getAccTitle;
setAccTitle = setAccTitle;
getAccDescription = getAccDescription;
setAccDescription = setAccDescription;
setDiagramTitle = setDiagramTitle;
getDiagramTitle = getDiagramTitle;
}

View File

@@ -0,0 +1,693 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { generateId } from '../../utils.js';
import common from '../common/common.js';
import {
clear as commonClear,
getAccDescription,
getAccTitle,
getDiagramTitle,
setAccDescription,
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import { dataFetcher, reset as resetDataFetcher } from './dataFetcher.js';
import { getDir } from './stateRenderer-v3-unified.js';
import {
DEFAULT_DIAGRAM_DIRECTION,
DEFAULT_STATE_TYPE,
DIVIDER_TYPE,
STMT_APPLYCLASS,
STMT_CLASSDEF,
STMT_RELATION,
STMT_ROOT,
STMT_DIRECTION,
STMT_STATE,
STMT_STYLEDEF,
} from './stateCommon.js';
import type { MermaidConfig } from '../../config.type.js';
const CONSTANTS = {
START_NODE: '[*]',
START_TYPE: 'start',
END_NODE: '[*]',
END_TYPE: 'end',
COLOR_KEYWORD: 'color',
FILL_KEYWORD: 'fill',
BG_FILL: 'bgFill',
STYLECLASS_SEP: ',',
} as const;
interface BaseStmt {
stmt: 'applyClass' | 'classDef' | 'dir' | 'relation' | 'state' | 'style' | 'root' | 'default';
}
interface ApplyClassStmt extends BaseStmt {
stmt: 'applyClass';
id: string;
styleClass: string;
}
interface ClassDefStmt extends BaseStmt {
stmt: 'classDef';
id: string;
classes: string;
}
interface DirectionStmt extends BaseStmt {
stmt: 'dir';
value: 'TB' | 'BT' | 'RL' | 'LR';
}
interface RelationStmt extends BaseStmt {
stmt: 'relation';
state1: StateStmt;
state2: StateStmt;
description?: string;
}
export interface StateStmt extends BaseStmt {
stmt: 'state' | 'default';
id: string;
type: 'default' | 'fork' | 'join' | 'choice' | 'divider' | 'start' | 'end';
description?: string;
descriptions?: string[];
doc?: Stmt[];
note?: Note;
start?: boolean;
classes?: string[];
styles?: string[];
textStyles?: string[];
}
interface StyleStmt extends BaseStmt {
stmt: 'style';
id: string;
styleClass: string;
}
export interface RootStmt {
id: 'root';
stmt: 'root';
doc?: Stmt[];
}
interface Note {
position?: 'left of' | 'right of';
text: string;
}
export type Stmt =
| ApplyClassStmt
| ClassDefStmt
| DirectionStmt
| RelationStmt
| StateStmt
| StyleStmt
| RootStmt;
interface DiagramEdge {
id1: string;
id2: string;
relationTitle?: string;
}
interface Document {
relations: DiagramEdge[];
states: Map<string, StateStmt>;
documents: Record<string, Document>;
}
export interface StyleClass {
id: string;
styles: string[];
textStyles: string[];
}
export interface NodeData {
labelStyle?: string;
shape: string;
label?: string | string[];
cssClasses: string;
cssCompiledStyles?: string[];
cssStyles: string[];
id: string;
dir?: string;
domId?: string;
type?: string;
isGroup?: boolean;
padding?: number;
rx?: number;
ry?: number;
look?: MermaidConfig['look'];
parentId?: string;
centerLabel?: boolean;
position?: string;
description?: string | string[];
}
export interface Edge {
id: string;
start: string;
end: string;
arrowhead: string;
arrowTypeEnd: string;
style: string;
labelStyle: string;
label?: string;
arrowheadStyle: string;
labelpos: string;
labelType: string;
thickness: string;
classes: string;
look: MermaidConfig['look'];
}
/**
* Returns a new list of classes.
* In the future, this can be replaced with a class common to all diagrams.
* ClassDef information = \{ id: id, styles: [], textStyles: [] \}
*/
const newClassesList = (): Map<string, StyleClass> => new Map();
const newDoc = (): Document => ({
relations: [],
states: new Map(),
documents: {},
});
const clone = <T>(o: T): T => JSON.parse(JSON.stringify(o));
export class StateDB {
private nodes: NodeData[] = [];
private edges: Edge[] = [];
private rootDoc: Stmt[] = [];
private classes = newClassesList();
private documents = { root: newDoc() };
private currentDocument = this.documents.root;
private startEndCount = 0;
private dividerCnt = 0;
static readonly relationType = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3,
} as const;
constructor(private version: 1 | 2) {
this.clear();
// Bind methods used by JISON
this.setRootDoc = this.setRootDoc.bind(this);
this.getDividerId = this.getDividerId.bind(this);
this.setDirection = this.setDirection.bind(this);
this.trimColon = this.trimColon.bind(this);
}
/**
* Convert all of the statements (stmts) that were parsed into states and relationships.
* This is done because a state diagram may have nested sections,
* where each section is a 'document' and has its own set of statements.
* Ex: the section within a fork has its own statements, and incoming and outgoing statements
* refer to the fork as a whole (document).
* See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement.
* This will push the statement into the list of statements for the current document.
*/
extract(statements: Stmt[] | { doc: Stmt[] }) {
this.clear(true);
for (const item of Array.isArray(statements) ? statements : statements.doc) {
switch (item.stmt) {
case STMT_STATE:
this.addState(item.id.trim(), item.type, item.doc, item.description, item.note);
break;
case STMT_RELATION:
this.addRelation(item.state1, item.state2, item.description);
break;
case STMT_CLASSDEF:
this.addStyleClass(item.id.trim(), item.classes);
break;
case STMT_STYLEDEF:
this.handleStyleDef(item);
break;
case STMT_APPLYCLASS:
this.setCssClass(item.id.trim(), item.styleClass);
break;
}
}
const diagramStates = this.getStates();
const config = getConfig();
resetDataFetcher();
dataFetcher(
undefined,
this.getRootDocV2() as StateStmt,
diagramStates,
this.nodes,
this.edges,
true,
config.look,
this.classes
);
// Process node labels
for (const node of this.nodes) {
if (!Array.isArray(node.label)) {
continue;
}
node.description = node.label.slice(1);
if (node.isGroup && node.description.length > 0) {
throw new Error(
`Group nodes can only have label. Remove the additional description for node [${node.id}]`
);
}
node.label = node.label[0];
}
}
private handleStyleDef(item: StyleStmt) {
const ids = item.id.trim().split(',');
const styles = item.styleClass.split(',');
for (const id of ids) {
let state = this.getState(id);
if (!state) {
const trimmedId = id.trim();
this.addState(trimmedId);
state = this.getState(trimmedId);
}
if (state) {
state.styles = styles.map((s) => s.replace(/;/g, '')?.trim());
}
}
}
setRootDoc(o: Stmt[]) {
log.info('Setting root doc', o);
this.rootDoc = o;
if (this.version === 1) {
this.extract(o);
} else {
this.extract(this.getRootDocV2());
}
}
docTranslator(parent: RootStmt | StateStmt, node: Stmt, first: boolean) {
if (node.stmt === STMT_RELATION) {
this.docTranslator(parent, node.state1, true);
this.docTranslator(parent, node.state2, false);
return;
}
if (node.stmt === STMT_STATE) {
if (node.id === CONSTANTS.START_NODE) {
node.id = parent.id + (first ? '_start' : '_end');
node.start = first;
} else {
// This is just a plain state, not a start or end
node.id = node.id.trim();
}
}
if ((node.stmt !== STMT_ROOT && node.stmt !== STMT_STATE) || !node.doc) {
return;
}
const doc = [];
// Check for concurrency
let currentDoc = [];
for (const stmt of node.doc) {
if ((stmt as StateStmt).type === DIVIDER_TYPE) {
const newNode = clone(stmt as StateStmt);
newNode.doc = clone(currentDoc);
doc.push(newNode);
currentDoc = [];
} else {
currentDoc.push(stmt);
}
}
// If any divider was encountered
if (doc.length > 0 && currentDoc.length > 0) {
const newNode = {
stmt: STMT_STATE,
id: generateId(),
type: 'divider',
doc: clone(currentDoc),
} satisfies StateStmt;
doc.push(clone(newNode));
node.doc = doc;
}
node.doc.forEach((docNode) => this.docTranslator(node, docNode, true));
}
private getRootDocV2() {
this.docTranslator(
{ id: STMT_ROOT, stmt: STMT_ROOT },
{ id: STMT_ROOT, stmt: STMT_ROOT, doc: this.rootDoc },
true
);
return { id: STMT_ROOT, doc: this.rootDoc };
}
/**
* Function called by parser when a node definition has been found.
*
* @param descr - description for the state. Can be a string or a list or strings
* @param classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class.
* @param styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style.
* @param textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style.
*/
addState(
id: string,
type: StateStmt['type'] = DEFAULT_STATE_TYPE,
doc: Stmt[] | undefined = undefined,
descr: string | string[] | undefined = undefined,
note: Note | undefined = undefined,
classes: string | string[] | undefined = undefined,
styles: string | string[] | undefined = undefined,
textStyles: string | string[] | undefined = undefined
) {
const trimmedId = id?.trim();
if (!this.currentDocument.states.has(trimmedId)) {
log.info('Adding state ', trimmedId, descr);
this.currentDocument.states.set(trimmedId, {
stmt: STMT_STATE,
id: trimmedId,
descriptions: [],
type,
doc,
note,
classes: [],
styles: [],
textStyles: [],
});
} else {
const state = this.currentDocument.states.get(trimmedId);
if (!state) {
throw new Error(`State not found: ${trimmedId}`);
}
if (!state.doc) {
state.doc = doc;
}
if (!state.type) {
state.type = type;
}
}
if (descr) {
log.info('Setting state description', trimmedId, descr);
const descriptions = Array.isArray(descr) ? descr : [descr];
descriptions.forEach((des) => this.addDescription(trimmedId, des.trim()));
}
if (note) {
const doc2 = this.currentDocument.states.get(trimmedId);
if (!doc2) {
throw new Error(`State not found: ${trimmedId}`);
}
doc2.note = note;
doc2.note.text = common.sanitizeText(doc2.note.text, getConfig());
}
if (classes) {
log.info('Setting state classes', trimmedId, classes);
const classesList = Array.isArray(classes) ? classes : [classes];
classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim()));
}
if (styles) {
log.info('Setting state styles', trimmedId, styles);
const stylesList = Array.isArray(styles) ? styles : [styles];
stylesList.forEach((style) => this.setStyle(trimmedId, style.trim()));
}
if (textStyles) {
log.info('Setting state styles', trimmedId, styles);
const textStylesList = Array.isArray(textStyles) ? textStyles : [textStyles];
textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim()));
}
}
clear(saveCommon?: boolean) {
this.nodes = [];
this.edges = [];
this.documents = { root: newDoc() };
this.currentDocument = this.documents.root;
// number of start and end nodes; used to construct ids
this.startEndCount = 0;
this.classes = newClassesList();
if (!saveCommon) {
commonClear();
}
}
getState(id: string) {
return this.currentDocument.states.get(id);
}
getStates() {
return this.currentDocument.states;
}
logDocuments() {
log.info('Documents = ', this.documents);
}
getRelations() {
return this.currentDocument.relations;
}
/**
* If the id is a start node ( [*] ), then return a new id constructed from
* the start node name and the current start node count.
* else return the given id
*/
startIdIfNeeded(id = '') {
if (id === CONSTANTS.START_NODE) {
this.startEndCount++;
return `${CONSTANTS.START_TYPE}${this.startEndCount}`;
}
return id;
}
/**
* If the id is a start node ( [*] ), then return the start type ('start')
* else return the given type
*/
startTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) {
return id === CONSTANTS.START_NODE ? CONSTANTS.START_TYPE : type;
}
/**
* If the id is an end node ( [*] ), then return a new id constructed from
* the end node name and the current start_end node count.
* else return the given id
*/
endIdIfNeeded(id = '') {
if (id === CONSTANTS.END_NODE) {
this.startEndCount++;
return `${CONSTANTS.END_TYPE}${this.startEndCount}`;
}
return id;
}
/**
* If the id is an end node ( [*] ), then return the end type
* else return the given type
*
*/
endTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) {
return id === CONSTANTS.END_NODE ? CONSTANTS.END_TYPE : type;
}
addRelationObjs(item1: StateStmt, item2: StateStmt, relationTitle = '') {
const id1 = this.startIdIfNeeded(item1.id.trim());
const type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type);
const id2 = this.startIdIfNeeded(item2.id.trim());
const type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type);
this.addState(
id1,
type1,
item1.doc,
item1.description,
item1.note,
item1.classes,
item1.styles,
item1.textStyles
);
this.addState(
id2,
type2,
item2.doc,
item2.description,
item2.note,
item2.classes,
item2.styles,
item2.textStyles
);
this.currentDocument.relations.push({
id1,
id2,
relationTitle: common.sanitizeText(relationTitle, getConfig()),
});
}
/**
* Add a relation between two items. The items may be full objects or just the string id of a state.
*/
addRelation(item1: string | StateStmt, item2: string | StateStmt, title?: string) {
if (typeof item1 === 'object' && typeof item2 === 'object') {
this.addRelationObjs(item1, item2, title);
} else if (typeof item1 === 'string' && typeof item2 === 'string') {
const id1 = this.startIdIfNeeded(item1.trim());
const type1 = this.startTypeIfNeeded(item1);
const id2 = this.endIdIfNeeded(item2.trim());
const type2 = this.endTypeIfNeeded(item2);
this.addState(id1, type1);
this.addState(id2, type2);
this.currentDocument.relations.push({
id1,
id2,
relationTitle: title ? common.sanitizeText(title, getConfig()) : undefined,
});
}
}
addDescription(id: string, descr: string) {
const theState = this.currentDocument.states.get(id);
const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr;
theState?.descriptions?.push(common.sanitizeText(_descr, getConfig()));
}
cleanupLabel(label: string) {
return label.startsWith(':') ? label.slice(2).trim() : label.trim();
}
getDividerId() {
this.dividerCnt++;
return `divider-id-${this.dividerCnt}`;
}
/**
* Called when the parser comes across a (style) class definition
* @example classDef my-style fill:#f96;
*
* @param id - the id of this (style) class
* @param styleAttributes - the string with 1 or more style attributes (each separated by a comma)
*/
addStyleClass(id: string, styleAttributes = '') {
// create a new style class object with this id
if (!this.classes.has(id)) {
this.classes.set(id, { id, styles: [], textStyles: [] });
}
const foundClass = this.classes.get(id);
if (styleAttributes && foundClass) {
styleAttributes.split(CONSTANTS.STYLECLASS_SEP).forEach((attrib) => {
const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim();
if (RegExp(CONSTANTS.COLOR_KEYWORD).exec(attrib)) {
const newStyle1 = fixedAttrib.replace(CONSTANTS.FILL_KEYWORD, CONSTANTS.BG_FILL);
const newStyle2 = newStyle1.replace(CONSTANTS.COLOR_KEYWORD, CONSTANTS.FILL_KEYWORD);
foundClass.textStyles.push(newStyle2);
}
foundClass.styles.push(fixedAttrib);
});
}
}
getClasses() {
return this.classes;
}
/**
* Add a (style) class or css class to a state with the given id.
* If the state isn't already in the list of known states, add it.
* Might be called by parser when a style class or CSS class should be applied to a state
*
* @param itemIds - The id or a list of ids of the item(s) to apply the css class to
* @param cssClassName - CSS class name
*/
setCssClass(itemIds: string, cssClassName: string) {
itemIds.split(',').forEach((id) => {
let foundState = this.getState(id);
if (!foundState) {
const trimmedId = id.trim();
this.addState(trimmedId);
foundState = this.getState(trimmedId);
}
foundState?.classes?.push(cssClassName);
});
}
/**
* Add a style to a state with the given id.
* @example style stateId fill:#f9f,stroke:#333,stroke-width:4px
* where 'style' is the keyword
* stateId is the id of a state
* the rest of the string is the styleText (all of the attributes to be applied to the state)
*
* @param itemId - The id of item to apply the style to
* @param styleText - the text of the attributes for the style
*/
setStyle(itemId: string, styleText: string) {
this.getState(itemId)?.styles?.push(styleText);
}
/**
* Add a text style to a state with the given id
*
* @param itemId - The id of item to apply the css class to
* @param cssClassName - CSS class name
*/
setTextStyle(itemId: string, cssClassName: string) {
this.getState(itemId)?.textStyles?.push(cssClassName);
}
/**
* Finds the direction statement in the root document.
* @returns the direction statement if present
*/
private getDirectionStatement() {
return this.rootDoc.find((doc): doc is DirectionStmt => doc.stmt === STMT_DIRECTION);
}
getDirection() {
return this.getDirectionStatement()?.value ?? DEFAULT_DIAGRAM_DIRECTION;
}
setDirection(dir: DirectionStmt['value']) {
const doc = this.getDirectionStatement();
if (doc) {
doc.value = dir;
} else {
this.rootDoc.unshift({ stmt: STMT_DIRECTION, value: dir });
}
}
trimColon(str: string) {
return str.startsWith(':') ? str.slice(1).trim() : str.trim();
}
getData() {
const config = getConfig();
return {
nodes: this.nodes,
edges: this.edges,
other: {},
config,
direction: getDir(this.getRootDocV2()),
};
}
getConfig() {
return getConfig().state;
}
getAccTitle = getAccTitle;
setAccTitle = setAccTitle;
getAccDescription = getAccDescription;
setAccDescription = setAccDescription;
setDiagramTitle = setDiagramTitle;
getDiagramTitle = getDiagramTitle;
}

View File

@@ -5,6 +5,7 @@ import { StateDB } from './stateDb.js';
describe('state diagram V2, ', function () {
// TODO - these examples should be put into ./parser/stateDiagram.spec.js
describe('when parsing an info graph it', function () {
/** @type {StateDB} */
let stateDb;
beforeEach(function () {
stateDb = new StateDB(2);
@@ -347,6 +348,20 @@ describe('state diagram V2, ', function () {
`;
parser.parse(str);
expect(stateDb.getState('Active').note).toMatchInlineSnapshot(`
{
"position": "left of",
"text": "this is a short<br>note",
}
`);
expect(stateDb.getState('Inactive').note).toMatchInlineSnapshot(`
{
"position": "right of",
"text": "A note can also
be defined on
several lines",
}
`);
});
it('should handle multiline notes with different line breaks', function () {
const str = `stateDiagram-v2
@@ -357,6 +372,12 @@ describe('state diagram V2, ', function () {
`;
parser.parse(str);
expect(stateDb.getStates().get('State1').note).toMatchInlineSnapshot(`
{
"position": "right of",
"text": "Line1<br>Line2<br>Line3<br>Line4<br>Line5",
}
`);
});
it('should handle floating notes', function () {
const str = `stateDiagram-v2
@@ -367,15 +388,14 @@ describe('state diagram V2, ', function () {
parser.parse(str);
});
it('should handle floating notes', function () {
const str = `stateDiagram-v2\n
const str = `stateDiagram-v2
state foo
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle notes for composite (nested) states', function () {
const str = `stateDiagram-v2\n
const str = `stateDiagram-v2
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
@@ -390,6 +410,12 @@ describe('state diagram V2, ', function () {
`;
parser.parse(str);
expect(stateDb.getState('NotShooting').note).toMatchInlineSnapshot(`
{
"position": "right of",
"text": "This is a note on a composite state",
}
`);
});
it('A composite state should be able to link to itself', () => {

View File

@@ -13,15 +13,17 @@ export const setConf = function (cnf) {
};
const actors = {};
let maxWidth = 0;
/** @param diagram - The diagram to draw to. */
function drawActorLegend(diagram) {
const conf = getConfig().journey;
// Draw the actors
const maxLabelWidth = conf.maxLabelWidth;
maxWidth = 0;
let yPos = 60;
Object.keys(actors).forEach((person) => {
const colour = actors[person].color;
const circleData = {
cx: 20,
cy: yPos,
@@ -32,25 +34,97 @@ function drawActorLegend(diagram) {
};
svgDraw.drawCircle(diagram, circleData);
// First, measure the full text width without wrapping.
let measureText = diagram.append('text').attr('visibility', 'hidden').text(person);
const fullTextWidth = measureText.node().getBoundingClientRect().width;
measureText.remove();
let lines = [];
// If the text is naturally within the max width, use it as a single line.
if (fullTextWidth <= maxLabelWidth) {
lines = [person];
} else {
// Otherwise, wrap the text using the knuth-plass algorithm.
const words = person.split(' '); // Split the text into words.
let currentLine = '';
measureText = diagram.append('text').attr('visibility', 'hidden');
words.forEach((word) => {
// check the width of the line with the new word.
const testLine = currentLine ? `${currentLine} ${word}` : word;
measureText.text(testLine);
const textWidth = measureText.node().getBoundingClientRect().width;
if (textWidth > maxLabelWidth) {
// If adding the new word exceeds max width, push the current line.
if (currentLine) {
lines.push(currentLine);
}
currentLine = word; // Start a new line with the current word.
// If the word itself is too long, break it with a hyphen.
measureText.text(word);
if (measureText.node().getBoundingClientRect().width > maxLabelWidth) {
let brokenWord = '';
for (const char of word) {
brokenWord += char;
measureText.text(brokenWord + '-');
if (measureText.node().getBoundingClientRect().width > maxLabelWidth) {
// Push the broken part with a hyphen.
lines.push(brokenWord.slice(0, -1) + '-');
brokenWord = char;
}
}
currentLine = brokenWord;
}
} else {
// If the line with the new word fits, add the new word to the current line.
currentLine = testLine;
}
});
// Push the last line.
if (currentLine) {
lines.push(currentLine);
}
measureText.remove(); // Remove the text element used for measuring.
}
lines.forEach((line, index) => {
const labelData = {
x: 40,
y: yPos + 7,
y: yPos + 7 + index * 20,
fill: '#666',
text: person,
textMargin: conf.boxTextMargin | 5,
text: line,
textMargin: conf.boxTextMargin ?? 5,
};
svgDraw.drawText(diagram, labelData);
yPos += 20;
// Draw the text and measure the width.
const textElement = svgDraw.drawText(diagram, labelData);
const lineWidth = textElement.node().getBoundingClientRect().width;
// Use conf.leftMargin as the initial spacing baseline,
// but expand maxWidth if the line is wider.
if (lineWidth > maxWidth && lineWidth > conf.leftMargin - lineWidth) {
maxWidth = lineWidth;
}
});
yPos += Math.max(20, lines.length * 20);
});
}
// TODO: Cleanup?
const conf = getConfig().journey;
const LEFT_MARGIN = conf.leftMargin;
let leftMargin = 0;
export const draw = function (text, id, version, diagObj) {
const conf = getConfig().journey;
const configObject = getConfig();
const titleColor = configObject.journey.titleColor;
const titleFontSize = configObject.journey.titleFontSize;
const titleFontFamily = configObject.journey.titleFontFamily;
const securityLevel = getConfig().securityLevel;
const securityLevel = configObject.securityLevel;
// Handle root and Document for when rendering in sandbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {
@@ -84,7 +158,8 @@ export const draw = function (text, id, version, diagObj) {
});
drawActorLegend(diagram);
bounds.insert(0, 0, LEFT_MARGIN, Object.keys(actors).length * 50);
leftMargin = conf.leftMargin + maxWidth;
bounds.insert(0, 0, leftMargin, Object.keys(actors).length * 50);
drawTasks(diagram, tasks, 0);
const box = bounds.getBounds();
@@ -92,23 +167,25 @@ export const draw = function (text, id, version, diagObj) {
diagram
.append('text')
.text(title)
.attr('x', LEFT_MARGIN)
.attr('font-size', '4ex')
.attr('x', leftMargin)
.attr('font-size', titleFontSize)
.attr('font-weight', 'bold')
.attr('y', 25);
.attr('y', 25)
.attr('fill', titleColor)
.attr('font-family', titleFontFamily);
}
const height = box.stopy - box.starty + 2 * conf.diagramMarginY;
const width = LEFT_MARGIN + box.stopx + 2 * conf.diagramMarginX;
const width = leftMargin + box.stopx + 2 * conf.diagramMarginX;
configureSvgSize(diagram, height, width, conf.useMaxWidth);
// Draw activity line
diagram
.append('line')
.attr('x1', LEFT_MARGIN)
.attr('x1', leftMargin)
.attr('y1', conf.height * 4) // One section head + one task + margins
.attr('x2', width - LEFT_MARGIN - 4) // Subtract stroke width so arrow point is retained
.attr('x2', width - leftMargin - 4) // Subtract stroke width so arrow point is retained
.attr('y2', conf.height * 4)
.attr('stroke-width', 4)
.attr('stroke', 'black')
@@ -234,7 +311,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) {
}
const section = {
x: i * conf.taskMargin + i * conf.width + LEFT_MARGIN,
x: i * conf.taskMargin + i * conf.width + leftMargin,
y: 50,
text: task.section,
fill,
@@ -258,7 +335,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) {
}, {});
// Add some rendering data to the object
task.x = i * conf.taskMargin + i * conf.width + LEFT_MARGIN;
task.x = i * conf.taskMargin + i * conf.width + leftMargin;
task.y = taskPos;
task.width = conf.diagramMarginX;
task.height = conf.diagramMarginY;

View File

@@ -93,6 +93,7 @@ export interface XYChartConfig {
titleFontSize: number;
titlePadding: number;
showTitle: boolean;
showDataLabel: boolean;
xAxis: XYChartAxisConfig;
yAxis: XYChartAxisConfig;
chartOrientation: 'vertical' | 'horizontal';

View File

@@ -195,6 +195,10 @@ function getChartConfig() {
return xyChartConfig;
}
function getXYChartData() {
return xyChartData;
}
const clear = function () {
commonClear();
plotIndex = 0;
@@ -226,4 +230,5 @@ export default {
setTmpSVGG,
getChartThemeConfig,
getChartConfig,
getXYChartData,
};

View File

@@ -14,6 +14,7 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
const db = diagObj.db as typeof XYChartDB;
const themeConfig = db.getChartThemeConfig();
const chartConfig = db.getChartConfig();
const labelData = db.getXYChartData().plots[0].data.map((data) => data[1]);
function getDominantBaseLine(horizontalPos: TextVerticalPos) {
return horizontalPos === 'top' ? 'text-before-edge' : 'middle';
}
@@ -49,6 +50,16 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
const groups: Record<string, any> = {};
interface BarItem {
data: {
x: number;
y: number;
width: number;
height: number;
};
label: string;
}
function getGroup(gList: string[]) {
let elem = group;
let prefix = '';
@@ -87,6 +98,113 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
.attr('fill', (data) => data.fill)
.attr('stroke', (data) => data.strokeFill)
.attr('stroke-width', (data) => data.strokeWidth);
if (chartConfig.showDataLabel) {
if (chartConfig.chartOrientation === 'horizontal') {
// Factor to approximate each character's width.
const charWidthFactor = 0.7;
// Filter out bars that have zero width or height.
const validItems = shape.data
.map((d, i) => ({ data: d, label: labelData[i].toString() }))
.filter((item) => item.data.width > 0 && item.data.height > 0);
// Helper function to check if the text fits horizontally with a 10px right margin.
function fitsHorizontally(item: BarItem, fontSize: number): boolean {
const { data, label } = item;
// Approximate the text width.
const textWidth: number = fontSize * label.length * charWidthFactor;
// The available width is the bar's width minus a 10px right margin.
return textWidth <= data.width - 10;
}
// For each valid bar, start with an initial candidate font size (70% of the bar's height),
// then reduce it until the text fits horizontally.
const candidateFontSizes = validItems.map((item) => {
const { data } = item;
let fontSize = data.height * 0.7;
// Decrease fontSize until the text fits horizontally.
while (!fitsHorizontally(item, fontSize) && fontSize > 0) {
fontSize -= 1;
}
return fontSize;
});
// Choose the smallest candidate font size across all valid bars for uniformity.
const uniformFontSize = Math.floor(Math.min(...candidateFontSizes));
shapeGroup
.selectAll('text')
.data(validItems)
.enter()
.append('text')
.attr('x', (item) => item.data.x + item.data.width - 10)
.attr('y', (item) => item.data.y + item.data.height / 2)
.attr('text-anchor', 'end')
.attr('dominant-baseline', 'middle')
.attr('fill', 'black')
.attr('font-size', `${uniformFontSize}px`)
.text((item) => item.label);
} else {
const yOffset = 10;
// filter out bars that have zero width or height.
const validItems = shape.data
.map((d, i) => ({ data: d, label: labelData[i].toString() }))
.filter((item) => item.data.width > 0 && item.data.height > 0);
// Helper function that checks if the text with a given fontSize fits within the bar boundaries.
function fitsInBar(item: BarItem, fontSize: number, yOffset: number): boolean {
const { data, label } = item;
const charWidthFactor = 0.7;
const textWidth = fontSize * label.length * charWidthFactor;
// Compute horizontal boundaries using the center.
const centerX = data.x + data.width / 2;
const leftEdge = centerX - textWidth / 2;
const rightEdge = centerX + textWidth / 2;
// Check that text doesn't overflow horizontally.
const horizontalFits = leftEdge >= data.x && rightEdge <= data.x + data.width;
// For vertical placement, we use 'dominant-baseline: hanging' so that y marks the top of the text.
// Thus, the bottom edge is y + yOffset + fontSize.
const verticalFits = data.y + yOffset + fontSize <= data.y + data.height;
return horizontalFits && verticalFits;
}
// For each valid item, start with a candidate font size based on the width,
// then reduce it until the text fits within both the horizontal and vertical boundaries.
const candidateFontSizes = validItems.map((item) => {
const { data, label } = item;
let fontSize = data.width / (label.length * 0.7);
// Decrease the font size until the text fits or fontSize reaches 0.
while (!fitsInBar(item, fontSize, yOffset) && fontSize > 0) {
fontSize -= 1;
}
return fontSize;
});
// Choose the smallest candidate across all valid bars for uniformity.
const uniformFontSize = Math.floor(Math.min(...candidateFontSizes));
// Render text only for valid items.
shapeGroup
.selectAll('text')
.data(validItems)
.enter()
.append('text')
.attr('x', (item) => item.data.x + item.data.width / 2)
.attr('y', (item) => item.data.y + yOffset)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'hanging')
.attr('fill', 'black')
.attr('font-size', `${uniformFontSize}px`)
.text((item) => item.label);
}
}
break;
case 'text':
shapeGroup

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { type Ref, ref, onMounted } from 'vue';
interface Taglines {
label: string;
@@ -93,13 +93,22 @@ const allTaglines: { [key: string]: { design: number; taglines: Taglines[] } } =
},
};
const { design, taglines } =
Object.values(allTaglines)[Math.floor(Math.random() * Object.values(allTaglines).length)];
// Initialize with default values
const design: Ref<number> = ref(1);
const taglines: Ref<Taglines[]> = ref([]);
const index: Ref<number> = ref(0);
let index = ref(Math.floor(Math.random() * taglines.length));
onMounted(() => {
// Select a random variant on client side
const variant =
Object.values(allTaglines)[Math.floor(Math.random() * Object.values(allTaglines).length)];
design.value = variant.design;
taglines.value = variant.taglines;
index.value = Math.floor(Math.random() * taglines.value.length);
// Set up the interval for cycling through taglines
setInterval(() => {
index.value = (index.value + 1) % taglines.length;
index.value = (index.value + 1) % taglines.value.length;
}, 5_000);
});
</script>
@@ -109,16 +118,17 @@ onMounted(() => {
:class="[design === 1 ? 'bg-gradient-to-r from-[#bd34fe] to-[#ff3670] ' : 'bg-[#E0095F]']"
class="mb-4 w-full top-bar flex p-2"
>
<p class="w-full tracking-wide fade-text">
<p class="w-full tracking-wide fade-text text-sm">
<transition name="fade" mode="out-in">
<a
v-if="taglines.length > 0 && taglines[index]"
:key="index"
:href="taglines[index].url"
target="_blank"
class="unstyled flex justify-center items-center gap-4 text-white tracking-wide plausible-event-name=bannerClick"
>
<span class="font-semibold">{{ taglines[index].label }}</span>
<button class="bg-[#1E1A2E] rounded-lg p-1.5 px-4 font-semibold tracking-wide">
<button class="bg-[#1E1A2E] shrink-0 rounded-lg p-1.5 px-4 font-semibold tracking-wide">
Try now
</button>
</a>

View File

@@ -159,6 +159,7 @@ function sidebarSyntax() {
{ text: 'Packet 🔥', link: '/syntax/packet' },
{ text: 'Kanban 🔥', link: '/syntax/kanban' },
{ text: 'Architecture 🔥', link: '/syntax/architecture' },
{ text: 'Radar 🔥', link: '/syntax/radar' },
{ text: 'Other Examples', link: '/syntax/examples' },
],
},

View File

@@ -240,6 +240,22 @@ Code is the heart of every software project. We strive to make it better. Who if
The core of Mermaid is located under `packages/mermaid/src`.
### Building Mermaid Locally
**Host**
```bash
pnpm run build
```
**Docker**
```bash
./run build
```
This will build the Mermaid library and the documentation site.
### Running Mermaid Locally
**Host**

View File

@@ -35,6 +35,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Mermaid Charts & Diagrams for Jira](https://marketplace.atlassian.com/apps/1224537/)
- [Mermaid for Jira Cloud - Draw UML diagrams easily](https://marketplace.atlassian.com/apps/1223053/mermaid-for-jira-cloud-draw-uml-diagrams-easily?hosting=cloud&tab=overview)
- [CloudScript.io Mermaid Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
- [Mermaid plus for Confluence](https://marketplace.atlassian.com/apps/1236814/mermaid-plus-for-confluence?hosting=cloud&tab=overview)
- [Azure Devops](https://learn.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#add-mermaid-diagrams-to-a-wiki-page) ✅
- [Deepdwn](https://billiam.itch.io/deepdwn) ✅
- [Doctave](https://www.doctave.com/) ✅
@@ -262,7 +263,5 @@ Communication tools and platforms
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
- [Reveal CK](https://github.com/jedcn/reveal-ck)
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
- [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic)
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
<!--- cspell:ignore Blazorade HueHive --->

View File

@@ -17,7 +17,7 @@
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@vueuse/core": "^12.7.0",
"@vueuse/core": "^13.1.0",
"font-awesome": "^4.7.0",
"jiti": "^2.4.2",
"mermaid": "workspace:^",
@@ -26,7 +26,7 @@
"devDependencies": {
"@iconify-json/carbon": "^1.1.37",
"@unocss/reset": "^66.0.0",
"@vite-pwa/vitepress": "^0.5.3",
"@vite-pwa/vitepress": "^1.0.0",
"@vitejs/plugin-vue": "^5.0.5",
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
@@ -34,7 +34,7 @@
"unocss": "^66.0.0",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.1.1",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-pwa": "^1.0.0",
"vitepress": "1.6.3",
"workbox-window": "^7.3.0"
}

View File

@@ -56,7 +56,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
Where:
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens.
- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space").
- `relationship` describes the way that both entities inter-relate. See below.
- `second-entity` is the name of the other entity.
- `relationship-label` describes the relationship from the perspective of the first entity.
@@ -71,6 +71,24 @@ This statement can be read as _a property contains one or more rooms, and a room
Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory.
#### Unicode text
Entity names, relationships, and attributes all support unicode text.
```mermaid-example
erDiagram
"This ❤ Unicode"
```
#### Markdown formatting
Markdown formatting and text is also supported.
```mermaid-example
erDiagram
"This **is** _Markdown_"
```
### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components:
@@ -109,6 +127,11 @@ Cardinality is a property that describes how many elements of another entity can
Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line:
| Value | Alias for |
| :---: | :---------------: |
| -- | _identifying_ |
| .. | _non-identifying_ |
**Aliases**
| Value | Alias for |
@@ -116,10 +139,16 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and
| to | _identifying_ |
| optionally to | _non-identifying_ |
```mermaid
```mermaid-example
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
PERSON ||--o{ NAMED-DRIVER : is
PERSON }o..o{ NAMED-DRIVER : is
```
```mermaid-example
erDiagram
CAR 1 to zero or more NAMED-DRIVER : allows
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
### Attributes
@@ -144,9 +173,9 @@ erDiagram
The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types.
### Entity Name Aliases (v10.5.0+)
### Entity Name Aliases
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name.
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names.
```mermaid-example
erDiagram
@@ -162,7 +191,7 @@ erDiagram
#### Attribute Keys and Comments
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
```mermaid-example
erDiagram
@@ -188,35 +217,225 @@ erDiagram
MANUFACTURER only one to zero or more CAR : makes
```
### Other Things
### Direction
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
- (v11.1.0+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
The direction statement declares the direction of the diagram.
## Styling
This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`).
### Config options
```mermaid-example
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
For simple color customization:
This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`).
| Name | Used as |
| :------- | :------------------------------------------------------------------- |
| `fill` | Background color of an entity or attribute |
| `stroke` | Border color of an entity or attribute, line color of a relationship |
```mermaid-example
erDiagram
direction LR
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
### Classes used
Possible diagram orientations are:
The following CSS class selectors are available for richer styling:
- TB - Top to bottom
- BT - Bottom to top
- RL - Right to left
- LR - Left to right
| Selector | Description |
| :------------------------- | :---------------------------------------------------- |
| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows |
| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows |
| `.er.entityBox` | The box representing an entity |
| `.er.entityLabel` | The label for an entity |
| `.er.relationshipLabel` | The label for a relationship |
| `.er.relationshipLabelBox` | The box surrounding a relationship label |
| `.er.relationshipLine` | The line representing a relationship between entities |
### Styling a node
It is possible to apply specific styles such as a thicker border or a different background color to a node.
```mermaid-example
erDiagram
id1||--||id2 : label
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
```
It is also possible to attach styles to a list of nodes in one statement:
```
style nodeId1,nodeId2 styleList
```
#### Classes
More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that
should have a different look.
A class definition looks like the example below:
```
classDef className fill:#f9f,stroke:#333,stroke-width:4px
```
It is also possible to define multiple classes in one statement:
```
classDef firstClassName,secondClassName font-size:12pt
```
Attachment of a class to a node is done as per below:
```
class nodeId1 className
```
It is also possible to attach a class to a list of nodes in one statement:
```
class nodeId1,nodeId2 className
```
Multiple classes can be attached at the same time as well:
```
class nodeId1,nodeId2 className1,className2
```
A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below:
```mermaid-example
erDiagram
direction TB
CAR:::someclass {
string registrationNumber
string make
string model
}
PERSON:::someclass {
string firstName
string lastName
int age
}
HOUSE:::someclass
classDef someclass fill:#f96
```
This form can be used when declaring relationships between entities:
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
Similar to the class statement, the shorthand syntax can also apply multiple classes at once:
```
nodeId:::className1,className2
```
### Default class
If a class is named default it will be assigned to all classes without specific class definitions.
```
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
```
> **Note:** Custom styles from style or other class statements take priority and will overwrite the default styles. (e.g. The `default` class gives nodes a background color of pink but the `blue` class will give that node a background color of blue if applied.)
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef default fill:#f9f,stroke-width:4px
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
## Configuration
### Layout
The layout of the diagram is handled by [`render()`](../config/setup/mermaid/interfaces/Mermaid.md#render). The default layout is dagre.
For larger or more-complex diagrams, you can alternatively apply the ELK (Eclipse Layout Kernel) layout using your YAML frontmatter's `config`. For more information, see [Customizing ELK Layout](../intro/syntax-reference.md#customizing-elk-layout).
```yaml
---
config:
layout: elk
---
```
Your Mermaid code should be similar to the following:
```mermaid-example
---
title: Order example
config:
layout: elk
---
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
```note
Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration.
```
<!--- cspell:locale en,en-gb --->

View File

@@ -721,7 +721,7 @@ To give an edge an ID, prepend the edge syntax with the ID followed by an `@` ch
```mermaid
flowchart LR
A e1@> B
A e1@--> B
```
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
@@ -746,7 +746,7 @@ In the initial version, two animation speeds are supported: `fast` and `slow`. S
```mermaid
flowchart LR
A e1@> B
A e1@--> B
e1@{ animation: fast }
```
@@ -758,7 +758,7 @@ You can also animate edges by assigning a class to them and then defining animat
```mermaid
flowchart LR
A e1@> B
A e1@--> B
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
```

View File

@@ -0,0 +1,198 @@
# Radar Diagram (v11.6.0+)
## Introduction
A radar diagram is a simple way to plot low-dimensional data in a circular format.
It is also known as a **radar chart**, **spider chart**, **star chart**, **cobweb chart**, **polar chart**, or **Kiviat diagram**.
## Usage
This diagram type is particularly useful for developers, data scientists, and engineers who require a clear and concise way to represent data in a circular format.
It is commonly used to graphically summarize and compare the performance of multiple entities across multiple dimensions.
## Syntax
```md
radar-beta
axis A, B, C, D, E
curve c1{1,2,3,4,5}
curve c2{5,4,3,2,1}
... More Fields ...
```
## Examples
```mermaid-example
---
title: "Grades"
---
radar-beta
axis m["Math"], s["Science"], e["English"]
axis h["History"], g["Geography"], a["Art"]
curve a["Alice"]{85, 90, 80, 70, 75, 90}
curve b["Bob"]{70, 75, 85, 80, 90, 85}
max 100
min 0
```
```mermaid-example
radar-beta
title Restaurant Comparison
axis food["Food Quality"], service["Service"], price["Price"]
axis ambiance["Ambiance"],
curve a["Restaurant A"]{4, 3, 2, 4}
curve b["Restaurant B"]{3, 4, 3, 3}
curve c["Restaurant C"]{2, 3, 4, 2}
curve d["Restaurant D"]{2, 2, 4, 3}
graticule polygon
max 5
```
## Details of Syntax
### Title
`title`: The title is an optional field that allows to render a title at the top of the radar diagram.
```
radar-beta
title Title of the Radar Diagram
...
```
### Axis
`axis`: The axis keyword is used to define the axes of the radar diagram.
Each axis is represented by an ID and an optional label.
Multiple axes can be defined in a single line.
```
radar-beta
axis id1["Label1"]
axis id2["Label2"], id3["Label3"]
...
```
### Curve
`curve`: The curve keyword is used to define the data points for a curve in the radar diagram.
Each curve is represented by an ID, an optional label, and a list of values.
Values can be defined by a list of numbers or a list of key-value pairs. If key-value pairs are used, the key represents the axis ID and the value represents the data point. Else, the data points are assumed to be in the order of the axes defined.
Multiple curves can be defined in a single line.
```
radar-beta
axis axis1, axis2, axis3
curve id1["Label1"]{1, 2, 3}
curve id2["Label2"]{4, 5, 6}, id3{7, 8, 9}
curve id4{ axis3: 30, axis1: 20, axis2: 10 }
...
```
### Options
- `showLegend`: The showLegend keyword is used to show or hide the legend in the radar diagram. The legend is shown by default.
- `max`: The maximum value for the radar diagram. This is used to scale the radar diagram. If not provided, the maximum value is calculated from the data points.
- `min`: The minimum value for the radar diagram. This is used to scale the radar diagram. If not provided, the minimum value is `0`.
- `graticule`: The graticule keyword is used to define the type of graticule to be rendered in the radar diagram. The graticule can be `circle` or `polygon`. If not provided, the default graticule is `circle`.
- `ticks`: The ticks keyword is used to define the number of ticks on the graticule. It is the number of concentric circles or polygons drawn to indicate the scale of the radar diagram. If not provided, the default number of ticks is `5`.
```
radar-beta
...
showLegend true
max 100
min 0
graticule circle
ticks 5
...
```
## Configuration
Please refer to the [configuration](/config/schema-docs/config-defs-radar-diagram-config.html) guide for details.
| Parameter | Description | Default Value |
| --------------- | ---------------------------------------- | ------------- |
| width | Width of the radar diagram | `600` |
| height | Height of the radar diagram | `600` |
| marginTop | Top margin of the radar diagram | `50` |
| marginBottom | Bottom margin of the radar diagram | `50` |
| marginLeft | Left margin of the radar diagram | `50` |
| marginRight | Right margin of the radar diagram | `50` |
| axisScaleFactor | Scale factor for the axis | `1` |
| axisLabelFactor | Factor to adjust the axis label position | `1.05` |
| curveTension | Tension for the rounded curves | `0.17` |
## Theme Variables
### Global Theme Variables
```note
The default values for these variables depend on the theme used. To override the default values, set the desired values in the themeVariables section of the configuration:
%%{init: {"themeVariables": {"cScale0": "#FF0000", "cScale1": "#00FF00"}} }%%
```
Radar charts support the color scales `cScale${i}` where `i` is a number from `0` to the theme's maximum number of colors in its color scale. Usually, the maximum number of colors is `12`.
| Property | Description |
| ---------- | ------------------------------ |
| fontSize | Font size of the title |
| titleColor | Color of the title |
| cScale${i} | Color scale for the i-th curve |
### Radar Style Options
```note
Specific variables for radar resides inside the `radar` key. To set the radar style options, use this syntax.
%%{init: {"themeVariables": {"radar": {"axisColor": "#FF0000"}} } }%%
```
| Property | Description | Default Value |
| -------------------- | ---------------------------- | ------------- |
| axisColor | Color of the axis lines | `black` |
| axisStrokeWidth | Width of the axis lines | `1` |
| axisLabelFontSize | Font size of the axis labels | `12px` |
| curveOpacity | Opacity of the curves | `0.7` |
| curveStrokeWidth | Width of the curves | `2` |
| graticuleColor | Color of the graticule | `black` |
| graticuleOpacity | Opacity of the graticule | `0.5` |
| graticuleStrokeWidth | Width of the graticule | `1` |
| legendBoxSize | Size of the legend box | `10` |
| legendFontSize | Font size of the legend | `14px` |
## Example on config and theme
```mermaid-example
---
config:
radar:
axisScaleFactor: 0.25
curveTension: 0.1
theme: base
themeVariables:
cScale0: "#FF0000"
cScale1: "#00FF00"
cScale2: "#0000FF"
radar:
curveOpacity: 0
---
radar-beta
axis A, B, C, D, E
curve c1{1,2,3,4,5}
curve c2{5,4,3,2,1}
curve c3{3,3,3,3,3}
```
<!--- cspell:ignore Kiviat --->

View File

@@ -96,7 +96,7 @@ xychart-beta
## Chart Configurations
| Parameter | Description | Default value |
| ------------------------ | ---------------------------------------------- | :-----------: |
| ------------------------ | ------------------------------------------------------------- | :-----------: |
| width | Width of the chart | 700 |
| height | Height of the chart | 500 |
| titlePadding | Top and Bottom padding of the title | 10 |
@@ -106,6 +106,7 @@ xychart-beta
| yAxis | yAxis configuration | AxisConfig |
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
| showDataLabel | Should show the value corresponding to the bar within the bar | false |
### AxisConfig
@@ -152,6 +153,7 @@ config:
xyChart:
width: 900
height: 600
showDataLabel: true
themeVariables:
xyChart:
titleColor: "#ff0000"

View File

@@ -30,6 +30,8 @@ vi.mock('./diagrams/packet/renderer.js');
vi.mock('./diagrams/xychart/xychartRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.js');
vi.mock('./diagrams/sequence/sequenceRenderer.js');
vi.mock('./diagrams/radar/renderer.js');
vi.mock('./diagrams/architecture/architectureRenderer.js');
// -------------------------------------
@@ -797,6 +799,8 @@ graph TD;A--x|text including URL space|B;`)
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
{ textDiagramType: 'radar-beta', expectedType: 'radar' },
{ textDiagramType: 'architecture-beta', expectedType: 'architecture' },
];
describe('accessibility', () => {

View File

@@ -346,6 +346,7 @@ export const render = async (data4Layout, svg) => {
edge1.label = '';
edge1.arrowTypeEnd = 'none';
edge1.id = nodeId + '-cyclic-special-1';
edgeMid.arrowTypeStart = 'none';
edgeMid.arrowTypeEnd = 'none';
edgeMid.id = nodeId + '-cyclic-special-mid';
edge2.label = '';
@@ -354,6 +355,7 @@ export const render = async (data4Layout, svg) => {
edge2.toCluster = nodeId;
}
edge2.id = nodeId + '-cyclic-special-2';
edge2.arrowTypeStart = 'none';
graph.setEdge(nodeId, specialId1, edge1, nodeId + '-cyclic-special-0');
graph.setEdge(specialId1, specialId2, edgeMid, nodeId + '-cyclic-special-1');
graph.setEdge(specialId2, nodeId, edge2, nodeId + '-cyc<lic-special-2');

View File

@@ -15,28 +15,33 @@ export const addEdgeMarkers = (
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
url: string,
id: string,
diagramType: string
diagramType: string,
strokeColor?: string
) => {
if (edge.arrowTypeStart) {
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType, strokeColor);
}
if (edge.arrowTypeEnd) {
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType, strokeColor);
}
};
const arrowTypesMap = {
arrow_cross: 'cross',
arrow_point: 'point',
arrow_barb: 'barb',
arrow_circle: 'circle',
aggregation: 'aggregation',
extension: 'extension',
composition: 'composition',
dependency: 'dependency',
lollipop: 'lollipop',
requirement_arrow: 'requirement_arrow',
requirement_contains: 'requirement_contains',
arrow_cross: { type: 'cross', fill: false },
arrow_point: { type: 'point', fill: true },
arrow_barb: { type: 'barb', fill: true },
arrow_circle: { type: 'circle', fill: false },
aggregation: { type: 'aggregation', fill: false },
extension: { type: 'extension', fill: false },
composition: { type: 'composition', fill: true },
dependency: { type: 'dependency', fill: true },
lollipop: { type: 'lollipop', fill: false },
only_one: { type: 'onlyOne', fill: false },
zero_or_one: { type: 'zeroOrOne', fill: false },
one_or_more: { type: 'oneOrMore', fill: false },
zero_or_more: { type: 'zeroOrMore', fill: false },
requirement_arrow: { type: 'requirement_arrow', fill: false },
requirement_contains: { type: 'requirement_contains', fill: false },
} as const;
const addEdgeMarker = (
@@ -45,15 +50,55 @@ const addEdgeMarker = (
arrowType: string,
url: string,
id: string,
diagramType: string
diagramType: string,
strokeColor?: string
) => {
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
const arrowTypeInfo = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
if (!endMarkerType) {
if (!arrowTypeInfo) {
log.warn(`Unknown arrow type: ${arrowType}`);
return; // unknown arrow type, ignore
}
const endMarkerType = arrowTypeInfo.type;
const suffix = position === 'start' ? 'Start' : 'End';
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
const originalMarkerId = `${id}_${diagramType}-${endMarkerType}${suffix}`;
// If stroke color is specified and non-empty, create or use a colored variant of the marker
if (strokeColor && strokeColor.trim() !== '') {
// Create a sanitized color value for use in IDs
const colorId = strokeColor.replace(/[^\dA-Za-z]/g, '_');
const coloredMarkerId = `${originalMarkerId}_${colorId}`;
// Check if the colored marker already exists
if (!document.getElementById(coloredMarkerId)) {
// Get the original marker
const originalMarker = document.getElementById(originalMarkerId);
if (originalMarker) {
// Clone the marker and create colored version
const coloredMarker = originalMarker.cloneNode(true) as Element;
coloredMarker.id = coloredMarkerId;
// Apply colors to the paths inside the marker
const paths = coloredMarker.querySelectorAll('path, circle, line');
paths.forEach((path) => {
path.setAttribute('stroke', strokeColor);
// Apply fill only to markers that should be filled
if (arrowTypeInfo.fill) {
path.setAttribute('fill', strokeColor);
}
});
// Add the new colored marker to the defs section
originalMarker.parentNode?.appendChild(coloredMarker);
}
}
// Use the colored marker
svgPath.attr(`marker-${position}`, `url(${url}#${coloredMarkerId})`);
} else {
// Always use the original marker for unstyled edges
svgPath.attr(`marker-${position}`, `url(${url}#${originalMarkerId})`);
}
};

Some files were not shown because too many files have changed in this diff Show More