Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component Diagram #1462

Open
gergelyk opened this issue Jun 10, 2020 · 60 comments
Open

Component Diagram #1462

gergelyk opened this issue Jun 10, 2020 · 60 comments
Labels

Comments

@gergelyk
Copy link

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

@gergelyk gergelyk added Status: Triage Needs to be verified, categorized, etc Type: Enhancement New feature or request labels Jun 10, 2020
@vincenthavinh
Copy link

I would love to have this diagram type handled by mermaidjs !

@malidiab
Copy link

+1 for component diagrams

@vanillajonathan
Copy link

We would need:

  • Nested components
  • Ports
  • Provided interfaces
  • Required interfaces

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

@pcvarma-shadkona
Copy link

+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

@pcandido
Copy link

+1 for component diagrams

1 similar comment
@farukparhat
Copy link

+1 for component diagrams

@pcvarma-shadkona
Copy link

Team,
Please consider this as we have to use another tool like PlantUML for component diagrams.

@cassiomolin
Copy link

cassiomolin commented Apr 21, 2021

+1 for component diagrams

@benzid-wael
Copy link

+1 for component diagram

@prodis
Copy link

prodis commented Jun 18, 2021

+1 for component diagram.

1 similar comment
@auttam
Copy link

auttam commented Jul 6, 2021

+1 for component diagram.

@magoolation
Copy link

+1

1 similar comment
@azZur0
Copy link

azZur0 commented Jul 20, 2021

+1

@gfraiteur
Copy link

+1

We're now using Mermaid to document our API: https://doc.postsharp.net/caravela/api/caravela_framework_aspects

@jabba-jedi
Copy link

+1

@gleisonpauloc
Copy link

+1 for component diagrams.

@bvkeersop
Copy link

+1, would love to see this implemented as well!

@andrewmcgivery
Copy link

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"]
  }
}

image

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!

@vanillajonathan
Copy link

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

@andrewmcgivery
Copy link

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:

image

@gfraiteur
Copy link

Good work.

@philippkoelmel-qubit9
Copy link

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.

@financelurker
Copy link

@andrewmcgivery I'm also interested in your component diagram effort... is there any PR tangling around or have you committed anything of that anywhere?

@kenpower
Copy link

+1

@bamartin125
Copy link

👍 for component diagrams

@rhangelxs
Copy link

+1

1 similar comment
@gkovacs81
Copy link

+1

@jmugan
Copy link

jmugan commented Jul 24, 2022

Came here looking for component diagrams :)

@poetinger
Copy link

+1 for component diagram support. I'd really like to ditch plantuml.

@sidmitra
Copy link

sidmitra commented Mar 6, 2023

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.

@jgreywolf
Copy link
Contributor

@andrewmcgivery Are you still working on this?

@Balu-Varanasi
Copy link

+1

@m-kaminski
Copy link

Hello, what's the status on this bug?

@honza-zidek
Copy link

Hallo mermaid team, any progress here please?

@writeonlycode
Copy link

+1

3 similar comments
@leolcao
Copy link

leolcao commented Aug 22, 2023

+1

@tetofonta
Copy link

+1

@robertjndw
Copy link

+1

@andrewmcgivery
Copy link

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;

@kyouheicf
Copy link

+1

1 similar comment
@cw0
Copy link

cw0 commented Nov 1, 2023

+1

@blakejwc
Copy link

+1

Why not?

@pedrofsn
Copy link

pedrofsn commented Mar 4, 2024

+1

4 similar comments
@david-capilla
Copy link

+1

@salim7
Copy link

salim7 commented Mar 14, 2024

+1

@timidak
Copy link

timidak commented Mar 22, 2024

+1

@timelord666
Copy link

+1

@zuilintan
Copy link

? 谁这么闲在点踩

@david-auk
Copy link

+1

2 similar comments
@Many5900
Copy link

+1

@tbdlarsen
Copy link

+1

@adsouza
Copy link

adsouza commented Dec 22, 2024

https://docs.mermaidchart.com/blog/posts/mermaid-supports-architecture-diagrams

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests