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 {