Merge branch 'feature/945_state_diagrams'

This commit is contained in:
Knut Sveidqvist 2019-10-09 18:16:32 +02:00
commit fe1e09f06b
8 changed files with 888 additions and 593 deletions

View File

@ -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 +,-<br/>
`,
{ 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 &lt;&lt;fork&gt;&gt;
[*] --> fork_state
fork_state --> State2
fork_state --> State3
state join_state &lt;&lt;join&gt;&gt;
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');
});
});

View File

@ -1,7 +1,7 @@
<html>
<head>
<script src="/e2e.js"></script>
<link
<lnk
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
rel="stylesheet"
/>

View File

@ -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
};

View File

@ -38,41 +38,44 @@
<INITIAL,ID,STATE,struct,LINE>\#[^\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'; }
<SCALE>\d+ return 'WIDTH';
<SCALE>\s+"width" {this.popState();}
<INITIAL,struct>"state"\s+ { this.pushState('STATE'); }
<STATE>.*"<<fork>>" {this.popState();console.log('Fork: ',yytext);return 'FORK';}
<STATE>.*"<<join>>" {this.popState();console.log('Join: ',yytext);return 'JOIN';}
<STATE>.*"<<fork>>" {this.popState();yytext=yytext.slice(0,-8).trim(); console.warn('Fork Fork: ',yytext);return 'FORK';}
<STATE>.*"<<join>>" {this.popState();yytext=yytext.slice(0,-8).trim();console.warn('Fork Join: ',yytext);return 'JOIN';}
<STATE>.*"[[fork]]" {this.popState();yytext=yytext.slice(0,-8).trim();console.warn('Fork Fork: ',yytext);return 'FORK';}
<STATE>.*"[[join]]" {this.popState();yytext=yytext.slice(0,-8).trim();console.warn('Fork Join: ',yytext);return 'JOIN';}
<STATE>["] this.begin("STATE_STRING");
<STATE>"as"\s* {this.popState();this.pushState('STATE_ID');return "AS";}
<STATE_ID>[^\n\{]* {this.popState();console.log('STATE_ID', yytext);return "ID";}
<STATE_ID>[^\n\{]* {this.popState();/* console.log('STATE_ID', yytext);*/return "ID";}
<STATE_STRING>["] this.popState();
<STATE_STRING>[^"]* { console.log('Long description:', yytext);return "STATE_DESCR";}
<STATE>[^\n\s\{]+ {console.log('COMPOSIT_STATE', yytext);return 'COMPOSIT_STATE';}
<STATE_STRING>[^"]* { /*console.log('Long description:', yytext);*/return "STATE_DESCR";}
<STATE>[^\n\s\{]+ {/*console.log('COMPOSIT_STATE', yytext);*/return 'COMPOSIT_STATE';}
<STATE>\n {this.popState();}
<INITIAL,STATE>\{ {this.popState();this.pushState('struct'); console.log('begin struct', yytext);return 'STRUCT_START';}
<struct>\} { console.log('Ending struct'); this.popState(); return 'STRUCT_STOP';}}
<INITIAL,STATE>\{ {this.popState();this.pushState('struct'); /*console.log('begin struct', yytext);*/return 'STRUCT_START';}
<struct>\} { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';}}
<struct>[\n] /* nothing */
<INITIAL,struct>"note"\s+ { this.begin('NOTE'); return 'note'; }
<NOTE>"left of" { this.popState();this.pushState('NOTE_ID');console.log('Got dir');return 'left_of';}
<NOTE>"left of" { this.popState();this.pushState('NOTE_ID');return 'left_of';}
<NOTE>"right of" { this.popState();this.pushState('NOTE_ID');return 'right_of';}
<NOTE>\" { this.popState();this.pushState('FLOATING_NOTE');}
<FLOATING_NOTE>\s*"as"\s* {this.popState();this.pushState('FLOATING_NOTE_ID');return "AS";}
<FLOATING_NOTE>["] /**/
<FLOATING_NOTE>[^"]* { console.log('Floating note text: ', yytext);return "NOTE_TEXT";}
<FLOATING_NOTE_ID>[^\n]* {this.popState();console.log('Floating note ID', yytext);return "ID";}
<NOTE_ID>\s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');console.log('Got ID for note', yytext);return 'ID';}
<NOTE_TEXT>\s*":"[^\+\-:\n,;]+ { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';}
<NOTE_TEXT>\s*[^\+\-:,;]+"end note" { this.popState();console.log('Got NOTE_TEXT for note',yytext);return 'NOTE_TEXT';}
<FLOATING_NOTE>[^"]* { /*console.log('Floating note text: ', yytext);*/return "NOTE_TEXT";}
<FLOATING_NOTE_ID>[^\n]* {this.popState();/*console.log('Floating note ID', yytext);*/return "ID";}
<NOTE_ID>\s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');/*console.log('Got ID for note', yytext);*/return 'ID';}
<NOTE_TEXT>\s*":"[^:\n;]+ { this.popState();/*console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.substr(2).trim();return 'NOTE_TEXT';}
<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'; }
<INITIAL,struct>"[*]" { console.log('EDGE_STATE=',yytext); return 'EDGE_STATE';}
<INITIAL,struct>[^:\n\s\-\{]+ { console.log('=>ID=',yytext); return 'ID';}
<INITIAL,struct>\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'; }
<INITIAL,struct>"[*]" { /*console.log('EDGE_STATE=',yytext);*/ return 'EDGE_STATE';}
<INITIAL,struct>[^:\n\s\-\{]+ { /*console.log('=>ID=',yytext);*/ return 'ID';}
// <INITIAL,struct>\s*":"[^\+\->:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
<INITIAL,struct>\s*":"[^:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
<INITIAL,struct>"-->" return '-->';
<struct>"--" return 'CONCURRENT';
<<EOF>> 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");} ;
%%

View File

@ -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(/<br\/?>/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, '<br/>');
text = text.replace(/\n/g, '<br/>');
const lines = text.split(/<br\/?>/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++;
};

View File

@ -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
};

View File

@ -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 --> [*]
`;

View File

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