From 03bbfa0b9467ac79d91542760b7bbe624eb4c042 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Mon, 11 Mar 2024 13:37:47 -0500 Subject: [PATCH 01/46] build: added cytoscape-fcose --- packages/mermaid/package.json | 2 ++ pnpm-lock.yaml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 3626671ca5..ad2a708d2e 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -64,6 +64,7 @@ "@types/d3-scale-chromatic": "^3.0.0", "cytoscape": "^3.28.1", "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", "d3": "^7.4.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.10", @@ -83,6 +84,7 @@ "devDependencies": { "@adobe/jsonschema2md": "^7.1.4", "@types/cytoscape": "^3.19.9", + "@types/cytoscape-fcose": "^2.2.4", "@types/d3": "^7.4.0", "@types/d3-sankey": "^0.12.1", "@types/d3-scale": "^4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faac50fae9..0853a8d390 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,6 +214,9 @@ importers: cytoscape-cose-bilkent: specifier: ^4.1.0 version: 4.1.0(cytoscape@3.28.1) + cytoscape-fcose: + specifier: ^2.2.0 + version: 2.2.0(cytoscape@3.28.1) d3: specifier: ^7.4.0 version: 7.8.5 @@ -266,6 +269,9 @@ importers: '@types/cytoscape': specifier: ^3.19.9 version: 3.19.16 + '@types/cytoscape-fcose': + specifier: ^2.2.4 + version: 2.2.4 '@types/d3': specifier: ^7.4.0 version: 7.4.3 @@ -4811,6 +4817,12 @@ packages: '@types/node': 20.11.24 dev: true + /@types/cytoscape-fcose@2.2.4: + resolution: {integrity: sha512-QwWtnT8HI9h+DHhG5krGc1ZY0Ex+cn85MvX96ZNAjSxuXiZDnjIZW/ypVkvvubTjIY4rSdkJY1D/Nsn8NDpmAw==} + dependencies: + '@types/cytoscape': 3.19.16 + dev: true + /@types/cytoscape@3.19.16: resolution: {integrity: sha512-A3zkjaZ6cOGyqEvrVuC1YUgiRSJhDZOj8Qhd1ALH2/+YxH2za1BOmR4RWQsKYHsc+aMP/IWoqg1COuUbZ39t/g==} dev: true @@ -7885,6 +7897,12 @@ packages: layout-base: 1.0.2 dev: false + /cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + dependencies: + layout-base: 2.0.1 + dev: false + /cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.2)(typescript@5.3.3): resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} engines: {node: '>=v14.21.3'} @@ -8230,6 +8248,15 @@ packages: cytoscape: 3.28.1(patch_hash=claipxynndhyqyu2csninuoh5e) dev: false + /cytoscape-fcose@2.2.0(cytoscape@3.28.1): + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 2.2.0 + cytoscape: 3.28.1(patch_hash=claipxynndhyqyu2csninuoh5e) + dev: false + /cytoscape@3.28.1(patch_hash=claipxynndhyqyu2csninuoh5e): resolution: {integrity: sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==} engines: {node: '>=0.10'} @@ -12125,6 +12152,10 @@ packages: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} dev: false + /layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + dev: false + /lazy-ass@1.6.0: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} From 5a4d4972e22828de1b6dc630e987b525efdb8ca2 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Mon, 11 Mar 2024 13:41:43 -0500 Subject: [PATCH 02/46] feat: added architecture diagram parser --- .../architecture/parser/architecture.jison | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 packages/mermaid/src/diagrams/architecture/parser/architecture.jison diff --git a/packages/mermaid/src/diagrams/architecture/parser/architecture.jison b/packages/mermaid/src/diagrams/architecture/parser/architecture.jison new file mode 100644 index 0000000000..d92a1d1dbc --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/parser/architecture.jison @@ -0,0 +1,119 @@ +%lex +%options case-insensitive + +%x GROUP SERVICE LINE +%% + +\%\%(?!\{)[^\n]* /* skip comments */ +[^\}]\%\%[^\n]* /* skip comments */ +[\n\r]+ { this.popState(); return 'NEWLINE'; } +\%\%[^\n]* /* do nothing */ +[\s]+ /* skip all whitespace */ +"architecture" return 'ARCHITECTURE'; +"service" { this.begin('SERVICE'); return 'SERVICE'; } +"group" { this.begin('GROUP'); return 'GROUP'; } +((?!\n)\s)+ /* skip same-line whitespace */ +"in" return 'IN'; +[\w]+ return 'id'; +\([\w]*\) return 'icon'; +\[[\w ]*\] return 'title'; +[\w]+ { this.begin('LINE'); return 'id'; } +\<[L|R|T|B]"-" return 'ARROW_LEFT_INTO'; +[L|R|T|B]"-" return 'ARROW_LEFT'; +"-"[L|R|T|B]\> return 'ARROW_RIGHT_INTO'; +"-"[L|R|T|B] return 'ARROW_RIGHT'; +[\w]+ return 'id'; +\[[\w ]*\] return 'title'; +<> return 'EOF'; + +/lex + +%start start + +%% + +start + : eol start + | ARCHITECTURE document { return $2; } + ; + +document + : /* empty */ + | document line + ; + +line + : statement eol { $$ = $1 } + ; + +statement + : /* empty */ + | group_statement + | service_statement + | line_statement + ; + +line_statement + : id ARROW_LEFT_INTO ARROW_RIGHT_INTO id + { yy.addLine($1, $2[1], $4, $3[1], {lhs_into: true, rhs_into: true}) } + | id ARROW_LEFT_INTO ARROW_RIGHT id + { yy.addLine($1, $2[1], $4, $3[1], {lhs_into: true}) } + | id ARROW_LEFT ARROW_RIGHT_INTO id + { yy.addLine($1, $2[0], $4, $3[1], {rhs_into: true}) } + | id ARROW_LEFT ARROW_RIGHT id + { yy.addLine($1, $2[0], $4, $3[1]) } + | id ARROW_LEFT_INTO title ARROW_RIGHT_INTO id + { yy.addLine($1, $2[1], $5, $4[1], { title: $3.slice(1,-1), lhs_into: true, rhs_into: true }) } + | id ARROW_LEFT_INTO title ARROW_RIGHT id + { yy.addLine($1, $2[1], $5, $4[1], { title: $3.slice(1,-1), lhs_into: true }) } + | id ARROW_LEFT title ARROW_RIGHT_INTO id + { yy.addLine($1, $2[0], $5, $4[1], { title: $3.slice(1,-1), rhs_into: true }) } + | id ARROW_LEFT title ARROW_RIGHT id + { yy.addLine($1, $2[0], $5, $4[1], { title: $3.slice(1,-1) }) } + ; + +group_statement + : 'GROUP' id + { yy.addGroup($2) } + | 'GROUP' id icon + { yy.addGroup($2, {icon: $3.slice(1,-1)}) } + | 'GROUP' id title + { yy.addGroup($2, {title: $3.slice(1,-1)}) } + | 'GROUP' id icon title + { yy.addGroup($2, {icon: $3.slice(1,-1), title: $4.slice(1,-1)}) } + | 'GROUP' id 'IN' id + { yy.addGroup($2, {in: $4.trim()}) } + | 'GROUP' id icon 'IN' id + { yy.addGroup($2, {icon: $3.slice(1,-1), in: $5.trim()}) } + | 'GROUP' id title 'IN' id + { yy.addGroup($2, {title: $3.slice(1,-1), in: $5.trim()}) } + | 'GROUP' id icon title 'IN' id + { yy.addGroup($2, {icon: $3.slice(1,-1), title: $4.slice(1,-1), in: $6.trim()}) } + ; + +service_statement + : 'SERVICE' id + { yy.addService($2) } + | 'SERVICE' id icon + { yy.addService($2, { icon: $3.slice(1,-1) }) } + | 'SERVICE' id title + { yy.addService($2, { title: $3.slice(1,-1) }) } + | 'SERVICE' id icon title + { yy.addService($2, { icon: $3.slice(1,-1), title: $4.slice(1,-1) }) } + | 'SERVICE' id 'IN' id + { yy.addService($2, { in: $4.trim() }) } + | 'SERVICE' id icon 'IN' id + { yy.addService($2, { icon: $3.slice(1,-1), in: $5.trim() }) } + | 'SERVICE' id title 'IN' id + { yy.addService($2, { title: $3.slice(1,-1), in: $5.trim() }) } + | 'SERVICE' id icon title 'IN' id + { yy.addService($2, { icon: $3.slice(1,-1), title: $4.slice(1,-1), in: $6.trim() }) } + ; + +eol + : NEWLINE + | ';' + | EOF + ; + +%% \ No newline at end of file From 346ae22108c45ee0b204897d153c6929fcedcfb4 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Mon, 11 Mar 2024 15:13:05 -0500 Subject: [PATCH 03/46] feat: registered architecture diagram --- .vite/jsonSchemaPlugin.ts | 1 + .../scripts/create-types-from-json-schema.mts | 1 + packages/mermaid/src/config.type.ts | 11 +++++++++ .../src/diagram-api/diagram-orchestration.ts | 4 +++- .../architecture/architectureDetector.ts | 24 +++++++++++++++++++ .../mermaid/src/schemas/config.schema.yaml | 21 ++++++++++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 packages/mermaid/src/diagrams/architecture/architectureDetector.ts diff --git a/.vite/jsonSchemaPlugin.ts b/.vite/jsonSchemaPlugin.ts index dd9af8cc55..a69f6889b3 100644 --- a/.vite/jsonSchemaPlugin.ts +++ b/.vite/jsonSchemaPlugin.ts @@ -26,6 +26,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'c4', 'sankey', 'block', + 'architecture' ] as const; /** diff --git a/packages/mermaid/scripts/create-types-from-json-schema.mts b/packages/mermaid/scripts/create-types-from-json-schema.mts index b028fe818d..fbd12a37ff 100644 --- a/packages/mermaid/scripts/create-types-from-json-schema.mts +++ b/packages/mermaid/scripts/create-types-from-json-schema.mts @@ -54,6 +54,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'gitGraph', 'c4', 'sankey', + 'architecture' ]; /** diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 0ba3178680..014ff65e77 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -165,6 +165,7 @@ export interface MermaidConfig { quadrantChart?: QuadrantChartConfig; xyChart?: XYChartConfig; requirement?: RequirementDiagramConfig; + architecture?: ArchitectureDiagramConfig; mindmap?: MindmapDiagramConfig; gitGraph?: GitGraphDiagramConfig; c4?: C4DiagramConfig; @@ -629,6 +630,16 @@ export interface RequirementDiagramConfig extends BaseDiagramConfig { rect_padding?: number; line_height?: number; } +/** + * The object containing configurations specific for architecture diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "ArchitectureDiagramConfig". + */ +export interface ArchitectureDiagramConfig extends BaseDiagramConfig { + padding?: number; + maxNodeWidth?: number; +} /** * The object containing configurations specific for mindmap diagrams * diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index eb123c4a20..84697022ca 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -21,6 +21,7 @@ import timeline from '../diagrams/timeline/detector.js'; import mindmap from '../diagrams/mindmap/detector.js'; import sankey from '../diagrams/sankey/sankeyDetector.js'; import block from '../diagrams/block/blockDetector.js'; +import architecture from '../diagrams/architecture/architectureDetector.js' import { registerLazyLoadedDiagrams } from './detectType.js'; import { registerDiagram } from './diagramAPI.js'; @@ -89,6 +90,7 @@ export const addDiagrams = () => { quadrantChart, sankey, xychart, - block + block, + architecture ); }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureDetector.ts b/packages/mermaid/src/diagrams/architecture/architectureDetector.ts new file mode 100644 index 0000000000..c15b474abc --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureDetector.ts @@ -0,0 +1,24 @@ +import type { + DiagramDetector, + DiagramLoader, + ExternalDiagramDefinition, +} from '../../diagram-api/types.js'; + +const id = 'architecture'; + +const detector: DiagramDetector = (txt) => { + return /^\s*architecture/.test(txt); +}; + +const loader: DiagramLoader = async () => { + const { diagram } = await import('./architectureDiagram.js'); + return { id, diagram }; +}; + +const architecture: ExternalDiagramDefinition = { + id, + detector, + loader, +}; + +export default architecture; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 3e7fd58ec5..601939beed 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -45,6 +45,7 @@ required: - quadrantChart - xyChart - requirement + - architecture - mindmap - gitGraph - c4 @@ -217,6 +218,8 @@ properties: $ref: '#/$defs/XYChartConfig' requirement: $ref: '#/$defs/RequirementDiagramConfig' + architecture: + $ref: '#/$defs/ArchitectureDiagramConfig' mindmap: $ref: '#/$defs/MindmapDiagramConfig' gitGraph: @@ -853,6 +856,24 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) type: number default: 20 + ArchitectureDiagramConfig: + title: Architecture Diagram Config + allOf: [{ $ref: '#/$defs/BaseDiagramConfig'}] + description: The object containing configurations specific for architecture diagrams + type: object + unevaluatedProperties: false + required: + - useMaxWidth + - padding + - maxNodeWidth + properties: + padding: + type: number + default: 10 + maxNodeWidth: + type: number + default: 200 + MindmapDiagramConfig: title: Mindmap Diagram Config allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }] From cc22e13e71423491926d413199702a3d85ebff16 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Mon, 11 Mar 2024 15:14:21 -0500 Subject: [PATCH 04/46] feat: added architecture DB & types --- .../diagrams/architecture/architectureDb.ts | 115 ++++++++++++++++++ .../architecture/architectureTypes.ts | 64 ++++++++++ 2 files changed, 179 insertions(+) create mode 100644 packages/mermaid/src/diagrams/architecture/architectureDb.ts create mode 100644 packages/mermaid/src/diagrams/architecture/architectureTypes.ts diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts new file mode 100644 index 0000000000..5aecba746d --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -0,0 +1,115 @@ +import type { ArchitectureFields, ArchitectureDB, ArchitectureService, ArchitectureGroup, ArchitectureDirection, ArchitectureLine } from "./architectureTypes.js"; +import { isArchitectureDirection } from "./architectureTypes.js"; +import { + setAccTitle, + getAccTitle, + setDiagramTitle, + getDiagramTitle, + getAccDescription, + setAccDescription, + clear as commonClear, +} from '../common/commonDb.js'; +import type { ArchitectureDiagramConfig } from '../../config.type.js'; +import DEFAULT_CONFIG from '../../defaultConfig.js'; +import type { D3Element } from "../../mermaidAPI.js"; + +export const DEFAULT_ARCHITECTURE_CONFIG: Required = DEFAULT_CONFIG.architecture; +export const DEFAULT_ARCHITECTURE_DB: ArchitectureFields = { + services: [], + groups: [], + lines: [], + cnt: 0, + config: DEFAULT_ARCHITECTURE_CONFIG, +} as const; + +let services = DEFAULT_ARCHITECTURE_DB.services; +let groups = DEFAULT_ARCHITECTURE_DB.groups; +let lines = DEFAULT_ARCHITECTURE_DB.lines; +let elements: Record = {}; +let cnt = DEFAULT_ARCHITECTURE_DB.cnt; + +const config: Required = structuredClone(DEFAULT_ARCHITECTURE_CONFIG); + +const getConfig = (): Required => structuredClone(config); + +const clear = (): void => { + services = structuredClone(DEFAULT_ARCHITECTURE_DB.services); + groups = structuredClone(DEFAULT_ARCHITECTURE_DB.groups); + lines = structuredClone(DEFAULT_ARCHITECTURE_DB.lines); + elements = {}; + cnt = 0; + commonClear(); +}; + +const addService = function (id: string, opts: Omit = {}) { + const {icon, in: inside, title} = opts; + services.push({ + id, + icon, + title, + in: inside + }); +} +const getServices = (): ArchitectureService[] => services; + +const addGroup = function (id: string, opts: Omit = {}) { + const {icon, in: inside, title} = opts; + groups.push({ + id, + icon, + title, + in: inside + }); +} +const getGroups = (): ArchitectureGroup[] => groups; + + +const addLine = function (lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id: string, rhs_dir: ArchitectureDirection, opts: Omit = {}) { + const {title, lhs_into, rhs_into} = opts; + + if (!isArchitectureDirection(lhs_dir)) { + throw new Error(`Invalid direction given for left hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${lhs_dir}`) + } + if (!isArchitectureDirection(rhs_dir)) { + throw new Error(`Invalid direction given for right hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${rhs_dir}`) + } + + lines.push({ + lhs_id, + lhs_dir, + rhs_id, + rhs_dir, + title, + lhs_into, + rhs_into + }); +} +const getLines = (): ArchitectureLine[] => lines; + + +const setElementForId = (id: string, element: D3Element) => { + elements[id] = element; +}; +const getElementById = (id: string) => elements[id]; + + + +export const db: ArchitectureDB = { + getConfig, + clear, + setDiagramTitle, + getDiagramTitle, + setAccTitle, + getAccTitle, + setAccDescription, + getAccDescription, + + addService, + getServices, + addGroup, + getGroups, + addLine, + getLines, + setElementForId, + getElementById +}; \ No newline at end of file diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts new file mode 100644 index 0000000000..dbc663b73d --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -0,0 +1,64 @@ +import type { DiagramDB } from '../../diagram-api/types.js'; +import type { ArchitectureDiagramConfig } from '../../config.type.js'; +import type { D3Element } from '../../mermaidAPI.js'; + +export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B' +export const isArchitectureDirection = function(x: unknown): x is ArchitectureDirection { + const temp = x as ArchitectureDirection; + return (temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B') +} +export const isArchitectureDirectionX = function(x: ArchitectureDirection): x is Extract { + const temp = x as Extract + return (temp === 'L' || temp === 'R') +} +export const isArchitectureDirectionY = function(x: ArchitectureDirection): x is Extract { + const temp = x as Extract + return (temp === 'T' || temp === 'B') +} + +export interface ArchitectureStyleOptions { + fontFamily: string; +} + +export interface ArchitectureService { + id: string; + icon?: string; + title?: string; + in?: string; +} + +export interface ArchitectureGroup { + id: string; + icon?: string; + title?: string; + in?: string; +} + +export interface ArchitectureLine { + lhs_id: string; + lhs_dir: ArchitectureDirection; + title?: string; + rhs_id: string; + rhs_dir: ArchitectureDirection; + lhs_into?: boolean; + rhs_into?: boolean; +} + +export interface ArchitectureDB extends DiagramDB { + addService: (id: string, opts: Omit) => void + getServices: () => ArchitectureService[] + addGroup: (id: string, opts: Omit) => void + getGroups: () => ArchitectureGroup[] + addLine: (lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id: string, rhs_dir: ArchitectureDirection, opts: Omit) => void + getLines: () => ArchitectureLine[] + setElementForId: (id: string, element: D3Element) => void; + getElementById: (id: string) => D3Element; +} + +export interface ArchitectureFields { + services: ArchitectureService[], + groups: ArchitectureGroup[], + lines: ArchitectureLine[], + cnt: number, + config: ArchitectureDiagramConfig +} \ No newline at end of file From e01acec12b631a1385aeddc354b6db947b37b0ae Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Wed, 13 Mar 2024 09:24:04 -0500 Subject: [PATCH 05/46] feat(arch): implemented icon registration --- .../mermaid/src/rendering-util/svgRegister.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/mermaid/src/rendering-util/svgRegister.ts diff --git a/packages/mermaid/src/rendering-util/svgRegister.ts b/packages/mermaid/src/rendering-util/svgRegister.ts new file mode 100644 index 0000000000..303c43f579 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svgRegister.ts @@ -0,0 +1,33 @@ +import { Selection } from "d3-selection"; + +type IconResolver = (parent: Selection) => Selection +type IconLibrary = Record + +const icons: IconLibrary = {} + +const isIconNameInUse = (name: string): boolean => { + return icons[name] !== undefined; +} + +const registerIcon = (name: string, resolver: IconResolver) => { + if(!isIconNameInUse(name)) { + icons[name] = resolver; + } +} + +const registerIcons = (library: IconLibrary) => { + Object.entries(library).forEach(([name, resolver]) => { + if (!isIconNameInUse(name)) { + icons[name] = resolver; + } + }) +} + +const getIcon = (name: string): IconResolver | null => { + if (isIconNameInUse(name)) { + return icons[name]; + } + return null; // TODO: return default +} + +export { registerIcon, registerIcons, getIcon, isIconNameInUse, IconLibrary } \ No newline at end of file From 6c6ce28f7df4d52891d341051a9219e768accead Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Wed, 13 Mar 2024 09:25:20 -0500 Subject: [PATCH 06/46] feat(arch): implemented basic rendering for diagram --- .../architecture/architectureDiagram.ts | 13 ++ .../architecture/architectureRenderer.ts | 216 ++++++++++++++++++ .../architecture/architectureStyles.ts | 73 ++++++ .../src/diagrams/architecture/svgDraw.ts | 120 ++++++++++ 4 files changed, 422 insertions(+) create mode 100644 packages/mermaid/src/diagrams/architecture/architectureDiagram.ts create mode 100644 packages/mermaid/src/diagrams/architecture/architectureRenderer.ts create mode 100644 packages/mermaid/src/diagrams/architecture/architectureStyles.ts create mode 100644 packages/mermaid/src/diagrams/architecture/svgDraw.ts diff --git a/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts new file mode 100644 index 0000000000..614e2a7f3a --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts @@ -0,0 +1,13 @@ +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types +import parser from './parser/architecture.jison'; +import { db } from './architectureDb.js'; +import styles from './architectureStyles.js'; +import { renderer } from './architectureRenderer.js'; + +export const diagram: DiagramDefinition = { + parser, + db, + renderer, + styles, +}; diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts new file mode 100644 index 0000000000..a55b23bffd --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -0,0 +1,216 @@ +import cytoscape from 'cytoscape'; +import type { Diagram } from '../../Diagram.js'; +import fcose, {FcoseLayoutOptions} from 'cytoscape-fcose'; +import type { MermaidConfig } from '../../config.type.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; +import type { DrawDefinition, SVG } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { + isArchitectureDirectionX, + type ArchitectureDB, + type ArchitectureDirection, + type ArchitectureGroup, + type ArchitectureLine, + type ArchitectureService, + isArchitectureDirectionY, +} from './architectureTypes.js'; +import { select } from 'd3'; +import { setupGraphViewbox } from '../../setupGraphViewbox.js'; +import defaultConfig from '../../defaultConfig.js'; +import type { D3Element } from '../../mermaidAPI.js'; +import { drawEdges, drawService, getEdgeThicknessCallback } from './svgDraw.js'; + +cytoscape.use(fcose); + +function addServices(services: ArchitectureService[], cy: cytoscape.Core) { + services.forEach((service) => { + cy.add({ + group: 'nodes', + data: { + id: service.id, + icon: service.icon, + title: service.title, + parent: service.in, + // TODO: dynamic size + width: 80, + height: 80 + }, + }); + }); +} + +function drawServices( + db: ArchitectureDB, + svg: D3Element, + services: ArchitectureService[], + conf: MermaidConfig +) { + services.forEach((service) => drawService(db, svg, service, conf)); +} + +function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { + groups.forEach((group) => { + cy.add({ + group: 'nodes', + data: { + id: group.id, + icon: group.icon, + title: group.title, + parent: group.in + }, + }); + }); +} + +function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { + cy.nodes().map((node, id) => { + + const data = node.data(); + data.x = node.position().x; + data.y = node.position().y; + + const nodeElem = db.getElementById(data.id); + nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')'); + }); +} + +function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) { + lines.forEach((line) => { + cy.add({ + group: 'edges', + data: { + id: `${line.lhs_id}-${line.rhs_id}`, + source: line.lhs_id, + sourceDir: line.lhs_dir, + target: line.rhs_id, + targetDir: line.rhs_dir, + }, + }); + }); +} + +function layoutArchitecture( + services: ArchitectureService[], + groups: ArchitectureGroup[], + lines: ArchitectureLine[] +): Promise { + return new Promise((resolve) => { + const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none'); + const cy = cytoscape({ + container: document.getElementById('cy'), + style: [ + { + selector: 'edge', + style: { + 'curve-style': 'straight', + 'source-endpoint': '100% 100%', + 'target-endpoint': '100% 100%', + }, + }, + ], + }); + // Remove element after layout + renderEl.remove(); + + addGroups(groups, cy); + addServices(services, cy); + addEdges(lines, cy); + + // Make cytoscape care about the dimensions of the nodes + cy.nodes().forEach(function (n) { + n.layoutDimensions = () => { + const data = n.data(); + return { w: data.width, h: data.height }; + }; + }); + + cy.layout({ + name: 'fcose', + quality: 'proof', + styleEnabled: false, + animate: false, + alignmentConstraint: { + horizontal: cy + .edges() + .filter( + (edge) => + isArchitectureDirectionX(edge.data('sourceDir')) && + isArchitectureDirectionX(edge.data('targetDir')) + ) + .map((edge) => [edge.data('source'), edge.data('target')]), + vertical: cy + .edges() + .filter( + (edge) => + isArchitectureDirectionY(edge.data('sourceDir')) && + isArchitectureDirectionY(edge.data('targetDir')) + ) + .map((edge) => [edge.data('source'), edge.data('target')]), + }, + relativePlacementConstraint: cy.edges().map((edge) => { + const sourceDir = edge.data('sourceDir') as ArchitectureDirection; + const targetDir = edge.data('targetDir') as ArchitectureDirection; + const sourceId = edge.data('source') as ArchitectureDirection; + const targetId = edge.data('target') as ArchitectureDirection; + + if ( + isArchitectureDirectionX(sourceDir) && + isArchitectureDirectionX(targetDir) + ) { + return {left: sourceDir === 'L' ? sourceId : targetId, right: sourceDir === 'R' ? sourceId : targetId, gap: 180} + } else if ( + isArchitectureDirectionY(sourceDir) && + isArchitectureDirectionY(targetDir) + ) { + return {top: sourceDir === 'T' ? sourceId : targetId, bottom: sourceDir === 'B' ? sourceId : targetId, gap: 180} + } + // TODO: fallback case + RB, TL, etc + + }), + } as FcoseLayoutOptions).run(); + cy.ready((e) => { + log.info('Ready', e); + resolve(cy); + }); + }); +} + +export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) => { + const db = diagObj.db as ArchitectureDB; + const conf: MermaidConfig = getConfig(); + + const services = db.getServices(); + const groups = db.getGroups(); + const lines = db.getLines(); + log.info('Services: ', services); + log.info('Lines: ', lines); + + const svg: SVG = selectSvgElement(id); + + const edgesElem = svg.append('g'); + edgesElem.attr('class', 'architecture-edges'); + + const servicesElem = svg.append('g'); + servicesElem.attr('class', 'architecture-services'); + + drawServices(db, servicesElem, services, conf); + const getEdgeThickness = getEdgeThicknessCallback(svg); + + const cy = await layoutArchitecture(services, groups, lines); + + const edgeThickness = getEdgeThickness(); + drawEdges(edgesElem, edgeThickness, cy); + positionServices(db, cy); + + setupGraphViewbox( + undefined, + svg, + conf.architecture?.padding ?? defaultConfig.architecture.padding, + conf.architecture?.useMaxWidth ?? defaultConfig.architecture.useMaxWidth + ); + + +}; + +export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureStyles.ts b/packages/mermaid/src/diagrams/architecture/architectureStyles.ts new file mode 100644 index 0000000000..b92d227e63 --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureStyles.ts @@ -0,0 +1,73 @@ +import type { DiagramStylesProvider } from '../../diagram-api/types.js'; +import type { ArchitectureStyleOptions } from './architectureTypes.js'; +// @ts-expect-error Incorrect khroma types +import { darken, lighten, isDark } from 'khroma'; + +const genSections: DiagramStylesProvider = (options) => { + let sections = ''; + + for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) { + options['lineColor' + i] = options['lineColor' + i] || options['cScaleInv' + i]; + if (isDark(options['lineColor' + i])) { + options['lineColor' + i] = lighten(options['lineColor' + i], 20); + } else { + options['lineColor' + i] = darken(options['lineColor' + i], 20); + } + } + + for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) { + const sw = '' + (17 - 3 * i); + sections += ` + .section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${ + i - 1 + } polygon, .section-${i - 1} path { + fill: ${options['cScale' + i]}; + } + .section-${i - 1} text { + fill: ${options['cScaleLabel' + i]}; + } + .node-icon-${i - 1} { + font-size: 40px; + color: ${options['cScaleLabel' + i]}; + } + .section-edge-${i - 1}{ + stroke: ${options['cScale' + i]}; + } + .edge-depth-${i - 1}{ + stroke-width: ${sw}; + } + .section-${i - 1} line { + stroke: ${options['cScaleInv' + i]} ; + stroke-width: 3; + } + + .disabled, .disabled circle, .disabled text { + fill: lightgray; + } + .disabled text { + fill: #efefef; + } + `; + } + return sections; +}; + +const getStyles: DiagramStylesProvider = (options: ArchitectureStyleOptions) => + ` + .edge { + stroke-width: 3; + stroke: #777; + } + .section-root rect, .section-root path, .section-root circle, .section-root polygon { + fill: #333; + } + .section-root text { + fill:#333; + } + ${genSections(options)} + .edge { + fill: none; + } +`; + +export default getStyles; diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts new file mode 100644 index 0000000000..55742ed7fe --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -0,0 +1,120 @@ +import type { D3Element } from '../../mermaidAPI.js'; +import { createText } from '../../rendering-util/createText.js'; +import type { ArchitectureDB, ArchitectureService } from './architectureTypes.js'; +import type { MermaidConfig } from '../../config.type.js'; +import type cytoscape from 'cytoscape'; +import { log } from '../../logger.js'; +import {getIcon, isIconNameInUse} from '../../rendering-util/svgRegister.js'; + +declare module 'cytoscape' { + interface EdgeSingular { + _private: { + bodyBounds: unknown; + rscratch: { + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + }; + }; + } +} + +/** + * Creates a temporary path which can be used to compute the line thickness. + * @param root root element to add the temporary path to + * @returns callback function which gets the bounding box dimensions and removes the path from root + */ +export const getEdgeThicknessCallback = function (root: D3Element) { + const tempPath = root.insert('path') + .attr( + 'd', + `M 10,10 L 10,20` + ) + .attr('class', 'edge') + .attr('id', 'temp-thickness-edge'); + + return () => { + const dims = tempPath.node().getBBox(); + tempPath.remove(); + return dims.height as number; + } +} + +export const drawEdges = function (edgesEl: D3Element, edgeThickness: number, cy: cytoscape.Core) { + cy.edges().map((edge, id) => { + const data = edge.data(); + if (edge[0]._private.bodyBounds) { + const bounds = edge[0]._private.rscratch; + const translateX = bounds.startX === bounds.endX ? ((edgeThickness + 2) / 1.5) : 0; + const translateY = bounds.startY === bounds.endY ? ((edgeThickness + 2) / 1.5) : 0; + + log.trace('Edge: ', id, data); + edgesEl + .insert('path') + .attr( + 'd', + `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} ` + ) + .attr('class', 'edge') + .attr( + 'transform', + 'translate(' + translateX + ', ' + translateY + ')' + ); + } + }) +} + +export const drawService = function ( + db: ArchitectureDB, + elem: D3Element, + service: ArchitectureService, + conf: MermaidConfig +): number { + const serviceElem = elem.append('g'); + + if (service.title) { + const textElem = serviceElem.append('g'); + createText(textElem, service.title, { + useHtmlLabels: false, + width: 80, + classes: 'architecture-service-label', + }); + textElem + .attr('dy', '1em') + .attr('alignment-baseline', 'middle') + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'middle'); + + textElem.attr( + 'transform', + 'translate(' + 80 / 2 + ', ' + 80 + ')' + ); + + } + + let bkgElem = serviceElem.append('g'); + if (service.icon) { + if (!isIconNameInUse(service.icon)) { + throw new Error(`Invalid SVG Icon name: "${service.icon}"`) + } + bkgElem = getIcon(service.icon)?.(bkgElem); + } else { + bkgElem.append('path').attr('class', 'node-bkg').attr('id', 'node-' + service.id).attr( + 'd', + `M0 ${80 - 0} v${-80 + 2 * 0} q0,-5 5,-5 h${ + 80 - 2 * 0 + } q5,0 5,5 v${80 - 0} H0 Z` + ); + } + + serviceElem.attr('class', 'architecture-service'); + + const icon = serviceElem.append('foreignObject').attr('height', '80px').attr('width', '80px'); + icon.append('div').attr('class', 'icon-container').append('i').attr('class', 'service-icon fa fa-phone') + + db.setElementForId(service.id, serviceElem); + return 0; +}; From 84bd20b04b3fca5c948cac068dcbaae420398644 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sun, 17 Mar 2024 15:24:17 -0500 Subject: [PATCH 07/46] feat(arch): improved group rendering --- .../architecture/architectureRenderer.ts | 182 +++++++++++++----- .../architecture/architectureStyles.ts | 7 + .../architecture/architectureTypes.ts | 2 + .../src/diagrams/architecture/svgDraw.ts | 139 ++++++++----- 4 files changed, 229 insertions(+), 101 deletions(-) diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index a55b23bffd..f4566e85eb 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -1,6 +1,6 @@ import cytoscape from 'cytoscape'; import type { Diagram } from '../../Diagram.js'; -import fcose, {FcoseLayoutOptions} from 'cytoscape-fcose'; +import fcose, { FcoseLayoutOptions } from 'cytoscape-fcose'; import type { MermaidConfig } from '../../config.type.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import type { DrawDefinition, SVG } from '../../diagram-api/types.js'; @@ -17,9 +17,9 @@ import { } from './architectureTypes.js'; import { select } from 'd3'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import defaultConfig from '../../defaultConfig.js'; +import defaultConfig from '../../defaultConfig.js'; import type { D3Element } from '../../mermaidAPI.js'; -import { drawEdges, drawService, getEdgeThicknessCallback } from './svgDraw.js'; +import { drawEdges, drawGroups, drawService } from './svgDraw.js'; cytoscape.use(fcose); @@ -28,23 +28,25 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) { cy.add({ group: 'nodes', data: { + type: 'service', id: service.id, icon: service.icon, - title: service.title, + label: service.title, parent: service.in, // TODO: dynamic size width: 80, - height: 80 + height: 80, }, + classes: 'node-service' }); }); } function drawServices( - db: ArchitectureDB, - svg: D3Element, - services: ArchitectureService[], - conf: MermaidConfig + db: ArchitectureDB, + svg: D3Element, + services: ArchitectureService[], + conf: MermaidConfig ) { services.forEach((service) => drawService(db, svg, service, conf)); } @@ -54,19 +56,22 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { cy.add({ group: 'nodes', data: { + type: 'group', id: group.id, icon: group.icon, - title: group.title, + label: group.title, parent: group.in }, + classes: 'node-group' }); }); } function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { cy.nodes().map((node, id) => { - + const data = node.data(); + if (data.type === 'group') return; data.x = node.position().x; data.y = node.position().y; @@ -104,10 +109,40 @@ function layoutArchitecture( selector: 'edge', style: { 'curve-style': 'straight', - 'source-endpoint': '100% 100%', - 'target-endpoint': '100% 100%', + 'source-endpoint': '50% 50%', + 'target-endpoint': '50% 50%', }, }, + { + selector: 'node', + style: { + //@ts-ignore + 'compound-sizing-wrt-labels': 'include', + } + }, + { + selector: 'node[label]', + style: { + 'text-valign': 'bottom', + 'text-halign': 'center', + 'font-size': '16px', + } + }, + { + selector: '.node-service', + style: { + 'label': 'data(label)', + 'width': 'data(width)', + 'height': 'data(height)', + } + }, + { + selector: '.node-group', + style: { + //@ts-ignore + "padding": '30px' + } + } ], }); // Remove element after layout @@ -115,55 +150,97 @@ function layoutArchitecture( addGroups(groups, cy); addServices(services, cy); - addEdges(lines, cy); - - // Make cytoscape care about the dimensions of the nodes - cy.nodes().forEach(function (n) { - n.layoutDimensions = () => { - const data = n.data(); - return { w: data.width, h: data.height }; - }; - }); + addEdges(lines, cy); + + /** + * Merge alignment pairs together if they share a common node. + * + * Example: [["a", "b"], ["b", "c"], ["d", "e"]] -> [["a", "b", "c"], ["d", "e"]] + */ + const mergeAlignments = (orig: string[][]): string[][] => { + console.log('Start: ', orig); + // Mapping of discovered ids to their index in the new alignment array + const map: Record = {}; + const newAlignments: string[][] = [orig[0]]; + map[orig[0][0]] = 0; + map[orig[0][1]] = 0; + orig = orig.slice(1); + while (orig.length > 0) { + const pair = orig[0]; + const pairLHSIdx = map[pair[0]]; + const pairRHSIdx = map[pair[1]]; + console.log(pair); + console.log(map); + console.log(newAlignments); + // If neither id appears in the new array, add the pair to the new array + if (pairLHSIdx === undefined && pairRHSIdx === undefined) { + newAlignments.push(pair); + map[pair[0]] = newAlignments.length - 1; + map[pair[1]] = newAlignments.length - 1; + // If the LHS of the pair doesn't appear in the new array, add the LHS to the existing array it shares an id with + } else if (pairLHSIdx === undefined) { + newAlignments[pairRHSIdx].push(pair[0]); + map[pair[0]] = pairRHSIdx; + // If the RHS of the pair doesn't appear in the new array, add the RHS to the existing array it shares an id with + } else if (pairRHSIdx === undefined) { + newAlignments[pairLHSIdx].push(pair[1]); + map[pair[1]] = pairLHSIdx; + // If both ids already have been added to the new array and their index is different, merge all 3 arrays + } else if (pairLHSIdx != pairRHSIdx) { + console.log('ELSE'); + newAlignments.push(pair); + } + orig = orig.slice(1); + } + + console.log('End: ', newAlignments); + return newAlignments; + } + + const horizontalAlignments = cy + .edges() + .filter( + (edge) => + isArchitectureDirectionX(edge.data('sourceDir')) && + isArchitectureDirectionX(edge.data('targetDir')) + ) + .map((edge) => [edge.data('source'), edge.data('target')]); + + const verticalAlignments = cy + .edges() + .filter( + (edge) => + isArchitectureDirectionY(edge.data('sourceDir')) && + isArchitectureDirectionY(edge.data('targetDir')) + ) + .map((edge) => [edge.data('source'), edge.data('target')]); cy.layout({ name: 'fcose', quality: 'proof', styleEnabled: false, animate: false, + nodeDimensionsIncludeLabels: true, alignmentConstraint: { - horizontal: cy - .edges() - .filter( - (edge) => - isArchitectureDirectionX(edge.data('sourceDir')) && - isArchitectureDirectionX(edge.data('targetDir')) - ) - .map((edge) => [edge.data('source'), edge.data('target')]), - vertical: cy - .edges() - .filter( - (edge) => - isArchitectureDirectionY(edge.data('sourceDir')) && - isArchitectureDirectionY(edge.data('targetDir')) - ) - .map((edge) => [edge.data('source'), edge.data('target')]), + horizontal: mergeAlignments(horizontalAlignments), + vertical: mergeAlignments(verticalAlignments) }, relativePlacementConstraint: cy.edges().map((edge) => { const sourceDir = edge.data('sourceDir') as ArchitectureDirection; const targetDir = edge.data('targetDir') as ArchitectureDirection; - const sourceId = edge.data('source') as ArchitectureDirection; - const targetId = edge.data('target') as ArchitectureDirection; + const sourceId = edge.data('source') as string; + const targetId = edge.data('target') as string; if ( - isArchitectureDirectionX(sourceDir) && - isArchitectureDirectionX(targetDir) + isArchitectureDirectionX(sourceDir) && + isArchitectureDirectionX(targetDir) ) { - return {left: sourceDir === 'L' ? sourceId : targetId, right: sourceDir === 'R' ? sourceId : targetId, gap: 180} + return { left: sourceDir === 'R' ? sourceId : targetId, right: sourceDir === 'L' ? sourceId : targetId, gap: 180 } } else if ( - isArchitectureDirectionY(sourceDir) && - isArchitectureDirectionY(targetDir) + isArchitectureDirectionY(sourceDir) && + isArchitectureDirectionY(targetDir) ) { - return {top: sourceDir === 'T' ? sourceId : targetId, bottom: sourceDir === 'B' ? sourceId : targetId, gap: 180} + return { top: sourceDir === 'B' ? sourceId : targetId, bottom: sourceDir === 'T' ? sourceId : targetId, gap: 180 } } // TODO: fallback case + RB, TL, etc @@ -183,8 +260,9 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) const services = db.getServices(); const groups = db.getGroups(); const lines = db.getLines(); - log.info('Services: ', services); - log.info('Lines: ', lines); + console.log('Services: ', services); + console.log('Lines: ', lines); + console.log('Groups: ', groups); const svg: SVG = selectSvgElement(id); @@ -194,13 +272,15 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) const servicesElem = svg.append('g'); servicesElem.attr('class', 'architecture-services'); + const groupElem = svg.append('g'); + groupElem.attr('class', 'architecture-groups'); + drawServices(db, servicesElem, services, conf); - const getEdgeThickness = getEdgeThicknessCallback(svg); const cy = await layoutArchitecture(services, groups, lines); - const edgeThickness = getEdgeThickness(); - drawEdges(edgesElem, edgeThickness, cy); + drawEdges(edgesElem, cy); + drawGroups(groupElem, cy); positionServices(db, cy); setupGraphViewbox( diff --git a/packages/mermaid/src/diagrams/architecture/architectureStyles.ts b/packages/mermaid/src/diagrams/architecture/architectureStyles.ts index b92d227e63..7cdcbf4aef 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureStyles.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureStyles.ts @@ -68,6 +68,13 @@ const getStyles: DiagramStylesProvider = (options: ArchitectureStyleOptions) => .edge { fill: none; } + + .node-bkg { + fill: none; + stroke: #000; + stroke-width: 2px; + stroke-dasharray: 8; + } `; export default getStyles; diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index dbc663b73d..9cf78443a0 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -25,6 +25,8 @@ export interface ArchitectureService { icon?: string; title?: string; in?: string; + width?: number; + height?: number; } export interface ArchitectureGroup { diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index 55742ed7fe..b78d2d0449 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -1,10 +1,10 @@ import type { D3Element } from '../../mermaidAPI.js'; import { createText } from '../../rendering-util/createText.js'; -import type { ArchitectureDB, ArchitectureService } from './architectureTypes.js'; +import type { ArchitectureDB, ArchitectureGroup, ArchitectureService } from './architectureTypes.js'; import type { MermaidConfig } from '../../config.type.js'; import type cytoscape from 'cytoscape'; import { log } from '../../logger.js'; -import {getIcon, isIconNameInUse} from '../../rendering-util/svgRegister.js'; +import { getIcon, isIconNameInUse } from '../../rendering-util/svgRegister.js'; declare module 'cytoscape' { interface EdgeSingular { @@ -20,48 +20,86 @@ declare module 'cytoscape' { }; }; } -} - -/** - * Creates a temporary path which can be used to compute the line thickness. - * @param root root element to add the temporary path to - * @returns callback function which gets the bounding box dimensions and removes the path from root - */ -export const getEdgeThicknessCallback = function (root: D3Element) { - const tempPath = root.insert('path') - .attr( - 'd', - `M 10,10 L 10,20` - ) - .attr('class', 'edge') - .attr('id', 'temp-thickness-edge'); - - return () => { - const dims = tempPath.node().getBBox(); - tempPath.remove(); - return dims.height as number; + interface NodeSingular { + _private: { + bodyBounds: { + h: number; + w: number; + x1: number; + x2: number; + y1: number; + y2: number; + }; + children: cytoscape.NodeSingular[] + }; + data: () => { + type: 'service', + id: string, + icon?: string, + label?: string, + parent?: string, + width: number, + height: number, + [key: string]: any + } | { + type: 'group', + id: string, + icon?: string, + label?: string, + parent?: string, + [key: string]: any + } } } -export const drawEdges = function (edgesEl: D3Element, edgeThickness: number, cy: cytoscape.Core) { +export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { cy.edges().map((edge, id) => { const data = edge.data(); if (edge[0]._private.bodyBounds) { const bounds = edge[0]._private.rscratch; - const translateX = bounds.startX === bounds.endX ? ((edgeThickness + 2) / 1.5) : 0; - const translateY = bounds.startY === bounds.endY ? ((edgeThickness + 2) / 1.5) : 0; - log.trace('Edge: ', id, data); - edgesEl - .insert('path') - .attr( - 'd', - `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} ` - ) - .attr('class', 'edge') - .attr( + log.trace('Edge: ', id, data); + edgesEl + .insert('path') + .attr( + 'd', + `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} ` + ) + .attr('class', 'edge') + } + }) +} + +export const drawGroups = function ( + groupsEl: D3Element, + cy: cytoscape.Core +) { + cy.nodes().map((node, id) => { + const data = node.data(); + if (data.type === 'group') { + const { h, w, x1, x2, y1, y2 } = node.boundingBox(); + let bkgElem = groupsEl.append('rect') + .attr('x', x1 + 40) + .attr('y', y1 + 40) + .attr('width', w) + .attr('height', h) + .attr('class', 'node-bkg'); + + const textElem = groupsEl.append('g'); + createText(textElem, data.title, { + useHtmlLabels: false, + width: w, + classes: 'architecture-service-label', + }); + textElem + .attr('dy', '1em') + .attr('alignment-baseline', 'middle') + .attr('dominant-baseline', 'start') + .attr('text-anchor', 'start'); + + textElem.attr( 'transform', - 'translate(' + translateX + ', ' + translateY + ')' + 'translate(' + (x1 + 44) + ', ' + (y1 + 42) + ')' ); } }) @@ -76,22 +114,23 @@ export const drawService = function ( const serviceElem = elem.append('g'); if (service.title) { - const textElem = serviceElem.append('g'); - createText(textElem, service.title, { - useHtmlLabels: false, - width: 80, - classes: 'architecture-service-label', - }); - textElem + const textElem = serviceElem.append('g'); + createText(textElem, service.title, { + useHtmlLabels: false, + width: 110, + classes: 'architecture-service-label', + }); + textElem .attr('dy', '1em') .attr('alignment-baseline', 'middle') .attr('dominant-baseline', 'middle') .attr('text-anchor', 'middle'); - textElem.attr( - 'transform', - 'translate(' + 80 / 2 + ', ' + 80 + ')' - ); + textElem.attr( + 'transform', + // TODO: dynamic size + 'translate(' + 80 / 2 + ', ' + 80 + ')' + ); } @@ -104,16 +143,16 @@ export const drawService = function ( } else { bkgElem.append('path').attr('class', 'node-bkg').attr('id', 'node-' + service.id).attr( 'd', - `M0 ${80 - 0} v${-80 + 2 * 0} q0,-5 5,-5 h${ - 80 - 2 * 0 + `M0 ${80 - 0} v${-80 + 2 * 0} q0,-5 5,-5 h${80 - 2 * 0 } q5,0 5,5 v${80 - 0} H0 Z` ); } serviceElem.attr('class', 'architecture-service'); - const icon = serviceElem.append('foreignObject').attr('height', '80px').attr('width', '80px'); - icon.append('div').attr('class', 'icon-container').append('i').attr('class', 'service-icon fa fa-phone') + const { width, height } = serviceElem._groups[0][0].getBBox(); + service.width = width; + service.height = height; db.setElementForId(service.id, serviceElem); return 0; From 5e214877a421d3d26a1c72d5c5f8b914e152875c Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Tue, 19 Mar 2024 13:57:37 -0500 Subject: [PATCH 08/46] style(arch): prettier formatting --- .vite/jsonSchemaPlugin.ts | 2 +- .../scripts/create-types-from-json-schema.mts | 2 +- .../src/diagram-api/diagram-orchestration.ts | 2 +- .../diagrams/architecture/architectureDb.ts | 64 ++++++----- .../architecture/architectureRenderer.ts | 58 +++++----- .../architecture/architectureTypes.ts | 100 ++++++++++-------- .../src/diagrams/architecture/svgDraw.ts | 79 +++++++------- .../mermaid/src/schemas/config.schema.yaml | 2 +- 8 files changed, 165 insertions(+), 144 deletions(-) diff --git a/.vite/jsonSchemaPlugin.ts b/.vite/jsonSchemaPlugin.ts index a69f6889b3..e83acd31f1 100644 --- a/.vite/jsonSchemaPlugin.ts +++ b/.vite/jsonSchemaPlugin.ts @@ -26,7 +26,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'c4', 'sankey', 'block', - 'architecture' + 'architecture', ] as const; /** diff --git a/packages/mermaid/scripts/create-types-from-json-schema.mts b/packages/mermaid/scripts/create-types-from-json-schema.mts index fbd12a37ff..7129cb94eb 100644 --- a/packages/mermaid/scripts/create-types-from-json-schema.mts +++ b/packages/mermaid/scripts/create-types-from-json-schema.mts @@ -54,7 +54,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'gitGraph', 'c4', 'sankey', - 'architecture' + 'architecture', ]; /** diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 84697022ca..4259043b1a 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -21,7 +21,7 @@ import timeline from '../diagrams/timeline/detector.js'; import mindmap from '../diagrams/mindmap/detector.js'; import sankey from '../diagrams/sankey/sankeyDetector.js'; import block from '../diagrams/block/blockDetector.js'; -import architecture from '../diagrams/architecture/architectureDetector.js' +import architecture from '../diagrams/architecture/architectureDetector.js'; import { registerLazyLoadedDiagrams } from './detectType.js'; import { registerDiagram } from './diagramAPI.js'; diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index 5aecba746d..896c342aa6 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -1,5 +1,12 @@ -import type { ArchitectureFields, ArchitectureDB, ArchitectureService, ArchitectureGroup, ArchitectureDirection, ArchitectureLine } from "./architectureTypes.js"; -import { isArchitectureDirection } from "./architectureTypes.js"; +import type { + ArchitectureFields, + ArchitectureDB, + ArchitectureService, + ArchitectureGroup, + ArchitectureDirection, + ArchitectureLine, +} from './architectureTypes.js'; +import { isArchitectureDirection } from './architectureTypes.js'; import { setAccTitle, getAccTitle, @@ -11,9 +18,10 @@ import { } from '../common/commonDb.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js'; import DEFAULT_CONFIG from '../../defaultConfig.js'; -import type { D3Element } from "../../mermaidAPI.js"; +import type { D3Element } from '../../mermaidAPI.js'; -export const DEFAULT_ARCHITECTURE_CONFIG: Required = DEFAULT_CONFIG.architecture; +export const DEFAULT_ARCHITECTURE_CONFIG: Required = + DEFAULT_CONFIG.architecture; export const DEFAULT_ARCHITECTURE_DB: ArchitectureFields = { services: [], groups: [], @@ -41,37 +49,46 @@ const clear = (): void => { commonClear(); }; -const addService = function (id: string, opts: Omit = {}) { - const {icon, in: inside, title} = opts; +const addService = function (id: string, opts: Omit = {}) { + const { icon, in: inside, title } = opts; services.push({ id, icon, title, - in: inside + in: inside, }); -} +}; const getServices = (): ArchitectureService[] => services; -const addGroup = function (id: string, opts: Omit = {}) { - const {icon, in: inside, title} = opts; +const addGroup = function (id: string, opts: Omit = {}) { + const { icon, in: inside, title } = opts; groups.push({ id, icon, title, - in: inside + in: inside, }); -} +}; const getGroups = (): ArchitectureGroup[] => groups; +const addLine = function ( + lhs_id: string, + lhs_dir: ArchitectureDirection, + rhs_id: string, + rhs_dir: ArchitectureDirection, + opts: Omit = {} +) { + const { title, lhs_into, rhs_into } = opts; -const addLine = function (lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id: string, rhs_dir: ArchitectureDirection, opts: Omit = {}) { - const {title, lhs_into, rhs_into} = opts; - if (!isArchitectureDirection(lhs_dir)) { - throw new Error(`Invalid direction given for left hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${lhs_dir}`) + throw new Error( + `Invalid direction given for left hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${lhs_dir}` + ); } if (!isArchitectureDirection(rhs_dir)) { - throw new Error(`Invalid direction given for right hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${rhs_dir}`) + throw new Error( + `Invalid direction given for right hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${rhs_dir}` + ); } lines.push({ @@ -81,19 +98,16 @@ const addLine = function (lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id rhs_dir, title, lhs_into, - rhs_into + rhs_into, }); -} +}; const getLines = (): ArchitectureLine[] => lines; - const setElementForId = (id: string, element: D3Element) => { elements[id] = element; }; const getElementById = (id: string) => elements[id]; - - export const db: ArchitectureDB = { getConfig, clear, @@ -103,7 +117,7 @@ export const db: ArchitectureDB = { getAccTitle, setAccDescription, getAccDescription, - + addService, getServices, addGroup, @@ -111,5 +125,5 @@ export const db: ArchitectureDB = { addLine, getLines, setElementForId, - getElementById -}; \ No newline at end of file + getElementById, +}; diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index f4566e85eb..3063849c37 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -37,7 +37,7 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) { width: 80, height: 80, }, - classes: 'node-service' + classes: 'node-service', }); }); } @@ -60,16 +60,15 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { id: group.id, icon: group.icon, label: group.title, - parent: group.in + parent: group.in, }, - classes: 'node-group' + classes: 'node-group', }); }); } function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { cy.nodes().map((node, id) => { - const data = node.data(); if (data.type === 'group') return; data.x = node.position().x; @@ -118,7 +117,7 @@ function layoutArchitecture( style: { //@ts-ignore 'compound-sizing-wrt-labels': 'include', - } + }, }, { selector: 'node[label]', @@ -126,23 +125,23 @@ function layoutArchitecture( 'text-valign': 'bottom', 'text-halign': 'center', 'font-size': '16px', - } + }, }, { selector: '.node-service', style: { - 'label': 'data(label)', - 'width': 'data(width)', - 'height': 'data(height)', - } + label: 'data(label)', + width: 'data(width)', + height: 'data(height)', + }, }, { selector: '.node-group', style: { //@ts-ignore - "padding": '30px' - } - } + padding: '30px', + }, + }, ], }); // Remove element after layout @@ -150,11 +149,11 @@ function layoutArchitecture( addGroups(groups, cy); addServices(services, cy); - addEdges(lines, cy); + addEdges(lines, cy); /** * Merge alignment pairs together if they share a common node. - * + * * Example: [["a", "b"], ["b", "c"], ["d", "e"]] -> [["a", "b", "c"], ["d", "e"]] */ const mergeAlignments = (orig: string[][]): string[][] => { @@ -195,7 +194,7 @@ function layoutArchitecture( console.log('End: ', newAlignments); return newAlignments; - } + }; const horizontalAlignments = cy .edges() @@ -223,7 +222,7 @@ function layoutArchitecture( nodeDimensionsIncludeLabels: true, alignmentConstraint: { horizontal: mergeAlignments(horizontalAlignments), - vertical: mergeAlignments(verticalAlignments) + vertical: mergeAlignments(verticalAlignments), }, relativePlacementConstraint: cy.edges().map((edge) => { const sourceDir = edge.data('sourceDir') as ArchitectureDirection; @@ -231,19 +230,20 @@ function layoutArchitecture( const sourceId = edge.data('source') as string; const targetId = edge.data('target') as string; - if ( - isArchitectureDirectionX(sourceDir) && - isArchitectureDirectionX(targetDir) - ) { - return { left: sourceDir === 'R' ? sourceId : targetId, right: sourceDir === 'L' ? sourceId : targetId, gap: 180 } - } else if ( - isArchitectureDirectionY(sourceDir) && - isArchitectureDirectionY(targetDir) - ) { - return { top: sourceDir === 'B' ? sourceId : targetId, bottom: sourceDir === 'T' ? sourceId : targetId, gap: 180 } + if (isArchitectureDirectionX(sourceDir) && isArchitectureDirectionX(targetDir)) { + return { + left: sourceDir === 'R' ? sourceId : targetId, + right: sourceDir === 'L' ? sourceId : targetId, + gap: 180, + }; + } else if (isArchitectureDirectionY(sourceDir) && isArchitectureDirectionY(targetDir)) { + return { + top: sourceDir === 'B' ? sourceId : targetId, + bottom: sourceDir === 'T' ? sourceId : targetId, + gap: 180, + }; } // TODO: fallback case + RB, TL, etc - }), } as FcoseLayoutOptions).run(); cy.ready((e) => { @@ -289,8 +289,6 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) conf.architecture?.padding ?? defaultConfig.architecture.padding, conf.architecture?.useMaxWidth ?? defaultConfig.architecture.useMaxWidth ); - - }; export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index 9cf78443a0..cb95074e4a 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -2,65 +2,75 @@ import type { DiagramDB } from '../../diagram-api/types.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js'; import type { D3Element } from '../../mermaidAPI.js'; -export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B' -export const isArchitectureDirection = function(x: unknown): x is ArchitectureDirection { - const temp = x as ArchitectureDirection; - return (temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B') -} -export const isArchitectureDirectionX = function(x: ArchitectureDirection): x is Extract { - const temp = x as Extract - return (temp === 'L' || temp === 'R') -} -export const isArchitectureDirectionY = function(x: ArchitectureDirection): x is Extract { - const temp = x as Extract - return (temp === 'T' || temp === 'B') -} +export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B'; +export const isArchitectureDirection = function (x: unknown): x is ArchitectureDirection { + const temp = x as ArchitectureDirection; + return temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B'; +}; +export const isArchitectureDirectionX = function ( + x: ArchitectureDirection +): x is Extract { + const temp = x as Extract; + return temp === 'L' || temp === 'R'; +}; +export const isArchitectureDirectionY = function ( + x: ArchitectureDirection +): x is Extract { + const temp = x as Extract; + return temp === 'T' || temp === 'B'; +}; export interface ArchitectureStyleOptions { - fontFamily: string; + fontFamily: string; } export interface ArchitectureService { - id: string; - icon?: string; - title?: string; - in?: string; - width?: number; - height?: number; + id: string; + icon?: string; + title?: string; + in?: string; + width?: number; + height?: number; } export interface ArchitectureGroup { - id: string; - icon?: string; - title?: string; - in?: string; + id: string; + icon?: string; + title?: string; + in?: string; } export interface ArchitectureLine { - lhs_id: string; - lhs_dir: ArchitectureDirection; - title?: string; - rhs_id: string; - rhs_dir: ArchitectureDirection; - lhs_into?: boolean; - rhs_into?: boolean; + lhs_id: string; + lhs_dir: ArchitectureDirection; + title?: string; + rhs_id: string; + rhs_dir: ArchitectureDirection; + lhs_into?: boolean; + rhs_into?: boolean; } export interface ArchitectureDB extends DiagramDB { - addService: (id: string, opts: Omit) => void - getServices: () => ArchitectureService[] - addGroup: (id: string, opts: Omit) => void - getGroups: () => ArchitectureGroup[] - addLine: (lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id: string, rhs_dir: ArchitectureDirection, opts: Omit) => void - getLines: () => ArchitectureLine[] - setElementForId: (id: string, element: D3Element) => void; - getElementById: (id: string) => D3Element; + addService: (id: string, opts: Omit) => void; + getServices: () => ArchitectureService[]; + addGroup: (id: string, opts: Omit) => void; + getGroups: () => ArchitectureGroup[]; + addLine: ( + lhs_id: string, + lhs_dir: ArchitectureDirection, + rhs_id: string, + rhs_dir: ArchitectureDirection, + opts: Omit + ) => void; + getLines: () => ArchitectureLine[]; + setElementForId: (id: string, element: D3Element) => void; + getElementById: (id: string) => D3Element; } export interface ArchitectureFields { - services: ArchitectureService[], - groups: ArchitectureGroup[], - lines: ArchitectureLine[], - cnt: number, - config: ArchitectureDiagramConfig -} \ No newline at end of file + services: ArchitectureService[]; + groups: ArchitectureGroup[]; + lines: ArchitectureLine[]; + cnt: number; + config: ArchitectureDiagramConfig; +} diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index b78d2d0449..473bf74074 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -1,6 +1,9 @@ import type { D3Element } from '../../mermaidAPI.js'; import { createText } from '../../rendering-util/createText.js'; -import type { ArchitectureDB, ArchitectureGroup, ArchitectureService } from './architectureTypes.js'; +import type { + ArchitectureDB, + ArchitectureService, +} from './architectureTypes.js'; import type { MermaidConfig } from '../../config.type.js'; import type cytoscape from 'cytoscape'; import { log } from '../../logger.js'; @@ -30,25 +33,27 @@ declare module 'cytoscape' { y1: number; y2: number; }; - children: cytoscape.NodeSingular[] + children: cytoscape.NodeSingular[]; }; - data: () => { - type: 'service', - id: string, - icon?: string, - label?: string, - parent?: string, - width: number, - height: number, - [key: string]: any - } | { - type: 'group', - id: string, - icon?: string, - label?: string, - parent?: string, - [key: string]: any - } + data: () => + | { + type: 'service'; + id: string; + icon?: string; + label?: string; + parent?: string; + width: number; + height: number; + [key: string]: any; + } + | { + type: 'group'; + id: string; + icon?: string; + label?: string; + parent?: string; + [key: string]: any; + }; } } @@ -65,20 +70,18 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { 'd', `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} ` ) - .attr('class', 'edge') + .attr('class', 'edge'); } - }) -} + }); +}; -export const drawGroups = function ( - groupsEl: D3Element, - cy: cytoscape.Core -) { +export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { cy.nodes().map((node, id) => { const data = node.data(); if (data.type === 'group') { const { h, w, x1, x2, y1, y2 } = node.boundingBox(); - let bkgElem = groupsEl.append('rect') + let bkgElem = groupsEl + .append('rect') .attr('x', x1 + 40) .attr('y', y1 + 40) .attr('width', w) @@ -97,13 +100,10 @@ export const drawGroups = function ( .attr('dominant-baseline', 'start') .attr('text-anchor', 'start'); - textElem.attr( - 'transform', - 'translate(' + (x1 + 44) + ', ' + (y1 + 42) + ')' - ); + textElem.attr('transform', 'translate(' + (x1 + 44) + ', ' + (y1 + 42) + ')'); } - }) -} + }); +}; export const drawService = function ( db: ArchitectureDB, @@ -131,21 +131,20 @@ export const drawService = function ( // TODO: dynamic size 'translate(' + 80 / 2 + ', ' + 80 + ')' ); - } let bkgElem = serviceElem.append('g'); if (service.icon) { if (!isIconNameInUse(service.icon)) { - throw new Error(`Invalid SVG Icon name: "${service.icon}"`) + throw new Error(`Invalid SVG Icon name: "${service.icon}"`); } bkgElem = getIcon(service.icon)?.(bkgElem); } else { - bkgElem.append('path').attr('class', 'node-bkg').attr('id', 'node-' + service.id).attr( - 'd', - `M0 ${80 - 0} v${-80 + 2 * 0} q0,-5 5,-5 h${80 - 2 * 0 - } q5,0 5,5 v${80 - 0} H0 Z` - ); + bkgElem + .append('path') + .attr('class', 'node-bkg') + .attr('id', 'node-' + service.id) + .attr('d', `M0 ${80 - 0} v${-80 + 2 * 0} q0,-5 5,-5 h${80 - 2 * 0} q5,0 5,5 v${80 - 0} H0 Z`); } serviceElem.attr('class', 'architecture-service'); diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 601939beed..8723d4ca12 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -858,7 +858,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) ArchitectureDiagramConfig: title: Architecture Diagram Config - allOf: [{ $ref: '#/$defs/BaseDiagramConfig'}] + allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }] description: The object containing configurations specific for architecture diagrams type: object unevaluatedProperties: false From a493e2fbb31d141301f717efc0b21fe402161af7 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Mon, 25 Mar 2024 14:17:05 -0500 Subject: [PATCH 09/46] feat(arch): dynamic node sizing --- packages/mermaid/src/config.type.ts | 2 +- .../diagrams/architecture/architectureDb.ts | 17 ++++++++++++----- .../architecture/architectureRenderer.ts | 11 +++++------ .../src/diagrams/architecture/svgDraw.ts | 19 ++++++++++++------- .../mermaid/src/schemas/config.schema.yaml | 6 +++--- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 014ff65e77..e57e0d886b 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -638,7 +638,7 @@ export interface RequirementDiagramConfig extends BaseDiagramConfig { */ export interface ArchitectureDiagramConfig extends BaseDiagramConfig { padding?: number; - maxNodeWidth?: number; + iconSize?: number; } /** * The object containing configurations specific for mindmap diagrams diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index 896c342aa6..6f00be0d44 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -6,6 +6,7 @@ import type { ArchitectureDirection, ArchitectureLine, } from './architectureTypes.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; import { isArchitectureDirection } from './architectureTypes.js'; import { setAccTitle, @@ -36,10 +37,6 @@ let lines = DEFAULT_ARCHITECTURE_DB.lines; let elements: Record = {}; let cnt = DEFAULT_ARCHITECTURE_DB.cnt; -const config: Required = structuredClone(DEFAULT_ARCHITECTURE_CONFIG); - -const getConfig = (): Required => structuredClone(config); - const clear = (): void => { services = structuredClone(DEFAULT_ARCHITECTURE_DB.services); groups = structuredClone(DEFAULT_ARCHITECTURE_DB.groups); @@ -109,7 +106,6 @@ const setElementForId = (id: string, element: D3Element) => { const getElementById = (id: string) => elements[id]; export const db: ArchitectureDB = { - getConfig, clear, setDiagramTitle, getDiagramTitle, @@ -127,3 +123,14 @@ export const db: ArchitectureDB = { setElementForId, getElementById, }; + +function getConfigField(field: T): Required[T] { + const arch = getConfig().architecture; + if (arch && arch[field] !== undefined) { + const a = arch[field]; + return arch[field] as Required[T] + } + return DEFAULT_ARCHITECTURE_CONFIG[field] +} + +export { getConfigField } \ No newline at end of file diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index 3063849c37..29b6e4d44c 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -17,9 +17,9 @@ import { } from './architectureTypes.js'; import { select } from 'd3'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import defaultConfig from '../../defaultConfig.js'; import type { D3Element } from '../../mermaidAPI.js'; import { drawEdges, drawGroups, drawService } from './svgDraw.js'; +import { getConfigField } from './architectureDb.js'; cytoscape.use(fcose); @@ -33,9 +33,8 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) { icon: service.icon, label: service.title, parent: service.in, - // TODO: dynamic size - width: 80, - height: 80, + width: getConfigField('iconSize'), + height: getConfigField('iconSize'), }, classes: 'node-service', }); @@ -286,8 +285,8 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) setupGraphViewbox( undefined, svg, - conf.architecture?.padding ?? defaultConfig.architecture.padding, - conf.architecture?.useMaxWidth ?? defaultConfig.architecture.useMaxWidth + getConfigField('padding'), + getConfigField('useMaxWidth') ); }; diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index 473bf74074..446704af6d 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -8,6 +8,7 @@ import type { MermaidConfig } from '../../config.type.js'; import type cytoscape from 'cytoscape'; import { log } from '../../logger.js'; import { getIcon, isIconNameInUse } from '../../rendering-util/svgRegister.js'; +import { getConfigField } from './architectureDb.js'; declare module 'cytoscape' { interface EdgeSingular { @@ -76,20 +77,23 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { }; export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { + const iconSize = getConfigField('iconSize') + const halfIconSize = iconSize / 2 + cy.nodes().map((node, id) => { const data = node.data(); if (data.type === 'group') { const { h, w, x1, x2, y1, y2 } = node.boundingBox(); let bkgElem = groupsEl .append('rect') - .attr('x', x1 + 40) - .attr('y', y1 + 40) + .attr('x', x1 + halfIconSize) + .attr('y', y1 + halfIconSize) .attr('width', w) .attr('height', h) .attr('class', 'node-bkg'); const textElem = groupsEl.append('g'); - createText(textElem, data.title, { + createText(textElem, data.label, { useHtmlLabels: false, width: w, classes: 'architecture-service-label', @@ -100,7 +104,7 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { .attr('dominant-baseline', 'start') .attr('text-anchor', 'start'); - textElem.attr('transform', 'translate(' + (x1 + 44) + ', ' + (y1 + 42) + ')'); + textElem.attr('transform', 'translate(' + (x1 + halfIconSize + 4) + ', ' + (y1 + halfIconSize + 2) + ')'); } }); }; @@ -112,6 +116,7 @@ export const drawService = function ( conf: MermaidConfig ): number { const serviceElem = elem.append('g'); + const iconSize = getConfigField('iconSize') if (service.title) { const textElem = serviceElem.append('g'); @@ -129,7 +134,7 @@ export const drawService = function ( textElem.attr( 'transform', // TODO: dynamic size - 'translate(' + 80 / 2 + ', ' + 80 + ')' + 'translate(' + (iconSize / 2) + ', ' + iconSize + ')' ); } @@ -138,13 +143,13 @@ export const drawService = function ( if (!isIconNameInUse(service.icon)) { throw new Error(`Invalid SVG Icon name: "${service.icon}"`); } - bkgElem = getIcon(service.icon)?.(bkgElem); + bkgElem = getIcon(service.icon)?.(bkgElem, iconSize); } else { bkgElem .append('path') .attr('class', 'node-bkg') .attr('id', 'node-' + service.id) - .attr('d', `M0 ${80 - 0} v${-80 + 2 * 0} q0,-5 5,-5 h${80 - 2 * 0} q5,0 5,5 v${80 - 0} H0 Z`); + .attr('d', `M0 ${iconSize - 0} v${-iconSize + 2 * 0} q0,-5 5,-5 h${iconSize - 2 * 0} q5,0 5,5 v${iconSize - 0} H0 Z`); } serviceElem.attr('class', 'architecture-service'); diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 8723d4ca12..146237eb88 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -865,14 +865,14 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) required: - useMaxWidth - padding - - maxNodeWidth + - iconSize properties: padding: type: number default: 10 - maxNodeWidth: + iconSize: type: number - default: 200 + default: 80 MindmapDiagramConfig: title: Mindmap Diagram Config From 0ab7a3d8ec0ef5aa6735d91acb3134c4d81c113a Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Wed, 27 Mar 2024 09:30:44 -0500 Subject: [PATCH 10/46] feat(arch): added 4 default icons, added config field for icons --- packages/mermaid/src/config.type.ts | 6 +++ packages/mermaid/src/mermaidAPI.ts | 9 ++++ .../src/rendering-util/svg/database.ts | 16 ++++++ .../mermaid/src/rendering-util/svg/disk.ts | 18 +++++++ .../mermaid/src/rendering-util/svg/index.ts | 14 +++++ .../src/rendering-util/svg/internet.ts | 17 +++++++ .../mermaid/src/rendering-util/svg/server.ts | 40 +++++++++++++++ .../mermaid/src/rendering-util/svgRegister.ts | 51 +++++++++++-------- .../mermaid/src/schemas/config.schema.yaml | 6 +++ 9 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 packages/mermaid/src/rendering-util/svg/database.ts create mode 100644 packages/mermaid/src/rendering-util/svg/disk.ts create mode 100644 packages/mermaid/src/rendering-util/svg/index.ts create mode 100644 packages/mermaid/src/rendering-util/svg/internet.ts create mode 100644 packages/mermaid/src/rendering-util/svg/server.ts diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index e57e0d886b..1c88621403 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -135,6 +135,12 @@ export interface MermaidConfig { * */ legacyMathML?: boolean; + /** + * This option specifies an object contianing a mappig of SVG icon names to a resolver that returns the svg code. + * For supported diagrams (i.e., Architecture), their syntax allows refering to key names in this object to display the corresponding SVG icon in the rendered diagram. + * + */ + iconLibraries?: Array; /** * This option controls if the generated ids of nodes in the SVG are * generated randomly or based on a seed. diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index ad9d7d286b..ba580be88a 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -31,6 +31,8 @@ import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility. import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js'; import { preprocessDiagram } from './preprocess.js'; import { decodeEntities } from './utils.js'; +import { registerIcons } from './rendering-util/svgRegister.js'; +import defaultIconLibrary from './rendering-util/svg/index.js'; const MAX_TEXTLENGTH = 50_000; const MAX_TEXTLENGTH_EXCEEDED_MSG = @@ -502,6 +504,13 @@ function initialize(options: MermaidConfig = {}) { // Set default options configApi.saveConfigFromInitialize(options); + registerIcons(defaultIconLibrary) + if (options?.iconLibraries) { + options.iconLibraries.forEach((library) => { + registerIcons(library); + }); + } + if (options?.theme && options.theme in theme) { // Todo merge with user options options.themeVariables = theme[options.theme as keyof typeof theme].getThemeVariables( diff --git a/packages/mermaid/src/rendering-util/svg/database.ts b/packages/mermaid/src/rendering-util/svg/database.ts new file mode 100644 index 0000000000..697114b314 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/database.ts @@ -0,0 +1,16 @@ +/** + * @author Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from "../svgRegister.js"; + +export default createIcon(` + + + + + + + +`, 80) + diff --git a/packages/mermaid/src/rendering-util/svg/disk.ts b/packages/mermaid/src/rendering-util/svg/disk.ts new file mode 100644 index 0000000000..e96d337626 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/disk.ts @@ -0,0 +1,18 @@ +/** + * @author Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from "../svgRegister.js"; + +export default createIcon(` + + + + + + + + + +`, 80) + diff --git a/packages/mermaid/src/rendering-util/svg/index.ts b/packages/mermaid/src/rendering-util/svg/index.ts new file mode 100644 index 0000000000..7a34b78e4e --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/index.ts @@ -0,0 +1,14 @@ +import { IconLibrary } from "../svgRegister.js"; +import database from "./database.js"; +import server from "./server.js"; +import disk from "./disk.js"; +import internet from "./internet.js"; + +const defaultIconLibrary: IconLibrary = { + database: database, + server: server, + disk: disk, + internet: internet, +} + +export default defaultIconLibrary \ No newline at end of file diff --git a/packages/mermaid/src/rendering-util/svg/internet.ts b/packages/mermaid/src/rendering-util/svg/internet.ts new file mode 100644 index 0000000000..c4edf9b4e2 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/internet.ts @@ -0,0 +1,17 @@ +/** + * @author Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from "../svgRegister.js"; + +export default createIcon(` + + + + + + + + +`, 80) + diff --git a/packages/mermaid/src/rendering-util/svg/server.ts b/packages/mermaid/src/rendering-util/svg/server.ts new file mode 100644 index 0000000000..559148a71b --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/server.ts @@ -0,0 +1,40 @@ +/** + * @author Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from "../svgRegister.js"; + +export default createIcon(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, 80) + diff --git a/packages/mermaid/src/rendering-util/svgRegister.ts b/packages/mermaid/src/rendering-util/svgRegister.ts index 303c43f579..4ab15b560e 100644 --- a/packages/mermaid/src/rendering-util/svgRegister.ts +++ b/packages/mermaid/src/rendering-util/svgRegister.ts @@ -1,33 +1,42 @@ -import { Selection } from "d3-selection"; +import { Selection } from 'd3-selection'; -type IconResolver = (parent: Selection) => Selection -type IconLibrary = Record +type IconResolver = ( + parent: Selection, width?: number +) => Selection; +type IconLibrary = Record; -const icons: IconLibrary = {} +const icons: IconLibrary = {}; const isIconNameInUse = (name: string): boolean => { - return icons[name] !== undefined; -} + return icons[name] !== undefined; +}; const registerIcon = (name: string, resolver: IconResolver) => { - if(!isIconNameInUse(name)) { - icons[name] = resolver; - } -} + if (!isIconNameInUse(name)) { + icons[name] = resolver; + } +}; const registerIcons = (library: IconLibrary) => { - Object.entries(library).forEach(([name, resolver]) => { - if (!isIconNameInUse(name)) { - icons[name] = resolver; - } - }) -} + Object.entries(library).forEach(([name, resolver]) => { + if (!isIconNameInUse(name)) { + icons[name] = resolver; + } + }); +}; const getIcon = (name: string): IconResolver | null => { - if (isIconNameInUse(name)) { - return icons[name]; - } - return null; // TODO: return default + if (isIconNameInUse(name)) { + return icons[name]; + } + return null; // TODO: return default +}; + +const createIcon = (icon: string, originalSize: number): IconResolver => { + return (parent: Selection, size: number = originalSize) => { + parent.html(`${icon}`) + return parent + } } -export { registerIcon, registerIcons, getIcon, isIconNameInUse, IconLibrary } \ No newline at end of file +export { registerIcon, registerIcons, getIcon, isIconNameInUse, createIcon, IconLibrary }; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 146237eb88..349de47f70 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -177,6 +177,12 @@ properties: fall back to legacy rendering for KaTeX. type: boolean default: false + iconLibraries: + description: | + This option specifies an object contianing a mappig of SVG icon names to a resolver that returns the svg code. + For supported diagrams (i.e., Architecture), their syntax allows refering to key names in this object to display the corresponding SVG icon in the rendered diagram. + tsType: Array + # tsType: Record deterministicIds: description: | This option controls if the generated ids of nodes in the SVG are From 10682ef31f10edd67f4db16313ec5f3caf93859b Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Wed, 27 Mar 2024 09:31:06 -0500 Subject: [PATCH 11/46] feat(arch): added demo --- demos/architecture.html | 102 ++++++++++++++++++++++++++++++++++++++++ demos/index.html | 3 ++ 2 files changed, 105 insertions(+) create mode 100644 demos/architecture.html diff --git a/demos/architecture.html b/demos/architecture.html new file mode 100644 index 0000000000..19fd09d975 --- /dev/null +++ b/demos/architecture.html @@ -0,0 +1,102 @@ + + + + + + Architecture Mermaid Quick Test Page + + + + + +

Architecture diagram demo

+
+      architecture
+        group api[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
+        service gateway(internet)[Gateway] 
+
+        db L--R server
+        disk1 T--B server
+        disk2 T--B db
+        server T--B gateway
+    
+ + + + diff --git a/demos/index.html b/demos/index.html index efe054b4d5..4e35e72a7f 100644 --- a/demos/index.html +++ b/demos/index.html @@ -84,6 +84,9 @@

Sankey

  • Layered Blocks

  • +
  • +

    Architecture

    +
  • From b911bd3e42012d7026744f4c5d2e04aad92fe42c Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sun, 31 Mar 2024 13:51:40 -0500 Subject: [PATCH 12/46] feat(arch): improved error handling --- .../diagrams/architecture/architectureDb.ts | 40 +++++++++++++++++-- .../architecture/architectureTypes.ts | 3 +- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index 6f00be0d44..e08e71c366 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -27,27 +27,44 @@ export const DEFAULT_ARCHITECTURE_DB: ArchitectureFields = { services: [], groups: [], lines: [], - cnt: 0, + registeredIds: {}, config: DEFAULT_ARCHITECTURE_CONFIG, } as const; let services = DEFAULT_ARCHITECTURE_DB.services; let groups = DEFAULT_ARCHITECTURE_DB.groups; let lines = DEFAULT_ARCHITECTURE_DB.lines; +let registeredIds = DEFAULT_ARCHITECTURE_DB.registeredIds; let elements: Record = {}; -let cnt = DEFAULT_ARCHITECTURE_DB.cnt; const clear = (): void => { services = structuredClone(DEFAULT_ARCHITECTURE_DB.services); groups = structuredClone(DEFAULT_ARCHITECTURE_DB.groups); lines = structuredClone(DEFAULT_ARCHITECTURE_DB.lines); + registeredIds = structuredClone(DEFAULT_ARCHITECTURE_DB.registeredIds) elements = {}; - cnt = 0; commonClear(); }; const addService = function (id: string, opts: Omit = {}) { const { icon, in: inside, title } = opts; + if (registeredIds[id] !== undefined) { + throw new Error(`The service id [${id}] is already in use by another ${registeredIds[id]}`) + } + if (inside !== undefined) { + if (id === inside) { + throw new Error(`The service [${id}] cannot be placed within itself`) + } + if (registeredIds[inside] === undefined) { + throw new Error(`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`) + } + if (registeredIds[inside] === 'service') { + throw new Error(`The service [${id}]'s parent is not a group`); + } + } + + registeredIds[id] = 'service'; + services.push({ id, icon, @@ -59,6 +76,23 @@ const getServices = (): ArchitectureService[] => services; const addGroup = function (id: string, opts: Omit = {}) { const { icon, in: inside, title } = opts; + if (registeredIds[id] !== undefined) { + throw new Error(`The group id [${id}] is already in use by another ${registeredIds[id]}`) + } + if (inside !== undefined) { + if (id === inside) { + throw new Error(`The group [${id}] cannot be placed within itself`) + } + if (registeredIds[inside] === undefined) { + throw new Error(`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`) + } + if (registeredIds[inside] === 'service') { + throw new Error(`The group [${id}]'s parent is not a group`); + } + } + + registeredIds[id] = 'group'; + groups.push({ id, icon, diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index cb95074e4a..21c620c616 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -51,6 +51,7 @@ export interface ArchitectureLine { } export interface ArchitectureDB extends DiagramDB { + clear: () => void; addService: (id: string, opts: Omit) => void; getServices: () => ArchitectureService[]; addGroup: (id: string, opts: Omit) => void; @@ -71,6 +72,6 @@ export interface ArchitectureFields { services: ArchitectureService[]; groups: ArchitectureGroup[]; lines: ArchitectureLine[]; - cnt: number; + registeredIds: Record; config: ArchitectureDiagramConfig; } From 46a37a6eea72dbb7b3c845e5f94fa9f66dce9878 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sun, 31 Mar 2024 13:52:23 -0500 Subject: [PATCH 13/46] feat(arch): added fallback icon --- packages/mermaid/src/rendering-util/svg/index.ts | 2 ++ .../mermaid/src/rendering-util/svg/unknown.ts | 10 ++++++++++ .../mermaid/src/rendering-util/svgRegister.ts | 16 ++++++++-------- 3 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 packages/mermaid/src/rendering-util/svg/unknown.ts diff --git a/packages/mermaid/src/rendering-util/svg/index.ts b/packages/mermaid/src/rendering-util/svg/index.ts index 7a34b78e4e..bc8f1c5ac3 100644 --- a/packages/mermaid/src/rendering-util/svg/index.ts +++ b/packages/mermaid/src/rendering-util/svg/index.ts @@ -3,12 +3,14 @@ import database from "./database.js"; import server from "./server.js"; import disk from "./disk.js"; import internet from "./internet.js"; +import unknown from "./unknown.js"; const defaultIconLibrary: IconLibrary = { database: database, server: server, disk: disk, internet: internet, + unknown: unknown, } export default defaultIconLibrary \ No newline at end of file diff --git a/packages/mermaid/src/rendering-util/svg/unknown.ts b/packages/mermaid/src/rendering-util/svg/unknown.ts new file mode 100644 index 0000000000..25b1ef5c40 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/unknown.ts @@ -0,0 +1,10 @@ +/** + * @author Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from "../svgRegister.js"; + +export default createIcon(` + + ? +`, 80) \ No newline at end of file diff --git a/packages/mermaid/src/rendering-util/svgRegister.ts b/packages/mermaid/src/rendering-util/svgRegister.ts index 4ab15b560e..b5b4c5e280 100644 --- a/packages/mermaid/src/rendering-util/svgRegister.ts +++ b/packages/mermaid/src/rendering-util/svgRegister.ts @@ -5,6 +5,13 @@ type IconResolver = ( ) => Selection; type IconLibrary = Record; +const createIcon = (icon: string, originalSize: number): IconResolver => { + return (parent: Selection, size: number = originalSize) => { + parent.html(`${icon}`) + return parent + } +} + const icons: IconLibrary = {}; const isIconNameInUse = (name: string): boolean => { @@ -29,14 +36,7 @@ const getIcon = (name: string): IconResolver | null => { if (isIconNameInUse(name)) { return icons[name]; } - return null; // TODO: return default + return icons["unknown"]; }; -const createIcon = (icon: string, originalSize: number): IconResolver => { - return (parent: Selection, size: number = originalSize) => { - parent.html(`${icon}`) - return parent - } -} - export { registerIcon, registerIcons, getIcon, isIconNameInUse, createIcon, IconLibrary }; From 361e25ba34df09e2cc03a239ae0b20b3960202c2 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sun, 31 Mar 2024 13:53:34 -0500 Subject: [PATCH 14/46] fix(arch): fixed compound nodes overlapping --- .../architecture/architectureRenderer.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index 29b6e4d44c..e517183563 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -72,6 +72,7 @@ function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { if (data.type === 'group') return; data.x = node.position().x; data.y = node.position().y; + console.log(`Position service (${data.id}): (${data.x}, ${data.y})`) const nodeElem = db.getElementById(data.id); nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')'); @@ -156,6 +157,8 @@ function layoutArchitecture( * Example: [["a", "b"], ["b", "c"], ["d", "e"]] -> [["a", "b", "c"], ["d", "e"]] */ const mergeAlignments = (orig: string[][]): string[][] => { + if (orig.length < 1) return orig; + console.log('===== mergeAlignments ====='); console.log('Start: ', orig); // Mapping of discovered ids to their index in the new alignment array const map: Record = {}; @@ -192,6 +195,8 @@ function layoutArchitecture( } console.log('End: ', newAlignments); + console.log('==========================='); + return newAlignments; }; @@ -219,6 +224,23 @@ function layoutArchitecture( styleEnabled: false, animate: false, nodeDimensionsIncludeLabels: true, + // Adjust the edge parameters if it passes through the border of a group + // Hacky fix for: https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues/67 + idealEdgeLength(edge) { + const [nodeA, nodeB] = edge.connectedNodes() + const {parent: parentA} = nodeA.data(); + const {parent: parentB} = nodeB.data(); + const elasticity = parentA === parentB ? 1.25*getConfigField('iconSize') : 0.5*getConfigField('iconSize'); + return elasticity; + }, + edgeElasticity(edge) { + const [nodeA, nodeB] = edge.connectedNodes() + console.log(nodeA.data()); + const {parent: parentA} = nodeA.data(); + const {parent: parentB} = nodeB.data(); + const elasticity = parentA === parentB ? 0.45 : 0.001; + return elasticity + }, alignmentConstraint: { horizontal: mergeAlignments(horizontalAlignments), vertical: mergeAlignments(verticalAlignments), @@ -228,19 +250,23 @@ function layoutArchitecture( const targetDir = edge.data('targetDir') as ArchitectureDirection; const sourceId = edge.data('source') as string; const targetId = edge.data('target') as string; - + + let gap = 1.25*getConfigField('iconSize'); + console.log(`relativeConstraint: ${sourceId} ${sourceDir}--${targetDir} ${targetId} (gap=${gap})`); if (isArchitectureDirectionX(sourceDir) && isArchitectureDirectionX(targetDir)) { return { left: sourceDir === 'R' ? sourceId : targetId, right: sourceDir === 'L' ? sourceId : targetId, - gap: 180, + gap, }; } else if (isArchitectureDirectionY(sourceDir) && isArchitectureDirectionY(targetDir)) { return { top: sourceDir === 'B' ? sourceId : targetId, bottom: sourceDir === 'T' ? sourceId : targetId, - gap: 180, + gap, }; + } else { + console.log('FALLBACK CASE NEEDED') } // TODO: fallback case + RB, TL, etc }), @@ -277,6 +303,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) drawServices(db, servicesElem, services, conf); const cy = await layoutArchitecture(services, groups, lines); + console.log(cy.nodes().map(node => ({a: node.data()}))); drawEdges(edgesElem, cy); drawGroups(groupElem, cy); @@ -288,6 +315,8 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) getConfigField('padding'), getConfigField('useMaxWidth') ); + + console.log('==============================================================') }; export const renderer = { draw }; From f47bbee24af2e1223c85abaf2164900a66828bcc Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sun, 31 Mar 2024 13:54:06 -0500 Subject: [PATCH 15/46] feat(arch): added more demos --- demos/architecture.html | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/demos/architecture.html b/demos/architecture.html index 19fd09d975..96a376957e 100644 --- a/demos/architecture.html +++ b/demos/architecture.html @@ -15,6 +15,7 @@

    Architecture diagram demo

    +

    Simple diagram with groups

           architecture
             group api[API]
    @@ -31,6 +32,66 @@ 

    Architecture diagram demo

    server T--B gateway
    +
    +

    Groups within groups

    +
    +      architecture
    +        group api[API]
    +        group public[Public API] in api
    +        group private[Private API] in api
    +
    +
    +        service serv1(server)[Server] in public
    +
    +
    +        service serv2(server)[Server] in private
    +        service db(database)[Database] in private
    +
    +        service gateway(internet)[Gateway] in api
    +
    +        serv1 B--T serv2
    +
    +        serv2 L--R db
    +
    +        serv1 L--R gateway
    +    
    + +
    + +

    Default icon (?) from uknown icon name

    +
    +      architecture
    +        service unknown(iconnamedoesntexist)[Uknown Icon]
    +    
    + +
    + +
    +        architecture
    +            group vpc[Private VPC]
    +            group vpc2[Public VPC]
    +
    +            service s3(s3)[S3 Bucket]
    +            service rds(database)[RDS DB]
    +            service ddb(dynamodb)[DynamoDB]
    +            service ec2(ec2)[EC2 Server] in vpc
    +            service gateway(api_gateway)[API Gateway] in vpc
    +            service docdb(documentdb)[DocumentDB]
    +            service lambda(lambda)[Lambda] in vpc
    +
    +            service serv1(ec2)[Public Server] in vpc2
    +            service serv2(ec2)[Private Server] in vpc2
    +
    +            serv1 L--R serv2
    +
    +            s3 L--R rds
    +            s3 B--T ddb
    +            s3 R--L docdb
    +            gateway L--R ec2
    +            gateway T--B rds
    +            gateway B--T lambda
    +    
    + - - + }, + architecture: { + iconSize: 80, + }, + useMaxWidth: false, + }); + function callback() { + alert('It worked'); + } + mermaid.parseError = function (err, hash) { + console.error('In parse error:'); + console.error(err); + }; + + + + \ No newline at end of file diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index d3338260fe..3c30ca1d47 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -128,6 +128,8 @@ const addEdge = function ({ rhsDir, lhsInto, rhsInto, + lhsGroup, + rhsGroup, title, }: ArchitectureEdge) { if (!isArchitectureDirection(lhsDir)) { @@ -152,14 +154,29 @@ const addEdge = function ({ ); } + const lhsGroupId = state.records.services[lhsId].in + const rhsGroupId = state.records.services[rhsId].in + if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { + throw new Error( + `The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` + ) + } + if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { + throw new Error( + `The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` + ) + } + const edge = { lhsId, lhsDir, + lhsInto, + lhsGroup, rhsId, rhsDir, - title, - lhsInto, rhsInto, + rhsGroup, + title, }; state.records.edges.push(edge); diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index dd02c2f3d5..af90a58ddb 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -84,7 +84,7 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) { edges.forEach((parsedEdge) => { - const { lhsId, rhsId, lhsInto, rhsInto, lhsDir, rhsDir, title } = parsedEdge; + const { lhsId, rhsId, lhsInto, lhsGroup, rhsInto, lhsDir, rhsDir, rhsGroup, title } = parsedEdge; const edgeType = isArchitectureDirectionXY(parsedEdge.lhsDir, parsedEdge.rhsDir) ? 'segments' : 'straight'; @@ -94,6 +94,7 @@ function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) { source: lhsId, sourceDir: lhsDir, sourceArrow: lhsInto, + sourceGroup: lhsGroup, sourceEndpoint: lhsDir === 'L' ? '0 50%' @@ -105,6 +106,7 @@ function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) { target: rhsId, targetDir: rhsDir, targetArrow: rhsInto, + targetGroup: rhsGroup, targetEndpoint: rhsDir === 'L' ? '0 50%' diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index 34ea04658b..9e043d6d3c 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -199,11 +199,13 @@ export interface ArchitectureGroup { export interface ArchitectureEdge { lhsId: string; lhsDir: ArchitectureDirection; - title?: string; + lhsInto?: boolean; + lhsGroup?: boolean; rhsId: string; rhsDir: ArchitectureDirection; - lhsInto?: boolean; rhsInto?: boolean; + rhsGroup?: boolean; + title?: string; } export interface ArchitectureDB extends DiagramDB { @@ -246,9 +248,11 @@ export type EdgeSingularData = { source: string; sourceDir: ArchitectureDirection; sourceArrow?: boolean; + sourceGroup?: boolean; target: string; targetDir: ArchitectureDirection; targetArrow?: boolean; + targetGroup?: boolean; [key: string]: any; }; @@ -274,23 +278,23 @@ export interface EdgeSingular extends cytoscape.EdgeSingular { export type NodeSingularData = | { - type: 'service'; - id: string; - icon?: string; - label?: string; - parent?: string; - width: number; - height: number; - [key: string]: any; - } + type: 'service'; + id: string; + icon?: string; + label?: string; + parent?: string; + width: number; + height: number; + [key: string]: any; + } | { - type: 'group'; - id: string; - icon?: string; - label?: string; - parent?: string; - [key: string]: any; - }; + type: 'group'; + id: string; + icon?: string; + label?: string; + parent?: string; + [key: string]: any; + }; export const nodeData = (node: cytoscape.NodeSingular) => { return node.data() as NodeSingularData; diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index 5218bf1cc7..41b5135399 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -22,17 +22,37 @@ import { getConfigField } from './architectureDb.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { + const padding = getConfigField('padding'); const iconSize = getConfigField('iconSize'); const arrowSize = iconSize / 6; const halfArrowSize = arrowSize / 2; cy.edges().map((edge, id) => { - const { sourceDir, sourceArrow, targetDir, targetArrow, label } = edgeData(edge); - const { x: startX, y: startY } = edge[0].sourceEndpoint(); + const { sourceDir, sourceArrow, sourceGroup, targetDir, targetArrow, targetGroup, label } = edgeData(edge); + let { x: startX, y: startY } = edge[0].sourceEndpoint(); const { x: midX, y: midY } = edge[0].midpoint(); - const { x: endX, y: endY } = edge[0].targetEndpoint(); + let { x: endX, y: endY } = edge[0].targetEndpoint(); + + const groupEdgeShift = padding + 4; + // +18 comes from the service label height that extends the padding on the bottom side of each group + if (sourceGroup) { + if (isArchitectureDirectionX(sourceDir)) { + sourceDir === 'L' ? startX -= groupEdgeShift : startX += groupEdgeShift; + } else { + sourceDir === 'T' ? startY -= groupEdgeShift : startY += (groupEdgeShift + 18); + } + } + + if (targetGroup) { + if (isArchitectureDirectionX(targetDir)) { + targetDir === 'L' ? endX -= groupEdgeShift : endX += groupEdgeShift; + } else { + targetDir === 'T' ? endY -= groupEdgeShift : endY += (groupEdgeShift + 18); + } + } + if (edge[0]._private.rscratch) { - const bounds = edge[0]._private.rscratch; + // const bounds = edge[0]._private.rscratch; const g = edgesEl.insert('g'); @@ -42,11 +62,11 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { if (sourceArrow) { const xShift = isArchitectureDirectionX(sourceDir) - ? ArchitectureDirectionArrowShift[sourceDir](bounds.startX, arrowSize) - : bounds.startX - halfArrowSize; + ? ArchitectureDirectionArrowShift[sourceDir](startX, arrowSize) + : startX - halfArrowSize; const yShift = isArchitectureDirectionY(sourceDir) - ? ArchitectureDirectionArrowShift[sourceDir](bounds.startY, arrowSize) - : bounds.startY - halfArrowSize; + ? ArchitectureDirectionArrowShift[sourceDir](startY, arrowSize) + : startY - halfArrowSize; g.insert('polygon') .attr('points', ArchitectureDirectionArrow[sourceDir](arrowSize)) @@ -55,11 +75,11 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { } if (targetArrow) { const xShift = isArchitectureDirectionX(targetDir) - ? ArchitectureDirectionArrowShift[targetDir](bounds.endX, arrowSize) - : bounds.endX - halfArrowSize; + ? ArchitectureDirectionArrowShift[targetDir](endX, arrowSize) + : endX - halfArrowSize; const yShift = isArchitectureDirectionY(targetDir) - ? ArchitectureDirectionArrowShift[targetDir](bounds.endY, arrowSize) - : bounds.endY - halfArrowSize; + ? ArchitectureDirectionArrowShift[targetDir](endY, arrowSize) + : endY - halfArrowSize; g.insert('polygon') .attr('points', ArchitectureDirectionArrow[targetDir](arrowSize)) @@ -165,10 +185,10 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { bkgElem.attr( 'transform', 'translate(' + - (shiftedX1 + halfIconSize + 1) + - ', ' + - (shiftedY1 + halfIconSize + 1) + - ')' + (shiftedX1 + halfIconSize + 1) + + ', ' + + (shiftedY1 + halfIconSize + 1) + + ')' ); shiftedX1 += groupIconSize; // TODO: test with more values @@ -196,10 +216,10 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { textElem.attr( 'transform', 'translate(' + - (shiftedX1 + halfIconSize + 4) + - ', ' + - (shiftedY1 + halfIconSize + 2) + - ')' + (shiftedX1 + halfIconSize + 4) + + ', ' + + (shiftedY1 + halfIconSize + 2) + + ')' ); } } diff --git a/packages/parser/src/language/architecture/architecture.langium b/packages/parser/src/language/architecture/architecture.langium index 101390e841..18a823b356 100644 --- a/packages/parser/src/language/architecture/architecture.langium +++ b/packages/parser/src/language/architecture/architecture.langium @@ -30,7 +30,7 @@ Service: ; Edge: - lhsId=ARCH_ID Arrow rhsId=ARCH_ID EOL + lhsId=ARCH_ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ARCH_ID rhsGroup?=ARROW_GROUP? EOL ; terminal ARROW_DIRECTION: 'L' | 'R' | 'T' | 'B'; @@ -38,4 +38,5 @@ terminal ARCH_ID: /[\w]+/; terminal ARCH_TEXT_ICON: /\("[^"]+"\)/; terminal ARCH_ICON: /\([\w]+\)/; terminal ARCH_TITLE: /\[[\w ]+\]/; +terminal ARROW_GROUP: /\{group\}/; terminal ARROW_INTO: /\(|\)/; \ No newline at end of file From b09dc5db67000ca726cb158eae3d855bc9a4e397 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Fri, 10 May 2024 10:10:19 -0500 Subject: [PATCH 41/46] feat(arch): implemented junction nodes --- demos/architecture.html | 45 +++++++++++++- .../diagrams/architecture/architectureDb.ts | 58 ++++++++++++++----- .../architecture/architectureParser.ts | 3 +- .../architecture/architectureRenderer.ts | 41 +++++++++++-- .../architecture/architectureTypes.ts | 38 +++++++++++- .../src/diagrams/architecture/svgDraw.ts | 50 +++++++++++++++- .../architecture/architecture.langium | 5 ++ 7 files changed, 213 insertions(+), 27 deletions(-) diff --git a/demos/architecture.html b/demos/architecture.html index 2f805547ed..32edb6f1d7 100644 --- a/demos/architecture.html +++ b/demos/architecture.html @@ -16,7 +16,6 @@

    Architecture diagram demo

    -

    Simple diagram with groups

           architecture
    @@ -182,6 +181,50 @@ 

    Edge Label Test


    +

    Junction Demo

    +
    +      architecture
    +        service left_disk(disk)[Disk]
    +        service top_disk(disk)[Disk]
    +        service bottom_disk(disk)[Disk]
    +        service top_gateway(internet)[Gateway]
    +        service bottom_gateway(internet)[Gateway]
    +        junction juncC
    +        junction juncR
    +
    +        left_disk R--L juncC
    +        top_disk B--T juncC
    +        bottom_disk T--B juncC
    +        juncC R--L juncR
    +        top_gateway B--T juncR
    +        bottom_gateway T--B juncR
    +    
    +
    + +

    Junction Demo Groups

    +
    +      architecture
    +        group left
    +        group right
    +        service left_disk(disk)[Disk] in left
    +        service top_disk(disk)[Disk] in left
    +        service bottom_disk(disk)[Disk] in left
    +        service top_gateway(internet)[Gateway] in right
    +        service bottom_gateway(internet)[Gateway] in right
    +        junction juncC in left
    +        junction juncR in right
    +
    +        left_disk R--L juncC
    +        top_disk B--T juncC
    +        bottom_disk T--B juncC
    +
    +
    +        top_gateway (B--T juncR
    +        bottom_gateway (T--B juncR
    +
    +        juncC{group} R--L) juncR{group}
    +    
    +