diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js
index 6ae7cbe4e..bf2055008 100644
--- a/cypress/integration/rendering/stateDiagram.spec.js
+++ b/cypress/integration/rendering/stateDiagram.spec.js
@@ -13,6 +13,65 @@ describe('State diagram', () => {
);
cy.get('svg');
});
+ it('should render a state with a note', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ State1: The state with a note
+ note right of State1
+ Important information! You can write
+ notes.
+ end note
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
+ it('should render a state with on the left side when so specified', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ State1: The state with a note with minus - and plus + in it
+ note left of State1
+ Important information! You can write
+ notes with . and in them.
+ end note
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
+ it('should render a state with a note together with another state', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ State1: The state with a note +,-
+ note right of State1
+ Important information! You can write +,-
+ notes.
+ end note
+ State1 --> State2 : With +,-
+ note left of State2 : This is the note +,-
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
+ it('should render a states with descriptions including multi-line descriptions', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ State1: This a a single line description
+ State2: This a a multi line description
+ State2: here comes the multi part
+ [*] --> State1
+ State1 --> State2
+ State2 --> [*]
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
it('should render a simple state diagrams', () => {
imgSnapshotTest(
`
@@ -49,9 +108,101 @@ describe('State diagram', () => {
state "Long state description" as XState1
state "Another Long state description" as XState2
XState2 : New line
+ XState1 --> XState2
`,
{ logLevel: 0 }
);
cy.get('svg');
});
+ it('should render composit states', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ [*] --> NotShooting: Pacifist
+ NotShooting --> A
+ NotShooting --> B
+ NotShooting --> C
+
+ state NotShooting {
+ [*] --> Idle: Yet another long long öong öong öong label
+ Idle --> Configuring : EvConfig
+ Configuring --> Idle : EvConfig EvConfig EvConfig EvConfig EvConfig
+ }
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
+ it('should render multiple composit states', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ [*]-->TV
+
+ state TV {
+ [*] --> Off: Off to start with
+ On --> Off : Turn off
+ Off --> On : Turn on
+ }
+
+ TV--> Console : KarlMartin
+
+ state Console {
+ [*] --> Off2: Off to start with
+ On2--> Off2 : Turn off
+ Off2 --> On2 : Turn on
+ On2-->Playing
+
+ state Playing {
+ Alive --> Dead
+ Dead-->Alive
+ }
+ }
+ `,
+ { logLevel: 0 }
+ );
+ });
+ it('should render forks and joins', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ state fork_state <<fork>>
+ [*] --> fork_state
+ fork_state --> State2
+ fork_state --> State3
+
+ state join_state <<join>>
+ State2 --> join_state
+ State3 --> join_state
+ join_state --> State4
+ State4 --> [*]
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
+ it('should render conurrency states', () => {
+ imgSnapshotTest(
+ `
+ stateDiagram
+ [*] --> Active
+
+ state Active {
+ [*] --> NumLockOff
+ NumLockOff --> NumLockOn : EvNumLockPressed
+ NumLockOn --> NumLockOff : EvNumLockPressed
+ --
+ [*] --> CapsLockOff
+ CapsLockOff --> CapsLockOn : EvCapsLockPressed
+ CapsLockOn --> CapsLockOff : EvCapsLockPressed
+ --
+ [*] --> ScrollLockOff
+ ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
+ ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
+ }
+ `,
+ { logLevel: 0 }
+ );
+ cy.get('svg');
+ });
});
diff --git a/cypress/platform/e2e.html b/cypress/platform/e2e.html
index 498b374d2..b1587d6c1 100644
--- a/cypress/platform/e2e.html
+++ b/cypress/platform/e2e.html
@@ -1,7 +1,7 @@
-
diff --git a/src/diagrams/state/id-cache.js b/src/diagrams/state/id-cache.js
new file mode 100644
index 000000000..7a4be0eb1
--- /dev/null
+++ b/src/diagrams/state/id-cache.js
@@ -0,0 +1,16 @@
+const idCache = {};
+
+export const set = (key, val) => {
+ idCache[key] = val;
+};
+
+export const get = k => idCache[k];
+export const keys = () => Object.keys(idCache);
+export const size = () => keys().length;
+
+export default {
+ get,
+ set,
+ keys,
+ size
+};
diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison
index c54ccd357..cbb2aea91 100644
--- a/src/diagrams/state/parser/stateDiagram.jison
+++ b/src/diagrams/state/parser/stateDiagram.jison
@@ -38,41 +38,44 @@
\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */
-"scale"\s+ { this.pushState('SCALE'); console.log('Got scale', yytext);return 'scale'; }
+"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
\d+ return 'WIDTH';
\s+"width" {this.popState();}
"state"\s+ { this.pushState('STATE'); }
-.*"<>" {this.popState();console.log('Fork: ',yytext);return 'FORK';}
-.*"<>" {this.popState();console.log('Join: ',yytext);return 'JOIN';}
+.*"<>" {this.popState();yytext=yytext.slice(0,-8).trim(); console.warn('Fork Fork: ',yytext);return 'FORK';}
+.*"<>" {this.popState();yytext=yytext.slice(0,-8).trim();console.warn('Fork Join: ',yytext);return 'JOIN';}
+.*"[[fork]]" {this.popState();yytext=yytext.slice(0,-8).trim();console.warn('Fork Fork: ',yytext);return 'FORK';}
+.*"[[join]]" {this.popState();yytext=yytext.slice(0,-8).trim();console.warn('Fork Join: ',yytext);return 'JOIN';}
["] this.begin("STATE_STRING");
"as"\s* {this.popState();this.pushState('STATE_ID');return "AS";}
-[^\n\{]* {this.popState();console.log('STATE_ID', yytext);return "ID";}
+[^\n\{]* {this.popState();/* console.log('STATE_ID', yytext);*/return "ID";}
["] this.popState();
-[^"]* { console.log('Long description:', yytext);return "STATE_DESCR";}
-[^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';}
+[^"]* { /*console.log('Long description:', yytext);*/return "STATE_DESCR";}
+[^\n\s\{]+ {/*console.log('COMPOSIT_STATE', yytext);*/return 'COMPOSIT_STATE';}
\n {this.popState();}
-\{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';}
-\} { console.log('Ending struct'); this.popState(); return 'STRUCT_STOP';}}
+\{ {this.popState();this.pushState('struct'); /*console.log('begin struct', yytext);*/return 'STRUCT_START';}
+\} { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';}}
[\n] /* nothing */
"note"\s+ { this.begin('NOTE'); return 'note'; }
-"left of" { this.popState();this.pushState('NOTE_ID');console.log('Got dir');return 'left_of';}
+"left of" { this.popState();this.pushState('NOTE_ID');return 'left_of';}
"right of" { this.popState();this.pushState('NOTE_ID');return 'right_of';}
\" { this.popState();this.pushState('FLOATING_NOTE');}
\s*"as"\s* {this.popState();this.pushState('FLOATING_NOTE_ID');return "AS";}
["] /**/
-[^"]* { console.log('Floating note text: ', yytext);return "NOTE_TEXT";}
-[^\n]* {this.popState();console.log('Floating note ID', yytext);return "ID";}
-\s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');console.log('Got ID for note', yytext);return 'ID';}
-\s*":"[^\+\-:\n,;]+ { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';}
-\s*[^\+\-:,;]+"end note" { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';}
+[^"]* { /*console.log('Floating note text: ', yytext);*/return "NOTE_TEXT";}
+[^\n]* {this.popState();/*console.log('Floating note ID', yytext);*/return "ID";}
+\s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');/*console.log('Got ID for note', yytext);*/return 'ID';}
+\s*":"[^:\n;]+ { this.popState();/*console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.substr(2).trim();return 'NOTE_TEXT';}
+\s*[^:;]+"end note" { this.popState();/*console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.slice(0,-8).trim();return 'NOTE_TEXT';}
-"stateDiagram"\s+ { console.log('Got state diagram', yytext,'#');return 'SD'; }
-"hide empty description" { console.log('HIDE_EMPTY', yytext,'#');return 'HIDE_EMPTY'; }
-"[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';}
-[^:\n\s\-\{]+ { console.log('=>ID=',yytext); return 'ID';}
-\s*":"[^\+\->:\n,;]+ { yytext = yytext.trim(); console.log('Descr = ', yytext); return 'DESCR'; }
+"stateDiagram"\s+ { /*console.log('Got state diagram', yytext,'#');*/return 'SD'; }
+"hide empty description" { /*console.log('HIDE_EMPTY', yytext,'#');*/return 'HIDE_EMPTY'; }
+"[*]" { /*console.log('EDGE_STATE=',yytext);*/ return 'EDGE_STATE';}
+[^:\n\s\-\{]+ { /*console.log('=>ID=',yytext);*/ return 'ID';}
+// \s*":"[^\+\->:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
+\s*":"[^:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
"-->" return '-->';
"--" return 'CONCURRENT';
<> return 'NL';
@@ -89,34 +92,65 @@
start
: SPACE start
| NL start
- | SD document { return $2; }
+ | SD document { /*console.warn('Root document', $2);*/ yy.setRootDoc($2);return $2; }
;
document
: /* empty */ { $$ = [] }
- | document line {$1.push($2);$$ = $1}
+ | document line {
+ if($2!='nl'){
+ $1.push($2);$$ = $1
+ }
+ // console.warn('Got document',$1, $2);
+ }
;
line
- : SPACE statement { console.log('here');$$ = $2 }
- | statement {console.log('line', $1); $$ = $1 }
- | NL { $$=[];}
+ : SPACE statement { $$ = $2 }
+ | statement { $$ = $1 }
+ | NL { $$='nl';}
;
statement
- : idStatement DESCR {yy.addState($1, 'default');yy.addDescription($1, $2.trim());}
- | idStatement '-->' idStatement {yy.addRelation($1, $3);}
- | idStatement '-->' idStatement DESCR {yy.addRelation($1, $3, $4.substr(1).trim());}
+ : idStatement DESCR { /*console.warn('got id and descr', $1, $2.trim());*/$$={ stmt: 'state', id: $1, type: 'default', description: $2.trim()};}
+ | idStatement '-->' idStatement
+ {
+ /*console.warn('got id', $1);yy.addRelation($1, $3);*/
+ $$={ stmt: 'relation', state1: { stmt: 'state', id: $1, type: 'default', description: '' }, state2:{ stmt: 'state', id: $3 ,type: 'default', description: ''}};
+ }
+ | idStatement '-->' idStatement DESCR
+ {
+ /*yy.addRelation($1, $3, $4.substr(1).trim());*/
+ $$={ stmt: 'relation', state1: { stmt: 'state', id: $1, type: 'default', description: '' }, state2:{ stmt: 'state', id: $3 ,type: 'default', description: ''}, description: $4.substr(1).trim()};
+ }
| HIDE_EMPTY
| scale WIDTH
| COMPOSIT_STATE
| COMPOSIT_STATE STRUCT_START document STRUCT_STOP
- | STATE_DESCR AS ID {yy.addState($3, 'default');yy.addDescription($3, $1);}
+ {
+ /* console.warn('Adding document for state without id ', $1);*/
+ $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: $3 }
+ }
+ | STATE_DESCR AS ID { $$={id: $3, type: 'default', description: $1.trim()};}
| STATE_DESCR AS ID STRUCT_START document STRUCT_STOP
- | FORK
- | JOIN
- | CONCURRENT
+ {
+ //console.warn('Adding document for state with id ', $3, $4); yy.addDocument($3);
+ $$={ stmt: 'state', id: $3, type: 'default', description: $1, doc: $5 }
+ }
+ | FORK {
+ $$={ stmt: 'state', id: $1, type: 'fork' }
+ }
+ | JOIN {
+ $$={ stmt: 'state', id: $1, type: 'join' }
+ }
+ | CONCURRENT {
+ $$={ stmt: 'state', id: yy.getDividerId(), type: 'divider' }
+ }
| note notePosition ID NOTE_TEXT
+ {
+ /*console.warn('got NOTE, position: ', $2.trim(), 'id = ', $3.trim(), 'note: ', $4);*/
+ $$={ stmt: 'state', id: $3.trim(), note:{position: $2.trim(), text: $4.trim()}};
+ }
| note NOTE_TEXT AS ID
;
@@ -129,112 +163,5 @@ notePosition
: left_of
| right_of
;
-// statement
-// : 'participant' actor 'AS' restOfLine 'NL' {$2.description=$4; $$=$2;}
-// | 'participant' actor 'NL' {$$=$2;}
-// | signal 'NL'
-// | 'activate' actor 'NL' {$$={type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $2};}
-// | 'deactivate' actor 'NL' {$$={type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $2};}
-// | note_statement 'NL'
-// | title text2 'NL' {$$=[{type:'setTitle', text:$2}]}
-// | 'loop' restOfLine document end
-// {
-// $3.unshift({type: 'loopStart', loopText:$2, signalType: yy.LINETYPE.LOOP_START});
-// $3.push({type: 'loopEnd', loopText:$2, signalType: yy.LINETYPE.LOOP_END});
-// $$=$3;}
-// | 'rect' restOfLine document end
-// {
-// $3.unshift({type: 'rectStart', color:$2, signalType: yy.LINETYPE.RECT_START });
-// $3.push({type: 'rectEnd', color:$2, signalType: yy.LINETYPE.RECT_END });
-// $$=$3;}
-// | opt restOfLine document end
-// {
-// $3.unshift({type: 'optStart', optText:$2, signalType: yy.LINETYPE.OPT_START});
-// $3.push({type: 'optEnd', optText:$2, signalType: yy.LINETYPE.OPT_END});
-// $$=$3;}
-// | alt restOfLine else_sections end
-// {
-// // Alt start
-// $3.unshift({type: 'altStart', altText:$2, signalType: yy.LINETYPE.ALT_START});
-// // Content in alt is already in $3
-// // End
-// $3.push({type: 'altEnd', signalType: yy.LINETYPE.ALT_END});
-// $$=$3;}
-// | par restOfLine par_sections end
-// {
-// // Parallel start
-// $3.unshift({type: 'parStart', parText:$2, signalType: yy.LINETYPE.PAR_START});
-// // Content in par is already in $3
-// // End
-// $3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END});
-// $$=$3;}
-// ;
-
-// par_sections
-// : document
-// | document and restOfLine par_sections
-// { $$ = $1.concat([{type: 'and', parText:$3, signalType: yy.LINETYPE.PAR_AND}, $4]); }
-// ;
-
-// else_sections
-// : document
-// | document else restOfLine else_sections
-// { $$ = $1.concat([{type: 'else', altText:$3, signalType: yy.LINETYPE.ALT_ELSE}, $4]); }
-// ;
-
-// note_statement
-// : 'note' placement actor text2
-// {
-// $$ = [$3, {type:'addNote', placement:$2, actor:$3.actor, text:$4}];}
-// | 'note' 'over' actor_pair text2
-// {
-// // Coerce actor_pair into a [to, from, ...] array
-// $2 = [].concat($3, $3).slice(0, 2);
-// $2[0] = $2[0].actor;
-// $2[1] = $2[1].actor;
-// $$ = [$3, {type:'addNote', placement:yy.PLACEMENT.OVER, actor:$2.slice(0, 2), text:$4}];}
-// ;
-
-// spaceList
-// : SPACE spaceList
-// | SPACE
-// ;
-// actor_pair
-// : actor ',' actor { $$ = [$1, $3]; }
-// | actor { $$ = $1; }
-// ;
-
-// placement
-// : 'left_of' { $$ = yy.PLACEMENT.LEFTOF; }
-// | 'right_of' { $$ = yy.PLACEMENT.RIGHTOF; }
-// ;
-
-// signal
-// : actor signaltype '+' actor text2
-// { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5},
-// {type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $4}
-// ]}
-// | actor signaltype '-' actor text2
-// { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5},
-// {type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $1}
-// ]}
-// | actor signaltype actor text2
-// { $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
-// ;
-
-// actor
-// : ACTOR {$$={type: 'addActor', actor:$1}}
-// ;
-
-// signaltype
-// : SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
-// | DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
-// | SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
-// | DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
-// | SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
-// | DOTTED_CROSS { $$ = yy.LINETYPE.DOTTED_CROSS; }
-// ;
-
-// text2: TXT {$$ = $1.substring(1).trim().replace(/\\n/gm, "\n");} ;
%%
diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js
new file mode 100644
index 000000000..92ffc6c11
--- /dev/null
+++ b/src/diagrams/state/shapes.js
@@ -0,0 +1,389 @@
+import * as d3 from 'd3';
+import idCache from './id-cache.js';
+import stateDb from './stateDb';
+import utils from '../../utils';
+
+// TODO Move conf object to main conf in mermaidAPI
+const conf = {
+ dividerMargin: 10,
+ padding: 5,
+ textHeight: 10,
+ noteMargin: 10
+};
+
+/**
+ * Draws a start state as a black circle
+ */
+export const drawStartState = g =>
+ g
+ .append('circle')
+ .style('stroke', 'black')
+ .style('fill', 'black')
+ .attr('r', 5)
+ .attr('cx', conf.padding + 5)
+ .attr('cy', conf.padding + 5);
+
+/**
+ * Draws a start state as a black circle
+ */
+export const drawDivider = g =>
+ g
+ .append('line')
+ .style('stroke', 'grey')
+ .style('stroke-dasharray', '3')
+ .attr('x1', 10)
+ .attr('class', 'divider')
+ .attr('x2', 20)
+ .attr('y1', 0)
+ .attr('y2', 0);
+
+/**
+ * Draws a an end state as a black circle
+ */
+export const drawSimpleState = (g, stateDef) => {
+ const state = g
+ .append('text')
+ .attr('x', 2 * conf.padding)
+ .attr('y', conf.textHeight + 2 * conf.padding)
+ .attr('font-size', 24)
+ .text(stateDef.id);
+
+ const classBox = state.node().getBBox();
+ g.insert('rect', ':first-child')
+ .attr('x', conf.padding)
+ .attr('y', conf.padding)
+ .attr('width', classBox.width + 2 * conf.padding)
+ .attr('height', classBox.height + 2 * conf.padding)
+ .attr('rx', '5');
+
+ return state;
+};
+
+/**
+ * Draws a state with descriptions
+ * @param {*} g
+ * @param {*} stateDef
+ */
+export const drawDescrState = (g, stateDef) => {
+ const addTspan = function(textEl, txt, isFirst) {
+ const tSpan = textEl
+ .append('tspan')
+ .attr('x', 2 * conf.padding)
+ .text(txt);
+ if (!isFirst) {
+ tSpan.attr('dy', conf.textHeight);
+ }
+ };
+ const title = g
+ .append('text')
+ .attr('x', 2 * conf.padding)
+ .attr('y', conf.textHeight + 1.5 * conf.padding)
+ .attr('font-size', 24)
+ .attr('class', 'state-title')
+ .text(stateDef.id);
+
+ const titleHeight = title.node().getBBox().height;
+
+ const description = g
+ .append('text') // text label for the x axis
+ .attr('x', conf.padding)
+ .attr('y', titleHeight + conf.padding * 0.2 + conf.dividerMargin + conf.textHeight)
+ .attr('fill', 'white')
+ .attr('class', 'state-description');
+
+ let isFirst = true;
+ stateDef.descriptions.forEach(function(descr) {
+ addTspan(description, descr, isFirst);
+ isFirst = false;
+ });
+
+ const descrLine = g
+ .append('line') // text label for the x axis
+ .attr('x1', conf.padding)
+ .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
+ .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2)
+ .attr('class', 'descr-divider');
+ const descrBox = description.node().getBBox();
+ descrLine.attr('x2', descrBox.width + 3 * conf.padding);
+ // const classBox = title.node().getBBox();
+
+ g.insert('rect', ':first-child')
+ .attr('x', conf.padding)
+ .attr('y', conf.padding)
+ .attr('width', descrBox.width + 2 * conf.padding)
+ .attr('height', descrBox.height + titleHeight + 2 * conf.padding)
+ .attr('rx', '5');
+
+ return g;
+};
+
+/**
+ * Adds the creates a box around the existing content and adds a
+ * panel for the id on top of the content.
+ */
+export const addIdAndBox = (g, stateDef) => {
+ // TODO Move hardcodings to conf
+ const addTspan = function(textEl, txt, isFirst) {
+ const tSpan = textEl
+ .append('tspan')
+ .attr('x', 2 * conf.padding)
+ .text(txt);
+ if (!isFirst) {
+ tSpan.attr('dy', conf.textHeight);
+ }
+ };
+ const title = g
+ .append('text')
+ .attr('x', 2 * conf.padding)
+ .attr('y', -15)
+ .attr('font-size', 24)
+ .attr('class', 'state-title')
+ .text(stateDef.id);
+
+ const titleHeight = title.node().getBBox().height;
+
+ const lineY = -9;
+ const descrLine = g
+ .append('line') // text label for the x axis
+ .attr('x1', 0)
+ .attr('y1', lineY)
+ .attr('y2', lineY)
+ .attr('class', 'descr-divider');
+
+ const graphBox = g.node().getBBox();
+ title.attr('x', graphBox.width / 2 - title.node().getBBox().width / 2);
+ descrLine.attr('x2', graphBox.width + conf.padding);
+
+ g.insert('rect', ':first-child')
+ .attr('x', graphBox.x)
+ .attr('y', -15 - conf.textHeight - conf.padding)
+ .attr('width', graphBox.width + conf.padding)
+ .attr('height', graphBox.height + 3 + conf.textHeight)
+ .attr('rx', '5');
+
+ return g;
+};
+
+const drawEndState = g => {
+ g.append('circle')
+ .style('stroke', 'black')
+ .style('fill', 'white')
+ .attr('r', 7)
+ .attr('cx', conf.padding + 7)
+ .attr('cy', conf.padding + 7);
+
+ return g
+ .append('circle')
+ .style('stroke', 'black')
+ .style('fill', 'black')
+ .attr('r', 5)
+ .attr('cx', conf.padding + 7)
+ .attr('cy', conf.padding + 7);
+};
+const drawForkJoinState = g => {
+ return g
+ .append('rect')
+ .style('stroke', 'black')
+ .style('fill', 'black')
+ .attr('width', 70)
+ .attr('height', 7)
+ .attr('x', conf.padding)
+ .attr('y', conf.padding);
+};
+
+export const drawText = function(elem, textData, width) {
+ // Remove and ignore br:s
+ const nText = textData.text.replace(/
/gi, ' ');
+
+ const textElem = elem.append('text');
+ textElem.attr('x', textData.x);
+ textElem.attr('y', textData.y);
+ textElem.style('text-anchor', textData.anchor);
+ textElem.attr('fill', textData.fill);
+ if (typeof textData.class !== 'undefined') {
+ textElem.attr('class', textData.class);
+ }
+
+ const span = textElem.append('tspan');
+ span.attr('x', textData.x + textData.textMargin * 2);
+ span.attr('fill', textData.fill);
+ span.text(nText);
+
+ return textElem;
+};
+
+const _drawLongText = (_text, x, y, g) => {
+ let textHeight = 0;
+ let textWidth = 0;
+ const textElem = g.append('text');
+ textElem.style('text-anchor', 'start');
+ textElem.attr('class', 'noteText');
+
+ let text = _text.replace(/\r\n/g, '
');
+ text = text.replace(/\n/g, '
');
+ const lines = text.split(/
/gi);
+ for (const line of lines) {
+ const txt = line.trim();
+
+ if (txt.length > 0) {
+ const span = textElem.append('tspan');
+ span.text(txt);
+ const textBounds = span.node().getBBox();
+ textHeight += textBounds.height;
+ span.attr('x', x + conf.noteMargin);
+ span.attr('y', y + textHeight + 1.25 * conf.noteMargin);
+ // textWidth = Math.max(textBounds.width, textWidth);
+ }
+ }
+ return { textWidth: textElem.node().getBBox().width, textHeight };
+};
+
+/**
+ * Draws an actor in the diagram with the attaced line
+ * @param center - The center of the the actor
+ * @param pos The position if the actor in the liost of actors
+ * @param description The text in the box
+ */
+
+export const drawNote = (text, g) => {
+ g.attr('class', 'note');
+ const note = g
+ .append('rect')
+ .attr('x', 0)
+ .attr('y', conf.padding);
+ const rectElem = g.append('g');
+
+ const { textWidth, textHeight } = _drawLongText(text, 0, 0, rectElem);
+ note.attr('height', textHeight + 2 * conf.noteMargin);
+ note.attr('width', textWidth + conf.noteMargin * 2);
+
+ return note;
+};
+
+/**
+ * Starting point for drawing a state. The function finds out the specifics
+ * about the state and renders with approprtiate function.
+ * @param {*} elem
+ * @param {*} stateDef
+ */
+export const drawState = function(elem, stateDef, graph, doc) {
+ const id = stateDef.id;
+ const stateInfo = {
+ id: id,
+ label: stateDef.id,
+ width: 0,
+ height: 0
+ };
+
+ const g = elem
+ .append('g')
+ .attr('id', id)
+ .attr('class', 'classGroup');
+
+ if (stateDef.type === 'start') drawStartState(g);
+ if (stateDef.type === 'end') drawEndState(g);
+ if (stateDef.type === 'fork' || stateDef.type === 'join') drawForkJoinState(g);
+ if (stateDef.type === 'note') drawNote(stateDef.note.text, g);
+ if (stateDef.type === 'divider') drawDivider(g);
+ if (stateDef.type === 'default' && stateDef.descriptions.length === 0)
+ drawSimpleState(g, stateDef);
+ if (stateDef.type === 'default' && stateDef.descriptions.length > 0) drawDescrState(g, stateDef);
+
+ const stateBox = g.node().getBBox();
+ stateInfo.width = stateBox.width + 2 * conf.padding;
+ stateInfo.height = stateBox.height + 2 * conf.padding;
+
+ idCache.set(id, stateInfo);
+ // stateCnt++;
+ return stateInfo;
+};
+
+let edgeCount = 0;
+export const drawEdge = function(elem, path, relation) {
+ const getRelationType = function(type) {
+ switch (type) {
+ case stateDb.relationType.AGGREGATION:
+ return 'aggregation';
+ case stateDb.relationType.EXTENSION:
+ return 'extension';
+ case stateDb.relationType.COMPOSITION:
+ return 'composition';
+ case stateDb.relationType.DEPENDENCY:
+ return 'dependency';
+ }
+ };
+
+ path.points = path.points.filter(p => !Number.isNaN(p.y));
+
+ // The data for our line
+ const lineData = path.points;
+
+ // This is the accessor function we talked about above
+ const lineFunction = d3
+ .line()
+ .x(function(d) {
+ return d.x;
+ })
+ .y(function(d) {
+ return d.y;
+ })
+ .curve(d3.curveBasis);
+
+ const svgPath = elem
+ .append('path')
+ .attr('d', lineFunction(lineData))
+ .attr('id', 'edge' + edgeCount)
+ .attr('class', 'relation');
+ let url = '';
+ if (conf.arrowMarkerAbsolute) {
+ url =
+ window.location.protocol +
+ '//' +
+ window.location.host +
+ window.location.pathname +
+ window.location.search;
+ url = url.replace(/\(/g, '\\(');
+ url = url.replace(/\)/g, '\\)');
+ }
+
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')'
+ );
+
+ if (typeof relation.title !== 'undefined') {
+ const g = elem.append('g').attr('class', 'classLabel');
+ const label = g
+ .append('text')
+ .attr('class', 'label')
+ .attr('fill', 'red')
+ .attr('text-anchor', 'middle')
+ .text(relation.title);
+ const { x, y } = utils.calcLabelPosition(path.points);
+ label.attr('x', x).attr('y', y);
+ const bounds = label.node().getBBox();
+ g.insert('rect', ':first-child')
+ .attr('class', 'box')
+ .attr('x', bounds.x - conf.padding / 2)
+ .attr('y', bounds.y - conf.padding / 2)
+ .attr('width', bounds.width + conf.padding)
+ .attr('height', bounds.height + conf.padding);
+ // Debug points
+ // path.points.forEach(point => {
+ // g.append('circle')
+ // .style('stroke', 'red')
+ // .style('fill', 'red')
+ // .attr('r', 1)
+ // .attr('cx', point.x)
+ // .attr('cy', point.y);
+ // });
+ // g.append('circle')
+ // .style('stroke', 'blue')
+ // .style('fill', 'blue')
+ // .attr('r', 1)
+ // .attr('cx', x)
+ // .attr('cy', y);
+ }
+
+ edgeCount++;
+};
diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js
index 5a24f5680..b84d66fad 100644
--- a/src/diagrams/state/stateDb.js
+++ b/src/diagrams/state/stateDb.js
@@ -1,7 +1,40 @@
import { logger } from '../../logger';
-let relations = [];
-let states = {};
+let rootDoc = [];
+const setRootDoc = o => {
+ logger.info('Setting root doc', o);
+ rootDoc = o;
+};
+
+const getRootDoc = () => rootDoc;
+
+const extract = doc => {
+ const res = { states: [], relations: [] };
+ clear();
+
+ doc.forEach(item => {
+ if (item.stmt === 'state') {
+ addState(item.id, item.type, item.doc, item.description, item.note);
+ }
+ if (item.stmt === 'relation') {
+ addRelation(item.state1.id, item.state2.id, item.description);
+ }
+ });
+};
+
+const newDoc = () => {
+ return {
+ relations: [],
+ states: {},
+ documents: {}
+ };
+};
+
+let documents = {
+ root: newDoc()
+};
+
+let currentDocument = documents.root;
let startCnt = 0;
let endCnt = 0;
@@ -13,32 +46,46 @@ let endCnt = 0;
* @param type
* @param style
*/
-export const addState = function(id, type) {
- if (typeof states[id] === 'undefined') {
- states[id] = {
+export const addState = function(id, type, doc, descr, note) {
+ if (typeof currentDocument.states[id] === 'undefined') {
+ currentDocument.states[id] = {
id: id,
descriptions: [],
- type
+ type,
+ doc,
+ note
};
+ } else {
+ if (!currentDocument.states[id].doc) {
+ currentDocument.states[id].doc = doc;
+ }
+ if (!currentDocument.states[id].type) {
+ currentDocument.states[id].type = type;
+ }
}
+ if (descr) addDescription(id, descr.trim());
+ if (note) currentDocument.states[id].note = note;
};
export const clear = function() {
- relations = [];
- states = {};
+ documents = {
+ root: newDoc()
+ };
+ currentDocument = documents.root;
};
export const getState = function(id) {
- return states[id];
-};
-export const getStates = function() {
- return states;
+ return currentDocument.states[id];
};
+export const getStates = function() {
+ return currentDocument.states;
+};
+export const logDocuments = function() {
+ logger.info('Documents = ', documents);
+};
export const getRelations = function() {
- // const relations1 = [{ id1: 'start1', id2: 'state1' }, { id1: 'state1', id2: 'exit1' }];
- // return relations;
- return relations;
+ return currentDocument.relations;
};
export const addRelation = function(_id1, _id2, title) {
@@ -56,14 +103,13 @@ export const addRelation = function(_id1, _id2, title) {
id2 = 'end' + startCnt;
type2 = 'end';
}
- console.log(id1, id2, title);
addState(id1, type1);
addState(id2, type2);
- relations.push({ id1, id2, title });
+ currentDocument.relations.push({ id1, id2, title });
};
-export const addDescription = function(id, _descr) {
- const theState = states[id];
+const addDescription = function(id, _descr) {
+ const theState = currentDocument.states[id];
let descr = _descr;
if (descr[0] === ':') {
descr = descr.substr(1).trim();
@@ -72,12 +118,6 @@ export const addDescription = function(id, _descr) {
theState.descriptions.push(descr);
};
-export const addMembers = function(className, MembersArr) {
- if (Array.isArray(MembersArr)) {
- MembersArr.forEach(member => addMember(className, member));
- }
-};
-
export const cleanupLabel = function(label) {
if (label.substring(0, 1) === ':') {
return label.substr(2).trim();
@@ -91,6 +131,12 @@ export const lineType = {
DOTTED_LINE: 1
};
+let dividerCnt = 0;
+const getDividerId = () => {
+ dividerCnt++;
+ return 'divider-id-' + dividerCnt;
+};
+
export const relationType = {
AGGREGATION: 0,
EXTENSION: 1,
@@ -105,9 +151,13 @@ export default {
getStates,
getRelations,
addRelation,
- addDescription,
- addMembers,
+ getDividerId,
+ // addDescription,
cleanupLabel,
lineType,
- relationType
+ relationType,
+ logDocuments,
+ getRootDoc,
+ setRootDoc,
+ extract
};
diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js
index d8918edd7..ee3152682 100644
--- a/src/diagrams/state/stateDiagram.spec.js
+++ b/src/diagrams/state/stateDiagram.spec.js
@@ -8,7 +8,7 @@ describe('state diagram, ', function() {
parser.yy = stateDb;
});
- fit('super simple', function() {
+ it('super simple', function() {
const str = `
stateDiagram
[*] --> State1
@@ -16,27 +16,6 @@ describe('state diagram, ', function() {
`;
parser.parse(str);
- expect(stateDb.getRelations()).toEqual([
- { id1: 'start1', id2: 'State1' },
- { id1: 'State1', id2: 'end1' }
- ]);
- expect(stateDb.getStates()).toEqual({
- State1: {
- id: 'State1',
- type: 'default',
- descriptions: []
- },
- end1: {
- id: 'end1',
- type: 'end',
- descriptions: []
- },
- start1: {
- id: 'start1',
- type: 'start',
- descriptions: []
- }
- });
});
it('simple', function() {
const str = `stateDiagram\n
@@ -79,7 +58,7 @@ describe('state diagram, ', function() {
scale 350 width
[*] --> State1
State1 --> [*]
- State1 : this is a string
+ State1 : this is a string with - in it
State1 : this is another string
State1 --> State2
@@ -92,7 +71,16 @@ describe('state diagram, ', function() {
it('description after second state', function() {
const str = `stateDiagram\n
scale 350 width
- [*] --> State1 : This is the description
+ [*] --> State1 : This is the description with - in it
+ State1 --> [*]
+ `;
+
+ parser.parse(str);
+ });
+ it('shall handle descriptions inkluding minus signs', function() {
+ const str = `stateDiagram\n
+ scale 350 width
+ [*] --> State1 : This is the description +-!
State1 --> [*]
`;
diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js
index 800465845..467393224 100644
--- a/src/diagrams/state/stateRenderer.js
+++ b/src/diagrams/state/stateRenderer.js
@@ -5,29 +5,30 @@ import { logger } from '../../logger';
import stateDb from './stateDb';
import { parser } from './parser/stateDiagram';
import utils from '../../utils';
+import idCache from './id-cache';
+import { drawState, addIdAndBox, drawEdge, drawNote } from './shapes';
parser.yy = stateDb;
-const idCache = {};
-
-let stateCnt = 0;
let total = 0;
-let edgeCount = 0;
+// TODO Move conf object to main conf in mermaidAPI
const conf = {
dividerMargin: 10,
padding: 5,
textHeight: 10
};
+const transformationLog = {};
+
export const setConf = function(cnf) {};
// Todo optimize
const getGraphId = function(label) {
- const keys = Object.keys(idCache);
+ const keys = idCache.keys();
for (let i = 0; i < keys.length; i++) {
- if (idCache[keys[i]].label === label) {
+ if (idCache.get(keys[i]).label === label) {
return keys[i];
}
}
@@ -39,94 +40,6 @@ const getGraphId = function(label) {
* Setup arrow head and define the marker. The result is appended to the svg.
*/
const insertMarkers = function(elem) {
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'extensionStart')
- .attr('class', 'extension')
- .attr('refX', 0)
- .attr('refY', 7)
- .attr('markerWidth', 190)
- .attr('markerHeight', 240)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 1,7 L18,13 V 1 Z');
-
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'extensionEnd')
- .attr('refX', 19)
- .attr('refY', 7)
- .attr('markerWidth', 20)
- .attr('markerHeight', 28)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
-
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'compositionStart')
- .attr('class', 'extension')
- .attr('refX', 0)
- .attr('refY', 7)
- .attr('markerWidth', 190)
- .attr('markerHeight', 240)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
-
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'compositionEnd')
- .attr('refX', 19)
- .attr('refY', 7)
- .attr('markerWidth', 20)
- .attr('markerHeight', 28)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
-
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'aggregationStart')
- .attr('class', 'extension')
- .attr('refX', 0)
- .attr('refY', 7)
- .attr('markerWidth', 190)
- .attr('markerHeight', 240)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
-
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'aggregationEnd')
- .attr('refX', 19)
- .attr('refY', 7)
- .attr('markerWidth', 20)
- .attr('markerHeight', 28)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
-
- elem
- .append('defs')
- .append('marker')
- .attr('id', 'dependencyStart')
- .attr('class', 'extension')
- .attr('refX', 0)
- .attr('refY', 7)
- .attr('markerWidth', 190)
- .attr('markerHeight', 240)
- .attr('orient', 'auto')
- .append('path')
- .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
-
elem
.append('defs')
.append('marker')
@@ -139,292 +52,6 @@ const insertMarkers = function(elem) {
.append('path')
.attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z');
};
-const drawStart = function(elem, stateDef) {
- logger.info('Rendering class ' + stateDef);
-
- const addTspan = function(textEl, txt, isFirst) {
- const tSpan = textEl
- .append('tspan')
- .attr('x', conf.padding)
- .text(txt);
- if (!isFirst) {
- tSpan.attr('dy', conf.textHeight);
- }
- };
-
- const id = 'classId' + (stateCnt % total);
- const stateInfo = {
- id: id,
- label: stateDef.id,
- width: 0,
- height: 0
- };
-
- const g = elem
- .append('g')
- .attr('id', id)
- .attr('class', 'classGroup');
- const title = g
- .append('text')
- .attr('x', conf.padding)
- .attr('y', conf.textHeight + conf.padding)
- .text(stateDef.id);
-
- const titleHeight = title.node().getBBox().height;
-
- const stateBox = g.node().getBBox();
- g.insert('rect', ':first-child')
- .attr('x', 0)
- .attr('y', 0)
- .attr('width', stateBox.width + 2 * conf.padding)
- .attr('height', stateBox.height + conf.padding + 0.5 * conf.dividerMargin);
-
- membersLine.attr('x2', stateBox.width + 2 * conf.padding);
- methodsLine.attr('x2', stateBox.width + 2 * conf.padding);
-
- stateInfo.width = stateBox.width + 2 * conf.padding;
- stateInfo.height = stateBox.height + conf.padding + 0.5 * conf.dividerMargin;
-
- idCache[id] = stateInfo;
- stateCnt++;
- return stateInfo;
-};
-
-/**
- * Draws a start state as a black circle
- */
-const drawStartState = g =>
- g
- .append('circle')
- .style('stroke', 'black')
- .style('fill', 'black')
- .attr('r', 5)
- .attr('cx', conf.padding + 5)
- .attr('cy', conf.padding + 5);
-/**
- * Draws a an end state as a black circle
- */
-const drawSimpleState = (g, stateDef) => {
- const state = g
- .append('text')
- .attr('x', 2 * conf.padding)
- .attr('y', conf.textHeight + 2 * conf.padding)
- .attr('font-size', 24)
- .text(stateDef.id);
-
- const classBox = state.node().getBBox();
- g.insert('rect', ':first-child')
- .attr('x', conf.padding)
- .attr('y', conf.padding)
- .attr('width', classBox.width + 2 * conf.padding)
- .attr('height', classBox.height + 2 * conf.padding)
- .attr('rx', '5');
-
- return state;
-};
-/**
- * Draws a state with descriptions
- * @param {*} g
- * @param {*} stateDef
- */
-const drawDescrState = (g, stateDef) => {
- const addTspan = function(textEl, txt, isFirst) {
- const tSpan = textEl
- .append('tspan')
- .attr('x', 2 * conf.padding)
- .text(txt);
- if (!isFirst) {
- tSpan.attr('dy', conf.textHeight);
- }
- };
- const title = g
- .append('text')
- .attr('x', 2 * conf.padding)
- .attr('y', conf.textHeight + 1.5 * conf.padding)
- .attr('font-size', 24)
- .attr('class', 'state-title')
- .text(stateDef.id);
-
- const titleHeight = title.node().getBBox().height;
-
- const description = g
- .append('text') // text label for the x axis
- .attr('x', conf.padding)
- .attr('y', titleHeight + conf.padding * 0.2 + conf.dividerMargin + conf.textHeight)
- .attr('fill', 'white')
- .attr('class', 'state-description');
-
- let isFirst = true;
- stateDef.descriptions.forEach(function(descr) {
- addTspan(description, descr, isFirst);
- isFirst = false;
- });
-
- const descrLine = g
- .append('line') // text label for the x axis
- .attr('x1', conf.padding)
- .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
- .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2)
- .attr('class', 'descr-divider');
- const descrBox = description.node().getBBox();
- descrLine.attr('x2', descrBox.width + 3 * conf.padding);
- // const classBox = title.node().getBBox();
-
- g.insert('rect', ':first-child')
- .attr('x', conf.padding)
- .attr('y', conf.padding)
- .attr('width', descrBox.width + 2 * conf.padding)
- .attr('height', descrBox.height + titleHeight + 2 * conf.padding)
- .attr('rx', '5');
-
- return g;
-};
-const drawEndState = g => {
- g.append('circle')
- .style('stroke', 'black')
- .style('fill', 'white')
- .attr('r', 7)
- .attr('cx', conf.padding + 7)
- .attr('cy', conf.padding + 7);
-
- return g
- .append('circle')
- .style('stroke', 'black')
- .style('fill', 'black')
- .attr('r', 5)
- .attr('cx', conf.padding + 7)
- .attr('cy', conf.padding + 7);
-};
-
-const drawEdge = function(elem, path, relation) {
- const getRelationType = function(type) {
- switch (type) {
- case stateDb.relationType.AGGREGATION:
- return 'aggregation';
- case stateDb.relationType.EXTENSION:
- return 'extension';
- case stateDb.relationType.COMPOSITION:
- return 'composition';
- case stateDb.relationType.DEPENDENCY:
- return 'dependency';
- }
- };
-
- path.points = path.points.filter(p => !Number.isNaN(p.y));
-
- // The data for our line
- const lineData = path.points;
-
- // This is the accessor function we talked about above
- const lineFunction = d3
- .line()
- .x(function(d) {
- return d.x;
- })
- .y(function(d) {
- return d.y;
- })
- .curve(d3.curveBasis);
-
- const svgPath = elem
- .append('path')
- .attr('d', lineFunction(lineData))
- .attr('id', 'edge' + edgeCount)
- .attr('class', 'relation');
- let url = '';
- if (conf.arrowMarkerAbsolute) {
- url =
- window.location.protocol +
- '//' +
- window.location.host +
- window.location.pathname +
- window.location.search;
- url = url.replace(/\(/g, '\\(');
- url = url.replace(/\)/g, '\\)');
- }
-
- svgPath.attr(
- 'marker-end',
- 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')'
- );
-
- if (typeof relation.title !== 'undefined') {
- const g = elem.append('g').attr('class', 'classLabel');
- const label = g
- .append('text')
- .attr('class', 'label')
- .attr('fill', 'red')
- .attr('text-anchor', 'middle')
- .text(relation.title);
-
- const { x, y } = utils.calcLabelPosition(path.points);
- label.attr('x', x).attr('y', y);
-
- const bounds = label.node().getBBox();
- g.insert('rect', ':first-child')
- .attr('class', 'box')
- .attr('x', bounds.x - conf.padding / 2)
- .attr('y', bounds.y - conf.padding / 2)
- .attr('width', bounds.width + conf.padding)
- .attr('height', bounds.height + conf.padding);
-
- // Debug points
- // path.points.forEach(point => {
- // g.append('circle')
- // .style('stroke', 'red')
- // .style('fill', 'red')
- // .attr('r', 1)
- // .attr('cx', point.x)
- // .attr('cy', point.y);
- // });
-
- // g.append('circle')
- // .style('stroke', 'blue')
- // .style('fill', 'blue')
- // .attr('r', 1)
- // .attr('cx', x)
- // .attr('cy', y);
- }
-
- edgeCount++;
-};
-
-/**
- * Draws a state
- * @param {*} elem
- * @param {*} stateDef
- */
-const drawState = function(elem, stateDef) {
- // logger.info('Rendering class ' + stateDef);
-
- const id = stateDef.id;
- const stateInfo = {
- id: id,
- label: stateDef.id,
- width: 0,
- height: 0
- };
-
- const g = elem
- .append('g')
- .attr('id', id)
- .attr('class', 'classGroup');
-
- if (stateDef.type === 'start') drawStartState(g);
- if (stateDef.type === 'end') drawEndState(g);
- if (stateDef.type === 'default' && stateDef.descriptions.length === 0)
- drawSimpleState(g, stateDef);
- if (stateDef.type === 'default' && stateDef.descriptions.length > 0) drawDescrState(g, stateDef);
-
- const stateBox = g.node().getBBox();
-
- stateInfo.width = stateBox.width + 2 * conf.padding;
- stateInfo.height = stateBox.height + 2 * conf.padding;
-
- idCache[id] = stateInfo;
- stateCnt++;
- return stateInfo;
-};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
@@ -434,7 +61,7 @@ const drawState = function(elem, stateDef) {
export const draw = function(text, id) {
parser.yy.clear();
parser.parse(text);
- logger.info('Rendering diagram ' + text);
+ logger.warn('Rendering diagram ' + text);
// /// / Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id='${id}']`);
@@ -442,39 +69,152 @@ export const draw = function(text, id) {
// // Layout graph, Create a new directed graph
const graph = new graphlib.Graph({
- multigraph: false
+ multigraph: false,
+ compound: true,
+ // acyclicer: 'greedy',
+ rankdir: 'RL'
});
// // Set an object for the graph label
- graph.setGraph({
- isMultiGraph: false
- });
+ // graph.setGraph({
+ // isMultiGraph: false,
+ // rankdir: 'RL'
+ // });
// // Default to assigning a new object as a label for each new edge.
graph.setDefaultEdgeLabel(function() {
return {};
});
- const states = stateDb.getStates();
- const keys = Object.keys(states);
- total = keys.length;
- for (let i = 0; i < keys.length; i++) {
- const stateDef = states[keys[i]];
- const node = drawState(diagram, stateDef);
- // Add nodes to the graph. The first argument is the node id. The second is
- // metadata about the node. In this case we're going to add labels to each of
- // our nodes.
- graph.setNode(node.id, node);
- // logger.info('Org height: ' + node.height);
+ const rootDoc = stateDb.getRootDoc();
+ const n = renderDoc(rootDoc, diagram);
+
+ const bounds = diagram.node().getBBox();
+
+ diagram.attr('height', '100%');
+ diagram.attr('width', '100%');
+ diagram.attr('viewBox', '0 0 ' + bounds.width * 2 + ' ' + (bounds.height + 50));
+};
+const getLabelWidth = text => {
+ return text ? text.length * 5.02 : 1;
+};
+
+const renderDoc = (doc, diagram, parentId) => {
+ // // Layout graph, Create a new directed graph
+ const graph = new graphlib.Graph({
+ compound: true
+ });
+
+ // Set an object for the graph label
+ if (parentId)
+ graph.setGraph({
+ rankdir: 'LR',
+ // multigraph: false,
+ compound: true,
+ // acyclicer: 'greedy',
+ rankdir: 'LR',
+ ranker: 'tight-tree',
+ ranksep: '20'
+ // isMultiGraph: false
+ });
+ else {
+ graph.setGraph({
+ rankdir: 'TB',
+ compound: true,
+ // isCompound: true,
+ // acyclicer: 'greedy',
+ // ranker: 'longest-path'
+ ranker: 'tight-tree'
+ // ranker: 'network-simplex'
+ // isMultiGraph: false
+ });
}
+ // Default to assigning a new object as a label for each new edge.
+ graph.setDefaultEdgeLabel(function() {
+ return {};
+ });
+
+ stateDb.extract(doc);
+ const states = stateDb.getStates();
const relations = stateDb.getRelations();
+
+ const keys = Object.keys(states);
+
+ total = keys.length;
+ let first = true;
+ for (let i = 0; i < keys.length; i++) {
+ const stateDef = states[keys[i]];
+
+ let node;
+ if (stateDef.doc) {
+ let sub = diagram
+ .append('g')
+ .attr('id', stateDef.id)
+ .attr('class', 'classGroup');
+ node = renderDoc(stateDef.doc, sub, stateDef.id);
+
+ if (first) {
+ first = false;
+ sub = addIdAndBox(sub, stateDef);
+ let boxBounds = sub.node().getBBox();
+ node.width = boxBounds.width;
+ node.height = boxBounds.height + 10;
+ transformationLog[stateDef.id] = { y: 35 };
+ } else {
+ // sub = addIdAndBox(sub, stateDef);
+ let boxBounds = sub.node().getBBox();
+ node.width = boxBounds.width;
+ node.height = boxBounds.height;
+ // transformationLog[stateDef.id] = { y: 35 };
+ }
+ } else {
+ node = drawState(diagram, stateDef, graph);
+ }
+
+ if (stateDef.note) {
+ // Draw note note
+ const noteDef = {
+ descriptions: [],
+ id: stateDef.id + '-note',
+ note: stateDef.note,
+ type: 'note'
+ };
+ const note = drawState(diagram, noteDef, graph);
+
+ // graph.setNode(node.id, node);
+ if (stateDef.note.position === 'left of') {
+ graph.setNode(node.id + '-note', note);
+ graph.setNode(node.id, node);
+ } else {
+ graph.setNode(node.id, node);
+ graph.setNode(node.id + '-note', note);
+ }
+ // graph.setNode(node.id);
+ graph.setParent(node.id, node.id + '-group');
+ graph.setParent(node.id + '-note', node.id + '-group');
+ } else {
+ // Add nodes to the graph. The first argument is the node id. The second is
+ // metadata about the node. In this case we're going to add labels to each of
+ // our nodes.
+ graph.setNode(node.id, node);
+ }
+ }
+
+ logger.info('Count=', graph.nodeCount());
relations.forEach(function(relation) {
- graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), {
- relation: relation
+ graph.setEdge(relation.id1, relation.id2, {
+ relation: relation,
+ width: getLabelWidth(relation.title),
+ height: 16,
+ labelpos: 'c'
});
});
+
dagre.layout(graph);
+
+ logger.debug('Graph after layout', graph.nodes());
+
graph.nodes().forEach(function(v) {
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
logger.debug('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
@@ -483,11 +223,35 @@ export const draw = function(text, id) {
'translate(' +
(graph.node(v).x - graph.node(v).width / 2) +
',' +
- (graph.node(v).y - graph.node(v).height / 2) +
+ (graph.node(v).y +
+ (transformationLog[v] ? transformationLog[v].y : 0) -
+ graph.node(v).height / 2) +
' )'
);
+ d3.select('#' + v).attr('data-x-shift', graph.node(v).x - graph.node(v).width / 2);
+ const dividers = document.querySelectorAll('#' + v + ' .divider');
+ dividers.forEach(divider => {
+ const parent = divider.parentElement;
+ let pWidth = 0;
+ let pShift = 0;
+ if (parent) {
+ if (parent.parentElement) pWidth = parent.parentElement.getBBox().width;
+
+ pShift = parseInt(parent.getAttribute('data-x-shift'), 10);
+ if (Number.isNaN(pShift)) {
+ pShift = 0;
+ }
+ }
+ divider.setAttribute('x1', 0 - pShift);
+ divider.setAttribute('x2', pWidth - pShift);
+ });
+ } else {
+ logger.debug('No Node ' + v + ': ' + JSON.stringify(graph.node(v)));
}
});
+
+ let stateBox = diagram.node().getBBox();
+
graph.edges().forEach(function(e) {
if (typeof e !== 'undefined' && typeof graph.edge(e) !== 'undefined') {
logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
@@ -495,9 +259,19 @@ export const draw = function(text, id) {
}
});
- diagram.attr('height', '100%');
- diagram.attr('width', '100%');
- diagram.attr('viewBox', '0 0 ' + (graph.graph().width + 20) + ' ' + (graph.graph().height + 20));
+ stateBox = diagram.node().getBBox();
+ const stateInfo = {
+ id: parentId ? parentId : 'root',
+ label: parentId ? parentId : 'root',
+ width: 0,
+ height: 0
+ };
+
+ stateInfo.width = stateBox.width + 2 * conf.padding;
+ stateInfo.height = stateBox.height + 2 * conf.padding;
+
+ logger.info('Doc rendered', stateInfo, graph);
+ return stateInfo;
};
export default {