diff --git a/cypress/platform/neo-test.html b/cypress/platform/neo-test.html index 9473d4c24..a23e74114 100644 --- a/cypress/platform/neo-test.html +++ b/cypress/platform/neo-test.html @@ -113,6 +113,402 @@ handdrawn-default classic-default + + + +
+
+
+              classNode
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + + + + +
+
+
+              Filled classNode
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + + + + +
+
+
+              classA --> classB
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + + + + +
+
+
+              nameSpace { classA --> classB } note
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + diff --git a/cypress/platform/size-tester.html b/cypress/platform/size-tester.html index 32204085c..acf59d24a 100644 --- a/cypress/platform/size-tester.html +++ b/cypress/platform/size-tester.html @@ -54,6 +54,9 @@ //layout: 'elk', fontFamily: 'Kalam', logLevel: 1, + class: { + hideEmptyMembersBox: true, + }, }); let shape = 'circle'; @@ -70,6 +73,41 @@ n84@{ shape: '${shape}'} `; + let code2 = ` + classDiagram + class class1 { + int num + string test + string test + string test + string test + string test + method() + } + class class2 { + int num + string test + string test + string test + string test + string test + method() + method() + } + class class3 { + int test + } + <> class3 + class class4 { + int[] id + method() + method() + method() + method() + } + <> class4 + `; + let positions = { edges: {}, nodes: { @@ -104,7 +142,37 @@ }, }; - const { svg } = await mermaid.render('the-id-of-the-svg', code, undefined, positions); + let positions2 = { + edges: {}, + nodes: { + class1: { + x: 0, + y: 10, + width: 100, + height: 400, + }, + class2: { + x: -300, + y: 100, + width: 100, + height: 0, + }, + class3: { + x: 400, + y: 10, + width: 0, + height: 0, + }, + class4: { + x: 800, + y: 10, + width: 0, + height: 0, + }, + }, + }; + + const { svg } = await mermaid.render('the-id-of-the-svg', code2, undefined, positions2); const elem = document.querySelector('#diagram'); elem.innerHTML = svg; diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts index 670f93f16..353abfb19 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts @@ -38,7 +38,13 @@ export const getClasses = function ( return diagramObj.db.getClasses(); }; -export const draw = async function (text: string, id: string, _version: string, diag: any) { +export const draw = async function ( + text: string, + id: string, + _version: string, + diag: any, + positions: any +) { log.info('REF0:'); log.info('Drawing class diagram (v3)', id); const { securityLevel, state: conf, layout } = getConfig(); @@ -60,7 +66,7 @@ export const draw = async function (text: string, id: string, _version: string, data4Layout.rankSpacing = conf?.rankSpacing || 50; data4Layout.markers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop']; data4Layout.diagramId = id; - await render(data4Layout, svg); + await render(data4Layout, svg, positions); const padding = 8; utils.insertTitle( svg, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts index e35ee94ab..01e5d1e37 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts @@ -15,6 +15,7 @@ export async function classBox(parent: D3Selection const PADDING = config.class!.padding ?? 12; const GAP = PADDING; const useHtmlLabels = node.useHtmlLabels ?? evaluate(config.htmlLabels) ?? true; + // Treat node as classNode const classNode = node as unknown as ClassNode; classNode.annotations = classNode.annotations ?? []; @@ -49,15 +50,25 @@ export async function classBox(parent: D3Selection options.fillStyle = 'solid'; } - const w = bbox.width; - let h = bbox.height; + const w = Math.max(node.width ?? 0, bbox.width); + let h = Math.max(node.height ?? 0, bbox.height); + const nodeHeightGreater = (node.height ?? 0) > bbox.height; if (classNode.members.length === 0 && classNode.methods.length === 0) { h += GAP; } else if (classNode.members.length > 0 && classNode.methods.length === 0) { h += GAP * 2; } + const x = -w / 2; const y = -h / 2; + let extraHeight = renderExtraBox + ? PADDING * 2 + : classNode.members.length === 0 && classNode.methods.length === 0 + ? -PADDING + : 0; + if (nodeHeightGreater) { + extraHeight = PADDING * 2; + } // Create and center rectangle const roughRect = rc.rectangle( @@ -70,13 +81,7 @@ export async function classBox(parent: D3Selection ? -PADDING / 2 : 0), w + 2 * PADDING, - h + - 2 * PADDING + - (renderExtraBox - ? PADDING * 2 - : classNode.members.length === 0 && classNode.methods.length === 0 - ? -PADDING - : 0), + h + 2 * PADDING + extraHeight, options ); @@ -85,6 +90,31 @@ export async function classBox(parent: D3Selection const rectBBox = rect.node()!.getBBox(); // Rect is centered so now adjust labels. + const annotationGroupHeight = + (shapeSvg.select('.annotation-group').node() as SVGGraphicsElement).getBBox().height - + (renderExtraBox ? PADDING / 2 : 0) || 0; + const labelGroupHeight = + (shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height - + (renderExtraBox ? PADDING / 2 : 0) || 0; + const membersGroupHeight = + (shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height - + (renderExtraBox ? PADDING / 2 : 0) || 0; + + // Y value in the middle of the first line and remaining space. + const methodsAreaPlacement = + (annotationGroupHeight + + labelGroupHeight + + y + + PADDING - + (y - + PADDING - + (renderExtraBox + ? PADDING + : classNode.members.length === 0 && classNode.methods.length === 0 + ? -PADDING / 2 + : 0))) / + 2; + // TODO: Fix types shapeSvg.selectAll('.text').each((_: any, i: number, nodes: any) => { const text = select(nodes[i]); @@ -110,6 +140,30 @@ export async function classBox(parent: D3Selection : classNode.members.length === 0 && classNode.methods.length === 0 ? -PADDING / 2 : 0); + if (text.attr('class').includes('methods-group')) { + if (nodeHeightGreater) { + newTranslateY = + Math.max( + methodsAreaPlacement, + annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING + ) + + GAP * 2; + } else { + newTranslateY = + annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 4 + PADDING; + } + } + if ( + classNode.members.length === 0 && + classNode.methods.length === 0 && + config.class?.hideEmptyMembersBox + ) { + if (classNode.annotations.length > 0) { + newTranslateY = translateY - GAP; + } else { + newTranslateY = translateY; + } + } if (!useHtmlLabels) { // Fix so non html labels are better centered. // BBox of text seems to be slightly different when calculated so we offset @@ -132,22 +186,16 @@ export async function classBox(parent: D3Selection }); // Render divider lines. - const annotationGroupHeight = - (shapeSvg.select('.annotation-group').node() as SVGGraphicsElement).getBBox().height - - (renderExtraBox ? PADDING / 2 : 0) || 0; - const labelGroupHeight = - (shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height - - (renderExtraBox ? PADDING / 2 : 0) || 0; - const membersGroupHeight = - (shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height - - (renderExtraBox ? PADDING / 2 : 0) || 0; + // Line y-values are offset by 0.001 so gradient stroke can apply. + // If y-values are the same then the height of the bounding box is zero and it doesn't work. // First line (under label) if (classNode.members.length > 0 || classNode.methods.length > 0 || renderExtraBox) { + const firstLineY = annotationGroupHeight + labelGroupHeight + y + PADDING; const roughLine = rc.line( rectBBox.x, - annotationGroupHeight + labelGroupHeight + y + PADDING, + firstLineY, rectBBox.x + rectBBox.width, - annotationGroupHeight + labelGroupHeight + y + PADDING, + firstLineY + 0.001, options ); const line = shapeSvg.insert(() => roughLine); @@ -156,11 +204,13 @@ export async function classBox(parent: D3Selection // Second line (under members) if (renderExtraBox || classNode.members.length > 0 || classNode.methods.length > 0) { + const secondLineY = + annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING; const roughLine = rc.line( rectBBox.x, - annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING, + nodeHeightGreater ? Math.max(methodsAreaPlacement, secondLineY) : secondLineY, rectBBox.x + rectBBox.width, - annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + PADDING + GAP * 2, + (nodeHeightGreater ? Math.max(methodsAreaPlacement, secondLineY) : secondLineY) + 0.001, options ); const line = shapeSvg.insert(() => roughLine);