From 22c0090979ac0d13e1d5a128fba76de8a9964ab2 Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sat, 6 Apr 2024 21:46:37 -0500 Subject: [PATCH] refactor(arch): reorganized code and added more documentation --- .../diagrams/architecture/architectureDb.ts | 168 ++++++++------- .../architecture/architectureRenderer.ts | 195 +++++++++--------- .../architecture/architectureTypes.ts | 85 +++++++- .../architecture/parser/architecture.jison | 16 +- .../src/diagrams/architecture/svgDraw.ts | 142 ++++--------- 5 files changed, 319 insertions(+), 287 deletions(-) diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index d8c15afb8..dbb9bb8ea 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -119,82 +119,7 @@ const getGroups = (): ArchitectureGroup[] => { return groups; }; -const getDataStructures = () => { - if (datastructures === undefined) { - // Create an adjacency list of the diagram to perform BFS on - // Outer reduce applied on all services - // Inner reduce applied on the edges for a service - const adjList = Object.entries(services).reduce<{ [id: string]: ArchitectureDirectionPairMap }>( - (prev, [id, service]) => { - prev[id] = service.edges.reduce((prev, edge) => { - if (edge.lhs_id === id) { - // source is LHS - const pair = getArchitectureDirectionPair(edge.lhs_dir, edge.rhs_dir); - if (pair) { - prev[pair] = edge.rhs_id; - } - } else { - // source is RHS - const pair = getArchitectureDirectionPair(edge.rhs_dir, edge.lhs_dir); - if (pair) { - prev[pair] = edge.lhs_id; - } - } - return prev; - }, {}); - return prev; - }, - {} - ); - - // Configuration for the initial pass of BFS - const [firstId, _] = Object.entries(adjList)[0]; - const visited = { [firstId]: 1 }; - const notVisited = Object.keys(adjList).reduce( - (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }), - {} as Record - ); - - // Perform BFS on adjacency list - const BFS = (startingId: string): ArchitectureSpatialMap => { - const spatialMap = { [startingId]: [0, 0] }; - const queue = [startingId]; - while (queue.length > 0) { - const id = queue.shift(); - if (id) { - visited[id] = 1; - delete notVisited[id]; - const adj = adjList[id]; - const [posX, posY] = spatialMap[id]; - Object.entries(adj).forEach(([dir, rhsId]) => { - if (!visited[rhsId]) { - spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( - [posX, posY], - dir as ArchitectureDirectionPair - ); - queue.push(rhsId); - } - }); - } - } - return spatialMap; - }; - const spatialMaps = [BFS(firstId)]; - - // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found - while (Object.keys(notVisited).length > 0) { - spatialMaps.push(BFS(Object.keys(notVisited)[0])); - } - datastructures = { - adjList, - spatialMaps, - }; - console.log(datastructures); - } - return datastructures; -}; - -const addLine = function ( +const addEdge = function ( lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id: string, @@ -238,7 +163,87 @@ const addLine = function ( services[lhs_id].edges.push(lines[lines.length - 1]); services[rhs_id].edges.push(lines[lines.length - 1]); }; -const getLines = (): ArchitectureLine[] => lines; +const getEdges = (): ArchitectureLine[] => lines; + +/** + * Returns the current diagram's adjacency list & spatial map. + * If they have not been created, run the algorithms to generate them. + * @returns + */ +const getDataStructures = () => { + if (datastructures === undefined) { + // Create an adjacency list of the diagram to perform BFS on + // Outer reduce applied on all services + // Inner reduce applied on the edges for a service + const adjList = Object.entries(services).reduce<{ [id: string]: ArchitectureDirectionPairMap }>( + (prev, [id, service]) => { + prev[id] = service.edges.reduce((prev, edge) => { + if (edge.lhs_id === id) { + // source is LHS + const pair = getArchitectureDirectionPair(edge.lhs_dir, edge.rhs_dir); + if (pair) { + prev[pair] = edge.rhs_id; + } + } else { + // source is RHS + const pair = getArchitectureDirectionPair(edge.rhs_dir, edge.lhs_dir); + if (pair) { + prev[pair] = edge.lhs_id; + } + } + return prev; + }, {}); + return prev; + }, + {} + ); + + // Configuration for the initial pass of BFS + const [firstId, _] = Object.entries(adjList)[0]; + const visited = { [firstId]: 1 }; + const notVisited = Object.keys(adjList).reduce( + (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }), + {} as Record + ); + + // Perform BFS on the adjacency list + const BFS = (startingId: string): ArchitectureSpatialMap => { + const spatialMap = { [startingId]: [0, 0] }; + const queue = [startingId]; + while (queue.length > 0) { + const id = queue.shift(); + if (id) { + visited[id] = 1; + delete notVisited[id]; + const adj = adjList[id]; + const [posX, posY] = spatialMap[id]; + Object.entries(adj).forEach(([dir, rhsId]) => { + if (!visited[rhsId]) { + spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( + [posX, posY], + dir as ArchitectureDirectionPair + ); + queue.push(rhsId); + } + }); + } + } + return spatialMap; + }; + const spatialMaps = [BFS(firstId)]; + + // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found + while (Object.keys(notVisited).length > 0) { + spatialMaps.push(BFS(Object.keys(notVisited)[0])); + } + datastructures = { + adjList, + spatialMaps, + }; + console.log(datastructures); + } + return datastructures; +}; const setElementForId = (id: string, element: D3Element) => { elements[id] = element; @@ -258,13 +263,18 @@ export const db: ArchitectureDB = { getServices, addGroup, getGroups, - addLine, - getLines, + addEdge: addEdge, + getEdges: getEdges, setElementForId, getElementById, getDataStructures, }; +/** + * Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined + * @param field + * @returns + */ function getConfigField( field: T ): Required[T] { diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index 84638de1e..832a0ce83 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -17,11 +17,12 @@ import { getOppositeArchitectureDirection, isArchitectureDirectionXY, isArchitectureDirectionY, + ArchitectureSpatialMap, } from './architectureTypes.js'; import { select } from 'd3'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import type { D3Element } from '../../mermaidAPI.js'; -import { drawEdges, drawGroups, drawService } from './svgDraw.js'; +import { drawEdges, drawGroups, drawServices } from './svgDraw.js'; import { getConfigField } from './architectureDb.js'; cytoscape.use(fcose); @@ -44,15 +45,6 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) { }); } -function drawServices( - db: ArchitectureDB, - svg: D3Element, - services: ArchitectureService[], - conf: MermaidConfig -) { - services.forEach((service) => drawService(db, svg, service, conf)); -} - function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { cy.nodes().map((node, id) => { const data = node.data(); @@ -65,7 +57,6 @@ function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { }); } - function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { groups.forEach((group) => { cy.add({ @@ -121,6 +112,92 @@ function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) { }); } +function getAlignments(spatialMaps: ArchitectureSpatialMap[]): fcose.FcoseAlignmentConstraint { + const alignments = spatialMaps.map((spatialMap) => { + const horizontalAlignments: Record = {}; + const verticalAlignments: Record = {}; + // Group service ids in an object with their x and y coordinate as the key + Object.entries(spatialMap).forEach(([id, [x, y]]) => { + if (!horizontalAlignments[y]) horizontalAlignments[y] = []; + if (!verticalAlignments[x]) verticalAlignments[x] = []; + horizontalAlignments[y].push(id); + verticalAlignments[x].push(id); + }); + // Merge the values of each object into a list if the inner list has at least 2 elements + return { + horiz: Object.values(horizontalAlignments).filter((arr) => arr.length > 1), + vert: Object.values(verticalAlignments).filter((arr) => arr.length > 1), + }; + }); + + // Merge the alginment lists for each spatial map into one 2d array per axis + const [horizontal, vertical] = alignments.reduce( + ([prevHoriz, prevVert], { horiz, vert }) => { + return [ + [...prevHoriz, ...horiz], + [...prevVert, ...vert], + ]; + }, + [[] as string[][], [] as string[][]] + ); + + return { + horizontal, + vertical, + }; +} + +function getRelativeConstraints( + spatialMaps: ArchitectureSpatialMap[] +): fcose.FcoseRelativePlacementConstraint[] { + const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []; + const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`; + const strToPos = (pos: string) => pos.split(',').map((p) => parseInt(p)); + + spatialMaps.forEach((spatialMap) => { + const invSpatialMap = Object.fromEntries( + Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]) + ); + + // perform BFS + const queue = [posToStr([0, 0])]; + const visited: Record = {}; + const directions: Record = { + L: [-1, 0], + R: [1, 0], + T: [0, 1], + B: [0, -1], + }; + while (queue.length > 0) { + const curr = queue.shift(); + if (curr) { + visited[curr] = 1; + const currId = invSpatialMap[curr]; + if (currId) { + const currPos = strToPos(curr); + Object.entries(directions).forEach(([dir, shift]) => { + const newPos = posToStr([currPos[0] + shift[0], currPos[1] + shift[1]]); + const newId = invSpatialMap[newPos]; + // If there is an adjacent service to the current one and it has not yet been visited + if (newId && !visited[newPos]) { + queue.push(newPos); + // @ts-ignore cannot determine if left/right or top/bottom are paired together + relativeConstraints.push({ + [ArchitectureDirectionName[dir as ArchitectureDirection]]: newId, + [ArchitectureDirectionName[ + getOppositeArchitectureDirection(dir as ArchitectureDirection) + ]]: currId, + gap: 1.5 * getConfigField('iconSize'), + }); + } + }); + } + } + } + }); + return relativeConstraints; +} + function layoutArchitecture( services: ArchitectureService[], groups: ArchitectureGroup[], @@ -192,91 +269,17 @@ function layoutArchitecture( addEdges(lines, cy); // Use the spatial map to create alignment arrays for fcose - const [horizontalAlignments, verticalAlignments] = (() => { - const alignments = spatialMaps.map((spatialMap) => { - const _horizontalAlignments: Record = {}; - const _verticalAlignments: Record = {}; - // Group service ids in an object with their x and y coordinate as the key - Object.entries(spatialMap).forEach(([id, [x, y]]) => { - if (!_horizontalAlignments[y]) _horizontalAlignments[y] = []; - if (!_verticalAlignments[x]) _verticalAlignments[x] = []; - _horizontalAlignments[y].push(id); - _verticalAlignments[x].push(id); - }); - // Merge the values of each object into a list if the inner list has at least 2 elements - return { - horiz: Object.values(_horizontalAlignments).filter((arr) => arr.length > 1), - vert: Object.values(_verticalAlignments).filter((arr) => arr.length > 1), - }; - }); - - // Merge the alginment lists for each spatial map into one 2d array per axis - return alignments.reduce( - ([prevHoriz, prevVert], { horiz, vert }) => { - return [ - [...prevHoriz, ...horiz], - [...prevVert, ...vert], - ]; - }, - [[] as string[][], [] as string[][]] - ); - })(); + const alignmentConstraint = getAlignments(spatialMaps); // Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it - const relativeConstraints = (() => { - const _relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []; - const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`; - const strToPos = (pos: string) => pos.split(',').map((p) => parseInt(p)); + const relativePlacementConstraint = getRelativeConstraints(spatialMaps); - spatialMaps.forEach((spatialMap) => { - const invSpatialMap = Object.fromEntries( - Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]) - ); - - // perform BFS - const queue = [posToStr([0, 0])]; - const visited: Record = {}; - const directions: Record = { - L: [-1, 0], - R: [1, 0], - T: [0, 1], - B: [0, -1], - }; - while (queue.length > 0) { - const curr = queue.shift(); - if (curr) { - visited[curr] = 1; - const currId = invSpatialMap[curr]; - if (currId) { - const currPos = strToPos(curr); - Object.entries(directions).forEach(([dir, shift]) => { - const newPos = posToStr([currPos[0] + shift[0], currPos[1] + shift[1]]); - const newId = invSpatialMap[newPos]; - // If there is an adjacent service to the current one and it has not yet been visited - if (newId && !visited[newPos]) { - queue.push(newPos); - // @ts-ignore cannot determine if left/right or top/bottom are paired together - _relativeConstraints.push({ - [ArchitectureDirectionName[dir as ArchitectureDirection]]: newId, - [ArchitectureDirectionName[ - getOppositeArchitectureDirection(dir as ArchitectureDirection) - ]]: currId, - gap: 1.5 * getConfigField('iconSize'), - }); - } - }); - } - } - } - }); - return _relativeConstraints; - })(); console.log(`Horizontal Alignments:`); - console.log(horizontalAlignments); + console.log(alignmentConstraint.horizontal); console.log(`Vertical Alignments:`); - console.log(verticalAlignments); + console.log(alignmentConstraint.vertical); console.log(`Relative Alignments:`); - console.log(relativeConstraints); + console.log(relativePlacementConstraint); const layout = cy.layout({ name: 'fcose', @@ -301,13 +304,11 @@ function layoutArchitecture( const elasticity = parentA === parentB ? 0.45 : 0.001; return elasticity; }, - alignmentConstraint: { - horizontal: horizontalAlignments, - vertical: verticalAlignments, - }, - relativePlacementConstraint: relativeConstraints, + alignmentConstraint, + relativePlacementConstraint, } as FcoseLayoutOptions); + // Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend layout.one('layoutstop', (_event) => { function getSegmentWeights( source: Position, @@ -392,7 +393,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) const services = db.getServices(); const groups = db.getGroups(); - const lines = db.getLines(); + const lines = db.getEdges(); const ds = db.getDataStructures(); console.log('Services: ', services); console.log('Lines: ', lines); @@ -409,7 +410,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) const groupElem = svg.append('g'); groupElem.attr('class', 'architecture-groups'); - drawServices(db, servicesElem, services, conf); + drawServices(db, servicesElem, services); const cy = await layoutArchitecture(services, groups, lines, ds); // console.log(cy.nodes().map((node) => ({ a: node.data() }))); diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index bf8738e06..cb92b056d 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -2,6 +2,10 @@ import type { DiagramDB } from '../../diagram-api/types.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js'; import type { D3Element } from '../../mermaidAPI.js'; +/*=======================================*\ +| Architecture Diagram Types | +\*=======================================*/ + export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B'; export type ArchitectureDirectionX = Extract; export type ArchitectureDirectionY = Extract; @@ -66,13 +70,18 @@ export const isArchitectureDirectionXY = function ( }; /** - * Contains LL, RR, TT, BB which are impossible conections + * Contains LL, RR, TT, BB which are impossible connections */ export type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`; export type ArchitectureDirectionPair = Exclude< InvalidArchitectureDirectionPair, 'LL' | 'RR' | 'TT' | 'BB' >; +/** + * Verifies that the architecture direction pair does not contain an invalid match (LL, RR, TT, BB) + * @param x architecture direction pair which could potentially be invalid + * @returns true if the pair is not LL, RR, TT, or BB + */ export const isValidArchitectureDirectionPair = function ( x: InvalidArchitectureDirectionPair ): x is ArchitectureDirectionPair { @@ -98,6 +107,12 @@ export const getArchitectureDirectionPair = function ( return isValidArchitectureDirectionPair(pair) ? pair : undefined; }; +/** + * Given an x,y position for an arrow and the direction of the edge it belongs to, return a factor for slightly shifting the edge + * @param param0 [x, y] coordinate pair + * @param pair architecture direction pair + * @returns a new [x, y] coordinate pair + */ export const shiftPositionByArchitectureDirectionPair = function ( [x, y]: number[], pair: ArchitectureDirectionPair @@ -156,14 +171,14 @@ export interface ArchitectureDB extends DiagramDB { getServices: () => ArchitectureService[]; addGroup: (id: string, opts: Omit) => void; getGroups: () => ArchitectureGroup[]; - addLine: ( + addEdge: ( lhs_id: string, lhs_dir: ArchitectureDirection, rhs_id: string, rhs_dir: ArchitectureDirection, opts: Omit ) => void; - getLines: () => ArchitectureLine[]; + getEdges: () => ArchitectureLine[]; setElementForId: (id: string, element: D3Element) => void; getElementById: (id: string) => D3Element; getDataStructures: () => ArchitectureDataStructures; @@ -184,3 +199,67 @@ export interface ArchitectureFields { datastructures?: ArchitectureDataStructures; config: ArchitectureDiagramConfig; } + +/*=======================================*\ +| Cytoscape Override Types | +\*=======================================*/ + +declare module 'cytoscape' { + type _EdgeSingularData = { + id: string; + source: string; + sourceDir: ArchitectureDirection; + sourceArrow?: boolean; + target: string; + targetDir: ArchitectureDirection; + targetArrow?: boolean; + [key: string]: any; + }; + interface EdgeSingular { + _private: { + bodyBounds: unknown; + rscratch: { + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + }; + }; + data(): _EdgeSingularData; + data(key: T): _EdgeSingularData[T]; + } + 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; + }; + } +} diff --git a/packages/mermaid/src/diagrams/architecture/parser/architecture.jison b/packages/mermaid/src/diagrams/architecture/parser/architecture.jison index 8f96cc383..d3c9640f8 100644 --- a/packages/mermaid/src/diagrams/architecture/parser/architecture.jison +++ b/packages/mermaid/src/diagrams/architecture/parser/architecture.jison @@ -55,21 +55,21 @@ 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}) } + { yy.addEdge($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}) } + { yy.addEdge($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}) } + { yy.addEdge($1, $2[0], $4, $3[1], {rhs_into: true}) } | id ARROW_LEFT ARROW_RIGHT id - { yy.addLine($1, $2[0], $4, $3[1]) } + { yy.addEdge($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 }) } + { yy.addEdge($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 }) } + { yy.addEdge($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 }) } + { yy.addEdge($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) }) } + { yy.addEdge($1, $2[0], $5, $4[1], { title: $3.slice(1,-1) }) } ; group_statement diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index 6b1555599..a603919a8 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -3,78 +3,16 @@ import { createText } from '../../rendering-util/createText.js'; import { ArchitectureDirectionArrow, type ArchitectureDB, - type ArchitectureDirection, type ArchitectureService, ArchitectureDirectionArrowShift, isArchitectureDirectionX, isArchitectureDirectionY, } from './architectureTypes.js'; -import type { MermaidConfig } from '../../config.type.js'; import type cytoscape from 'cytoscape'; import { log } from '../../logger.js'; import { getIcon } from '../../rendering-util/svgRegister.js'; import { getConfigField } from './architectureDb.js'; -declare module 'cytoscape' { - type _EdgeSingularData = { - id: string; - source: string; - sourceDir: ArchitectureDirection; - sourceArrow?: boolean; - target: string; - targetDir: ArchitectureDirection; - targetArrow?: boolean; - [key: string]: any; - }; - interface EdgeSingular { - _private: { - bodyBounds: unknown; - rscratch: { - startX: number; - startY: number; - midX: number; - midY: number; - endX: number; - endY: number; - }; - }; - data(): _EdgeSingularData; - data(key: T): _EdgeSingularData[T]; - } - 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, cy: cytoscape.Core) { const iconSize = getConfigField('iconSize'); const arrowSize = iconSize / 6; @@ -161,51 +99,55 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { }); }; -export const drawService = function ( +export const drawServices = function ( db: ArchitectureDB, elem: D3Element, - service: ArchitectureService, - conf: MermaidConfig + services: ArchitectureService[] ): number { - const serviceElem = elem.append('g'); - const iconSize = getConfigField('iconSize'); + services.forEach((service) => { + const serviceElem = elem.append('g'); + const iconSize = getConfigField('iconSize'); - if (service.title) { - const textElem = serviceElem.append('g'); - createText(textElem, service.title, { - useHtmlLabels: false, - width: iconSize * 1.5, - classes: 'architecture-service-label', - }); - textElem - .attr('dy', '1em') - .attr('alignment-baseline', 'middle') - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'middle'); + if (service.title) { + const textElem = serviceElem.append('g'); + createText(textElem, service.title, { + useHtmlLabels: false, + width: iconSize * 1.5, + classes: 'architecture-service-label', + }); + textElem + .attr('dy', '1em') + .attr('alignment-baseline', 'middle') + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'middle'); - textElem.attr('transform', 'translate(' + iconSize / 2 + ', ' + iconSize + ')'); - } + textElem.attr('transform', 'translate(' + iconSize / 2 + ', ' + iconSize + ')'); + } - let bkgElem = serviceElem.append('g'); - if (service.icon) { - // TODO: should a warning be given to end-users saying which icon names are available? - // if (!isIconNameInUse(service.icon)) { - // throw new Error(`Invalid SVG Icon name: "${service.icon}"`); - // } - bkgElem = getIcon(service.icon)?.(bkgElem, iconSize); - } else { - bkgElem - .append('path') - .attr('class', 'node-bkg') - .attr('id', 'node-' + service.id) - .attr('d', `M0 ${iconSize} v${-iconSize} q0,-5 5,-5 h${iconSize} q5,0 5,5 v${iconSize} H0 Z`); - } + let bkgElem = serviceElem.append('g'); + if (service.icon) { + // TODO: should a warning be given to end-users saying which icon names are available? + // if (!isIconNameInUse(service.icon)) { + // throw new Error(`Invalid SVG Icon name: "${service.icon}"`); + // } + bkgElem = getIcon(service.icon)?.(bkgElem, iconSize); + } else { + bkgElem + .append('path') + .attr('class', 'node-bkg') + .attr('id', 'node-' + service.id) + .attr( + 'd', + `M0 ${iconSize} v${-iconSize} q0,-5 5,-5 h${iconSize} q5,0 5,5 v${iconSize} H0 Z` + ); + } - serviceElem.attr('class', 'architecture-service'); + serviceElem.attr('class', 'architecture-service'); - const { width, height } = serviceElem._groups[0][0].getBBox(); - service.width = width; - service.height = height; - db.setElementForId(service.id, serviceElem); + const { width, height } = serviceElem._groups[0][0].getBBox(); + service.width = width; + service.height = height; + db.setElementForId(service.id, serviceElem); + }); return 0; };