feat(arch): edge labels implemented

This commit is contained in:
NicolasNewman
2024-04-17 12:27:53 -05:00
parent cb302a08b8
commit a5d3164ea4
5 changed files with 181 additions and 24 deletions

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -120,6 +120,46 @@
</pre>
<hr />
<h2>Edge Label Test</h2>
<pre class="mermaid">
architecture
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC L-[Label]-R servL
servC R-[Label]-L servR
servC T-[Label]-B servT
servC B-[Label]-T servB
servL T-[Label]-L servT
servL B-[Label]-L servB
servR T-[Label]-R servT
servR B-[Label]-R servB
</pre>
<pre class="mermaid">
architecture
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC L-[Label that is Long]-R servL
servC R-[Label that is Long]-L servR
servC T-[Label that is Long]-B servT
servC B-[Label that is Long]-T servB
servL T-[Label that is Long]-L servT
servL B-[Label that is Long]-L servB
servR T-[Label that is Long]-R servT
servR B-[Label that is Long]-R servB
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';

View File

@@ -1,3 +1,5 @@
// TODO remove no-console
/* eslint-disable no-console */
import type { Position } from 'cytoscape';
import cytoscape from 'cytoscape';
import type { Diagram } from '../../Diagram.js';
@@ -80,12 +82,13 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {
edges.forEach((parsedEdge) => {
const { lhsId, rhsId, lhsInto, rhsInto, lhsDir, rhsDir } = parsedEdge;
const { lhsId, rhsId, lhsInto, rhsInto, lhsDir, rhsDir, title } = parsedEdge;
const edgeType = isArchitectureDirectionXY(parsedEdge.lhsDir, parsedEdge.rhsDir)
? 'segments'
: 'straight';
const edge: EdgeSingularData = {
id: `${lhsId}-${rhsId}`,
label: title,
source: lhsId,
sourceDir: lhsDir,
sourceArrow: lhsInto,
@@ -218,6 +221,7 @@ function layoutArchitecture(
selector: 'edge',
style: {
'curve-style': 'straight',
'label': 'data(label)',
'source-endpoint': 'data(sourceEndpoint)',
'target-endpoint': 'data(targetEndpoint)',
},

View File

@@ -11,6 +11,20 @@ export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
/**
* Contains LL, RR, TT, BB which are impossible connections
*/
export type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`;
export type ArchitectureDirectionPair = Exclude<
InvalidArchitectureDirectionPair,
'LL' | 'RR' | 'TT' | 'BB'
>;
export type ArchitectureDirectionPairXY = Exclude<
InvalidArchitectureDirectionPair,
'LL' | 'RR' | 'TT' | 'BB'
| 'LR' | 'RL' | 'TB' | 'BT'
>;
export const ArchitectureDirectionName = {
L: 'left',
R: 'right',
@@ -70,14 +84,16 @@ export const isArchitectureDirectionXY = function (
return aX_bY || aY_bX;
};
/**
* Contains LL, RR, TT, BB which are impossible connections
*/
export type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`;
export type ArchitectureDirectionPair = Exclude<
InvalidArchitectureDirectionPair,
'LL' | 'RR' | 'TT' | 'BB'
>;
export const isArchitecturePairXY = function (
pair: ArchitectureDirectionPair,
): pair is ArchitectureDirectionPairXY {
const lhs = pair[0] as ArchitectureDirection;
const rhs = pair[1] as ArchitectureDirection;
const aX_bY = isArchitectureDirectionX(lhs) && isArchitectureDirectionY(rhs);
const aY_bX = isArchitectureDirectionY(lhs) && isArchitectureDirectionX(rhs);
return aX_bY || aY_bX;
};
/**
* 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
@@ -88,6 +104,7 @@ export const isValidArchitectureDirectionPair = function (
): x is ArchitectureDirectionPair {
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';
};
export type ArchitectureDirectionPairMap = {
[key in ArchitectureDirectionPair]?: string;
};
@@ -135,6 +152,25 @@ export const shiftPositionByArchitectureDirectionPair = function (
}
};
/**
* Given the directional pair of an XY edge, get the scale factors necessary to shift the coordinates inwards towards the edge
* @param pair - XY pair of an edge
* @returns - number[] containing [+/- 1, +/- 1]
*/
export const getArchitectureDirectionXYFactors = function (
pair: ArchitectureDirectionPairXY
): number[] {
if (pair === 'LT' || pair === 'TL') {
return [1, 1]
} else if (pair === 'BL' || pair === 'LB') {
return [1, -1]
} else if (pair === 'BR' || pair === 'RB') {
return [-1, -1]
} else {
return [-1, 1]
}
};
export interface ArchitectureStyleOptions {
fontFamily: string;
}
@@ -204,6 +240,7 @@ export interface ArchitectureState extends Record<string, unknown> {
export type EdgeSingularData = {
id: string;
label?: string;
source: string;
sourceDir: ArchitectureDirection;
sourceArrow?: boolean;

View File

@@ -1,3 +1,5 @@
// TODO remove no-console
/* eslint-disable no-console */
import type { D3Element } from '../../mermaidAPI.js';
import { createText } from '../../rendering-util/createText.js';
import {
@@ -9,6 +11,10 @@ import {
isArchitectureDirectionY,
edgeData,
nodeData,
isArchitectureDirectionXY,
getArchitectureDirectionPair,
getArchitectureDirectionXYFactors,
isArchitecturePairXY,
} from './architectureTypes.js';
import type cytoscape from 'cytoscape';
import { getIcon } from '../../rendering-util/svgRegister.js';
@@ -21,7 +27,7 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
const halfArrowSize = arrowSize / 2;
cy.edges().map((edge, id) => {
const { sourceDir, sourceArrow, targetDir, targetArrow } = edgeData(edge);
const { sourceDir, sourceArrow, targetDir, targetArrow, label } = edgeData(edge);
const { x: startX, y: startY } = edge[0].sourceEndpoint();
const { x: midX, y: midY } = edge[0].midpoint();
const { x: endX, y: endY } = edge[0].targetEndpoint();
@@ -60,6 +66,68 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
.attr('transform', `translate(${xShift},${yShift})`)
.attr('class', 'arrow');
}
if (label) {
const axis = !isArchitectureDirectionXY(sourceDir, targetDir)
? isArchitectureDirectionX(sourceDir)
? 'X'
: 'Y'
: 'XY';
let width = 0;
if (axis === 'X') {
width = Math.abs(startX - endX);
} else if (axis === 'Y') {
// Reduce width by a factor of 1.5 to avoid overlapping service labels
width = Math.abs(startY - endY) / 1.5;
} else {
width = Math.abs(startX - endX) / 2;
}
const textElem = g.append('g');
createText(
textElem,
label,
{
useHtmlLabels: false,
width,
classes: 'architecture-service-label',
},
getConfig()
);
textElem
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle');
if (axis === 'X') {
textElem.attr('transform', 'translate(' + midX + ', ' + midY + ')');
} else if (axis === 'Y') {
textElem.attr('transform', 'translate(' + midX + ', ' + midY + ') rotate(90)');
} else if (axis === 'XY') {
const pair = getArchitectureDirectionPair(sourceDir, targetDir);
if (pair && isArchitecturePairXY(pair)) {
const bboxOrig = textElem.node().getBoundingClientRect();
const [x, y] = getArchitectureDirectionXYFactors(pair);
textElem
.attr('dominant-baseline', 'auto')
.attr('transform', `rotate(${-1 * x * y * 45})`);
// Calculate the new width/height with the rotation and transform to the proper position
const bboxNew = textElem.node().getBoundingClientRect();
textElem
.attr('transform', `
translate(${midX}, ${midY - (bboxOrig.height / 2)})
translate(${x * bboxNew.width / 2}, ${y * bboxNew.height / 2})
rotate(${-1 * x * y * 45}, 0, ${bboxOrig.height / 2})
`);
}
}
}
}
});
};
@@ -84,12 +152,16 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) {
if (data.label) {
const textElem = groupsEl.append('g');
createText(textElem, data.label, {
useHtmlLabels: false,
width: w,
classes: 'architecture-service-label',
},
getConfig());
createText(
textElem,
data.label,
{
useHtmlLabels: false,
width: w,
classes: 'architecture-service-label',
},
getConfig()
);
textElem
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
@@ -116,12 +188,16 @@ export const drawServices = function (
if (service.title) {
const textElem = serviceElem.append('g');
createText(textElem, service.title, {
useHtmlLabels: false,
width: iconSize * 1.5,
classes: 'architecture-service-label',
},
getConfig());
createText(
textElem,
service.title,
{
useHtmlLabels: false,
width: iconSize * 1.5,
classes: 'architecture-service-label',
},
getConfig()
);
textElem
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')

View File

@@ -18,7 +18,7 @@ fragment Statement:
;
fragment Arrow:
lhsInto?=ARROW_INTO? lhsDir=ARROW_DIRECTION '--' rhsDir=ARROW_DIRECTION rhsInto?=ARROW_INTO?
lhsInto?=ARROW_INTO? lhsDir=ARROW_DIRECTION ('--' | '-' title=ARCH_TITLE '-') rhsDir=ARROW_DIRECTION rhsInto?=ARROW_INTO?
;
Group: