Merge branch 'feature/945_state_diagrams' into develop

This commit is contained in:
Knut Sveidqvist 2019-10-02 19:17:38 +02:00
commit 0abeaa4dc2
7 changed files with 1309 additions and 1 deletions

View File

@ -0,0 +1,57 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../../helpers/util';
describe('State diagram', () => {
it('should render a simple state diagrams', () => {
imgSnapshotTest(
`
stateDiagram
[*] --> State1
State1 --> [*]
`,
{ logLevel: 0 }
);
cy.get('svg');
});
it('should render a simple state diagrams', () => {
imgSnapshotTest(
`
stateDiagram
[*] --> State1
State1 --> State2
State1 --> State3
State1 --> [*]
`,
{ logLevel: 0 }
);
cy.get('svg');
});
it('should render a simple state diagrams with labels', () => {
imgSnapshotTest(
`
stateDiagram
[*] --> State1
State1 --> State2 : Transition 1
State1 --> State3 : Transition 2
State1 --> State4 : Transition 3
State1 --> State5 : Transition 4
State2 --> State3 : Transition 5
State1 --> [*]
`,
{ logLevel: 0 }
);
cy.get('svg');
});
it('should render state descriptions', () => {
imgSnapshotTest(
`
stateDiagram
state "Long state description" as XState1
state "Another Long state description" as XState2
XState2 : New line
`,
{ logLevel: 0 }
);
cy.get('svg');
});
});

View File

@ -0,0 +1,240 @@
/** mermaid
* https://mermaidjs.github.io/
* (c) 2014-2015 Knut Sveidqvist
* MIT license.
*
* Based on js sequence diagrams jison grammr
* http://bramp.github.io/js-sequence-diagrams/
* (c) 2012-2013 Andrew Brampton (bramp.net)
* Simplified BSD license.
*/
%lex
%options case-insensitive
// Special states for recognizing aliases
%x ID
%x STATE
%x FORK_STATE
%x STATE_STRING
%x STATE_ID
%x ALIAS
%x SCALE
%x NOTE
%x NOTE_ID
%x NOTE_TEXT
%x FLOATING_NOTE
%x FLOATING_NOTE_ID
%x struct
// A special state for grabbing text up to the first comment/newline
%x LINE
%%
[\n]+ return 'NL';
\s+ /* skip all whitespace */
<ID,STATE,struct,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,STATE,struct,LINE>\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */
"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>["] 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_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>\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';}}
<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>"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';}
"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>"-->" return '-->';
<struct>"--" return 'CONCURRENT';
<<EOF>> return 'NL';
. return 'INVALID';
/lex
%left '^'
%start start
%% /* language grammar */
start
: SPACE start
| NL start
| SD document { return $2; }
;
document
: /* empty */ { $$ = [] }
| document line {$1.push($2);$$ = $1}
;
line
: SPACE statement { console.log('here');$$ = $2 }
| statement {console.log('line', $1); $$ = $1 }
| 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());}
| 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);}
| STATE_DESCR AS ID STRUCT_START document STRUCT_STOP
| FORK
| JOIN
| CONCURRENT
| note notePosition ID NOTE_TEXT
| note NOTE_TEXT AS ID
;
idStatement
: ID {$$=$1;}
| EDGE_STATE {$$=$1;}
;
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,113 @@
import { logger } from '../../logger';
let relations = [];
let states = {};
let startCnt = 0;
let endCnt = 0;
/**
* Function called by parser when a node definition has been found.
* @param id
* @param text
* @param type
* @param style
*/
export const addState = function(id, type) {
if (typeof states[id] === 'undefined') {
states[id] = {
id: id,
descriptions: [],
type
};
}
};
export const clear = function() {
relations = [];
states = {};
};
export const getState = function(id) {
return states[id];
};
export const getStates = function() {
return states;
};
export const getRelations = function() {
// const relations1 = [{ id1: 'start1', id2: 'state1' }, { id1: 'state1', id2: 'exit1' }];
// return relations;
return relations;
};
export const addRelation = function(_id1, _id2, title) {
let id1 = _id1;
let id2 = _id2;
let type1 = 'default';
let type2 = 'default';
if (_id1 === '[*]') {
startCnt++;
id1 = 'start' + startCnt;
type1 = 'start';
}
if (_id2 === '[*]') {
endCnt++;
id2 = 'end' + startCnt;
type2 = 'end';
}
console.log(id1, id2, title);
addState(id1, type1);
addState(id2, type2);
relations.push({ id1, id2, title });
};
export const addDescription = function(id, _descr) {
const theState = states[id];
let descr = _descr;
if (descr[0] === ':') {
descr = descr.substr(1).trim();
}
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();
} else {
return label.trim();
}
};
export const lineType = {
LINE: 0,
DOTTED_LINE: 1
};
export const relationType = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3
};
export default {
addState,
clear,
getState,
getStates,
getRelations,
addRelation,
addDescription,
addMembers,
cleanupLabel,
lineType,
relationType
};

View File

@ -0,0 +1,328 @@
/* eslint-env jasmine */
import { parser } from './parser/stateDiagram';
import stateDb from './stateDb';
describe('state diagram, ', function() {
describe('when parsing an info graph it', function() {
beforeEach(function() {
parser.yy = stateDb;
});
fit('super simple', function() {
const str = `
stateDiagram
[*] --> State1
State1 --> [*]
`;
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
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function() {
const str = `stateDiagram\n
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('hide empty description', function() {
const str = `stateDiagram\n
hide empty description
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('scale', function() {
const str = `stateDiagram\n
scale 350 width
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('description after second state', function() {
const str = `stateDiagram\n
scale 350 width
[*] --> State1 : This is the description
State1 --> [*]
`;
parser.parse(str);
});
it('should handle state statements', function() {
const str = `stateDiagram\n
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved1
}
`;
parser.parse(str);
});
it('should handle recursive state definitions', function() {
const str = `stateDiagram\n
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 --> State2
}
}
`;
parser.parse(str);
});
it('should handle multiple recursive state definitions', function() {
const str = `stateDiagram\n
scale 350 width
[*] --> NotShooting
state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 --> State2
}
}
`;
parser.parse(str);
});
it('should handle state deifintions with separation of id', function() {
const str = `stateDiagram\n
state "Long state description" as state1
`;
parser.parse(str);
});
it('should handle state deifintions with separation of id', function() {
const str = `stateDiagram
state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
`;
parser.parse(str);
});
it('should State definition with quotes', function() {
const str = `stateDiagram\n
scale 600 width
[*] --> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
state "Accumulate Enough Data\nLong State Name" as long1
long1 : Just a test
[*] --> long1
long1 --> long1 : New Data
long1 --> ProcessData : Enough Data
}
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted
`;
parser.parse(str);
});
it('should handle fork statements', function() {
const str = `stateDiagram\n
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 --> [*]
`;
parser.parse(str);
});
it('should handle concurrent state', function() {
const str = `stateDiagram\n
[*] --> Active
state Active {
[*] --> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] --> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] --> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
`;
parser.parse(str);
});
it('should handle concurrent state', function() {
const str = `stateDiagram\n
[*] --> Active
state Active {
[*] --> NumLockOff
--
[*] --> CapsLockOff
--
[*] --> ScrollLockOff
}
`;
parser.parse(str);
});
// it('should handle arrow directions definitions', function() {
// const str = `stateDiagram\n
// [*] -up-> First
// First -right-> Second
// Second --> Third
// Third -left-> Last
// `;
// parser.parse(str);
// });
it('should handle note statements', function() {
const str = `stateDiagram\n
[*] --> Active
Active --> Inactive
note left of Active : this is a short<br/>note
note right of Inactive
A note can also
be defined on
several lines
end note
`;
parser.parse(str);
});
it('should handle floating notes', function() {
const str = `stateDiagram
foo: bar
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle floating notes', function() {
const str = `stateDiagram\n
state foo
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle notes for composit states', function() {
const str = `stateDiagram\n
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
note right of NotShooting : This is a note on a composite state
`;
parser.parse(str);
});
xit('should handle if statements', function() {
const str = `stateDiagram\n
[*] --> "Order Submitted"
if "Payment Accepted" then
-->[yes] "Pack products"
--> "Send parcel"
-right-> (*)
else
->[no] "Send error message"
-->[Cancel Order] [*]
endif
}
note right of NotShooting : This is a note on a composite state
`;
parser.parse(str);
});
});
});

View File

@ -0,0 +1,506 @@
import * as d3 from 'd3';
import dagre from 'dagre-layout';
import graphlib from 'graphlibrary';
import { logger } from '../../logger';
import stateDb from './stateDb';
import { parser } from './parser/stateDiagram';
import utils from '../../utils';
parser.yy = stateDb;
const idCache = {};
let stateCnt = 0;
let total = 0;
let edgeCount = 0;
const conf = {
dividerMargin: 10,
padding: 5,
textHeight: 10
};
export const setConf = function(cnf) {};
// Todo optimize
const getGraphId = function(label) {
const keys = Object.keys(idCache);
for (let i = 0; i < keys.length; i++) {
if (idCache[keys[i]].label === label) {
return keys[i];
}
}
return undefined;
};
/**
* 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')
.attr('id', 'dependencyEnd')
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', 'auto')
.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.
* @param text
* @param id
*/
export const draw = function(text, id) {
parser.yy.clear();
parser.parse(text);
logger.info('Rendering diagram ' + text);
// /// / Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id='${id}']`);
insertMarkers(diagram);
// // Layout graph, Create a new directed graph
const graph = new graphlib.Graph({
multigraph: false
});
// // Set an object for the graph label
graph.setGraph({
isMultiGraph: false
});
// // 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 relations = stateDb.getRelations();
relations.forEach(function(relation) {
graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), {
relation: relation
});
});
dagre.layout(graph);
graph.nodes().forEach(function(v) {
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
logger.debug('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
d3.select('#' + v).attr(
'transform',
'translate(' +
(graph.node(v).x - graph.node(v).width / 2) +
',' +
(graph.node(v).y - graph.node(v).height / 2) +
' )'
);
}
});
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)));
drawEdge(diagram, graph.edge(e), graph.edge(e).relation);
}
});
diagram.attr('height', '100%');
diagram.attr('width', '100%');
diagram.attr('viewBox', '0 0 ' + (graph.graph().width + 20) + ' ' + (graph.graph().height + 20));
};
export default {
setConf,
draw
};

View File

@ -28,6 +28,9 @@ import ganttDb from './diagrams/gantt/ganttDb';
import classRenderer from './diagrams/class/classRenderer';
import classParser from './diagrams/class/parser/classDiagram';
import classDb from './diagrams/class/classDb';
import stateRenderer from './diagrams/state/stateRenderer';
import stateParser from './diagrams/state/parser/stateDiagram';
import stateDb from './diagrams/state/stateDb';
import gitGraphRenderer from './diagrams/git/gitGraphRenderer';
import gitGraphParser from './diagrams/git/parser/gitGraph';
import gitGraphAst from './diagrams/git/gitGraphAst';
@ -332,6 +335,10 @@ function parse(text) {
parser = classParser;
parser.parser.yy = classDb;
break;
case 'state':
parser = stateParser;
parser.parser.yy = stateDb;
break;
case 'info':
logger.debug('info info info');
console.warn('In API', pkg.version);
@ -522,6 +529,11 @@ const render = function(id, txt, cb, container) {
classRenderer.setConf(config.class);
classRenderer.draw(txt, id);
break;
case 'state':
// config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
stateRenderer.setConf(config.state);
stateRenderer.draw(txt, id);
break;
case 'info':
config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
infoRenderer.setConf(config.class);

View File

@ -33,6 +33,10 @@ export const detectType = function(text) {
return 'class';
}
if (text.match(/^\s*stateDiagram/)) {
return 'state';
}
if (text.match(/^\s*gitGraph/)) {
return 'git';
}
@ -69,8 +73,56 @@ export const interpolateToCurve = (interpolate, defaultCurve) => {
return d3[curveName] || defaultCurve;
};
const distance = (p1, p2) =>
p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0;
const traverseEdge = points => {
let prevPoint;
let totalDistance = 0;
points.forEach(point => {
totalDistance += distance(point, prevPoint);
prevPoint = point;
});
// Traverse half of total distance along points
const distanceToLabel = totalDistance / 2;
let remainingDistance = distanceToLabel;
let center;
prevPoint = undefined;
points.forEach(point => {
if (prevPoint && !center) {
const vectorDistance = distance(point, prevPoint);
if (vectorDistance < remainingDistance) {
remainingDistance -= vectorDistance;
} else {
// The point is remainingDistance from prevPoint in the vector between prevPoint and point
// Calculate the coordinates
const distanceRatio = remainingDistance / vectorDistance;
if (distanceRatio <= 0) center = prevPoint;
if (distanceRatio >= 1) center = { x: point.x, y: point.y };
if (distanceRatio > 0 && distanceRatio < 1) {
center = {
x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x,
y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y
};
}
}
}
prevPoint = point;
});
return center;
};
const calcLabelPosition = points => {
const p = traverseEdge(points);
return p;
};
export default {
detectType,
isSubstringInArray,
interpolateToCurve
interpolateToCurve,
calcLabelPosition
};