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,