diff --git a/.changeset/fix-7560-self-referential-multiplicity.md b/.changeset/fix-7560-self-referential-multiplicity.md new file mode 100644 index 00000000000..d0974dc009a --- /dev/null +++ b/.changeset/fix-7560-self-referential-multiplicity.md @@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix: self-referential class multiplicity labels no longer rendered multiple times + +Fixes #7560. Resolves an issue where cardinality labels on self-referential class relationships were rendered three times due to edge splitting in the dagre layout. The fix ensures that each sub-edge only carries its relevant label positions. diff --git a/cypress/integration/rendering/classDiagram-v3.spec.js b/cypress/integration/rendering/classDiagram-v3.spec.js index 7e8d2ff0a0d..911d3aed16d 100644 --- a/cypress/integration/rendering/classDiagram-v3.spec.js +++ b/cypress/integration/rendering/classDiagram-v3.spec.js @@ -1042,4 +1042,19 @@ class C13["With Città foreign language"] { logLevel: 1, htmlLabels: true } ); }); + + it('should render a self-referential class diagram with multiplicity labels (fixes #7560)', () => { + imgSnapshotTest( + ` + classDiagram + class SelfReferential{ + +int id + +int self_referential_id + +SelfReferential referenced + } + SelfReferential "1" --> "0..1" SelfReferential : referenced + `, + { logLevel: 1, htmlLabels: true } + ); + }); }); diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index 0defc1e6fa1..4b5d1e30cf0 100644 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ b/packages/mermaid/src/dagre-wrapper/edges.js @@ -97,16 +97,10 @@ export const insertEdgeLabel = async (elem, edge) => { setTerminalWidth(fo, edge.startLabelLeft); } if (edge.startLabelRight) { - // Create the actual text element const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); const inner = startEdgeLabelRight.insert('g').attr('class', 'inner'); - const startLabelElement = await createLabel( - startEdgeLabelRight, - edge.startLabelRight, - edge.labelStyle - ); + const startLabelElement = await createLabel(inner, edge.startLabelRight, edge.labelStyle); fo = startLabelElement; - inner.node().appendChild(startLabelElement); let slBox = startLabelElement.getBBox(); if (useHtmlLabels) { const div = startLabelElement.children[0]; @@ -124,7 +118,6 @@ export const insertEdgeLabel = async (elem, edge) => { setTerminalWidth(fo, edge.startLabelRight); } if (edge.endLabelLeft) { - // Create the actual text element const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals'); const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner'); const endLabelElement = await createLabel(inner, edge.endLabelLeft, edge.labelStyle); @@ -139,8 +132,6 @@ export const insertEdgeLabel = async (elem, edge) => { } inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - endEdgeLabelLeft.node().appendChild(endLabelElement); - if (!terminalLabels[edge.id]) { terminalLabels[edge.id] = {}; } @@ -148,7 +139,6 @@ export const insertEdgeLabel = async (elem, edge) => { setTerminalWidth(fo, edge.endLabelLeft); } if (edge.endLabelRight) { - // Create the actual text element const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); const inner = endEdgeLabelRight.insert('g').attr('class', 'inner'); const endLabelElement = await createLabel(inner, edge.endLabelRight, edge.labelStyle); @@ -163,7 +153,6 @@ export const insertEdgeLabel = async (elem, edge) => { } inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - endEdgeLabelRight.node().appendChild(endLabelElement); if (!terminalLabels[edge.id]) { terminalLabels[edge.id] = {}; } diff --git a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js index 3873f8f8177..7e5c5f582ff 100644 --- a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js +++ b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js @@ -306,7 +306,16 @@ export const adjustClustersAndEdges = (graph, depth) => { const edge2 = structuredClone(edge); edge1.label = ''; edge1.arrowTypeEnd = 'none'; + // Clear multiplicity labels on edge1 (start->mid segment) - keep startLabelRight if needed, clear others + edge1.startLabelLeft = ''; + edge1.endLabelRight = ''; + edge1.endLabelLeft = ''; edge2.label = ''; + edge2.arrowTypeStart = 'none'; + // Clear multiplicity labels on edge2 (mid->end segment) - keep endLabelLeft if needed, clear others + edge2.startLabelRight = ''; + edge2.startLabelLeft = ''; + edge2.endLabelRight = ''; edge1.fromCluster = e.v; edge2.toCluster = e.v; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js index e9f0266e263..d02e6b8427b 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js @@ -345,11 +345,21 @@ export const render = async (data4Layout, svg) => { const edge2 = structuredClone(edge); edge1.label = ''; edge1.arrowTypeEnd = 'none'; + edge1.endLabelLeft = ''; + edge1.endLabelRight = ''; // defensive + edge1.startLabelLeft = ''; // defensive edge1.id = nodeId + '-cyclic-special-1'; + edgeMid.startLabelRight = ''; + edgeMid.startLabelLeft = ''; // defensive + edgeMid.endLabelLeft = ''; + edgeMid.endLabelRight = ''; // defensive edgeMid.arrowTypeStart = 'none'; edgeMid.arrowTypeEnd = 'none'; edgeMid.id = nodeId + '-cyclic-special-mid'; edge2.label = ''; + edge2.startLabelRight = ''; + edge2.startLabelLeft = ''; // defensive + edge2.arrowTypeStart = 'none'; if (node.isGroup) { edge1.fromCluster = nodeId; edge2.toCluster = nodeId; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 5bad2e061a0..6855a43da52 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -148,7 +148,6 @@ export const insertEdgeLabel = async (elem, edge) => { setTerminalWidth(fo, edge.startLabelLeft); } if (edge.startLabelRight) { - // Create the actual text element const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); const inner = startEdgeLabelRight.insert('g').attr('class', 'inner'); const startLabelElement = await createLabel( @@ -159,7 +158,6 @@ export const insertEdgeLabel = async (elem, edge) => { false ); fo = startLabelElement; - inner.node().appendChild(startLabelElement); let slBox = startLabelElement.getBBox(); if (useHtmlLabels) { const div = startLabelElement.children[0]; @@ -177,7 +175,6 @@ export const insertEdgeLabel = async (elem, edge) => { setTerminalWidth(fo, edge.startLabelRight); } if (edge.endLabelLeft) { - // Create the actual text element const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals'); const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner'); const endLabelElement = await createLabel( @@ -198,8 +195,6 @@ export const insertEdgeLabel = async (elem, edge) => { } inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - endEdgeLabelLeft.node().appendChild(endLabelElement); - if (!terminalLabels.get(edge.id)) { terminalLabels.set(edge.id, {}); } @@ -207,10 +202,8 @@ export const insertEdgeLabel = async (elem, edge) => { setTerminalWidth(fo, edge.endLabelLeft); } if (edge.endLabelRight) { - // Create the actual text element const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); const inner = endEdgeLabelRight.insert('g').attr('class', 'inner'); - const endLabelElement = await createLabel( inner, edge.endLabelRight, @@ -229,7 +222,6 @@ export const insertEdgeLabel = async (elem, edge) => { } inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - endEdgeLabelRight.node().appendChild(endLabelElement); if (!terminalLabels.get(edge.id)) { terminalLabels.set(edge.id, {}); }