mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-18 07:49:26 +02:00
feat(arch): edge labels implemented
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -120,6 +120,46 @@
|
|||||||
</pre>
|
</pre>
|
||||||
<hr />
|
<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">
|
<script type="module">
|
||||||
import mermaid from './mermaid.esm.mjs';
|
import mermaid from './mermaid.esm.mjs';
|
||||||
|
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
// TODO remove no-console
|
||||||
|
/* eslint-disable no-console */
|
||||||
import type { Position } from 'cytoscape';
|
import type { Position } from 'cytoscape';
|
||||||
import cytoscape from 'cytoscape';
|
import cytoscape from 'cytoscape';
|
||||||
import type { Diagram } from '../../Diagram.js';
|
import type { Diagram } from '../../Diagram.js';
|
||||||
@@ -80,12 +82,13 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
|
|||||||
|
|
||||||
function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {
|
function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {
|
||||||
edges.forEach((parsedEdge) => {
|
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)
|
const edgeType = isArchitectureDirectionXY(parsedEdge.lhsDir, parsedEdge.rhsDir)
|
||||||
? 'segments'
|
? 'segments'
|
||||||
: 'straight';
|
: 'straight';
|
||||||
const edge: EdgeSingularData = {
|
const edge: EdgeSingularData = {
|
||||||
id: `${lhsId}-${rhsId}`,
|
id: `${lhsId}-${rhsId}`,
|
||||||
|
label: title,
|
||||||
source: lhsId,
|
source: lhsId,
|
||||||
sourceDir: lhsDir,
|
sourceDir: lhsDir,
|
||||||
sourceArrow: lhsInto,
|
sourceArrow: lhsInto,
|
||||||
@@ -218,6 +221,7 @@ function layoutArchitecture(
|
|||||||
selector: 'edge',
|
selector: 'edge',
|
||||||
style: {
|
style: {
|
||||||
'curve-style': 'straight',
|
'curve-style': 'straight',
|
||||||
|
'label': 'data(label)',
|
||||||
'source-endpoint': 'data(sourceEndpoint)',
|
'source-endpoint': 'data(sourceEndpoint)',
|
||||||
'target-endpoint': 'data(targetEndpoint)',
|
'target-endpoint': 'data(targetEndpoint)',
|
||||||
},
|
},
|
||||||
|
@@ -11,6 +11,20 @@ export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
|||||||
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
|
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
|
||||||
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
|
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 = {
|
export const ArchitectureDirectionName = {
|
||||||
L: 'left',
|
L: 'left',
|
||||||
R: 'right',
|
R: 'right',
|
||||||
@@ -70,14 +84,16 @@ export const isArchitectureDirectionXY = function (
|
|||||||
return aX_bY || aY_bX;
|
return aX_bY || aY_bX;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const isArchitecturePairXY = function (
|
||||||
* Contains LL, RR, TT, BB which are impossible connections
|
pair: ArchitectureDirectionPair,
|
||||||
*/
|
): pair is ArchitectureDirectionPairXY {
|
||||||
export type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`;
|
const lhs = pair[0] as ArchitectureDirection;
|
||||||
export type ArchitectureDirectionPair = Exclude<
|
const rhs = pair[1] as ArchitectureDirection;
|
||||||
InvalidArchitectureDirectionPair,
|
const aX_bY = isArchitectureDirectionX(lhs) && isArchitectureDirectionY(rhs);
|
||||||
'LL' | 'RR' | 'TT' | 'BB'
|
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)
|
* 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
|
* @param x - architecture direction pair which could potentially be invalid
|
||||||
@@ -88,6 +104,7 @@ export const isValidArchitectureDirectionPair = function (
|
|||||||
): x is ArchitectureDirectionPair {
|
): x is ArchitectureDirectionPair {
|
||||||
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';
|
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ArchitectureDirectionPairMap = {
|
export type ArchitectureDirectionPairMap = {
|
||||||
[key in ArchitectureDirectionPair]?: string;
|
[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 {
|
export interface ArchitectureStyleOptions {
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
}
|
}
|
||||||
@@ -204,6 +240,7 @@ export interface ArchitectureState extends Record<string, unknown> {
|
|||||||
|
|
||||||
export type EdgeSingularData = {
|
export type EdgeSingularData = {
|
||||||
id: string;
|
id: string;
|
||||||
|
label?: string;
|
||||||
source: string;
|
source: string;
|
||||||
sourceDir: ArchitectureDirection;
|
sourceDir: ArchitectureDirection;
|
||||||
sourceArrow?: boolean;
|
sourceArrow?: boolean;
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
// TODO remove no-console
|
||||||
|
/* eslint-disable no-console */
|
||||||
import type { D3Element } from '../../mermaidAPI.js';
|
import type { D3Element } from '../../mermaidAPI.js';
|
||||||
import { createText } from '../../rendering-util/createText.js';
|
import { createText } from '../../rendering-util/createText.js';
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +11,10 @@ import {
|
|||||||
isArchitectureDirectionY,
|
isArchitectureDirectionY,
|
||||||
edgeData,
|
edgeData,
|
||||||
nodeData,
|
nodeData,
|
||||||
|
isArchitectureDirectionXY,
|
||||||
|
getArchitectureDirectionPair,
|
||||||
|
getArchitectureDirectionXYFactors,
|
||||||
|
isArchitecturePairXY,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import type cytoscape from 'cytoscape';
|
import type cytoscape from 'cytoscape';
|
||||||
import { getIcon } from '../../rendering-util/svgRegister.js';
|
import { getIcon } from '../../rendering-util/svgRegister.js';
|
||||||
@@ -21,7 +27,7 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) {
|
|||||||
const halfArrowSize = arrowSize / 2;
|
const halfArrowSize = arrowSize / 2;
|
||||||
|
|
||||||
cy.edges().map((edge, id) => {
|
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: startX, y: startY } = edge[0].sourceEndpoint();
|
||||||
const { x: midX, y: midY } = edge[0].midpoint();
|
const { x: midX, y: midY } = edge[0].midpoint();
|
||||||
const { x: endX, y: endY } = edge[0].targetEndpoint();
|
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('transform', `translate(${xShift},${yShift})`)
|
||||||
.attr('class', 'arrow');
|
.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) {
|
if (data.label) {
|
||||||
const textElem = groupsEl.append('g');
|
const textElem = groupsEl.append('g');
|
||||||
createText(textElem, data.label, {
|
createText(
|
||||||
useHtmlLabels: false,
|
textElem,
|
||||||
width: w,
|
data.label,
|
||||||
classes: 'architecture-service-label',
|
{
|
||||||
},
|
useHtmlLabels: false,
|
||||||
getConfig());
|
width: w,
|
||||||
|
classes: 'architecture-service-label',
|
||||||
|
},
|
||||||
|
getConfig()
|
||||||
|
);
|
||||||
textElem
|
textElem
|
||||||
.attr('dy', '1em')
|
.attr('dy', '1em')
|
||||||
.attr('alignment-baseline', 'middle')
|
.attr('alignment-baseline', 'middle')
|
||||||
@@ -116,12 +188,16 @@ export const drawServices = function (
|
|||||||
|
|
||||||
if (service.title) {
|
if (service.title) {
|
||||||
const textElem = serviceElem.append('g');
|
const textElem = serviceElem.append('g');
|
||||||
createText(textElem, service.title, {
|
createText(
|
||||||
useHtmlLabels: false,
|
textElem,
|
||||||
width: iconSize * 1.5,
|
service.title,
|
||||||
classes: 'architecture-service-label',
|
{
|
||||||
},
|
useHtmlLabels: false,
|
||||||
getConfig());
|
width: iconSize * 1.5,
|
||||||
|
classes: 'architecture-service-label',
|
||||||
|
},
|
||||||
|
getConfig()
|
||||||
|
);
|
||||||
textElem
|
textElem
|
||||||
.attr('dy', '1em')
|
.attr('dy', '1em')
|
||||||
.attr('alignment-baseline', 'middle')
|
.attr('alignment-baseline', 'middle')
|
||||||
|
@@ -18,7 +18,7 @@ fragment Statement:
|
|||||||
;
|
;
|
||||||
|
|
||||||
fragment Arrow:
|
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:
|
Group:
|
||||||
|
Reference in New Issue
Block a user