mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-28 07:03:17 +08:00
feat(arch): implemented junction nodes
This commit is contained in:
parent
0049127bb7
commit
b09dc5db67
@ -16,7 +16,6 @@
|
||||
|
||||
<body>
|
||||
<h1>Architecture diagram demo</h1>
|
||||
|
||||
<h2>Simple diagram with groups</h2>
|
||||
<pre class="mermaid">
|
||||
architecture
|
||||
@ -182,6 +181,50 @@
|
||||
</pre>
|
||||
|
||||
<hr />
|
||||
<h2>Junction Demo</h2>
|
||||
<pre class="mermaid">
|
||||
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
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<h2>Junction Demo Groups</h2>
|
||||
<pre class="mermaid">
|
||||
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}
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
|
@ -9,11 +9,15 @@ import type {
|
||||
ArchitectureDirectionPairMap,
|
||||
ArchitectureDirectionPair,
|
||||
ArchitectureSpatialMap,
|
||||
ArchitectureNode,
|
||||
ArchitectureJunction,
|
||||
} from './architectureTypes.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import {
|
||||
getArchitectureDirectionPair,
|
||||
isArchitectureDirection,
|
||||
isArchitectureJunction,
|
||||
isArchitectureService,
|
||||
shiftPositionByArchitectureDirectionPair,
|
||||
} from './architectureTypes.js';
|
||||
import {
|
||||
@ -34,7 +38,7 @@ const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||
DEFAULT_CONFIG.architecture;
|
||||
|
||||
const state = new ImperativeState<ArchitectureState>(() => ({
|
||||
services: {},
|
||||
nodes: {},
|
||||
groups: {},
|
||||
edges: [],
|
||||
registeredIds: {},
|
||||
@ -69,15 +73,16 @@ const addService = function ({
|
||||
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'service') {
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The service [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
|
||||
state.records.registeredIds[id] = 'service';
|
||||
state.records.registeredIds[id] = 'node';
|
||||
|
||||
state.records.services[id] = {
|
||||
state.records.nodes[id] = {
|
||||
id,
|
||||
type: 'service',
|
||||
icon,
|
||||
iconText,
|
||||
title,
|
||||
@ -86,7 +91,27 @@ const addService = function ({
|
||||
};
|
||||
};
|
||||
|
||||
const getServices = (): ArchitectureService[] => Object.values(state.records.services);
|
||||
const getServices = (): ArchitectureService[] => Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
|
||||
|
||||
const addJunction = function ({
|
||||
id, in: parent
|
||||
}: Omit<ArchitectureJunction, 'edges'>) {
|
||||
state.records.registeredIds[id] = 'node';
|
||||
|
||||
state.records.nodes[id] = {
|
||||
id,
|
||||
type: 'junction',
|
||||
edges: [],
|
||||
in: parent,
|
||||
};
|
||||
}
|
||||
|
||||
const getJunctions = (): ArchitectureJunction[] => Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);
|
||||
|
||||
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);
|
||||
|
||||
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];
|
||||
|
||||
|
||||
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
|
||||
if (state.records.registeredIds[id] !== undefined) {
|
||||
@ -103,7 +128,7 @@ const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
|
||||
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
|
||||
);
|
||||
}
|
||||
if (state.records.registeredIds[parent] === 'service') {
|
||||
if (state.records.registeredIds[parent] === 'node') {
|
||||
throw new Error(`The group [${id}]'s parent is not a group`);
|
||||
}
|
||||
}
|
||||
@ -143,19 +168,19 @@ const addEdge = function ({
|
||||
);
|
||||
}
|
||||
|
||||
if (state.records.services[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
if (state.records.services[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
|
||||
throw new Error(
|
||||
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
|
||||
);
|
||||
}
|
||||
|
||||
const lhsGroupId = state.records.services[lhsId].in
|
||||
const rhsGroupId = state.records.services[rhsId].in
|
||||
const lhsGroupId = state.records.nodes[lhsId].in
|
||||
const rhsGroupId = state.records.nodes[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.`
|
||||
@ -180,10 +205,9 @@ const addEdge = function ({
|
||||
};
|
||||
|
||||
state.records.edges.push(edge);
|
||||
if (state.records.services[lhsId] && state.records.services[rhsId]) {
|
||||
state.records.services[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
state.records.services[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
} else if (state.records.groups[lhsId] && state.records.groups[rhsId]) {
|
||||
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {
|
||||
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -199,7 +223,7 @@ const getDataStructures = () => {
|
||||
// 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(state.records.services).reduce<{
|
||||
const adjList = Object.entries(state.records.nodes).reduce<{
|
||||
[id: string]: ArchitectureDirectionPairMap;
|
||||
}>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
@ -284,6 +308,10 @@ export const db: ArchitectureDB = {
|
||||
|
||||
addService,
|
||||
getServices,
|
||||
addJunction,
|
||||
getJunctions,
|
||||
getNodes,
|
||||
getNode,
|
||||
addGroup,
|
||||
getGroups,
|
||||
addEdge,
|
||||
|
@ -9,7 +9,8 @@ import { db } from './architectureDb.js';
|
||||
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
|
||||
populateCommonDb(ast, db);
|
||||
ast.groups.map(db.addGroup);
|
||||
ast.services.map(db.addService);
|
||||
ast.services.map((service) => db.addService({ ...service, type: 'service' }));
|
||||
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
|
||||
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?
|
||||
ast.edges.map(db.addEdge);
|
||||
};
|
||||
|
@ -13,6 +13,8 @@ import type {
|
||||
ArchitectureSpatialMap,
|
||||
EdgeSingularData,
|
||||
EdgeSingular,
|
||||
ArchitectureJunction,
|
||||
NodeSingularData,
|
||||
} from './architectureTypes.js';
|
||||
import {
|
||||
type ArchitectureDB,
|
||||
@ -29,7 +31,7 @@ import {
|
||||
} from './architectureTypes.js';
|
||||
import { select } from 'd3';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import { drawEdges, drawGroups, drawServices } from './svgDraw.js';
|
||||
import { drawEdges, drawGroups, drawJunctions, drawServices } from './svgDraw.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
|
||||
cytoscape.use(fcose);
|
||||
@ -46,13 +48,29 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
|
||||
parent: service.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
},
|
||||
} as NodeSingularData,
|
||||
classes: 'node-service',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function positionServices(db: ArchitectureDB, cy: cytoscape.Core) {
|
||||
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
|
||||
junctions.forEach((junction) => {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
type: 'junction',
|
||||
id: junction.id,
|
||||
parent: junction.in,
|
||||
width: getConfigField('iconSize'),
|
||||
height: getConfigField('iconSize'),
|
||||
} as NodeSingularData,
|
||||
classes: 'node-junction',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function positionNodes(db: ArchitectureDB, cy: cytoscape.Core) {
|
||||
cy.nodes().map((node) => {
|
||||
const data = nodeData(node);
|
||||
if (data.type === 'group') {
|
||||
@ -76,7 +94,7 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
|
||||
icon: group.icon,
|
||||
label: group.title,
|
||||
parent: group.in,
|
||||
},
|
||||
} as NodeSingularData,
|
||||
classes: 'node-group',
|
||||
});
|
||||
});
|
||||
@ -216,6 +234,7 @@ function getRelativeConstraints(
|
||||
|
||||
function layoutArchitecture(
|
||||
services: ArchitectureService[],
|
||||
junctions: ArchitectureJunction[],
|
||||
groups: ArchitectureGroup[],
|
||||
edges: ArchitectureEdge[],
|
||||
{ spatialMaps }: ArchitectureDataStructures
|
||||
@ -269,6 +288,13 @@ function layoutArchitecture(
|
||||
height: 'data(height)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.node-junction',
|
||||
style: {
|
||||
width: 'data(width)',
|
||||
height: 'data(height)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.node-group',
|
||||
style: {
|
||||
@ -283,6 +309,7 @@ function layoutArchitecture(
|
||||
|
||||
addGroups(groups, cy);
|
||||
addServices(services, cy);
|
||||
addJunctions(junctions, cy);
|
||||
addEdges(edges, cy);
|
||||
|
||||
// Use the spatial map to create alignment arrays for fcose
|
||||
@ -408,6 +435,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
const db = diagObj.db as ArchitectureDB;
|
||||
|
||||
const services = db.getServices();
|
||||
const junctions = db.getJunctions();
|
||||
const groups = db.getGroups();
|
||||
const edges = db.getEdges();
|
||||
const ds = db.getDataStructures();
|
||||
@ -427,12 +455,13 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
groupElem.attr('class', 'architecture-groups');
|
||||
|
||||
drawServices(db, servicesElem, services);
|
||||
drawJunctions(db, servicesElem, junctions);
|
||||
|
||||
const cy = await layoutArchitecture(services, groups, edges, ds);
|
||||
const cy = await layoutArchitecture(services, junctions, groups, edges, ds);
|
||||
|
||||
drawEdges(edgesElem, cy);
|
||||
drawGroups(groupElem, cy);
|
||||
positionServices(db, cy);
|
||||
positionNodes(db, cy);
|
||||
|
||||
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
|
||||
|
||||
|
@ -180,6 +180,7 @@ export interface ArchitectureStyleOptions {
|
||||
|
||||
export interface ArchitectureService {
|
||||
id: string;
|
||||
type: 'service';
|
||||
edges: ArchitectureEdge[];
|
||||
icon?: string;
|
||||
iconText?: string;
|
||||
@ -189,6 +190,27 @@ export interface ArchitectureService {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface ArchitectureJunction {
|
||||
id: string;
|
||||
type: 'junction';
|
||||
edges: ArchitectureEdge[];
|
||||
in?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ArchitectureNode = ArchitectureService | ArchitectureJunction;
|
||||
|
||||
export const isArchitectureService = function (x: ArchitectureNode): x is ArchitectureService {
|
||||
const temp = x as ArchitectureService;
|
||||
return temp.type === 'service';
|
||||
};
|
||||
|
||||
export const isArchitectureJunction = function (x: ArchitectureNode): x is ArchitectureJunction {
|
||||
const temp = x as ArchitectureJunction;
|
||||
return temp.type === 'junction';
|
||||
};
|
||||
|
||||
export interface ArchitectureGroup {
|
||||
id: string;
|
||||
icon?: string;
|
||||
@ -212,6 +234,10 @@ export interface ArchitectureDB extends DiagramDB {
|
||||
clear: () => void;
|
||||
addService: (service: Omit<ArchitectureService, 'edges'>) => void;
|
||||
getServices: () => ArchitectureService[];
|
||||
addJunction: (service: Omit<ArchitectureJunction, 'edges'>) => void;
|
||||
getJunctions: () => ArchitectureJunction[];
|
||||
getNodes: () => ArchitectureNode[];
|
||||
getNode: (id: string) => ArchitectureNode | null;
|
||||
addGroup: (group: ArchitectureGroup) => void;
|
||||
getGroups: () => ArchitectureGroup[];
|
||||
addEdge: (edge: ArchitectureEdge) => void;
|
||||
@ -229,10 +255,10 @@ export type ArchitectureDataStructures = {
|
||||
};
|
||||
|
||||
export interface ArchitectureState extends Record<string, unknown> {
|
||||
services: Record<string, ArchitectureService>;
|
||||
nodes: Record<string, ArchitectureNode>;
|
||||
groups: Record<string, ArchitectureGroup>;
|
||||
edges: ArchitectureEdge[];
|
||||
registeredIds: Record<string, 'service' | 'group'>;
|
||||
registeredIds: Record<string, 'node' | 'group'>;
|
||||
dataStructures?: ArchitectureDataStructures;
|
||||
elements: Record<string, D3Element>;
|
||||
config: ArchitectureDiagramConfig;
|
||||
@ -287,6 +313,14 @@ export type NodeSingularData =
|
||||
height: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
| {
|
||||
type: 'junction';
|
||||
id: string;
|
||||
parent?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
| {
|
||||
type: 'group';
|
||||
id: string;
|
||||
|
@ -15,24 +15,27 @@ import {
|
||||
getArchitectureDirectionPair,
|
||||
getArchitectureDirectionXYFactors,
|
||||
isArchitecturePairXY,
|
||||
ArchitectureJunction,
|
||||
} from './architectureTypes.js';
|
||||
import type cytoscape from 'cytoscape';
|
||||
import { getIcon } from '../../rendering-util/svgRegister.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
import { db, 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 halfIconSize = iconSize / 2;
|
||||
const arrowSize = iconSize / 6;
|
||||
const halfArrowSize = arrowSize / 2;
|
||||
|
||||
cy.edges().map((edge, id) => {
|
||||
const { sourceDir, sourceArrow, sourceGroup, targetDir, targetArrow, targetGroup, label } = edgeData(edge);
|
||||
const { source, sourceDir, sourceArrow, sourceGroup, target, targetDir, targetArrow, targetGroup, label } = edgeData(edge);
|
||||
let { x: startX, y: startY } = edge[0].sourceEndpoint();
|
||||
const { x: midX, y: midY } = edge[0].midpoint();
|
||||
let { x: endX, y: endY } = edge[0].targetEndpoint();
|
||||
|
||||
// Adjust the edge distance if it has the {group} modifier
|
||||
const groupEdgeShift = padding + 4;
|
||||
// +18 comes from the service label height that extends the padding on the bottom side of each group
|
||||
if (sourceGroup) {
|
||||
@ -51,6 +54,22 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the edge distance if it doesn't have the {group} modifier and the endpoint is a junction node
|
||||
if (!sourceGroup && db.getNode(source)?.type === 'junction') {
|
||||
if (isArchitectureDirectionX(sourceDir)) {
|
||||
sourceDir === 'L' ? startX += halfIconSize : startX -= halfIconSize;
|
||||
} else {
|
||||
sourceDir === 'T' ? startY += halfIconSize : startY -= halfIconSize;
|
||||
}
|
||||
}
|
||||
if (!targetGroup && db.getNode(target)?.type === 'junction') {
|
||||
if (isArchitectureDirectionX(targetDir)) {
|
||||
targetDir === 'L' ? endX += halfIconSize : endX -= halfIconSize;
|
||||
} else {
|
||||
targetDir === 'T' ? endY += halfIconSize : endY -= halfIconSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (edge[0]._private.rscratch) {
|
||||
// const bounds = edge[0]._private.rscratch;
|
||||
|
||||
@ -305,3 +324,30 @@ export const drawServices = function (
|
||||
});
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const drawJunctions = function (
|
||||
db: ArchitectureDB,
|
||||
elem: D3Element,
|
||||
junctions: ArchitectureJunction[]
|
||||
|
||||
) {
|
||||
junctions.forEach((junction) => {
|
||||
const junctionElem = elem.append('g');
|
||||
const iconSize = getConfigField('iconSize');
|
||||
|
||||
let bkgElem = junctionElem.append('g');
|
||||
bkgElem
|
||||
.append('rect')
|
||||
.attr('id', 'node-' + junction.id)
|
||||
.attr('fill-opacity', '0')
|
||||
.attr('width', iconSize)
|
||||
.attr('height', iconSize);
|
||||
|
||||
junctionElem.attr('class', 'architecture-junction');
|
||||
|
||||
const { width, height } = junctionElem._groups[0][0].getBBox();
|
||||
junctionElem.width = width;
|
||||
junctionElem.height = height;
|
||||
db.setElementForId(junction.id, junctionElem);
|
||||
});
|
||||
}
|
@ -14,6 +14,7 @@ entry Architecture:
|
||||
fragment Statement:
|
||||
groups+=Group
|
||||
| services+=Service
|
||||
| junctions+=Junction
|
||||
| edges+=Edge
|
||||
;
|
||||
|
||||
@ -29,6 +30,10 @@ Service:
|
||||
'service' id=ARCH_ID (iconText=ARCH_TEXT_ICON | icon=ARCH_ICON)? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL
|
||||
;
|
||||
|
||||
Junction:
|
||||
'junction' id=ARCH_ID ('in' in=ARCH_ID)? EOL
|
||||
;
|
||||
|
||||
Edge:
|
||||
lhsId=ARCH_ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ARCH_ID rhsGroup?=ARROW_GROUP? EOL
|
||||
;
|
||||
|
Loading…
x
Reference in New Issue
Block a user