Merge pull request #20 from Mermaid-Chart/yari/classBox-neofication

"classBox" Shape Neofication and Resize Support
This commit is contained in:
pbrolin47
2024-11-21 11:01:44 +01:00
committed by GitHub
4 changed files with 545 additions and 25 deletions

View File

@@ -113,6 +113,402 @@
<th>handdrawn-default</th>
<th>classic-default</th>
</tr>
<tr>
<th class="vertical-header">
<button class="collapsible">Simple classNode</button>
<div class="content">
<div class="pre-scrollable">
<pre>
classNode
</pre
>
</div>
</div>
</th>
<td>
<pre id="diagram1" class="mermaid">
%%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
classDiagram
class classNode
</pre>
</td>
<td>
<pre id="diagram1" class="mermaid">
%%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
classDiagram
class classNode
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classNode
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
classDiagram
class classNode
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classNode
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classNode
</pre
>
</td>
</tr>
<tr>
<th class="vertical-header">
<button class="collapsible">Filled classNode</button>
<div class="content">
<div class="pre-scrollable">
<pre>
Filled classNode
</pre
>
</div>
</div>
</th>
<td>
<pre id="diagram1" class="mermaid">
%%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
classDiagram
class classNode {
+int number
method()
}
</pre>
</td>
<td>
<pre id="diagram1" class="mermaid">
%%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
classDiagram
class classNode {
+int number
method()
}
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classNode {
+int number
method()
}
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
classDiagram
class classNode {
+int number
method()
}
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classNode {
+int number
method()
}
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classNode {
+int number
method()
}
</pre
>
</td>
</tr>
<tr>
<th class="vertical-header">
<button class="collapsible">Class with relation</button>
<div class="content">
<div class="pre-scrollable">
<pre>
classA --> classB
</pre
>
</div>
</div>
</th>
<td>
<pre id="diagram1" class="mermaid">
%%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
classDiagram
class classA {
+int number
method()
}
class classB {
+int number
-string text
method()
another_method()
}
classA --> classB
</pre>
</td>
<td>
<pre id="diagram1" class="mermaid">
%%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
classDiagram
class classA {
+int number
method()
}
class classB {
+int number
-string text
method()
another_method()
}
classA --> classB
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classA {
+int number
method()
}
class classB {
+int number
-string text
method()
another_method()
}
classA --> classB
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
classDiagram
class classA {
+int number
method()
}
class classB {
+int number
-string text
method()
another_method()
}
classA --> classB
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classA {
+int number
method()
}
class classB {
+int number
-string text
method()
another_method()
}
classA --> classB
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
classDiagram
class classA {
+int number
method()
}
class classB {
+int number
-string text
method()
another_method()
}
classA --> classB
</pre
>
</td>
</tr>
<tr>
<th class="vertical-header">
<button class="collapsible">Full class diagram</button>
<div class="content">
<div class="pre-scrollable">
<pre>
nameSpace { classA --> classB } note
</pre
>
</div>
</div>
</th>
<td>
<pre id="diagram1" class="mermaid">
%%{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"
</pre>
</td>
<td>
<pre id="diagram1" class="mermaid">
%%{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"
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{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"
</pre
>
</td>
<td>
<pre id="diagram2" class="mermaid">
%%{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"
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{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"
</pre
>
</td>
<td>
<pre id="diagram3" class="mermaid">
%%{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"
</pre
>
</td>
</tr>
<tr>
<th class="vertical-header">
<button class="collapsible">Simple State (only id)</button>

View File

@@ -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
}
<<interface>> class3
class class4 {
int[] id
method()
method()
method()
method()
}
<<interface>> 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;
</script>

View File

@@ -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,

View File

@@ -15,6 +15,7 @@ export async function classBox<T extends SVGGraphicsElement>(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<T extends SVGGraphicsElement>(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<T extends SVGGraphicsElement>(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<T extends SVGGraphicsElement>(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<any, unknown>(nodes[i]);
@@ -110,6 +140,30 @@ export async function classBox<T extends SVGGraphicsElement>(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<T extends SVGGraphicsElement>(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<T extends SVGGraphicsElement>(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);