Open gergelyk opened 4 years ago
I would love to have this diagram type handled by mermaidjs !
+1 for component diagrams
We would need:
Maybe something like this:
component HttpClient
port Port12345
providesInterface HTTP
component HttpServer
port Port80
port Port443
requiresInteface HTTP
component FooApp
requiresInteface CGI
HTTP --> CGI
Port12345 --> Port80 : HTTP
+1 for component diagrams if mermaidjs supports component diagrams, it would be a complete solution to all the Software Engineers from Developers to Architects. Please consider this and do the needful
+1 for component diagrams
+1 for component diagrams
Team, Please consider this as we have to use another tool like PlantUML for component diagrams.
+1 for component diagrams
+1 for component diagram
+1 for component diagram.
+1 for component diagram.
+1
+1
+1
We're now using Mermaid to document our API: https://doc.postsharp.net/caravela/api/caravela_framework_aspects
+1
+1 for component diagrams.
+1, would love to see this implemented as well!
I've started an early POC for component diagrams... needs a lot of work and haven't put in the relationships yet, but I've got a starting point which includes system components, system nodes, and support for nested nodes which can go infinitely deep due to utilizing an AST structure.
componentDiagram
["my-awesome-api"]
[["infrastructure-container-one"]] {
<<"PCF Org">>
[["infrastructure-container-two"]] {
<<"PCF Space">>
["my-awesome-api-2"]
["my-awesome-api-3"]
}
}
Syntax is different that one proposed above... and definitely subject to change. I'm somewhat modeling it after the flowchart style where []
signifies a system component and [[]]
signifies a system node. It also includes the <<>>
syntax for signifying the "type" of a system node.
Curious if anyone has any thoughts/feedback, especially @knsv, incase you had some other ideas or if this is already a WIP in some way. :) Don't want to go too far down a dead-end rabbit hole!
@andrewmcgivery, I think the hard thing will be relations. Especially things like provided interfaces, required interfaces, relationships through interfaces, relations through ports, and interfaces on ports.
Update... did some initial work on interfaces and relationships.
Representing interface with ()
and relationships with dashes and arrows.
%% Interface
("my-interface-1")
%% Relationship
"my-awesome-api" -> "my-interface-1"
%% Relationship (dotted line)
"my-interface-1" --> "my-awesome-api-2"
%% Relationship (with line label)
"my-awesome-api-2" -"use"-> "my-awesome-api-3"
%% Relationship (with line label and dotted line)
"my-awesome-api-3" --"use"--> "my-interface-2"
Example Output:
Good work.
Looks promising! @andrewmcgivery where can I access your work? Would love to experiment with it and evaluate if it already does the job for me.
@andrewmcgivery I'm also interested in your component diagram effort... is there any PR tangling around or have you committed anything of that anywhere?
+1
:+1: for component diagrams
+1
+1
Came here looking for component diagrams :)
+1
+1 Here!!!
+1
Friendly ask that those wishing to express their support for this request to please do so via the reactions on the initial post instead of leaving +1-style comments.
The +1 comments don't get registered with GitHub (e.g. https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) and tend to bifurcate reaction counts. They also unnecessarily lengthen the thread, and add noise for maintainers as well as those of us (like myself) that are subscribed to this issue so that we can get notified as progress as made.
I would love component diagrams. However one thing that might make this more complicated to implement over other diagrams in mermaid will be the containers like those demonstrated in the post above.
These have been implemented in plantuml and I notice that plantuml's layout engine has real trouble linking different layers of containers together.
Eg: in demonstration above this would be very hard to get right because my-awesome-api-2
is two layers deep in boxes but my-interface-1
is not:
%% Relationship (dotted line)
"my-interface-1" --> "my-awesome-api-2"
The problem seems to be the sequencing of layout decisions:
In plantuml's case the decisions appear to go in that order but it leads to some very ugly diagrams. That is, container boxes make either:
I'm not saying it's impossible, but care will need to be given to getting this feature right.
The problem seems to be the sequencing of layout decisions:
I am sometime annoying for these problems. Of course, if we can avoid these problems, it's very good. But, I prefer we can write component diagram for now as first step even if there are the same problems as plantuml. What do you think?
+1
The initial work mentioned here https://github.com/mermaid-js/mermaid/issues/1462#issuecomment-972483791 looks very promising - is there a timeframe for when this could become available already? π₯Ίππ
For anybody else stumbling over this issue: You can consider using C4-diagrams instead of UML component diagrams, see: https://mermaid.js.org/syntax/c4c.html
+1 for component diagram support. I'd really like to ditch plantuml
.
I've been debating mermaid over d2. Mermaid has widepsread support already on Github, notion and tooling around sphinx etc. But this one bit was missing and making me look at D2.
Although @da-kami comment above makes sense. I'll look at C4 diagrams to see if they have all the things i need.
@andrewmcgivery Are you still working on this?
+1
Hello, what's the status on this bug?
Hallo mermaid team, any progress here please?
+1
+1
+1
+1
Almost 2 years later since I took a stab at this and it's still not in Mermaid X_X
The code I started is super old now so likely no longer relevant or anything I could even begin to craft a PR out of... but incase anyone is feeling brave and wants to build on what I had started, I'll share the relevant pieces. I likely won't have time to pick this up again.
Keep in mind this was all very much work in progress (from 2 years ago!) so it's a bit messy!
componentDiagam.jison
/** mermaid
* https://knsv.github.io/mermaid
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
%lex
%options case-insensitive
%x string
%x token
%x unqString
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
(\r?\n)+ return 'NEWLINE';
\s+ /* skip all whitespace */
\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */
<<EOF>> return 'EOF';
"componentDiagram" return 'COMPONENT_DIAGRAM';
"{" return 'STRUCT_START';
"}" return 'STRUCT_END';
":" return 'COLONSEP';
"[[" return 'NODE_START';
"]]" return 'NODE_END';
"[" return 'COMPONENT_START';
"]" return 'COMPONENT_END';
"<<" return 'NODE_TYPE_START';
">>" return 'NODE_TYPE_END';
"(" return "INTERFACE_START";
")" return "INTERFACE_END";
/*
"id" return 'ID';
"text" return 'TEXT';
"risk" return 'RISK';
"verifyMethod" return 'VERIFYMTHD';
"requirement" return 'REQUIREMENT';
"functionalRequirement" return 'FUNCTIONAL_REQUIREMENT';
"interfaceRequirement" return 'INTERFACE_REQUIREMENT';
"performanceRequirement" return 'PERFORMANCE_REQUIREMENT';
"physicalRequirement" return 'PHYSICAL_REQUIREMENT';
"designConstraint" return 'DESIGN_CONSTRAINT';
"low" return 'LOW_RISK';
"medium" return 'MED_RISK';
"high" return 'HIGH_RISK';
"analysis" return 'VERIFY_ANALYSIS';
"demonstration" return 'VERIFY_DEMONSTRATION';
"inspection" return 'VERIFY_INSPECTION';
"test" return 'VERIFY_TEST';
"element" return 'ELEMENT';
"contains" return 'CONTAINS';
"copies" return 'COPIES';
"derives" return 'DERIVES';
"satisfies" return 'SATISFIES';
"verifies" return 'VERIFIES';
"refines" return 'REFINES';
"traces" return 'TRACES';
"type" return 'TYPE';
"docref" return 'DOCREF';*/
"<-" return 'END_ARROW_L';
"->" {return 'END_ARROW_R';}
"-" {return 'LINE';}
["] { this.begin("string"); }
<string>["] { this.popState(); }
<string>[^"]* { return "qString"; }
[\w][^\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
/lex
%start start
%% /* language grammar */
start
: directive NEWLINE start
| directive start
| COMPONENT_DIAGRAM NEWLINE document EOF;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); };
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); };
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); };
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'pie'); };
separator: NEWLINE | SEMI | EOF ;
document
: /* empty */
{ $$ = [];}
| document line
{
//console.log($1, $2);
if($2 !== []){
$1.push($2);
}
$$=$1;}
;
line
: statement
{$$=$1;}
| SEMI
| NEWLINE
| SPACE
| EOF
;
statement
: COMPONENT_START qString COMPONENT_END
{ $$=yy.addComponent($2) }
| NODE_START qString NODE_END STRUCT_START separator NODE_TYPE_START qString NODE_TYPE_END separator document STRUCT_END
{$$=yy.addNode($2, $7, $10)}
| INTERFACE_START qString INTERFACE_END
{$$=yy.addInterface($2)}
| qString END_ARROW_R qString
{$$=yy.addRelationship($1, $3, 'SOLID')}
| qString LINE END_ARROW_R qString
{$$=yy.addRelationship($1, $4, 'DASHED')}
| qString LINE qString END_ARROW_R qString
{$$=yy.addRelationship($1, $5, 'SOLID', $3)}
| qString LINE LINE qString LINE END_ARROW_R qString
{$$=yy.addRelationship($1, $7, 'DASHED', $4)};
/*
diagram
: { $$ = [] }
| requirementDef diagram
| elementDef diagram
| relationshipDef diagram
| directive diagram
| NEWLINE diagram;
*/
/*
requirementDef
: requirementType requirementName STRUCT_START NEWLINE requirementBody
{ yy.addRequirement($2, $1) };
requirementBody
: ID COLONSEP id NEWLINE requirementBody
{ yy.setNewReqId($3); }
| TEXT COLONSEP text NEWLINE requirementBody
{ yy.setNewReqText($3); }
| RISK COLONSEP riskLevel NEWLINE requirementBody
{ yy.setNewReqRisk($3); }
| VERIFYMTHD COLONSEP verifyType NEWLINE requirementBody
{ yy.setNewReqVerifyMethod($3); }
| NEWLINE requirementBody
| STRUCT_STOP;
requirementType
: REQUIREMENT
{ $$=yy.RequirementType.REQUIREMENT;}
| FUNCTIONAL_REQUIREMENT
{ $$=yy.RequirementType.FUNCTIONAL_REQUIREMENT;}
| INTERFACE_REQUIREMENT
{ $$=yy.RequirementType.INTERFACE_REQUIREMENT;}
| PERFORMANCE_REQUIREMENT
{ $$=yy.RequirementType.PERFORMANCE_REQUIREMENT;}
| PHYSICAL_REQUIREMENT
{ $$=yy.RequirementType.PHYSICAL_REQUIREMENT;}
| DESIGN_CONSTRAINT
{ $$=yy.RequirementType.DESIGN_CONSTRAINT;};
riskLevel
: LOW_RISK { $$=yy.RiskLevel.LOW_RISK;}
| MED_RISK { $$=yy.RiskLevel.MED_RISK;}
| HIGH_RISK { $$=yy.RiskLevel.HIGH_RISK;};
verifyType
: VERIFY_ANALYSIS
{ $$=yy.VerifyType.VERIFY_ANALYSIS;}
| VERIFY_DEMONSTRATION
{ $$=yy.VerifyType.VERIFY_DEMONSTRATION;}
| VERIFY_INSPECTION
{ $$=yy.VerifyType.VERIFY_INSPECTION;}
| VERIFY_TEST
{ $$=yy.VerifyType.VERIFY_TEST;};
elementDef
: ELEMENT elementName STRUCT_START NEWLINE elementBody
{ yy.addElement($2) };
elementBody
: TYPE COLONSEP type NEWLINE elementBody
{ yy.setNewElementType($3); }
| DOCREF COLONSEP ref NEWLINE elementBody
{ yy.setNewElementDocRef($3); }
| NEWLINE elementBody
| STRUCT_STOP;
relationshipDef
: id END_ARROW_L relationship LINE id
{ yy.addRelationship($3, $5, $1) }
| id LINE relationship END_ARROW_R id
{ yy.addRelationship($3, $1, $5) };
relationship
: CONTAINS
{ $$=yy.Relationships.CONTAINS;}
| COPIES
{ $$=yy.Relationships.COPIES;}
| DERIVES
{ $$=yy.Relationships.DERIVES;}
| SATISFIES
{ $$=yy.Relationships.SATISFIES;}
| VERIFIES
{ $$=yy.Relationships.VERIFIES;}
| REFINES
{ $$=yy.Relationships.REFINES;}
| TRACES
{ $$=yy.Relationships.TRACES;};
requirementName: unqString | qString;
id : unqString | qString;
text : unqString | qString;
elementName : unqString | qString;
type : unqString | qString;
ref : unqString | qString;*/
%%
componentDb.js
import * as configApi from '../../config';
import { log } from '../../logger';
import mermaidAPI from '../../mermaidAPI';
let AST_NODE_TYPE = {
COMPONENT: 'COMPONENT',
NODE: 'NODE',
INTERFACE: 'INTERFACE',
};
let AST = {};
let relationships = [];
const addComponent = (name) => {
if (typeof AST[name] === 'undefined') {
AST[name] = {
name,
astType: AST_NODE_TYPE.COMPONENT,
};
}
return AST[name];
};
const addInterface = (name) => {
if (typeof AST[name] === 'undefined') {
AST[name] = {
name,
astType: AST_NODE_TYPE.INTERFACE,
};
}
return AST[name];
};
const addNode = (name, type, list) => {
//console.log(`Adding node ${name}, type: ${type}`);
if (typeof AST[name] === 'undefined') {
AST[name] = {
name,
type: type,
children: list.filter((item) => item.astType),
astType: AST_NODE_TYPE.NODE,
};
}
list.forEach((item) => {
delete AST[item.name];
});
return AST[name];
};
const getAST = () => AST;
const addRelationship = (src, dst, type, text) => {
relationships.push({
src,
dst,
type,
text,
});
};
const getRelationships = () => relationships;
const clear = () => {
AST = {};
relationships = [];
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().req,
AST_NODE_TYPE,
addComponent,
addInterface,
addNode,
addRelationship,
getAST,
getRelationships,
clear,
};
componentMarkers.js
const ReqMarkers = {
CONTAINS: 'contains',
ARROW: 'arrow',
};
const insertLineEndings = (parentNode, conf) => {
let containsNode = parentNode
.append('defs')
.append('marker')
.attr('id', ReqMarkers.CONTAINS + '_line_ending')
.attr('refX', 0)
.attr('refY', conf.line_height / 2)
.attr('markerWidth', conf.line_height)
.attr('markerHeight', conf.line_height)
.attr('orient', 'auto')
.append('g');
containsNode
.append('circle')
.attr('cx', conf.line_height / 2)
.attr('cy', conf.line_height / 2)
.attr('r', conf.line_height / 2)
// .attr('stroke', conf.rect_border_color)
// .attr('stroke-width', 1)
.attr('fill', 'none');
containsNode
.append('line')
.attr('x1', 0)
.attr('x2', conf.line_height)
.attr('y1', conf.line_height / 2)
.attr('y2', conf.line_height / 2)
// .attr('stroke', conf.rect_border_color)
.attr('stroke-width', 1);
containsNode
.append('line')
.attr('y1', 0)
.attr('y2', conf.line_height)
.attr('x1', conf.line_height / 2)
.attr('x2', conf.line_height / 2)
// .attr('stroke', conf.rect_border_color)
.attr('stroke-width', 1);
parentNode
.append('defs')
.append('marker')
.attr('id', ReqMarkers.ARROW + '_line_ending')
.attr('refX', conf.line_height)
.attr('refY', 0.5 * conf.line_height)
.attr('markerWidth', conf.line_height)
.attr('markerHeight', conf.line_height)
.attr('orient', 'auto')
.append('path')
.attr(
'd',
`M0,0
L${conf.line_height},${conf.line_height / 2}
M${conf.line_height},${conf.line_height / 2}
L0,${conf.line_height}`
)
.attr('stroke-width', 1);
// .attr('stroke', conf.rect_border_color);
};
export default {
ReqMarkers,
insertLineEndings,
};
componentRenderer.js
import { line, select } from 'd3';
import dagre from 'dagre';
import graphlib from 'graphlib';
// import * as configApi from '../../config';
import { log, setLogLevel } from '../../logger';
import { configureSvgSize } from '../../utils';
import common from '../common/common';
import { parser } from './parser/componentDiagram';
import componentDb from './componentDb';
import markers from './componentMarkers';
const conf = {};
let relCnt = 0;
export const setConf = function (cnf) {
if (typeof cnf === 'undefined') {
return;
}
const keys = Object.keys(cnf);
for (let i = 0; i < keys.length; i++) {
conf[keys[i]] = cnf[keys[i]];
}
};
const newSystemComponentNode = (parentNode, id, name) => {
const mainBox = parentNode
.insert('rect', '#' + id)
.attr('class', 'component componentBox')
.attr('x', 0)
.attr('y', 0)
.attr('width', conf.rect_min_width + 'px')
.attr('height', '60px');
const decorWidth = 16;
const decorHeight = 16;
const decorOffset = 5;
parentNode
.append('rect')
.attr('id', id + '-decor')
.attr('class', 'component componentBox cornerDecor')
.attr('x', `${conf.rect_min_width - decorWidth - decorOffset}px`)
.attr('y', `${decorOffset}px`)
.attr('width', `${decorWidth}px`)
.attr('height', `${decorHeight}px`);
parentNode
.append('rect')
.attr('id', id + '-decor-2')
.attr('class', 'component componentBox cornerDecor')
.attr('x', `${conf.rect_min_width - decorWidth - decorOffset - 5}px`)
.attr('y', `${decorOffset + 3}px`)
.attr('width', `10px`)
.attr('height', `3px`);
parentNode
.append('rect')
.attr('id', id + '-decor-3')
.attr('class', 'component componentBox cornerDecor')
.attr('x', `${conf.rect_min_width - decorWidth - decorOffset - 5}px`)
.attr('y', `${decorOffset + 9}px`)
.attr('width', `10px`)
.attr('height', `3px`);
let x = conf.rect_min_width / 2;
let title = parentNode
.append('text')
.attr('class', 'component componentLabel componentTitle')
.attr('id', id + '-name')
.attr('x', x)
.attr('y', '25px')
.attr('dominant-baseline', 'hanging')
.append('tspan')
.attr('text-anchor', 'middle')
.attr('x', conf.rect_min_width / 2)
.attr('dy', 0)
.text(name);
return mainBox;
};
const newSystemInterfaceNode = (parentNode, id, name) => {
const mainBox = parentNode
.insert('rect', '#' + id)
.attr('class', 'component interface')
.attr('x', 0)
.attr('y', 0)
.attr('width', '100px')
.attr('height', '32px');
const circleNode = parentNode
.append('circle')
.attr('id', id + '-decor')
.attr('class', 'component interface interface-decor')
.attr('cx', `${50}px`)
.attr('cy', `16px`)
.attr('r', `10px`);
parentNode
.append('text')
.attr('class', 'component interface interfaceTitle')
.attr('id', id + '-name')
.attr('x', `0px`)
.attr('y', '30px')
.attr('dominant-baseline', 'hanging')
.append('tspan')
.attr('text-anchor', 'middle')
.attr('x', 100 / 2)
.attr('dy', 0)
.text(name);
return mainBox;
};
const newSystemNodeNode = (parentNode, id, name, type) => {
parentNode
.append('rect')
.attr('id', id + '-box')
.attr('class', 'component componentBox')
.attr('x', 0)
.attr('y', 0)
.attr('width', conf.rect_min_width + 'px')
.attr('height', conf.rect_min_height + 'px');
parentNode
.append('rect')
.attr('id', id + '-decor1')
.attr('class', 'component componentBox componentBox-decor')
.attr('x', '0')
.attr('y', '0')
.attr('width', '10px')
.attr('height', conf.rect_min_height + 'px')
.attr('transform', `translate(${conf.rect_min_width}, 0), skewY(-45)`);
parentNode
.append('rect')
.attr('id', id + '-decor2')
.attr('class', 'component componentBox')
.attr('x', '0px')
.attr('y', '-10px')
.attr('width', conf.rect_min_width + 'px')
.attr('height', '10px')
.attr('transform', 'skewX(-45)');
// <rect x="-3" y="-3" width="2" height="6" fill="red" transform="skewY(-30)"></rect>
let x = conf.rect_min_width / 2;
let title = parentNode
.append('text')
.attr('class', 'component componentLabel componentTitle')
.attr('id', id + '-name')
.attr('x', x)
.attr('y', conf.rect_padding + 'px')
.attr('dominant-baseline', 'hanging');
title
.append('tspan')
.attr('text-anchor', 'left')
.attr('x', '20px')
.attr('dy', 0)
.text(`<<${type}>>`);
title
.append('tspan')
.attr('text-anchor', 'left')
.attr('x', '20px')
.attr('dy', conf.line_height * 0.75)
.text(name);
};
const addEdgeLabel = (parentNode, svgPath, conf, txt) => {
// Find the half-way point
const len = svgPath.node().getTotalLength();
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
// Append a text node containing the label
const labelId = 'rel' + relCnt;
relCnt++;
const labelNode = parentNode
.append('text')
.attr('class', 'component relationshipLabel')
.attr('id', labelId)
.attr('x', labelPoint.x)
.attr('y', labelPoint.y)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
// .attr('style', 'font-family: ' + conf.fontFamily + '; font-size: ' + conf.fontSize + 'px')
.text(txt);
// Figure out how big the opaque 'container' rectangle needs to be
const labelBBox = labelNode.node().getBBox();
// Insert the opaque rectangle before the text label
parentNode
.insert('rect', '#' + labelId)
.attr('class', 'component componentLabelBox')
.attr('x', labelPoint.x - labelBBox.width / 2)
.attr('y', labelPoint.y - labelBBox.height / 2)
.attr('width', labelBBox.width)
.attr('height', labelBBox.height)
.attr('fill', 'white')
.attr('fill-opacity', '85%');
};
const drawRelationshipFromLayout = function (svg, rel, g, insert) {
// Find the edge relating to this relationship
const edge = g.edge(elementString(rel.src), elementString(rel.dst));
// TODO: Comment this putting the arrow up to the right side of the source circle
const srcNode = g.node(rel.src);
if (srcNode.shape === 'CIRCLE' && edge.points[0].x > srcNode.x && edge.points[0].y > srcNode.y) {
edge.points[0].x = srcNode.x;
edge.points[0].y = srcNode.y + 10;
} else if (
srcNode.shape === 'CIRCLE' &&
edge.points[0].x > srcNode.x &&
edge.points[0].y < srcNode.y
) {
edge.points[0].x = srcNode.x;
edge.points[0].y = srcNode.y - 10;
} else if (srcNode.shape === 'CIRCLE' && edge.points[0].x > srcNode.x) {
edge.points[0].x = srcNode.x + 10;
}
// TODO: Comment this putting the arrow up to the left side of the destination circle
const dstNode = g.node(rel.dst);
if (
dstNode.shape === 'CIRCLE' &&
edge.points[edge.points.length - 1].x < dstNode.x &&
edge.points[edge.points.length - 1].y > dstNode.y
) {
edge.points[edge.points.length - 1].x = dstNode.x;
edge.points[edge.points.length - 1].y = dstNode.y + 10;
} else if (
dstNode.shape === 'CIRCLE' &&
edge.points[edge.points.length - 1].x < dstNode.x &&
edge.points[edge.points.length - 1].y < dstNode.y
) {
edge.points[edge.points.length - 1].x = dstNode.x;
edge.points[edge.points.length - 1].y = dstNode.y - 10;
} else if (dstNode.shape === 'CIRCLE' && edge.points[edge.points.length - 1].x < dstNode.x) {
edge.points[edge.points.length - 1].x = dstNode.x - 10;
}
// Get a function that will generate the line path
const lineFunction = line()
.x(function (d) {
return d.x;
})
.y(function (d) {
return d.y;
});
// Insert the line at the right place
const svgPath = svg
.insert('path', '#' + insert)
.attr('class', 'er relationshipLine')
.attr('d', lineFunction(edge.points))
.attr('fill', 'none');
if (rel.type === 'DASHED') {
svgPath.attr('stroke-dasharray', '10,7');
}
svgPath.attr(
'marker-end',
'url(' +
common.getUrl(conf.arrowMarkerAbsolute) +
'#' +
markers.ReqMarkers.ARROW +
'_line_ending' +
')'
);
addEdgeLabel(svg, svgPath, conf, rel.text);
return;
};
export const drawASTNode = (ASTNode, graph, svgNode, parentNodeName = null) => {
log.info('Adding new ASTNode: ', ASTNode.name);
const groupNode = svgNode.append('g').attr('id', ASTNode.name);
const textId = `${ASTNode.astType.toLowerCase()}-${ASTNode.name}`;
let mainNode;
let shape = 'RECTANGLE';
if (ASTNode.astType === componentDb.AST_NODE_TYPE.COMPONENT) {
mainNode = newSystemComponentNode(groupNode, textId, ASTNode.name);
} else if (ASTNode.astType === componentDb.AST_NODE_TYPE.INTERFACE) {
mainNode = newSystemInterfaceNode(groupNode, textId, ASTNode.name);
shape = 'CIRCLE';
} else if (ASTNode.astType === componentDb.AST_NODE_TYPE.NODE) {
newSystemNodeNode(groupNode, textId, ASTNode.name, ASTNode.type);
mainNode = groupNode;
ASTNode.children.forEach((child) => {
drawASTNode(child, graph, svgNode, ASTNode.name);
});
}
if (parentNodeName) {
graph.setParent(ASTNode.name, parentNodeName);
}
const rectBBox = mainNode.node().getBBox();
// Add the entity to the graph
graph.setNode(ASTNode.name, {
width: rectBBox.width,
height: rectBBox.height,
id: ASTNode.name,
shape,
});
};
const addRelationships = (relationships, g) => {
relationships.forEach(function (r) {
let src = elementString(r.src);
let dst = elementString(r.dst);
g.setEdge(src, dst, { relationship: r });
});
return relationships;
};
const calculateContainerDimentions = (graph, nodeName) => {
let furthestRight = 0;
let furthestBottom = 0;
graph.children(nodeName).forEach((childName) => {
const child = graph.node(childName);
if (nodeName === 'gb-core-cac-dev') {
console.log(childName, child.x, child.width);
}
furthestRight = Math.max(furthestRight, child.x - child.width / 2 + child.width);
furthestBottom = Math.max(furthestBottom, child.y - child.height / 2 + child.height);
});
if (nodeName === 'gb-core-cac-dev') {
console.log(furthestRight);
}
return { furthestRight, furthestBottom };
};
const adjustEntities = function (svgNode, graph) {
console.log(graph);
graph.nodes().forEach(function (v) {
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
svgNode.select('#' + v);
svgNode
.select('#' + v)
.attr(
'transform',
'translate(' +
(graph.node(v).x - graph.node(v).width / 2) +
',' +
(graph.node(v).y - graph.node(v).height / 2) +
' )'
);
if (graph.children(v).length > 0) {
const dimentions = calculateContainerDimentions(graph, v);
const newWidth =
dimentions.furthestRight - (graph.node(v).x - graph.node(v).width / 2) + 20;
const newHeight =
dimentions.furthestBottom - (graph.node(v).y - graph.node(v).height / 2) + 20;
svgNode
.select(`#node-${v}-box`)
.attr('width', `${newWidth}px`)
.attr('height', `${newHeight}px`);
svgNode
.select(`#node-${v}-decor1`)
.attr('height', `${newHeight}px`)
.attr('transform', `translate(${newWidth}, 0), skewY(-45)`);
svgNode.select(`#node-${v}-decor2`).attr('width', `${newWidth}px`);
}
}
});
return;
};
const elementString = (str) => {
return str.replace(/\s/g, '').replace(/\./g, '_');
};
export const draw = (text, id) => {
parser.yy = componentDb;
parser.yy.clear();
parser.parse(text);
const svg = select(`[id='${id}']`);
markers.insertLineEndings(svg, conf);
const g = new graphlib.Graph({
multigraph: true,
compound: true,
})
.setGraph({
rankdir: 'LR', //conf.layoutDirection,
marginx: 20,
marginy: 20,
nodesep: 50,
edgesep: 60,
ranksep: 100,
})
.setDefaultEdgeLabel(function () {
return {};
});
let AST = componentDb.getAST();
let relationships = componentDb.getRelationships();
console.log(AST);
console.table(relationships);
Object.keys(AST).forEach((nodeName) => {
let ASTNode = AST[nodeName];
drawASTNode(ASTNode, g, svg);
});
addRelationships(relationships, g);
dagre.layout(g);
adjustEntities(svg, g);
relationships.forEach(function (rel) {
drawRelationshipFromLayout(svg, rel, g, id);
});
const padding = conf.rect_padding;
const svgBounds = svg.node().getBBox();
const width = svgBounds.width + padding * 2;
const height = svgBounds.height + padding * 2;
configureSvgSize(svg, height, width, conf.useMaxWidth);
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
};
export default {
setConf,
draw,
};
styles.js
const getStyles = (options) => {
return `
marker {
fill: ${options.relationColor};
stroke: ${options.relationColor};
}
marker.cross {
stroke: ${options.lineColor};
}
svg {
font-family: ${options.fontFamily};
font-size: ${options.fontSize};
}
.componentBox {
fill: ${options.componentBackground};
fill-opacity: 100%;
stroke: ${options.componentBorderColor};
stroke-width: ${options.componentBorderSize};
}
.systemNode {
fill: ${options.componentBackground};
fill-opacity: 100%;
stroke: ${options.componentBorderColor};
stroke-width: ${options.componentBorderSize};
}
.componentTitle, .componentLabel{
fill: ${options.componentTextColor};
}
.componentLabelBox {
fill: ${options.relationLabelBackground};
fill-opacity: 100%;
}
.component-title-line {
stroke: ${options.componentBorderColor};
stroke-width: ${options.componentBorderSize};
}
.relationshipLine {
stroke: ${options.relationColor};
stroke-width: 1;
}
.relationshipLabel {
fill: ${options.relationLabelColor};
}
.interface {
fill: transparent;
}
.interface.interface-decor {
fill: ${options.componentBackground};
fill-opacity: 100%;
stroke: ${options.componentBorderColor};
stroke-width: ${options.componentBorderSize};
}
.interface.interfaceTitle {
fill: ${options.componentTextColor};
}
`;
};
// fill', conf.rect_fill)
export default getStyles;
+1
+1
+1
Why not?
As a software architect I use mermaid for drawing class diagrams. Classes compose components. Therefore on the top level I need to create component diagrams too. This type of diagrams is missing in mermaid though.
As an alternative I use plantUML: https://plantuml.com/component-diagram
For reference: https://en.wikipedia.org/wiki/Component_diagram