From f9f8785aefe9dde712900bf0d1d332835cdda633 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 2 Oct 2019 19:32:13 +0200 Subject: [PATCH 01/12] #945 Recursive object from parsing and stateDb --- .../rendering/stateDiagram.spec.js | 17 ++ src/diagrams/state/parser/stateDiagram.jison | 147 ++++-------------- src/diagrams/state/stateDb.js | 56 ++++--- src/diagrams/state/stateRenderer.js | 113 +++----------- 4 files changed, 107 insertions(+), 226 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 6ae7cbe4e..78ea61cfe 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -49,6 +49,23 @@ 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 + + state NotShooting { + [*] --> Idle + Idle --> Configuring : EvConfig + Configuring --> Idle : EvConfig + } `, { logLevel: 0 } ); diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index c54ccd357..d23193dbb 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -89,30 +89,52 @@ start : SPACE start | NL start - | SD document { return $2; } + | SD document { console.warn('Root document', $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 { console.warn('here');$$ = $2 } + | statement {console.warn('line', $1); $$ = $1 } + | NL { console.warn('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 { $$={ 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 ', $3); + // yy.addDocument('noId'); + $$={ stmt: 'state', id: 'noId', 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 + { + //console.warn('Adding document for state with id ', $3, $4); yy.addDocument($3); + $$={ stmt: 'state', id: $3, type: 'default', description: $1, doc: $5 } + } | FORK | JOIN | CONCURRENT @@ -129,112 +151,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/stateDb.js b/src/diagrams/state/stateDb.js index 5a24f5680..aff96b909 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -1,7 +1,18 @@ import { logger } from '../../logger'; -let relations = []; -let states = {}; +const newDoc = () => { + return { + relations: [], + states: {}, + documents: {} + }; +}; + +let documents = { + root: newDoc() +}; + +let currentDocument = documents.root; let startCnt = 0; let endCnt = 0; @@ -14,8 +25,9 @@ let endCnt = 0; * @param style */ export const addState = function(id, type) { - if (typeof states[id] === 'undefined') { - states[id] = { + console.warn('Add state', id); + if (typeof currentDocument.states[id] === 'undefined') { + currentDocument.states[id] = { id: id, descriptions: [], type @@ -24,21 +36,30 @@ export const addState = function(id, type) { }; export const clear = function() { - relations = []; - states = {}; + documents = { + root: newDoc() + }; }; export const getState = function(id) { - return states[id]; + return currentDocument.states[id]; +}; +export const addDocument = id => { + console.warn(currentDocument, documents); + currentDocument.documents[id] = newDoc(); + currentDocument.documents[id].parent = currentDocument; + currentDocument = currentDocument.documents[id]; }; export const getStates = function() { - return states; + return currentDocument.states; +}; +export const logDocuments = function() { + console.warn('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) { @@ -59,11 +80,11 @@ export const addRelation = function(_id1, _id2, title) { 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 theState = currentDocument.states[id]; let descr = _descr; if (descr[0] === ':') { descr = descr.substr(1).trim(); @@ -72,12 +93,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(); @@ -106,8 +121,9 @@ export default { getRelations, addRelation, addDescription, - addMembers, cleanupLabel, lineType, - relationType + relationType, + logDocuments, + addDocument }; diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 800465845..eb58c034d 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -39,94 +39,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') @@ -434,6 +346,7 @@ const drawState = function(elem, stateDef) { export const draw = function(text, id) { parser.yy.clear(); parser.parse(text); + stateDb.logDocuments(); logger.info('Rendering diagram ' + text); // /// / Fetch the default direction, use TD if none was found @@ -442,10 +355,11 @@ export const draw = function(text, id) { // // Layout graph, Create a new directed graph const graph = new graphlib.Graph({ - multigraph: false + multigraph: false, + compound: true }); - // // Set an object for the graph label + // Set an object for the graph label graph.setGraph({ isMultiGraph: false }); @@ -457,22 +371,41 @@ export const draw = function(text, id) { 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); + // const nodeAppendix = drawStartState(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); + // graph.setNode(node.id + 'note', nodeAppendix); + + // let parent = 'p1'; + // if (node.id === 'XState1') { + // parent = 'p2'; + // } + + // graph.setParent(node.id, parent); + // graph.setParent(node.id + 'note', parent); + // logger.info('Org height: ' + node.height); } + console.info('Count=', graph.nodeCount()); const relations = stateDb.getRelations(); relations.forEach(function(relation) { graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { + relation: relation, + width: 38 + }); + console.warn(getGraphId(relation.id1), getGraphId(relation.id2), { relation: relation }); + // graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2)); }); dagre.layout(graph); graph.nodes().forEach(function(v) { From 7865fd4f028b09ec53c4df8a643c82aefff9d901 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Thu, 3 Oct 2019 19:08:15 +0200 Subject: [PATCH 02/12] #945 Rendering of composite state in a box --- .../rendering/stateDiagram.spec.js | 9 +- src/diagrams/state/parser/stateDiagram.jison | 14 +- src/diagrams/state/stateDb.js | 46 +++- src/diagrams/state/stateRenderer.js | 250 +++++++++++++++--- 4 files changed, 274 insertions(+), 45 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 78ea61cfe..bbaf1a2d8 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -59,12 +59,15 @@ describe('State diagram', () => { imgSnapshotTest( ` stateDiagram - [*] --> NotShooting + [*] --> NotShooting: Pacifist + NotShooting --> A + NotShooting --> B + NotShooting --> C state NotShooting { - [*] --> Idle + [*] --> Idle: Yet another long long öong öong öong label Idle --> Configuring : EvConfig - Configuring --> Idle : EvConfig + Configuring --> Idle : EvConfig EvConfig EvConfig EvConfig EvConfig } `, { logLevel: 0 } diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index d23193dbb..8ae1bbf44 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -89,7 +89,7 @@ start : SPACE start | NL start - | SD document { console.warn('Root document', $2); return $2; } + | SD document { console.warn('Root document', $2); yy.setRootDoc($2);return $2; } ; document @@ -98,14 +98,14 @@ document if($2!='nl'){ $1.push($2);$$ = $1 } - console.warn('Got document',$1, $2); + // console.warn('Got document',$1, $2); } ; line - : SPACE statement { console.warn('here');$$ = $2 } - | statement {console.warn('line', $1); $$ = $1 } - | NL { console.warn('NL'); $$='nl';} + : SPACE statement { $$ = $2 } + | statement { $$ = $1 } + | NL { $$='nl';} ; statement @@ -125,9 +125,9 @@ statement | COMPOSIT_STATE | COMPOSIT_STATE STRUCT_START document STRUCT_STOP { - console.warn('Adding document for state without id ', $3); + console.warn('Adding document for state without id ', $1); // yy.addDocument('noId'); - $$={ stmt: 'state', id: 'noId', type: 'default', description: '', doc: $3 } + $$={ 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 diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index aff96b909..76bae029c 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -1,5 +1,33 @@ import { logger } from '../../logger'; +let rootDoc = []; +const setRootDoc = o => { + console.warn('Setting root doc', o); + rootDoc = o; +}; + +const getRootDoc = () => rootDoc; + +const extract = doc => { + const res = { states: [], relations: [] }; + clear(); + doc.forEach(item => { + if (item.stmt === 'state') { + // if (item.doc) { + // addState(item.id, 'composit'); + // addDocument(item.id); + // extract(item.doc); + // currentDocument = currentDocument.parent; + // } else { + addState(item.id, item.type, item.doc); + // } + } + if (item.stmt === 'relation') { + addRelation(item.state1.id, item.state2.id, item.description); + } + }); +}; + const newDoc = () => { return { relations: [], @@ -24,14 +52,22 @@ let endCnt = 0; * @param type * @param style */ -export const addState = function(id, type) { +export const addState = function(id, type, doc) { console.warn('Add state', id); if (typeof currentDocument.states[id] === 'undefined') { currentDocument.states[id] = { id: id, descriptions: [], - type + type, + doc }; + } else { + if (!currentDocument.states[id].doc) { + currentDocument.states[id].doc = doc; + } + if (!currentDocument.states[id].type) { + currentDocument.states[id].type = type; + } } }; @@ -39,6 +75,7 @@ export const clear = function() { documents = { root: newDoc() }; + currentDocument = documents.root; }; export const getState = function(id) { @@ -125,5 +162,8 @@ export default { lineType, relationType, logDocuments, - addDocument + addDocument, + getRootDoc, + setRootDoc, + extract }; diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index eb58c034d..e4b76113c 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -20,6 +20,8 @@ const conf = { textHeight: 10 }; +const transformationLog = {}; + export const setConf = function(cnf) {}; // Todo optimize @@ -191,6 +193,60 @@ const drawDescrState = (g, stateDef) => { return g; }; +const addIdAndBox = (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', -5) + .attr('font-size', 24) + .attr('class', 'state-title') + .text(stateDef.id); + + const titleHeight = title.node().getBBox().height; + + // let isFirst = true; + // stateDef.descriptions.forEach(function(descr) { + // addTspan(description, descr, isFirst); + // isFirst = false; + // }); + + const lineY = 3; + 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 descrBox = description.node().getBBox(); + const graphBox = g.node().getBBox(); + title.attr('x', graphBox.width / 2 - title.node().getBBox().width / 2); + descrLine.attr('x2', graphBox.width); + // const classBox = title.node().getBBox(); + console.warn('Box', graphBox, stateDef); + g.insert('rect', ':first-child') + .attr('x', graphBox.x) + .attr('y', -5 - conf.textHeight - conf.padding) + .attr('width', graphBox.width) + .attr('height', graphBox.height + 5 + conf.textHeight + conf.padding) + .attr('rx', '5'); + // 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') @@ -268,10 +324,8 @@ const drawEdge = function(elem, path, relation) { .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') @@ -279,7 +333,6 @@ const drawEdge = function(elem, path, relation) { .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') @@ -289,7 +342,6 @@ const drawEdge = function(elem, path, relation) { // .attr('cx', point.x) // .attr('cy', point.y); // }); - // g.append('circle') // .style('stroke', 'blue') // .style('fill', 'blue') @@ -306,8 +358,8 @@ const drawEdge = function(elem, path, relation) { * @param {*} elem * @param {*} stateDef */ -const drawState = function(elem, stateDef) { - // logger.info('Rendering class ' + stateDef); +const drawState = function(elem, stateDef, graph, doc) { + console.warn('Rendering class ', stateDef); const id = stateDef.id; const stateInfo = { @@ -327,9 +379,13 @@ const drawState = function(elem, stateDef) { if (stateDef.type === 'default' && stateDef.descriptions.length === 0) drawSimpleState(g, stateDef); if (stateDef.type === 'default' && stateDef.descriptions.length > 0) drawDescrState(g, stateDef); + // if (stateDef.type === 'default' && stateDef.doc) { + // // renderDoc(stateDef.doc, graph, elem); + // drawSimpleState(g, stateDef); + // renderDoc(stateDef.doc, graph, g, id); + // } const stateBox = g.node().getBBox(); - stateInfo.width = stateBox.width + 2 * conf.padding; stateInfo.height = stateBox.height + 2 * conf.padding; @@ -369,19 +425,75 @@ export const draw = function(text, id) { return {}; }); + const rootDoc = stateDb.getRootDoc(); + const n = renderDoc2(rootDoc, diagram); + + console.warn(graph, graph.graph().getBBox); + + diagram.attr('height', '100%'); + diagram.attr('width', '100%'); + diagram.attr('viewBox', '0 0 ' + 400 + ' ' + 600); +}; +const getLabelWidth = text => { + return text ? text.length * 5.02 : 1; +}; + +const renderDoc2 = (doc, diagram, parentId) => { + // // Layout graph, Create a new directed graph + const graph = new graphlib.Graph({ + multigraph: false, + compound: 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 {}; + }); + + stateDb.extract(doc); const states = stateDb.getStates(); + const relations = stateDb.getRelations(); + const keys = Object.keys(states); + console.warn('rendering doc 2', states, relations); total = keys.length; for (let i = 0; i < keys.length; i++) { const stateDef = states[keys[i]]; - const node = drawState(diagram, stateDef); + console.warn('keys[i]', keys[i]); + let node; + if (stateDef.doc) { + let sub = diagram + .append('g') + .attr('id', stateDef.id) + .attr('class', 'classGroup'); + node = renderDoc2(stateDef.doc, sub, stateDef.id); + + sub = addIdAndBox(sub, stateDef); + let boxBounds = sub.node().getBBox(); + node.width = boxBounds.width; + node.height = boxBounds.height; + transformationLog[stateDef.id] = { y: 20 }; + // node.x = boxBounds.y; + // node.y = boxBounds.x; + } else { + node = drawState(diagram, stateDef, graph); + } // const nodeAppendix = drawStartState(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); + // if (parentId) { + // console.warn('apa1 P>', node.id, parentId); + // // graph.setParent(node.id, parentId); + // } // graph.setNode(node.id + 'note', nodeAppendix); // let parent = 'p1'; @@ -396,8 +508,104 @@ export const draw = function(text, id) { } console.info('Count=', graph.nodeCount()); - const relations = stateDb.getRelations(); relations.forEach(function(relation) { + console.warn('Rendering edge', relation); + graph.setEdge(relation.id1, relation.id2, { + relation: relation, + width: getLabelWidth(relation.title), + height: 16, + labelpos: 'c' + }); + console.warn(getGraphId(relation.id1), relation.id2, { + relation: relation + }); + // graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2)); + }); + + dagre.layout(graph); + + graph.nodes().forEach(function(v) { + if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { + console.warn('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 + + (transformationLog[v] ? transformationLog[v].y : 0) - + graph.node(v).height / 2) + + ' )' + ); + } + }); + let stateBox = diagram.node().getBBox(); + console.warn('Node before labels ', stateBox.width); + + 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); + } + }); + + stateBox = diagram.node().getBBox(); + console.warn('Node after labels ', stateBox.width); + 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; + + console.warn('Doc rendered', stateInfo, graph); + return stateInfo; +}; +const renderDoc = (doc, graph, diagram, parentId) => { + stateDb.extract(doc); + const states = stateDb.getStates(); + const relations = stateDb.getRelations(); + + const keys = Object.keys(states); + console.warn('rendering doc', states, relations); + + total = keys.length; + for (let i = 0; i < keys.length; i++) { + const stateDef = states[keys[i]]; + console.warn('keys[i]', keys[i]); + if (stateDef.doc) { + renderDoc(stateDef.doc, graph, diagram, stateDef.id); + } + const node = drawState(diagram, stateDef, graph); + // const nodeAppendix = drawStartState(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); + if (parentId) { + console.warn('Setting parent', parentId); + graph.setParent(node.id, parentId); + } + // graph.setNode(node.id + 'note', nodeAppendix); + + // let parent = 'p1'; + // if (node.id === 'XState1') { + // parent = 'p2'; + // } + + // graph.setParent(node.id, parent); + // graph.setParent(node.id + 'note', parent); + + // logger.info('Org height: ' + node.height); + } + + console.info('Count=', graph.nodeCount()); + relations.forEach(function(relation) { + console.warn('Rendering edge', relation); graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { relation: relation, width: 38 @@ -407,30 +615,8 @@ export const draw = function(text, id) { }); // graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2)); }); - 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)); + console.warn('Doc rendered'); }; export default { From 65cbfbdb401142330456f44e8d451ad258b6f3e1 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Thu, 3 Oct 2019 19:54:07 +0200 Subject: [PATCH 03/12] #945 Tweaking --- .../rendering/stateDiagram.spec.js | 28 +++++++++ src/diagrams/state/stateRenderer.js | 59 ++++++++++++------- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index bbaf1a2d8..84e4b3252 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -74,4 +74,32 @@ describe('State diagram', () => { ); 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 + } + state Console { + [*] --> Off2: Off to start with + On2--> Off2 : Turn off + Off2 --> On2 : Turn on + On2-->Playing + + state Playing { + Alive --> Dead + Dead-->Alive + } + } + TV--> Console + Console --> TV + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); }); diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index e4b76113c..4b4fed83a 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -206,7 +206,7 @@ const addIdAndBox = (g, stateDef) => { const title = g .append('text') .attr('x', 2 * conf.padding) - .attr('y', -5) + .attr('y', -15) .attr('font-size', 24) .attr('class', 'state-title') .text(stateDef.id); @@ -219,7 +219,7 @@ const addIdAndBox = (g, stateDef) => { // isFirst = false; // }); - const lineY = 3; + const lineY = -9; const descrLine = g .append('line') // text label for the x axis .attr('x1', 0) @@ -229,14 +229,14 @@ const addIdAndBox = (g, stateDef) => { // const descrBox = description.node().getBBox(); const graphBox = g.node().getBBox(); title.attr('x', graphBox.width / 2 - title.node().getBBox().width / 2); - descrLine.attr('x2', graphBox.width); + descrLine.attr('x2', graphBox.width + conf.padding); // const classBox = title.node().getBBox(); console.warn('Box', graphBox, stateDef); g.insert('rect', ':first-child') .attr('x', graphBox.x) - .attr('y', -5 - conf.textHeight - conf.padding) - .attr('width', graphBox.width) - .attr('height', graphBox.height + 5 + conf.textHeight + conf.padding) + .attr('y', -15 - conf.textHeight - conf.padding) + .attr('width', graphBox.width + conf.padding) + .attr('height', graphBox.height + 3 + conf.textHeight) .attr('rx', '5'); // g.insert('rect', ':first-child') // .attr('x', conf.padding) @@ -412,13 +412,16 @@ export const draw = function(text, id) { // // Layout graph, Create a new directed graph const graph = new graphlib.Graph({ multigraph: false, - compound: true + // compound: true, + // acyclicer: 'greedy', + rankdir: 'RL' }); - // Set an object for the graph label - graph.setGraph({ - isMultiGraph: false - }); + // // Set an object for the graph label + // graph.setGraph({ + // isMultiGraph: false, + // rankdir: 'RL' + // }); // // Default to assigning a new object as a label for each new edge. graph.setDefaultEdgeLabel(function() { @@ -428,11 +431,11 @@ export const draw = function(text, id) { const rootDoc = stateDb.getRootDoc(); const n = renderDoc2(rootDoc, diagram); - console.warn(graph, graph.graph().getBBox); + const bounds = diagram.node().getBBox(); diagram.attr('height', '100%'); diagram.attr('width', '100%'); - diagram.attr('viewBox', '0 0 ' + 400 + ' ' + 600); + diagram.attr('viewBox', '0 0 ' + bounds.width + ' ' + (bounds.height + 50)); }; const getLabelWidth = text => { return text ? text.length * 5.02 : 1; @@ -440,15 +443,27 @@ const getLabelWidth = text => { const renderDoc2 = (doc, diagram, parentId) => { // // Layout graph, Create a new directed graph - const graph = new graphlib.Graph({ - multigraph: false, - compound: false - }); + const graph = new graphlib.Graph({}); // Set an object for the graph label - graph.setGraph({ - isMultiGraph: false - }); + if (parentId) + graph.setGraph({ + rankdir: 'LR', + multigraph: false, + compound: false, + // acyclicer: 'greedy', + rankdir: 'LR', + ranker: 'tight-tree' + // isMultiGraph: false + }); + else { + graph.setGraph({ + rankdir: 'TB', + // acyclicer: 'greedy' + ranker: 'longest-path' + // isMultiGraph: false + }); + } // // Default to assigning a new object as a label for each new edge. graph.setDefaultEdgeLabel(function() { @@ -477,8 +492,8 @@ const renderDoc2 = (doc, diagram, parentId) => { sub = addIdAndBox(sub, stateDef); let boxBounds = sub.node().getBBox(); node.width = boxBounds.width; - node.height = boxBounds.height; - transformationLog[stateDef.id] = { y: 20 }; + node.height = boxBounds.height + 10; + transformationLog[stateDef.id] = { y: 35 }; // node.x = boxBounds.y; // node.y = boxBounds.x; } else { From d4306e61c2fdad2f0fbf8a35e3de2fcf8935c9c4 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sat, 5 Oct 2019 09:02:20 +0200 Subject: [PATCH 04/12] #945 Some cleanup --- .../rendering/stateDiagram.spec.js | 6 +- cypress/platform/e2e.html | 2 +- src/diagrams/state/id-cache.js | 16 + src/diagrams/state/shapes.js | 295 +++++++++++++ src/diagrams/state/stateDiagram.spec.js | 21 - src/diagrams/state/stateRenderer.js | 410 +----------------- 6 files changed, 323 insertions(+), 427 deletions(-) create mode 100644 src/diagrams/state/id-cache.js create mode 100644 src/diagrams/state/shapes.js diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 84e4b3252..f1fc7cea8 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -79,11 +79,15 @@ describe('State diagram', () => { ` 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 @@ -95,8 +99,6 @@ describe('State diagram', () => { Dead-->Alive } } - TV--> Console - Console --> TV `, { logLevel: 0 } ); 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/shapes.js b/src/diagrams/state/shapes.js new file mode 100644 index 000000000..c7a08d7ae --- /dev/null +++ b/src/diagrams/state/shapes.js @@ -0,0 +1,295 @@ +import * as d3 from 'd3'; +import idCache from './id-cache.js'; +import stateDb from './stateDb'; + +console.warn('ID cache', idCache); + +// TODO Move conf object to main conf in mermaidAPI +const conf = { + dividerMargin: 10, + padding: 5, + textHeight: 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 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); +}; + +/** + * 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) { + console.warn('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.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/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index d8918edd7..d3e1bf93c 100644 --- a/src/diagrams/state/stateDiagram.spec.js +++ b/src/diagrams/state/stateDiagram.spec.js @@ -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 diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 4b4fed83a..83c394fe3 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -5,15 +5,14 @@ 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 } 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, @@ -26,10 +25,10 @@ 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]; } } @@ -53,346 +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 addIdAndBox = (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', -15) - .attr('font-size', 24) - .attr('class', 'state-title') - .text(stateDef.id); - - const titleHeight = title.node().getBBox().height; - - // let isFirst = true; - // stateDef.descriptions.forEach(function(descr) { - // addTspan(description, descr, isFirst); - // isFirst = false; - // }); - - 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 descrBox = description.node().getBBox(); - const graphBox = g.node().getBBox(); - title.attr('x', graphBox.width / 2 - title.node().getBBox().width / 2); - descrLine.attr('x2', graphBox.width + conf.padding); - // const classBox = title.node().getBBox(); - console.warn('Box', graphBox, stateDef); - 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'); - // 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, graph, doc) { - console.warn('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); - // if (stateDef.type === 'default' && stateDef.doc) { - // // renderDoc(stateDef.doc, graph, elem); - // drawSimpleState(g, stateDef); - // renderDoc(stateDef.doc, graph, g, id); - // } - - 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. @@ -429,7 +88,7 @@ export const draw = function(text, id) { }); const rootDoc = stateDb.getRootDoc(); - const n = renderDoc2(rootDoc, diagram); + const n = renderDoc(rootDoc, diagram); const bounds = diagram.node().getBBox(); @@ -441,7 +100,7 @@ const getLabelWidth = text => { return text ? text.length * 5.02 : 1; }; -const renderDoc2 = (doc, diagram, parentId) => { +const renderDoc = (doc, diagram, parentId) => { // // Layout graph, Create a new directed graph const graph = new graphlib.Graph({}); @@ -499,7 +158,6 @@ const renderDoc2 = (doc, diagram, parentId) => { } else { node = drawState(diagram, stateDef, graph); } - // const nodeAppendix = drawStartState(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 @@ -579,60 +237,6 @@ const renderDoc2 = (doc, diagram, parentId) => { console.warn('Doc rendered', stateInfo, graph); return stateInfo; }; -const renderDoc = (doc, graph, diagram, parentId) => { - stateDb.extract(doc); - const states = stateDb.getStates(); - const relations = stateDb.getRelations(); - - const keys = Object.keys(states); - console.warn('rendering doc', states, relations); - - total = keys.length; - for (let i = 0; i < keys.length; i++) { - const stateDef = states[keys[i]]; - console.warn('keys[i]', keys[i]); - if (stateDef.doc) { - renderDoc(stateDef.doc, graph, diagram, stateDef.id); - } - const node = drawState(diagram, stateDef, graph); - // const nodeAppendix = drawStartState(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); - if (parentId) { - console.warn('Setting parent', parentId); - graph.setParent(node.id, parentId); - } - // graph.setNode(node.id + 'note', nodeAppendix); - - // let parent = 'p1'; - // if (node.id === 'XState1') { - // parent = 'p2'; - // } - - // graph.setParent(node.id, parent); - // graph.setParent(node.id + 'note', parent); - - // logger.info('Org height: ' + node.height); - } - - console.info('Count=', graph.nodeCount()); - relations.forEach(function(relation) { - console.warn('Rendering edge', relation); - graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { - relation: relation, - width: 38 - }); - console.warn(getGraphId(relation.id1), getGraphId(relation.id2), { - relation: relation - }); - // graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2)); - }); - - console.warn('Doc rendered'); -}; export default { setConf, From cfc14ade2ae448140cb2d4f9b447d797cfb9476a Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sat, 5 Oct 2019 10:02:58 +0200 Subject: [PATCH 05/12] #945 Some more cleanup focusing on stateDb --- src/diagrams/state/parser/stateDiagram.jison | 1 - src/diagrams/state/shapes.js | 1 + src/diagrams/state/stateDb.js | 18 ++------------- src/diagrams/state/stateRenderer.js | 23 ++------------------ 4 files changed, 5 insertions(+), 38 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 8ae1bbf44..9ab2d24db 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -126,7 +126,6 @@ statement | COMPOSIT_STATE STRUCT_START document STRUCT_STOP { console.warn('Adding document for state without id ', $1); - // yy.addDocument('noId'); $$={ stmt: 'state', id: $1, type: 'default', description: '', doc: $3 } } | STATE_DESCR AS ID { $$={id: $3, type: 'default', description: $1.trim()};} diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index c7a08d7ae..4eb372eed 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -1,6 +1,7 @@ import * as d3 from 'd3'; import idCache from './id-cache.js'; import stateDb from './stateDb'; +import utils from '../../utils'; console.warn('ID cache', idCache); diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 76bae029c..266de58d4 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -11,16 +11,10 @@ const getRootDoc = () => rootDoc; const extract = doc => { const res = { states: [], relations: [] }; clear(); + doc.forEach(item => { if (item.stmt === 'state') { - // if (item.doc) { - // addState(item.id, 'composit'); - // addDocument(item.id); - // extract(item.doc); - // currentDocument = currentDocument.parent; - // } else { addState(item.id, item.type, item.doc); - // } } if (item.stmt === 'relation') { addRelation(item.state1.id, item.state2.id, item.description); @@ -81,12 +75,7 @@ export const clear = function() { export const getState = function(id) { return currentDocument.states[id]; }; -export const addDocument = id => { - console.warn(currentDocument, documents); - currentDocument.documents[id] = newDoc(); - currentDocument.documents[id].parent = currentDocument; - currentDocument = currentDocument.documents[id]; -}; + export const getStates = function() { return currentDocument.states; }; @@ -94,8 +83,6 @@ export const logDocuments = function() { console.warn('Documents = ', documents); }; export const getRelations = function() { - // const relations1 = [{ id1: 'start1', id2: 'state1' }, { id1: 'state1', id2: 'exit1' }]; - // return relations; return currentDocument.relations; }; @@ -162,7 +149,6 @@ export default { lineType, relationType, logDocuments, - addDocument, getRootDoc, setRootDoc, extract diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 83c394fe3..0f7ca8d38 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -61,7 +61,6 @@ const insertMarkers = function(elem) { export const draw = function(text, id) { parser.yy.clear(); parser.parse(text); - stateDb.logDocuments(); logger.info('Rendering diagram ' + text); // /// / Fetch the default direction, use TD if none was found @@ -124,7 +123,7 @@ const renderDoc = (doc, diagram, parentId) => { }); } - // // Default to assigning a new object as a label for each new edge. + // Default to assigning a new object as a label for each new edge. graph.setDefaultEdgeLabel(function() { return {}; }); @@ -146,15 +145,13 @@ const renderDoc = (doc, diagram, parentId) => { .append('g') .attr('id', stateDef.id) .attr('class', 'classGroup'); - node = renderDoc2(stateDef.doc, sub, stateDef.id); + node = renderDoc(stateDef.doc, sub, stateDef.id); sub = addIdAndBox(sub, stateDef); let boxBounds = sub.node().getBBox(); node.width = boxBounds.width; node.height = boxBounds.height + 10; transformationLog[stateDef.id] = { y: 35 }; - // node.x = boxBounds.y; - // node.y = boxBounds.x; } else { node = drawState(diagram, stateDef, graph); } @@ -163,21 +160,6 @@ const renderDoc = (doc, diagram, parentId) => { // metadata about the node. In this case we're going to add labels to each of // our nodes. graph.setNode(node.id, node); - // if (parentId) { - // console.warn('apa1 P>', node.id, parentId); - // // graph.setParent(node.id, parentId); - // } - // graph.setNode(node.id + 'note', nodeAppendix); - - // let parent = 'p1'; - // if (node.id === 'XState1') { - // parent = 'p2'; - // } - - // graph.setParent(node.id, parent); - // graph.setParent(node.id + 'note', parent); - - // logger.info('Org height: ' + node.height); } console.info('Count=', graph.nodeCount()); @@ -192,7 +174,6 @@ const renderDoc = (doc, diagram, parentId) => { console.warn(getGraphId(relation.id1), relation.id2, { relation: relation }); - // graph.setEdge(getGraphId(relation.id1), getGraphId(relation.id2)); }); dagre.layout(graph); From 3b731282e3be4eb42fb37f1367ce89986a69cc12 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sat, 5 Oct 2019 12:15:14 +0200 Subject: [PATCH 06/12] #945Renabling support for descriptions --- .../integration/rendering/stateDiagram.spec.js | 15 +++++++++++++++ src/diagrams/state/parser/stateDiagram.jison | 2 +- src/diagrams/state/stateDb.js | 9 +++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index f1fc7cea8..f71a725cc 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -13,6 +13,21 @@ describe('State diagram', () => { ); 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( ` diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 9ab2d24db..f1ed4786b 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -109,7 +109,7 @@ line ; statement - : idStatement DESCR { $$={ stmt: 'state', id: $1, type: 'default', description: $2.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);*/ diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 266de58d4..96c7f5544 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -14,7 +14,7 @@ const extract = doc => { doc.forEach(item => { if (item.stmt === 'state') { - addState(item.id, item.type, item.doc); + addState(item.id, item.type, item.doc, item.description); } if (item.stmt === 'relation') { addRelation(item.state1.id, item.state2.id, item.description); @@ -46,7 +46,7 @@ let endCnt = 0; * @param type * @param style */ -export const addState = function(id, type, doc) { +export const addState = function(id, type, doc, descr) { console.warn('Add state', id); if (typeof currentDocument.states[id] === 'undefined') { currentDocument.states[id] = { @@ -63,6 +63,7 @@ export const addState = function(id, type, doc) { currentDocument.states[id].type = type; } } + if (descr) addDescription(id, descr.trim()); }; export const clear = function() { @@ -107,7 +108,7 @@ export const addRelation = function(_id1, _id2, title) { currentDocument.relations.push({ id1, id2, title }); }; -export const addDescription = function(id, _descr) { +const addDescription = function(id, _descr) { const theState = currentDocument.states[id]; let descr = _descr; if (descr[0] === ':') { @@ -144,7 +145,7 @@ export default { getStates, getRelations, addRelation, - addDescription, + // addDescription, cleanupLabel, lineType, relationType, From 1cb52a602ad3c6393db22854a72163a2ccdabcaf Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 10:52:37 +0200 Subject: [PATCH 07/12] #945 Support for notes --- .../rendering/stateDiagram.spec.js | 43 +++++++++++ src/diagrams/state/parser/stateDiagram.jison | 6 +- src/diagrams/state/shapes.js | 75 ++++++++++++++++++- src/diagrams/state/stateDb.js | 8 +- src/diagrams/state/stateRenderer.js | 59 ++++++++++++--- 5 files changed, 174 insertions(+), 17 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index f71a725cc..777359bd7 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -13,6 +13,49 @@ 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 + note left of State1 + Important information! You can write + notes. + 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 + `, + { logLevel: 0 } + ); + cy.get('svg'); + }); it('should render a states with descriptions including multi-line descriptions', () => { imgSnapshotTest( ` diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index f1ed4786b..4a5c9c963 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -66,7 +66,7 @@ [^\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';} +\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'; } @@ -138,6 +138,10 @@ statement | JOIN | CONCURRENT | 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 ; diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index 4eb372eed..359404c73 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -9,7 +9,8 @@ console.warn('ID cache', idCache); const conf = { dividerMargin: 10, padding: 5, - textHeight: 10 + textHeight: 10, + noteMargin: 10 }; /** @@ -168,6 +169,77 @@ const drawEndState = g => { .attr('cy', conf.padding + 7); }; +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'); + const textBounds = span.node().getBBox(); + textHeight += textBounds.height; + span.attr('x', x + conf.noteMargin); + span.attr('y', y + textHeight + 1.75 * conf.noteMargin); + span.text(txt); + + textWidth = Math.max(textBounds.width, textWidth); + } + } + return { textWidth, 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. @@ -192,6 +264,7 @@ export const drawState = function(elem, stateDef, graph, doc) { if (stateDef.type === 'start') drawStartState(g); if (stateDef.type === 'end') drawEndState(g); + if (stateDef.type === 'note') drawNote(stateDef.note.text, g); if (stateDef.type === 'default' && stateDef.descriptions.length === 0) drawSimpleState(g, stateDef); if (stateDef.type === 'default' && stateDef.descriptions.length > 0) drawDescrState(g, stateDef); diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 96c7f5544..9a5ed487b 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -14,7 +14,7 @@ const extract = doc => { doc.forEach(item => { if (item.stmt === 'state') { - addState(item.id, item.type, item.doc, item.description); + addState(item.id, item.type, item.doc, item.description, item.note); } if (item.stmt === 'relation') { addRelation(item.state1.id, item.state2.id, item.description); @@ -46,14 +46,15 @@ let endCnt = 0; * @param type * @param style */ -export const addState = function(id, type, doc, descr) { +export const addState = function(id, type, doc, descr, note) { console.warn('Add state', id); if (typeof currentDocument.states[id] === 'undefined') { currentDocument.states[id] = { id: id, descriptions: [], type, - doc + doc, + note }; } else { if (!currentDocument.states[id].doc) { @@ -64,6 +65,7 @@ export const addState = function(id, type, doc, descr) { } } if (descr) addDescription(id, descr.trim()); + if (note) currentDocument.states[id].note = note; }; export const clear = function() { diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 0f7ca8d38..8812dee32 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -6,7 +6,7 @@ import stateDb from './stateDb'; import { parser } from './parser/stateDiagram'; import utils from '../../utils'; import idCache from './id-cache'; -import { drawState, addIdAndBox, drawEdge } from './shapes'; +import { drawState, addIdAndBox, drawEdge, drawNote } from './shapes'; parser.yy = stateDb; @@ -70,7 +70,7 @@ export const draw = function(text, id) { // // Layout graph, Create a new directed graph const graph = new graphlib.Graph({ multigraph: false, - // compound: true, + compound: true, // acyclicer: 'greedy', rankdir: 'RL' }); @@ -93,7 +93,7 @@ export const draw = function(text, id) { diagram.attr('height', '100%'); diagram.attr('width', '100%'); - diagram.attr('viewBox', '0 0 ' + bounds.width + ' ' + (bounds.height + 50)); + diagram.attr('viewBox', '0 0 ' + bounds.width * 2 + ' ' + (bounds.height + 50)); }; const getLabelWidth = text => { return text ? text.length * 5.02 : 1; @@ -101,15 +101,17 @@ const getLabelWidth = text => { const renderDoc = (doc, diagram, parentId) => { // // Layout graph, Create a new directed graph - const graph = new graphlib.Graph({}); + const graph = new graphlib.Graph({ + compound: true + }); // Set an object for the graph label if (parentId) graph.setGraph({ rankdir: 'LR', multigraph: false, - compound: false, - // acyclicer: 'greedy', + compound: true, + acyclicer: 'greedy', rankdir: 'LR', ranker: 'tight-tree' // isMultiGraph: false @@ -117,8 +119,12 @@ const renderDoc = (doc, diagram, parentId) => { else { graph.setGraph({ rankdir: 'TB', - // acyclicer: 'greedy' - ranker: 'longest-path' + compound: true, + // isCompound: true, + // acyclicer: 'greedy', + // ranker: 'longest-path' + ranker: 'tight-tree' + // ranker: 'network-simplex' // isMultiGraph: false }); } @@ -139,6 +145,7 @@ const renderDoc = (doc, diagram, parentId) => { for (let i = 0; i < keys.length; i++) { const stateDef = states[keys[i]]; console.warn('keys[i]', keys[i]); + let node; if (stateDef.doc) { let sub = diagram @@ -156,10 +163,34 @@ const renderDoc = (doc, diagram, parentId) => { node = drawState(diagram, stateDef, graph); } - // 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); + if (stateDef.note) { + // Draw note note + console.warn('Def=', stateDef); + 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); + } } console.info('Count=', graph.nodeCount()); @@ -178,6 +209,8 @@ const renderDoc = (doc, diagram, parentId) => { dagre.layout(graph); + console.warn('Graph after layout', graph.nodes()); + graph.nodes().forEach(function(v) { if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { console.warn('Node ' + v + ': ' + JSON.stringify(graph.node(v))); @@ -191,6 +224,8 @@ const renderDoc = (doc, diagram, parentId) => { graph.node(v).height / 2) + ' )' ); + } else { + console.warn('No Node ' + v + ': ' + JSON.stringify(graph.node(v))); } }); let stateBox = diagram.node().getBBox(); From 4f1186a610d94e4be816cfc8fe9e3a067e6c98b3 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 11:35:46 +0200 Subject: [PATCH 08/12] #945 Support for notes, better width and handling of +/- --- .../integration/rendering/stateDiagram.spec.js | 11 ++++++----- src/diagrams/state/parser/stateDiagram.jison | 7 ++++--- src/diagrams/state/shapes.js | 12 ++++++------ src/diagrams/state/stateDiagram.spec.js | 15 ++++++++++++--- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 777359bd7..55c131da2 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -31,10 +31,10 @@ describe('State diagram', () => { imgSnapshotTest( ` stateDiagram - State1: The state with a note + State1: The state with a note with minus - and plus + in it note left of State1 Important information! You can write - notes. + notes with . and in them. end note `, { logLevel: 0 } @@ -45,12 +45,13 @@ describe('State diagram', () => { imgSnapshotTest( ` stateDiagram - State1: The state with a note + State1: The state with a note +,- note right of State1 - Important information! You can write + Important information! You can write +,- notes. end note - State1 --> State2 + State1 --> State2 : With +,- + note left of State2 : This is the note +,-
`, { logLevel: 0 } ); diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 4a5c9c963..b05eb8c9c 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -65,14 +65,15 @@ [^"]* { 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);yytext = yytext.slice(0,-8).trim();return 'NOTE_TEXT';} +\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'; } +// \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'; diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index 359404c73..8da33e594 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -205,16 +205,15 @@ const _drawLongText = (_text, x, y, g) => { 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.75 * conf.noteMargin); - span.text(txt); - - textWidth = Math.max(textBounds.width, textWidth); + span.attr('y', y + textHeight + 1.25* conf.noteMargin); + // textWidth = Math.max(textBounds.width, textWidth); } } - return { textWidth, textHeight }; + return { textWidth: textElem.node().getBBox().width, textHeight }; }; /** @@ -226,6 +225,7 @@ const _drawLongText = (_text, x, y, g) => { export const drawNote = (text, g) => { g.attr('class', 'note'); + console.warn('Text of note', text); const note = g .append('rect') .attr('x', 0) @@ -233,7 +233,7 @@ export const drawNote = (text, g) => { const rectElem = g.append('g'); const { textWidth, textHeight } = _drawLongText(text, 0, 0, rectElem); - + console.warn('Text of note', text, textWidth); note.attr('height', textHeight + 2 * conf.noteMargin); note.attr('width', textWidth + conf.noteMargin * 2); diff --git a/src/diagrams/state/stateDiagram.spec.js b/src/diagrams/state/stateDiagram.spec.js index d3e1bf93c..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 @@ -58,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 @@ -71,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 --> [*] `; From dce09586cd61547b7c30887e24531fe468aed0c6 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 14:11:17 +0200 Subject: [PATCH 09/12] #945 Support for forks and joins --- .../integration/rendering/stateDiagram.spec.js | 18 ++++++++++++++++++ src/diagrams/state/parser/stateDiagram.jison | 14 ++++++++++---- src/diagrams/state/shapes.js | 13 ++++++++++++- src/diagrams/state/stateRenderer.js | 2 +- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 55c131da2..c7f214be3 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -161,6 +161,24 @@ describe('State diagram', () => { `, { 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'); }); }); diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index b05eb8c9c..e79c8c4f1 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -43,8 +43,10 @@ \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";} @@ -135,8 +137,12 @@ statement //console.warn('Adding document for state with id ', $3, $4); yy.addDocument($3); $$={ stmt: 'state', id: $3, type: 'default', description: $1, doc: $5 } } - | FORK - | JOIN + | FORK { + $$={ stmt: 'state', id: $1, type: 'fork' } + } + | JOIN { + $$={ stmt: 'state', id: $1, type: 'join' } + } | CONCURRENT | note notePosition ID NOTE_TEXT { diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index 8da33e594..f1d19ac18 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -168,6 +168,16 @@ const drawEndState = g => { .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 @@ -209,7 +219,7 @@ const _drawLongText = (_text, x, y, g) => { const textBounds = span.node().getBBox(); textHeight += textBounds.height; span.attr('x', x + conf.noteMargin); - span.attr('y', y + textHeight + 1.25* conf.noteMargin); + span.attr('y', y + textHeight + 1.25 * conf.noteMargin); // textWidth = Math.max(textBounds.width, textWidth); } } @@ -264,6 +274,7 @@ export const drawState = function(elem, stateDef, graph, doc) { 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 === 'default' && stateDef.descriptions.length === 0) drawSimpleState(g, stateDef); diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 8812dee32..34a28cf4b 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -61,7 +61,7 @@ const insertMarkers = function(elem) { 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}']`); From ce0b0fa0c8e654ce833df52676f1fe157b9d2de4 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 15:44:31 +0200 Subject: [PATCH 10/12] #945 Divider lines for concurrency --- .../rendering/stateDiagram.spec.js | 24 +++++++++++++++++++ src/diagrams/state/parser/stateDiagram.jison | 4 +++- src/diagrams/state/shapes.js | 15 ++++++++++++ src/diagrams/state/stateDb.js | 7 ++++++ src/diagrams/state/stateRenderer.js | 18 ++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index c7f214be3..bf2055008 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -181,4 +181,28 @@ describe('State diagram', () => { ); 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/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index e79c8c4f1..000f26b4b 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -143,7 +143,9 @@ statement | JOIN { $$={ stmt: 'state', id: $1, type: 'join' } } - | CONCURRENT + | 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); diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index f1d19ac18..dc3754de8 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -25,6 +25,20 @@ export const drawStartState = g => .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', 20) + .attr('y2', 20); + /** * Draws a an end state as a black circle */ @@ -276,6 +290,7 @@ export const drawState = function(elem, stateDef, graph, doc) { 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); diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index 9a5ed487b..c2749087a 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -133,6 +133,12 @@ export const lineType = { DOTTED_LINE: 1 }; +let dividerCnt = 0; +const getDividerId = () => { + dividerCnt++; + return 'divider-id-' + dividerCnt; +}; + export const relationType = { AGGREGATION: 0, EXTENSION: 1, @@ -147,6 +153,7 @@ export default { getStates, getRelations, addRelation, + getDividerId, // addDescription, cleanupLabel, lineType, diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 34a28cf4b..23fa0c778 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -224,10 +224,28 @@ const renderDoc = (doc, diagram, parentId) => { 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 { console.warn('No Node ' + v + ': ' + JSON.stringify(graph.node(v))); } }); + let stateBox = diagram.node().getBBox(); console.warn('Node before labels ', stateBox.width); From b12791d3e0c65aa0a1faecce39ebcd8262fd551a Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 15:53:34 +0200 Subject: [PATCH 11/12] #945 Divider lines for concurrency, full width when in a composit state --- src/diagrams/state/shapes.js | 4 ++-- src/diagrams/state/stateRenderer.js | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index dc3754de8..718deb2fa 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -36,8 +36,8 @@ export const drawDivider = g => .attr('x1', 10) .attr('class', 'divider') .attr('x2', 20) - .attr('y1', 20) - .attr('y2', 20); + .attr('y1', 0) + .attr('y2', 0); /** * Draws a an end state as a black circle diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 23fa0c778..8276c36ca 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -109,9 +109,9 @@ const renderDoc = (doc, diagram, parentId) => { if (parentId) graph.setGraph({ rankdir: 'LR', - multigraph: false, + // multigraph: false, compound: true, - acyclicer: 'greedy', + // acyclicer: 'greedy', rankdir: 'LR', ranker: 'tight-tree' // isMultiGraph: false @@ -142,6 +142,7 @@ const renderDoc = (doc, diagram, parentId) => { console.warn('rendering doc 2', states, relations); total = keys.length; + let first = true; for (let i = 0; i < keys.length; i++) { const stateDef = states[keys[i]]; console.warn('keys[i]', keys[i]); @@ -154,11 +155,22 @@ const renderDoc = (doc, diagram, parentId) => { .attr('class', 'classGroup'); node = renderDoc(stateDef.doc, sub, stateDef.id); - sub = addIdAndBox(sub, stateDef); - let boxBounds = sub.node().getBBox(); - node.width = boxBounds.width; - node.height = boxBounds.height + 10; - transformationLog[stateDef.id] = { y: 35 }; + 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 }; + console.warn('Here2'); + } else { + console.warn('Here'); + // 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); } From ebede9b9107ab4bc5457df21614a995e744f080e Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 16:06:15 +0200 Subject: [PATCH 12/12] #945 Log removal --- src/diagrams/state/parser/stateDiagram.jison | 44 ++++++++++---------- src/diagrams/state/shapes.js | 6 --- src/diagrams/state/stateDb.js | 6 +-- src/diagrams/state/stateRenderer.js | 24 ++++------- 4 files changed, 31 insertions(+), 49 deletions(-) diff --git a/src/diagrams/state/parser/stateDiagram.jison b/src/diagrams/state/parser/stateDiagram.jison index 000f26b4b..cbb2aea91 100644 --- a/src/diagrams/state/parser/stateDiagram.jison +++ b/src/diagrams/state/parser/stateDiagram.jison @@ -38,7 +38,7 @@ \#[^\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();} @@ -49,33 +49,33 @@ .*"[[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);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';} +[^"]* { /*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'; } -\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'; @@ -92,7 +92,7 @@ start : SPACE start | NL start - | SD document { console.warn('Root document', $2); yy.setRootDoc($2);return $2; } + | SD document { /*console.warn('Root document', $2);*/ yy.setRootDoc($2);return $2; } ; document @@ -112,7 +112,7 @@ line ; statement - : idStatement DESCR { console.warn('got id and descr', $1, $2.trim());$$={ stmt: 'state', id: $1, type: 'default', description: $2.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);*/ @@ -128,7 +128,7 @@ statement | COMPOSIT_STATE | COMPOSIT_STATE STRUCT_START document STRUCT_STOP { - console.warn('Adding document for state without id ', $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()};} @@ -148,7 +148,7 @@ statement } | note notePosition ID NOTE_TEXT { - console.warn('got NOTE, position: ', $2.trim(), 'id = ', $3.trim(), 'note: ', $4); + /*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 diff --git a/src/diagrams/state/shapes.js b/src/diagrams/state/shapes.js index 718deb2fa..92ffc6c11 100644 --- a/src/diagrams/state/shapes.js +++ b/src/diagrams/state/shapes.js @@ -3,8 +3,6 @@ import idCache from './id-cache.js'; import stateDb from './stateDb'; import utils from '../../utils'; -console.warn('ID cache', idCache); - // TODO Move conf object to main conf in mermaidAPI const conf = { dividerMargin: 10, @@ -249,7 +247,6 @@ const _drawLongText = (_text, x, y, g) => { export const drawNote = (text, g) => { g.attr('class', 'note'); - console.warn('Text of note', text); const note = g .append('rect') .attr('x', 0) @@ -257,7 +254,6 @@ export const drawNote = (text, g) => { const rectElem = g.append('g'); const { textWidth, textHeight } = _drawLongText(text, 0, 0, rectElem); - console.warn('Text of note', text, textWidth); note.attr('height', textHeight + 2 * conf.noteMargin); note.attr('width', textWidth + conf.noteMargin * 2); @@ -271,8 +267,6 @@ export const drawNote = (text, g) => { * @param {*} stateDef */ export const drawState = function(elem, stateDef, graph, doc) { - console.warn('Rendering class ', stateDef); - const id = stateDef.id; const stateInfo = { id: id, diff --git a/src/diagrams/state/stateDb.js b/src/diagrams/state/stateDb.js index c2749087a..b84d66fad 100644 --- a/src/diagrams/state/stateDb.js +++ b/src/diagrams/state/stateDb.js @@ -2,7 +2,7 @@ import { logger } from '../../logger'; let rootDoc = []; const setRootDoc = o => { - console.warn('Setting root doc', o); + logger.info('Setting root doc', o); rootDoc = o; }; @@ -47,7 +47,6 @@ let endCnt = 0; * @param style */ export const addState = function(id, type, doc, descr, note) { - console.warn('Add state', id); if (typeof currentDocument.states[id] === 'undefined') { currentDocument.states[id] = { id: id, @@ -83,7 +82,7 @@ export const getStates = function() { return currentDocument.states; }; export const logDocuments = function() { - console.warn('Documents = ', documents); + logger.info('Documents = ', documents); }; export const getRelations = function() { return currentDocument.relations; @@ -104,7 +103,6 @@ export const addRelation = function(_id1, _id2, title) { id2 = 'end' + startCnt; type2 = 'end'; } - console.log(id1, id2, title); addState(id1, type1); addState(id2, type2); currentDocument.relations.push({ id1, id2, title }); diff --git a/src/diagrams/state/stateRenderer.js b/src/diagrams/state/stateRenderer.js index 8276c36ca..467393224 100644 --- a/src/diagrams/state/stateRenderer.js +++ b/src/diagrams/state/stateRenderer.js @@ -113,7 +113,8 @@ const renderDoc = (doc, diagram, parentId) => { compound: true, // acyclicer: 'greedy', rankdir: 'LR', - ranker: 'tight-tree' + ranker: 'tight-tree', + ranksep: '20' // isMultiGraph: false }); else { @@ -139,13 +140,11 @@ const renderDoc = (doc, diagram, parentId) => { const relations = stateDb.getRelations(); const keys = Object.keys(states); - console.warn('rendering doc 2', states, relations); total = keys.length; let first = true; for (let i = 0; i < keys.length; i++) { const stateDef = states[keys[i]]; - console.warn('keys[i]', keys[i]); let node; if (stateDef.doc) { @@ -162,9 +161,7 @@ const renderDoc = (doc, diagram, parentId) => { node.width = boxBounds.width; node.height = boxBounds.height + 10; transformationLog[stateDef.id] = { y: 35 }; - console.warn('Here2'); } else { - console.warn('Here'); // sub = addIdAndBox(sub, stateDef); let boxBounds = sub.node().getBBox(); node.width = boxBounds.width; @@ -177,7 +174,6 @@ const renderDoc = (doc, diagram, parentId) => { if (stateDef.note) { // Draw note note - console.warn('Def=', stateDef); const noteDef = { descriptions: [], id: stateDef.id + '-note', @@ -205,27 +201,23 @@ const renderDoc = (doc, diagram, parentId) => { } } - console.info('Count=', graph.nodeCount()); + logger.info('Count=', graph.nodeCount()); relations.forEach(function(relation) { - console.warn('Rendering edge', relation); graph.setEdge(relation.id1, relation.id2, { relation: relation, width: getLabelWidth(relation.title), height: 16, labelpos: 'c' }); - console.warn(getGraphId(relation.id1), relation.id2, { - relation: relation - }); }); dagre.layout(graph); - console.warn('Graph after layout', graph.nodes()); + logger.debug('Graph after layout', graph.nodes()); graph.nodes().forEach(function(v) { if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { - console.warn('Node ' + v + ': ' + JSON.stringify(graph.node(v))); + logger.debug('Node ' + v + ': ' + JSON.stringify(graph.node(v))); d3.select('#' + v).attr( 'transform', 'translate(' + @@ -254,12 +246,11 @@ const renderDoc = (doc, diagram, parentId) => { divider.setAttribute('x2', pWidth - pShift); }); } else { - console.warn('No Node ' + v + ': ' + JSON.stringify(graph.node(v))); + logger.debug('No Node ' + v + ': ' + JSON.stringify(graph.node(v))); } }); let stateBox = diagram.node().getBBox(); - console.warn('Node before labels ', stateBox.width); graph.edges().forEach(function(e) { if (typeof e !== 'undefined' && typeof graph.edge(e) !== 'undefined') { @@ -269,7 +260,6 @@ const renderDoc = (doc, diagram, parentId) => { }); stateBox = diagram.node().getBBox(); - console.warn('Node after labels ', stateBox.width); const stateInfo = { id: parentId ? parentId : 'root', label: parentId ? parentId : 'root', @@ -280,7 +270,7 @@ const renderDoc = (doc, diagram, parentId) => { stateInfo.width = stateBox.width + 2 * conf.padding; stateInfo.height = stateBox.height + 2 * conf.padding; - console.warn('Doc rendered', stateInfo, graph); + logger.info('Doc rendered', stateInfo, graph); return stateInfo; };