Skip to content

Commit

Permalink
feat(arch): implemented junction nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasNewman committed May 10, 2024
1 parent 0049127 commit b09dc5d
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 27 deletions.
45 changes: 44 additions & 1 deletion demos/architecture.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

<body>
<h1>Architecture diagram demo</h1>

<h2>Simple diagram with groups</h2>
<pre class="mermaid">
architecture
Expand Down Expand Up @@ -182,6 +181,50 @@ <h2>Edge Label Test</h2>
</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';
Expand Down
58 changes: 43 additions & 15 deletions packages/mermaid/src/diagrams/architecture/architectureDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,7 +38,7 @@ const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
DEFAULT_CONFIG.architecture;

const state = new ImperativeState<ArchitectureState>(() => ({
services: {},
nodes: {},
groups: {},
edges: [],
registeredIds: {},
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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`);
}
}
Expand Down Expand Up @@ -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.`
Expand All @@ -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]);
}
};

Expand All @@ -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) => {
Expand Down Expand Up @@ -284,6 +308,10 @@ export const db: ArchitectureDB = {

addService,
getServices,
addJunction,
getJunctions,
getNodes,
getNode,
addGroup,
getGroups,
addEdge,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
41 changes: 35 additions & 6 deletions packages/mermaid/src/diagrams/architecture/architectureRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
ArchitectureSpatialMap,
EdgeSingularData,
EdgeSingular,
ArchitectureJunction,
NodeSingularData,
} from './architectureTypes.js';
import {
type ArchitectureDB,
Expand All @@ -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);
Expand All @@ -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') {
Expand All @@ -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',
});
});
Expand Down Expand Up @@ -216,6 +234,7 @@ function getRelativeConstraints(

function layoutArchitecture(
services: ArchitectureService[],
junctions: ArchitectureJunction[],
groups: ArchitectureGroup[],
edges: ArchitectureEdge[],
{ spatialMaps }: ArchitectureDataStructures
Expand Down Expand Up @@ -269,6 +288,13 @@ function layoutArchitecture(
height: 'data(height)',
},
},
{
selector: '.node-junction',
style: {
width: 'data(width)',
height: 'data(height)',
},
},
{
selector: '.node-group',
style: {
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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'));

Expand Down
38 changes: 36 additions & 2 deletions packages/mermaid/src/diagrams/architecture/architectureTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export interface ArchitectureStyleOptions {

export interface ArchitectureService {
id: string;
type: 'service';
edges: ArchitectureEdge[];
icon?: string;
iconText?: string;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit b09dc5d

Please sign in to comment.