From 1cb52a602ad3c6393db22854a72163a2ccdabcaf Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 6 Oct 2019 10:52:37 +0200 Subject: [PATCH] #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();