mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
feat(arch): edge labels implemented
This commit is contained in:
@@ -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';
|
||||
|
||||
|
@@ -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)',
|
||||
},
|
||||
|
@@ -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;
|
||||
|
@@ -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')
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user