mermaid-js / mermaid

Generation of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown
https://mermaid.js.org
MIT License
71.16k stars 6.41k forks source link

Component Diagram #1462

Open gergelyk opened 4 years ago

gergelyk commented 4 years ago

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

vincenthavinh commented 3 years ago

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

malidiab commented 3 years ago

+1 for component diagrams

vanillajonathan commented 3 years ago

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
pcvarma-shadkona commented 3 years ago

+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 commented 3 years ago

+1 for component diagrams

farukparhat commented 3 years ago

+1 for component diagrams

pcvarma-shadkona commented 3 years ago

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

cassiomolin commented 3 years ago

+1 for component diagrams

benzid-wael commented 3 years ago

+1 for component diagram

prodis commented 3 years ago

+1 for component diagram.

auttam commented 3 years ago

+1 for component diagram.

magoolation commented 3 years ago

+1

azZur0 commented 3 years ago

+1

gfraiteur commented 3 years ago

+1

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

jabba-jedi commented 3 years ago

+1

gleisonpauloc commented 2 years ago

+1 for component diagrams.

bvkeersop commented 2 years ago

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

andrewmcgivery commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

Good work.

philippkoelmel-qubit9 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

+1

bamartin125 commented 2 years ago

:+1: for component diagrams

rhangelxs commented 2 years ago

+1

gkovacs81 commented 2 years ago

+1

jmugan commented 2 years ago

Came here looking for component diagrams :)

nevesenin commented 2 years ago

+1

nicola-lunghi commented 2 years ago

+1 Here!!!

FilipaSimao commented 2 years ago

+1

calebcartwright commented 2 years ago

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.

couling commented 2 years ago

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:

  1. Where a component is placed with it's box (influencing the two dimensional size of the box)
  2. How to route an arrow between connected components.
  3. Where the box is placed on the diagram

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.

nackdai commented 2 years ago

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?

leolcao commented 1 year ago

+1

da-kami commented 1 year ago

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

poetinger commented 1 year ago

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

sidmitra commented 1 year ago

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 commented 1 year ago

@andrewmcgivery Are you still working on this?

Balu-Varanasi commented 1 year ago

+1

m-kaminski commented 1 year ago

Hello, what's the status on this bug?

honza-zidek commented 1 year ago

Hallo mermaid team, any progress here please?

writeonlycode commented 1 year ago

+1

leolcao commented 1 year ago

+1

tetofonta commented 1 year ago

+1

robertjndw commented 1 year ago

+1

andrewmcgivery commented 1 year ago

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 commented 11 months ago

+1

cw0 commented 11 months ago

+1

blakejwc commented 7 months ago

+1

Why not?